From 9dea560572c61e6240ad977daec7e0e835469c7d Mon Sep 17 00:00:00 2001 From: tengqm Date: Mon, 13 Mar 2017 07:05:32 -0400 Subject: [PATCH] Adopt operation for nova server profile This adds the operation support for adopting an existing server. There are still some items we cannot get from nova server, due to Nova API limitations, but the current support should be okay for most use cases if users don't care much about 'user_data'. Change-Id: Iaca7a5f5ab8cc4aad6707f5a1148457874d415ea --- senlin/profiles/os/nova/server.py | 80 +++++++++++ .../tests/unit/profiles/test_nova_server.py | 128 ++++++++++++++++-- 2 files changed, 193 insertions(+), 15 deletions(-) diff --git a/senlin/profiles/os/nova/server.py b/senlin/profiles/os/nova/server.py index 9329b77d8..9acb41b0d 100644 --- a/senlin/profiles/os/nova/server.py +++ b/senlin/profiles/os/nova/server.py @@ -1048,6 +1048,86 @@ class ServerProfile(base.Profile): return dict((k, details[k]) for k in sorted(details)) + def do_adopt(self, obj, overrides=None, snapshot=False): + """Adopt an existing server node for management. + + :param obj: A node object for this operation. It could be a puppet + node that provides only 'user', 'project' and 'physical_id' + properties when doing a preview. It can be a real Node object for + node adoption. + :param overrides: A dict containing the properties that will be + overridden when generating a profile for the server. + :param snapshot: A boolean flag indicating whether the profile should + attempt a snapshot operation before adopting the server. If set to + True, the ID of the snapshot will be used as the image ID. + + :returns: A dict containing the spec created from the server object or + a dict containing error information if failure occurred. + """ + driver = self.compute(obj) + + # TODO(Qiming): Add snapshot support + # snapshot = driver.snapshot_create(...) + + error = {} + try: + server = driver.server_get(obj.physical_id) + except exc.InternalError as ex: + error = {'code': ex.code, 'message': six.text_type(ex)} + + if error: + return {'Error': error} + + spec = {} + # Context? + # TODO(Qiming): Need to fetch admin password from a different API + disk_config = server.disk_config + spec[self.AUTO_DISK_CONFIG] = disk_config and disk_config == 'AUTO' + + spec[self.AVAILABILITY_ZONE] = server.availability_zone + + # TODO(Anyone): verify if this needs a format conversion + bdm = server.block_device_mapping + spec[self.BLOCK_DEVICE_MAPPING_V2] = bdm + + spec[self.CONFIG_DRIVE] = server.has_config_drive + spec[self.FLAVOR] = server.flavor['id'] + spec[self.IMAGE] = server.image['id'] or server.image + spec[self.KEY_NAME] = server.key_name + + # metadata + metadata = server.metadata or {} + metadata.pop('cluster_id', None) + metadata.pop('cluster_node_id', None) + metadata.pop('cluster_node_index', None) + spec[self.METADATA] = metadata + + # name + spec[self.NAME] = server.name + + networks = server.addresses + net_list = [] + for network, interfaces in networks.items(): + for intf in interfaces: + ip_type = intf.get('OS-EXT-IPS:type') + if ip_type == 'fixed': + net_list.append({self.NETWORK: network}) + + spec[self.NETWORKS] = net_list + # NOTE: the personality attribute is missing for ever. + spec[self.SECURITY_GROUPS] = [ + sg['name'] for sg in server.security_groups + ] + # TODO(Qiming): get server user_data and parse it. + # Note: user_data is returned in 2.3 microversion API, in a different + # property name. + # spec[self.USER_DATA] = server.user_data + + if overrides: + spec.update(overrides) + + return spec + def do_join(self, obj, cluster_id): if not obj.physical_id: return False diff --git a/senlin/tests/unit/profiles/test_nova_server.py b/senlin/tests/unit/profiles/test_nova_server.py index 4a59c44c8..5e4b1369f 100644 --- a/senlin/tests/unit/profiles/test_nova_server.py +++ b/senlin/tests/unit/profiles/test_nova_server.py @@ -859,28 +859,126 @@ class TestNovaServerBasic(base.SenlinTestCase): self.assertEqual(expected, res) cc.server_get.assert_called_once_with('FAKE_ID') - def test_do_join_successful(self): + def test_do_adopt(self): profile = server.ServerProfile('t', self.spec) - - cluster_id = "FAKE_CLUSTER_ID" + x_server = mock.Mock( + disk_config="", + availability_zone="AZ01", + block_device_mapping={"foo": "bar"}, + has_config_drive=False, + flavor={"id": "FLAVOR_ID"}, + image={"id": "IMAGE_ID"}, + key_name="FAKE_KEY", + metadata={ + "mkey": "mvalue", + "cluster_id": "CLUSTER_ID", + "cluster_node_id": "NODE_ID", + "cluster_node_index": 123 + }, + addresses={ + "NET1": [{ + "OS-EXT-IPS:type": "fixed", + "addr": "ADDR1" + }], + "NET2": [{ + "OS-EXT-IPS:type": "fixed", + "addr": "ADDR2" + }], + }, + security_groups=[{'name': 'GROUP1'}, {'name': 'GROUP2'}] + ) + x_server.name = "FAKE_NAME" cc = mock.Mock() - cc.server_metadata_get.return_value = {'FOO': 'BAR'} - cc.server_metadata_update.return_value = {'cluster_id': cluster_id} + cc.server_get.return_value = x_server profile._computeclient = cc + node_obj = mock.Mock(physical_id='FAKE_ID') - node_obj = mock.Mock(physical_id='FAKE_ID', index=567) + res = profile.do_adopt(node_obj) - res = profile.do_join(node_obj, cluster_id) + self.assertEqual('', res['auto_disk_config']) + self.assertEqual('AZ01', res['availability_zone']) + self.assertEqual({'foo': 'bar'}, res['block_device_mapping_v2']) + self.assertFalse(res['config_drive']) + self.assertEqual('FLAVOR_ID', res['flavor']) + self.assertEqual('IMAGE_ID', res['image']) + self.assertEqual('FAKE_KEY', res['key_name']) + self.assertEqual({'mkey': 'mvalue'}, res['metadata']) + self.assertIn({'network': 'NET1'}, res['networks']) + self.assertIn({'network': 'NET2'}, res['networks']) + self.assertIn('GROUP1', res['security_groups']) + self.assertIn('GROUP2', res['security_groups']) + cc.server_get.assert_called_once_with('FAKE_ID') - self.assertTrue(res) - cc.server_metadata_get.assert_called_once_with('FAKE_ID') - expected_metadata = { - 'cluster_id': 'FAKE_CLUSTER_ID', - 'cluster_node_index': '567', - 'FOO': 'BAR' + def test_do_adopt_failed_get(self): + profile = server.ServerProfile('t', self.spec) + cc = mock.Mock() + err = exc.InternalError(code=404, message='No Server found for ID') + cc.server_get.side_effect = err + profile._computeclient = cc + node_obj = mock.Mock(physical_id='FAKE_ID') + + res = profile.do_adopt(node_obj) + + expected = { + 'Error': { + 'code': 404, + 'message': 'No Server found for ID', + } } - cc.server_metadata_update.assert_called_once_with( - 'FAKE_ID', expected_metadata) + self.assertEqual(expected, res) + cc.server_get.assert_called_once_with('FAKE_ID') + + def test_do_adopt_with_overrides(self): + profile = server.ServerProfile('t', self.spec) + x_server = mock.Mock( + disk_config="", + availability_zone="AZ01", + block_device_mapping={"foo": "bar"}, + has_config_drive=False, + flavor={"id": "FLAVOR_ID"}, + image={"id": "IMAGE_ID"}, + key_name="FAKE_KEY", + metadata={ + "mkey": "mvalue", + "cluster_id": "CLUSTER_ID", + "cluster_node_id": "NODE_ID", + "cluster_node_index": 123 + }, + addresses={ + "NET1": [{ + "OS-EXT-IPS:type": "fixed", + }], + "NET2": [{ + "OS-EXT-IPS:type": "fixed", + }], + }, + security_groups=[{'name': 'GROUP1'}, {'name': 'GROUP2'}] + ) + x_server.name = "FAKE_NAME" + cc = mock.Mock() + cc.server_get.return_value = x_server + profile._computeclient = cc + node_obj = mock.Mock(physical_id='FAKE_ID') + overrides = { + 'networks': [{"network": "NET3"}] + } + + res = profile.do_adopt(node_obj, overrides=overrides) + + self.assertEqual('', res['auto_disk_config']) + self.assertEqual('AZ01', res['availability_zone']) + self.assertEqual({'foo': 'bar'}, res['block_device_mapping_v2']) + self.assertFalse(res['config_drive']) + self.assertEqual('FLAVOR_ID', res['flavor']) + self.assertEqual('IMAGE_ID', res['image']) + self.assertEqual('FAKE_KEY', res['key_name']) + self.assertEqual({'mkey': 'mvalue'}, res['metadata']) + self.assertIn({'network': 'NET3'}, res['networks']) + self.assertNotIn({'network': 'NET1'}, res['networks']) + self.assertNotIn({'network': 'NET2'}, res['networks']) + self.assertIn('GROUP1', res['security_groups']) + self.assertIn('GROUP2', res['security_groups']) + cc.server_get.assert_called_once_with('FAKE_ID') def test_do_join_server_not_created(self): # Test path where server not specified