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
This commit is contained in:
Randall Burt 2013-08-06 17:07:26 -05:00
parent 5270ec8d9f
commit 7601916b0c
2 changed files with 282 additions and 0 deletions

View File

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

View File

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