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:
parent
5f79b9e43f
commit
7d34b341a9
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user