Merge "Add support for Capsule volume"

This commit is contained in:
Zuul 2018-01-11 08:59:54 +00:00 committed by Gerrit Code Review
commit 97524cba92
11 changed files with 533 additions and 117 deletions

View File

@ -0,0 +1,35 @@
capsule_template_version: 2017-12-20
# use "-" because that the fields have many items
capsule_version: beta
kind: capsule
metadata:
name: capsule-volume
labels:
foo: bar
restart_policy: always
spec:
containers:
- image: test
command:
- "/bin/bash"
workdir: /root
labels:
app: web
volumeMounts:
- name: volume1
mountPath: /data1
- name: volume2
mountPath: /data2
- name: volume3
mountPath: /data3
volumes:
- name: volume1
cinder:
size: 3
autoRemove: True
- name: volume2
cinder:
volumeID: 473e4a6a-99f2-4b42-88ce-5ab03a00b756
- name: volume3
cinder:
volumeID: f4246aa1-1c87-479c-a2ab-4dbaf0c3c7bb

View File

@ -28,6 +28,10 @@ spec:
memory: 1024 memory: 1024
environment: environment:
PATCH: /usr/local/bin PATCH: /usr/local/bin
volumeMounts:
- name: volume1
mountPath: /data1
readOnly: True
- image: centos - image: centos
command: command:
- "echo" - "echo"
@ -53,10 +57,19 @@ spec:
memory: 1024 memory: 1024
environment: environment:
NWH: /usr/bin/ NWH: /usr/bin/
volumeMounts:
- name: volume2
mountPath: /data2
- name: volume3
mountPath: /data3
volumes: volumes:
- name: volume1 - name: volume1
drivers: cinder cinder:
driverOptions: options size: 5
size: 5GB autoRemove: True
volumeType: type1 - name: volume2
image: ubuntu-xenial cinder:
volumeID: 473e4a6a-99f2-4b42-88ce-5ab03a00b756
- name: volume3
cinder:
volumeID: f4246aa1-1c87-479c-a2ab-4dbaf0c3c7bb

View File

@ -14,6 +14,7 @@
from oslo_log import log as logging from oslo_log import log as logging
import pecan import pecan
import six
from zun.api.controllers import base from zun.api.controllers import base
from zun.api.controllers.experimental import collection from zun.api.controllers.experimental import collection
@ -23,11 +24,13 @@ from zun.api.controllers import link
from zun.api import utils as api_utils from zun.api import utils as api_utils
from zun.common import consts from zun.common import consts
from zun.common import exception from zun.common import exception
from zun.common.i18n import _
from zun.common import name_generator from zun.common import name_generator
from zun.common import policy from zun.common import policy
from zun.common import utils from zun.common import utils
from zun.common import validation from zun.common import validation
from zun import objects from zun import objects
from zun.volume import cinder_api as cinder
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -126,8 +129,14 @@ class CapsuleController(base.Controller):
compute_api = pecan.request.compute_api compute_api = pecan.request.compute_api
policy.enforce(context, "capsule:create", policy.enforce(context, "capsule:create",
action="capsule:create") action="capsule:create")
# Abstract the capsule specification
capsules_spec = capsule_dict['spec'] capsules_spec = capsule_dict['spec']
containers_spec = utils.check_capsule_template(capsules_spec) spec_content = utils.check_capsule_template(capsules_spec)
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 = objects.Capsule(context, **capsule_dict)
new_capsule.project_id = context.project_id new_capsule.project_id = context.project_id
new_capsule.user_id = context.user_id new_capsule.user_id = context.user_id
@ -137,12 +146,15 @@ class CapsuleController(base.Controller):
new_capsule.volumes = [] new_capsule.volumes = []
capsule_need_cpu = 0 capsule_need_cpu = 0
capsule_need_memory = 0 capsule_need_memory = 0
count = len(containers_spec) container_volume_requests = []
capsule_restart_policy = capsules_spec.get('restart_policy', 'always') capsule_restart_policy = capsules_spec.get('restart_policy', 'always')
metadata_info = capsules_spec.get('metadata', None) metadata_info = capsules_spec.get('metadata', None)
requested_networks = capsules_spec.get('nets', []) requested_networks_info = capsules_spec.get('nets', [])
requested_networks = \
utils.build_requested_networks(context, requested_networks_info)
if metadata_info: if metadata_info:
new_capsule.meta_name = metadata_info.get('name', None) new_capsule.meta_name = metadata_info.get('name', None)
new_capsule.meta_labels = metadata_info.get('labels', None) new_capsule.meta_labels = metadata_info.get('labels', None)
@ -157,7 +169,8 @@ class CapsuleController(base.Controller):
new_capsule.containers.append(sandbox_container) new_capsule.containers.append(sandbox_container)
new_capsule.containers_uuids.append(sandbox_container.uuid) new_capsule.containers_uuids.append(sandbox_container.uuid)
for k in range(count): container_num = len(containers_spec)
for k in range(container_num):
container_dict = containers_spec[k] container_dict = containers_spec[k]
container_dict['project_id'] = context.project_id container_dict['project_id'] = context.project_id
container_dict['user_id'] = context.user_id container_dict['user_id'] = context.user_id
@ -181,7 +194,7 @@ class CapsuleController(base.Controller):
container_dict['command'] = container_dict['args'] container_dict['command'] = container_dict['args']
container_dict.pop('args') container_dict.pop('args')
# NOTE(kevinz): Don't support pod remapping, will find a # NOTE(kevinz): Don't support port remapping, will find a
# easy way to implement it. # easy way to implement it.
# if container need to open some port, just open it in container, # if container need to open some port, just open it in container,
# user can change the security group and getting access to port. # user can change the security group and getting access to port.
@ -205,6 +218,11 @@ class CapsuleController(base.Controller):
"Name": capsule_restart_policy} "Name": capsule_restart_policy}
utils.check_for_restart_policy(container_dict) 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['status'] = consts.CREATING
container_dict['interactive'] = True container_dict['interactive'] = True
new_container = objects.Container(context, **container_dict) new_container = objects.Container(context, **container_dict)
@ -212,10 +230,15 @@ class CapsuleController(base.Controller):
new_capsule.containers.append(new_container) new_capsule.containers.append(new_container)
new_capsule.containers_uuids.append(new_container.uuid) 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.cpu = capsule_need_cpu new_capsule.cpu = capsule_need_cpu
new_capsule.memory = str(capsule_need_memory) + 'M' new_capsule.memory = str(capsule_need_memory) + 'M'
new_capsule.save(context) new_capsule.save(context)
compute_api.capsule_create(context, new_capsule, requested_networks) compute_api.capsule_create(context, new_capsule, requested_networks,
requested_volumes)
# Set the HTTP Location Header # Set the HTTP Location Header
pecan.response.location = link.build_url('capsules', pecan.response.location = link.build_url('capsules',
new_capsule.uuid) new_capsule.uuid)
@ -302,3 +325,64 @@ class CapsuleController(base.Controller):
dict = container_dict[field][k] dict = container_dict[field][k]
container_dict[field] = dict container_dict[field] = dict
return container_dict return container_dict
def _build_requested_volumes(self, context, volume_spec, volume_mounts):
# 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))
raise e
return requested_volumes

