Merge "Add capsule controller in API side and add create method"
This commit is contained in:
commit
71f46ec0ef
@ -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",
|
||||
}
|
||||
|
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 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):
|
||||
|
@ -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):
|
||||
|
@ -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.")
|
||||
|
@ -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
|
||||
|
@ -225,3 +225,7 @@ security_groups = {
|
||||
'maxLength': 255
|
||||
}
|
||||
}
|
||||
|
||||
spec = {
|
||||
'type': ['object'],
|
||||
}
|
||||
|
@ -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'])
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user