Add user_data support

We should allow users to specify user data when creating an
instance.

Change-Id: I8fcd9aa4ca091ba3435292b519a999653a6d5b19
This commit is contained in:
Zhenguo Niu 2017-04-11 14:49:43 +08:00
parent 62dd3c37ec
commit c610213199
17 changed files with 90 additions and 27 deletions

View File

@ -38,6 +38,7 @@ Request
- networks: networks
- networks.net_id: network_uuid
- networks.port_type: network_port_type
- user_data: user_data
**Example Create Instance: JSON request**

View File

@ -338,6 +338,12 @@ updated_at:
in: body
required: true
type: string
user_data:
description: |
Configuration information or scripts to use upon launch. Must be Base64 encoded.
in: body
required: false
type: string
user_id_body:
description: |
The user ID of the user who owns the instance.

View File

@ -11,5 +11,6 @@
"net_id": "8e8ceb07-4641-4188-9b22-840755e92ee2",
"port_type": "10GE"
}
]
],
"user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg=="
}

View File

@ -582,6 +582,7 @@ class InstanceController(InstanceControllerBase):
requested_networks = instance.pop('networks', None)
instance_type_uuid = instance.get('instance_type_uuid')
image_uuid = instance.get('image_uuid')
user_data = instance.get('user_data')
try:
instance_type = objects.InstanceType.get(pecan.request.context,
@ -596,6 +597,7 @@ class InstanceController(InstanceControllerBase):
availability_zone=instance.get('availability_zone'),
extra=instance.get('extra'),
requested_networks=requested_networks,
user_data=user_data,
min_count=min_count,
max_count=max_count)
except exception.InstanceTypeNotFound:
@ -615,6 +617,8 @@ class InstanceController(InstanceControllerBase):
raise wsme.exc.ClientSideError(
msg, status_code=http_client.BAD_REQUEST)
except (exception.GlanceConnectionFailed,
exception.InstanceUserDataMalformed,
exception.InstanceUserDataTooLarge,
exception.NetworkRequiresSubnet,
exception.NetworkNotFound) as e:
raise wsme.exc.ClientSideError(

View File

@ -37,6 +37,7 @@ create_instance = {
'additionalProperties': False,
},
},
'user_data': {'type': 'string', 'format': 'base64'},
'min_count': {'type': 'integer', 'minimum': 1},
'max_count': {'type': 'integer', 'minimum': 1},
'extra': parameter_types.extra,

View File

@ -386,4 +386,14 @@ class ConfigDriveUnknownFormat(MoganException):
"iso9660 or vfat.")
class InstanceUserDataTooLarge(MoganException):
msg_fmt = _("User data too large. User data must be no larger than "
"%(maxsize)s bytes once base64 encoded. Your data is "
"%(length)d bytes")
class InstanceUserDataMalformed(MoganException):
msg_fmt = _("User data needs to be valid base 64.")
ObjectActionError = obj_exc.ObjectActionError

View File