View File

@ -238,7 +238,7 @@ class ContainersController(base.Controller):
min_version=min_version) min_version=min_version)
nets = container_dict.get('nets', []) nets = container_dict.get('nets', [])
requested_networks = self._build_requested_networks(context, nets) requested_networks = utils.build_requested_networks(context, nets)
pci_req = self._create_pci_requests_for_sriov_ports(context, pci_req = self._create_pci_requests_for_sriov_ports(context,
requested_networks) requested_networks)
@ -365,57 +365,6 @@ class ContainersController(base.Controller):
phynet_name = net.get('provider:physical_network') phynet_name = net.get('provider:physical_network')
return phynet_name return phynet_name
def _check_external_network_attach(self, context, nets):
"""Check if attaching to external network is permitted."""
if not context.can(NETWORK_ATTACH_EXTERNAL,
fatal=False):
for net in nets:
if net.get('router:external') and not net.get('shared'):
raise exception.ExternalNetworkAttachForbidden(
network_uuid=net['network'])
def _build_requested_networks(self, context, nets):
neutron_api = neutron.NeutronAPI(context)
requested_networks = []
for net in nets:
if net.get('port'):
port = neutron_api.get_neutron_port(net['port'])
neutron_api.ensure_neutron_port_usable(port)
network = neutron_api.get_neutron_network(port['network_id'])
requested_networks.append({'network': port['network_id'],
'port': port['id'],
'router:external':
network.get('router:external'),
'shared': network.get('shared'),
'v4-fixed-ip': '',
'v6-fixed-ip': '',
'preserve_on_delete': True})
elif net.get('network'):
network = neutron_api.get_neutron_network(net['network'])
requested_networks.append({'network': network['id'],
'port': '',
'router:external':
network.get('router:external'),
'shared': network.get('shared'),
'v4-fixed-ip':
net.get('v4-fixed-ip', ''),
'v6-fixed-ip':
net.get('v6-fixed-ip', ''),
'preserve_on_delete': False})
if not requested_networks:
# Find an available neutron net and create docker network by
# wrapping the neutron net.
neutron_net = neutron_api.get_available_network()
requested_networks.append({'network': neutron_net['id'],
'port': '',
'v4-fixed-ip': '',
'v6-fixed-ip': '',
'preserve_on_delete': False})
self._check_external_network_attach(context, requested_networks)
return requested_networks
def _build_requested_volumes(self, context, mounts): def _build_requested_volumes(self, context, mounts):
# NOTE(hongbin): We assume cinder is the only volume provider here. # NOTE(hongbin): We assume cinder is the only volume provider here.
# The logic needs to be re-visited if a second volume provider # The logic needs to be re-visited if a second volume provider

