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:
parent
df66e4cf58
commit
31076dc1b2
@ -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)
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -113,6 +113,12 @@ nets = {
|
||||
'type': ['array', 'null']
|
||||
}
|
||||
|
||||
availability_zone = {
|
||||
'type': ['string', 'null'],
|
||||
'minLength': 1,
|
||||
'maxLength': 255,
|
||||
}
|
||||
|
||||
mounts = {
|
||||
'type': ['array', 'null'],
|
||||
'items': {
|
||||
|
@ -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.
|
||||
"""),
|
||||
]
|
||||
|
||||
|
@ -63,6 +63,7 @@ Related options:
|
||||
"""),
|
||||
cfg.ListOpt("enabled_filters",
|
||||
default=[
|
||||
"AvailabilityZoneFilter",
|
||||
"CPUFilter",
|
||||
"RamFilter",
|
||||
"ComputeFilter"
|
||||
|
47
zun/scheduler/filters/availability_zone_filter.py
Normal file
47
zun/scheduler/filters/availability_zone_filter.py
Normal 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
|
@ -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')
|
||||
|
@ -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)
|
Loading…
Reference in New Issue
Block a user