diff --git a/heat/engine/resources/openstack/nova/server.py b/heat/engine/resources/openstack/nova/server.py index c09365438d..8c95a47d4d 100644 --- a/heat/engine/resources/openstack/nova/server.py +++ b/heat/engine/resources/openstack/nova/server.py @@ -12,17 +12,14 @@ # under the License. import copy -import uuid from oslo_config import cfg from oslo_log import log as logging -from oslo_serialization import jsonutils from oslo_utils import uuidutils import six from heat.common import exception from heat.common.i18n import _ -from heat.common.i18n import _LE from heat.engine import attributes from heat.engine.clients import progress from heat.engine import constraints @@ -32,19 +29,18 @@ from heat.engine.resources.openstack.neutron import port as neutron_port from heat.engine.resources.openstack.neutron import subnet from heat.engine.resources.openstack.nova import server_network_mixin from heat.engine.resources import scheduler_hints as sh -from heat.engine.resources import stack_user +from heat.engine.resources import server_base from heat.engine import support from heat.engine import translation from heat.rpc import api as rpc_api cfg.CONF.import_opt('default_software_config_transport', 'heat.common.config') cfg.CONF.import_opt('default_user_data_format', 'heat.common.config') -cfg.CONF.import_opt('max_server_name_length', 'heat.common.config') LOG = logging.getLogger(__name__) -class Server(stack_user.StackUser, sh.SchedulerHintsMixin, +class Server(server_base.BaseServer, sh.SchedulerHintsMixin, server_network_mixin.ServerNetworkMixin): """A resource for managing Nova instances. @@ -593,12 +589,8 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin, ) } - physical_resource_name_limit = cfg.CONF.max_server_name_length - default_client_name = 'nova' - entity = 'servers' - def translation_rules(self, props): rules = [ translation.TranslationRule( @@ -670,145 +662,10 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin, if self.user_data_software_config(): self._register_access_key() - def _server_name(self): - name = self.properties[self.NAME] - if name: - return name - - return self.physical_resource_name() - def _config_drive(self): # This method is overridden by the derived CloudServer resource return self.properties[self.CONFIG_DRIVE] - def _populate_deployments_metadata(self, meta, props): - meta['deployments'] = meta.get('deployments', []) - meta['os-collect-config'] = meta.get('os-collect-config', {}) - occ = meta['os-collect-config'] - collectors = ['ec2'] - occ['collectors'] = collectors - - # 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}}) - collectors.append('heat') - - elif self.transport_zaqar_message(props): - queue_id = self.physical_resource_name() - self.data_set('metadata_queue_id', queue_id) - 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}}) - collectors.append('zaqar') - - elif self.transport_poll_server_cfn(props): - heat_client_plugin = self.stack.clients.client_plugin('heat') - config_url = heat_client_plugin.get_cfn_metadata_server_url() - occ.update({'cfn': { - 'metadata_url': config_url, - 'access_key_id': self.access_key, - 'secret_access_key': self.secret_key, - 'stack_name': self.stack.name, - 'path': '%s.Metadata' % self.name}}) - collectors.append('cfn') - - elif self.transport_poll_temp_url(props): - container = self.physical_resource_name() - object_name = self.data().get('metadata_object_name') - if not object_name: - object_name = str(uuid.uuid4()) - - self.client('swift').put_container(container) - - url = self.client_plugin('swift').get_temp_url( - container, object_name, method='GET') - put_url = self.client_plugin('swift').get_temp_url( - container, object_name) - self.data_set('metadata_put_url', put_url) - self.data_set('metadata_object_name', object_name) - - collectors.append('request') - occ.update({'request': { - 'metadata_url': url}}) - - collectors.append('local') - self.metadata_set(meta) - - # push replacement polling config to any existing push-based sources - queue_id = self.data().get('metadata_queue_id') - if queue_id: - zaqar_plugin = self.client_plugin('zaqar') - zaqar = zaqar_plugin.create_for_tenant( - self.stack.stack_user_project_id, self._user_token()) - queue = zaqar.queue(queue_id) - queue.post({'body': meta, 'ttl': zaqar_plugin.DEFAULT_TTL}) - - object_name = self.data().get('metadata_object_name') - if object_name: - container = self.physical_resource_name() - self.client('swift').put_object( - container, object_name, jsonutils.dumps(meta)) - - def _register_access_key(self): - """Access is limited to this resource, which created the keypair.""" - def access_allowed(resource_name): - return resource_name == self.name - - if self.access_key is not None: - self.stack.register_access_allowed_handler( - self.access_key, access_allowed) - 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, props): - if self.transport_poll_server_cfn(props): - self._create_user() - self._create_keypair() - - elif (self.transport_poll_server_heat(props) or - self.transport_zaqar_message(props)): - self.password = uuid.uuid4().hex - self._create_user() - - self._register_access_key() - - @property - def access_key(self): - return self.data().get('access_key') - - @property - def secret_key(self): - return self.data().get('secret_key') - - @property - def password(self): - return self.data().get('password') - - @password.setter - def password(self, password): - if password is None: - self.data_delete('password') - else: - self.data_set('password', password, True) - def user_data_raw(self): return self.properties[self.USER_DATA_FORMAT] == self.RAW @@ -816,22 +673,6 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin, return self.properties[ self.USER_DATA_FORMAT] == self.SOFTWARE_CONFIG - def transport_poll_server_cfn(self, props): - return props[ - self.SOFTWARE_CONFIG_TRANSPORT] == self.POLL_SERVER_CFN - - def transport_poll_server_heat(self, props): - return props[ - self.SOFTWARE_CONFIG_TRANSPORT] == self.POLL_SERVER_HEAT - - def transport_poll_temp_url(self, props): - return props[ - self.SOFTWARE_CONFIG_TRANSPORT] == self.POLL_TEMP_URL - - def transport_zaqar_message(self, props): - return props[ - self.SOFTWARE_CONFIG_TRANSPORT] == self.ZAQAR_MESSAGE - def get_software_config(self, ud_content): try: sc = self.rpc_client().show_software_config( @@ -1299,30 +1140,12 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin, return ud_update_policy == 'REPLACE' def handle_update(self, json_snippet, tmpl_diff, prop_diff): - if tmpl_diff.metadata_changed(): - # If SOFTWARE_CONFIG user_data_format is enabled we require - # the "deployments" and "os-collect-config" keys for Deployment - # polling. We can attempt to merge the occ data, but any - # metadata update containing deployments will be discarded. - new_md = json_snippet.metadata() - if self.user_data_software_config(): - metadata = self.metadata_get(True) or {} - new_occ_md = new_md.get('os-collect-config', {}) - occ_md = metadata.get('os-collect-config', {}) - occ_md.update(new_occ_md) - new_md['os-collect-config'] = occ_md - deployment_md = metadata.get('deployments', []) - new_md['deployments'] = deployment_md - self.metadata_set(new_md) - - updaters = [] + updaters = super(Server, self).handle_update( + json_snippet, + tmpl_diff, + prop_diff) server = None - if self.METADATA in prop_diff: - server = self.client_plugin().get_server(self.resource_id) - self.client_plugin().meta_update(server, - prop_diff[self.METADATA]) - if self.TAGS in prop_diff: self._update_server_tags(prop_diff[self.TAGS] or []) @@ -1344,42 +1167,11 @@ 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: - # 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.exception( - _LE('Error while updating software config transport') - ) - def check_update_complete(self, updaters): """Push all updaters to completion in list order.""" for prg in updaters: @@ -1398,28 +1190,6 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin, self.store_external_ports() return status - def metadata_update(self, new_metadata=None): - """Refresh the metadata if new_metadata is None.""" - if new_metadata is None: - # Re-resolve the template metadata and merge it with the - # current resource metadata. This is necessary because the - # attributes referenced in the template metadata may change - # and the resource itself adds keys to the metadata which - # are not specified in the template (e.g the deployments data) - meta = self.metadata_get(refresh=True) or {} - tmpl_meta = self.t.metadata() - meta.update(tmpl_meta) - self.metadata_set(meta) - - @staticmethod - def _check_maximum(count, maximum, msg): - """Check a count against a maximum. - - Unless maximum is -1 which indicates that there is no limit. - """ - if maximum != -1 and count > maximum: - raise exception.StackValidationFailed(message=msg) - def _validate_block_device_mapping(self): # either volume_id or snapshot_id needs to be specified, but not both @@ -1593,29 +1363,6 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin, ) if contents is not None else 0, limits['maxPersonalitySize'], msg) - def _delete_temp_url(self): - object_name = self.data().get('metadata_object_name') - if not object_name: - return - with self.client_plugin('swift').ignore_not_found: - container = self.physical_resource_name() - swift = self.client('swift') - swift.delete_object(container, object_name) - headers = swift.head_container(container) - if int(headers['x-container-object-count']) == 0: - swift.delete_container(container) - - def _delete_queue(self): - queue_id = self.data().get('metadata_queue_id') - if not queue_id: - return - client_plugin = self.client_plugin('zaqar') - zaqar = client_plugin.create_for_tenant( - self.stack.stack_user_project_id, self._user_token()) - with client_plugin.ignore_not_found: - zaqar.queue(queue_id).delete() - self.data_delete('metadata_queue_id') - def _delete(self): if self.user_data_software_config(): self._delete_queue() diff --git a/heat/engine/resources/server_base.py b/heat/engine/resources/server_base.py new file mode 100644 index 0000000000..4f34750933 --- /dev/null +++ b/heat/engine/resources/server_base.py @@ -0,0 +1,309 @@ +# +# 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. + +import uuid + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_serialization import jsonutils + +from heat.common import exception +from heat.common.i18n import _LE +from heat.engine.clients import progress +from heat.engine.resources import stack_user + +cfg.CONF.import_opt('max_server_name_length', 'heat.common.config') + +LOG = logging.getLogger(__name__) + + +class BaseServer(stack_user.StackUser): + """Base Server resource.""" + + physical_resource_name_limit = cfg.CONF.max_server_name_length + + entity = 'servers' + + def __init__(self, name, json_snippet, stack): + super(BaseServer, self).__init__(name, json_snippet, stack) + + def _server_name(self): + name = self.properties[self.NAME] + if name: + return name + + return self.physical_resource_name() + + def _populate_deployments_metadata(self, meta, props): + meta['deployments'] = meta.get('deployments', []) + meta['os-collect-config'] = meta.get('os-collect-config', {}) + occ = meta['os-collect-config'] + collectors = ['ec2'] + occ['collectors'] = collectors + + # 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}}) + collectors.append('heat') + + elif self.transport_zaqar_message(props): + queue_id = self.physical_resource_name() + self.data_set('metadata_queue_id', queue_id) + 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}}) + collectors.append('zaqar') + + elif self.transport_poll_server_cfn(props): + heat_client_plugin = self.stack.clients.client_plugin('heat') + config_url = heat_client_plugin.get_cfn_metadata_server_url() + occ.update({'cfn': { + 'metadata_url': config_url, + 'access_key_id': self.access_key, + 'secret_access_key': self.secret_key, + 'stack_name': self.stack.name, + 'path': '%s.Metadata' % self.name}}) + collectors.append('cfn') + + elif self.transport_poll_temp_url(props): + container = self.physical_resource_name() + object_name = self.data().get('metadata_object_name') + if not object_name: + object_name = str(uuid.uuid4()) + + self.client('swift').put_container(container) + + url = self.client_plugin('swift').get_temp_url( + container, object_name, method='GET') + put_url = self.client_plugin('swift').get_temp_url( + container, object_name) + self.data_set('metadata_put_url', put_url) + self.data_set('metadata_object_name', object_name) + + collectors.append('request') + occ.update({'request': { + 'metadata_url': url}}) + + collectors.append('local') + self.metadata_set(meta) + + # push replacement polling config to any existing push-based sources + queue_id = self.data().get('metadata_queue_id') + if queue_id: + zaqar_plugin = self.client_plugin('zaqar') + zaqar = zaqar_plugin.create_for_tenant( + self.stack.stack_user_project_id, self._user_token()) + queue = zaqar.queue(queue_id) + queue.post({'body': meta, 'ttl': zaqar_plugin.DEFAULT_TTL}) + + object_name = self.data().get('metadata_object_name') + if object_name: + container = self.physical_resource_name() + self.client('swift').put_object( + container, object_name, jsonutils.dumps(meta)) + + 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(props) or + self.transport_zaqar_message(props)): + self.password = uuid.uuid4().hex + self._create_user() + + self._register_access_key() + + @property + def access_key(self): + return self.data().get('access_key') + + @property + def secret_key(self): + return self.data().get('secret_key') + + @property + def password(self): + return self.data().get('password') + + @password.setter + def password(self, password): + if password is None: + self.data_delete('password') + else: + self.data_set('password', password, True) + + def transport_poll_server_cfn(self, props): + return props[ + self.SOFTWARE_CONFIG_TRANSPORT] == self.POLL_SERVER_CFN + + def transport_poll_server_heat(self, props): + return props[ + self.SOFTWARE_CONFIG_TRANSPORT] == self.POLL_SERVER_HEAT + + def transport_poll_temp_url(self, props): + return props[ + self.SOFTWARE_CONFIG_TRANSPORT] == self.POLL_TEMP_URL + + def transport_zaqar_message(self, props): + return props[ + self.SOFTWARE_CONFIG_TRANSPORT] == self.ZAQAR_MESSAGE + + def check_create_complete(self, server_id): + return True + + def _resolve_attribute(self, name): + if self.resource_id is None: + return + if name == self.NAME_ATTR: + return self._server_name() + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if tmpl_diff.metadata_changed(): + # If SOFTWARE_CONFIG user_data_format is enabled we require + # the "deployments" and "os-collect-config" keys for Deployment + # polling. We can attempt to merge the occ data, but any + # metadata update containing deployments will be discarded. + new_md = json_snippet.metadata() + if self.user_data_software_config(): + metadata = self.metadata_get(True) or {} + new_occ_md = new_md.get('os-collect-config', {}) + occ_md = metadata.get('os-collect-config', {}) + occ_md.update(new_occ_md) + new_md['os-collect-config'] = occ_md + deployment_md = metadata.get('deployments', []) + new_md['deployments'] = deployment_md + self.metadata_set(new_md) + + updaters = [] + server = None + + if self.METADATA in prop_diff: + server = self.client_plugin().get_server(self.resource_id) + self.client_plugin().meta_update(server, + prop_diff[self.METADATA]) + + 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: + # 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.exception( + _LE('Error while updating software config transport') + ) + + def metadata_update(self, new_metadata=None): + """Refresh the metadata if new_metadata is None.""" + if new_metadata is None: + # Re-resolve the template metadata and merge it with the + # current resource metadata. This is necessary because the + # attributes referenced in the template metadata may change + # and the resource itself adds keys to the metadata which + # are not specified in the template (e.g the deployments data) + meta = self.metadata_get(refresh=True) or {} + tmpl_meta = self.t.metadata() + meta.update(tmpl_meta) + self.metadata_set(meta) + + @staticmethod + def _check_maximum(count, maximum, msg): + """Check a count against a maximum. + + Unless maximum is -1 which indicates that there is no limit. + """ + if maximum != -1 and count > maximum: + raise exception.StackValidationFailed(message=msg) + + def _delete_temp_url(self): + object_name = self.data().get('metadata_object_name') + if not object_name: + return + with self.client_plugin('swift').ignore_not_found: + container = self.physical_resource_name() + swift = self.client('swift') + swift.delete_object(container, object_name) + headers = swift.head_container(container) + if int(headers['x-container-object-count']) == 0: + swift.delete_container(container) + + def _delete_queue(self): + queue_id = self.data().get('metadata_queue_id') + if not queue_id: + return + client_plugin = self.client_plugin('zaqar') + zaqar = client_plugin.create_for_tenant( + self.stack.stack_user_project_id, self._user_token()) + with client_plugin.ignore_not_found: + zaqar.queue(queue_id).delete() + self.data_delete('metadata_queue_id') + + def handle_snapshot_delete(self, state): + + if state[1] != self.FAILED and self.resource_id: + image_id = self.client().servers.create_image( + self.resource_id, self.physical_resource_name()) + return progress.ServerDeleteProgress( + self.resource_id, image_id, False) + return self._delete() + + def handle_delete(self): + + return self._delete() + + def check_delete_complete(self, prg): + if not prg: + return True diff --git a/heat/engine/resources/stack_user.py b/heat/engine/resources/stack_user.py index 853b1db13d..cb57d9d585 100644 --- a/heat/engine/resources/stack_user.py +++ b/heat/engine/resources/stack_user.py @@ -165,3 +165,15 @@ class StackUser(resource.Resource): for data_key in ('access_key', 'secret_key', 'credential_id'): self.data_delete(data_key) + + def _register_access_key(self): + """Access is limited to this resource, which created the keypair.""" + def access_allowed(resource_name): + return resource_name == self.name + + if self.access_key is not None: + self.stack.register_access_allowed_handler( + self.access_key, access_allowed) + if self._get_user_id() is not None: + self.stack.register_access_allowed_handler( + self._get_user_id(), access_allowed)