View File

@ -37,9 +37,11 @@ from zun.common import exception
from zun.common.i18n import _ from zun.common.i18n import _
from zun.common import privileged from zun.common import privileged
import zun.conf import zun.conf
from zun.network import neutron
CONF = zun.conf.CONF CONF = zun.conf.CONF
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
NETWORK_ATTACH_EXTERNAL = 'network:attach_external_network'
synchronized = lockutils.synchronized_with_prefix('zun-') synchronized = lockutils.synchronized_with_prefix('zun-')
@ -338,29 +340,63 @@ def execute(*cmd, **kwargs):
def check_capsule_template(tpl): def check_capsule_template(tpl):
# TODO(kevinz): add volume spec check # TODO(kevinz): add volume spec check
kind_field = tpl.get('kind', None) kind_field = tpl.get('kind')
if kind_field not in ['capsule', 'Capsule']: if kind_field not in ['capsule', 'Capsule']:
raise exception.InvalidCapsuleTemplate("kind fields need to be " raise exception.InvalidCapsuleTemplate("kind fields need to be "
"set as capsule or Capsule") "set as capsule or Capsule")
spec_field = tpl.get('spec', None) spec_field = tpl.get('spec')
if spec_field is None: if spec_field is None:
raise exception.InvalidCapsuleTemplate("No Spec found") raise exception.InvalidCapsuleTemplate("No Spec found")
if spec_field.get('containers', None) is None: if spec_field.get('containers') is None:
raise exception.InvalidCapsuleTemplate("No valid containers field") raise exception.InvalidCapsuleTemplate("No valid containers field")
containers_spec = spec_field.get('containers', None) return spec_field
def capsule_get_container_spec(spec_field):
containers_spec = spec_field.get('containers')
containers_num = len(containers_spec) containers_num = len(containers_spec)
if containers_num == 0: if containers_num == 0:
raise exception.InvalidCapsuleTemplate("Capsule need to have one " raise exception.InvalidCapsuleTemplate("Capsule need to have one "
"container at least") "container at least")
for i in range(0, containers_num): for i in range(0, containers_num):
container_image = containers_spec[i].get('image', None) container_image = containers_spec[i].get('image')
if container_image is None: if container_image is None:
raise exception.InvalidCapsuleTemplate("Container " raise exception.InvalidCapsuleTemplate("Container "
"image is needed") "image is needed")
return containers_spec return containers_spec
def capsule_get_volume_spec(spec_field):
volumes_spec = spec_field.get('volumes')
if not volumes_spec:
return []
volumes_num = len(volumes_spec)
for i in range(volumes_num):
volume_name = volumes_spec[i].get('name')
if volume_name is None:
raise exception.InvalidCapsuleTemplate("Volume name "
"is needed")
if volumes_spec[i].get('cinder'):
cinder_spec = volumes_spec[i].get('cinder')
volume_uuid = cinder_spec.get('volumeID')
volume_size = cinder_spec.get('size')
if not volume_uuid:
if volume_size is None:
raise exception.InvalidCapsuleTemplate("Volume size "
"is needed")
elif volume_uuid and volume_size:
raise exception.InvalidCapsuleTemplate("Volume size and uuid "
"could not be set at "
"the same time")
else:
raise exception.InvalidCapsuleTemplate("Zun now Only support "
"Cinder volume driver")
return volumes_spec
def is_all_projects(search_opts): def is_all_projects(search_opts):
all_projects = search_opts.get('all_projects') or \ all_projects = search_opts.get('all_projects') or \
search_opts.get('all_tenants') search_opts.get('all_tenants')
@ -411,3 +447,61 @@ def check_for_restart_policy(container_dict):
raise exception.InvalidValue(msg) raise exception.InvalidValue(msg)
elif name in ['no']: elif name in ['no']:
container_dict.get('restart_policy')['MaximumRetryCount'] = '0' container_dict.get('restart_policy')['MaximumRetryCount'] = '0'
def build_requested_networks(context, nets):
"""Build requested networks by calling neutron client
:param nets: The special network uuid when create container
if none, will call neutron to create new network.
:returns: available network and ports
"""
neutron_api = neutron.NeutronAPI(context)
requested_networks = []
for net in nets:
if net.get('port'):
port = neutron_api.get_neutron_port(net['port'])
neutron_api.ensure_neutron_port_usable(port)
network = neutron_api.get_neutron_network(port['network_id'])
requested_networks.append({'network': port['network_id'],
'port': port['id'],
'router:external':
network.get('router:external'),
'shared': network.get('shared'),
'v4-fixed-ip': '',
'v6-fixed-ip': '',
'preserve_on_delete': True})
elif net.get('network'):
network = neutron_api.get_neutron_network(net['network'])
requested_networks.append({'network': network['id'],
'port': '',
'router:external':
network.get('router:external'),
'shared': network.get('shared'),
'v4-fixed-ip':
net.get('v4-fixed-ip', ''),
'v6-fixed-ip':
net.get('v6-fixed-ip', ''),
'preserve_on_delete': False})
if not requested_networks:
# Find an available neutron net and create docker network by
# wrapping the neutron net.
neutron_net = neutron_api.get_available_network()
requested_networks.append({'network': neutron_net['id'],
'port': '',
'v4-fixed-ip': '',
'v6-fixed-ip': '',
'preserve_on_delete': False})
check_external_network_attach(context, requested_networks)
return requested_networks
def check_external_network_attach(context, nets):
"""Check if attaching to external network is permitted."""
if not context.can(NETWORK_ATTACH_EXTERNAL,
fatal=False):
for net in nets:
if net.get('router:external') and not net.get('shared'):
raise exception.ExternalNetworkAttachForbidden(
network_uuid=net['network'])

