diff options
-rw-r--r-- | ironic/nova/tests/virt/ironic/test_driver.py | 7 | ||||
-rw-r--r-- | ironic/nova/virt/ironic/client_wrapper.py | 98 | ||||
-rw-r--r-- | ironic/nova/virt/ironic/driver.py | 20 |
3 files changed, 112 insertions, 13 deletions
diff --git a/ironic/nova/tests/virt/ironic/test_driver.py b/ironic/nova/tests/virt/ironic/test_driver.py index 25af73a6a..80cf2fdcf 100644 --- a/ironic/nova/tests/virt/ironic/test_driver.py +++ b/ironic/nova/tests/virt/ironic/test_driver.py @@ -22,6 +22,7 @@ from ironicclient import exc as ironic_exception import mock from oslo.config import cfg +from ironic.nova.virt.ironic import client_wrapper as cw from ironic.nova.virt.ironic import driver as ironic_driver from ironic.nova.virt.ironic import ironic_states @@ -326,11 +327,12 @@ class IronicDriverTestCase(test.NoDBTestCase): # append a node w/o instance_uuid which shouldn't be listed nodes.append(get_test_node(instance_uuid=None)) - with mock.patch.object(FAKE_CLIENT.node, 'list') as mock_list: + with mock.patch.object(cw.IronicClientWrapper, 'call') as mock_list: mock_list.return_value = nodes expected = [n for n in nodes if n.instance_uuid] instances = self.driver.list_instances() + mock_list.assert_called_with("node.list") self.assertEqual(sorted(expected), sorted(instances)) self.assertEqual(num_nodes, len(instances)) @@ -343,11 +345,12 @@ class IronicDriverTestCase(test.NoDBTestCase): # append a node w/o power_state which shouldn't be listed nodes.append(get_test_node(power_state=None)) - with mock.patch.object(FAKE_CLIENT.node, 'list') as mock_list: + with mock.patch.object(cw.IronicClientWrapper, 'call') as mock_list: mock_list.return_value = nodes expected = [n.uuid for n in nodes if n.power_state] available_nodes = self.driver.get_available_nodes() + mock_list.assert_called_with("node.list") self.assertEqual(sorted(expected), sorted(available_nodes)) self.assertEqual(num_nodes, len(available_nodes)) diff --git a/ironic/nova/virt/ironic/client_wrapper.py b/ironic/nova/virt/ironic/client_wrapper.py new file mode 100644 index 000000000..5120b991a --- /dev/null +++ b/ironic/nova/virt/ironic/client_wrapper.py @@ -0,0 +1,98 @@ +# 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 nova import exception +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import log as logging +from oslo.config import cfg + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +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'} + 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): + """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. + + :raises: NovaException if all retries failed. + """ + retry_excs = (ironic_exception.HTTPServiceUnavailable, + ironic_exception.CommunicationError) + 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) + 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) diff --git a/ironic/nova/virt/ironic/driver.py b/ironic/nova/virt/ironic/driver.py index 154152f76..bb5a8e671 100644 --- a/ironic/nova/virt/ironic/driver.py +++ b/ironic/nova/virt/ironic/driver.py @@ -25,6 +25,7 @@ from ironicclient import client as ironic_client from ironicclient import exc as ironic_exception from oslo.config import cfg +from ironic.nova.virt.ironic import client_wrapper from ironic.nova.virt.ironic import ironic_states from nova.compute import power_state from nova import exception @@ -331,19 +332,15 @@ class IronicDriver(virt_driver.ComputeDriver): return CONF.ironic.api_version def list_instances(self): - try: - icli = self._get_client() - except ironic_exception.Unauthorized: - LOG.error(_("Unable to authenticate Ironic client.")) - return [] - - instances = [i for i in icli.node.list() if i.instance_uuid] + icli = client_wrapper.IronicClientWrapper() + node_list = icli.call("node.list") + instances = [i for i in node_list if i.instance_uuid] return instances def get_available_nodes(self, refresh=False): nodes = [] - icli = self._get_client() - node_list = icli.node.list() + icli = client_wrapper.IronicClientWrapper() + node_list = icli.call("node.list") for n in node_list: # for now we'll use the nodes power state. if power_state is None @@ -590,8 +587,9 @@ class IronicDriver(virt_driver.ComputeDriver): def get_host_stats(self, refresh=False): caps = [] - icli = self._get_client() - for node in icli.node.list(): + icli = client_wrapper.IronicClientWrapper() + node_list = icli.call("node.list") + for node in node_list: data = self._node_resource(node) caps.append(data) return caps |