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)