View File

@ -135,8 +135,8 @@ class API(object):
return self.rpcapi.image_search(context, image, image_driver, return self.rpcapi.image_search(context, image, image_driver,
exact_match, *args) exact_match, *args)
def capsule_create(self, context, new_capsule, def capsule_create(self, context, new_capsule, requested_networks=None,
requested_networks=None, extra_spec=None): requested_volumes=None, extra_spec=None):
host_state = None host_state = None
try: try:
host_state = self._schedule_container(context, new_capsule, host_state = self._schedule_container(context, new_capsule,
@ -147,7 +147,8 @@ class API(object):
new_capsule.save(context) new_capsule.save(context)
return return
self.rpcapi.capsule_create(context, host_state['host'], new_capsule, self.rpcapi.capsule_create(context, host_state['host'], new_capsule,
requested_networks, host_state['limits']) requested_networks, requested_volumes,
host_state['limits'])
def capsule_delete(self, context, capsule, *args): def capsule_delete(self, context, capsule, *args):
return self.rpcapi.capsule_delete(context, capsule, *args) return self.rpcapi.capsule_delete(context, capsule, *args)

View File

@ -235,7 +235,6 @@ class Manager(periodic_task.PeriodicTasks):
if self.use_sandbox: if self.use_sandbox:
sandbox = self._create_sandbox(context, container, sandbox = self._create_sandbox(context, container,
requested_networks, requested_networks,
requested_volumes,
reraise) reraise)
if sandbox is None: if sandbox is None:
return return
@ -315,7 +314,7 @@ class Manager(periodic_task.PeriodicTasks):
'driver': self.driver}) 'driver': self.driver})
def _create_sandbox(self, context, container, requested_networks, def _create_sandbox(self, context, container, requested_networks,
requested_volumes, reraise=False): reraise=False):
self._update_task_state(context, container, consts.SANDBOX_CREATING) self._update_task_state(context, container, consts.SANDBOX_CREATING)
sandbox_image = CONF.sandbox_image sandbox_image = CONF.sandbox_image
sandbox_image_driver = CONF.sandbox_image_driver sandbox_image_driver = CONF.sandbox_image_driver
@ -330,7 +329,7 @@ class Manager(periodic_task.PeriodicTasks):
sandbox_id = self.driver.create_sandbox( sandbox_id = self.driver.create_sandbox(
context, container, image=sandbox_image, context, container, image=sandbox_image,
requested_networks=requested_networks, requested_networks=requested_networks,
requested_volumes=requested_volumes) requested_volumes=[])
return sandbox_id return sandbox_id
except Exception as e: except Exception as e:
with excutils.save_and_reraise_exception(reraise=reraise): with excutils.save_and_reraise_exception(reraise=reraise):
@ -866,16 +865,29 @@ class Manager(periodic_task.PeriodicTasks):
except Exception: except Exception:
return return
def capsule_create(self, context, capsule, requested_networks, limits): def capsule_create(self, context, capsule, requested_networks,
requested_volumes, limits):
@utils.synchronized("capsule-" + capsule.uuid) @utils.synchronized("capsule-" + capsule.uuid)
def do_capsule_create(): def do_capsule_create():
self._do_capsule_create(context, capsule, requested_networks, self._do_capsule_create(context, capsule, requested_networks,
limits) requested_volumes, limits)
utils.spawn_n(do_capsule_create) utils.spawn_n(do_capsule_create)
def _do_capsule_create(self, context, capsule, requested_networks=None, def _do_capsule_create(self, context, capsule,
requested_networks=None,
requested_volumes=None,
limits=None, reraise=False): limits=None, reraise=False):
"""Create capsule in the compute node
:param context: security context
:param capsule: the special capsule object
:param requested_networks: the network ports that capsule will
connect
:param requested_volumes: the volume that capsule need
:param limits: no use field now.
:param reraise: flag of reraise the error, default is Falses
"""
capsule.containers[0].image = CONF.sandbox_image capsule.containers[0].image = CONF.sandbox_image
capsule.containers[0].image_driver = CONF.sandbox_image_driver capsule.containers[0].image_driver = CONF.sandbox_image_driver
capsule.containers[0].image_pull_policy = \ capsule.containers[0].image_pull_policy = \
@ -883,20 +895,33 @@ class Manager(periodic_task.PeriodicTasks):
capsule.containers[0].save(context) capsule.containers[0].save(context)
sandbox = self._create_sandbox(context, sandbox = self._create_sandbox(context,
capsule.containers[0], capsule.containers[0],
requested_networks, reraise) requested_networks,
reraise)
capsule.containers[0].task_state = None capsule.containers[0].task_state = None
capsule.containers[0].status = consts.RUNNING capsule.containers[0].status = consts.RUNNING
sandbox_id = capsule.containers[0].get_sandbox_id() sandbox_id = capsule.containers[0].get_sandbox_id()
capsule.containers[0].container_id = sandbox_id capsule.containers[0].container_id = sandbox_id
capsule.containers[0].save(context) capsule.containers[0].save(context)
count = len(capsule.containers) count = len(capsule.containers)
for k in range(1, count): for k in range(1, count):
container_requested_volumes = []
capsule.containers[k].set_sandbox_id(sandbox_id) capsule.containers[k].set_sandbox_id(sandbox_id)
capsule.containers[k].addresses = capsule.containers[0].addresses capsule.containers[k].addresses = capsule.containers[0].addresses
container_name = capsule.containers[k].name
for volume in requested_volumes:
if volume.get(container_name, None):
container_requested_volumes.append(
volume.get(container_name))
if not self._attach_volumes(context, capsule.containers[k],
container_requested_volumes):
return
# Add volume assignment
created_container = \ created_container = \
self._do_container_create_base(context, self._do_container_create_base(context,
capsule.containers[k], capsule.containers[k],
requested_networks, requested_networks,
container_requested_volumes,
sandbox=sandbox, sandbox=sandbox,
limits=limits) limits=limits)
if created_container: if created_container:

