diff --git a/contrib/rackspace/rackspace/resources/cloud_server.py b/contrib/rackspace/rackspace/resources/cloud_server.py index 5f99317f9..8ed0d8dd9 100644 --- a/contrib/rackspace/rackspace/resources/cloud_server.py +++ b/contrib/rackspace/rackspace/resources/cloud_server.py @@ -16,7 +16,6 @@ import copy from heat.common import exception from heat.engine import attributes from heat.engine import properties -from heat.engine.resources import nova_utils from heat.engine.resources import server from heat.openstack.common.gettextutils import _ from heat.openstack.common import log as logging @@ -200,7 +199,7 @@ class CloudServer(server.Server): if not self._check_active(server): return False - nova_utils.refresh_server(server) + self.client_plugin().refresh_server(server) if 'rack_connect' in self.context.roles and not \ self._check_rack_connect_complete(server): @@ -216,7 +215,7 @@ class CloudServer(server.Server): if name == self.DISTRO: return self.distro if name == self.PRIVATE_IP_V4: - return nova_utils.get_ip(self.server, 'private', 4) + return self.client_plugin().get_ip(self.server, 'private', 4) if name == self.ADMIN_PASS_ATTR: return self.data().get(self.ADMIN_PASS_ATTR, '') return super(CloudServer, self)._resolve_attribute(name) diff --git a/heat/engine/clients/os/nova.py b/heat/engine/clients/os/nova.py index 60dbe11ec..22aa78ff6 100644 --- a/heat/engine/clients/os/nova.py +++ b/heat/engine/clients/os/nova.py @@ -11,15 +11,42 @@ # License for the specific language governing permissions and limitations # under the License. +import email +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import json +import logging +import os +import pkgutil +import string + from novaclient import client as nc from novaclient import exceptions from novaclient import shell as novashell +from oslo.config import cfg +import six +from six.moves.urllib import parse as urlparse +from heat.common import exception from heat.engine.clients import client_plugin +from heat.engine import scheduler + +LOG = logging.getLogger(__name__) class NovaClientPlugin(client_plugin.ClientPlugin): + deferred_server_statuses = ['BUILD', + 'HARD_REBOOT', + 'PASSWORD', + 'REBOOT', + 'RESCUE', + 'RESIZE', + 'REVERT_RESIZE', + 'SHUTOFF', + 'SUSPENDED', + 'VERIFY_RESIZE'] + exceptions_module = exceptions def _create(self): @@ -58,3 +85,307 @@ class NovaClientPlugin(client_plugin.ClientPlugin): def is_bad_request(self, ex): return isinstance(ex, exceptions.BadRequest) + + def refresh_server(self, server): + ''' + Refresh server's attributes and log warnings for non-critical + API errors. + ''' + try: + server.get() + except exceptions.OverLimit as exc: + msg = _("Server %(name)s (%(id)s) received an OverLimit " + "response during server.get(): %(exception)s") + LOG.warning(msg % {'name': server.name, + 'id': server.id, + 'exception': exc}) + except exceptions.ClientException as exc: + if ((getattr(exc, 'http_status', getattr(exc, 'code', None)) in + (500, 503))): + msg = _('Server "%(name)s" (%(id)s) received the following ' + 'exception during server.get(): %(exception)s') + LOG.warning(msg % {'name': server.name, + 'id': server.id, + 'exception': exc}) + else: + raise + + def get_ip(self, server, net_type, ip_version): + """Return the server's IP of the given type and version.""" + if net_type in server.addresses: + for ip in server.addresses[net_type]: + if ip['version'] == ip_version: + return ip['addr'] + + def get_flavor_id(self, flavor): + ''' + Get the id for the specified flavor name. + If the specified value is flavor id, just return it. + + :param flavor: the name of the flavor to find + :returns: the id of :flavor: + :raises: exception.FlavorMissing + ''' + flavor_id = None + flavor_list = self.client().flavors.list() + for o in flavor_list: + if o.name == flavor: + flavor_id = o.id + break + if o.id == flavor: + flavor_id = o.id + break + if flavor_id is None: + raise exception.FlavorMissing(flavor_id=flavor) + return flavor_id + + def get_keypair(self, key_name): + ''' + Get the public key specified by :key_name: + + :param key_name: the name of the key to look for + :returns: the keypair (name, public_key) for :key_name: + :raises: exception.UserKeyPairMissing + ''' + try: + return self.client().keypairs.get(key_name) + except exceptions.NotFound: + raise exception.UserKeyPairMissing(key_name=key_name) + + def build_userdata(self, metadata, userdata=None, instance_user=None, + user_data_format='HEAT_CFNTOOLS'): + ''' + Build multipart data blob for CloudInit which includes user-supplied + Metadata, user data, and the required Heat in-instance configuration. + + :param resource: the resource implementation + :type resource: heat.engine.Resource + :param userdata: user data string + :type userdata: str or None + :param instance_user: the user to create on the server + :type instance_user: string + :param user_data_format: Format of user data to return + :type user_data_format: string + :returns: multipart mime as a string + ''' + + if user_data_format == 'RAW': + return userdata + + is_cfntools = user_data_format == 'HEAT_CFNTOOLS' + is_software_config = user_data_format == 'SOFTWARE_CONFIG' + + def make_subpart(content, filename, subtype=None): + if subtype is None: + subtype = os.path.splitext(filename)[0] + msg = MIMEText(content, _subtype=subtype) + msg.add_header('Content-Disposition', 'attachment', + filename=filename) + return msg + + def read_cloudinit_file(fn): + return pkgutil.get_data('heat', 'cloudinit/%s' % fn) + + if instance_user: + config_custom_user = 'user: %s' % instance_user + # FIXME(shadower): compatibility workaround for cloud-init 0.6.3. + # We can drop this once we stop supporting 0.6.3 (which ships + # with Ubuntu 12.04 LTS). + # + # See bug https://bugs.launchpad.net/heat/+bug/1257410 + boothook_custom_user = r"""useradd -m %s +echo -e '%s\tALL=(ALL)\tNOPASSWD: ALL' >> /etc/sudoers +""" % (instance_user, instance_user) + else: + config_custom_user = '' + boothook_custom_user = '' + + cloudinit_config = string.Template( + read_cloudinit_file('config')).safe_substitute( + add_custom_user=config_custom_user) + cloudinit_boothook = string.Template( + read_cloudinit_file('boothook.sh')).safe_substitute( + add_custom_user=boothook_custom_user) + + attachments = [(cloudinit_config, 'cloud-config'), + (cloudinit_boothook, 'boothook.sh', 'cloud-boothook'), + (read_cloudinit_file('part_handler.py'), + 'part-handler.py')] + + if is_cfntools: + attachments.append((userdata, 'cfn-userdata', 'x-cfninitdata')) + elif is_software_config: + # attempt to parse userdata as a multipart message, and if it + # is, add each part as an attachment + userdata_parts = None + try: + userdata_parts = email.message_from_string(userdata) + except Exception: + pass + if userdata_parts and userdata_parts.is_multipart(): + for part in userdata_parts.get_payload(): + attachments.append((part.get_payload(), + part.get_filename(), + part.get_content_subtype())) + else: + attachments.append((userdata, 'userdata', 'x-shellscript')) + + if is_cfntools: + attachments.append((read_cloudinit_file('loguserdata.py'), + 'loguserdata.py', 'x-shellscript')) + + if metadata: + attachments.append((json.dumps(metadata), + 'cfn-init-data', 'x-cfninitdata')) + + attachments.append((cfg.CONF.heat_watch_server_url, + 'cfn-watch-server', 'x-cfninitdata')) + + if is_cfntools: + attachments.append((cfg.CONF.heat_metadata_server_url, + 'cfn-metadata-server', 'x-cfninitdata')) + + # Create a boto config which the cfntools on the host use to know + # where the cfn and cw API's are to be accessed + cfn_url = urlparse.urlparse(cfg.CONF.heat_metadata_server_url) + cw_url = urlparse.urlparse(cfg.CONF.heat_watch_server_url) + is_secure = cfg.CONF.instance_connection_is_secure + vcerts = cfg.CONF.instance_connection_https_validate_certificates + boto_cfg = "\n".join(["[Boto]", + "debug = 0", + "is_secure = %s" % is_secure, + "https_validate_certificates = %s" % vcerts, + "cfn_region_name = heat", + "cfn_region_endpoint = %s" % + cfn_url.hostname, + "cloudwatch_region_name = heat", + "cloudwatch_region_endpoint = %s" % + cw_url.hostname]) + attachments.append((boto_cfg, + 'cfn-boto-cfg', 'x-cfninitdata')) + + subparts = [make_subpart(*args) for args in attachments] + mime_blob = MIMEMultipart(_subparts=subparts) + + return mime_blob.as_string() + + def delete_server(self, server): + ''' + Deletes a server and waits for it to disappear from Nova. + ''' + if not server: + return + try: + server.delete() + except Exception as exc: + self.ignore_not_found(exc) + return + + while True: + yield + + try: + self.refresh_server(server) + except Exception as exc: + self.ignore_not_found(exc) + break + else: + # Some clouds append extra (STATUS) strings to the status + short_server_status = server.status.split('(')[0] + if short_server_status == "DELETED": + break + if short_server_status == "ERROR": + fault = getattr(server, 'fault', {}) + message = fault.get('message', 'Unknown') + code = fault.get('code') + errmsg = (_("Server %(name)s delete failed: (%(code)s) " + "%(message)s")) + raise exception.Error(errmsg % {"name": server.name, + "code": code, + "message": message}) + + @scheduler.wrappertask + def resize(self, server, flavor, flavor_id): + """Resize the server and then call check_resize task to verify.""" + server.resize(flavor_id) + yield self.check_resize(server, flavor, flavor_id) + + def rename(self, server, name): + """Update the name for a server.""" + server.update(name) + + def check_resize(self, server, flavor, flavor_id): + """ + Verify that a resizing server is properly resized. + If that's the case, confirm the resize, if not raise an error. + """ + self.refresh_server(server) + while server.status == 'RESIZE': + yield + self.refresh_server(server) + if server.status == 'VERIFY_RESIZE': + server.confirm_resize() + else: + raise exception.Error( + _("Resizing to '%(flavor)s' failed, status '%(status)s'") % + dict(flavor=flavor, status=server.status)) + + @scheduler.wrappertask + def rebuild(self, server, image_id, preserve_ephemeral=False): + """Rebuild the server and call check_rebuild to verify.""" + server.rebuild(image_id, preserve_ephemeral=preserve_ephemeral) + yield self.check_rebuild(server, image_id) + + def check_rebuild(self, server, image_id): + """ + Verify that a rebuilding server is rebuilt. + Raise error if it ends up in an ERROR state. + """ + self.refresh_server(server) + while server.status == 'REBUILD': + yield + self.refresh_server(server) + if server.status == 'ERROR': + raise exception.Error( + _("Rebuilding server failed, status '%s'") % server.status) + + def meta_serialize(self, metadata): + """ + Serialize non-string metadata values before sending them to + Nova. + """ + return dict((key, (value if isinstance(value, + six.string_types) + else json.dumps(value)) + ) for (key, value) in metadata.items()) + + def meta_update(self, server, metadata): + """Delete/Add the metadata in nova as needed.""" + metadata = self.meta_serialize(metadata) + current_md = server.metadata + to_del = [key for key in current_md.keys() if key not in metadata] + client = self.client() + if len(to_del) > 0: + client.servers.delete_meta(server, to_del) + + client.servers.set_meta(server, metadata) + + def server_to_ipaddress(self, server): + ''' + Return the server's IP address, fetching it from Nova. + ''' + try: + server = self.client().servers.get(server) + except exceptions.NotFound as ex: + LOG.warn(_('Instance (%(server)s) not found: %(ex)s') + % {'server': server, 'ex': ex}) + else: + for n in server.networks: + if len(server.networks[n]) > 0: + return server.networks[n][0] + + def absolute_limits(self): + """Return the absolute limits as a dictionary.""" + limits = self.client().limits.get() + return dict([(limit.name, limit.value) + for limit in list(limits.absolute)]) diff --git a/heat/engine/clients/os/trove.py b/heat/engine/clients/os/trove.py index 850f44b19..7ee1fdd86 100644 --- a/heat/engine/clients/os/trove.py +++ b/heat/engine/clients/os/trove.py @@ -14,6 +14,7 @@ from troveclient import client as tc from troveclient.openstack.common.apiclient import exceptions +from heat.common import exception from heat.engine.clients import client_plugin @@ -49,3 +50,25 @@ class TroveClientPlugin(client_plugin.ClientPlugin): def is_over_limit(self, ex): return isinstance(ex, exceptions.RequestEntityTooLarge) + + def get_flavor_id(self, flavor): + ''' + Get the id for the specified flavor name. + If the specified value is flavor id, just return it. + + :param flavor: the name of the flavor to find + :returns: the id of :flavor: + :raises: exception.FlavorMissing + ''' + flavor_id = None + flavor_list = self.client().flavors.list() + for o in flavor_list: + if o.name == flavor: + flavor_id = o.id + break + if o.id == flavor: + flavor_id = o.id + break + if flavor_id is None: + raise exception.FlavorMissing(flavor_id=flavor) + return flavor_id diff --git a/heat/engine/resources/instance.py b/heat/engine/resources/instance.py index fe10ff61a..4701a9f7e 100644 --- a/heat/engine/resources/instance.py +++ b/heat/engine/resources/instance.py @@ -23,7 +23,6 @@ from heat.engine import properties from heat.engine import resource from heat.engine.resources.network_interface import NetworkInterface from heat.engine.resources.neutron import neutron -from heat.engine.resources import nova_utils from heat.engine.resources import volume from heat.engine import scheduler from heat.engine import signal_responder @@ -432,8 +431,8 @@ class Instance(resource.Resource): Return the server's IP address, fetching it from Nova if necessary ''' if self.ipaddress is None: - self.ipaddress = nova_utils.server_to_ipaddress( - self.nova(), self.resource_id) + self.ipaddress = self.client_plugin().server_to_ipaddress( + self.resource_id) return self.ipaddress or '0.0.0.0' @@ -547,7 +546,7 @@ class Instance(resource.Resource): image_id = self.client_plugin('glance').get_image_id(image_name) - flavor_id = nova_utils.get_flavor_id(self.nova(), flavor) + flavor_id = self.client_plugin().get_flavor_id(flavor) scheduler_hints = {} if self.properties[self.NOVA_SCHEDULER_HINTS]: @@ -587,8 +586,8 @@ class Instance(resource.Resource): flavor=flavor_id, key_name=self.properties[self.KEY_NAME], security_groups=security_groups, - userdata=nova_utils.build_userdata(self, userdata, - instance_user), + userdata=self.client_plugin().build_userdata( + self.metadata_get(), userdata, instance_user), meta=self._get_nova_metadata(self.properties), scheduler_hints=scheduler_hints, nics=nics, @@ -624,15 +623,16 @@ class Instance(resource.Resource): return volume_attach_task.step() def _check_active(self, server): + cp = self.client_plugin() if server.status != 'ACTIVE': - nova_utils.refresh_server(server) + cp.refresh_server(server) if server.status == 'ACTIVE': return True # Some clouds append extra (STATUS) strings to the status short_server_status = server.status.split('(')[0] - if short_server_status in nova_utils.deferred_server_statuses: + if short_server_status in cp.deferred_server_statuses: return False if server.status == 'ERROR': @@ -671,17 +671,16 @@ class Instance(resource.Resource): server = None if self.TAGS in prop_diff: server = self.nova().servers.get(self.resource_id) - nova_utils.meta_update(self.nova(), - server, - self._get_nova_metadata(prop_diff)) + self.client_plugin().meta_update( + server, self._get_nova_metadata(prop_diff)) if self.INSTANCE_TYPE in prop_diff: flavor = prop_diff[self.INSTANCE_TYPE] - flavor_id = nova_utils.get_flavor_id(self.nova(), flavor) + flavor_id = self.client_plugin().get_flavor_id(flavor) if not server: server = self.nova().servers.get(self.resource_id) - checker = scheduler.TaskRunner(nova_utils.resize, server, flavor, - flavor_id) + checker = scheduler.TaskRunner(self.client_plugin().resize, + server, flavor, flavor_id) checkers.append(checker) if self.NETWORK_INTERFACES in prop_diff: new_network_ifaces = prop_diff.get(self.NETWORK_INTERFACES) @@ -815,11 +814,11 @@ class Instance(resource.Resource): server = self.nova().servers.get(self.resource_id) except Exception as e: self.client_plugin().ignore_not_found(e) - self.resource_id_set(None) return deleters = ( scheduler.TaskRunner(self._detach_volumes_task()), - scheduler.TaskRunner(nova_utils.delete_server, server)) + scheduler.TaskRunner(self.client_plugin().delete_server, + server)) deleters[0].start() return deleters @@ -872,11 +871,12 @@ class Instance(resource.Resource): if server.status == 'SUSPENDED': return True - nova_utils.refresh_server(server) + cp = self.client_plugin() + cp.refresh_server(server) LOG.debug("%(name)s check_suspend_complete " "status = %(status)s", {'name': self.name, 'status': server.status}) - if server.status in list(nova_utils.deferred_server_statuses + + if server.status in list(cp.deferred_server_statuses + ['ACTIVE']): return server.status == 'SUSPENDED' else: diff --git a/heat/engine/resources/loadbalancer.py b/heat/engine/resources/loadbalancer.py index 2b849df38..e211c5207 100644 --- a/heat/engine/resources/loadbalancer.py +++ b/heat/engine/resources/loadbalancer.py @@ -19,7 +19,6 @@ from heat.common import template_format from heat.engine import attributes from heat.engine import constraints from heat.engine import properties -from heat.engine.resources import nova_utils from heat.engine import stack_resource from heat.openstack.common.gettextutils import _ from heat.openstack.common import log as logging @@ -422,9 +421,9 @@ class LoadBalancer(stack_resource.StackResource): servers = [] n = 1 - client = self.nova() + nova_cp = self.client_plugin('nova') for i in instances: - ip = nova_utils.server_to_ipaddress(client, i) or '0.0.0.0' + ip = nova_cp.server_to_ipaddress(i) or '0.0.0.0' LOG.debug('haproxy server:%s' % ip) servers.append('%sserver server%d %s:%s %s' % (spaces, n, ip, inst_port, diff --git a/heat/engine/resources/neutron/loadbalancer.py b/heat/engine/resources/neutron/loadbalancer.py index 8c4e1236b..faefa278a 100644 --- a/heat/engine/resources/neutron/loadbalancer.py +++ b/heat/engine/resources/neutron/loadbalancer.py @@ -18,7 +18,6 @@ from heat.engine import properties from heat.engine import resource from heat.engine.resources.neutron import neutron from heat.engine.resources.neutron import neutron_utils -from heat.engine.resources import nova_utils from heat.engine import scheduler from heat.engine import support @@ -647,11 +646,10 @@ class LoadBalancer(resource.Resource): def handle_create(self): pool = self.properties[self.POOL_ID] client = self.neutron() - nova_client = self.nova() protocol_port = self.properties[self.PROTOCOL_PORT] for member in self.properties.get(self.MEMBERS): - address = nova_utils.server_to_ipaddress(nova_client, member) + address = self.client_plugin('nova').server_to_ipaddress(member) lb_member = client.create_member({ 'member': { 'pool_id': pool, @@ -673,10 +671,10 @@ class LoadBalancer(resource.Resource): self.client_plugin().ignore_not_found(ex) self.data_delete(member) pool = self.properties[self.POOL_ID] - nova_client = self.nova() protocol_port = self.properties[self.PROTOCOL_PORT] for member in members - old_members: - address = nova_utils.server_to_ipaddress(nova_client, member) + address = self.client_plugin('nova').server_to_ipaddress( + member) lb_member = client.create_member({ 'member': { 'pool_id': pool, diff --git a/heat/engine/resources/nova_keypair.py b/heat/engine/resources/nova_keypair.py index 387183ee5..5d67d9c2d 100644 --- a/heat/engine/resources/nova_keypair.py +++ b/heat/engine/resources/nova_keypair.py @@ -16,7 +16,6 @@ from heat.engine import attributes from heat.engine import constraints from heat.engine import properties from heat.engine import resource -from heat.engine.resources import nova_utils from heat.openstack.common.gettextutils import _ @@ -97,8 +96,7 @@ class KeyPair(resource.Resource): if self.properties[self.PUBLIC_KEY]: self._public_key = self.properties[self.PUBLIC_KEY] elif self.resource_id: - nova_key = nova_utils.get_keypair(self.nova(), - self.resource_id) + nova_key = self.client_plugin().get_keypair(self.resource_id) self._public_key = nova_key.public_key return self._public_key @@ -138,8 +136,7 @@ class KeypairConstraint(constraints.BaseCustomConstraint): # Don't validate empty key, which can happen when you use a KeyPair # resource return True - nova_client = client.client('nova') - nova_utils.get_keypair(nova_client, value) + client.client_plugin('nova').get_keypair(value) def resource_mapping(): diff --git a/heat/engine/resources/nova_utils.py b/heat/engine/resources/nova_utils.py index 253e93227..c418b8d2a 100644 --- a/heat/engine/resources/nova_utils.py +++ b/heat/engine/resources/nova_utils.py @@ -24,6 +24,7 @@ import string from oslo.config import cfg import six from six.moves.urllib import parse as urlparse +import warnings from heat.common import exception from heat.engine import scheduler @@ -49,6 +50,8 @@ def refresh_server(server): ''' Refresh server's attributes and log warnings for non-critical API errors. ''' + warnings.warn('nova_utils.refresh_server is deprecated. ' + 'Use self.client_plugin("nova").refresh_server') try: server.get() except nova_exceptions.OverLimit as exc: @@ -72,6 +75,8 @@ def refresh_server(server): def get_ip(server, net_type, ip_version): """Return the server's IP of the given type and version.""" + warnings.warn('nova_utils.get_ip is deprecated. ' + 'Use self.client_plugin("nova").get_ip') if net_type in server.addresses: for ip in server.addresses[net_type]: if ip['version'] == ip_version: @@ -79,6 +84,8 @@ def get_ip(server, net_type, ip_version): def get_flavor_id(nova_client, flavor): + warnings.warn('nova_utils.get_flavor_id is deprecated. ' + 'Use self.client_plugin("nova").get_flavor_id') ''' Get the id for the specified flavor name. If the specified value is flavor id, just return it. @@ -103,6 +110,8 @@ def get_flavor_id(nova_client, flavor): def get_keypair(nova_client, key_name): + warnings.warn('nova_utils.get_keypair is deprecated. ' + 'Use self.client_plugin("nova").get_keypair') ''' Get the public key specified by :key_name: @@ -119,6 +128,8 @@ def get_keypair(nova_client, key_name): def build_userdata(resource, userdata=None, instance_user=None, user_data_format='HEAT_CFNTOOLS'): + warnings.warn('nova_utils.build_userdata is deprecated. ' + 'Use self.client_plugin("nova").build_userdata') ''' Build multipart data blob for CloudInit which includes user-supplied Metadata, user data, and the required Heat in-instance configuration. @@ -241,6 +252,8 @@ def delete_server(server): A co-routine that deletes the server and waits for it to disappear from Nova. ''' + warnings.warn('nova_utils.delete_server is deprecated. ' + 'Use self.client_plugin("nova").delete_server') if not server: return try: @@ -274,12 +287,16 @@ def delete_server(server): @scheduler.wrappertask def resize(server, flavor, flavor_id): """Resize the server and then call check_resize task to verify.""" + warnings.warn('nova_utils.resize is deprecated. ' + 'Use self.client_plugin("nova").resize') server.resize(flavor_id) yield check_resize(server, flavor, flavor_id) def rename(server, name): """Update the name for a server.""" + warnings.warn('nova_utils.rename is deprecated. ' + 'Use self.client_plugin("nova").rename') server.update(name) @@ -288,6 +305,8 @@ def check_resize(server, flavor, flavor_id): Verify that a resizing server is properly resized. If that's the case, confirm the resize, if not raise an error. """ + warnings.warn('nova_utils.check_resize is deprecated. ' + 'Use self.client_plugin("nova").check_resize') refresh_server(server) while server.status == 'RESIZE': yield @@ -303,6 +322,8 @@ def check_resize(server, flavor, flavor_id): @scheduler.wrappertask def rebuild(server, image_id, preserve_ephemeral=False): """Rebuild the server and call check_rebuild to verify.""" + warnings.warn('nova_utils.rebuild is deprecated. ' + 'Use self.client_plugin("nova").rebuild') server.rebuild(image_id, preserve_ephemeral=preserve_ephemeral) yield check_rebuild(server, image_id) @@ -312,6 +333,8 @@ def check_rebuild(server, image_id): Verify that a rebuilding server is rebuilt. Raise error if it ends up in an ERROR state. """ + warnings.warn('nova_utils.check_rebuild is deprecated. ' + 'Use self.client_plugin("nova").check_rebuild') refresh_server(server) while server.status == 'REBUILD': yield @@ -326,6 +349,8 @@ def meta_serialize(metadata): Serialize non-string metadata values before sending them to Nova. """ + warnings.warn('nova_utils.meta_serialize is deprecated. ' + 'Use self.client_plugin("nova").meta_serialize') return dict((key, (value if isinstance(value, six.string_types) else json.dumps(value)) @@ -334,6 +359,8 @@ def meta_serialize(metadata): def meta_update(client, server, metadata): """Delete/Add the metadata in nova as needed.""" + warnings.warn('nova_utils.meta_update is deprecated. ' + 'Use self.client_plugin("nova").meta_update') metadata = meta_serialize(metadata) current_md = server.metadata to_del = [key for key in current_md.keys() if key not in metadata] @@ -347,6 +374,8 @@ def server_to_ipaddress(client, server): ''' Return the server's IP address, fetching it from Nova. ''' + warnings.warn('nova_utils.server_to_ipaddress is deprecated. ' + 'Use self.client_plugin("nova").server_to_ipaddress') try: server = client.servers.get(server) except nova_exceptions.NotFound as ex: @@ -360,5 +389,7 @@ def server_to_ipaddress(client, server): def absolute_limits(nova_client): """Return the absolute limits as a dictionary.""" + warnings.warn('nova_utils.absolute_limits is deprecated. ' + 'Use self.client_plugin("nova").absolute_limits') limits = nova_client.limits.get() return dict([(limit.name, limit.value) for limit in list(limits.absolute)]) diff --git a/heat/engine/resources/os_database.py b/heat/engine/resources/os_database.py index 2bc02650b..8bf94ea93 100644 --- a/heat/engine/resources/os_database.py +++ b/heat/engine/resources/os_database.py @@ -16,7 +16,6 @@ from heat.engine import attributes from heat.engine import constraints from heat.engine import properties from heat.engine import resource -from heat.engine.resources import nova_utils from heat.openstack.common.gettextutils import _ from heat.openstack.common import log as logging @@ -228,8 +227,8 @@ class OSDBInstance(resource.Resource): ''' Create cloud database instance. ''' - self.flavor = nova_utils.get_flavor_id(self.trove(), - self.properties[self.FLAVOR]) + self.flavor = self.client_plugin().get_flavor_id( + self.properties[self.FLAVOR]) self.volume = {'size': self.properties[self.SIZE]} self.databases = self.properties.get(self.DATABASES) self.users = self.properties.get(self.USERS) diff --git a/heat/engine/resources/server.py b/heat/engine/resources/server.py index d209b0f62..8e0fff434 100644 --- a/heat/engine/resources/server.py +++ b/heat/engine/resources/server.py @@ -22,7 +22,6 @@ from heat.engine import constraints from heat.engine import properties from heat.engine import resource from heat.engine.resources.neutron import subnet -from heat.engine.resources import nova_utils from heat.engine import scheduler from heat.engine import stack_user from heat.engine import support @@ -471,8 +470,8 @@ class Server(stack_user.StackUser): else: instance_user = None - userdata = nova_utils.build_userdata( - self, + userdata = self.client_plugin().build_userdata( + self.metadata_get(), ud_content, instance_user=instance_user, user_data_format=user_data_format) @@ -484,11 +483,12 @@ class Server(stack_user.StackUser): if image: image = self.client_plugin('glance').get_image_id(image) - flavor_id = nova_utils.get_flavor_id(self.nova(), flavor) + flavor_id = self.client_plugin().get_flavor_id(flavor) instance_meta = self.properties.get(self.METADATA) if instance_meta is not None: - instance_meta = nova_utils.meta_serialize(instance_meta) + instance_meta = self.client_plugin().meta_serialize( + instance_meta) scheduler_hints = self.properties.get(self.SCHEDULER_HINTS) nics = self._build_nics(self.properties.get(self.NETWORKS)) @@ -532,12 +532,13 @@ class Server(stack_user.StackUser): def _check_active(self, server): + cp = self.client_plugin() if server.status != 'ACTIVE': - nova_utils.refresh_server(server) + cp.refresh_server(server) # Some clouds append extra (STATUS) strings to the status short_server_status = server.status.split('(')[0] - if short_server_status in nova_utils.deferred_server_statuses: + if short_server_status in cp.deferred_server_statuses: return False elif server.status == 'ACTIVE': return True @@ -621,8 +622,8 @@ class Server(stack_user.StackUser): def _resolve_attribute(self, name): if name == self.FIRST_ADDRESS: - return nova_utils.server_to_ipaddress( - self.nova(), self.resource_id) or '' + return self.client_plugin().server_to_ipaddress( + self.resource_id) or '' try: server = self.nova().servers.get(self.resource_id) except Exception as e: @@ -718,9 +719,8 @@ class Server(stack_user.StackUser): if self.METADATA in prop_diff: server = self.nova().servers.get(self.resource_id) - nova_utils.meta_update(self.nova(), - server, - prop_diff[self.METADATA]) + self.client_plugin().meta_update(server, + prop_diff[self.METADATA]) if self.FLAVOR in prop_diff: @@ -732,11 +732,11 @@ class Server(stack_user.StackUser): raise resource.UpdateReplace(self.name) flavor = prop_diff[self.FLAVOR] - flavor_id = nova_utils.get_flavor_id(self.nova(), flavor) + flavor_id = self.client_plugin().get_flavor_id(flavor) if not server: server = self.nova().servers.get(self.resource_id) - checker = scheduler.TaskRunner(nova_utils.resize, server, flavor, - flavor_id) + checker = scheduler.TaskRunner(self.client_plugin().resize, + server, flavor, flavor_id) checkers.append(checker) if self.IMAGE in prop_diff: @@ -752,14 +752,14 @@ class Server(stack_user.StackUser): preserve_ephemeral = ( image_update_policy == 'REBUILD_PRESERVE_EPHEMERAL') checker = scheduler.TaskRunner( - nova_utils.rebuild, server, image_id, + self.client_plugin().rebuild, server, image_id, preserve_ephemeral=preserve_ephemeral) checkers.append(checker) if self.NAME in prop_diff: if not server: server = self.nova().servers.get(self.resource_id) - nova_utils.rename(server, prop_diff[self.NAME]) + self.client_plugin().rename(server, prop_diff[self.NAME]) if self.NETWORKS in prop_diff: new_networks = prop_diff.get(self.NETWORKS) @@ -928,7 +928,7 @@ class Server(stack_user.StackUser): metadata = self.properties.get(self.METADATA) personality = self.properties.get(self.PERSONALITY) if metadata is not None or personality: - limits = nova_utils.absolute_limits(self.nova()) + limits = self.client_plugin().absolute_limits() # if 'security_groups' present for the server and explict 'port' # in one or more entries in 'networks', raise validation error @@ -977,7 +977,8 @@ class Server(stack_user.StackUser): except Exception as e: self.client_plugin().ignore_not_found(e) else: - deleter = scheduler.TaskRunner(nova_utils.delete_server, server) + deleter = scheduler.TaskRunner(self.client_plugin().delete_server, + server) deleter.start() return deleter @@ -1022,10 +1023,11 @@ class Server(stack_user.StackUser): if server.status == 'SUSPENDED': return True - nova_utils.refresh_server(server) + cp = self.client_plugin() + cp.refresh_server(server) LOG.debug('%(name)s check_suspend_complete status = %(status)s' % {'name': self.name, 'status': server.status}) - if server.status in list(nova_utils.deferred_server_statuses + + if server.status in list(cp.deferred_server_statuses + ['ACTIVE']): return server.status == 'SUSPENDED' else: @@ -1067,8 +1069,7 @@ class FlavorConstraint(constraints.BaseCustomConstraint): expected_exceptions = (exception.FlavorMissing,) def validate_with_client(self, client, value): - nova_client = client.client('nova') - nova_utils.get_flavor_id(nova_client, value) + client.client_plugin('nova').get_flavor_id(value) def resource_mapping(): diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index 9110bc74c..7dddcb0ec 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -40,7 +40,6 @@ from heat.engine import parser from heat.engine.properties import Properties from heat.engine import resource as res from heat.engine.resources import instance as instances -from heat.engine.resources import nova_utils from heat.engine import service from heat.engine import stack_lock from heat.engine import watchrule @@ -209,6 +208,7 @@ def setup_mocks(mocks, stack, mock_image_constraint=True): mocks.StubOutWithMock(nova.NovaClientPlugin, '_create') nova.NovaClientPlugin._create().AndReturn(fc) instance = stack['WebServer'] + metadata = instance.metadata_get() if mock_image_constraint: setup_mock_for_image_constraint(mocks, instance.t['Properties']['ImageId']) @@ -216,11 +216,11 @@ def setup_mocks(mocks, stack, mock_image_constraint=True): setup_keystone_mocks(mocks, stack) user_data = instance.properties['UserData'] - server_userdata = nova_utils.build_userdata(instance, user_data, - 'ec2-user') - mocks.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata( - instance, + server_userdata = instance.client_plugin().build_userdata( + metadata, user_data, 'ec2-user') + mocks.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, instance.t['Properties']['UserData'], 'ec2-user').AndReturn(server_userdata) diff --git a/heat/tests/test_instance.py b/heat/tests/test_instance.py index 39844852b..51af3e40a 100644 --- a/heat/tests/test_instance.py +++ b/heat/tests/test_instance.py @@ -28,7 +28,6 @@ from heat.engine import parser from heat.engine import resource from heat.engine.resources import instance as instances from heat.engine.resources import network_interface -from heat.engine.resources import nova_utils from heat.engine import scheduler from heat.tests.common import HeatTestCase from heat.tests import utils @@ -1336,8 +1335,10 @@ class InstancesTest(HeatTestCase): """The default value for instance_user in heat.conf is ec2-user.""" return_server = self.fc.servers.list()[1] instance = self._setup_test_instance(return_server, 'default_user') - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata(instance, 'wordpress', 'ec2-user') + metadata = instance.metadata_get() + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, 'wordpress', 'ec2-user') self.m.ReplayAll() scheduler.TaskRunner(instance.create)() self.m.VerifyAll() @@ -1354,8 +1355,10 @@ class InstancesTest(HeatTestCase): instance = self._setup_test_instance(return_server, 'custom_user') self.m.StubOutWithMock(instances.cfg.CONF, 'instance_user') instances.cfg.CONF.instance_user = 'custom_user' - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata(instance, 'wordpress', 'custom_user') + metadata = instance.metadata_get() + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, 'wordpress', 'custom_user') self.m.ReplayAll() scheduler.TaskRunner(instance.create)() self.m.VerifyAll() @@ -1373,8 +1376,10 @@ class InstancesTest(HeatTestCase): instance = self._setup_test_instance(return_server, 'empty_user') self.m.StubOutWithMock(instances.cfg.CONF, 'instance_user') instances.cfg.CONF.instance_user = '' - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata(instance, 'wordpress', 'ec2-user') + metadata = instance.metadata_get() + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, 'wordpress', 'ec2-user') self.m.ReplayAll() scheduler.TaskRunner(instance.create)() self.m.VerifyAll() diff --git a/heat/tests/test_instance_network.py b/heat/tests/test_instance_network.py index 605a5f44b..28e06fed7 100644 --- a/heat/tests/test_instance_network.py +++ b/heat/tests/test_instance_network.py @@ -20,7 +20,6 @@ from heat.engine import environment from heat.engine import parser from heat.engine.resources import instance as instances from heat.engine.resources import network_interface as network_interfaces -from heat.engine.resources import nova_utils from heat.engine import scheduler from heat.tests.common import HeatTestCase from heat.tests import utils @@ -170,6 +169,7 @@ class instancesTest(HeatTestCase): resource_defns = stack.t.resource_definitions(stack) instance = instances.Instance('%s_name' % name, resource_defns['WebServer'], stack) + metadata = instance.metadata_get() self.m.StubOutWithMock(nova.NovaClientPlugin, '_create') nova.NovaClientPlugin._create().AndReturn(self.fc) @@ -180,13 +180,13 @@ class instancesTest(HeatTestCase): instance.neutron().MultipleTimes().AndReturn(FakeNeutron()) # need to resolve the template functions - server_userdata = nova_utils.build_userdata( - instance, + server_userdata = instance.client_plugin().build_userdata( + metadata, instance.t['Properties']['UserData'], 'ec2-user') - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata( - instance, + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, instance.t['Properties']['UserData'], 'ec2-user').AndReturn(server_userdata) @@ -225,6 +225,7 @@ class instancesTest(HeatTestCase): instance = instances.Instance('%s_name' % name, resource_defns['WebServer'], stack) + metadata = instance.metadata_get() self._mock_get_image_id_success(image_id, 1) self.m.StubOutWithMock(nic, 'neutron') @@ -234,13 +235,13 @@ class instancesTest(HeatTestCase): nova.NovaClientPlugin._create().AndReturn(self.fc) # need to resolve the template functions - server_userdata = nova_utils.build_userdata( - instance, + server_userdata = instance.client_plugin().build_userdata( + metadata, instance.t['Properties']['UserData'], 'ec2-user') - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata( - instance, + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, instance.t['Properties']['UserData'], 'ec2-user').AndReturn(server_userdata) diff --git a/heat/tests/test_neutron_autoscaling.py b/heat/tests/test_neutron_autoscaling.py index 0ee388e5d..4a266c4d6 100644 --- a/heat/tests/test_neutron_autoscaling.py +++ b/heat/tests/test_neutron_autoscaling.py @@ -20,10 +20,11 @@ from oslo.config import cfg from heat.common import template_format from heat.db import api as db_api +from heat.engine.clients.os import glance +from heat.engine.clients.os import nova from heat.engine import environment from heat.engine import parser from heat.engine.resources import instance -from heat.engine.resources import nova_utils from heat.engine import template from heat.tests.common import HeatTestCase from heat.tests import utils @@ -123,11 +124,12 @@ class AutoScalingTest(HeatTestCase): self.m.StubOutWithMock(neutronclient.Client, 'create_member') self.m.StubOutWithMock(neutronclient.Client, 'list_members') - self.m.StubOutWithMock(nova_utils, 'server_to_ipaddress') + self.m.StubOutWithMock(nova.NovaClientPlugin, 'server_to_ipaddress') self.m.StubOutWithMock(parser.Stack, 'validate') self.m.StubOutWithMock(instance.Instance, 'handle_create') self.m.StubOutWithMock(instance.Instance, 'check_create_complete') + self.m.StubOutWithMock(glance.ImageConstraint, "validate") def test_lb(self): @@ -276,10 +278,10 @@ class AutoScalingTest(HeatTestCase): instance.Instance.check_create_complete(mox.IgnoreArg())\ .AndReturn(True) - self.stub_ImageConstraint_validate() + glance.ImageConstraint.validate( + mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(True) - nova_utils.server_to_ipaddress( - mox.IgnoreArg(), + nova.NovaClientPlugin.server_to_ipaddress( mox.IgnoreArg()).AndReturn('1.2.3.4') neutronclient.Client.create_member(membera_block).\ @@ -304,15 +306,13 @@ class AutoScalingTest(HeatTestCase): instance.Instance.check_create_complete(mox.IgnoreArg())\ .AndReturn(True) - nova_utils.server_to_ipaddress( - mox.IgnoreArg(), + nova.NovaClientPlugin.server_to_ipaddress( mox.IgnoreArg()).AndReturn('1.2.3.5') neutronclient.Client.create_member(memberb_block).\ AndReturn(memberb_ret_block) - nova_utils.server_to_ipaddress( - mox.IgnoreArg(), + nova.NovaClientPlugin.server_to_ipaddress( mox.IgnoreArg()).AndReturn('1.2.3.6') neutronclient.Client.create_member(memberc_block).\ diff --git a/heat/tests/test_nokey.py b/heat/tests/test_nokey.py index 1115dc4f9..e8d9a1c4b 100644 --- a/heat/tests/test_nokey.py +++ b/heat/tests/test_nokey.py @@ -15,7 +15,6 @@ from heat.common import template_format from heat.engine.clients.os import glance from heat.engine.clients.os import nova from heat.engine.resources import instance as instances -from heat.engine.resources import nova_utils from heat.engine import scheduler from heat.tests.common import HeatTestCase from heat.tests import utils @@ -66,13 +65,14 @@ class nokeyTest(HeatTestCase): 'CentOS 5.2').MultipleTimes().AndReturn(1) # need to resolve the template functions - server_userdata = nova_utils.build_userdata( - instance, + metadata = instance.metadata_get() + server_userdata = instance.client_plugin().build_userdata( + metadata, instance.t['Properties']['UserData'], 'ec2-user') - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata( - instance, + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, instance.t['Properties']['UserData'], 'ec2-user').AndReturn(server_userdata) diff --git a/heat/tests/test_nova_client.py b/heat/tests/test_nova_client.py new file mode 100644 index 000000000..422ccc9d7 --- /dev/null +++ b/heat/tests/test_nova_client.py @@ -0,0 +1,238 @@ +# +# 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. +"""Tests for :module:'heat.engine.resources.nova_utls'.""" + +import mock +from novaclient import exceptions as nova_exceptions +from oslo.config import cfg +import uuid + +from heat.common import exception +from heat.engine.clients.os import nova +from heat.tests.common import HeatTestCase +from heat.tests import utils + + +class NovaClientPluginTestCase(HeatTestCase): + def setUp(self): + super(NovaClientPluginTestCase, self).setUp() + self.nova_client = self.m.CreateMockAnything() + con = utils.dummy_context() + c = con.clients + self.nova_plugin = c.client_plugin('nova') + self.nova_plugin._client = self.nova_client + + +class NovaClientPluginTests(NovaClientPluginTestCase): + """ + Basic tests for the helper methods in + :module:'heat.engine.resources.nova_utils'. + """ + + def test_get_ip(self): + my_image = self.m.CreateMockAnything() + my_image.addresses = { + 'public': [{'version': 4, + 'addr': '4.5.6.7'}, + {'version': 6, + 'addr': '2401:1801:7800:0101:c058:dd33:ff18:04e6'}], + 'private': [{'version': 4, + 'addr': '10.13.12.13'}]} + + expected = '4.5.6.7' + observed = self.nova_plugin.get_ip(my_image, 'public', 4) + self.assertEqual(expected, observed) + + expected = '10.13.12.13' + observed = self.nova_plugin.get_ip(my_image, 'private', 4) + self.assertEqual(expected, observed) + + expected = '2401:1801:7800:0101:c058:dd33:ff18:04e6' + observed = self.nova_plugin.get_ip(my_image, 'public', 6) + self.assertEqual(expected, observed) + + def test_get_flavor_id(self): + """Tests the get_flavor_id function.""" + flav_id = str(uuid.uuid4()) + flav_name = 'X-Large' + my_flavor = self.m.CreateMockAnything() + my_flavor.name = flav_name + my_flavor.id = flav_id + self.nova_client.flavors = self.m.CreateMockAnything() + self.nova_client.flavors.list().MultipleTimes().AndReturn([my_flavor]) + self.m.ReplayAll() + self.assertEqual(flav_id, self.nova_plugin.get_flavor_id(flav_name)) + self.assertEqual(flav_id, self.nova_plugin.get_flavor_id(flav_id)) + self.assertRaises(exception.FlavorMissing, + self.nova_plugin.get_flavor_id, 'noflavor') + self.m.VerifyAll() + + def test_get_keypair(self): + """Tests the get_keypair function.""" + my_pub_key = 'a cool public key string' + my_key_name = 'mykey' + my_key = self.m.CreateMockAnything() + my_key.public_key = my_pub_key + my_key.name = my_key_name + self.nova_client.keypairs = self.m.CreateMockAnything() + self.nova_client.keypairs.get( + my_key_name).AndReturn(my_key) + self.nova_client.keypairs.get( + 'notakey').AndRaise(nova_exceptions.NotFound(404)) + self.m.ReplayAll() + self.assertEqual(my_key, self.nova_plugin.get_keypair(my_key_name)) + self.assertRaises(exception.UserKeyPairMissing, + self.nova_plugin.get_keypair, 'notakey') + self.m.VerifyAll() + + +class NovaUtilsRefreshServerTests(NovaClientPluginTestCase): + + def test_successful_refresh(self): + server = self.m.CreateMockAnything() + server.get().AndReturn(None) + self.m.ReplayAll() + + self.assertIsNone(self.nova_plugin.refresh_server(server)) + self.m.VerifyAll() + + def test_overlimit_error(self): + server = mock.Mock() + server.get.side_effect = nova_exceptions.OverLimit( + 413, "limit reached") + self.assertIsNone(self.nova_plugin.refresh_server(server)) + + def test_500_error(self): + server = self.m.CreateMockAnything() + msg = ("ClientException: The server has either erred or is " + "incapable of performing the requested operation.") + server.get().AndRaise( + nova_exceptions.ClientException(500, msg)) + self.m.ReplayAll() + + self.assertIsNone(self.nova_plugin.refresh_server(server)) + self.m.VerifyAll() + + def test_503_error(self): + server = self.m.CreateMockAnything() + msg = ("ClientException: The server has either erred or is " + "incapable of performing the requested operation.") + server.get().AndRaise( + nova_exceptions.ClientException(503, msg)) + self.m.ReplayAll() + + self.assertIsNone(self.nova_plugin.refresh_server(server)) + self.m.VerifyAll() + + def test_unhandled_exception(self): + server = self.m.CreateMockAnything() + msg = ("ClientException: The server has either erred or is " + "incapable of performing the requested operation.") + server.get().AndRaise( + nova_exceptions.ClientException(501, msg)) + self.m.ReplayAll() + + self.assertRaises(nova_exceptions.ClientException, + self.nova_plugin.refresh_server, server) + self.m.VerifyAll() + + +class NovaUtilsUserdataTests(NovaClientPluginTestCase): + + def test_build_userdata(self): + """Tests the build_userdata function.""" + cfg.CONF.set_override('heat_metadata_server_url', + 'http://server.test:123') + cfg.CONF.set_override('heat_watch_server_url', + 'http://server.test:345') + cfg.CONF.set_override('instance_connection_is_secure', + False) + cfg.CONF.set_override( + 'instance_connection_https_validate_certificates', False) + data = self.nova_plugin.build_userdata({}) + self.assertIn("Content-Type: text/cloud-config;", data) + self.assertIn("Content-Type: text/cloud-boothook;", data) + self.assertIn("Content-Type: text/part-handler;", data) + self.assertIn("Content-Type: text/x-cfninitdata;", data) + self.assertIn("Content-Type: text/x-shellscript;", data) + self.assertIn("http://server.test:345", data) + self.assertIn("http://server.test:123", data) + self.assertIn("[Boto]", data) + + def test_build_userdata_without_instance_user(self): + """Don't add a custom instance user when not requested.""" + cfg.CONF.set_override('instance_user', + 'config_instance_user') + cfg.CONF.set_override('heat_metadata_server_url', + 'http://server.test:123') + cfg.CONF.set_override('heat_watch_server_url', + 'http://server.test:345') + data = self.nova_plugin.build_userdata({}, instance_user=None) + self.assertNotIn('user: ', data) + self.assertNotIn('useradd', data) + self.assertNotIn('config_instance_user', data) + + def test_build_userdata_with_instance_user(self): + """Add the custom instance user when requested.""" + self.m.StubOutWithMock(nova.cfg, 'CONF') + cnf = nova.cfg.CONF + cnf.instance_user = 'config_instance_user' + cnf.heat_metadata_server_url = 'http://server.test:123' + cnf.heat_watch_server_url = 'http://server.test:345' + data = self.nova_plugin.build_userdata( + None, instance_user="custominstanceuser") + self.assertNotIn('config_instance_user', data) + self.assertIn("custominstanceuser", data) + + +class NovaUtilsMetadataTests(NovaClientPluginTestCase): + + def test_serialize_string(self): + original = {'test_key': 'simple string value'} + self.assertEqual(original, self.nova_plugin.meta_serialize(original)) + + def test_serialize_int(self): + original = {'test_key': 123} + expected = {'test_key': '123'} + self.assertEqual(expected, self.nova_plugin.meta_serialize(original)) + + def test_serialize_list(self): + original = {'test_key': [1, 2, 3]} + expected = {'test_key': '[1, 2, 3]'} + self.assertEqual(expected, self.nova_plugin.meta_serialize(original)) + + def test_serialize_dict(self): + original = {'test_key': {'a': 'b', 'c': 'd'}} + expected = {'test_key': '{"a": "b", "c": "d"}'} + self.assertEqual(expected, self.nova_plugin.meta_serialize(original)) + + def test_serialize_none(self): + original = {'test_key': None} + expected = {'test_key': 'null'} + self.assertEqual(expected, self.nova_plugin.meta_serialize(original)) + + def test_serialize_combined(self): + original = { + 'test_key_1': 123, + 'test_key_2': 'a string', + 'test_key_3': {'a': 'b'}, + 'test_key_4': None, + } + expected = { + 'test_key_1': '123', + 'test_key_2': 'a string', + 'test_key_3': '{"a": "b"}', + 'test_key_4': 'null', + } + + self.assertEqual(expected, self.nova_plugin.meta_serialize(original)) diff --git a/heat/tests/test_nova_utils.py b/heat/tests/test_nova_utils.py index 1599582d0..98e276818 100644 --- a/heat/tests/test_nova_utils.py +++ b/heat/tests/test_nova_utils.py @@ -35,6 +35,10 @@ class NovaUtilsTests(HeatTestCase): def setUp(self): super(NovaUtilsTests, self).setUp() self.nova_client = self.m.CreateMockAnything() + self.mock_warnings = mock.patch( + 'heat.engine.resources.nova_utils.warnings') + self.mock_warnings.start() + self.addCleanup(self.mock_warnings.stop) def test_get_ip(self): my_image = self.m.CreateMockAnything() @@ -126,6 +130,13 @@ class NovaUtilsTests(HeatTestCase): class NovaUtilsRefreshServerTests(HeatTestCase): + def setUp(self): + super(NovaUtilsRefreshServerTests, self).setUp() + self.mock_warnings = mock.patch( + 'heat.engine.resources.nova_utils.warnings') + self.mock_warnings.start() + self.addCleanup(self.mock_warnings.stop) + def test_successful_refresh(self): server = self.m.CreateMockAnything() server.get().AndReturn(None) @@ -170,6 +181,10 @@ class NovaUtilsUserdataTests(HeatTestCase): def setUp(self): super(NovaUtilsUserdataTests, self).setUp() self.nova_client = self.m.CreateMockAnything() + self.mock_warnings = mock.patch( + 'heat.engine.resources.nova_utils.warnings') + self.mock_warnings.start() + self.addCleanup(self.mock_warnings.stop) def test_build_userdata(self): """Tests the build_userdata function.""" @@ -228,6 +243,13 @@ class NovaUtilsUserdataTests(HeatTestCase): class NovaUtilsMetadataTests(HeatTestCase): + def setUp(self): + super(NovaUtilsMetadataTests, self).setUp() + self.mock_warnings = mock.patch( + 'heat.engine.resources.nova_utils.warnings') + self.mock_warnings.start() + self.addCleanup(self.mock_warnings.stop) + def test_serialize_string(self): original = {'test_key': 'simple string value'} self.assertEqual(original, nova_utils.meta_serialize(original)) diff --git a/heat/tests/test_os_database.py b/heat/tests/test_os_database.py index 290e5150f..b3da418f9 100644 --- a/heat/tests/test_os_database.py +++ b/heat/tests/test_os_database.py @@ -18,6 +18,7 @@ import six from heat.common import exception from heat.common import template_format +from heat.engine.clients.os import trove from heat.engine import parser from heat.engine.resources import os_database from heat.engine import scheduler @@ -97,12 +98,10 @@ class OSDBInstanceTest(HeatTestCase): return instance def _stubout_create(self, instance, fake_dbinstance): - self.m.StubOutWithMock(instance, 'trove') - instance.trove().MultipleTimes().AndReturn(self.fc) - self.m.StubOutWithMock(self.fc, 'flavors') - self.m.StubOutWithMock(self.fc.flavors, "list") - self.fc.flavors.list().AndReturn([FakeFlavor(1, '1GB'), - FakeFlavor(2, '2GB')]) + self.m.StubOutWithMock(trove.TroveClientPlugin, '_create') + trove.TroveClientPlugin._create().AndReturn(self.fc) + self.m.StubOutWithMock(trove.TroveClientPlugin, 'get_flavor_id') + trove.TroveClientPlugin.get_flavor_id('1GB').AndReturn(1) self.m.StubOutWithMock(self.fc, 'instances') self.m.StubOutWithMock(self.fc.instances, 'create') users = [{"name": "testuser", "password": "pass", "host": "%", @@ -121,8 +120,8 @@ class OSDBInstanceTest(HeatTestCase): self.m.ReplayAll() def _stubout_validate(self, instance): - self.m.StubOutWithMock(instance, 'trove') - instance.trove().MultipleTimes().AndReturn(self.fc) + self.m.StubOutWithMock(trove.TroveClientPlugin, '_create') + trove.TroveClientPlugin._create().AndReturn(self.fc) self.m.StubOutWithMock(self.fc, 'datastore_versions') self.m.StubOutWithMock(self.fc.datastore_versions, 'list') self.fc.datastore_versions.list(instance.properties['datastore_type'] @@ -144,8 +143,8 @@ class OSDBInstanceTest(HeatTestCase): t['Resources']['MySqlCloudDB']['Properties']['restore_point'] = "1234" instance = self._setup_test_clouddbinstance('dbinstance_create', t) - self.m.StubOutWithMock(instance, 'trove') - instance.trove().MultipleTimes().AndReturn(self.fc) + self.m.StubOutWithMock(trove.TroveClientPlugin, '_create') + trove.TroveClientPlugin._create().AndReturn(self.fc) self.m.StubOutWithMock(self.fc, 'flavors') self.m.StubOutWithMock(self.fc.flavors, "list") self.fc.flavors.list().AndReturn([FakeFlavor(1, '1GB'), @@ -281,8 +280,8 @@ class OSDBInstanceTest(HeatTestCase): t = template_format.parse(db_template) instance = self._setup_test_clouddbinstance('dbinstance_test', t) instance.resource_id = 12345 - self.m.StubOutWithMock(instance, 'trove') - instance.trove().MultipleTimes().AndReturn(self.fc) + self.m.StubOutWithMock(trove.TroveClientPlugin, '_create') + trove.TroveClientPlugin._create().AndReturn(self.fc) self.m.StubOutWithMock(self.fc, 'instances') self.m.StubOutWithMock(self.fc.instances, 'get') self.fc.instances.get(12345).AndReturn(fake_dbinstance) @@ -296,8 +295,8 @@ class OSDBInstanceTest(HeatTestCase): t = template_format.parse(db_template) instance = self._setup_test_clouddbinstance('dbinstance_test', t) instance.resource_id = 12345 - self.m.StubOutWithMock(instance, 'trove') - instance.trove().MultipleTimes().AndReturn(self.fc) + self.m.StubOutWithMock(trove.TroveClientPlugin, '_create') + trove.TroveClientPlugin._create().AndReturn(self.fc) self.m.StubOutWithMock(self.fc, 'instances') self.m.StubOutWithMock(self.fc.instances, 'get') self.fc.instances.get(12345).AndReturn(fake_dbinstance) @@ -312,8 +311,8 @@ class OSDBInstanceTest(HeatTestCase): t = template_format.parse(db_template) instance = self._setup_test_clouddbinstance('dbinstance_test', t) instance.resource_id = 12345 - self.m.StubOutWithMock(instance, 'trove') - instance.trove().MultipleTimes().AndReturn(self.fc) + self.m.StubOutWithMock(trove.TroveClientPlugin, '_create') + trove.TroveClientPlugin._create().AndReturn(self.fc) self.m.StubOutWithMock(self.fc, 'instances') self.m.StubOutWithMock(self.fc.instances, 'get') self.fc.instances.get(12345).AndReturn(fake_dbinstance) @@ -419,8 +418,8 @@ class OSDBInstanceTest(HeatTestCase): 'datastore_type'] = 'mysql' t['Resources']['MySqlCloudDB']['Properties'].pop('datastore_version') instance = self._setup_test_clouddbinstance('dbinstance_test', t) - self.m.StubOutWithMock(instance, 'trove') - instance.trove().MultipleTimes().AndReturn(self.fc) + self.m.StubOutWithMock(trove.TroveClientPlugin, '_create') + trove.TroveClientPlugin._create().AndReturn(self.fc) self.m.StubOutWithMock(self.fc, 'datastore_versions') self.m.StubOutWithMock(self.fc.datastore_versions, 'list') self.fc.datastore_versions.list( diff --git a/heat/tests/test_server.py b/heat/tests/test_server.py index 14be2b4d1..a112653f3 100644 --- a/heat/tests/test_server.py +++ b/heat/tests/test_server.py @@ -28,7 +28,6 @@ from heat.engine.clients.os import nova from heat.engine import environment from heat.engine import parser from heat.engine import resource -from heat.engine.resources import nova_utils from heat.engine.resources import server as servers from heat.engine import scheduler from heat.openstack.common.gettextutils import _ @@ -175,12 +174,8 @@ class ServersTest(HeatTestCase): glance.GlanceClientPlugin.get_image_id(image_id).AndRaise(exp) def _mock_get_keypair_success(self, keypair_input, keypair): - n_cli_mock = self.m.CreateMockAnything() - self.m.StubOutWithMock(nova.NovaClientPlugin, '_create') - nova.NovaClientPlugin._create().AndReturn( - n_cli_mock) - self.m.StubOutWithMock(nova_utils, 'get_keypair') - nova_utils.get_keypair(n_cli_mock, keypair_input).MultipleTimes().\ + self.m.StubOutWithMock(nova.NovaClientPlugin, 'get_keypair') + nova.NovaClientPlugin.get_keypair(keypair_input).MultipleTimes().\ AndReturn(keypair) def _server_validate_mock(self, server): @@ -763,8 +758,11 @@ class ServersTest(HeatTestCase): web_server = tmpl.t['Resources']['WebServer'] del web_server['Properties']['image'] - def create_server(device_name, mock_create=True): - self.m.UnsetStubs() + self.m.StubOutWithMock(nova.NovaClientPlugin, '_create') + nova.NovaClientPlugin._create().AndReturn(self.fc) + self.m.ReplayAll() + + def create_server(device_name): web_server['Properties']['block_device_mapping'] = [{ "device_name": device_name, "volume_id": "5d7e27da-6703-4f7e-9f94-1f67abef734c", @@ -773,17 +771,13 @@ class ServersTest(HeatTestCase): resource_defns = tmpl.resource_definitions(stack) server = servers.Server('server_with_bootable_volume', resource_defns['WebServer'], stack) - if mock_create: - self.m.StubOutWithMock(nova.NovaClientPlugin, '_create') - nova.NovaClientPlugin._create().AndReturn(self.fc) - self.m.ReplayAll() return server server = create_server(u'vda') self.assertIsNone(server.validate()) - server = create_server('vda', mock_create=False) + server = create_server('vda') self.assertIsNone(server.validate()) - server = create_server('vdb', mock_create=False) + server = create_server('vdb') ex = self.assertRaises(exception.StackValidationFailed, server.validate) self.assertEqual('Neither image nor bootable volume is specified for ' @@ -825,7 +819,9 @@ class ServersTest(HeatTestCase): server = servers.Server('server_validate_test', resource_defns['WebServer'], stack) - self.stub_ImageConstraint_validate() + self.m.StubOutWithMock(glance.ImageConstraint, "validate") + glance.ImageConstraint.validate( + mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(True) self.m.ReplayAll() self.assertIsNone(server.validate()) @@ -1015,7 +1011,7 @@ class ServersTest(HeatTestCase): new_meta = {'test': 123} self.m.StubOutWithMock(self.fc.servers, 'set_meta') self.fc.servers.set_meta(return_server, - nova_utils.meta_serialize( + server.client_plugin().meta_serialize( new_meta)).AndReturn(None) self.m.ReplayAll() update_template = copy.deepcopy(server.t) @@ -1040,7 +1036,7 @@ class ServersTest(HeatTestCase): # If we're going to call set_meta() directly we # need to handle the serialization ourselves. self.fc.servers.set_meta(return_server, - nova_utils.meta_serialize( + server.client_plugin().meta_serialize( new_meta)).AndReturn(None) self.m.ReplayAll() update_template = copy.deepcopy(server.t) @@ -1229,7 +1225,9 @@ class ServersTest(HeatTestCase): image_id = self.getUniqueString() self.m.StubOutWithMock(nova.NovaClientPlugin, '_create') nova.NovaClientPlugin._create().AndReturn(self.fc) - self.stub_ImageConstraint_validate() + self.m.StubOutWithMock(glance.ImageConstraint, "validate") + glance.ImageConstraint.validate( + mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(True) self.m.ReplayAll() update_template = copy.deepcopy(server.t) @@ -1237,8 +1235,6 @@ class ServersTest(HeatTestCase): updater = scheduler.TaskRunner(server.update, update_template) self.assertRaises(resource.UpdateReplace, updater) - self.m.VerifyAll() - def _test_server_update_image_rebuild(self, status, policy='REBUILD'): # Server.handle_update supports changing the image, and makes # the change making a rebuild API call against Nova. @@ -1339,7 +1335,9 @@ class ServersTest(HeatTestCase): server = self._create_test_server(return_server, 'update_prop') - self.stub_ImageConstraint_validate() + self.m.StubOutWithMock(glance.ImageConstraint, "validate") + glance.ImageConstraint.validate( + mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(True) self.m.ReplayAll() update_template = copy.deepcopy(server.t) @@ -1347,8 +1345,6 @@ class ServersTest(HeatTestCase): updater = scheduler.TaskRunner(server.update, update_template) self.assertRaises(resource.UpdateReplace, updater) - self.m.VerifyAll() - def test_server_status_build(self): return_server = self.fc.servers.list()[0] server = self._setup_test_server(return_server, @@ -1857,8 +1853,6 @@ class ServersTest(HeatTestCase): self.m.StubOutWithMock(self.fc.limits, 'get') self.fc.limits.get().MultipleTimes().AndReturn(self.limits) - self.m.StubOutWithMock(server, 'nova') - server.nova().MultipleTimes().AndReturn(self.fc) self.m.StubOutWithMock(nova.NovaClientPlugin, '_create') nova.NovaClientPlugin._create().AndReturn(self.fc) self._mock_get_image_id_success('F17-x86_64-gold', 'image_id') @@ -1985,11 +1979,13 @@ class ServersTest(HeatTestCase): """The default value for instance_user in heat.conf is ec2-user.""" return_server = self.fc.servers.list()[1] server = self._setup_test_server(return_server, 'default_user') - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata(server, - 'wordpress', - instance_user='ec2-user', - user_data_format='HEAT_CFNTOOLS') + metadata = server.metadata_get() + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, + 'wordpress', + instance_user='ec2-user', + user_data_format='HEAT_CFNTOOLS') self.m.ReplayAll() scheduler.TaskRunner(server.create)() self.m.VerifyAll() @@ -2023,11 +2019,13 @@ class ServersTest(HeatTestCase): self.m.StubOutWithMock(nova.NovaClientPlugin, '_create') nova.NovaClientPlugin._create().AndReturn(self.fc) self._mock_get_image_id_success('F17-x86_64-gold', image_id) - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata(server, - 'wordpress', - instance_user='custom_user', - user_data_format='HEAT_CFNTOOLS') + metadata = server.metadata_get() + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, + 'wordpress', + instance_user='custom_user', + user_data_format='HEAT_CFNTOOLS') self.m.ReplayAll() scheduler.TaskRunner(server.create)() self.m.VerifyAll() @@ -2044,11 +2042,13 @@ class ServersTest(HeatTestCase): server = self._setup_test_server(return_server, 'custom_user') self.m.StubOutWithMock(servers.cfg.CONF, 'instance_user') servers.cfg.CONF.instance_user = 'custom_user' - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata(server, - 'wordpress', - instance_user='custom_user', - user_data_format='HEAT_CFNTOOLS') + metadata = server.metadata_get() + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, + 'wordpress', + instance_user='custom_user', + user_data_format='HEAT_CFNTOOLS') self.m.ReplayAll() scheduler.TaskRunner(server.create)() self.m.VerifyAll() @@ -2067,11 +2067,13 @@ class ServersTest(HeatTestCase): server = self._setup_test_server(return_server, 'custom_user') self.m.StubOutWithMock(servers.cfg.CONF, 'instance_user') servers.cfg.CONF.instance_user = '' - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata(server, - 'wordpress', - instance_user=None, - user_data_format='HEAT_CFNTOOLS') + metadata = server.metadata_get() + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, + 'wordpress', + instance_user=None, + user_data_format='HEAT_CFNTOOLS') self.m.ReplayAll() scheduler.TaskRunner(server.create)() self.m.VerifyAll() @@ -2531,7 +2533,7 @@ class ServersTest(HeatTestCase): # is NOT called during call to server.validate(). # This is the way to validate that no excessive calls to Nova # are made during validation. - self.m.StubOutWithMock(nova_utils, 'absolute_limits') + self.m.StubOutWithMock(nova.NovaClientPlugin, 'absolute_limits') self.m.StubOutWithMock(nova.NovaClientPlugin, '_create') nova.NovaClientPlugin._create().AndReturn(self.fc) self._mock_get_image_id_success('F17-x86_64-gold', 'image_id') diff --git a/heat/tests/test_server_tags.py b/heat/tests/test_server_tags.py index 968a49d78..d8b1f45b0 100644 --- a/heat/tests/test_server_tags.py +++ b/heat/tests/test_server_tags.py @@ -22,7 +22,6 @@ from heat.engine.clients.os import nova from heat.engine import environment from heat.engine import parser from heat.engine.resources import instance as instances -from heat.engine.resources import nova_utils from heat.engine import scheduler from heat.tests.common import HeatTestCase from heat.tests import utils @@ -152,13 +151,14 @@ class ServerTagsTest(HeatTestCase): nova.NovaClientPlugin._create().AndReturn(self.fc) self._mock_get_image_id_success('CentOS 5.2', 1) # need to resolve the template functions - server_userdata = nova_utils.build_userdata( - instance, + metadata = instance.metadata_get() + server_userdata = instance.client_plugin().build_userdata( + metadata, instance.t['Properties']['UserData'], 'ec2-user') - self.m.StubOutWithMock(nova_utils, 'build_userdata') - nova_utils.build_userdata( - instance, + self.m.StubOutWithMock(nova.NovaClientPlugin, 'build_userdata') + nova.NovaClientPlugin.build_userdata( + metadata, instance.t['Properties']['UserData'], 'ec2-user').AndReturn(server_userdata)