Compatibility fixes for Python 3.3
1) Added a py33 environment to tox 2) Updated tests to mock the correctly named builtins.open based on python version 3) Other minor compatibility fixes Tests for Python 3.3 will not pass due to this bug: https://github.com/eventlet/eventlet/issues/83 among possibly others in eventlet. Change-Id: Ie4b512a926fa690ee77a71a89851c871ea1f6be0
This commit is contained in:
parent
cae81837ce
commit
c121bef9f0
@ -19,6 +19,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
import six
|
||||||
from stevedore import extension
|
from stevedore import extension
|
||||||
from wsgiref import simple_server
|
from wsgiref import simple_server
|
||||||
|
|
||||||
@ -148,7 +149,7 @@ class IronicPythonAgent(object):
|
|||||||
return self.node['uuid']
|
return self.node['uuid']
|
||||||
|
|
||||||
def list_command_results(self):
|
def list_command_results(self):
|
||||||
return self.command_results.values()
|
return list(self.command_results.values())
|
||||||
|
|
||||||
def get_command_result(self, result_id):
|
def get_command_result(self, result_id):
|
||||||
try:
|
try:
|
||||||
@ -171,7 +172,7 @@ class IronicPythonAgent(object):
|
|||||||
extension_part, command_part = self._split_command(command_name)
|
extension_part, command_part = self._split_command(command_name)
|
||||||
|
|
||||||
if len(self.command_results) > 0:
|
if len(self.command_results) > 0:
|
||||||
last_command = self.command_results.values()[-1]
|
last_command = list(self.command_results.values())[-1]
|
||||||
if not last_command.is_done():
|
if not last_command.is_done():
|
||||||
raise errors.CommandExecutionError('agent is busy')
|
raise errors.CommandExecutionError('agent is busy')
|
||||||
|
|
||||||
@ -192,7 +193,7 @@ class IronicPythonAgent(object):
|
|||||||
result = base.SyncCommandResult(command_name,
|
result = base.SyncCommandResult(command_name,
|
||||||
kwargs,
|
kwargs,
|
||||||
False,
|
False,
|
||||||
unicode(e))
|
six.text_type(e))
|
||||||
|
|
||||||
self.command_results[result.id] = result
|
self.command_results[result.id] = result
|
||||||
return result
|
return result
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
from ironic_python_agent import encoding
|
from ironic_python_agent import encoding
|
||||||
from ironic_python_agent import errors
|
from ironic_python_agent import errors
|
||||||
@ -31,7 +32,7 @@ class AgentCommandStatus(object):
|
|||||||
|
|
||||||
class BaseCommandResult(encoding.Serializable):
|
class BaseCommandResult(encoding.Serializable):
|
||||||
def __init__(self, command_name, command_params):
|
def __init__(self, command_name, command_params):
|
||||||
self.id = unicode(uuid.uuid4())
|
self.id = six.text_type(uuid.uuid4())
|
||||||
self.command_name = command_name
|
self.command_name = command_name
|
||||||
self.command_params = command_params
|
self.command_params = command_params
|
||||||
self.command_status = AgentCommandStatus.RUNNING
|
self.command_status = AgentCommandStatus.RUNNING
|
||||||
|
@ -38,13 +38,13 @@ class ConfigDriveWriter(object):
|
|||||||
os.makedirs(os.path.join(location, prefix, 'content'))
|
os.makedirs(os.path.join(location, prefix, 'content'))
|
||||||
|
|
||||||
metadata = {}
|
metadata = {}
|
||||||
for k, v in self.metadata.iteritems():
|
for k, v in self.metadata.items():
|
||||||
metadata[k] = v
|
metadata[k] = v
|
||||||
|
|
||||||
if self.files:
|
if self.files:
|
||||||
metadata['files'] = []
|
metadata['files'] = []
|
||||||
filenumber = 0
|
filenumber = 0
|
||||||
for filepath, contents in self.files.iteritems():
|
for filepath, contents in self.files.items():
|
||||||
content_path = '/content/{0:04}'.format(filenumber)
|
content_path = '/content/{0:04}'.format(filenumber)
|
||||||
file_info = {
|
file_info = {
|
||||||
'content_path': content_path,
|
'content_path': content_path,
|
||||||
@ -72,10 +72,10 @@ def write_configdrive(location, metadata, files, prefix='openstack',
|
|||||||
"""
|
"""
|
||||||
writer = ConfigDriveWriter()
|
writer = ConfigDriveWriter()
|
||||||
|
|
||||||
for k, v in metadata.iteritems():
|
for k, v in metadata.items():
|
||||||
writer.add_metadata(k, v)
|
writer.add_metadata(k, v)
|
||||||
|
|
||||||
for path, b64_contents in files.iteritems():
|
for path, b64_contents in files.items():
|
||||||
contents = base64.b64decode(b64_contents)
|
contents = base64.b64decode(b64_contents)
|
||||||
writer.add_file(path, contents)
|
writer.add_file(path, contents)
|
||||||
|
|
||||||
|
@ -15,9 +15,11 @@ limitations under the License.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
import six
|
||||||
import stevedore
|
import stevedore
|
||||||
|
|
||||||
from ironic_python_agent import encoding
|
from ironic_python_agent import encoding
|
||||||
@ -169,7 +171,15 @@ def get_manager():
|
|||||||
|
|
||||||
# There will always be at least one extension available (the
|
# There will always be at least one extension available (the
|
||||||
# GenericHardwareManager).
|
# GenericHardwareManager).
|
||||||
preferred_extension = sorted(extension_manager, _compare_extensions)[0]
|
if six.PY2:
|
||||||
|
preferred_extension = sorted(
|
||||||
|
extension_manager,
|
||||||
|
_compare_extensions)[0]
|
||||||
|
else:
|
||||||
|
preferred_extension = sorted(
|
||||||
|
extension_manager,
|
||||||
|
key=functools.cmp_to_key(_compare_extensions))[0]
|
||||||
|
|
||||||
preferred_manager = preferred_extension.obj
|
preferred_manager = preferred_extension.obj
|
||||||
|
|
||||||
if preferred_manager.evaluate_hardware_support() <= 0:
|
if preferred_manager.evaluate_hardware_support() <= 0:
|
||||||
|
@ -118,7 +118,7 @@ def _download_image(image_info):
|
|||||||
|
|
||||||
def _verify_image(image_info, image_location):
|
def _verify_image(image_info, image_location):
|
||||||
hashes = image_info['hashes']
|
hashes = image_info['hashes']
|
||||||
for k, v in hashes.iteritems():
|
for k, v in hashes.items():
|
||||||
algo = getattr(hashlib, k, None)
|
algo = getattr(hashlib, k, None)
|
||||||
if algo is None:
|
if algo is None:
|
||||||
continue
|
continue
|
||||||
|
@ -20,6 +20,7 @@ import time
|
|||||||
import mock
|
import mock
|
||||||
from oslotest import base as test_base
|
from oslotest import base as test_base
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
import six
|
||||||
from stevedore import extension
|
from stevedore import extension
|
||||||
from wsgiref import simple_server
|
from wsgiref import simple_server
|
||||||
|
|
||||||
@ -32,6 +33,11 @@ from ironic_python_agent import hardware
|
|||||||
|
|
||||||
EXPECTED_ERROR = RuntimeError('command execution failed')
|
EXPECTED_ERROR = RuntimeError('command execution failed')
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
OPEN_FUNCTION_NAME = '__builtin__.open'
|
||||||
|
else:
|
||||||
|
OPEN_FUNCTION_NAME = 'builtins.open'
|
||||||
|
|
||||||
|
|
||||||
def foo_execute(*args, **kwargs):
|
def foo_execute(*args, **kwargs):
|
||||||
if kwargs['fail']:
|
if kwargs['fail']:
|
||||||
@ -248,13 +254,13 @@ class TestBaseAgent(test_base.BaseTestCase):
|
|||||||
|
|
||||||
class TestAgentCmd(test_base.BaseTestCase):
|
class TestAgentCmd(test_base.BaseTestCase):
|
||||||
@mock.patch('ironic_python_agent.openstack.common.log.getLogger')
|
@mock.patch('ironic_python_agent.openstack.common.log.getLogger')
|
||||||
@mock.patch('__builtin__.open')
|
@mock.patch(OPEN_FUNCTION_NAME)
|
||||||
def test__get_kernel_params_fail(self, logger_mock, open_mock):
|
def test__get_kernel_params_fail(self, logger_mock, open_mock):
|
||||||
open_mock.side_effect = Exception
|
open_mock.side_effect = Exception
|
||||||
params = agent_cmd._get_kernel_params()
|
params = agent_cmd._get_kernel_params()
|
||||||
self.assertEqual(params, {})
|
self.assertEqual(params, {})
|
||||||
|
|
||||||
@mock.patch('__builtin__.open')
|
@mock.patch(OPEN_FUNCTION_NAME)
|
||||||
def test__get_kernel_params(self, open_mock):
|
def test__get_kernel_params(self, open_mock):
|
||||||
kernel_line = 'api-url=http://localhost:9999 baz foo=bar\n'
|
kernel_line = 'api-url=http://localhost:9999 baz foo=bar\n'
|
||||||
open_mock.return_value.__enter__ = lambda s: s
|
open_mock.return_value.__enter__ = lambda s: s
|
||||||
|
@ -21,10 +21,16 @@ import json
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslotest import base as test_base
|
from oslotest import base as test_base
|
||||||
|
import six
|
||||||
|
|
||||||
from ironic_python_agent import configdrive
|
from ironic_python_agent import configdrive
|
||||||
from ironic_python_agent import utils
|
from ironic_python_agent import utils
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
OPEN_FUNCTION_NAME = '__builtin__.open'
|
||||||
|
else:
|
||||||
|
OPEN_FUNCTION_NAME = 'builtins.open'
|
||||||
|
|
||||||
|
|
||||||
class ConfigDriveWriterTestCase(test_base.BaseTestCase):
|
class ConfigDriveWriterTestCase(test_base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -43,12 +49,12 @@ class ConfigDriveWriterTestCase(test_base.BaseTestCase):
|
|||||||
self.assertEqual(files, {'/etc/filename': 'contents'})
|
self.assertEqual(files, {'/etc/filename': 'contents'})
|
||||||
|
|
||||||
@mock.patch('os.makedirs', autospec=True)
|
@mock.patch('os.makedirs', autospec=True)
|
||||||
@mock.patch('__builtin__.open', autospec=True)
|
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
|
||||||
def test_write_no_files(self, open_mock, makedirs_mock):
|
def test_write_no_files(self, open_mock, makedirs_mock):
|
||||||
metadata = {'admin_pass': 'password', 'hostname': 'test'}
|
metadata = {'admin_pass': 'password', 'hostname': 'test'}
|
||||||
json_metadata = json.dumps(metadata)
|
json_metadata = json.dumps(metadata)
|
||||||
metadata_path = '/lol/ironic/latest/meta_data.json'
|
metadata_path = '/lol/ironic/latest/meta_data.json'
|
||||||
for k, v in metadata.iteritems():
|
for k, v in metadata.items():
|
||||||
self.writer.add_metadata(k, v)
|
self.writer.add_metadata(k, v)
|
||||||
|
|
||||||
open_mock.return_value.__enter__ = lambda s: s
|
open_mock.return_value.__enter__ = lambda s: s
|
||||||
@ -65,16 +71,16 @@ class ConfigDriveWriterTestCase(test_base.BaseTestCase):
|
|||||||
self.assertEqual(makedirs_calls, makedirs_mock.call_args_list)
|
self.assertEqual(makedirs_calls, makedirs_mock.call_args_list)
|
||||||
|
|
||||||
@mock.patch('os.makedirs', autospec=True)
|
@mock.patch('os.makedirs', autospec=True)
|
||||||
@mock.patch('__builtin__.open', autospec=True)
|
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
|
||||||
def test_write_with_files(self, open_mock, makedirs_mock):
|
def test_write_with_files(self, open_mock, makedirs_mock):
|
||||||
metadata = {'admin_pass': 'password', 'hostname': 'test'}
|
metadata = {'admin_pass': 'password', 'hostname': 'test'}
|
||||||
for k, v in metadata.iteritems():
|
for k, v in metadata.items():
|
||||||
self.writer.add_metadata(k, v)
|
self.writer.add_metadata(k, v)
|
||||||
files = utils.get_ordereddict([
|
files = utils.get_ordereddict([
|
||||||
('/etc/conf0', 'contents0'),
|
('/etc/conf0', 'contents0'),
|
||||||
('/etc/conf1', 'contents1'),
|
('/etc/conf1', 'contents1'),
|
||||||
])
|
])
|
||||||
for path, contents in files.iteritems():
|
for path, contents in files.items():
|
||||||
self.writer.add_file(path, contents)
|
self.writer.add_file(path, contents)
|
||||||
|
|
||||||
open_mock.return_value.__enter__ = lambda s: s
|
open_mock.return_value.__enter__ = lambda s: s
|
||||||
@ -117,12 +123,12 @@ class ConfigDriveWriterTestCase(test_base.BaseTestCase):
|
|||||||
self.assertEqual(makedirs_calls, makedirs_mock.call_args_list)
|
self.assertEqual(makedirs_calls, makedirs_mock.call_args_list)
|
||||||
|
|
||||||
@mock.patch('os.makedirs', autospec=True)
|
@mock.patch('os.makedirs', autospec=True)
|
||||||
@mock.patch('__builtin__.open', autospec=True)
|
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
|
||||||
def test_write_configdrive(self, open_mock, makedirs_mock):
|
def test_write_configdrive(self, open_mock, makedirs_mock):
|
||||||
metadata = {'admin_pass': 'password', 'hostname': 'test'}
|
metadata = {'admin_pass': 'password', 'hostname': 'test'}
|
||||||
files = utils.get_ordereddict([
|
files = utils.get_ordereddict([
|
||||||
('/etc/conf0', base64.b64encode('contents0')),
|
('/etc/conf0', base64.b64encode(b'contents0')),
|
||||||
('/etc/conf1', base64.b64encode('contents1')),
|
('/etc/conf1', base64.b64encode(b'contents1')),
|
||||||
])
|
])
|
||||||
metadata['files'] = [
|
metadata['files'] = [
|
||||||
{'content_path': '/content/0000', 'path': '/etc/conf0'},
|
{'content_path': '/content/0000', 'path': '/etc/conf0'},
|
||||||
@ -148,10 +154,10 @@ class ConfigDriveWriterTestCase(test_base.BaseTestCase):
|
|||||||
|
|
||||||
open_calls = [
|
open_calls = [
|
||||||
mock.call('/lol/openstack/content/0000', 'wb'),
|
mock.call('/lol/openstack/content/0000', 'wb'),
|
||||||
mock.call().write('contents0'),
|
mock.call().write(b'contents0'),
|
||||||
mock.call().__exit__(None, None, None),
|
mock.call().__exit__(None, None, None),
|
||||||
mock.call('/lol/openstack/content/0001', 'wb'),
|
mock.call('/lol/openstack/content/0001', 'wb'),
|
||||||
mock.call().write('contents1'),
|
mock.call().write(b'contents1'),
|
||||||
mock.call().__exit__(None, None, None),
|
mock.call().__exit__(None, None, None),
|
||||||
mock.call('/lol/openstack/latest/meta_data.json', 'wb'),
|
mock.call('/lol/openstack/latest/meta_data.json', 'wb'),
|
||||||
# already checked
|
# already checked
|
||||||
|
@ -16,9 +16,15 @@ limitations under the License.
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslotest import base as test_base
|
from oslotest import base as test_base
|
||||||
|
import six
|
||||||
|
|
||||||
from ironic_python_agent import hardware
|
from ironic_python_agent import hardware
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
OPEN_FUNCTION_NAME = '__builtin__.open'
|
||||||
|
else:
|
||||||
|
OPEN_FUNCTION_NAME = 'builtins.open'
|
||||||
|
|
||||||
|
|
||||||
class TestGenericHardwareManager(test_base.BaseTestCase):
|
class TestGenericHardwareManager(test_base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -27,7 +33,7 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
|
|||||||
|
|
||||||
@mock.patch('os.listdir')
|
@mock.patch('os.listdir')
|
||||||
@mock.patch('os.path.exists')
|
@mock.patch('os.path.exists')
|
||||||
@mock.patch('__builtin__.open')
|
@mock.patch(OPEN_FUNCTION_NAME)
|
||||||
def test_list_network_interfaces(self,
|
def test_list_network_interfaces(self,
|
||||||
mocked_open,
|
mocked_open,
|
||||||
mocked_exists,
|
mocked_exists,
|
||||||
|
@ -16,10 +16,16 @@ limitations under the License.
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslotest import base as test_base
|
from oslotest import base as test_base
|
||||||
|
import six
|
||||||
|
|
||||||
from ironic_python_agent import errors
|
from ironic_python_agent import errors
|
||||||
from ironic_python_agent import standby
|
from ironic_python_agent import standby
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
OPEN_FUNCTION_NAME = '__builtin__.open'
|
||||||
|
else:
|
||||||
|
OPEN_FUNCTION_NAME = 'builtins.open'
|
||||||
|
|
||||||
|
|
||||||
class TestStandbyExtension(test_base.BaseTestCase):
|
class TestStandbyExtension(test_base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -101,7 +107,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
location = standby._image_location(image_info)
|
location = standby._image_location(image_info)
|
||||||
self.assertEqual(location, '/tmp/fake_id')
|
self.assertEqual(location, '/tmp/fake_id')
|
||||||
|
|
||||||
@mock.patch('__builtin__.open', autospec=True)
|
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
|
||||||
@mock.patch('subprocess.call', autospec=True)
|
@mock.patch('subprocess.call', autospec=True)
|
||||||
def test_write_image(self, call_mock, open_mock):
|
def test_write_image(self, call_mock, open_mock):
|
||||||
image_info = self._build_fake_image_info()
|
image_info = self._build_fake_image_info()
|
||||||
@ -124,7 +130,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
|
|
||||||
call_mock.assert_called_once_with(command)
|
call_mock.assert_called_once_with(command)
|
||||||
|
|
||||||
@mock.patch('__builtin__.open', autospec=True)
|
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
|
||||||
@mock.patch('subprocess.call', autospec=True)
|
@mock.patch('subprocess.call', autospec=True)
|
||||||
def test_copy_configdrive_to_disk(self, call_mock, open_mock):
|
def test_copy_configdrive_to_disk(self, call_mock, open_mock):
|
||||||
device = '/dev/sda'
|
device = '/dev/sda'
|
||||||
@ -147,7 +153,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
call_mock.assert_called_once_with(command)
|
call_mock.assert_called_once_with(command)
|
||||||
|
|
||||||
@mock.patch('hashlib.md5', autospec=True)
|
@mock.patch('hashlib.md5', autospec=True)
|
||||||
@mock.patch('__builtin__.open', autospec=True)
|
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
|
||||||
@mock.patch('requests.get', autospec=True)
|
@mock.patch('requests.get', autospec=True)
|
||||||
def test_download_image(self, requests_mock, open_mock, md5_mock):
|
def test_download_image(self, requests_mock, open_mock, md5_mock):
|
||||||
image_info = self._build_fake_image_info()
|
image_info = self._build_fake_image_info()
|
||||||
@ -159,7 +165,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
read_mock = open_mock.return_value.read
|
read_mock = open_mock.return_value.read
|
||||||
read_mock.return_value = 'content'
|
read_mock.return_value = 'content'
|
||||||
hexdigest_mock = md5_mock.return_value.hexdigest
|
hexdigest_mock = md5_mock.return_value.hexdigest
|
||||||
hexdigest_mock.return_value = image_info['hashes'].values()[0]
|
hexdigest_mock.return_value = list(image_info['hashes'].values())[0]
|
||||||
|
|
||||||
standby._download_image(image_info)
|
standby._download_image(image_info)
|
||||||
requests_mock.assert_called_once_with(image_info['urls'][0],
|
requests_mock.assert_called_once_with(image_info['urls'][0],
|
||||||
@ -179,7 +185,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
image_info)
|
image_info)
|
||||||
|
|
||||||
@mock.patch('ironic_python_agent.standby._verify_image', autospec=True)
|
@mock.patch('ironic_python_agent.standby._verify_image', autospec=True)
|
||||||
@mock.patch('__builtin__.open', autospec=True)
|
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
|
||||||
@mock.patch('requests.get', autospec=True)
|
@mock.patch('requests.get', autospec=True)
|
||||||
def test_download_image_verify_fails(self, requests_mock, open_mock,
|
def test_download_image_verify_fails(self, requests_mock, open_mock,
|
||||||
verify_mock):
|
verify_mock):
|
||||||
@ -191,7 +197,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
standby._download_image,
|
standby._download_image,
|
||||||
image_info)
|
image_info)
|
||||||
|
|
||||||
@mock.patch('__builtin__.open', autospec=True)
|
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
|
||||||
@mock.patch('hashlib.sha1', autospec=True)
|
@mock.patch('hashlib.sha1', autospec=True)
|
||||||
@mock.patch('hashlib.md5', autospec=True)
|
@mock.patch('hashlib.md5', autospec=True)
|
||||||
def test_verify_image_success(self, md5_mock, sha1_mock, open_mock):
|
def test_verify_image_success(self, md5_mock, sha1_mock, open_mock):
|
||||||
@ -208,7 +214,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
|
|||||||
# make sure we only check one hash, even though both are valid
|
# make sure we only check one hash, even though both are valid
|
||||||
self.assertEqual(md5_mock.call_count + sha1_mock.call_count, 1)
|
self.assertEqual(md5_mock.call_count + sha1_mock.call_count, 1)
|
||||||
|
|
||||||
@mock.patch('__builtin__.open', autospec=True)
|
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
|
||||||
@mock.patch('hashlib.md5', autospec=True)
|
@mock.patch('hashlib.md5', autospec=True)
|
||||||
def test_verify_image_failure(self, md5_mock, open_mock):
|
def test_verify_image_failure(self, md5_mock, open_mock):
|
||||||
image_info = self._build_fake_image_info()
|
image_info = self._build_fake_image_info()
|
||||||
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import ordereddict
|
|
||||||
|
|
||||||
from ironic_python_agent.openstack.common import gettextutils as gtu
|
from ironic_python_agent.openstack.common import gettextutils as gtu
|
||||||
from ironic_python_agent.openstack.common import log as logging
|
from ironic_python_agent.openstack.common import log as logging
|
||||||
@ -29,6 +28,7 @@ def get_ordereddict(*args, **kwargs):
|
|||||||
try:
|
try:
|
||||||
return collections.OrderedDict(*args, **kwargs)
|
return collections.OrderedDict(*args, **kwargs)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
import ordereddict
|
||||||
return ordereddict.OrderedDict(*args, **kwargs)
|
return ordereddict.OrderedDict(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,4 +8,4 @@ eventlet>=0.13.0
|
|||||||
oslo.config>=1.2.0
|
oslo.config>=1.2.0
|
||||||
Babel>=1.3
|
Babel>=1.3
|
||||||
iso8601>=0.1.9
|
iso8601>=0.1.9
|
||||||
oslotest==1.0
|
oslotest==1.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user