View File

@ -173,10 +173,11 @@ class API(rpc_service.API):
exact_match=exact_match) exact_match=exact_match)
def capsule_create(self, context, host, capsule, def capsule_create(self, context, host, capsule,
requested_networks, limits): requested_networks, requested_volumes, limits):
self._cast(host, 'capsule_create', self._cast(host, 'capsule_create',
capsule=capsule, capsule=capsule,
requested_networks=requested_networks, requested_networks=requested_networks,
requested_volumes=requested_volumes,
limits=limits) limits=limits)
def capsule_delete(self, context, capsule): def capsule_delete(self, context, capsule):

View File

@ -161,19 +161,18 @@ class DockerDriver(driver.ContainerDriver):
host_config = {} host_config = {}
host_config['runtime'] = runtime host_config['runtime'] = runtime
host_config['binds'] = binds
kwargs['volumes'] = [b['bind'] for b in binds.values()]
if sandbox_id: if sandbox_id:
host_config['network_mode'] = 'container:%s' % sandbox_id host_config['network_mode'] = 'container:%s' % sandbox_id
# TODO(hongbin): Uncomment this after docker-py add support for # TODO(hongbin): Uncomment this after docker-py add support for
# container mode for pid namespace. # container mode for pid namespace.
# host_config['pid_mode'] = 'container:%s' % sandbox_id # host_config['pid_mode'] = 'container:%s' % sandbox_id
host_config['ipc_mode'] = 'container:%s' % sandbox_id host_config['ipc_mode'] = 'container:%s' % sandbox_id
host_config['volumes_from'] = sandbox_id
else: else:
self._process_networking_config( self._process_networking_config(
context, container, requested_networks, host_config, context, container, requested_networks, host_config,
kwargs, docker) kwargs, docker)
host_config['binds'] = binds
kwargs['volumes'] = [b['bind'] for b in binds.values()]
if container.auto_remove: if container.auto_remove:
host_config['auto_remove'] = container.auto_remove host_config['auto_remove'] = container.auto_remove
if container.memory is not None: if container.memory is not None:

View File