@ -16,6 +16,7 @@
"""Handles all requests relating to compute resources"""
from oslo_log import log
from oslo_serialization import base64 as base64utils
from oslo_utils import excutils
from oslo_utils import uuidutils
import six
@ -33,6 +34,8 @@ from mogan.objects import quota
LOG = log.getLogger(__name__)
MAX_USERDATA_SIZE = 65535
def check_instance_lock(function):
@six.wraps(function)
@ -70,10 +73,21 @@ class API(object):
def _validate_and_build_base_options(self, context, instance_type,
image_uuid, name, description,
availability_zone, extra,
requested_networks,
requested_networks, user_data,
max_count):
"""Verify all the input parameters"""
if user_data:
l = len(user_data)
if l > MAX_USERDATA_SIZE:
raise exception.InstanceUserDataTooLarge(
length=l, maxsize=MAX_USERDATA_SIZE)
try:
base64utils.decode_as_bytes(user_data)
except TypeError:
raise exception.InstanceUserDataMalformed()
# Note: max_count is the number of instances requested by the user,
# max_network_count is the maximum number of instances taking into
# account any network quotas
@ -206,7 +220,7 @@ class API(object):
def _create_instance(self, context, instance_type, image_uuid,
name, description, availability_zone, extra,
requested_networks, min_count, max_count):
requested_networks, user_data, min_count, max_count):
"""Verify all the input parameters"""
# Verify the specified image exists
@ -215,7 +229,7 @@ class API(object):
base_options, max_net_count = self._validate_and_build_base_options(
context, instance_type, image_uuid, name, description,
availability_zone, extra, requested_networks, max_count)
availability_zone, extra, requested_networks, user_data, max_count)
# max_net_count is the maximum number of instances requested by the
# user adjusted for any network quota constraints, including
@ -247,6 +261,7 @@ class API(object):
for instance in instances:
self.engine_rpcapi.create_instance(context, instance,
requested_networks,
user_data,
request_spec,
filter_properties=None)
@ -254,8 +269,8 @@ class API(object):
def create(self, context, instance_type, image_uuid,
name=None, description=None, availability_zone=None,
extra=None, requested_networks=None, min_count=None,
max_count=None):
extra=None, requested_networks=None, user_data=None,
min_count=None, max_count=None):
"""Provision instances
Sending instance information to the engine and will handle
@ -273,8 +288,8 @@ class API(object):
return self._create_instance(context, instance_type,
image_uuid, name, description,
availability_zone, extra,
requested_networks, min_count,
max_count)
requested_networks, user_data,
min_count, max_count)
def _delete_instance(self, context, instance):

View File

@ -91,7 +91,7 @@ class BaseEngineDriver(object):
"""
raise NotImplementedError()
def spawn(self, context, instance):
def spawn(self, context, instance, user_data):
"""Create a new instance on the provision platform.
:param context: security context

View File

@ -290,7 +290,8 @@ class IronicDriver(base_driver.BaseEngineDriver):
except client_e.BadRequest:
pass
def _generate_configdrive(self, context, instance, node, extra_md=None):
def _generate_configdrive(self, context, instance, node, extra_md=None,
user_data=None):
"""Generate a config drive.
:param instance: The instance object.
@ -302,7 +303,7 @@ class IronicDriver(base_driver.BaseEngineDriver):
if not extra_md:
extra_md = {}
i_meta = instance_metadata.InstanceMetadata(instance,
i_meta = instance_metadata.InstanceMetadata(instance, user_data,
extra_md=extra_md)
with tempfile.NamedTemporaryFile() as uncompressed:
@ -319,7 +320,7 @@ class IronicDriver(base_driver.BaseEngineDriver):
compressed.seek(0)
return base64.b64encode(compressed.read())
def spawn(self, context, instance):
def spawn(self, context, instance, user_data):
"""Deploy an instance.
:param context: The security context.
@ -358,7 +359,8 @@ class IronicDriver(base_driver.BaseEngineDriver):
try:
configdrive_value = self._generate_configdrive(
context, instance, node, extra_md=extra_md)
context, instance, node, extra_md=extra_md,
user_data=user_data)
except Exception as e:
with excutils.save_and_reraise_exception():
msg = ("Failed to build configdrive: %s" %

View File

@ -199,7 +199,7 @@ class CreateInstanceTask(flow_utils.MoganTask):
"""Build and deploy the instance."""
def __init__(self, driver):
requires = ['instance', 'context']
requires = ['instance', 'user_data', 'context']
super(CreateInstanceTask, self).__init__(addons=[ACTION],
requires=requires)
self.driver = driver
@ -209,8 +209,8 @@ class CreateInstanceTask(flow_utils.MoganTask):
loopingcall.LoopingCallTimeOut,
]
def execute(self, context, instance):
self.driver.spawn(context, instance)
def execute(self, context, instance, user_data):
self.driver.spawn(context, instance, user_data)
LOG.info('Successfully provisioned Ironic node %s',
instance.node_uuid)
@ -225,8 +225,8 @@ class CreateInstanceTask(flow_utils.MoganTask):
return False
def get_flow(context, manager, instance, requested_networks, ports,
request_spec, filter_properties):
def get_flow(context, manager, instance, requested_networks, user_data,
ports, request_spec, filter_properties):
"""Constructs and returns the manager entrypoint flow
@ -249,6 +249,7 @@ def get_flow(context, manager, instance, requested_networks, ports,
'request_spec': request_spec,
'instance': instance,
'requested_networks': requested_networks,
'user_data': user_data,
'ports': ports
}

View File

@ -342,7 +342,7 @@ class EngineManager(base_manager.BaseEngineManager):
@wrap_instance_fault
def create_instance(self, context, instance, requested_networks,
request_spec=None, filter_properties=None):
user_data, request_spec=None, filter_properties=None):
"""Perform a deployment."""
LOG.debug("Starting instance...", instance=instance)
notifications.notify_about_instance_action(
@ -390,6 +390,7 @@ class EngineManager(base_manager.BaseEngineManager):
self,
instance,
requested_networks,
user_data,
node['ports'],
request_spec,
filter_properties,

View File

@ -50,11 +50,12 @@ class EngineAPI(object):
serializer=serializer)
def create_instance(self, context, instance, requested_networks,
request_spec, filter_properties):
user_data, request_spec, filter_properties):
"""Signal to engine service to perform a deployment."""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
cctxt.cast(context, 'create_instance', instance=instance,
requested_networks=requested_networks,
user_data=user_data,
request_spec=request_spec,
filter_properties=filter_properties)

