From 5b35225d16eb91edde72c3684887ad6f8b226a85 Mon Sep 17 00:00:00 2001 From: ricolin Date: Thu, 15 Aug 2019 12:42:34 +0800 Subject: [PATCH] Add ironic client plugin support Change-Id: I2f3ee94424c4dab75fbcef6f8b32e565b45684e4 Task: 36285 --- README.rst | 1 + heat/common/config.py | 5 ++ heat/engine/clients/os/ironic.py | 82 +++++++++++++++++++ heat/tests/clients/test_ironic_client.py | 77 +++++++++++++++++ heat/tests/openstack/ironic/__init__.py | 0 lower-constraints.txt | 1 + ...ironic-client-plugin-b7b91b7090579c81.yaml | 7 ++ requirements.txt | 1 + setup.cfg | 4 + 9 files changed, 178 insertions(+) create mode 100644 heat/engine/clients/os/ironic.py create mode 100644 heat/tests/clients/test_ironic_client.py create mode 100644 heat/tests/openstack/ironic/__init__.py create mode 100644 releasenotes/notes/support-ironic-client-plugin-b7b91b7090579c81.yaml diff --git a/README.rst b/README.rst index 10ec06dff7..6e1ff8e95a 100644 --- a/README.rst +++ b/README.rst @@ -76,3 +76,4 @@ We have integration with * https://opendev.org/openstack/python-octaviaclient.git (Load-balancer service) * https://opendev.org/openstack/python-senlinclient (Clustering service) * https://opendev.org/openstack/python-vitrageclient.git (RCA service) +* https://opendev.org/openstack/python-ironicclient (baremetal provisioning service) diff --git a/heat/common/config.py b/heat/common/config.py index 0a533439e1..06db5eaabf 100644 --- a/heat/common/config.py +++ b/heat/common/config.py @@ -183,6 +183,11 @@ engine_opts = [ 'this limitation, any nova feature supported with ' 'microversion number above max_nova_api_microversion ' 'will not be available.')), + cfg.FloatOpt('max_ironic_api_microversion', + help=_('Maximum ironic API version for client plugin. With ' + 'this limitation, any ironic feature supported with ' + 'microversion number above ' + 'max_ironic_api_microversion will not be available.')), cfg.IntOpt('event_purge_batch_size', min=1, default=200, diff --git a/heat/engine/clients/os/ironic.py b/heat/engine/clients/os/ironic.py new file mode 100644 index 0000000000..59ade7b4be --- /dev/null +++ b/heat/engine/clients/os/ironic.py @@ -0,0 +1,82 @@ +# +# 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.common.apiclient import exceptions as ic_exc +from ironicclient.v1 import client as ironic_client +from oslo_config import cfg + +from heat.common import exception +from heat.engine.clients import client_plugin +from heat.engine import constraints + +CLIENT_NAME = 'ironic' + + +class IronicClientPlugin(client_plugin.ClientPlugin): + + service_types = [BAREMETAL] = ['baremetal'] + IRONIC_API_VERSION = 1.58 + max_ironic_api_microversion = cfg.CONF.max_ironic_api_microversion + max_microversion = max_ironic_api_microversion if ( + max_ironic_api_microversion is not None and ( + IRONIC_API_VERSION > max_ironic_api_microversion) + ) else IRONIC_API_VERSION + + def _create(self): + interface = self._get_client_option(CLIENT_NAME, 'endpoint_type') + args = { + 'interface': interface, + 'service_type': self.BAREMETAL, + 'session': self.context.keystone_session, + 'region_name': self._get_region_name(), + 'os_ironic_api_version': self.max_microversion + } + client = ironic_client.Client(**args) + return client + + def is_not_found(self, ex): + return isinstance(ex, ic_exc.NotFound) + + def is_over_limit(self, ex): + return isinstance(ex, ic_exc.RequestEntityTooLarge) + + def is_conflict(self, ex): + return isinstance(ex, ic_exc.Conflict) + + def _get_rsrc_name_or_id(self, value, entity, entity_msg): + entity_client = getattr(self.client(), entity) + try: + return entity_client.get(value).uuid + except ic_exc.NotFound: + # Ironic cli will find the value either is name or id, + # so no need to call list() here. + raise exception.EntityNotFound(entity=entity_msg, + name=value) + + def get_portgroup(self, value): + return self._get_rsrc_name_or_id(value, entity='portgroup', + entity_msg='PortGroup') + + def get_node(self, value): + return self._get_rsrc_name_or_id(value, entity='node', + entity_msg='Node') + + +class PortGroupConstraint(constraints.BaseCustomConstraint): + resource_client_name = CLIENT_NAME + resource_getter_name = 'get_portgroup' + + +class NodeConstraint(constraints.BaseCustomConstraint): + resource_client_name = CLIENT_NAME + resource_getter_name = 'get_node' diff --git a/heat/tests/clients/test_ironic_client.py b/heat/tests/clients/test_ironic_client.py new file mode 100644 index 0000000000..a56ae3e1b7 --- /dev/null +++ b/heat/tests/clients/test_ironic_client.py @@ -0,0 +1,77 @@ +# +# 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 exceptions as ic_exc +import mock + +from heat.engine.clients.os import ironic as ic +from heat.tests import common +from heat.tests import utils + + +class IronicClientPluginTest(common.HeatTestCase): + + def test_create(self): + context = utils.dummy_context() + plugin = context.clients.client_plugin('ironic') + client = plugin.client() + self.assertEqual('http://server.test:5000/v3', + client.port.api.session.auth.endpoint) + + +class fake_resource(object): + def __init__(self, id=None, name=None): + self.uuid = id + self.name = name + + +class PortGroupConstraintTest(common.HeatTestCase): + def setUp(self): + super(PortGroupConstraintTest, self).setUp() + self.ctx = utils.dummy_context() + self.mock_port_group_get = mock.Mock() + self.ctx.clients.client_plugin( + 'ironic').client().portgroup.get = self.mock_port_group_get + self.constraint = ic.PortGroupConstraint() + + def test_validate(self): + self.mock_port_group_get.return_value = fake_resource( + id='my_port_group') + self.assertTrue(self.constraint.validate( + 'my_port_group', self.ctx)) + + def test_validate_fail(self): + self.mock_port_group_get.side_effect = ic_exc.NotFound() + self.assertFalse(self.constraint.validate( + "bad_port_group", self.ctx)) + + +class NodeConstraintTest(common.HeatTestCase): + def setUp(self): + super(NodeConstraintTest, self).setUp() + self.ctx = utils.dummy_context() + self.mock_node_get = mock.Mock() + self.ctx.clients.client_plugin( + 'ironic').client().node.get = self.mock_node_get + self.constraint = ic.NodeConstraint() + + def test_validate(self): + self.mock_node_get.return_value = fake_resource( + id='my_node') + self.assertTrue(self.constraint.validate( + 'my_node', self.ctx)) + + def test_validate_fail(self): + self.mock_node_get.side_effect = ic_exc.NotFound() + self.assertFalse(self.constraint.validate( + "bad_node", self.ctx)) diff --git a/heat/tests/openstack/ironic/__init__.py b/heat/tests/openstack/ironic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lower-constraints.txt b/lower-constraints.txt index 7804a44472..e859aba3a6 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -111,6 +111,7 @@ python-designateclient==2.7.0 python-editor==1.0.3 python-glanceclient==2.8.0 python-heatclient==1.10.0 +python-ironicclient==2.8.0 python-keystoneclient==3.8.0 python-magnumclient==2.3.0 python-manilaclient==1.16.0 diff --git a/releasenotes/notes/support-ironic-client-plugin-b7b91b7090579c81.yaml b/releasenotes/notes/support-ironic-client-plugin-b7b91b7090579c81.yaml new file mode 100644 index 0000000000..258317496b --- /dev/null +++ b/releasenotes/notes/support-ironic-client-plugin-b7b91b7090579c81.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Introduce a Ironic client plugin module that will be used by the Ironic's + resources. + Support only ironicclient version >=2.8.0 to get allocation functionality + support. diff --git a/requirements.txt b/requirements.txt index 05d431891a..f77d011a49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,6 +38,7 @@ python-cinderclient>=3.3.0 # Apache-2.0 python-designateclient>=2.7.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-heatclient>=1.10.0 # Apache-2.0 +python-ironicclient>=2.8.0 # Apache-2.0 python-keystoneclient>=3.8.0 # Apache-2.0 python-magnumclient>=2.3.0 # Apache-2.0 python-manilaclient>=1.16.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 2c2f4a08bf..648a88b096 100644 --- a/setup.cfg +++ b/setup.cfg @@ -74,6 +74,7 @@ heat.clients = designate = heat.engine.clients.os.designate:DesignateClientPlugin glance = heat.engine.clients.os.glance:GlanceClientPlugin heat = heat.engine.clients.os.heat_plugin:HeatClientPlugin + ironic = heat.engine.clients.os.ironic:IronicClientPlugin keystone = heat.engine.clients.os.keystone:KeystoneClientPlugin magnum = heat.engine.clients.os.magnum:MagnumClientPlugin manila = heat.engine.clients.os.manila:ManilaClientPlugin @@ -174,6 +175,9 @@ heat.constraints = senlin.profile_type = heat.engine.clients.os.senlin:ProfileTypeConstraint trove.flavor = heat.engine.clients.os.trove:FlavorConstraint zaqar.queue = heat.engine.clients.os.zaqar:QueueConstraint + #ironic + ironic.portgroup = heat.engine.clients.os.ironic:PortGroupConstraint + ironic.node = heat.engine.clients.os.ironic:NodeConstraint heat.stack_lifecycle_plugins =