From 70020a94473813885d02d3b09746e279d820ccb7 Mon Sep 17 00:00:00 2001 From: Kevin Zhao Date: Mon, 17 Jul 2017 22:47:37 +0800 Subject: [PATCH] Add capsule controller in API side and add create method 1.Add new entrypoint 'experimental', devstack will be modified to support this. 2.Add controller Capsule 3.Add capsule create method 4.Refactor the code for _do_container_create 5.Add one capsule template All capsule method in the API level will use: /experimental/capsules API, apart from v1 API. Part of blueprint introduce-compose Change-Id: Ia031ee5beb48ec70dab6bb371944140d025a499b Signed-off-by: Kevin Zhao --- etc/zun/policy.json | 9 +- template/capsule/capsule.yaml | 62 +++++ zun/api/controllers/experimental/__init__.py | 152 +++++++++++ zun/api/controllers/experimental/capsules.py | 244 ++++++++++++++++++ .../controllers/experimental/collection.py | 43 +++ .../experimental/schemas/__init__.py | 0 .../experimental/schemas/capsules.py | 26 ++ .../experimental/views/__init__.py | 0 .../experimental/views/capsules_view.py | 49 ++++ zun/api/controllers/root.py | 4 +- zun/api/controllers/v1/__init__.py | 2 + zun/common/exception.py | 12 + zun/common/utils.py | 25 ++ zun/common/validation/parameter_types.py | 4 + zun/compute/api.py | 14 + zun/compute/manager.py | 80 ++++-- zun/compute/rpcapi.py | 7 + 17 files changed, 711 insertions(+), 22 deletions(-) create mode 100644 template/capsule/capsule.yaml create mode 100644 zun/api/controllers/experimental/__init__.py create mode 100644 zun/api/controllers/experimental/capsules.py create mode 100644 zun/api/controllers/experimental/collection.py create mode 100644 zun/api/controllers/experimental/schemas/__init__.py create mode 100644 zun/api/controllers/experimental/schemas/capsules.py create mode 100644 zun/api/controllers/experimental/views/__init__.py create mode 100644 zun/api/controllers/experimental/views/capsules_view.py diff --git a/etc/zun/policy.json b/etc/zun/policy.json index 6195bdfc0..613840c71 100644 --- a/etc/zun/policy.json +++ b/etc/zun/policy.json @@ -43,5 +43,12 @@ "zun-service:get_all": "rule:admin_api", "host:get_all": "rule:admin_api", - "host:get": "rule:admin_api" + "host:get": "rule:admin_api", + "capsule:create": "rule:default", + "capsule:delete": "rule:default", + "capsule:delete_all_tenants": "rule:admin_api", + "capsule:get": "rule:default", + "capsule:get_one_all_tenants": "rule:admin_api", + "capsule:get_all": "rule:default", + "capsule:get_all_all_tenants": "rule:admin_api", } diff --git a/template/capsule/capsule.yaml b/template/capsule/capsule.yaml new file mode 100644 index 000000000..300529f99 --- /dev/null +++ b/template/capsule/capsule.yaml @@ -0,0 +1,62 @@ +capsule_template_version: 2017-06-21 +# use "-" because that the fields have many items +capsule_version: beta +kind: capsule +metadata: + name: capsule-example + labels: + - app: web + - nihao: baibai +restart_policy: always +spec: + containers: + - image: ubuntu + command: + - "/bin/bash" + image_pull_policy: ifnotpresent + workdir: /root + labels: + app: web + ports: + - name: nginx-port + containerPort: 80 + hostPort: 80 + protocol: TCP + resources: + allocation: + cpu: 1 + memory: 1024 + environment: + PATCH: /usr/local/bin + - image: centos + command: + - "echo" + args: + - "Hello" + - "World" + image_pull_policy: ifnotpresent + workdir: /root + labels: + app: web01 + ports: + - name: nginx-port + containerPort: 80 + hostPort: 80 + protocol: TCP + - name: mysql-port + containerPort: 3306 + hostPort: 3306 + protocol: TCP + resources: + allocation: + cpu: 1 + memory: 1024 + environment: + NWH: /usr/bin/ + volumes: + - name: volume1 + drivers: cinder + driverOptions: options + size: 5GB + volumeType: type1 + image: ubuntu-xenial diff --git a/zun/api/controllers/experimental/__init__.py b/zun/api/controllers/experimental/__init__.py new file mode 100644 index 000000000..5479ec4d6 --- /dev/null +++ b/zun/api/controllers/experimental/__init__.py @@ -0,0 +1,152 @@ +# Copyright 2017 ARM Holdings. +# +# 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. + +""" +Experimental of the Zun API + +NOTE: IN PROGRESS AND NOT FULLY IMPLEMENTED. +""" + +from oslo_log import log as logging +import pecan + +from zun.api.controllers import base as controllers_base +from zun.api.controllers.experimental import capsules as capsule_controller +from zun.api.controllers import link +from zun.api.controllers import versions as ver +from zun.api import http_error +from zun.common.i18n import _ + +LOG = logging.getLogger(__name__) + + +BASE_VERSION = 1 + +MIN_VER_STR = '%s %s' % (ver.Version.service_string, ver.BASE_VER) + +MAX_VER_STR = '%s %s' % (ver.Version.service_string, ver.CURRENT_MAX_VER) + +MIN_VER = ver.Version({ver.Version.string: MIN_VER_STR}, + MIN_VER_STR, MAX_VER_STR) +MAX_VER = ver.Version({ver.Version.string: MAX_VER_STR}, + MIN_VER_STR, MAX_VER_STR) + + +class MediaType(controllers_base.APIBase): + """A media type representation.""" + + fields = ( + 'base', + 'type', + ) + + +class Experimental(controllers_base.APIBase): + """The representation of the version experimental of the API.""" + + fields = ( + 'id', + 'media_types', + 'links', + 'capsules' + ) + + @staticmethod + def convert(): + experimental = Experimental() + experimental.id = "experimental" + experimental.links = [link.make_link('self', pecan.request.host_url, + 'experimental', '', + bookmark=True), + link.make_link('describedby', + 'https://docs.openstack.org', + 'developer/zun/dev', + 'api-spec-v1.html', + bookmark=True, + type='text/html')] + experimental.media_types = \ + [MediaType(base='application/json', + type='application/vnd.openstack.' + 'zun.experimental+json')] + experimental.capsules = [link.make_link('self', + pecan.request.host_url, + 'capsules', ''), + link.make_link('bookmark', + pecan.request.host_url, + 'capsules', '', + bookmark=True)] + return experimental + + +class Controller(controllers_base.Controller): + """Version expereimental API controller root.""" + + capsules = capsule_controller.CapsuleController() + + @pecan.expose('json') + def get(self): + return Experimental.convert() + + def _check_version(self, version, headers=None): + if headers is None: + headers = {} + # ensure that major version in the URL matches the header + if version.major != BASE_VERSION: + raise http_error.HTTPNotAcceptableAPIVersion(_( + "Mutually exclusive versions requested. Version %(ver)s " + "requested but not supported by this service. " + "The supported version range is: " + "[%(min)s, %(max)s].") % {'ver': version, + 'min': MIN_VER_STR, + 'max': MAX_VER_STR}, + headers=headers, + max_version=str(MAX_VER), + min_version=str(MIN_VER)) + # ensure the minor version is within the supported range + if version < MIN_VER or version > MAX_VER: + raise http_error.HTTPNotAcceptableAPIVersion(_( + "Version %(ver)s was requested but the minor version is not " + "supported by this service. The supported version range is: " + "[%(min)s, %(max)s].") % {'ver': version, 'min': MIN_VER_STR, + 'max': MAX_VER_STR}, + headers=headers, + max_version=str(MAX_VER), + min_version=str(MIN_VER)) + + @pecan.expose() + def _route(self, args): + version = ver.Version( + pecan.request.headers, MIN_VER_STR, MAX_VER_STR) + + # Always set the basic version headers + pecan.response.headers[ver.Version.min_string] = MIN_VER_STR + pecan.response.headers[ver.Version.max_string] = MAX_VER_STR + pecan.response.headers[ver.Version.string] = " ".join( + [ver.Version.service_string, str(version)]) + pecan.response.headers["vary"] = ver.Version.string + + # assert that requested version is supported + self._check_version(version, pecan.response.headers) + pecan.request.version = version + if pecan.request.body: + msg = ("Processing request: url: %(url)s, %(method)s, " + "body: %(body)s" % + {'url': pecan.request.url, + 'method': pecan.request.method, + 'body': pecan.request.body}) + LOG.debug(msg) + + return super(Controller, self)._route(args) + +__all__ = (Controller) diff --git a/zun/api/controllers/experimental/capsules.py b/zun/api/controllers/experimental/capsules.py new file mode 100644 index 000000000..5cc3800bc --- /dev/null +++ b/zun/api/controllers/experimental/capsules.py @@ -0,0 +1,244 @@ +# Copyright 2017 ARM Holdings. +# +# 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. + +from oslo_log import log as logging +from oslo_utils import uuidutils +import pecan + +from zun.api.controllers import base +from zun.api.controllers.experimental import collection +from zun.api.controllers.experimental.schemas import capsules as schema +from zun.api.controllers.experimental.views import capsules_view as view +from zun.api.controllers import link +from zun.api import utils as api_utils +from zun.common import consts +from zun.common import exception +from zun.common import name_generator +from zun.common import policy +from zun.common import utils +from zun.common import validation +from zun import objects + +LOG = logging.getLogger(__name__) + + +def _get_capsule(capsule_id): + capsule = api_utils.get_resource('Capsule', capsule_id) + if not capsule: + pecan.abort(404, ('Not found; the container you requested ' + 'does not exist.')) + return capsule + + +def _get_container(container_id): + container = api_utils.get_resource('Container', container_id) + if not container: + pecan.abort(404, ('Not found; the container you requested ' + 'does not exist.')) + return container + + +def check_policy_on_capsule(capsule, action): + context = pecan.request.context + policy.enforce(context, action, capsule, action=action) + + +class CapsuleCollection(collection.Collection): + """API representation of a collection of Capsules.""" + + fields = { + 'capsules', + 'next' + } + + """A list containing capsules objects""" + + def __init__(self, **kwargs): + self._type = 'capsules' + + @staticmethod + def convert_with_links(rpc_capsules, limit, url=None, + expand=False, **kwargs): + collection = CapsuleCollection() + collection.capsules = \ + [view.format_capsule(url, p) for p in rpc_capsules] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +class CapsuleController(base.Controller): + '''Controller for Capsules''' + + _custom_actions = { + + } + + @pecan.expose('json') + @api_utils.enforce_content_types(['application/json']) + @exception.wrap_pecan_controller_exception + @validation.validated(schema.capsule_create) + def post(self, **capsule_dict): + """Create a new capsule. + + :param capsule: a capsule within the request body. + """ + context = pecan.request.context + compute_api = pecan.request.compute_api + policy.enforce(context, "capsule:create", + action="capsule:create") + capsule_dict['capsule_version'] = 'alpha' + capsule_dict['kind'] = 'capsule' + + capsules_spec = capsule_dict['spec'] + containers_spec = utils.check_capsule_template(capsules_spec) + capsule_dict['uuid'] = uuidutils.generate_uuid() + new_capsule = objects.Capsule(context, **capsule_dict) + new_capsule.project_id = context.project_id + new_capsule.user_id = context.user_id + new_capsule.create(context) + new_capsule.containers = [] + new_capsule.containers_uuids = [] + new_capsule.volumes = [] + count = len(containers_spec) + + capsule_restart_policy = capsules_spec.get('restart_policy', 'always') + + metadata_info = capsules_spec.get('metadata', None) + requested_networks = capsules_spec.get('nets', []) + if metadata_info: + new_capsule.meta_name = metadata_info.get('name', None) + new_capsule.meta_labels = metadata_info.get('labels', None) + + # Generate Object for infra container + sandbox_container = objects.Container(context) + sandbox_container.project_id = context.project_id + sandbox_container.user_id = context.user_id + name = self._generate_name_for_capsule_sandbox( + capsule_dict['uuid']) + sandbox_container.name = name + sandbox_container.create(context) + new_capsule.containers.append(sandbox_container) + new_capsule.containers_uuids.append(sandbox_container.uuid) + + for k in range(count): + container_dict = containers_spec[k] + container_dict['project_id'] = context.project_id + container_dict['user_id'] = context.user_id + name = self._generate_name_for_capsule_container( + capsule_dict['uuid']) + container_dict['name'] = name + + if container_dict.get('args') and container_dict.get('command'): + container_dict = self._transfer_list_to_str(container_dict, + 'command') + container_dict = self._transfer_list_to_str(container_dict, + 'args') + container_dict['command'] = \ + container_dict['command'] + ' ' + container_dict['args'] + container_dict.pop('args') + elif container_dict.get('command'): + container_dict = self._transfer_list_to_str(container_dict, + 'command') + elif container_dict.get('args'): + container_dict = self._transfer_list_to_str(container_dict, + 'args') + container_dict['command'] = container_dict['args'] + container_dict.pop('args') + + # NOTE(kevinz): Don't support pod remapping, will find a + # easy way to implement it. + # if container need to open some port, just open it in container, + # user can change the security group and getting access to port. + if container_dict.get('ports'): + container_dict.pop('ports') + + if container_dict.get('resources'): + resources_list = container_dict.get('resources') + allocation = resources_list.get('allocation') + if allocation.get('cpu'): + container_dict['cpu'] = allocation.get('cpu') + if allocation.get('memory'): + container_dict['memory'] = \ + str(allocation['memory']) + 'M' + container_dict.pop('resources') + + if capsule_restart_policy: + container_dict['restart_policy'] = \ + {"MaximumRetryCount": "0", + "Name": capsule_restart_policy} + self._check_for_restart_policy(container_dict) + + container_dict['status'] = consts.CREATING + container_dict['interactive'] = True + new_container = objects.Container(context, **container_dict) + new_container.create(context) + new_capsule.containers.append(new_container) + new_capsule.containers_uuids.append(new_container.uuid) + + new_capsule.save(context) + compute_api.capsule_create(context, new_capsule, requested_networks) + # Set the HTTP Location Header + pecan.response.location = link.build_url('capsules', + new_capsule.uuid) + + pecan.response.status = 202 + return view.format_capsule(pecan.request.host_url, new_capsule) + + def _generate_name_for_capsule_container(self, capsule_uuid=None): + '''Generate a random name like: zeta-22-container.''' + name_gen = name_generator.NameGenerator() + name = name_gen.generate() + return 'capsule-' + capsule_uuid + '-' + name + + def _generate_name_for_capsule_sandbox(self, capsule_uuid=None): + '''Generate sandbox name inside the capsule''' + return 'capsule-' + capsule_uuid + '-' + 'sandbox' + + def _transfer_different_field(self, field_tpl, + field_container, **container_dict): + '''Transfer the template specified field to container_field''' + if container_dict.get(field_tpl): + container_dict[field_container] = api_utils.string_or_none( + container_dict.get(field_tpl)) + container_dict.pop(field_tpl) + return container_dict + + def _check_for_restart_policy(self, container_dict): + '''Check for restart policy input''' + restart_policy = container_dict.get('restart_policy') + if not restart_policy: + return + + name = restart_policy.get('Name') + num = restart_policy.setdefault('MaximumRetryCount', '0') + count = int(num) + if name in ['unless-stopped', 'always']: + if count != 0: + raise exception.InvalidValue(("maximum retry " + "count not valid " + "with restart policy " + "of %s") % name) + elif name in ['no']: + container_dict.get('restart_policy')['MaximumRetryCount'] = '0' + + def _transfer_list_to_str(self, container_dict, field): + if container_dict[field]: + dict = None + for k in range(0, len(container_dict[field])): + if dict: + dict = dict + ' ' + container_dict[field][k] + else: + dict = container_dict[field][k] + container_dict[field] = dict + return container_dict diff --git a/zun/api/controllers/experimental/collection.py b/zun/api/controllers/experimental/collection.py new file mode 100644 index 000000000..a2540078f --- /dev/null +++ b/zun/api/controllers/experimental/collection.py @@ -0,0 +1,43 @@ +# Copyright 2017 ARM Holdings. +# +# 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. + +import pecan + +from zun.api.controllers import base +from zun.api.controllers import link + + +class Collection(base.APIBase): + + @property + def collection(self): + return getattr(self, self._type) + + def has_next(self, limit): + """Return whether collection has more items.""" + return len(self.collection) and len(self.collection) == limit + + def get_next(self, limit, url=None, **kwargs): + """Return a link to the next subset of the collection.""" + if not self.has_next(limit): + return None + + resource_url = url or self._type + q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs]) + next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % { + 'args': q_args, 'limit': limit, + 'marker': self.collection[-1]['uuid']} + + return link.make_link('next', pecan.request.host_url, + resource_url, next_args)['href'] diff --git a/zun/api/controllers/experimental/schemas/__init__.py b/zun/api/controllers/experimental/schemas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zun/api/controllers/experimental/schemas/capsules.py b/zun/api/controllers/experimental/schemas/capsules.py new file mode 100644 index 000000000..3e9abb7a5 --- /dev/null +++ b/zun/api/controllers/experimental/schemas/capsules.py @@ -0,0 +1,26 @@ +# Copyright 2017 ARM Holdings. +# +# 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. + +from zun.common.validation import parameter_types + +_capsule_properties = { + 'spec': parameter_types.spec +} + +capsule_create = { + 'type': 'object', + 'properties': _capsule_properties, + 'required': ['spec'], + 'additionalProperties': False +} diff --git a/zun/api/controllers/experimental/views/__init__.py b/zun/api/controllers/experimental/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zun/api/controllers/experimental/views/capsules_view.py b/zun/api/controllers/experimental/views/capsules_view.py new file mode 100644 index 000000000..a3145fa07 --- /dev/null +++ b/zun/api/controllers/experimental/views/capsules_view.py @@ -0,0 +1,49 @@ +# Copyright 2017 ARM Holdings. +# +# 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. + +import itertools + +from zun.api.controllers import link + + +_basic_keys = ( + 'id', + 'uuid', + 'created_at', + 'status', + 'restart_policy', + 'meta_name', + 'meta_labels', + 'containers_uuids', + 'capsule_version' +) + + +def format_capsule(url, capsule): + def transform(key, value): + if key not in _basic_keys: + return + if key == 'uuid': + yield ('uuid', value) + yield ('links', [link.make_link( + 'self', url, 'capsules', value), + link.make_link( + 'bookmark', url, + 'capsules', value, + bookmark=True)]) + else: + yield (key, value) + + return dict(itertools.chain.from_iterable( + transform(k, v) for k, v in capsule.as_dict().items())) diff --git a/zun/api/controllers/root.py b/zun/api/controllers/root.py index adf3b97d8..b290f1ea2 100644 --- a/zun/api/controllers/root.py +++ b/zun/api/controllers/root.py @@ -14,6 +14,7 @@ import pecan from pecan import rest from zun.api.controllers import base +from zun.api.controllers import experimental from zun.api.controllers import link from zun.api.controllers import v1 from zun.api.controllers import versions @@ -69,13 +70,14 @@ class Root(base.APIBase): class RootController(rest.RestController): - _versions = ['v1'] + _versions = ['v1', 'experimental'] """All supported API versions""" _default_version = 'v1' """The default API version""" v1 = v1.Controller() + experimental = experimental.Controller() @pecan.expose('json') def get(self): diff --git a/zun/api/controllers/v1/__init__.py b/zun/api/controllers/v1/__init__.py index 2509d959f..6cd86a708 100644 --- a/zun/api/controllers/v1/__init__.py +++ b/zun/api/controllers/v1/__init__.py @@ -22,6 +22,7 @@ from oslo_log import log as logging import pecan from zun.api.controllers import base as controllers_base +from zun.api.controllers.experimental import capsules as capsule_controller from zun.api.controllers import link from zun.api.controllers.v1 import containers as container_controller from zun.api.controllers.v1 import hosts as host_controller @@ -115,6 +116,7 @@ class Controller(controllers_base.Controller): containers = container_controller.ContainersController() images = image_controller.ImagesController() hosts = host_controller.HostController() + capsules = capsule_controller.CapsuleController() @pecan.expose('json') def get(self): diff --git a/zun/common/exception.py b/zun/common/exception.py index 32a1c4883..2a3ac5281 100644 --- a/zun/common/exception.py +++ b/zun/common/exception.py @@ -542,3 +542,15 @@ class PciDeviceNotFoundById(NotFound): class PciDeviceNotFound(NotFound): message = _("PCI Device %(node_id)s:%(address)s not found.") + + +class CapsuleAlreadyExists(ResourceExists): + message = _("A capsule with %(field)s %(value)s already exists.") + + +class CapsuleNotFound(HTTPNotFound): + message = _("Capsule %(capsule)s could not be found.") + + +class InvalidCapsuleTemplate(ZunException): + message = _("Invalid capsule template: %(reason)s.") diff --git a/zun/common/utils.py b/zun/common/utils.py index fd4c922ec..a256bee81 100644 --- a/zun/common/utils.py +++ b/zun/common/utils.py @@ -313,3 +313,28 @@ def get_security_group_ids(context, security_groups, **kwargs): raise exception.ZunException(_( "Any of the security group in %s is not found ") % security_groups) + + +def check_capsule_template(tpl): + # TODO(kevinz): add volume spec check + kind_field = tpl.get('kind', None) + if kind_field != 'capsule' or kind_field != 'Capsule': + raise exception.InvalidCapsuleTemplate("kind fields need to " + "be set as capsule") + spec_field = tpl.get('spec', None) + if spec_field is None: + raise exception.InvalidCapsuleTemplate("No Spec found") + if spec_field.get('containers', None) is None: + raise exception.InvalidCapsuleTemplate("No valid containers field") + containers_spec = spec_field.get('containers', None) + containers_num = len(containers_spec) + if containers_num == 0: + raise exception.InvalidCapsuleTemplate("Capsule need to have one " + "container at least") + + for i in range(0, containers_num): + container_image = containers_spec[i].get('image', None) + if container_image is None: + raise exception.InvalidCapsuleTemplate("Container " + "image is needed") + return containers_spec diff --git a/zun/common/validation/parameter_types.py b/zun/common/validation/parameter_types.py index fe1599990..c8fa55676 100644 --- a/zun/common/validation/parameter_types.py +++ b/zun/common/validation/parameter_types.py @@ -225,3 +225,7 @@ security_groups = { 'maxLength': 255 } } + +spec = { + 'type': ['object'], +} diff --git a/zun/compute/api.py b/zun/compute/api.py index 76792df18..fd0d6add7 100644 --- a/zun/compute/api.py +++ b/zun/compute/api.py @@ -132,3 +132,17 @@ class API(object): def image_search(self, context, image, image_driver, *args): return self.rpcapi.image_search(context, image, image_driver, *args) + + def capsule_create(self, context, new_capsule, + requested_networks=None, extra_spec=None): + host_state = None + try: + host_state = self._schedule_container(context, new_capsule, + extra_spec) + except Exception as exc: + new_capsule.status = consts.ERROR + new_capsule.status_reason = str(exc) + new_capsule.save(context) + return + self.rpcapi.capsule_create(context, host_state['host'], new_capsule, + requested_networks, host_state['limits']) diff --git a/zun/compute/manager.py b/zun/compute/manager.py index e471f3c31..d9350a562 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -88,26 +88,8 @@ class Manager(periodic_task.PeriodicTasks): container.task_state = task_state container.save(context) - def _do_container_create(self, context, container, requested_networks, - limits=None, reraise=False): - LOG.debug('Creating container: %s', container.uuid) - - # check if container driver is NovaDockerDriver and - # security_groups is non empty, then return by setting - # the error message in database - if ('NovaDockerDriver' in CONF.container_driver and - container.security_groups): - msg = "security_groups can not be provided with NovaDockerDriver" - self._fail_container(self, context, container, msg) - return - - sandbox_id = None - if self.use_sandbox: - sandbox_id = self._create_sandbox(context, container, - requested_networks, reraise) - if sandbox_id is None: - return - + def _do_container_create_base(self, context, container, requested_networks, + sandbox=None, limits=None, reraise=False): self._update_task_state(context, container, consts.IMAGE_PULLING) repo, tag = utils.parse_image_name(container.image) image_pull_policy = utils.get_image_pull_policy( @@ -172,6 +154,33 @@ class Manager(periodic_task.PeriodicTasks): unset_host=True) return + def _do_container_create(self, context, container, requested_networks, + limits=None, reraise=False): + LOG.debug('Creating container: %s', container.uuid) + + # check if container driver is NovaDockerDriver and + # security_groups is non empty, then return by setting + # the error message in database + if ('NovaDockerDriver' in CONF.container_driver and + container.security_groups): + msg = "security_groups can not be provided with NovaDockerDriver" + self._fail_container(self, context, container, msg) + return + + sandbox = None + if self.use_sandbox: + sandbox = self._create_sandbox(context, container, + requested_networks, reraise) + if sandbox is None: + return + + created_container = self._do_container_create_base(context, + container, + requested_networks, + sandbox, limits, + reraise) + return created_container + def _use_sandbox(self): if CONF.use_sandbox and self.driver.capabilities["support_sandbox"]: return True @@ -681,3 +690,34 @@ class Manager(periodic_task.PeriodicTasks): return except Exception: return + + def capsule_create(self, context, capsule, requested_networks, limits): + utils.spawn_n(self._do_capsule_create, context, + capsule, requested_networks, limits) + + def _do_capsule_create(self, context, capsule, requested_networks=None, + limits=None, reraise=False): + capsule.containers[0].image = CONF.sandbox_image + capsule.containers[0].image_driver = CONF.sandbox_image_driver + capsule.containers[0].image_pull_policy = \ + CONF.sandbox_image_pull_policy + capsule.containers[0].save(context) + sandbox = self._create_sandbox(context, + capsule.containers[0], + requested_networks, reraise) + self._update_task_state(context, capsule.containers[0], None) + capsule.containers[0].status = consts.RUNNING + capsule.containers[0].save(context) + sandbox_id = capsule.containers[0].get_sandbox_id() + count = len(capsule.containers) + for k in range(1, count): + capsule.containers[k].set_sandbox_id(sandbox_id) + capsule.containers[k].addresses = capsule.containers[0].addresses + created_container = \ + self._do_container_create_base(context, + capsule.containers[k], + requested_networks, + sandbox, + limits) + if created_container: + self._do_container_start(context, created_container) diff --git a/zun/compute/rpcapi.py b/zun/compute/rpcapi.py index bdf27c2a3..a4b272b83 100644 --- a/zun/compute/rpcapi.py +++ b/zun/compute/rpcapi.py @@ -174,3 +174,10 @@ class API(rpc_service.API): return self._call(host, 'image_search', image=image, image_driver_name=image_driver, exact_match=exact_match) + + def capsule_create(self, context, host, capsule, + requested_networks, limits): + self._cast(host, 'capsule_create', + capsule=capsule, + requested_networks=requested_networks, + limits=limits)