Files
zun/zun/api/controllers/experimental/capsules.py

405 lines
17 KiB
Python

# 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
import pecan
import six
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.i18n import _
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
from zun.volume import cinder_api as cinder
LOG = logging.getLogger(__name__)
def _get_capsule(capsule_ident):
"""Get capsule by name or UUID"""
capsule = api_utils.get_resource('Capsule', capsule_ident)
if not capsule:
pecan.abort(404, ('Not found; the capsule you requested '
'does not exist.'))
return capsule
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')
@exception.wrap_pecan_controller_exception
def get_all(self, **kwargs):
'''Retrieve a list of capsules.'''
context = pecan.request.context
policy.enforce(context, "capsule:get_all",
action="capsule:get_all")
return self._get_capsules_collection(**kwargs)
def _get_capsules_collection(self, **kwargs):
context = pecan.request.context
if utils.is_all_projects(kwargs):
context.all_projects = True
limit = api_utils.validate_limit(kwargs.get('limit'))
sort_dir = api_utils.validate_sort_dir(kwargs.get('sort_dir', 'asc'))
sort_key = kwargs.get('sort_key', 'id')
resource_url = kwargs.get('resource_url')
expand = kwargs.get('expand')
filters = None
marker_obj = None
marker = kwargs.get('marker')
if marker:
marker_obj = objects.Capsule.get_by_uuid(context,
marker)
capsules = objects.Capsule.list(context,
limit,
marker_obj,
sort_key,
sort_dir,
filters=filters)
return CapsuleCollection.convert_with_links(capsules, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
@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_dict: a capsule within the request body.
"""
context = pecan.request.context
compute_api = pecan.request.compute_api
policy.enforce(context, "capsule:create",
action="capsule:create")
# Abstract the capsule specification
capsules_template = capsule_dict.get('template')
spec_content = utils.check_capsule_template(capsules_template)
containers_spec = utils.capsule_get_container_spec(spec_content)
volumes_spec = utils.capsule_get_volume_spec(spec_content)
# Create the capsule Object
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 = []
capsule_need_cpu = 0
capsule_need_memory = 0
container_volume_requests = []
capsule_restart_policy = capsules_template.get('restart_policy',
'always')
container_restart_policy = {"MaximumRetryCount": "0",
"Name": capsule_restart_policy}
new_capsule.restart_policy = capsule_restart_policy
metadata_info = capsules_template.get('metadata', None)
requested_networks_info = capsules_template.get('nets', [])
requested_networks = \
utils.build_requested_networks(context, requested_networks_info)
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(new_capsule)
sandbox_container.name = name
sandbox_container.create(context)
new_capsule.containers.append(sandbox_container)
new_capsule.containers_uuids.append(sandbox_container.uuid)
container_num = len(containers_spec)
for k in range(container_num):
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(new_capsule)
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 port 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('requests')
if allocation.get('cpu'):
capsule_need_cpu += allocation.get('cpu')
container_dict['cpu'] = allocation.get('cpu')
if allocation.get('memory'):
capsule_need_memory += allocation.get('memory')
container_dict['memory'] = str(allocation['memory']) + 'M'
container_dict.pop('resources')
container_dict['restart_policy'] = container_restart_policy
utils.check_for_restart_policy(container_dict)
if container_dict.get('volumeMounts'):
for volume in container_dict['volumeMounts']:
volume['container_name'] = name
container_volume_requests.append(volume)
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)
# Deal with the volume support
requested_volumes = \
self._build_requested_volumes(context,
volumes_spec,
container_volume_requests,
new_capsule)
new_capsule.cpu = capsule_need_cpu
new_capsule.memory = str(capsule_need_memory) + 'M'
new_capsule.save(context)
compute_api.capsule_create(context, new_capsule, requested_networks,
requested_volumes)
# 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)
@pecan.expose('json')
@exception.wrap_pecan_controller_exception
def get_one(self, capsule_ident):
"""Retrieve information about the given capsule.
:param capsule_ident: UUID or name of a capsule.
"""
capsule = _get_capsule(capsule_ident)
check_policy_on_capsule(capsule.as_dict(), "capsule:get")
context = pecan.request.context
compute_api = pecan.request.compute_api
sandbox = utils.get_container(capsule.containers_uuids[0])
try:
container = compute_api.container_show(context, sandbox)
capsule.status = container.status
capsule.save(context)
except Exception as e:
LOG.exception(("Error while show capsule %(uuid)s: "
"%(e)s."),
{'uuid': capsule.uuid, 'e': e})
capsule.status = consts.UNKNOWN
return view.format_capsule(pecan.request.host_url, capsule)
@pecan.expose('json')
@exception.wrap_pecan_controller_exception
def delete(self, capsule_ident, **kwargs):
"""Delete a capsule.
:param capsule_ident: UUID or Name of a capsule.
"""
context = pecan.request.context
if utils.is_all_projects(kwargs):
policy.enforce(context, "capsule:delete_all_projects",
action="capsule:delete_all_projects")
context.all_projects = True
capsule = _get_capsule(capsule_ident)
check_policy_on_capsule(capsule.as_dict(), "capsule:delete")
compute_api = pecan.request.compute_api
capsule.task_state = consts.CONTAINER_DELETING
capsule.save(context)
compute_api.capsule_delete(context, capsule)
pecan.response.status = 204
def _generate_name_for_capsule_container(self, new_capsule):
"""Generate a random name like: zeta-22-container."""
name_gen = name_generator.NameGenerator()
name = name_gen.generate()
if new_capsule.meta_name is None:
return 'capsule-' + new_capsule.uuid + '-' + name
else:
return 'capsule-' + new_capsule.meta_name + '-' + name
def _generate_name_for_capsule_sandbox(self, new_capsule):
"""Generate sandbox name inside the capsule"""
if new_capsule.meta_name is None:
return 'capsule-' + new_capsule.uuid + '-' + 'sandbox'
else:
return 'capsule-' + new_capsule.meta_name + '-' + '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 _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
def _build_requested_volumes(self, context, volume_spec,
volume_mounts, capsule):
# NOTE(hongbin): We assume cinder is the only volume provider here.
# The logic needs to be re-visited if a second volume provider
# (i.e. Manila) is introduced.
# NOTE(kevinz): We assume the volume_mounts has been pretreated,
# there won't occur that volume multiple attach and no untapped
# volume.
cinder_api = cinder.CinderAPI(context)
volume_driver = "cinder"
requested_volumes = []
volume_created = []
try:
for mount in volume_spec:
mount_driver = mount[volume_driver]
auto_remove = False
if mount_driver.get("volumeID"):
uuid = mount_driver.get("volumeID")
volume = cinder_api.search_volume(uuid)
cinder_api.ensure_volume_usable(volume)
else:
size = mount_driver.get("size")
volume = cinder_api.create_volume(size)
volume_created.append(volume)
if "autoRemove" in mount_driver.keys() \
and mount_driver.get("autoRemove", False):
auto_remove = True
mount_destination = None
container_name = None
for item in volume_mounts:
if item['name'] == mount['name']:
mount_destination = item['mountPath']
container_name = item['container_name']
break
if mount_destination and container_name:
volmapp = objects.VolumeMapping(
context,
volume_id=volume.id, volume_provider=volume_driver,
container_path=mount_destination,
user_id=context.user_id,
project_id=context.project_id,
auto_remove=auto_remove)
requested_volumes.append({container_name: volmapp})
else:
msg = _("volume mount parameters is invalid.")
raise exception.Invalid(msg)
except Exception as e:
# if volume search or created failed, will remove all
# the created volume. The existed volume will remain.
for volume in volume_created:
try:
cinder_api.delete_volume(volume.id)
except Exception as exc:
LOG.error('Error on deleting volume "%s": %s.',
volume.id, six.text_type(exc))
# Since the container and capsule database model has been created,
# we need to delete them here due to the volume create failed.
for container in capsule.containers:
try:
container.destroy(context)
except Exception as exc:
LOG.warning('fail to delete the container %s: %s',
container.uuid, exc)
capsule.destroy(context)
raise e
return requested_volumes