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:
Michael Davies 2014-08-01 09:41:21 +00:00 committed by Sean Dague
parent 166ddc58b1
commit e3fe96d71a
8 changed files with 496 additions and 0 deletions

View File

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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