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:
Steve Baker 2015-11-30 09:30:11 +13:00
parent 6a186c0f29
commit 73c41fe8a8
3 changed files with 149 additions and 32 deletions

View File

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

View File

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

View File

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