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 <kevin.zhao@arm.com>
This commit is contained in:
Kevin Zhao 2017-07-17 22:47:37 +08:00
parent 127794e2dd
commit 70020a9447
17 changed files with 711 additions and 22 deletions

View File

@ -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",
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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']

View File

@ -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
}

View File

@ -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()))

View File

@ -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):

View File

@ -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):

View File

@ -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.")

View File

@ -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

View File

@ -225,3 +225,7 @@ security_groups = {
'maxLength': 255
}
}
spec = {
'type': ['object'],
}

View File

@ -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'])

View File

@ -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)

View File

@ -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)