Import Ironic Driver & supporting files - part 1
Import the Ironic virt driver and supporting files (client lib wrapper and state mapping), as well as relevant unit tests, as of commit da967d77894be6f23d81fb5cc948f9d13898ba84 This is the dicing up of review/103167 into smaller chunks ready for review. Change-Id: If1f51c97212f687dd0d4d4044e9dbf7a90335e75 Co-authored-by: Adam Gandelman <adamg@ubuntu.com> Co-authored-by: Andrey Kurilin <akurilin@mirantis.com> Co-authored-by: ChangBo Guo(gcb) <eric.guo@easystack.cn> Co-authored-by: Chris Behrens <cbehrens@codestud.com> Co-authored-by: Chris Krelle <nobodycam@gmail.com> Co-authored-by: David Shrewsbury <shrewsbury.dave@gmail.com> Co-authored-by: Devananda van der Veen <devananda.vdv@gmail.com> Co-authored-by: Dmitry Tantsur <dtantsur@redhat.com> Co-authored-by: Jim Rollenhagen <jim@jimrollenhagen.com> Co-authored-by: Lucas Alvares Gomes <lucasagomes@gmail.com> Co-authored-by: Matthew Gilliard <matthew.gilliard@hp.com> Co-authored-by: Mikhail Durnosvistov <mdurnosvistov@mirantis.com> Co-authored-by: Pablo Fernando Cargnelutti <pablo.fernando.cargnelutti@intel.com> Co-authored-by: Robert Collins <rbtcollins@hp.com> Co-authored-by: ryo.kurahashi <kurahashi-rxa@necst.nec.co.jp>
This commit is contained in:
parent
166ddc58b1
commit
e3fe96d71a
|
@ -0,0 +1,124 @@
|
|||
# Copyright 2014 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from ironicclient import client as ironic_client
|
||||
from ironicclient import exc as ironic_exception
|
||||
import mock
|
||||
from oslo.config import cfg
|
||||
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova.tests.virt.ironic import utils as ironic_utils
|
||||
from nova.virt.ironic import client_wrapper
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
FAKE_CLIENT = ironic_utils.FakeClient()
|
||||
|
||||
|
||||
class IronicClientWrapperTestCase(test.NoDBTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(IronicClientWrapperTestCase, self).setUp()
|
||||
self.icli = client_wrapper.IronicClientWrapper()
|
||||
# Do not waste time sleeping
|
||||
cfg.CONF.set_override('api_retry_interval', 0, 'ironic')
|
||||
|
||||
@mock.patch.object(client_wrapper.IronicClientWrapper, '_multi_getattr')
|
||||
@mock.patch.object(client_wrapper.IronicClientWrapper, '_get_client')
|
||||
def test_call_good_no_args(self, mock_get_client, mock_multi_getattr):
|
||||
mock_get_client.return_value = FAKE_CLIENT
|
||||
self.icli.call("node.list")
|
||||
mock_get_client.assert_called_once_with()
|
||||
mock_multi_getattr.assert_called_once_with(FAKE_CLIENT, "node.list")
|
||||
mock_multi_getattr.return_value.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(client_wrapper.IronicClientWrapper, '_multi_getattr')
|
||||
@mock.patch.object(client_wrapper.IronicClientWrapper, '_get_client')
|
||||
def test_call_good_with_args(self, mock_get_client, mock_multi_getattr):
|
||||
mock_get_client.return_value = FAKE_CLIENT
|
||||
self.icli.call("node.list", 'test', associated=True)
|
||||
mock_get_client.assert_called_once_with()
|
||||
mock_multi_getattr.assert_called_once_with(FAKE_CLIENT, "node.list")
|
||||
mock_multi_getattr.return_value.assert_called_once_with(
|
||||
'test', associated=True)
|
||||
|
||||
@mock.patch.object(ironic_client, 'get_client')
|
||||
def test__get_client_no_auth_token(self, mock_ir_cli):
|
||||
self.flags(admin_auth_token=None, group='ironic')
|
||||
icli = client_wrapper.IronicClientWrapper()
|
||||
# dummy call to have _get_client() called
|
||||
icli.call("node.list")
|
||||
expected = {'os_username': CONF.ironic.admin_username,
|
||||
'os_password': CONF.ironic.admin_password,
|
||||
'os_auth_url': CONF.ironic.admin_url,
|
||||
'os_tenant_name': CONF.ironic.admin_tenant_name,
|
||||
'os_service_type': 'baremetal',
|
||||
'os_endpoint_type': 'public',
|
||||
'ironic_url': CONF.ironic.api_endpoint}
|
||||
mock_ir_cli.assert_called_once_with(CONF.ironic.api_version,
|
||||
**expected)
|
||||
|
||||
@mock.patch.object(ironic_client, 'get_client')
|
||||
def test__get_client_with_auth_token(self, mock_ir_cli):
|
||||
self.flags(admin_auth_token='fake-token', group='ironic')
|
||||
icli = client_wrapper.IronicClientWrapper()
|
||||
# dummy call to have _get_client() called
|
||||
icli.call("node.list")
|
||||
expected = {'os_auth_token': 'fake-token',
|
||||
'ironic_url': CONF.ironic.api_endpoint}
|
||||
mock_ir_cli.assert_called_once_with(CONF.ironic.api_version,
|
||||
**expected)
|
||||
|
||||
@mock.patch.object(client_wrapper.IronicClientWrapper, '_multi_getattr')
|
||||
@mock.patch.object(client_wrapper.IronicClientWrapper, '_get_client')
|
||||
def test_call_fail(self, mock_get_client, mock_multi_getattr):
|
||||
cfg.CONF.set_override('api_max_retries', 2, 'ironic')
|
||||
test_obj = mock.Mock()
|
||||
test_obj.side_effect = ironic_exception.HTTPServiceUnavailable
|
||||
mock_multi_getattr.return_value = test_obj
|
||||
mock_get_client.return_value = FAKE_CLIENT
|
||||
self.assertRaises(exception.NovaException, self.icli.call, "node.list")
|
||||
self.assertEqual(2, test_obj.call_count)
|
||||
|
||||
@mock.patch.object(client_wrapper.IronicClientWrapper, '_multi_getattr')
|
||||
@mock.patch.object(client_wrapper.IronicClientWrapper, '_get_client')
|
||||
def test_call_fail_unexpected_exception(self, mock_get_client,
|
||||
mock_multi_getattr):
|
||||
test_obj = mock.Mock()
|
||||
test_obj.side_effect = ironic_exception.HTTPNotFound
|
||||
mock_multi_getattr.return_value = test_obj
|
||||
mock_get_client.return_value = FAKE_CLIENT
|
||||
self.assertRaises(ironic_exception.HTTPNotFound, self.icli.call,
|
||||
"node.list")
|
||||
|
||||
@mock.patch.object(ironic_client, 'get_client')
|
||||
def test__get_client_unauthorized(self, mock_get_client):
|
||||
mock_get_client.side_effect = ironic_exception.Unauthorized
|
||||
self.assertRaises(exception.NovaException, self.icli._get_client)
|
||||
|
||||
@mock.patch.object(ironic_client, 'get_client')
|
||||
def test__get_client_unexpected_exception(self, mock_get_client):
|
||||
mock_get_client.side_effect = ironic_exception.ConnectionRefused
|
||||
self.assertRaises(ironic_exception.ConnectionRefused,
|
||||
self.icli._get_client)
|
||||
|
||||
def test__multi_getattr_good(self):
|
||||
response = self.icli._multi_getattr(FAKE_CLIENT, "node.list")
|
||||
self.assertEqual(FAKE_CLIENT.node.list, response)
|
||||
|
||||
def test__multi_getattr_fail(self):
|
||||
self.assertRaises(AttributeError, self.icli._multi_getattr,
|
||||
FAKE_CLIENT, "nonexistent")
|
|
@ -0,0 +1,115 @@
|
|||
# Copyright 2014 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from nova.virt.ironic import ironic_states
|
||||
|
||||
|
||||
def get_test_validation(**kw):
|
||||
return type('interfaces', (object,),
|
||||
{'power': kw.get('power', True),
|
||||
'deploy': kw.get('deploy', True),
|
||||
'console': kw.get('console', True),
|
||||
'rescue': kw.get('rescue', True)})()
|
||||
|
||||
|
||||
def get_test_node(**kw):
|
||||
return type('node', (object,),
|
||||
{'uuid': kw.get('uuid', 'eeeeeeee-dddd-cccc-bbbb-aaaaaaaaaaaa'),
|
||||
'chassis_uuid': kw.get('chassis_uuid'),
|
||||
'power_state': kw.get('power_state',
|
||||
ironic_states.NOSTATE),
|
||||
'target_power_state': kw.get('target_power_state',
|
||||
ironic_states.NOSTATE),
|
||||
'provision_state': kw.get('provision_state',
|
||||
ironic_states.NOSTATE),
|
||||
'target_provision_state': kw.get('target_provision_state',
|
||||
ironic_states.NOSTATE),
|
||||
'last_error': kw.get('last_error'),
|
||||
'instance_uuid': kw.get('instance_uuid'),
|
||||
'driver': kw.get('driver', 'fake'),
|
||||
'driver_info': kw.get('driver_info', {}),
|
||||
'properties': kw.get('properties', {}),
|
||||
'reservation': kw.get('reservation'),
|
||||
'maintenance': kw.get('maintenance', False),
|
||||
'extra': kw.get('extra', {}),
|
||||
'updated_at': kw.get('created_at'),
|
||||
'created_at': kw.get('updated_at')})()
|
||||
|
||||
|
||||
def get_test_port(**kw):
|
||||
return type('port', (object,),
|
||||
{'uuid': kw.get('uuid', 'gggggggg-uuuu-qqqq-ffff-llllllllllll'),
|
||||
'node_uuid': kw.get('node_uuid', get_test_node().uuid),
|
||||
'address': kw.get('address', 'FF:FF:FF:FF:FF:FF'),
|
||||
'extra': kw.get('extra', {}),
|
||||
'created_at': kw.get('created_at'),
|
||||
'updated_at': kw.get('updated_at')})()
|
||||
|
||||
|
||||
def get_test_flavor(**kw):
|
||||
default_extra_specs = {'baremetal:deploy_kernel_id':
|
||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'baremetal:deploy_ramdisk_id':
|
||||
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'}
|
||||
return {'name': kw.get('name', 'fake.flavor'),
|
||||
'extra_specs': kw.get('extra_specs', default_extra_specs),
|
||||
'swap': kw.get('swap', 0),
|
||||
'ephemeral_gb': kw.get('ephemeral_gb', 0)}
|
||||
|
||||
|
||||
def get_test_image_meta(**kw):
|
||||
return {'id': kw.get('id', 'cccccccc-cccc-cccc-cccc-cccccccccccc')}
|
||||
|
||||
|
||||
class FakePortClient(object):
|
||||
|
||||
def get(self, port_uuid):
|
||||
pass
|
||||
|
||||
def update(self, port_uuid, patch):
|
||||
pass
|
||||
|
||||
|
||||
class FakeNodeClient(object):
|
||||
|
||||
def list(self):
|
||||
return []
|
||||
|
||||
def get(self, node_uuid):
|
||||
pass
|
||||
|
||||
def get_by_instance_uuid(self, instance_uuid):
|
||||
pass
|
||||
|
||||
def list_ports(self, node_uuid):
|
||||
pass
|
||||
|
||||
def set_power_state(self, node_uuid, target):
|
||||
pass
|
||||
|
||||
def set_provision_state(self, node_uuid, target):
|
||||
pass
|
||||
|
||||
def update(self, node_uuid, patch):
|
||||
pass
|
||||
|
||||
def validate(self, node_uuid):
|
||||
pass
|
||||
|
||||
|
||||
class FakeClient(object):
|
||||
|
||||
node = FakeNodeClient()
|
||||
port = FakePortClient()
|
|
@ -0,0 +1,18 @@
|
|||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from nova.virt.ironic import driver
|
||||
|
||||
IronicDriver = driver.IronicDriver
|
|
@ -0,0 +1,104 @@
|
|||
# coding=utf-8
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import time
|
||||
|
||||
from ironicclient import client as ironic_client
|
||||
from ironicclient import exc as ironic_exception
|
||||
from oslo.config import cfg
|
||||
|
||||
from nova import exception
|
||||
from nova.openstack.common import gettextutils
|
||||
from nova.openstack.common import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
_ = gettextutils._
|
||||
|
||||
|
||||
class IronicClientWrapper(object):
|
||||
"""Ironic client wrapper class that encapsulates retry logic."""
|
||||
|
||||
def _get_client(self):
|
||||
# TODO(deva): save and reuse existing client & auth token
|
||||
# until it expires or is no longer valid
|
||||
auth_token = CONF.ironic.admin_auth_token
|
||||
if auth_token is None:
|
||||
kwargs = {'os_username': CONF.ironic.admin_username,
|
||||
'os_password': CONF.ironic.admin_password,
|
||||
'os_auth_url': CONF.ironic.admin_url,
|
||||
'os_tenant_name': CONF.ironic.admin_tenant_name,
|
||||
'os_service_type': 'baremetal',
|
||||
'os_endpoint_type': 'public',
|
||||
'ironic_url': CONF.ironic.api_endpoint}
|
||||
else:
|
||||
kwargs = {'os_auth_token': auth_token,
|
||||
'ironic_url': CONF.ironic.api_endpoint}
|
||||
|
||||
try:
|
||||
cli = ironic_client.get_client(CONF.ironic.api_version, **kwargs)
|
||||
except ironic_exception.Unauthorized:
|
||||
msg = (_("Unable to authenticate Ironic client."))
|
||||
LOG.error(msg)
|
||||
raise exception.NovaException(msg)
|
||||
|
||||
return cli
|
||||
|
||||
def _multi_getattr(self, obj, attr):
|
||||
"""Support nested attribute path for getattr().
|
||||
|
||||
:param obj: Root object.
|
||||
:param attr: Path of final attribute to get. E.g., "a.b.c.d"
|
||||
|
||||
:returns: The value of the final named attribute.
|
||||
:raises: AttributeError will be raised if the path is invalid.
|
||||
"""
|
||||
for attribute in attr.split("."):
|
||||
obj = getattr(obj, attribute)
|
||||
return obj
|
||||
|
||||
def call(self, method, *args, **kwargs):
|
||||
"""Call an Ironic client method and retry on errors.
|
||||
|
||||
:param method: Name of the client method to call as a string.
|
||||
:param args: Client method arguments.
|
||||
:param kwargs: Client method keyword arguments.
|
||||
|
||||
:raises: NovaException if all retries failed.
|
||||
"""
|
||||
retry_excs = (ironic_exception.ServiceUnavailable,
|
||||
ironic_exception.ConnectionRefused,
|
||||
ironic_exception.Conflict)
|
||||
num_attempts = CONF.ironic.api_max_retries
|
||||
|
||||
for attempt in range(1, num_attempts + 1):
|
||||
client = self._get_client()
|
||||
try:
|
||||
return self._multi_getattr(client, method)(*args, **kwargs)
|
||||
except retry_excs:
|
||||
msg = (_("Error contacting Ironic server for '%(method)s'. "
|
||||
"Attempt %(attempt)d of %(total)d")
|
||||
% {'method': method,
|
||||
'attempt': attempt,
|
||||
'total': num_attempts})
|
||||
if attempt == num_attempts:
|
||||
LOG.error(msg)
|
||||
raise exception.NovaException(msg)
|
||||
LOG.warning(msg)
|
||||
time.sleep(CONF.ironic.api_retry_interval)
|
|
@ -0,0 +1,68 @@
|
|||
# coding=utf-8
|
||||
#
|
||||
# Copyright 2014 Red Hat, Inc.
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
A driver wrapping the Ironic API, such that Nova may provision
|
||||
bare metal resources.
|
||||
"""
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from nova.virt import driver as virt_driver
|
||||
|
||||
|
||||
opts = [
|
||||
cfg.IntOpt('api_version',
|
||||
default=1,
|
||||
help='Version of Ironic API service endpoint.'),
|
||||
cfg.StrOpt('api_endpoint',
|
||||
help='URL for Ironic API endpoint.'),
|
||||
cfg.StrOpt('admin_username',
|
||||
help='Ironic keystone admin name'),
|
||||
cfg.StrOpt('admin_password',
|
||||
help='Ironic keystone admin password.'),
|
||||
cfg.StrOpt('admin_auth_token',
|
||||
help='Ironic keystone auth token.'),
|
||||
cfg.StrOpt('admin_url',
|
||||
help='Keystone public API endpoint.'),
|
||||
cfg.StrOpt('client_log_level',
|
||||
help='Log level override for ironicclient. Set this in '
|
||||
'order to override the global "default_log_levels", '
|
||||
'"verbose", and "debug" settings.'),
|
||||
cfg.StrOpt('admin_tenant_name',
|
||||
help='Ironic keystone tenant name.'),
|
||||
cfg.IntOpt('api_max_retries',
|
||||
default=60,
|
||||
help=('How many retries when a request does conflict.')),
|
||||
cfg.IntOpt('api_retry_interval',
|
||||
default=2,
|
||||
help=('How often to retry in seconds when a request '
|
||||
'does conflict')),
|
||||
]
|
||||
|
||||
ironic_group = cfg.OptGroup(name='ironic',
|
||||
title='Ironic Options')
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_group(ironic_group)
|
||||
CONF.register_opts(opts, ironic_group)
|
||||
|
||||
|
||||
class IronicDriver(virt_driver.ComputeDriver):
|
||||
"""Hypervisor driver for Ironic - bare metal provisioning."""
|
||||
pass
|
|
@ -0,0 +1,66 @@
|
|||
# Copyright (c) 2012 NTT DOCOMO, INC.
|
||||
# Copyright 2010 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Mapping of bare metal node states.
|
||||
|
||||
A node may have empty {} `properties` and `driver_info` in which case, it is
|
||||
said to be "initialized" but "not available", and the state is NOSTATE.
|
||||
|
||||
When updating `properties`, any data will be rejected if the data fails to be
|
||||
validated by the driver. Any node with non-empty `properties` is said to be
|
||||
"initialized", and the state is INIT.
|
||||
|
||||
When the driver has received both `properties` and `driver_info`, it will check
|
||||
the power status of the node and update the `power_state` accordingly. If the
|
||||
driver fails to read the power state from the node, it will reject the
|
||||
`driver_info` change, and the state will remain as INIT. If the power status
|
||||
check succeeds, `power_state` will change to one of POWER_ON or POWER_OFF,
|
||||
accordingly.
|
||||
|
||||
At this point, the power state may be changed via the API, a console
|
||||
may be started, and a tenant may be associated.
|
||||
|
||||
The `power_state` for a node always represents the current power state. Any
|
||||
power operation sets this to the actual state when done (whether successful or
|
||||
not). It is set to ERROR only when unable to get the power state from a node.
|
||||
|
||||
When `instance_uuid` is set to a non-empty / non-None value, the node is said
|
||||
to be "associated" with a tenant.
|
||||
|
||||
An associated node can not be deleted.
|
||||
|
||||
The `instance_uuid` field may be unset only if the node is in POWER_OFF or
|
||||
ERROR states.
|
||||
"""
|
||||
|
||||
NOSTATE = None
|
||||
INIT = 'initializing'
|
||||
ACTIVE = 'active'
|
||||
BUILDING = 'building'
|
||||
DEPLOYWAIT = 'wait call-back'
|
||||
DEPLOYING = 'deploying'
|
||||
DEPLOYFAIL = 'deploy failed'
|
||||
DEPLOYDONE = 'deploy complete'
|
||||
DELETING = 'deleting'
|
||||
DELETED = 'deleted'
|
||||
ERROR = 'error'
|
||||
REBUILD = 'rebuild'
|
||||
|
||||
POWER_ON = 'power on'
|
||||
POWER_OFF = 'power off'
|
||||
REBOOT = 'rebooting'
|
||||
SUSPEND = 'suspended'
|
|
@ -13,6 +13,7 @@ mox>=0.5.3
|
|||
MySQL-python
|
||||
psycopg2
|
||||
pylint==0.25.2
|
||||
python-ironicclient>=0.2.1
|
||||
python-subunit>=0.0.18
|
||||
sphinx>=1.1.2,!=1.2.0,<1.3
|
||||
oslosphinx>=2.2.0.0a2
|
||||
|
|
Loading…
Reference in New Issue