Support running containers in specified AZ

Add a new parameter 'availaibility_zone' to the REST API for
creating containers. The API server will pass down this parameter
the the scheduler as extra_spec. In the filter scheduler, add
a 'AvailabilityZoneFilter' filter for selecting hosts in the
requested availability zone. Below is the host selection scenario:

* If users specify an AZ for the container, scheduler will choose
  a host in the user-specified AZ.
* If users don't specify an AZ for the container, the AZ will be
  default to CONF.default_schedule_zone if this config is set.
* If users don't specify an AZ for the container and
  CONF.default_schedule_zone is not set (None), the scheduler will
  choose a host in arbitrary AZ.

Partial-Implements: blueprint zun-availability-zone
Change-Id: I01b8069f76e87a8e0c782da0888eee8a8cd58193
This commit is contained in:
Hongbin Lu 2018-02-11 20:26:54 +00:00
parent df66e4cf58
commit 31076dc1b2
8 changed files with 174 additions and 0 deletions

View File

@ -282,6 +282,8 @@ class ContainersController(base.Controller):
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')
new_container = objects.Container(context, **container_dict)
new_container.create(context)

View File

@ -35,6 +35,7 @@ _container_properties = {
'runtime': parameter_types.runtime,
'hostname': parameter_types.hostname,
'disk': parameter_types.positive_integer,
'availability_zone': parameter_types.availability_zone,
}
container_create = {

View File

@ -113,6 +113,12 @@ nets = {
'type': ['array', 'null']
}
availability_zone = {
'type': ['string', 'null'],
'minLength': 1,
'maxLength': 255,
}
mounts = {
'type': ['array', 'null'],
'items': {

View File

@ -24,6 +24,20 @@ services.
Possible values:
* Any string representing an existing availability zone name.
"""),
cfg.StrOpt('default_schedule_zone',
help="""
Default availability zone for containers.
This option determines the default availability zone for containers, which will
be used when a user does not specify one when creating a container. The
container(s) will be bound to this availability zone for their lifetime.
Possible values:
* Any string representing an existing availability zone name.
* None, which means that the container can move from one availability zone to
another during its lifetime if it is moved from one compute node to another.
"""),
]

View File

@ -63,6 +63,7 @@ Related options:
"""),
cfg.ListOpt("enabled_filters",
default=[
"AvailabilityZoneFilter",
"CPUFilter",
"RamFilter",
"ComputeFilter"

View File

@ -0,0 +1,47 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
import zun.conf
from zun.scheduler import filters
LOG = logging.getLogger(__name__)
CONF = zun.conf.CONF
class AvailabilityZoneFilter(filters.BaseHostFilter):
"""Filters Hosts by availability zone."""
# Availability zones do not change within a request
run_filter_once_per_request = True
def host_passes(self, host_state, container, extra_spec):
availability_zone = extra_spec.get('availability_zone') or \
CONF.default_schedule_zone
if not availability_zone:
return True
host_az = host_state.service.availability_zone
if not host_az:
host_az = CONF.default_availability_zone
hosts_passes = availability_zone == host_az
if not hosts_passes:
LOG.debug("Availability Zone '%(az)s' requested. "
"%(host_state)s has AZs: %(host_az)s",
{'host_state': host_state,
'az': availability_zone,
'host_az': host_az})
return hosts_passes

View File

@ -401,6 +401,37 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual(1, len(requested_networks))
self.assertEqual(fake_network['id'], requested_networks[0]['network'])
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.image_search')
def test_create_container_with_availability_zone(
self, mock_search, mock_container_create,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, **z: y
fake_network = {'id': 'foo'}
mock_neutron_get_network.return_value = fake_network
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
'"command": "env",'
'"availability_zone": "test-az"}')
response = self.post('/v1/containers/',
params=params,
content_type='application/json')
self.assertEqual(202, response.status_int)
response = self.get('/v1/containers/')
self.assertEqual(200, response.status_int)
self.assertEqual(2, len(response.json))
c = response.json['containers'][0]
self.assertIsNotNone(c.get('uuid'))
self.assertEqual('MyDocker', c.get('name'))
self.assertEqual('env', c.get('command'))
self.assertIsNone(c.get('memory'))
self.assertEqual({}, c.get('environment'))
mock_neutron_get_network.assert_called_once()
extra_spec = \
mock_container_create.call_args[1]['extra_spec']
self.assertEqual('test-az', extra_spec['availability_zone'])
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.image_search')

View File

@ -0,0 +1,72 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from zun.common import context
from zun import objects
from zun.scheduler.filters import availability_zone_filter as az_filter
from zun.tests import base
from zun.tests.unit.scheduler import fakes
class TestAvailabilityZoneFilter(base.TestCase):
def setUp(self):
super(TestAvailabilityZoneFilter, self).setUp()
self.context = context.RequestContext('fake_user', 'fake_project')
def test_az_filter(self):
self.assertIs(True,
self._test_az_filter(request_az='test-az',
node_az='test-az'))
self.assertIs(False,
self._test_az_filter(request_az='test-az',
node_az='another-az'))
def test_az_filter_default_az(self):
cfg.CONF.set_override("default_availability_zone", "default-az")
self.assertIs(True,
self._test_az_filter(request_az='default-az',
node_az=None))
self.assertIs(False,
self._test_az_filter(request_az='another-az',
node_az=None))
def test_az_filter_default_schedule_az(self):
cfg.CONF.set_override("default_schedule_zone", "schedule-az")
self.assertIs(True,
self._test_az_filter(request_az=None,
node_az='schedule-az'))
self.assertIs(False,
self._test_az_filter(request_az=None,
node_az='another-az'))
def test_az_filter_no_az_requested(self):
self.assertIs(True,
self._test_az_filter(request_az=None,
node_az=None))
self.assertIs(True,
self._test_az_filter(request_az=None,
node_az='any-az'))
def _test_az_filter(self, request_az, node_az):
filt_cls = az_filter.AvailabilityZoneFilter()
container = objects.Container(self.context)
service = objects.ZunService(self.context)
service.availability_zone = node_az
extra_spec = {}
if request_az:
extra_spec = {'availability_zone': request_az}
host = fakes.FakeHostState('fake-host',
{'service': service})
return filt_cls.host_passes(host, container, extra_spec)