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
This commit is contained in:
tengqm 2017-03-13 07:05:32 -04:00
parent ad13f9fa70
commit 9dea560572
2 changed files with 193 additions and 15 deletions

View File

@ -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

View File

@ -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