Make Server software_config_transport updatable
This change makes it possible to update a server's software_config_transport property without replacing the server. Some reasons for wanting to do this with long-running servers include: - moving to POLL_TEMP_URL to reduce load on the heat engines, or to be able to create deployments outside the stack - moving to ZAQAR_MESSAGE once the host cloud has deployed zaqar Any existing transport *must* continue working and return up-to-date data for the lifecycle of the stack since it can't be known when all servers have switched to the new transport. This means that a transport's enabled status should be determined by the enabling resource data rather than the software_config_transport property. The os-collect-config configuration data needs to ensure that old transport configurations are disabled. This includes local boot config which remains on the server for its lifetime, and is outdated once the transport changes. This disabling is achieved by setting any existing configuration values to None. This has been tested on a tripleo cloud by repeatedly switching between POLL_TEMP_URL and POLL_SERVER_CFN during a stack update and confirming that os-collect-config switches to the new transport. Change-Id: I9c475f0c489c67db5895924050186228403f2773 Closes-Bug: #1519609
This commit is contained in:
parent
6a186c0f29
commit
73c41fe8a8
|
@ -425,6 +425,7 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin,
|
|||
'create a dedicated zaqar queue and post the metadata '
|
||||
'for polling.'),
|
||||
default=cfg.CONF.default_software_config_transport,
|
||||
update_allowed=True,
|
||||
constraints=[
|
||||
constraints.AllowedValues(_SOFTWARE_CONFIG_TRANSPORTS),
|
||||
]
|
||||
|
@ -560,18 +561,29 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin,
|
|||
# This method is overridden by the derived CloudServer resource
|
||||
return self.properties[self.CONFIG_DRIVE]
|
||||
|
||||
def _populate_deployments_metadata(self, meta):
|
||||
def _populate_deployments_metadata(self, meta, props):
|
||||
meta['deployments'] = meta.get('deployments', [])
|
||||
meta['os-collect-config'] = meta.get('os-collect-config', {})
|
||||
if self.transport_poll_server_heat():
|
||||
meta['os-collect-config'].update({'heat': {
|
||||
occ = meta['os-collect-config']
|
||||
# set existing values to None to override any boot-time config
|
||||
occ_keys = ('heat', 'zaqar', 'cfn', 'request')
|
||||
for occ_key in occ_keys:
|
||||
if occ_key not in occ:
|
||||
continue
|
||||
existing = occ[occ_key]
|
||||
for k in existing:
|
||||
existing[k] = None
|
||||
|
||||
if self.transport_poll_server_heat(props):
|
||||
occ.update({'heat': {
|
||||
'user_id': self._get_user_id(),
|
||||
'password': self.password,
|
||||
'auth_url': self.context.auth_url,
|
||||
'project_id': self.stack.stack_user_project_id,
|
||||
'stack_id': self.stack.identifier().stack_path(),
|
||||
'resource_name': self.name}})
|
||||
if self.transport_zaqar_message():
|
||||
|
||||
elif self.transport_zaqar_message(props):
|
||||
queue_id = self.physical_resource_name()
|
||||
self.data_set('metadata_queue_id', queue_id)
|
||||
zaqar_plugin = self.client_plugin('zaqar')
|
||||
|
@ -579,22 +591,26 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin,
|
|||
self.stack.stack_user_project_id)
|
||||
queue = zaqar.queue(queue_id)
|
||||
queue.post({'body': meta, 'ttl': zaqar_plugin.DEFAULT_TTL})
|
||||
meta['os-collect-config'].update({'zaqar': {
|
||||
occ.update({'zaqar': {
|
||||
'user_id': self._get_user_id(),
|
||||
'password': self.password,
|
||||
'auth_url': self.context.auth_url,
|
||||
'project_id': self.stack.stack_user_project_id,
|
||||
'queue_id': queue_id}})
|
||||
elif self.transport_poll_server_cfn():
|
||||
meta['os-collect-config'].update({'cfn': {
|
||||
|
||||
elif self.transport_poll_server_cfn(props):
|
||||
occ.update({'cfn': {
|
||||
'metadata_url': '%s/v1/' % cfg.CONF.heat_metadata_server_url,
|
||||
'access_key_id': self.access_key,
|
||||
'secret_access_key': self.secret_key,
|
||||
'stack_name': self.stack.name,
|
||||
'path': '%s.Metadata' % self.name}})
|
||||
elif self.transport_poll_temp_url():
|
||||
|
||||
elif self.transport_poll_temp_url(props):
|
||||
container = self.physical_resource_name()
|
||||
object_name = str(uuid.uuid4())
|
||||
object_name = self.data().get('metadata_object_name')
|
||||
if not object_name:
|
||||
object_name = str(uuid.uuid4())
|
||||
|
||||
self.client('swift').put_container(container)
|
||||
|
||||
|
@ -605,10 +621,11 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin,
|
|||
self.data_set('metadata_put_url', put_url)
|
||||
self.data_set('metadata_object_name', object_name)
|
||||
|
||||
meta['os-collect-config'].update({'request': {
|
||||
occ.update({'request': {
|
||||
'metadata_url': url}})
|
||||
self.client('swift').put_object(
|
||||
container, object_name, jsonutils.dumps(meta))
|
||||
|
||||
self.metadata_set(meta)
|
||||
|
||||
def _register_access_key(self):
|
||||
|
@ -616,20 +633,20 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin,
|
|||
def access_allowed(resource_name):
|
||||
return resource_name == self.name
|
||||
|
||||
if self.transport_poll_server_cfn():
|
||||
if self.access_key is not None:
|
||||
self.stack.register_access_allowed_handler(
|
||||
self.access_key, access_allowed)
|
||||
elif self.transport_poll_server_heat():
|
||||
if self._get_user_id() is not None:
|
||||
self.stack.register_access_allowed_handler(
|
||||
self._get_user_id(), access_allowed)
|
||||
|
||||
def _create_transport_credentials(self):
|
||||
if self.transport_poll_server_cfn():
|
||||
def _create_transport_credentials(self, props):
|
||||
if self.transport_poll_server_cfn(props):
|
||||
self._create_user()
|
||||
self._create_keypair()
|
||||
|
||||
elif (self.transport_poll_server_heat() or
|
||||
self.transport_zaqar_message()):
|
||||
elif (self.transport_poll_server_heat(props) or
|
||||
self.transport_zaqar_message(props)):
|
||||
self.password = uuid.uuid4().hex
|
||||
self._create_user()
|
||||
|
||||
|
@ -661,20 +678,20 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin,
|
|||
return self.properties[
|
||||
self.USER_DATA_FORMAT] == self.SOFTWARE_CONFIG
|
||||
|
||||
def transport_poll_server_cfn(self):
|
||||
return self.properties[
|
||||
def transport_poll_server_cfn(self, props):
|
||||
return props[
|
||||
self.SOFTWARE_CONFIG_TRANSPORT] == self.POLL_SERVER_CFN
|
||||
|
||||
def transport_poll_server_heat(self):
|
||||
return self.properties[
|
||||
def transport_poll_server_heat(self, props):
|
||||
return props[
|
||||
self.SOFTWARE_CONFIG_TRANSPORT] == self.POLL_SERVER_HEAT
|
||||
|
||||
def transport_poll_temp_url(self):
|
||||
return self.properties[
|
||||
def transport_poll_temp_url(self, props):
|
||||
return props[
|
||||
self.SOFTWARE_CONFIG_TRANSPORT] == self.POLL_TEMP_URL
|
||||
|
||||
def transport_zaqar_message(self):
|
||||
return self.properties.get(
|
||||
def transport_zaqar_message(self, props):
|
||||
return props.get(
|
||||
self.SOFTWARE_CONFIG_TRANSPORT) == self.ZAQAR_MESSAGE
|
||||
|
||||
def get_software_config(self, ud_content):
|
||||
|
@ -699,8 +716,8 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin,
|
|||
metadata = self.metadata_get(True) or {}
|
||||
|
||||
if self.user_data_software_config():
|
||||
self._create_transport_credentials()
|
||||
self._populate_deployments_metadata(metadata)
|
||||
self._create_transport_credentials(self.properties)
|
||||
self._populate_deployments_metadata(metadata, self.properties)
|
||||
|
||||
userdata = self.client_plugin().build_userdata(
|
||||
metadata,
|
||||
|
@ -1079,11 +1096,41 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin,
|
|||
if self.NETWORKS in prop_diff:
|
||||
updaters.extend(self._update_networks(server, prop_diff))
|
||||
|
||||
if self.SOFTWARE_CONFIG_TRANSPORT in prop_diff:
|
||||
self._update_software_config_transport(prop_diff)
|
||||
|
||||
# NOTE(pas-ha) optimization is possible (starting first task
|
||||
# right away), but we'd rather not, as this method already might
|
||||
# have called several APIs
|
||||
return updaters
|
||||
|
||||
def _update_software_config_transport(self, prop_diff):
|
||||
if not self.user_data_software_config():
|
||||
return
|
||||
try:
|
||||
metadata = self.metadata_get(True) or {}
|
||||
self._create_transport_credentials(prop_diff)
|
||||
self._populate_deployments_metadata(metadata, prop_diff)
|
||||
# push new metadata to all sources by creating a dummy
|
||||
# deployment
|
||||
sc = self.rpc_client().create_software_config(
|
||||
self.context, 'ignored', 'ignored', '')
|
||||
sd = self.rpc_client().create_software_deployment(
|
||||
self.context, self.resource_id, sc['id'])
|
||||
self.rpc_client().delete_software_deployment(
|
||||
self.context, sd['id'])
|
||||
self.rpc_client().delete_software_config(
|
||||
self.context, sc['id'])
|
||||
except Exception as e:
|
||||
# Updating the software config transport is on a best-effort
|
||||
# basis as any raised exception here would result in the resource
|
||||
# going into an ERROR state, which will be replaced on the next
|
||||
# stack update. This is not desirable for a server. The old
|
||||
# transport will continue to work, and the new transport may work
|
||||
# despite exceptions in the above block.
|
||||
LOG.error("Error while updating software config transport")
|
||||
LOG.exception(e)
|
||||
|
||||
def check_update_complete(self, updaters):
|
||||
"""Push all updaters to completion in list order."""
|
||||
for prg in updaters:
|
||||
|
|
|
@ -103,14 +103,12 @@ class SoftwareConfigService(service.Service):
|
|||
for rd in rs.data:
|
||||
if rd.key == 'metadata_put_url':
|
||||
metadata_put_url = rd.value
|
||||
break
|
||||
elif rd.key == 'metadata_queue_id':
|
||||
if rd.key == 'metadata_queue_id':
|
||||
metadata_queue_id = rd.value
|
||||
break
|
||||
if metadata_put_url:
|
||||
json_md = jsonutils.dumps(md)
|
||||
requests.put(metadata_put_url, json_md)
|
||||
elif metadata_queue_id:
|
||||
if metadata_queue_id:
|
||||
zaqar_plugin = cnxt.clients.client_plugin('zaqar')
|
||||
zaqar = zaqar_plugin.create_for_tenant(sd.stack_user_project_id)
|
||||
queue = zaqar.queue(metadata_queue_id)
|
||||
|
|
|
@ -1569,6 +1569,8 @@ class ServersTest(common.HeatTestCase):
|
|||
ud_tmpl = self._get_test_template('update_stack')[0]
|
||||
ud_tmpl.t['Resources']['WebServer']['Metadata'] = {'test': 123}
|
||||
resource_defns = ud_tmpl.resource_definitions(server.stack)
|
||||
|
||||
self.m.ReplayAll()
|
||||
scheduler.TaskRunner(server.update, resource_defns['WebServer'])()
|
||||
self.assertEqual({'test': 123}, server.metadata_get())
|
||||
|
||||
|
@ -1578,6 +1580,7 @@ class ServersTest(common.HeatTestCase):
|
|||
self.assertEqual({'test': 123}, server.metadata_get())
|
||||
server.metadata_update()
|
||||
self.assertEqual({'test': 456}, server.metadata_get())
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_server_update_metadata_software_config(self):
|
||||
server, ud_tmpl = self._server_create_software_config(
|
||||
|
@ -1597,15 +1600,18 @@ class ServersTest(common.HeatTestCase):
|
|||
self.assertEqual(expected_md, server.metadata_get())
|
||||
|
||||
self.m.UnsetStubs()
|
||||
self._stub_glance_for_update()
|
||||
self._stub_glance_for_update(rebuild=True)
|
||||
|
||||
ud_tmpl.t['Resources']['WebServer']['Metadata'] = {'test': 123}
|
||||
resource_defns = ud_tmpl.resource_definitions(server.stack)
|
||||
|
||||
self.m.ReplayAll()
|
||||
scheduler.TaskRunner(server.update, resource_defns['WebServer'])()
|
||||
expected_md.update({'test': 123})
|
||||
self.assertEqual(expected_md, server.metadata_get())
|
||||
server.metadata_update()
|
||||
self.assertEqual(expected_md, server.metadata_get())
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_server_update_metadata_software_config_merge(self):
|
||||
md = {'os-collect-config': {'polling_interval': 10}}
|
||||
|
@ -1628,15 +1634,81 @@ class ServersTest(common.HeatTestCase):
|
|||
self.assertEqual(expected_md, server.metadata_get())
|
||||
|
||||
self.m.UnsetStubs()
|
||||
self._stub_glance_for_update()
|
||||
self._stub_glance_for_update(rebuild=True)
|
||||
|
||||
ud_tmpl.t['Resources']['WebServer']['Metadata'] = {'test': 123}
|
||||
resource_defns = ud_tmpl.resource_definitions(server.stack)
|
||||
|
||||
self.m.ReplayAll()
|
||||
scheduler.TaskRunner(server.update, resource_defns['WebServer'])()
|
||||
expected_md.update({'test': 123})
|
||||
self.assertEqual(expected_md, server.metadata_get())
|
||||
server.metadata_update()
|
||||
self.assertEqual(expected_md, server.metadata_get())
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_server_update_software_config_transport(self):
|
||||
md = {'os-collect-config': {'polling_interval': 10}}
|
||||
server = self._server_create_software_config(
|
||||
stack_name='update_meta_sc', md=md)
|
||||
|
||||
expected_md = {
|
||||
'os-collect-config': {
|
||||
'cfn': {
|
||||
'access_key_id': '4567',
|
||||
'metadata_url': '/v1/',
|
||||
'path': 'WebServer.Metadata',
|
||||
'secret_access_key': '8901',
|
||||
'stack_name': 'update_meta_sc'
|
||||
},
|
||||
'polling_interval': 10
|
||||
},
|
||||
'deployments': []}
|
||||
self.assertEqual(expected_md, server.metadata_get())
|
||||
|
||||
self.m.UnsetStubs()
|
||||
self._stub_glance_for_update(rebuild=True)
|
||||
self.m.StubOutWithMock(swift.SwiftClientPlugin, '_create')
|
||||
|
||||
sc = mock.Mock()
|
||||
sc.head_account.return_value = {
|
||||
'x-account-meta-temp-url-key': 'secrit'
|
||||
}
|
||||
sc.url = 'http://192.0.2.2'
|
||||
|
||||
swift.SwiftClientPlugin._create().AndReturn(sc)
|
||||
|
||||
update_template = copy.deepcopy(server.t)
|
||||
update_template['Properties'][
|
||||
'software_config_transport'] = 'POLL_TEMP_URL'
|
||||
|
||||
self.m.ReplayAll()
|
||||
scheduler.TaskRunner(server.update, update_template)()
|
||||
self.assertEqual((server.UPDATE, server.COMPLETE), server.state)
|
||||
|
||||
md = server.metadata_get()
|
||||
metadata_url = md['os-collect-config']['request']['metadata_url']
|
||||
self.assertTrue(metadata_url.startswith(
|
||||
'http://192.0.2.2/v1/AUTH_test_tenant_id/'))
|
||||
|
||||
expected_md = {
|
||||
'os-collect-config': {
|
||||
'cfn': {
|
||||
'access_key_id': None,
|
||||
'metadata_url': None,
|
||||
'path': None,
|
||||
'secret_access_key': None,
|
||||
'stack_name': None
|
||||
},
|
||||
'request': {
|
||||
'metadata_url': 'the_url',
|
||||
},
|
||||
'polling_interval': 10
|
||||
},
|
||||
'deployments': []}
|
||||
md['os-collect-config']['request']['metadata_url'] = 'the_url'
|
||||
self.assertDictEqual(expected_md, server.metadata_get())
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_server_update_nova_metadata(self):
|
||||
return_server = self.fc.servers.list()[1]
|
||||
|
|
Loading…
Reference in New Issue