OS::Nova::Server support for software config

Changes to OS::Nova::Server to deliver all
currently available SoftwareDeployment metadata to the server
via os-collect-config polling.

user_data_format has a new option SOFTWARE_CONFIG, and adds
behavours in SOFTWARE_CONFIG specific code paths.

metadata property is overridden to query available config
data every time metadata is accessed.

This resource now extends StackUser but the user and keypair are only created
for SOFTWARE_CONFIG. os-collect-config uses the keypair to poll for metadata
changes. An access_allowed handler is registered so that cfn metadata polling
gets authorised against this resource.

access_allowed simply limits access to the current resource, which seems
appropriate since this resource created the user.

nova_utils.build_userdata has become aware of user_data_format
SOFTWARE_CONFIG, so it only includes the cloud-init attachments
which os-collect-config actually needs.

partial blueprint: hot-software-config

Change-Id: Iaff98a471132f97db3adce301a61068d0770b540
This commit is contained in:
Steve Baker 2014-02-26 10:19:40 +13:00
parent 5f79b9e43f
commit 7d34b341a9
4 changed files with 223 additions and 45 deletions

View File

@ -13,6 +13,7 @@
# under the License.
"""Utilities for Resources that use the OpenStack Nova API."""
import email
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
@ -176,6 +177,9 @@ def build_userdata(resource, userdata=None, instance_user=None,
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]
@ -192,41 +196,62 @@ def build_userdata(resource, userdata=None, instance_user=None,
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')]
'cloud-boothook')]
attachments.append((read_cloudinit_file('part_handler.py'),
'part-handler.py'))
if 'Metadata' in resource.t:
attachments.append((json.dumps(resource.metadata),
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:
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'))
metadata = resource.metadata
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'))
attachments.append((cfg.CONF.heat_metadata_server_url,
'cfn-metadata-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 = urlutils.urlparse(cfg.CONF.heat_metadata_server_url)
cw_url = urlutils.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'))
# 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 = urlutils.urlparse(cfg.CONF.heat_metadata_server_url)
cw_url = urlutils.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)

View File

@ -17,8 +17,10 @@ from oslo.config import cfg
cfg.CONF.import_opt('instance_user', 'heat.common.config')
from heat.common import exception
from heat.db import api as db_api
from heat.engine import clients
from heat.engine import scheduler
from heat.engine import stack_user
from heat.engine.resources import nova_utils
from heat.engine.resources.software_config import software_config as sc
from heat.engine import constraints
@ -32,7 +34,7 @@ from heat.openstack.common import uuidutils
logger = logging.getLogger(__name__)
class Server(resource.Resource):
class Server(stack_user.StackUser):
PROPERTIES = (
NAME, IMAGE, BLOCK_DEVICE_MAPPING, FLAVOR,
@ -69,9 +71,9 @@ class Server(resource.Resource):
)
_SOFTWARE_CONFIG_FORMATS = (
HEAT_CFNTOOLS, RAW
HEAT_CFNTOOLS, RAW, SOFTWARE_CONFIG
) = (
'HEAT_CFNTOOLS', 'RAW'
'HEAT_CFNTOOLS', 'RAW', 'SOFTWARE_CONFIG'
)
properties_schema = {
@ -224,8 +226,11 @@ class Server(resource.Resource):
properties.Schema.STRING,
_('How the user_data should be formatted for the server. For '
'HEAT_CFNTOOLS, the user_data is bundled as part of the '
'heat-cfntools cloud-init boot configuration data. For RAW, '
'the user_data is passed to Nova unmodified.'),
'heat-cfntools cloud-init boot configuration data. For RAW '
'the user_data is passed to Nova unmodified. '
'For SOFTWARE_CONFIG user_data is bundled as part of the '
'software config data, and metadata is derived from any '
'associated SoftwareDeployment resources.'),
default=HEAT_CFNTOOLS,
constraints=[
constraints.AllowedValues(_SOFTWARE_CONFIG_FORMATS),
@ -293,6 +298,8 @@ class Server(resource.Resource):
def __init__(self, name, json_snippet, stack):
super(Server, self).__init__(name, json_snippet, stack)
if self.access_key:
self._register_access_key()
def physical_resource_name(self):
name = self.properties.get(self.NAME)
@ -309,22 +316,91 @@ class Server(resource.Resource):
# This method is overridden by the derived CloudServer resource
return self.properties.get(self.KEY_NAME)
@staticmethod
def _get_deployments_metadata(heatclient, server_id):
return heatclient.software_deployments.metadata(
server_id=server_id)
def _build_deployments_metadata(self):
meta = {'os-collect-config': {'cfn': {
'metadata_url': '%s/v1/' % cfg.CONF.heat_metadata_server_url,
'access_key_id': self.access_key,
'secret_access_key': self.secret_key,
'stack_name': self.stack.name,
'path': '%s.Metadata' % self.name}
}}
# cannot query the deployments if the nova server does
# not exist yet
if not self.resource_id:
return meta
meta['deployments'] = self._get_deployments_metadata(
self.heat(), self.resource_id)
return 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
self.stack.register_access_allowed_handler(
self.access_key, access_allowed)
@property
def access_key(self):
try:
return db_api.resource_data_get(self, 'access_key')
except exception.NotFound:
pass
@property
def secret_key(self):
try:
return db_api.resource_data_get(self, 'secret_key')
except exception.NotFound:
pass
@property
def metadata(self):
if self.user_data_software_config():
return self._build_deployments_metadata()
else:
return self._metadata
@metadata.setter
def metadata(self, metadata):
if not self.user_data_software_config():
self._metadata = metadata
def user_data_raw(self):
return self.properties.get(self.USER_DATA_FORMAT) == 'RAW'
return self.properties.get(self.USER_DATA_FORMAT) == self.RAW
def user_data_software_config(self):
return self.properties.get(
self.USER_DATA_FORMAT) == self.SOFTWARE_CONFIG
def handle_create(self):
security_groups = self.properties.get(self.SECURITY_GROUPS)
user_data_format = self.properties.get(self.USER_DATA_FORMAT)
ud_content = self.properties.get(self.USER_DATA)
if self.user_data_raw() and uuidutils.is_uuid_like(ud_content):
# attempt to load the userdata from software config
try:
ud_content = sc.SoftwareConfig.get_software_config(
self.heat(), ud_content)
except exception.SoftwareConfigMissing:
# no config was found, so do not modify the user_data
pass
if self.user_data_software_config() or self.user_data_raw():
if uuidutils.is_uuid_like(ud_content):
# attempt to load the userdata from software config
try:
ud_content = sc.SoftwareConfig.get_software_config(
self.heat(), ud_content)
except exception.SoftwareConfigMissing:
# no config was found, so do not modify the user_data
pass
if self.user_data_software_config():
self._create_user()
self._create_keypair()
self._register_access_key()
userdata = nova_utils.build_userdata(
self,
@ -658,6 +734,9 @@ class Server(resource.Resource):
if self.resource_id is None:
return
if self.user_data_software_config():
self._delete_user()
try:
server = self.nova().servers.get(self.resource_id)
except clients.novaclient.exceptions.NotFound:

View File

@ -179,7 +179,7 @@ class NovaUtilsUserdataTests(HeatTestCase):
def test_build_userdata(self):
"""Tests the build_userdata function."""
resource = self.m.CreateMockAnything()
resource.t = {}
resource.metadata = {}
self.m.StubOutWithMock(nova_utils.cfg, 'CONF')
cnf = nova_utils.cfg.CONF
cnf.instance_user = self.conf_user

View File

@ -18,7 +18,8 @@ import mox
import uuid
from heat.engine import environment
from heat.tests.v1_1 import fakes
from heat.tests.v1_1 import fakes as fakes_v1_1
from heat.tests import fakes
from heat.common import exception
from heat.common import template_format
from heat.engine import clients
@ -65,7 +66,9 @@ wp_template = '''
class ServersTest(HeatTestCase):
def setUp(self):
super(ServersTest, self).setUp()
self.fc = fakes.FakeClient()
self.fc = fakes_v1_1.FakeClient()
self.fkc = fakes.FakeKeystoneClient()
utils.setup_dummy_db()
self.limits = self.m.CreateMockAnything()
self.limits.absolute = self._limits_absolute()
@ -451,6 +454,77 @@ class ServersTest(HeatTestCase):
scheduler.TaskRunner(server.create)()
self.m.VerifyAll()
def test_server_create_software_config(self):
return_server = self.fc.servers.list()[1]
stack_name = 'software_config_s'
(t, stack) = self._setup_test_stack(stack_name)
t['Resources']['WebServer']['Properties']['user_data_format'] = \
'SOFTWARE_CONFIG'
stack.stack_user_project_id = '8888'
server = servers.Server('WebServer',
t['Resources']['WebServer'], stack)
self.m.StubOutWithMock(server, 'nova')
self.m.StubOutWithMock(server, 'keystone')
self.m.StubOutWithMock(server, 'heat')
self.m.StubOutWithMock(server, '_get_deployments_metadata')
server.nova().MultipleTimes().AndReturn(self.fc)
self.m.StubOutWithMock(clients.OpenStackClients, 'nova')
clients.OpenStackClients.nova().MultipleTimes().AndReturn(self.fc)
server.keystone().MultipleTimes().AndReturn(self.fkc)
server.heat().MultipleTimes().AndReturn(self.fc)
server._get_deployments_metadata(
self.fc, 5678).AndReturn({'foo': 'bar'})
server.t = server.stack.resolve_runtime_data(server.t)
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(
image=744, flavor=3, key_name='test',
name=utils.PhysName(stack_name, server.name),
security_groups=[],
userdata=mox.IgnoreArg(), scheduler_hints=None,
meta=None, nics=None, availability_zone=None,
block_device_mapping=None, config_drive=None,
disk_config=None, reservation_id=None, files={},
admin_pass=None).AndReturn(
return_server)
self.m.ReplayAll()
scheduler.TaskRunner(server.create)()
self.assertEqual('4567', server.access_key)
self.assertEqual('8901', server.secret_key)
self.assertEqual('1234', server._get_user_id())
self.assertTrue(stack.access_allowed('4567', 'WebServer'))
self.assertFalse(stack.access_allowed('45678', 'WebServer'))
self.assertFalse(stack.access_allowed('4567', 'wWebServer'))
self.assertEqual({
'os-collect-config': {
'cfn': {
'access_key_id': '4567',
'metadata_url': '/v1/',
'path': 'WebServer.Metadata',
'secret_access_key': '8901',
'stack_name': 'software_config_s'
}
},
'deployments': {'foo': 'bar'}
}, server.metadata)
created_server = servers.Server('WebServer',
t['Resources']['WebServer'], stack)
self.assertEqual('4567', created_server.access_key)
self.assertTrue(stack.access_allowed('4567', 'WebServer'))
self.m.VerifyAll()
@mock.patch.object(clients.OpenStackClients, 'nova')
def test_server_create_default_admin_pass(self, mock_nova):
return_server = self.fc.servers.list()[1]