Support 'host' on creating container

Implements: blueprint specify-host-in-creation
Change-Id: If83de304d8fa609c618030df92c2997fc0731956
This commit is contained in:
Hongbin Lu 2020-03-09 00:07:04 +00:00
parent c377cf8c17
commit dc49d557f0
16 changed files with 140 additions and 15 deletions

View File

@ -29,7 +29,7 @@
test-config:
$TEMPEST_CONFIG:
container_service:
min_microversion: 1.38
min_microversion: 1.39
post-config:
$ZUN_CONF:
docker:

View File

@ -61,6 +61,7 @@ Request
- privileged: privileged-request
- healthcheck: healthcheck-request
- exposed_ports: exposed_ports
- host: requested_host
Request Example
----------------

View File

@ -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

View File

@ -0,0 +1,5 @@
---
features:
- |
Add support for requesting specific host to run a container.
By default, host can be specified by administrators only.

View File

@ -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)

View File

@ -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': {

View File

@ -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):

View File

@ -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.

View File

@ -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

View File

@ -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,

View File

@ -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],

View File

@ -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):

View File

@ -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']

View File

@ -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):

View File

@ -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'}]}

View File

@ -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)