diff --git a/zun/api/controllers/v1/__init__.py b/zun/api/controllers/v1/__init__.py index a950201f2..f495fd746 100644 --- a/zun/api/controllers/v1/__init__.py +++ b/zun/api/controllers/v1/__init__.py @@ -23,6 +23,7 @@ import pecan from zun.api.controllers import base as controllers_base from zun.api.controllers import link +from zun.api.controllers.v1 import availability_zone as a_zone from zun.api.controllers.v1 import capsules as capsule_controller from zun.api.controllers.v1 import containers as container_controller from zun.api.controllers.v1 import hosts as host_controller @@ -67,7 +68,8 @@ class V1(controllers_base.APIBase): 'containers', 'images', 'hosts', - 'capsules' + 'capsules', + 'availability_zones' ) @staticmethod @@ -107,6 +109,12 @@ class V1(controllers_base.APIBase): pecan.request.host_url, 'hosts', '', bookmark=True)] + v1.availability_zones = [link.make_link('self', pecan.request.host_url, + 'availability_zones', ''), + link.make_link('bookmark', + pecan.request.host_url, + 'availability_zones', '', + bookmark=True)] v1.capsules = [link.make_link('self', pecan.request.host_url, 'capsules', ''), link.make_link('bookmark', @@ -123,6 +131,7 @@ class Controller(controllers_base.Controller): containers = container_controller.ContainersController() images = image_controller.ImagesController() hosts = host_controller.HostController() + availability_zones = a_zone.AvailabilityZoneController() capsules = capsule_controller.CapsuleController() @pecan.expose('json') @@ -180,4 +189,5 @@ class Controller(controllers_base.Controller): return super(Controller, self)._route(args) + __all__ = (Controller) diff --git a/zun/api/controllers/v1/availability_zone.py b/zun/api/controllers/v1/availability_zone.py new file mode 100644 index 000000000..c8c92cdc5 --- /dev/null +++ b/zun/api/controllers/v1/availability_zone.py @@ -0,0 +1,99 @@ +# Copyright (c) 2018 NEC, Corp. +# +# 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. + +import pecan + +from zun.api.controllers import base +from zun.api.controllers.v1 import collection +from zun.api.controllers.v1.views import availability_zone_view as view +from zun.api import utils as api_utils +from zun.common import exception +from zun.common import policy +import zun.conf +from zun import objects + + +CONF = zun.conf.CONF + + +def check_policy_on_availability_zones(availability_zone, action): + context = pecan.request.context + policy.enforce(context, action, availability_zone, action=action) + + +class AvailabilityZoneCollection(collection.Collection): + """API representation of a collection of availability zones.""" + + fields = { + 'availability_zones', + 'next' + } + + """A list containing availability zone objects""" + + def __init__(self, **kwargs): + super(AvailabilityZoneCollection, self).__init__(**kwargs) + self._type = 'availability_zones' + + @staticmethod + def convert_with_links(zones, limit, url=None, + expand=False, **kwargs): + collection = AvailabilityZoneCollection() + collection.availability_zones = [ + view.format_a_zone(url, p) for p in zones] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +class AvailabilityZoneController(base.Controller): + """Availability Zone info controller""" + + @pecan.expose('json') + @exception.wrap_pecan_controller_exception + def get_all(self, **kwargs): + """Retrieve a list of availability zones""" + + context = pecan.request.context + context.all_projects = True + + policy.enforce(context, "availability_zones:get_all", + action="availability_zones:get_all") + return self._get_host_collection(**kwargs) + + def _get_host_collection(self, **kwargs): + context = pecan.request.context + limit = api_utils.validate_limit(kwargs.get('limit')) + + sort_dir = api_utils.validate_sort_dir(kwargs.get('sort_dir', 'asc')) + sort_key = kwargs.get('sort_key', 'availability_zone') + expand = kwargs.get('expand') + marker_obj = None + resource_url = kwargs.get('resource_url') + marker = kwargs.get('marker') + if marker: + marker_obj = objects.ZunService.get_by_uuid(context, marker) + services = objects.ZunService.list(context, + limit, + marker_obj, + sort_key, + sort_dir) + zones = {} + for service in services: + zones[service.availability_zone] = service + return AvailabilityZoneCollection.convert_with_links(zones.values(), + limit, + url=resource_url, + expand=expand, + sort_key=sort_key, + sort_dir=sort_dir) diff --git a/zun/api/controllers/v1/views/availability_zone_view.py b/zun/api/controllers/v1/views/availability_zone_view.py new file mode 100644 index 000000000..f3d808524 --- /dev/null +++ b/zun/api/controllers/v1/views/availability_zone_view.py @@ -0,0 +1,41 @@ +# +# 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. + +import itertools + +from zun.api.controllers import link + + +_basic_keys = ( + 'availability_zone', + ) + + +def format_a_zone(url, a_zone): + def transform(key, value): + if key not in _basic_keys: + return + if key == 'id': + yield ('id', value) + yield ('links', [link.make_link( + 'self', url, 'availability_zones', value), + link.make_link( + 'bookmark', url, + 'availability_zones', value, + bookmark=True)]) + else: + yield (key, value) + + return dict( + itertools.chain.from_iterable( + transform(k, v)for k, v in a_zone.as_dict().items())) diff --git a/zun/common/policies/__init__.py b/zun/common/policies/__init__.py index 80ed2da7b..16df50bbf 100644 --- a/zun/common/policies/__init__.py +++ b/zun/common/policies/__init__.py @@ -12,6 +12,7 @@ import itertools +from zun.common.policies import availability_zone from zun.common.policies import base from zun.common.policies import capsule from zun.common.policies import container @@ -31,5 +32,6 @@ def list_rules(): host.list_rules(), capsule.list_rules(), network.list_rules(), - container_action.list_rules() + container_action.list_rules(), + availability_zone.list_rules() ) diff --git a/zun/common/policies/availability_zone.py b/zun/common/policies/availability_zone.py new file mode 100644 index 000000000..d16190730 --- /dev/null +++ b/zun/common/policies/availability_zone.py @@ -0,0 +1,39 @@ +# Copyright 2018 NEC, Corp. +# All Rights Reserved. +# +# 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_policy import policy + +from zun.common.policies import base + +AVAILABILITY_ZONE = 'availability_zones:%s' + + +rules = [ + policy.DocumentedRuleDefault( + name=AVAILABILITY_ZONE % 'get_all', + check_str=base.RULE_ADMIN_OR_OWNER, + description='List availability zone', + operations=[ + { + 'path': '/v1/availability_zones', + 'method': 'GET' + } + ] + ) +] + + +def list_rules(): + return rules diff --git a/zun/tests/unit/api/controllers/test_root.py b/zun/tests/unit/api/controllers/test_root.py index 576f56222..852d18aee 100644 --- a/zun/tests/unit/api/controllers/test_root.py +++ b/zun/tests/unit/api/controllers/test_root.py @@ -64,6 +64,11 @@ class TestRootController(api_base.FunctionalTest): 'rel': 'self'}, {'href': 'http://localhost/hosts/', 'rel': 'bookmark'}], + 'availability_zones': [ + {'href': 'http://localhost/v1/availability_zones/', + 'rel': 'self'}, + {'href': 'http://localhost/availability_zones/', + 'rel': 'bookmark'}], 'images': [{'href': 'http://localhost/v1/images/', 'rel': 'self'}, {'href': 'http://localhost/images/', diff --git a/zun/tests/unit/api/controllers/v1/test_availability_zones.py b/zun/tests/unit/api/controllers/v1/test_availability_zones.py new file mode 100644 index 000000000..06dc0c54c --- /dev/null +++ b/zun/tests/unit/api/controllers/v1/test_availability_zones.py @@ -0,0 +1,58 @@ +# 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. + +import mock +from mock import patch + +from zun import objects +from zun.tests.unit.api import base as api_base +from zun.tests.unit.db import utils + + +class TestAvailabilityZoneController(api_base.FunctionalTest): + + @mock.patch('zun.common.policy.enforce') + @patch('zun.objects.ZunService.list') + def test_get_all_availability_zones(self, + mock_availability_zone_list, + mock_policy): + mock_policy.return_value = True + test_a_zone = utils.get_test_zun_service() + availability_zones = [objects.ZunService(self.context, **test_a_zone)] + mock_availability_zone_list.return_value = availability_zones + + response = self.get('/v1/availability_zones') + + mock_availability_zone_list.assert_called_once_with( + mock.ANY, 1000, None, 'availability_zone', 'asc') + self.assertEqual(200, response.status_int) + actual_a_zones = response.json['availability_zones'] + self.assertEqual(1, len(actual_a_zones)) + self.assertEqual(test_a_zone['availability_zone'], + actual_a_zones[0].get('availability_zone')) + + +class TestAvailabilityZonetEnforcement(api_base.FunctionalTest): + + def _common_policy_check(self, rule, func, *arg, **kwarg): + self.policy.set_rules({rule: 'project_id:non_fake'}) + response = func(*arg, **kwarg) + self.assertEqual(403, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertTrue( + "Policy doesn't allow %s to be performed." % rule, + response.json['errors'][0]['detail']) + + def test_policy_disallow_get_all(self): + self._common_policy_check( + 'availability_zones:get_all', self.get_json, '/availability_zones', + expect_errors=True)