Merge "Add capsule controller in API side and add create method"

This commit is contained in:
Jenkins 2017-08-17 01:31:05 +00:00 committed by Gerrit Code Review
commit 71f46ec0ef
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)