@ -24,19 +24,24 @@ from zun.tests.unit.db import utils
class TestCapsuleController(api_base.FunctionalTest): class TestCapsuleController(api_base.FunctionalTest):
@patch('zun.compute.api.API.capsule_create') @patch('zun.compute.api.API.capsule_create')
def test_create_capsule(self, mock_capsule_create): @patch('zun.network.neutron.NeutronAPI.get_available_network')
params = ('{"spec": {"kind": "capsule",' def test_create_capsule(self, mock_capsule_create,
'"spec": {"containers":' mock_neutron_get_network):
'[{"environment": {"ROOT_PASSWORD": "foo0"}, ' params = ('{'
'"image": "test", "labels": {"app": "web"}, ' '"spec": '
'"image_driver": "docker", "resources": ' '{"kind": "capsule",'
'{"allocation": {"cpu": 1, "memory": 1024}}}], ' ' "spec": {'
'"volumes": [{"name": "volume1", ' ' "containers":'
'"image": "test", "drivers": "cinder", "volumeType": ' ' [{"environment": {"ROOT_PASSWORD": "foo0"}, '
'"type1", "driverOptions": "options", ' ' "image": "test", "labels": {"app": "web"}, '
'"size": "5GB"}]}, ' ' "image_driver": "docker", "resources": '
'"metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"}, ' ' {"allocation": {"cpu": 1, "memory": 1024}}'
'"name": "capsule-example"}}}') ' }]'
' }, '
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},'
' "name": "capsule-example"}'
' }'
'}')
response = self.post('/capsules/', response = self.post('/capsules/',
params=params, params=params,
content_type='application/json') content_type='application/json')
@ -54,21 +59,31 @@ class TestCapsuleController(api_base.FunctionalTest):
self.assertEqual(return_value["cpu"], expected_cpu) self.assertEqual(return_value["cpu"], expected_cpu)
self.assertEqual(202, response.status_int) self.assertEqual(202, response.status_int)
self.assertTrue(mock_capsule_create.called) self.assertTrue(mock_capsule_create.called)
self.assertTrue(mock_neutron_get_network.called)
@patch('zun.compute.api.API.capsule_create') @patch('zun.compute.api.API.capsule_create')
def test_create_capsule_two_containers(self, mock_capsule_create): @patch('zun.network.neutron.NeutronAPI.get_available_network')
params = ('{"spec": {"kind": "capsule",' def test_create_capsule_two_containers(self, mock_capsule_create,
'"spec": {"containers":' mock_neutron_get_network):
'[{"environment": {"ROOT_PASSWORD": "foo0"}, ' params = ('{'
'"image": "test1", "labels": {"app0": "web0"}, ' '"spec": '
'"image_driver": "docker", "resources": ' '{"kind": "capsule",'
'{"allocation": {"cpu": 1, "memory": 1024}}}, ' ' "spec": {'
'{"environment": {"ROOT_PASSWORD": "foo1"}, ' ' "containers":'
'"image": "test1", "labels": {"app1": "web1"}, ' ' [{"environment": {"ROOT_PASSWORD": "foo0"}, '
'"image_driver": "docker", "resources": ' ' "image": "test", "labels": {"app": "web"}, '
'{"allocation": {"cpu": 1, "memory": 1024}}}]}, ' ' "image_driver": "docker", "resources": '
'"metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"}, ' ' {"allocation": {"cpu": 1, "memory": 1024}}},'
'"name": "capsule-example"}}}') ' {"environment": {"ROOT_PASSWORD": "foo1"}, '
' "image": "test1", "labels": {"app1": "web1"}, '
' "image_driver": "docker", "resources": '
' {"allocation": {"cpu": 1, "memory": 1024}}}'
' ]'
' }, '
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},'
' "name": "capsule-example"}'
' }'
'}')
response = self.post('/capsules/', response = self.post('/capsules/',
params=params, params=params,
content_type='application/json') content_type='application/json')
@ -88,6 +103,7 @@ class TestCapsuleController(api_base.FunctionalTest):
self.assertEqual(return_value["cpu"], expected_cpu) self.assertEqual(return_value["cpu"], expected_cpu)
self.assertEqual(202, response.status_int) self.assertEqual(202, response.status_int)
self.assertTrue(mock_capsule_create.called) self.assertTrue(mock_capsule_create.called)
self.assertTrue(mock_neutron_get_network.called)
@patch('zun.compute.api.API.capsule_create') @patch('zun.compute.api.API.capsule_create')
@patch('zun.common.utils.check_capsule_template') @patch('zun.common.utils.check_capsule_template')
@ -150,6 +166,177 @@ class TestCapsuleController(api_base.FunctionalTest):
params=params, content_type='application/json') params=params, content_type='application/json')
self.assertFalse(mock_capsule_create.called) self.assertFalse(mock_capsule_create.called)
@patch('zun.volume.cinder_api.CinderAPI.ensure_volume_usable')
@patch('zun.volume.cinder_api.CinderAPI.create_volume')
@patch('zun.compute.api.API.capsule_create')
@patch('zun.network.neutron.NeutronAPI.get_available_network')
def test_create_capsule_with_create_new_volume(self, mock_capsule_create,
mock_neutron_get_network,
mock_create_volume,
mock_ensure_volume_usable):
fake_volume_id = 'fakevolid'
fake_volume = mock.Mock(id=fake_volume_id)
mock_create_volume.return_value = fake_volume
params = ('{'
'"spec":'
'{"kind": "capsule",'
' "spec":'
' {"containers":'
' [{"environment": {"ROOT_PASSWORD": "foo0"}, '
' "image": "test", "labels": {"app": "web"}, '
' "image_driver": "docker", "resources": '
' {"allocation": {"cpu": 1, "memory": 1024}},'
' "volumeMounts": [{"name": "volume1", '
' "mountPath": "/data1"}]'
' }'
' ],'
' "volumes":'
' [{"name": "volume1",'
' "cinder": {"size": 3, "autoRemove": "True"}'
' }]'
' }, '
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},'
' "name": "capsule-example"}'
' }'
'}')
response = self.post('/capsules/',
params=params,
content_type='application/json')
return_value = response.json
expected_meta_name = "capsule-example"
expected_meta_labels = {"foo0": "bar0", "foo1": "bar1"}
expected_memory = '1024M'
expected_cpu = 1.0
expected_container_num = 2
self.assertEqual(len(return_value["containers_uuids"]),
expected_container_num)
self.assertEqual(return_value["meta_name"], expected_meta_name)
self.assertEqual(return_value["meta_labels"], expected_meta_labels)
self.assertEqual(return_value["memory"], expected_memory)
self.assertEqual(return_value["cpu"], expected_cpu)
self.assertEqual(202, response.status_int)
self.assertTrue(mock_capsule_create.called)
self.assertTrue(mock_neutron_get_network.called)
self.assertTrue(mock_create_volume.called)
@patch('zun.volume.cinder_api.CinderAPI.ensure_volume_usable')
@patch('zun.volume.cinder_api.CinderAPI.search_volume')
@patch('zun.compute.api.API.capsule_create')
@patch('zun.network.neutron.NeutronAPI.get_available_network')
def test_create_capsule_with_existed_volume(self, mock_capsule_create,
mock_neutron_get_network,
mock_search_volume,
mock_ensure_volume_usable):
fake_volume_id = 'fakevolid'
fake_volume = mock.Mock(id=fake_volume_id)
mock_search_volume.return_value = fake_volume
params = ('{'
'"spec":'
'{"kind": "capsule",'
' "spec":'
' {"containers":'
' [{"environment": {"ROOT_PASSWORD": "foo0"}, '
' "image": "test", "labels": {"app": "web"}, '
' "image_driver": "docker", "resources": '
' {"allocation": {"cpu": 1, "memory": 1024}},'
' "volumeMounts": [{"name": "volume1", '
' "mountPath": "/data1"}]'
' }'
' ],'
' "volumes":'
' [{"name": "volume1",'
' "cinder": {"volumeID": "fakevolid"}'
' }]'
' }, '
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},'
' "name": "capsule-example"}'
' }'
'}')
response = self.post('/capsules/',
params=params,
content_type='application/json')
return_value = response.json
expected_meta_name = "capsule-example"
expected_meta_labels = {"foo0": "bar0", "foo1": "bar1"}
expected_memory = '1024M'
expected_cpu = 1.0
expected_container_num = 2
self.assertEqual(len(return_value["containers_uuids"]),
expected_container_num)
self.assertEqual(return_value["meta_name"], expected_meta_name)
self.assertEqual(return_value["meta_labels"], expected_meta_labels)
self.assertEqual(return_value["memory"], expected_memory)
self.assertEqual(return_value["cpu"], expected_cpu)
self.assertEqual(202, response.status_int)
self.assertTrue(mock_capsule_create.called)
self.assertTrue(mock_neutron_get_network.called)
self.assertTrue(mock_ensure_volume_usable.called)
self.assertTrue(mock_search_volume.called)
@patch('zun.volume.cinder_api.CinderAPI.create_volume')
@patch('zun.volume.cinder_api.CinderAPI.ensure_volume_usable')
@patch('zun.volume.cinder_api.CinderAPI.search_volume')
@patch('zun.compute.api.API.capsule_create')
@patch('zun.network.neutron.NeutronAPI.get_available_network')
def test_create_capsule_with_two_volumes(self, mock_capsule_create,
mock_neutron_get_network,
mock_search_volume,
mock_ensure_volume_usable,
mock_create_volume):
fake_volume_id1 = 'fakevolid1'
fake_volume = mock.Mock(id=fake_volume_id1)
mock_search_volume.return_value = fake_volume
fake_volume_id2 = 'fakevolid2'
fake_volume = mock.Mock(id=fake_volume_id2)
mock_create_volume.return_value = fake_volume
params = ('{'
'"spec":'
'{"kind": "capsule",'
' "spec":'
' {"containers":'
' [{"environment": {"ROOT_PASSWORD": "foo0"}, '
' "image": "test", "labels": {"app": "web"}, '
' "image_driver": "docker", "resources": '
' {"allocation": {"cpu": 1, "memory": 1024}},'
' "volumeMounts": [{"name": "volume1", '
' "mountPath": "/data1"},'
' {"name": "volume2", '
' "mountPath": "/data2"}]'
' }'
' ],'
' "volumes":'
' [{"name": "volume1",'
' "cinder": {"volumeID": "fakevolid1"}},'
' {"name": "volume2",'
' "cinder": {"size": 3, "autoRemove": "True"}'
' }]'
' }, '
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},'
' "name": "capsule-example"}'
' }'
'}')
response = self.post('/capsules/',
params=params,
content_type='application/json')
return_value = response.json
expected_meta_name = "capsule-example"
expected_meta_labels = {"foo0": "bar0", "foo1": "bar1"}
expected_memory = '1024M'
expected_cpu = 1.0
expected_container_num = 2
self.assertEqual(len(return_value["containers_uuids"]),
expected_container_num)
self.assertEqual(return_value["meta_name"], expected_meta_name)
self.assertEqual(return_value["meta_labels"], expected_meta_labels)
self.assertEqual(return_value["memory"], expected_memory)
self.assertEqual(return_value["cpu"], expected_cpu)
self.assertEqual(202, response.status_int)
self.assertTrue(mock_capsule_create.called)
self.assertTrue(mock_neutron_get_network.called)
self.assertTrue(mock_ensure_volume_usable.called)
self.assertTrue(mock_search_volume.called)
self.assertTrue(mock_create_volume.called)
@patch('zun.compute.api.API.container_show') @patch('zun.compute.api.API.container_show')
@patch('zun.objects.Capsule.get_by_uuid') @patch('zun.objects.Capsule.get_by_uuid')
@patch('zun.objects.Container.get_by_uuid') @patch('zun.objects.Container.get_by_uuid')

