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:
parent
127794e2dd
commit
70020a9447
@ -43,5 +43,12 @@
|
|||||||
"zun-service:get_all": "rule:admin_api",
|
"zun-service:get_all": "rule:admin_api",
|
||||||
|
|
||||||
"host: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",
|
||||||
}
|
}
|
||||||
|
62
template/capsule/capsule.yaml
Normal file
62
template/capsule/capsule.yaml
Normal 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
|
152
zun/api/controllers/experimental/__init__.py
Normal file
152
zun/api/controllers/experimental/__init__.py
Normal 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)
|
244
zun/api/controllers/experimental/capsules.py
Normal file
244
zun/api/controllers/experimental/capsules.py
Normal 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
|
43
zun/api/controllers/experimental/collection.py
Normal file
43
zun/api/controllers/experimental/collection.py
Normal 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']
|
26
zun/api/controllers/experimental/schemas/capsules.py
Normal file
26
zun/api/controllers/experimental/schemas/capsules.py
Normal 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
|
||||||
|
}
|
0
zun/api/controllers/experimental/views/__init__.py
Normal file
0
zun/api/controllers/experimental/views/__init__.py
Normal file
49
zun/api/controllers/experimental/views/capsules_view.py
Normal file
49
zun/api/controllers/experimental/views/capsules_view.py
Normal 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()))
|
@ -14,6 +14,7 @@ import pecan
|
|||||||
from pecan import rest
|
from pecan import rest
|
||||||
|
|
||||||
from zun.api.controllers import base
|
from zun.api.controllers import base
|
||||||
|
from zun.api.controllers import experimental
|
||||||
from zun.api.controllers import link
|
from zun.api.controllers import link
|
||||||
from zun.api.controllers import v1
|
from zun.api.controllers import v1
|
||||||
from zun.api.controllers import versions
|
from zun.api.controllers import versions
|
||||||
@ -69,13 +70,14 @@ class Root(base.APIBase):
|
|||||||
|
|
||||||
class RootController(rest.RestController):
|
class RootController(rest.RestController):
|
||||||
|
|
||||||
_versions = ['v1']
|
_versions = ['v1', 'experimental']
|
||||||
"""All supported API versions"""
|
"""All supported API versions"""
|
||||||
|
|
||||||
_default_version = 'v1'
|
_default_version = 'v1'
|
||||||
"""The default API version"""
|
"""The default API version"""
|
||||||
|
|
||||||
v1 = v1.Controller()
|
v1 = v1.Controller()
|
||||||
|
experimental = experimental.Controller()
|
||||||
|
|
||||||
@pecan.expose('json')
|
@pecan.expose('json')
|
||||||
def get(self):
|
def get(self):
|
||||||
|
@ -22,6 +22,7 @@ from oslo_log import log as logging
|
|||||||
import pecan
|
import pecan
|
||||||
|
|
||||||
from zun.api.controllers import base as controllers_base
|
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 link
|
||||||
from zun.api.controllers.v1 import containers as container_controller
|
from zun.api.controllers.v1 import containers as container_controller
|
||||||
from zun.api.controllers.v1 import hosts as host_controller
|
from zun.api.controllers.v1 import hosts as host_controller
|
||||||
@ -115,6 +116,7 @@ class Controller(controllers_base.Controller):
|
|||||||
containers = container_controller.ContainersController()
|
containers = container_controller.ContainersController()
|
||||||
images = image_controller.ImagesController()
|
images = image_controller.ImagesController()
|
||||||
hosts = host_controller.HostController()
|
hosts = host_controller.HostController()
|
||||||
|
capsules = capsule_controller.CapsuleController()
|
||||||
|
|
||||||
@pecan.expose('json')
|
@pecan.expose('json')
|
||||||
def get(self):
|
def get(self):
|
||||||
|
@ -542,3 +542,15 @@ class PciDeviceNotFoundById(NotFound):
|
|||||||
|
|
||||||
class PciDeviceNotFound(NotFound):
|
class PciDeviceNotFound(NotFound):
|
||||||
message = _("PCI Device %(node_id)s:%(address)s not found.")
|
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.")
|
||||||
|
@ -313,3 +313,28 @@ def get_security_group_ids(context, security_groups, **kwargs):
|
|||||||
raise exception.ZunException(_(
|
raise exception.ZunException(_(
|
||||||
"Any of the security group in %s is not found ") %
|
"Any of the security group in %s is not found ") %
|
||||||
security_groups)
|
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
|
||||||
|
@ -225,3 +225,7 @@ security_groups = {
|
|||||||
'maxLength': 255
|
'maxLength': 255
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spec = {
|
||||||
|
'type': ['object'],
|
||||||
|
}
|
||||||
|
@ -132,3 +132,17 @@ class API(object):
|
|||||||
|
|
||||||
def image_search(self, context, image, image_driver, *args):
|
def image_search(self, context, image, image_driver, *args):
|
||||||
return self.rpcapi.image_search(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'])
|
||||||
|
@ -88,26 +88,8 @@ class Manager(periodic_task.PeriodicTasks):
|
|||||||
container.task_state = task_state
|
container.task_state = task_state
|
||||||
container.save(context)
|
container.save(context)
|
||||||
|
|
||||||
def _do_container_create(self, context, container, requested_networks,
|
def _do_container_create_base(self, context, container, requested_networks,
|
||||||
limits=None, reraise=False):
|
sandbox=None, 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
|
|
||||||
|
|
||||||
self._update_task_state(context, container, consts.IMAGE_PULLING)
|
self._update_task_state(context, container, consts.IMAGE_PULLING)
|
||||||
repo, tag = utils.parse_image_name(container.image)
|
repo, tag = utils.parse_image_name(container.image)
|
||||||
image_pull_policy = utils.get_image_pull_policy(
|
image_pull_policy = utils.get_image_pull_policy(
|
||||||
@ -172,6 +154,33 @@ class Manager(periodic_task.PeriodicTasks):
|
|||||||
unset_host=True)
|
unset_host=True)
|
||||||
return
|
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):
|
def _use_sandbox(self):
|
||||||
if CONF.use_sandbox and self.driver.capabilities["support_sandbox"]:
|
if CONF.use_sandbox and self.driver.capabilities["support_sandbox"]:
|
||||||
return True
|
return True
|
||||||
@ -681,3 +690,34 @@ class Manager(periodic_task.PeriodicTasks):
|
|||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
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)
|
||||||
|
@ -174,3 +174,10 @@ class API(rpc_service.API):
|
|||||||
return self._call(host, 'image_search', image=image,
|
return self._call(host, 'image_search', image=image,
|
||||||
image_driver_name=image_driver,
|
image_driver_name=image_driver,
|
||||||
exact_match=exact_match)
|
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user