From 7601916b0c7df97ebbc2506176c587315a9d7f4c Mon Sep 17 00:00:00 2001 From: Randall Burt Date: Tue, 6 Aug 2013 17:07:26 -0500 Subject: [PATCH] Refactor useful nova functions for re-use. Refactor handy nova functions out of Instance and into a helper module. This allows alternate compute implementations to use this functionality without having to subclass. Change-Id: I529e2d1324981de7336264b5c697f1944668d013 --- heat/engine/resources/nova_utils.py | 175 ++++++++++++++++++++++++++++ heat/tests/test_nova_utils.py | 107 +++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 heat/engine/resources/nova_utils.py create mode 100644 heat/tests/test_nova_utils.py diff --git a/heat/engine/resources/nova_utils.py b/heat/engine/resources/nova_utils.py new file mode 100644 index 00000000..644c88b9 --- /dev/null +++ b/heat/engine/resources/nova_utils.py @@ -0,0 +1,175 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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. +"""Utilities for Resources that use the Openstack Nova API.""" + +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +import json +import os +import pkgutil + +from urlparse import urlparse + +from oslo.config import cfg + +from heat.common import exception +from heat.engine import clients +from heat.openstack.common import log as logging +from heat.openstack.common import uuidutils + +logger = logging.getLogger(__name__) + + +def get_image_id(nova_client, image_identifier): + ''' + Return an id for the specified image name or identifier. + + :param nova_client: the nova client to use + :param image_identifier: image name or a UUID-like identifier + :returns: the id of the requested :image_identifier: + :raises: exception.ImageNotFound, exception.NoUniqueImageFound + ''' + image_id = None + if uuidutils.is_uuid_like(image_identifier): + try: + image_id = nova_client.images.get(image_identifier).id + except clients.novaclient.exceptions.NotFound: + logger.info("Image %s was not found in glance" + % image_identifier) + raise exception.ImageNotFound(image_name=image_identifier) + else: + try: + image_list = nova_client.images.list() + except clients.novaclient.exceptions.ClientException as ex: + raise exception.ServerError(message=str(ex)) + image_names = dict( + (o.id, o.name) + for o in image_list if o.name == image_identifier) + if len(image_names) == 0: + logger.info("Image %s was not found in glance" % + image_identifier) + raise exception.ImageNotFound(image_name=image_identifier) + elif len(image_names) > 1: + logger.info("Mulitple images %s were found in glance with name" + % image_identifier) + raise exception.NoUniqueImageFound(image_name=image_identifier) + image_id = image_names.popitem()[0] + return image_id + + +def get_flavor_id(nova_client, flavor): + ''' + Get the id for the specified flavor name. + + :param nova_client: the nova client to use + :param flavor: the name of the flavor to find + :returns: the id of :flavor: + :raises: exception.FlavorMissing + ''' + flavor_id = None + flavor_list = nova_client.flavors.list() + for o in flavor_list: + if o.name == flavor: + flavor_id = o.id + break + if flavor_id is None: + raise exception.FlavorMissing(flavor_id=flavor) + return flavor_id + + +def get_keypair(nova_client, key_name): + ''' + Get the public key specified by :key_name: + + :param nova_client: the nova client to use + :param key_name: the name of the key to look for + :returns: the keypair (name, public_key) for :key_name: + :raises: exception.UserKeyPairMissing + ''' + for keypair in nova_client.keypairs.list(): + if keypair.name == key_name: + return keypair + raise exception.UserKeyPairMissing(key_name=key_name) + + +def build_userdata(resource, userdata=None): + ''' + 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 + :returns: multipart mime as a string + ''' + + 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): + data = pkgutil.get_data('heat', 'cloudinit/%s' % fn) + data = data.replace('@INSTANCE_USER@', + cfg.CONF.instance_user) + return data + + attachments = [(read_cloudinit_file('config'), 'cloud-config'), + (read_cloudinit_file('boothook.sh'), 'boothook.sh', + 'cloud-boothook'), + (read_cloudinit_file('part_handler.py'), + 'part-handler.py'), + (userdata, 'cfn-userdata', 'x-cfninitdata'), + (read_cloudinit_file('loguserdata.py'), + 'loguserdata.py', 'x-shellscript')] + + if 'Metadata' in resource.t: + attachments.append((json.dumps(resource.metadata), + 'cfn-init-data', 'x-cfninitdata')) + + attachments.append((cfg.CONF.heat_watch_server_url, + 'cfn-watch-server', 'x-cfninitdata')) + + 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(cfg.CONF.heat_metadata_server_url) + cw_url = 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() diff --git a/heat/tests/test_nova_utils.py b/heat/tests/test_nova_utils.py new file mode 100644 index 00000000..d9be8317 --- /dev/null +++ b/heat/tests/test_nova_utils.py @@ -0,0 +1,107 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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 uuid + +from heat.common import exception +from heat.engine.resources import nova_utils +from heat.tests.common import HeatTestCase + + +class NovaUtilsTests(HeatTestCase): + """ + Basic tests for the helper methods in + :module:'heat.engine.resources.nova_utils'. + """ + + def setUp(self): + super(NovaUtilsTests, self).setUp() + self.nova_client = self.m.CreateMockAnything() + + def test_get_image_id(self): + """Tests the get_image_id function.""" + my_image = self.m.CreateMockAnything() + img_id = str(uuid.uuid4()) + img_name = 'myfakeimage' + my_image.id = img_id + my_image.name = img_name + self.nova_client.images = self.m.CreateMockAnything() + self.nova_client.images.get(img_id).AndReturn(my_image) + self.nova_client.images.list().MultipleTimes().AndReturn([my_image]) + self.m.ReplayAll() + self.assertEqual(img_id, nova_utils.get_image_id(self.nova_client, + img_id)) + self.assertEqual(img_id, nova_utils.get_image_id(self.nova_client, + 'myfakeimage')) + self.assertRaises(exception.ImageNotFound, nova_utils.get_image_id, + self.nova_client, 'noimage') + self.m.VerifyAll() + + 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, nova_utils.get_flavor_id(self.nova_client, + flav_name)) + self.assertRaises(exception.FlavorMissing, nova_utils.get_flavor_id, + self.nova_client, '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.list().MultipleTimes().AndReturn([my_key]) + self.m.ReplayAll() + self.assertEqual(my_key, nova_utils.get_keypair(self.nova_client, + my_key_name)) + self.assertRaises(exception.UserKeyPairMissing, nova_utils.get_keypair, + self.nova_client, 'notakey') + self.m.VerifyAll() + + def test_build_userdata(self): + """Tests the build_userdata function.""" + resource = self.m.CreateMockAnything() + resource.t = {} + self.m.StubOutWithMock(nova_utils.cfg, 'CONF') + cnf = nova_utils.cfg.CONF + cnf.instance_user = 'testuser' + cnf.heat_metadata_server_url = 'http://localhost:123' + cnf.heat_watch_server_url = 'http://localhost:345' + cnf.instance_connection_is_secure = False + cnf.instance_connection_https_validate_certificates = False + self.m.ReplayAll() + data = nova_utils.build_userdata(resource) + self.assertTrue("Content-Type: text/cloud-config;" in data) + self.assertTrue("Content-Type: text/cloud-boothook;" in data) + self.assertTrue("Content-Type: text/part-handler;" in data) + self.assertTrue("Content-Type: text/x-cfninitdata;" in data) + self.assertTrue("Content-Type: text/x-shellscript;" in data) + self.assertTrue("http://localhost:345" in data) + self.assertTrue("http://localhost:123" in data) + self.assertTrue("[Boto]" in data) + self.assertTrue('testuser' in data) + self.m.VerifyAll() -- 2.45.2