View File

@ -19,6 +19,7 @@
import posixpath
from oslo_log import log as logging
from oslo_serialization import base64
from oslo_serialization import jsonutils
from oslo_utils import timeutils
@ -31,6 +32,7 @@ OPENSTACK_VERSIONS = [
VERSION = "version"
MD_JSON_NAME = "meta_data.json"
UD_NAME = "user_data"
LOG = logging.getLogger(__name__)
@ -46,7 +48,7 @@ class InvalidMetadataPath(Exception):
class InstanceMetadata(object):
"""Instance metadata."""
def __init__(self, instance, extra_md=None):
def __init__(self, instance, user_data=None, extra_md=None):
"""Creation of this object should basically cover all time consuming
collection. Methods after that should not cause time delays due to
network operations or lengthy cpu operations.
@ -58,6 +60,12 @@ class InstanceMetadata(object):
self.instance = instance
self.extra_md = extra_md
self.availability_zone = instance.availability_zone
if user_data is not None:
self.userdata_raw = base64.decode_as_bytes(user_data)
else:
self.userdata_raw = None
# TODO(zhenguo): Add hostname to instance object
self.hostname = instance.name
self.uuid = instance.uuid
@ -69,7 +77,8 @@ class InstanceMetadata(object):
if self.route_configuration:
return self.route_configuration
path_handlers = {MD_JSON_NAME: self._metadata_as_json}
path_handlers = {UD_NAME: self._user_data,
MD_JSON_NAME: self._metadata_as_json}
self.route_configuration = RouteConfiguration(path_handlers)
return self.route_configuration
@ -88,6 +97,11 @@ class InstanceMetadata(object):
return jsonutils.dump_as_bytes(metadata)
def _user_data(self, version, path):
if self.userdata_raw is None:
raise KeyError(path)
return self.userdata_raw
def lookup(self, path):
if path == "" or path[0] != "/":
path = posixpath.normpath("/" + path)
@ -135,6 +149,10 @@ class InstanceMetadata(object):
path = 'openstack/%s/%s' % (version, MD_JSON_NAME)
yield (path, self.lookup(path))
path = 'openstack/%s/%s' % (version, UD_NAME)
if self.userdata_raw is not None:
yield (path, self.lookup(path))
class RouteConfiguration(object):
"""Routes metadata paths to request handlers."""

View File

@ -1,7 +1,6 @@
# Copyright 2016 Huawei Technologies Co.,LTD.
# All Rights Reserved.
#
#
# 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
@ -23,7 +22,7 @@ from mogan import objects
from mogan.objects import base
from mogan.objects import fields as object_fields
OPTIONAL_ATTRS = ['nics', 'fault', ]
OPTIONAL_ATTRS = ['nics', 'fault']
LOG = logging.getLogger(__name__)

View File

@ -57,5 +57,5 @@ class CreateInstanceFlowTestCase(base.TestCase):
instance_obj = obj_utils.get_test_instance(self.ctxt)
mock_spawn.side_effect = None
task.execute(self.ctxt, instance_obj)
mock_spawn.assert_called_once_with(self.ctxt, instance_obj)
task.execute(self.ctxt, instance_obj, None)
mock_spawn.assert_called_once_with(self.ctxt, instance_obj, None)

View File

@ -60,6 +60,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
availability_zone='test_az',
extra={'k1', 'v1'},
requested_networks=None,
user_data=None,
max_count=2)
self.assertEqual('fake-user', base_opts['user_id'])
@ -135,7 +136,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
mock_validate.assert_called_once_with(
self.context, instance_type, 'fake-uuid', 'fake-name',
'fake-descritpion', 'test_az', {'k1', 'v1'}, requested_networks,
max_count)
None, max_count)
self.assertTrue(mock_create.called)
self.assertTrue(mock_get_image.called)
res = self.dbapi._get_quota_usages(self.context, self.project_id)
@ -195,6 +196,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
'test_az',
{'k1', 'v1'},
requested_networks,
None,
min_count,
max_count)

View File

@ -107,6 +107,7 @@ class RPCAPITestCase(base.DbTestCase):
version='1.0',
instance=self.fake_instance_obj,
requested_networks=[],
user_data=None,
request_spec=None,
filter_properties=None)