Support for testing adoption in the standalone job
This change adds a test for adoption. It's off by default because
it's destructive (removes and re-adds a node) and requires reading
(or guessing) the BMC credentials.
Change-Id: I0178c2b906449802ce38059d4191a63b4b317226
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index 79b6d18..dc709b6 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -147,6 +147,11 @@
cfg.BoolOpt('ipxe_enabled',
default=True,
help="Defines if IPXE is enabled"),
+ cfg.BoolOpt('adoption',
+ # Defaults to False since it's a destructive operation AND it
+ # requires the plugin to be able to read ipmi_password.
+ default=False,
+ help="Defines if adoption is enabled"),
]
BaremetalIntrospectionGroup = [
diff --git a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
index b326e0d..8d5fec5 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -277,6 +277,11 @@
return self._create_request('nodes', node)
@base.handle_errors
+ def create_node_raw(self, **kwargs):
+ """Create a baremetal node from the given body."""
+ return self._create_request('nodes', kwargs)
+
+ @base.handle_errors
def create_chassis(self, **kwargs):
"""Create a chassis with the specified parameters.
@@ -307,13 +312,12 @@
:return: A tuple with the server response and the created port.
"""
- port = {'extra': kwargs.get('extra', {'foo': 'bar'}),
- 'uuid': kwargs['uuid']}
+ port = {'extra': kwargs.get('extra', {'foo': 'bar'})}
if node_id is not None:
port['node_uuid'] = node_id
- for key in ('address', 'physical_network', 'portgroup_uuid'):
+ for key in ('uuid', 'address', 'physical_network', 'portgroup_uuid'):
if kwargs.get(key) is not None:
port[key] = kwargs[key]
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_adoption.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_adoption.py
new file mode 100644
index 0000000..63e5f5a
--- /dev/null
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_adoption.py
@@ -0,0 +1,107 @@
+#
+# Copyright 2017 Mirantis Inc.
+#
+# 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 oslo_log import log as logging
+from tempest.common import utils
+from tempest import config
+from tempest.lib import decorators
+
+from ironic_tempest_plugin.tests.scenario import \
+ baremetal_standalone_manager as bsm
+
+LOG = logging.getLogger(__name__)
+CONF = config.CONF
+
+
+class BaremetalAdoptionIpmiWholedisk(
+ bsm.BaremetalStandaloneScenarioTest):
+
+ driver = 'ipmi'
+ image_ref = CONF.baremetal.whole_disk_image_ref
+ wholedisk_image = True
+ deploy_interface = 'iscsi'
+ # 1.37 is required to be able to copy traits
+ api_microversion = '1.37'
+
+ @classmethod
+ def skip_checks(cls):
+ super(BaremetalAdoptionIpmiWholedisk, cls).skip_checks()
+ if not CONF.baremetal_feature_enabled.adoption:
+ skip_msg = ("Adoption feature is not enabled")
+ raise cls.skipException(skip_msg)
+
+ @classmethod
+ def recreate_node(cls):
+ # Now record all up-to-date node information for creation
+ cls.node = cls.get_node(cls.node['uuid'])
+ body = {'driver_info': cls.node['driver_info'],
+ 'instance_info': cls.node['instance_info'],
+ 'driver': cls.node['driver'],
+ 'properties': cls.node['properties']}
+ if set(body['driver_info'].get('ipmi_password')) == {'*'}:
+ # A hack to enable devstack testing without showing secrets
+ # secrets. Use the hardcoded devstack value.
+ body['driver_info']['ipmi_password'] = 'password'
+ # configdrive is hidden and anyway should be supplied on rebuild
+ body['instance_info'].pop('configdrive', None)
+ for key, value in cls.node.items():
+ if key.endswith('_interface') and value:
+ body[key] = value
+ traits = cls.node['traits']
+ _, vifs = cls.baremetal_client.vif_list(cls.node['uuid'])
+ _, ports = cls.baremetal_client.list_ports(node=cls.node['uuid'])
+
+ # Delete the active node using maintenance
+ cls.update_node(cls.node['uuid'], [{'op': 'replace',
+ 'path': '/maintenance',
+ 'value': True}])
+ cls.baremetal_client.delete_node(cls.node['uuid'])
+
+ # Now create an identical node and attach VIFs
+ _, cls.node = cls.baremetal_client.create_node_raw(**body)
+ if traits:
+ cls.baremetal_client.set_node_traits(cls.node['uuid'], traits)
+ for port in ports['ports']:
+ cls.baremetal_client.create_port(cls.node['uuid'],
+ address=port['address'])
+
+ cls.set_node_provision_state(cls.node['uuid'], 'manage')
+ cls.wait_provisioning_state(cls.node['uuid'], 'manageable',
+ timeout=300, interval=5)
+
+ for vif in vifs['vifs']:
+ cls.vif_attach(cls.node['uuid'], vif['id'])
+
+ return cls.node
+
+ @decorators.idempotent_id('2f51890e-20d9-43ef-af39-41b335ec066b')
+ @utils.services('image', 'network')
+ def test_adoption(self):
+ # First, prepare a deployed node.
+ self.boot_node()
+
+ # Then re-create it with the same parameters.
+ self.recreate_node()
+
+ # Now adoption!
+ self.set_node_provision_state(self.node['uuid'], 'adopt')
+ self.wait_provisioning_state(self.node['uuid'], 'active',
+ timeout=300, interval=5)
+
+ # Try to rebuild the server to make sure we can manage it now.
+ self.set_node_provision_state(self.node['uuid'], 'rebuild')
+ self.wait_provisioning_state(self.node['uuid'], 'active',
+ timeout=CONF.baremetal.active_timeout,
+ interval=30)