View File

@ -143,25 +143,53 @@ class TestUtils(base.TestCase):
params = ({"kind": "capsule", "spec": {}}) params = ({"kind": "capsule", "spec": {}})
utils.check_capsule_template(params) utils.check_capsule_template(params)
def test_capsule_get_container_spec(self):
with self.assertRaisesRegex( with self.assertRaisesRegex(
exception.InvalidCapsuleTemplate, exception.InvalidCapsuleTemplate,
"Capsule need to have one container at least"): "Capsule need to have one container at least"):
params = ({"kind": "capsule", "spec": {"containers": []}}) params = ({"containers": []})
utils.check_capsule_template(params) utils.capsule_get_container_spec(params)
with self.assertRaisesRegex( with self.assertRaisesRegex(
exception.InvalidCapsuleTemplate, "Container " exception.InvalidCapsuleTemplate, "Container "
"image is needed"): "image is needed"):
params = ({"kind": "capsule", params = ({"containers": [{"labels": {"app": "web"}}]})
"spec": {"containers": [{"labels": {"app": "web"}}]}}) utils.capsule_get_container_spec(params)
utils.check_capsule_template(params)
with self.assertRaisesRegex( with self.assertRaisesRegex(
exception.InvalidCapsuleTemplate, "Container image is needed"): exception.InvalidCapsuleTemplate, "Container image is needed"):
params = ({"kind": "capsule", params = ({"containers": [
"spec": {"containers": [{"image": "test1"}, {"image": "test1"},
{"environment": {"ROOT_PASSWORD": "foo0"}}]}}) {"environment": {"ROOT_PASSWORD": "foo0"}}]})
utils.check_capsule_template(params) utils.capsule_get_container_spec(params)
def test_capsule_get_volume_spec(self):
with self.assertRaisesRegex(
exception.InvalidCapsuleTemplate,
"Volume name is needed"):
params = ({"volumes": [{"foo": "bar"}]})
utils.capsule_get_volume_spec(params)
with self.assertRaisesRegex(
exception.InvalidCapsuleTemplate, "Volume size is needed"):
params = ({"volumes": [{"name": "test",
"cinder": {"foo": "bar"}}]})
utils.capsule_get_volume_spec(params)
with self.assertRaisesRegex(
exception.InvalidCapsuleTemplate, "Volume size and uuid "
"could not be set at "
"the same time"):
params = ({"volumes": [{"name": "test",
"cinder": {"size": 3,
"volumeID": "fakevolid"}}]})
utils.capsule_get_volume_spec(params)
with self.assertRaisesRegex(
exception.InvalidCapsuleTemplate, "Zun now Only support "
"Cinder volume driver"):
params = ({"volumes": [{"name": "test", "other": {}}]})
utils.capsule_get_volume_spec(params)
@patch('zun.objects.Image.get_by_uuid') @patch('zun.objects.Image.get_by_uuid')
def test_get_image(self, mock_image_get_by_uuid): def test_get_image(self, mock_image_get_by_uuid):