Add user_data support
We should allow users to specify user data when creating an instance. Change-Id: I8fcd9aa4ca091ba3435292b519a999653a6d5b19
This commit is contained in:
parent
62dd3c37ec
commit
c610213199
|
@ -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**
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -11,5 +11,6 @@
|
|||
"net_id": "8e8ceb07-4641-4188-9b22-840755e92ee2",
|
||||
"port_type": "10GE"
|
||||
}
|
||||
]
|
||||
],
|
||||
"user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg=="
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" %
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue