Support 'host' on creating container
Implements: blueprint specify-host-in-creation Change-Id: If83de304d8fa609c618030df92c2997fc0731956
This commit is contained in:
parent
c377cf8c17
commit
dc49d557f0
@ -29,7 +29,7 @@
|
||||
test-config:
|
||||
$TEMPEST_CONFIG:
|
||||
container_service:
|
||||
min_microversion: 1.38
|
||||
min_microversion: 1.39
|
||||
post-config:
|
||||
$ZUN_CONF:
|
||||
docker:
|
||||
|
@ -61,6 +61,7 @@ Request
|
||||
- privileged: privileged-request
|
||||
- healthcheck: healthcheck-request
|
||||
- exposed_ports: exposed_ports
|
||||
- host: requested_host
|
||||
|
||||
Request Example
|
||||
----------------
|
||||
|
@ -1294,6 +1294,14 @@ request_id_body:
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
requested_host:
|
||||
description: |
|
||||
The name of the host on which the container is to be created.
|
||||
The API will return 400 if no host are found with the given host name.
|
||||
By default, it can be specified by administrators only.
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
restart_policy:
|
||||
description: |
|
||||
Restart policy to apply when a container exits. It must contain a
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add support for requesting specific host to run a container.
|
||||
By default, host can be specified by administrators only.
|
@ -276,7 +276,7 @@ class ContainersController(base.Controller):
|
||||
container_dict['tty'] = interactive
|
||||
return self._do_post(run, **container_dict)
|
||||
|
||||
@base.Controller.api_version("1.36") # noqa
|
||||
@base.Controller.api_version("1.36", "1.38") # noqa
|
||||
@pecan.expose('json')
|
||||
@api_utils.enforce_content_types(['application/json'])
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@ -285,6 +285,15 @@ class ContainersController(base.Controller):
|
||||
def post(self, run=False, **container_dict):
|
||||
return self._do_post(run, **container_dict)
|
||||
|
||||
@base.Controller.api_version("1.39") # noqa
|
||||
@pecan.expose('json')
|
||||
@api_utils.enforce_content_types(['application/json'])
|
||||
@exception.wrap_pecan_controller_exception
|
||||
@validation.validate_query_param(pecan.request, schema.query_param_create)
|
||||
@validation.validated(schema.container_create_v139)
|
||||
def post(self, run=False, **container_dict):
|
||||
return self._do_post(run, **container_dict)
|
||||
|
||||
def _do_post(self, run=False, **container_dict):
|
||||
"""Create or run a new container.
|
||||
|
||||
@ -403,12 +412,18 @@ class ContainersController(base.Controller):
|
||||
registry = utils.get_registry(registry)
|
||||
container_dict['registry_id'] = registry.id
|
||||
|
||||
requested_host = container_dict.pop('host', None)
|
||||
if requested_host:
|
||||
policy.enforce(context, "container:create:requested_destination",
|
||||
action="container:create:requested_destination")
|
||||
|
||||
container_dict['status'] = consts.CREATING
|
||||
extra_spec = {}
|
||||
extra_spec['hints'] = container_dict.get('hints', None)
|
||||
extra_spec['pci_requests'] = pci_req
|
||||
extra_spec['availability_zone'] = container_dict.get(
|
||||
'availability_zone')
|
||||
extra_spec['requested_host'] = requested_host
|
||||
new_container = objects.Container(context, **container_dict)
|
||||
new_container.create(context)
|
||||
|
||||
|
@ -69,6 +69,10 @@ container_create = {
|
||||
'additionalProperties': False
|
||||
}
|
||||
|
||||
# Add host in container
|
||||
container_create_v139 = copy.deepcopy(container_create)
|
||||
container_create_v139['properties']['host'] = parameter_types.hostname
|
||||
|
||||
query_param_rename = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
|
@ -71,10 +71,11 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
* 1.36 - Add 'tty' to container
|
||||
* 1.37 - Add 'tty' and 'stdin' to capsule
|
||||
* 1.38 - Add 'annotations' to capsule
|
||||
* 1.39 - Support requested host on container creation
|
||||
"""
|
||||
|
||||
BASE_VER = '1.1'
|
||||
CURRENT_MAX_VER = '1.38'
|
||||
CURRENT_MAX_VER = '1.39'
|
||||
|
||||
|
||||
class Version(object):
|
||||
|
@ -293,3 +293,9 @@ user documentation.
|
||||
|
||||
Add 'annotations' to capsule.
|
||||
This field stores metadata of the capsule in key-value format.
|
||||
|
||||
1.39
|
||||
----
|
||||
|
||||
Add 'host' parameter on POST /v1/containers.
|
||||
This field is used to request a host to run the container.
|
||||
|
@ -889,6 +889,10 @@ class ComputeHostNotFound(NotFound):
|
||||
message = _("Compute host %(host)s could not be found.")
|
||||
|
||||
|
||||
class RequestedHostNotFound(NotFound):
|
||||
message = _("Requested host %(host)s could not be found.")
|
||||
|
||||
|
||||
class CNIError(ZunException):
|
||||
pass
|
||||
|
||||
|
@ -52,6 +52,17 @@ rules = [
|
||||
}
|
||||
]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=CONTAINER % 'create:requested_destination',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description=('Create a container on the requested compute host.'),
|
||||
operations=[
|
||||
{
|
||||
'path': '/v1/containers',
|
||||
'method': 'POST'
|
||||
}
|
||||
]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=CONTAINER % 'create:image_pull_policy',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
|
@ -46,6 +46,10 @@ class API(object):
|
||||
def container_create(self, context, new_container, extra_spec,
|
||||
requested_networks, requested_volumes, run,
|
||||
pci_requests=None):
|
||||
requested_host = extra_spec.get('requested_host')
|
||||
if requested_host:
|
||||
self._validate_host(context, new_container, requested_host)
|
||||
|
||||
try:
|
||||
host_state = self._schedule_container(context, new_container,
|
||||
extra_spec)
|
||||
@ -96,6 +100,27 @@ class API(object):
|
||||
requested_networks, requested_volumes,
|
||||
run, pci_requests)
|
||||
|
||||
def _validate_host(self, context, container, host):
|
||||
"""Check whether compute nodes exist by validating the host.
|
||||
If host is supplied, we can lookup the ComputeNode in
|
||||
the API DB.
|
||||
|
||||
:param context: The API request context.
|
||||
:param host: Target host.
|
||||
:raises: exception.RequestedHostNotFound if we find no compute nodes
|
||||
with host and/or hypervisor_hostname.
|
||||
"""
|
||||
|
||||
if host:
|
||||
# When host is specified.
|
||||
try:
|
||||
objects.ComputeNode.get_by_name(context, host)
|
||||
except exception.ComputeNodeNotFound:
|
||||
LOG.info('No compute node record found for host %(host)s.',
|
||||
{'host': host})
|
||||
container.destroy(context)
|
||||
raise exception.RequestedHostNotFound(host=host)
|
||||
|
||||
def _schedule_container(self, context, new_container, extra_spec):
|
||||
dests = self.scheduler_client.select_destinations(context,
|
||||
[new_container],
|
||||
|
@ -72,10 +72,7 @@ class FilterScheduler(driver.Scheduler):
|
||||
hosts = services.keys()
|
||||
nodes = [node for node in nodes if node.hostname in hosts]
|
||||
host_states = self.get_all_host_state(nodes, services)
|
||||
hosts = self.filter_handler.get_filtered_objects(self.enabled_filters,
|
||||
host_states,
|
||||
container,
|
||||
extra_specs)
|
||||
hosts = self._get_filtered_hosts(host_states, container, extra_specs)
|
||||
if not hosts:
|
||||
msg = _("Is the appropriate service running?")
|
||||
raise exception.NoValidHost(reason=msg)
|
||||
@ -122,6 +119,34 @@ class FilterScheduler(driver.Scheduler):
|
||||
|
||||
return claimed_host
|
||||
|
||||
def _get_filtered_hosts(self, hosts, container, extra_specs):
|
||||
"""Filter hosts and return only ones passing all filters."""
|
||||
|
||||
def _get_hosts_matching_request(hosts, requested_host):
|
||||
matched_hosts = [x for x in hosts
|
||||
if x.hostname == requested_host]
|
||||
if matched_hosts:
|
||||
LOG.info('Host filter only checking host %(host)s',
|
||||
{'host': requested_host})
|
||||
else:
|
||||
# NOTE(hongbin): The API level should prevent the user from
|
||||
# providing a wrong requested host but let's make sure a wrong
|
||||
# destination doesn't trample the scheduler still.
|
||||
LOG.info('No hosts matched due to not matching requested '
|
||||
'destination (%(host)s)', {'host': requested_host})
|
||||
return iter(matched_hosts)
|
||||
|
||||
requested_host = extra_specs.get('requested_host', [])
|
||||
|
||||
if requested_host:
|
||||
# NOTE(hongbin): Reduce a potentially long set of hosts as much as
|
||||
# possible to any requested destination nodes before passing the
|
||||
# list to the filters
|
||||
hosts = _get_hosts_matching_request(hosts, requested_host)
|
||||
|
||||
return self.filter_handler.get_filtered_objects(
|
||||
self.enabled_filters, hosts, container, extra_specs)
|
||||
|
||||
def select_destinations(self, context, containers, extra_specs,
|
||||
alloc_reqs_by_rp_uuid, provider_summaries,
|
||||
allocation_request_version=None):
|
||||
|
@ -22,6 +22,7 @@ import os_resource_classes as orc
|
||||
from oslo_log import log as logging
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from zun.common import exception
|
||||
import zun.conf
|
||||
from zun import objects
|
||||
|
||||
@ -285,7 +286,7 @@ class ResourceRequest(object):
|
||||
def resources_from_request_spec(ctxt, container_obj, extra_specs):
|
||||
"""Given a Container object, returns a ResourceRequest of the resources,
|
||||
traits, and aggregates it represents.
|
||||
:param context: The request context.
|
||||
:param ctxt: The request context.
|
||||
:param container_obj: A Container object.
|
||||
:return: A ResourceRequest object.
|
||||
:raises NoValidHost: If the specified host/node is not found in the DB.
|
||||
@ -341,6 +342,25 @@ def resources_from_request_spec(ctxt, container_obj, extra_specs):
|
||||
for group in requested_resources:
|
||||
res_req.add_request_group(group)
|
||||
|
||||
target_host = extra_specs.get('requested_host')
|
||||
if target_host:
|
||||
nodes = objects.ComputeNode.list(
|
||||
ctxt, filters={'hostname': target_host})
|
||||
if not nodes:
|
||||
reason = (_('No such host - host: %(host)s ') %
|
||||
{'host': target_host})
|
||||
raise exception.NoValidHost(reason=reason)
|
||||
if len(nodes) == 1:
|
||||
grp = res_req.get_request_group(None)
|
||||
grp.in_tree = nodes[0].rp_uuid
|
||||
else:
|
||||
# Multiple nodes are found when a target host is specified
|
||||
# without a specific node. Since placement doesn't support
|
||||
# multiple uuids in the `in_tree` queryparam, what we can do here
|
||||
# is to remove the limit from the `GET /a_c` query to prevent
|
||||
# the found nodes from being filtered out in placement.
|
||||
res_req._limit = None
|
||||
|
||||
# Don't limit allocation candidates when using affinity/anti-affinity.
|
||||
if (extra_specs.get('hints') and any(
|
||||
key in ['group', 'same_host', 'different_host']
|
||||
|
@ -28,7 +28,7 @@ from zun.tests.unit.db import base
|
||||
|
||||
|
||||
PATH_PREFIX = '/v1'
|
||||
CURRENT_VERSION = "container 1.38"
|
||||
CURRENT_VERSION = "container 1.39"
|
||||
|
||||
|
||||
class FunctionalTest(base.DbTestCase):
|
||||
|
@ -28,7 +28,7 @@ class TestRootController(api_base.FunctionalTest):
|
||||
'default_version':
|
||||
{'id': 'v1',
|
||||
'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}],
|
||||
'max_version': '1.38',
|
||||
'max_version': '1.39',
|
||||
'min_version': '1.1',
|
||||
'status': 'CURRENT'},
|
||||
'description': 'Zun is an OpenStack project which '
|
||||
@ -37,7 +37,7 @@ class TestRootController(api_base.FunctionalTest):
|
||||
'versions': [{'id': 'v1',
|
||||
'links': [{'href': 'http://localhost/v1/',
|
||||
'rel': 'self'}],
|
||||
'max_version': '1.38',
|
||||
'max_version': '1.39',
|
||||
'min_version': '1.1',
|
||||
'status': 'CURRENT'}]}
|
||||
|
||||
|
@ -59,7 +59,7 @@ class TestAPI(base.TestCase):
|
||||
'limits': {}}
|
||||
mock_image_search.return_value = [image_meta]
|
||||
self.compute_api.container_create(self.context, container,
|
||||
None, None, None, False)
|
||||
{}, None, None, False)
|
||||
self.assertTrue(mock_schedule_container.called)
|
||||
self.assertTrue(mock_image_search.called)
|
||||
self.assertTrue(mock_container_create.called)
|
||||
@ -81,7 +81,7 @@ class TestAPI(base.TestCase):
|
||||
mock_image_search.side_effect = exception.OperationNotSupported
|
||||
|
||||
self.compute_api.container_create(self.context, container,
|
||||
None, None, None, False)
|
||||
{}, None, None, False)
|
||||
self.assertTrue(mock_schedule_container.called)
|
||||
self.assertTrue(mock_image_search.called)
|
||||
self.assertTrue(mock_container_create.called)
|
||||
@ -95,7 +95,7 @@ class TestAPI(base.TestCase):
|
||||
mock_schedule_container.side_effect = exception.NoValidHost(
|
||||
reason='not enough host')
|
||||
self.compute_api.container_create(self.context, container,
|
||||
None, None, None, False)
|
||||
{}, None, None, False)
|
||||
self.assertTrue(mock_schedule_container.called)
|
||||
self.assertTrue(mock_save.called)
|
||||
self.assertEqual(consts.ERROR, container.status)
|
||||
@ -118,7 +118,7 @@ class TestAPI(base.TestCase):
|
||||
mock_image_search.side_effect = exception.ZunException
|
||||
|
||||
self.compute_api.container_create(
|
||||
self.context, container, None, None, None, False)
|
||||
self.context, container, {}, None, None, False)
|
||||
self.assertTrue(mock_schedule_container.called)
|
||||
self.assertTrue(mock_image_search.called)
|
||||
self.assertFalse(mock_save.called)
|
||||
|
Loading…
x
Reference in New Issue
Block a user