Add detailed parameters for Capsule create
Currently we don't have detailed parameters checking when creating a capsule, this patch add the volume parameters type checking and several test cases. Also modify the related template file and design document. Change some parameters in the template to Kubernetes friendly. Modify the field check from "spec" to "template" in check_capsule_template, since there already a low level spec field in capsule yaml. Will also modify the python-zunclient. Part of blueprint introduce-compose Change-Id: I88c1c248d83d0a27f5a291fcf9d952bb70234dff Signed-off-by: Kevin Zhao <kevin.zhao@arm.com>
This commit is contained in:
parent
97524cba92
commit
d4a5f9c5e9
@ -164,7 +164,7 @@ Sample capsule:
|
|||||||
env:
|
env:
|
||||||
PATH: /usr/local/bin
|
PATH: /usr/local/bin
|
||||||
resources:
|
resources:
|
||||||
allocation:
|
requests:
|
||||||
cpu: 1
|
cpu: 1
|
||||||
memory: 2GB
|
memory: 2GB
|
||||||
volumes:
|
volumes:
|
||||||
@ -219,7 +219,7 @@ Ports fields:
|
|||||||
* protocol(string): TCP or UDP, by default is TCP
|
* protocol(string): TCP or UDP, by default is TCP
|
||||||
|
|
||||||
RecourcesObject fields:
|
RecourcesObject fields:
|
||||||
* allocation(AllocationObject): the resources that the capsule needed
|
* requests(AllocationObject): the resources that the capsule needed
|
||||||
|
|
||||||
AllocationObject:
|
AllocationObject:
|
||||||
* cpu(string): cpu resources, cores number
|
* cpu(string): cpu resources, cores number
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
capsule_template_version: 2017-12-20
|
|
||||||
# use "-" because that the fields have many items
|
# use "-" because that the fields have many items
|
||||||
capsule_version: beta
|
capsuleVersion: beta
|
||||||
kind: capsule
|
kind: capsule
|
||||||
metadata:
|
metadata:
|
||||||
name: capsule-volume
|
name: capsule-volume
|
||||||
labels:
|
labels:
|
||||||
foo: bar
|
foo: bar
|
||||||
restart_policy: always
|
restartPolicy: always
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- image: test
|
- image: test
|
||||||
command:
|
command:
|
||||||
- "/bin/bash"
|
- "/bin/bash"
|
||||||
workdir: /root
|
workDir: /root
|
||||||
labels:
|
labels:
|
||||||
app: web
|
app: web
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
|
@ -1,33 +1,30 @@
|
|||||||
capsule_template_version: 2017-06-21
|
|
||||||
# use "-" because that the fields have many items
|
# use "-" because that the fields have many items
|
||||||
capsule_version: beta
|
capsuleVersion: beta
|
||||||
kind: capsule
|
kind: capsule
|
||||||
metadata:
|
metadata:
|
||||||
name: capsule-example
|
name: capsule-example
|
||||||
labels:
|
labels:
|
||||||
app: web
|
app: web
|
||||||
nihao: baibai
|
app1: web1
|
||||||
restart_policy: always
|
restartPolicy: always
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- image: ubuntu
|
- image: ubuntu
|
||||||
command:
|
command:
|
||||||
- "/bin/bash"
|
- "/bin/bash"
|
||||||
image_pull_policy: ifnotpresent
|
imagePullPolicy: ifnotpresent
|
||||||
workdir: /root
|
workDir: /root
|
||||||
labels:
|
|
||||||
app: web
|
|
||||||
ports:
|
ports:
|
||||||
- name: nginx-port
|
- name: nginx-port
|
||||||
containerPort: 80
|
containerPort: 80
|
||||||
hostPort: 80
|
hostPort: 80
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
resources:
|
resources:
|
||||||
allocation:
|
requests:
|
||||||
cpu: 1
|
cpu: 1
|
||||||
memory: 1024
|
memory: 1024
|
||||||
environment:
|
env:
|
||||||
PATCH: /usr/local/bin
|
ENV1: /usr/local/bin
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: volume1
|
- name: volume1
|
||||||
mountPath: /data1
|
mountPath: /data1
|
||||||
@ -38,10 +35,8 @@ spec:
|
|||||||
args:
|
args:
|
||||||
- "Hello"
|
- "Hello"
|
||||||
- "World"
|
- "World"
|
||||||
image_pull_policy: ifnotpresent
|
imagePullPolicy: ifnotpresent
|
||||||
workdir: /root
|
workDir: /root
|
||||||
labels:
|
|
||||||
app: web01
|
|
||||||
ports:
|
ports:
|
||||||
- name: nginx-port
|
- name: nginx-port
|
||||||
containerPort: 80
|
containerPort: 80
|
||||||
@ -52,11 +47,11 @@ spec:
|
|||||||
hostPort: 3306
|
hostPort: 3306
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
resources:
|
resources:
|
||||||
allocation:
|
requests:
|
||||||
cpu: 1
|
cpu: 1
|
||||||
memory: 1024
|
memory: 1024
|
||||||
environment:
|
env:
|
||||||
NWH: /usr/bin/
|
ENV2: /usr/bin/
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: volume2
|
- name: volume2
|
||||||
mountPath: /data2
|
mountPath: /data2
|
||||||
|
@ -131,8 +131,8 @@ class CapsuleController(base.Controller):
|
|||||||
action="capsule:create")
|
action="capsule:create")
|
||||||
|
|
||||||
# Abstract the capsule specification
|
# Abstract the capsule specification
|
||||||
capsules_spec = capsule_dict['spec']
|
capsules_template = capsule_dict.get('template')
|
||||||
spec_content = utils.check_capsule_template(capsules_spec)
|
spec_content = utils.check_capsule_template(capsules_template)
|
||||||
containers_spec = utils.capsule_get_container_spec(spec_content)
|
containers_spec = utils.capsule_get_container_spec(spec_content)
|
||||||
volumes_spec = utils.capsule_get_volume_spec(spec_content)
|
volumes_spec = utils.capsule_get_volume_spec(spec_content)
|
||||||
|
|
||||||
@ -148,10 +148,11 @@ class CapsuleController(base.Controller):
|
|||||||
capsule_need_memory = 0
|
capsule_need_memory = 0
|
||||||
container_volume_requests = []
|
container_volume_requests = []
|
||||||
|
|
||||||
capsule_restart_policy = capsules_spec.get('restart_policy', 'always')
|
capsule_restart_policy = capsules_template.get('restart_policy',
|
||||||
|
'always')
|
||||||
|
|
||||||
metadata_info = capsules_spec.get('metadata', None)
|
metadata_info = capsules_template.get('metadata', None)
|
||||||
requested_networks_info = capsules_spec.get('nets', [])
|
requested_networks_info = capsules_template.get('nets', [])
|
||||||
requested_networks = \
|
requested_networks = \
|
||||||
utils.build_requested_networks(context, requested_networks_info)
|
utils.build_requested_networks(context, requested_networks_info)
|
||||||
|
|
||||||
@ -203,7 +204,7 @@ class CapsuleController(base.Controller):
|
|||||||
|
|
||||||
if container_dict.get('resources'):
|
if container_dict.get('resources'):
|
||||||
resources_list = container_dict.get('resources')
|
resources_list = container_dict.get('resources')
|
||||||
allocation = resources_list.get('allocation')
|
allocation = resources_list.get('requests')
|
||||||
if allocation.get('cpu'):
|
if allocation.get('cpu'):
|
||||||
capsule_need_cpu += allocation.get('cpu')
|
capsule_need_cpu += allocation.get('cpu')
|
||||||
container_dict['cpu'] = allocation.get('cpu')
|
container_dict['cpu'] = allocation.get('cpu')
|
||||||
|
@ -15,12 +15,12 @@
|
|||||||
from zun.common.validation import parameter_types
|
from zun.common.validation import parameter_types
|
||||||
|
|
||||||
_capsule_properties = {
|
_capsule_properties = {
|
||||||
'spec': parameter_types.spec
|
'template': parameter_types.capsule_template
|
||||||
}
|
}
|
||||||
|
|
||||||
capsule_create = {
|
capsule_create = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': _capsule_properties,
|
'properties': _capsule_properties,
|
||||||
'required': ['spec'],
|
'required': ['template'],
|
||||||
'additionalProperties': False
|
'additionalProperties': False
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,28 @@ VALID_STATES = {
|
|||||||
consts.PAUSED]
|
consts.PAUSED]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VALID_CONTAINER_FILED = {
|
||||||
|
'image': 'image',
|
||||||
|
'command': 'command',
|
||||||
|
'args': 'args',
|
||||||
|
'resources': 'resources',
|
||||||
|
'ports': 'ports',
|
||||||
|
'volumeMounts': 'volumeMounts',
|
||||||
|
'env': 'environment',
|
||||||
|
'workDir': 'workdir',
|
||||||
|
'imagePullPolicy': 'image_pull_policy',
|
||||||
|
}
|
||||||
|
|
||||||
|
VALID_CAPSULE_FIELD = {
|
||||||
|
'restartPolicy': 'restart_policy',
|
||||||
|
}
|
||||||
|
|
||||||
|
VALID_CAPSULE_RESTART_POLICY = {
|
||||||
|
'Never': 'no',
|
||||||
|
'Always': 'always',
|
||||||
|
'OnFailure': 'on-failure',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def validate_container_state(container, action):
|
def validate_container_state(container, action):
|
||||||
if container.status not in VALID_STATES[action]:
|
if container.status not in VALID_STATES[action]:
|
||||||
@ -344,6 +366,12 @@ def check_capsule_template(tpl):
|
|||||||
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")
|
||||||
|
# Align the Capsule restartPolicy with container restart_policy
|
||||||
|
if 'restartPolicy' in tpl.keys():
|
||||||
|
tpl['restartPolicy'] = \
|
||||||
|
VALID_CAPSULE_RESTART_POLICY[tpl['restartPolicy']]
|
||||||
|
tpl[VALID_CAPSULE_FIELD['restartPolicy']] = tpl.pop('restartPolicy')
|
||||||
|
|
||||||
spec_field = tpl.get('spec')
|
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")
|
||||||
@ -360,10 +388,15 @@ def capsule_get_container_spec(spec_field):
|
|||||||
"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')
|
container_spec = containers_spec[i]
|
||||||
if container_image is None:
|
if 'image' not in container_spec.keys():
|
||||||
raise exception.InvalidCapsuleTemplate("Container "
|
raise exception.InvalidCapsuleTemplate("Container "
|
||||||
"image is needed")
|
"image is needed")
|
||||||
|
# Remap the Capsule's container fields to native Zun container fields.
|
||||||
|
for key in list(container_spec.keys()):
|
||||||
|
container_spec[VALID_CONTAINER_FILED[key]] = \
|
||||||
|
container_spec.pop(key)
|
||||||
|
|
||||||
return containers_spec
|
return containers_spec
|
||||||
|
|
||||||
|
|
||||||
|
@ -256,6 +256,176 @@ security_groups = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
spec = {
|
capsule_kind = {
|
||||||
'type': ['object'],
|
"type": ["string"],
|
||||||
|
'enum': ['capsule', 'Capsule']
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_version = {
|
||||||
|
"type": ["string"],
|
||||||
|
'enum': ['beta', 'Beta']
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_metadata = {
|
||||||
|
"type": ["object"],
|
||||||
|
"properties": {
|
||||||
|
"labels": labels,
|
||||||
|
# use the same format as container name
|
||||||
|
"name": container_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_restart_policy = {
|
||||||
|
"type": ["string"],
|
||||||
|
"enum": ['Always', 'OnFailure', 'Never']
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_container_command = {
|
||||||
|
'type': ['array'],
|
||||||
|
'items': command
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_container_args = capsule_container_command
|
||||||
|
|
||||||
|
capsule_container_resources = {
|
||||||
|
'type': ['object'],
|
||||||
|
'properties': {
|
||||||
|
'requests': {
|
||||||
|
"type": ["object"],
|
||||||
|
'properties': {
|
||||||
|
'cpu': cpu,
|
||||||
|
'memory': memory,
|
||||||
|
},
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"required": ['requests']
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_port_protocol = {
|
||||||
|
"type": ["string"],
|
||||||
|
'enum': ['TCP', 'UDP']
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_container_ports = {
|
||||||
|
'type': ['array'],
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': container_name,
|
||||||
|
'containerPort': non_negative_integer,
|
||||||
|
'hostPort': non_negative_integer,
|
||||||
|
'protocol': capsule_port_protocol,
|
||||||
|
},
|
||||||
|
'additionalProperties': False,
|
||||||
|
'required': ['containerPort', 'hostPort']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_name = {
|
||||||
|
'type': ['string'],
|
||||||
|
'minLength': 2,
|
||||||
|
'maxLength': 255,
|
||||||
|
'pattern': '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$'
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_volume_path = {
|
||||||
|
'type': ['string']
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_container_volume_list = {
|
||||||
|
'type': ['array'],
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': volume_name,
|
||||||
|
'mountPath': capsule_volume_path,
|
||||||
|
'readOnly': boolean,
|
||||||
|
},
|
||||||
|
'additionalProperties': False,
|
||||||
|
'required': ['name', 'mountPath']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_containers_list = {
|
||||||
|
'type': ['array'],
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'image': image_name,
|
||||||
|
'command': capsule_container_command,
|
||||||
|
'args': capsule_container_args,
|
||||||
|
'resources': capsule_container_resources,
|
||||||
|
'ports': capsule_container_ports,
|
||||||
|
'volumeMounts': capsule_container_volume_list,
|
||||||
|
'env': environment,
|
||||||
|
'workDir': workdir,
|
||||||
|
'imagePullPolicy': image_pull_policy,
|
||||||
|
},
|
||||||
|
'additionalProperties': False,
|
||||||
|
'required': ['image']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_size = {
|
||||||
|
'type': ['number'],
|
||||||
|
'pattern': '^[0-9]*$',
|
||||||
|
'minLength': 1
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_auto_remove = {
|
||||||
|
'type': boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_uuid = {
|
||||||
|
'type': 'string',
|
||||||
|
'maxLength': 36,
|
||||||
|
'minLength': 36
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_cinder_volume = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'volumeID': volume_uuid,
|
||||||
|
'size': volume_size,
|
||||||
|
'autoRemove': boolean,
|
||||||
|
},
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_volumes_list = {
|
||||||
|
'type': ['array', 'null'],
|
||||||
|
'items': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': image_name,
|
||||||
|
'cinder': capsule_cinder_volume,
|
||||||
|
},
|
||||||
|
'additionalProperties': True,
|
||||||
|
'required': ['name', 'cinder']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_spec = {
|
||||||
|
'type': ['object'],
|
||||||
|
"properties": {
|
||||||
|
"containers": capsule_containers_list,
|
||||||
|
"volumes": capsule_volumes_list,
|
||||||
|
},
|
||||||
|
"additionalProperties": True,
|
||||||
|
"required": ['containers']
|
||||||
|
}
|
||||||
|
|
||||||
|
capsule_template = {
|
||||||
|
'type': ['object'],
|
||||||
|
"properties": {
|
||||||
|
"kind": capsule_kind,
|
||||||
|
"capsuleVersion": capsule_version,
|
||||||
|
"metadata": capsule_metadata,
|
||||||
|
"restartPolicy": capsule_restart_policy,
|
||||||
|
"spec": capsule_spec,
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"required": ['kind', 'spec', 'metadata']
|
||||||
}
|
}
|
||||||
|
@ -28,14 +28,14 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
def test_create_capsule(self, mock_capsule_create,
|
def test_create_capsule(self, mock_capsule_create,
|
||||||
mock_neutron_get_network):
|
mock_neutron_get_network):
|
||||||
params = ('{'
|
params = ('{'
|
||||||
'"spec": '
|
'"template": '
|
||||||
'{"kind": "capsule",'
|
'{"kind": "capsule",'
|
||||||
' "spec": {'
|
' "spec": {'
|
||||||
' "containers":'
|
' "containers":'
|
||||||
' [{"environment": {"ROOT_PASSWORD": "foo0"}, '
|
' [{"env": {"ROOT_PASSWORD": "foo0"}, '
|
||||||
' "image": "test", "labels": {"app": "web"}, '
|
' "image": "test",'
|
||||||
' "image_driver": "docker", "resources": '
|
' "resources": '
|
||||||
' {"allocation": {"cpu": 1, "memory": 1024}}'
|
' {"requests": {"cpu": 1, "memory": 1024}}'
|
||||||
' }]'
|
' }]'
|
||||||
' }, '
|
' }, '
|
||||||
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},'
|
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},'
|
||||||
@ -66,21 +66,16 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
def test_create_capsule_two_containers(self, mock_capsule_create,
|
def test_create_capsule_two_containers(self, mock_capsule_create,
|
||||||
mock_neutron_get_network):
|
mock_neutron_get_network):
|
||||||
params = ('{'
|
params = ('{'
|
||||||
'"spec": '
|
'"template": '
|
||||||
'{"kind": "capsule",'
|
'{"kind": "capsule",'
|
||||||
' "spec": {'
|
' "spec": {'
|
||||||
' "containers":'
|
' "containers":'
|
||||||
' [{"environment": {"ROOT_PASSWORD": "foo0"}, '
|
' [{"image": "test", "resources": '
|
||||||
' "image": "test", "labels": {"app": "web"}, '
|
' {"requests": {"cpu": 1, "memory": 1024}}}, '
|
||||||
' "image_driver": "docker", "resources": '
|
' {"image": "test1", "resources": '
|
||||||
' {"allocation": {"cpu": 1, "memory": 1024}}},'
|
' {"requests": {"cpu": 1, "memory": 1024}}}]'
|
||||||
' {"environment": {"ROOT_PASSWORD": "foo1"}, '
|
|
||||||
' "image": "test1", "labels": {"app1": "web1"}, '
|
|
||||||
' "image_driver": "docker", "resources": '
|
|
||||||
' {"allocation": {"cpu": 1, "memory": 1024}}}'
|
|
||||||
' ]'
|
|
||||||
' }, '
|
' }, '
|
||||||
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},'
|
' "metadata": {"labels": {"foo0": "bar0"},'
|
||||||
' "name": "capsule-example"}'
|
' "name": "capsule-example"}'
|
||||||
' }'
|
' }'
|
||||||
'}')
|
'}')
|
||||||
@ -89,7 +84,7 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
content_type='application/json')
|
content_type='application/json')
|
||||||
return_value = response.json
|
return_value = response.json
|
||||||
expected_meta_name = "capsule-example"
|
expected_meta_name = "capsule-example"
|
||||||
expected_meta_labels = {"foo0": "bar0", "foo1": "bar1"}
|
expected_meta_labels = {"foo0": "bar0"}
|
||||||
expected_memory = '2048M'
|
expected_memory = '2048M'
|
||||||
expected_cpu = 2.0
|
expected_cpu = 2.0
|
||||||
expected_container_num = 3
|
expected_container_num = 3
|
||||||
@ -109,12 +104,11 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
@patch('zun.common.utils.check_capsule_template')
|
@patch('zun.common.utils.check_capsule_template')
|
||||||
def test_create_capsule_wrong_kind_set(self, mock_check_template,
|
def test_create_capsule_wrong_kind_set(self, mock_check_template,
|
||||||
mock_capsule_create):
|
mock_capsule_create):
|
||||||
params = ('{"spec": {"kind": "test",'
|
params = ('{"template": {"kind": "test",'
|
||||||
'"spec": {"containers":'
|
'"spec": {"containers":'
|
||||||
'[{"environment": {"ROOT_PASSWORD": "foo0"}, '
|
'[{"environment": {"ROOT_PASSWORD": "foo0"}, '
|
||||||
'"image": "test1", "labels": {"app0": "web0"}, '
|
'"image": "test1", "resources": '
|
||||||
'"image_driver": "docker", "resources": '
|
'{"requests": {"cpu": 1, "memory": 1024}}}]}, '
|
||||||
'{"allocation": {"cpu": 1, "memory": 1024}}}]}, '
|
|
||||||
'"metadata": {"labels": {"foo0": "bar0"}, '
|
'"metadata": {"labels": {"foo0": "bar0"}, '
|
||||||
'"name": "capsule-example"}}}')
|
'"name": "capsule-example"}}}')
|
||||||
mock_check_template.side_effect = exception.InvalidCapsuleTemplate(
|
mock_check_template.side_effect = exception.InvalidCapsuleTemplate(
|
||||||
@ -127,7 +121,7 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
@patch('zun.common.utils.check_capsule_template')
|
@patch('zun.common.utils.check_capsule_template')
|
||||||
def test_create_capsule_less_than_one_container(self, mock_check_template,
|
def test_create_capsule_less_than_one_container(self, mock_check_template,
|
||||||
mock_capsule_create):
|
mock_capsule_create):
|
||||||
params = ('{"spec": {"kind": "capsule",'
|
params = ('{"template": {"kind": "capsule",'
|
||||||
'"spec": {container:[]}, '
|
'"spec": {container:[]}, '
|
||||||
'"metadata": {"labels": {"foo0": "bar0"}, '
|
'"metadata": {"labels": {"foo0": "bar0"}, '
|
||||||
'"name": "capsule-example"}}}')
|
'"name": "capsule-example"}}}')
|
||||||
@ -141,7 +135,7 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
@patch('zun.common.utils.check_capsule_template')
|
@patch('zun.common.utils.check_capsule_template')
|
||||||
def test_create_capsule_no_container_field(self, mock_check_template,
|
def test_create_capsule_no_container_field(self, mock_check_template,
|
||||||
mock_capsule_create):
|
mock_capsule_create):
|
||||||
params = ('{"spec": {"kind": "capsule",'
|
params = ('{"template": {"kind": "capsule",'
|
||||||
'"spec": {}, '
|
'"spec": {}, '
|
||||||
'"metadata": {"labels": {"foo0": "bar0"}, '
|
'"metadata": {"labels": {"foo0": "bar0"}, '
|
||||||
'"name": "capsule-example"}}}')
|
'"name": "capsule-example"}}}')
|
||||||
@ -155,8 +149,8 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
@patch('zun.common.utils.check_capsule_template')
|
@patch('zun.common.utils.check_capsule_template')
|
||||||
def test_create_capsule_no_container_image(self, mock_check_template,
|
def test_create_capsule_no_container_image(self, mock_check_template,
|
||||||
mock_capsule_create):
|
mock_capsule_create):
|
||||||
params = ('{"spec": {"kind": "capsule",'
|
params = ('{"template": {"kind": "capsule",'
|
||||||
'"spec": {container:[{"environment": '
|
'"spec": {container:[{"env": '
|
||||||
'{"ROOT_PASSWORD": "foo1"}]}, '
|
'{"ROOT_PASSWORD": "foo1"}]}, '
|
||||||
'"metadata": {"labels": {"foo0": "bar0"}, '
|
'"metadata": {"labels": {"foo0": "bar0"}, '
|
||||||
'"name": "capsule-example"}}}')
|
'"name": "capsule-example"}}}')
|
||||||
@ -174,18 +168,17 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
mock_neutron_get_network,
|
mock_neutron_get_network,
|
||||||
mock_create_volume,
|
mock_create_volume,
|
||||||
mock_ensure_volume_usable):
|
mock_ensure_volume_usable):
|
||||||
fake_volume_id = 'fakevolid'
|
fake_volume_id = '3259309d-659c-4e20-b354-ee712e64b3b2'
|
||||||
fake_volume = mock.Mock(id=fake_volume_id)
|
fake_volume = mock.Mock(id=fake_volume_id)
|
||||||
mock_create_volume.return_value = fake_volume
|
mock_create_volume.return_value = fake_volume
|
||||||
params = ('{'
|
params = ('{'
|
||||||
'"spec":'
|
'"template":'
|
||||||
'{"kind": "capsule",'
|
'{"kind": "capsule",'
|
||||||
' "spec":'
|
' "spec":'
|
||||||
' {"containers":'
|
' {"containers":'
|
||||||
' [{"environment": {"ROOT_PASSWORD": "foo0"}, '
|
' [{"env": {"ROOT_PASSWORD": "foo0"}, '
|
||||||
' "image": "test", "labels": {"app": "web"}, '
|
' "image": "test", "resources": '
|
||||||
' "image_driver": "docker", "resources": '
|
' {"requests": {"cpu": 1, "memory": 1024}},'
|
||||||
' {"allocation": {"cpu": 1, "memory": 1024}},'
|
|
||||||
' "volumeMounts": [{"name": "volume1", '
|
' "volumeMounts": [{"name": "volume1", '
|
||||||
' "mountPath": "/data1"}]'
|
' "mountPath": "/data1"}]'
|
||||||
' }'
|
' }'
|
||||||
@ -195,7 +188,8 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
' "cinder": {"size": 3, "autoRemove": "True"}'
|
' "cinder": {"size": 3, "autoRemove": "True"}'
|
||||||
' }]'
|
' }]'
|
||||||
' }, '
|
' }, '
|
||||||
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},'
|
' "metadata": {"labels": '
|
||||||
|
' {"foo0": "bar0", "foo1": "bar1"},'
|
||||||
' "name": "capsule-example"}'
|
' "name": "capsule-example"}'
|
||||||
' }'
|
' }'
|
||||||
'}')
|
'}')
|
||||||
@ -227,28 +221,29 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
mock_neutron_get_network,
|
mock_neutron_get_network,
|
||||||
mock_search_volume,
|
mock_search_volume,
|
||||||
mock_ensure_volume_usable):
|
mock_ensure_volume_usable):
|
||||||
fake_volume_id = 'fakevolid'
|
fake_volume_id = '3259309d-659c-4e20-b354-ee712e64b3b2'
|
||||||
fake_volume = mock.Mock(id=fake_volume_id)
|
fake_volume = mock.Mock(id=fake_volume_id)
|
||||||
mock_search_volume.return_value = fake_volume
|
mock_search_volume.return_value = fake_volume
|
||||||
params = ('{'
|
params = ('{'
|
||||||
'"spec":'
|
'"template":'
|
||||||
'{"kind": "capsule",'
|
'{"kind": "capsule",'
|
||||||
' "spec":'
|
' "spec":'
|
||||||
' {"containers":'
|
' {"containers":'
|
||||||
' [{"environment": {"ROOT_PASSWORD": "foo0"}, '
|
' [{"env": {"ROOT_PASSWORD": "foo0"}, '
|
||||||
' "image": "test", "labels": {"app": "web"}, '
|
' "image": "test", "resources": '
|
||||||
' "image_driver": "docker", "resources": '
|
' {"requests": {"cpu": 1, "memory": 1024}},'
|
||||||
' {"allocation": {"cpu": 1, "memory": 1024}},'
|
|
||||||
' "volumeMounts": [{"name": "volume1", '
|
' "volumeMounts": [{"name": "volume1", '
|
||||||
' "mountPath": "/data1"}]'
|
' "mountPath": "/data1"}]'
|
||||||
' }'
|
' }'
|
||||||
' ],'
|
' ],'
|
||||||
' "volumes":'
|
' "volumes":'
|
||||||
' [{"name": "volume1",'
|
' [{"name": "volume1",'
|
||||||
' "cinder": {"volumeID": "fakevolid"}'
|
' "cinder": {"volumeID": '
|
||||||
|
' "3259309d-659c-4e20-b354-ee712e64b3b2"}'
|
||||||
' }]'
|
' }]'
|
||||||
' }, '
|
' }, '
|
||||||
' "metadata": {"labels": {"foo0": "bar0", "foo1": "bar1"},'
|
' "metadata": {"labels": '
|
||||||
|
' {"foo0": "bar0", "foo1": "bar1"},'
|
||||||
' "name": "capsule-example"}'
|
' "name": "capsule-example"}'
|
||||||
' }'
|
' }'
|
||||||
'}')
|
'}')
|
||||||
@ -283,21 +278,20 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
mock_search_volume,
|
mock_search_volume,
|
||||||
mock_ensure_volume_usable,
|
mock_ensure_volume_usable,
|
||||||
mock_create_volume):
|
mock_create_volume):
|
||||||
fake_volume_id1 = 'fakevolid1'
|
fake_volume_id1 = '3259309d-659c-4e20-b354-ee712e64b3b2'
|
||||||
fake_volume = mock.Mock(id=fake_volume_id1)
|
fake_volume = mock.Mock(id=fake_volume_id1)
|
||||||
mock_search_volume.return_value = fake_volume
|
mock_search_volume.return_value = fake_volume
|
||||||
fake_volume_id2 = 'fakevolid2'
|
fake_volume_id2 = 'ef770cfb-349a-483a-97f6-b86e46e344b8'
|
||||||
fake_volume = mock.Mock(id=fake_volume_id2)
|
fake_volume = mock.Mock(id=fake_volume_id2)
|
||||||
mock_create_volume.return_value = fake_volume
|
mock_create_volume.return_value = fake_volume
|
||||||
params = ('{'
|
params = ('{'
|
||||||
'"spec":'
|
'"template":'
|
||||||
'{"kind": "capsule",'
|
'{"kind": "capsule",'
|
||||||
' "spec":'
|
' "spec":'
|
||||||
' {"containers":'
|
' {"containers":'
|
||||||
' [{"environment": {"ROOT_PASSWORD": "foo0"}, '
|
' [{"env": {"ROOT_PASSWORD": "foo0"}, '
|
||||||
' "image": "test", "labels": {"app": "web"}, '
|
' "image": "test", "resources": '
|
||||||
' "image_driver": "docker", "resources": '
|
' {"requests": {"cpu": 1, "memory": 1024}},'
|
||||||
' {"allocation": {"cpu": 1, "memory": 1024}},'
|
|
||||||
' "volumeMounts": [{"name": "volume1", '
|
' "volumeMounts": [{"name": "volume1", '
|
||||||
' "mountPath": "/data1"},'
|
' "mountPath": "/data1"},'
|
||||||
' {"name": "volume2", '
|
' {"name": "volume2", '
|
||||||
@ -306,7 +300,8 @@ class TestCapsuleController(api_base.FunctionalTest):
|
|||||||
' ],'
|
' ],'
|
||||||
' "volumes":'
|
' "volumes":'
|
||||||
' [{"name": "volume1",'
|
' [{"name": "volume1",'
|
||||||
' "cinder": {"volumeID": "fakevolid1"}},'
|
' "cinder": {"volumeID": '
|
||||||
|
' "3259309d-659c-4e20-b354-ee712e64b3b2"}},'
|
||||||
' {"name": "volume2",'
|
' {"name": "volume2",'
|
||||||
' "cinder": {"size": 3, "autoRemove": "True"}'
|
' "cinder": {"size": 3, "autoRemove": "True"}'
|
||||||
' }]'
|
' }]'
|
||||||
|
@ -143,6 +143,12 @@ class TestUtils(base.TestCase):
|
|||||||
params = ({"kind": "capsule", "spec": {}})
|
params = ({"kind": "capsule", "spec": {}})
|
||||||
utils.check_capsule_template(params)
|
utils.check_capsule_template(params)
|
||||||
|
|
||||||
|
params = ({"kind": "capsule", "restartPolicy": "Always", "spec": {
|
||||||
|
"containers": [{"image": "test1"}]
|
||||||
|
}})
|
||||||
|
utils.check_capsule_template(params)
|
||||||
|
self.assertEqual(params["restart_policy"], "always")
|
||||||
|
|
||||||
def test_capsule_get_container_spec(self):
|
def test_capsule_get_container_spec(self):
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
exception.InvalidCapsuleTemplate,
|
exception.InvalidCapsuleTemplate,
|
||||||
@ -163,6 +169,13 @@ class TestUtils(base.TestCase):
|
|||||||
{"environment": {"ROOT_PASSWORD": "foo0"}}]})
|
{"environment": {"ROOT_PASSWORD": "foo0"}}]})
|
||||||
utils.capsule_get_container_spec(params)
|
utils.capsule_get_container_spec(params)
|
||||||
|
|
||||||
|
params = ({"containers": [
|
||||||
|
{"image": "test1", "env": {"ROOT_PASSWORD": "foo0"}}]})
|
||||||
|
utils.capsule_get_container_spec(params)
|
||||||
|
self.assertEqual(params.get("containers")[0].get("environment"),
|
||||||
|
{"ROOT_PASSWORD": "foo0"})
|
||||||
|
self.assertNotIn("env", params.get("containers"))
|
||||||
|
|
||||||
def test_capsule_get_volume_spec(self):
|
def test_capsule_get_volume_spec(self):
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
exception.InvalidCapsuleTemplate,
|
exception.InvalidCapsuleTemplate,
|
||||||
|
@ -37,6 +37,15 @@ CONTAINER_CREATE = {
|
|||||||
'additionalProperties': False,
|
'additionalProperties': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CAPSULE_CREATE = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'template': parameter_types.capsule_template
|
||||||
|
},
|
||||||
|
'required': ['template'],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestSchemaValidations(base.BaseTestCase):
|
class TestSchemaValidations(base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -194,3 +203,213 @@ class TestSchemaValidations(base.BaseTestCase):
|
|||||||
"Invalid input for field"
|
"Invalid input for field"
|
||||||
" 'runtime'"):
|
" 'runtime'"):
|
||||||
self.schema_validator.validate(request_to_validate)
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCapsuleSchemaValidations(base.BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCapsuleSchemaValidations, self).setUp()
|
||||||
|
self.schema_validator = validators.SchemaValidator(CAPSULE_CREATE)
|
||||||
|
|
||||||
|
def test_create_schema_with_all_valid_parameters(self):
|
||||||
|
request_to_validate = \
|
||||||
|
{"template":
|
||||||
|
{"kind": "capsule",
|
||||||
|
"capsuleVersion": "beta",
|
||||||
|
"metadata": {
|
||||||
|
"labels": {"app": "web"},
|
||||||
|
"name": "template"},
|
||||||
|
"restartPolicy": "Always",
|
||||||
|
"spec": {
|
||||||
|
"containers": [
|
||||||
|
{"workDir": "/root", "image": "ubuntu",
|
||||||
|
"volumeMounts": [{"readOnly": True,
|
||||||
|
"mountPath": "/data1",
|
||||||
|
"name": "volume1"}],
|
||||||
|
"command": ["/bin/bash"],
|
||||||
|
"env": {"ENV2": "/usr/bin"},
|
||||||
|
"imagePullPolicy": "ifnotpresent",
|
||||||
|
"ports": [{"containerPort": 80,
|
||||||
|
"protocol": "TCP",
|
||||||
|
"name": "nginx-port",
|
||||||
|
"hostPort": 80}],
|
||||||
|
"resources": {"requests": {"cpu": 1,
|
||||||
|
"memory": 1024}}}],
|
||||||
|
"volumes": [
|
||||||
|
{"cinder": {"autoRemove": True, "size": 5},
|
||||||
|
"name": "volume1"}
|
||||||
|
]}}}
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
|
||||||
|
def test_create_schema_kind_missing(self):
|
||||||
|
request_to_validate = \
|
||||||
|
{"template":
|
||||||
|
{"capsuleVersion": "beta",
|
||||||
|
"metadata": {
|
||||||
|
"labels": {"app": "web"},
|
||||||
|
"name": "template"},
|
||||||
|
"spec": {"containers": [{"image": "test"}]}
|
||||||
|
}}
|
||||||
|
with self.assertRaisesRegex(exception.SchemaValidationError,
|
||||||
|
"'kind' is a required property"):
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
|
||||||
|
def test_create_schema_metadata_missing(self):
|
||||||
|
request_to_validate = \
|
||||||
|
{"template":
|
||||||
|
{"capsuleVersion": "beta",
|
||||||
|
"kind": "capsule",
|
||||||
|
"spec": {"containers": [{"image": "test"}]}
|
||||||
|
}}
|
||||||
|
with self.assertRaisesRegex(exception.SchemaValidationError,
|
||||||
|
"'metadata' is a required property"):
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
|
||||||
|
def test_create_schema_spec_missing(self):
|
||||||
|
request_to_validate = \
|
||||||
|
{"template":
|
||||||
|
{"capsuleVersion": "beta",
|
||||||
|
"kind": "capsule",
|
||||||
|
"metadata": {
|
||||||
|
"labels": {"app": "web"},
|
||||||
|
"name": "template"},
|
||||||
|
}}
|
||||||
|
with self.assertRaisesRegex(exception.SchemaValidationError,
|
||||||
|
"'spec' is a required property"):
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
|
||||||
|
def test_create_schema_with_all_essential_params(self):
|
||||||
|
request_to_validate = \
|
||||||
|
{"template":
|
||||||
|
{"kind": "capsule",
|
||||||
|
"capsuleVersion": "beta",
|
||||||
|
"metadata": {
|
||||||
|
"labels": {},
|
||||||
|
"name": "test-essential"},
|
||||||
|
"spec": {
|
||||||
|
"containers": [
|
||||||
|
{"image": "test"}]
|
||||||
|
}}}
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
|
||||||
|
def test_create_schema_capsule_restart_policy(self):
|
||||||
|
valid_restart_policy = ["Always", "OnFailure", "Never"]
|
||||||
|
invalid_restart_policy = ["always", "4", "onfailure", "never"]
|
||||||
|
for restart_policy in valid_restart_policy:
|
||||||
|
request_to_validate = \
|
||||||
|
{"template":
|
||||||
|
{"capsuleVersion": "beta",
|
||||||
|
"kind": "capsule",
|
||||||
|
"metadata": {
|
||||||
|
"labels": {"app": "web"},
|
||||||
|
"name": "template"},
|
||||||
|
"restartPolicy": restart_policy,
|
||||||
|
"spec": {"containers": [{"image": "test"}]}
|
||||||
|
}}
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
for restart_policy in invalid_restart_policy:
|
||||||
|
request_to_validate = \
|
||||||
|
{"template":
|
||||||
|
{"capsuleVersion": "beta",
|
||||||
|
"kind": "capsule",
|
||||||
|
"metadata": {
|
||||||
|
"labels": {"app": "web"},
|
||||||
|
"name": "template"},
|
||||||
|
"restartPolicy": restart_policy,
|
||||||
|
"spec": {"containers": [{"image": "test"}]}
|
||||||
|
}}
|
||||||
|
with self.assertRaisesRegex(exception.SchemaValidationError,
|
||||||
|
"Invalid input for field "
|
||||||
|
"'restartPolicy'."):
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
|
||||||
|
def test_create_schema_capsule_existed_volume_mounts(self):
|
||||||
|
request_to_validate = {
|
||||||
|
"template": {
|
||||||
|
"kind": "capsule",
|
||||||
|
"metadata": {},
|
||||||
|
"spec": {
|
||||||
|
"containers": [
|
||||||
|
{"image": "test",
|
||||||
|
"volumeMounts": [{
|
||||||
|
"name": "volume1",
|
||||||
|
"mountPath": "/data"}]
|
||||||
|
}],
|
||||||
|
"volumes": [
|
||||||
|
{"name": "volume1",
|
||||||
|
"cinder": {
|
||||||
|
"volumeID":
|
||||||
|
"d2a28af0-e243-4525-adf9-2d091466e43d"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
|
||||||
|
def test_create_schema_capsule_new_volume_mounts(self):
|
||||||
|
request_to_validate = {
|
||||||
|
"template": {
|
||||||
|
"kind": "capsule",
|
||||||
|
"metadata": {},
|
||||||
|
"spec": {
|
||||||
|
"containers": [
|
||||||
|
{"image": "test",
|
||||||
|
"volumeMounts": [{
|
||||||
|
"name": "volume1",
|
||||||
|
"mountPath": "/data"}]
|
||||||
|
}],
|
||||||
|
"volumes": [
|
||||||
|
{"name": "volume1",
|
||||||
|
"cinder": {
|
||||||
|
"size": 5,
|
||||||
|
"autoRemove": True}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
|
||||||
|
def test_create_schema_capsule_volume_no_cinder(self):
|
||||||
|
request_to_validate = {
|
||||||
|
"template": {
|
||||||
|
"kind": "capsule",
|
||||||
|
"metadata": {},
|
||||||
|
"spec": {
|
||||||
|
"containers": [
|
||||||
|
{"image": "test"}],
|
||||||
|
"volumes": [
|
||||||
|
{"name": "volume1",
|
||||||
|
"no-cinder-driver": {
|
||||||
|
"size": 5,
|
||||||
|
"autoRemove": True}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with self.assertRaisesRegex(exception.SchemaValidationError,
|
||||||
|
"'cinder' is a required property"):
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
|
||||||
|
def test_create_schema_capsule_volume_no_name(self):
|
||||||
|
request_to_validate = {
|
||||||
|
"template": {
|
||||||
|
"kind": "capsule",
|
||||||
|
"metadata": {},
|
||||||
|
"spec": {
|
||||||
|
"containers": [
|
||||||
|
{"image": "test"}],
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"cinder": {
|
||||||
|
"size": 5,
|
||||||
|
"autoRemove": True}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with self.assertRaisesRegex(exception.SchemaValidationError,
|
||||||
|
"'name' is a required property"):
|
||||||
|
self.schema_validator.validate(request_to_validate)
|
||||||
|
@ -20,20 +20,26 @@ from zun.db import api as db_api
|
|||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
CAPSULE_SPEC = {"kind": "capsule", "capsule_template_version": "2017-06-21",
|
CAPSULE_SPEC = {"kind": "capsule",
|
||||||
"capsule_version": "beta", "restart_policy": "always",
|
"capsuleVersion": "beta",
|
||||||
|
"restartPolicy": "Always",
|
||||||
"spec": {"containers":
|
"spec": {"containers":
|
||||||
[{"environment": {"MYSQL_ROOT_PASSWORD": "password"},
|
[{"env": {"TEST": "password"},
|
||||||
"image": "mysql", "labels": {"app": "web"},
|
"image": "test",
|
||||||
"image_driver": "docker", "resources":
|
"resources":
|
||||||
{"allocation": {"cpu": 1,
|
{"requests": {"cpu": 1, "memory": 1024}},
|
||||||
"memory": 1024}}}],
|
"volumeMounts": [
|
||||||
"volumes": [{"name": "volume1",
|
{"name": "volume1", "mountPath": "/data1"},
|
||||||
"image": "ubuntu-xenial",
|
{"name": "volume2", "mountPath": "/data2"}]
|
||||||
"drivers": "cinder",
|
}],
|
||||||
"volumeType": "type1",
|
"volumes": [
|
||||||
"driverOptions": "options",
|
{"name": "volume1",
|
||||||
"size": "5GB"}]}}
|
"cinder": {
|
||||||
|
"volumeID":
|
||||||
|
"9600e785-9320-4d3f-ba02-04e3d43fddec"}
|
||||||
|
},
|
||||||
|
{"name": "volume2",
|
||||||
|
"cinder": {"size": 5}}]}}
|
||||||
|
|
||||||
|
|
||||||
def get_test_container(**kwargs):
|
def get_test_container(**kwargs):
|
||||||
|
Loading…
Reference in New Issue
Block a user