diff --git a/api-ref/source/hosts.inc b/api-ref/source/hosts.inc new file mode 100644 index 000000000..c4ecafa99 --- /dev/null +++ b/api-ref/source/hosts.inc @@ -0,0 +1,45 @@ +.. -*- rst -*- + +================== +Manage zun host +================== + +List all compute hosts +======================================== + +.. rest_method:: GET /v1/hosts + +Enables administrative users to list all Zun container hosts. + +Response Codes +-------------- + +.. rest_status_code:: success status.yaml + + - 200 + +.. rest_status_code:: error status.yaml + + - 400 + - 401 + - 403 + +Response Parameters +------------------- + +.. rest_parameters:: parameters.yaml + + - X-Openstack-Request-Id: request_id + - hosts: host_list + - uuid: uuid + - hostname: hostname + - mem_total: mem_total + - cpus: cpus + - os: os + - labels: labels + +Response Example +---------------- + +.. literalinclude:: samples/host-get-all-resp.json + :language: javascript diff --git a/api-ref/source/index.rst b/api-ref/source/index.rst index ee5e6e2cd..42af96d65 100644 --- a/api-ref/source/index.rst +++ b/api-ref/source/index.rst @@ -10,3 +10,4 @@ .. include:: containers.inc .. include:: images.inc .. include:: services.inc +.. include:: hosts.inc diff --git a/api-ref/source/samples/host-get-all-resp.json b/api-ref/source/samples/host-get-all-resp.json new file mode 100644 index 000000000..3a589183e --- /dev/null +++ b/api-ref/source/samples/host-get-all-resp.json @@ -0,0 +1,23 @@ +{ + "hosts": [ + { + "hostname": "testhost", + "uuid": "d0405f06-101e-4340-8735-d1bf9fa8b8ad", + "links": [{ + "href": "http://192.168.2.200:9517/v1/hosts/d0405f06-101e-4340-8735-d1bf9fa8b8ad", + "rel": "self" + }, + {"href": "http://192.168.2.200:9517/hosts/d0405f06-101e-4340-8735-d1bf9fa8b8ad", + "rel": "bookmark" + } + ], + "labels": { + "type": "test" + }, + "cpus": 48, + "mem_total": 128446, + "os": "CentOS Linux 7 (Core)" + } + ], + "next": null +} diff --git a/etc/zun/policy.json b/etc/zun/policy.json index 9e708378a..c24e1d842 100644 --- a/etc/zun/policy.json +++ b/etc/zun/policy.json @@ -38,5 +38,7 @@ "zun-service:disable": "rule:admin_api", "zun-service:enable": "rule:admin_api", "zun-service:force_down": "rule:admin_api", - "zun-service:get_all": "rule:admin_api" + "zun-service:get_all": "rule:admin_api", + + "host:get_all": "rule:admin_api" } diff --git a/zun/api/controllers/v1/__init__.py b/zun/api/controllers/v1/__init__.py index 246ef4550..2509d959f 100644 --- a/zun/api/controllers/v1/__init__.py +++ b/zun/api/controllers/v1/__init__.py @@ -24,6 +24,7 @@ import pecan from zun.api.controllers import base as controllers_base from zun.api.controllers import link from zun.api.controllers.v1 import containers as container_controller +from zun.api.controllers.v1 import hosts as host_controller from zun.api.controllers.v1 import images as image_controller from zun.api.controllers.v1 import zun_services from zun.api.controllers import versions as ver @@ -63,7 +64,8 @@ class V1(controllers_base.APIBase): 'links', 'services', 'containers', - 'images' + 'images', + 'hosts' ) @staticmethod @@ -97,6 +99,12 @@ class V1(controllers_base.APIBase): pecan.request.host_url, 'images', '', bookmark=True)] + v1.hosts = [link.make_link('self', pecan.request.host_url, + 'hosts', ''), + link.make_link('bookmark', + pecan.request.host_url, + 'hosts', '', + bookmark=True)] return v1 @@ -106,6 +114,7 @@ class Controller(controllers_base.Controller): services = zun_services.ZunServiceController() containers = container_controller.ContainersController() images = image_controller.ImagesController() + hosts = host_controller.HostController() @pecan.expose('json') def get(self): diff --git a/zun/api/controllers/v1/hosts.py b/zun/api/controllers/v1/hosts.py new file mode 100644 index 000000000..6551d5200 --- /dev/null +++ b/zun/api/controllers/v1/hosts.py @@ -0,0 +1,85 @@ +# 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 pecan + +from zun.api.controllers import base +from zun.api.controllers.v1 import collection +from zun.api.controllers.v1.views import hosts_view as view +from zun.api import utils as api_utils +from zun.common import exception +from zun.common import policy +from zun import objects + +LOG = logging.getLogger(__name__) + + +class HostCollection(collection.Collection): + """API representation of a collection of hosts.""" + + fields = { + 'hosts', + 'next' + } + + """A list containing compute node objects""" + + def __init__(self, **kwargs): + super(HostCollection, self).__init__(**kwargs) + self._type = 'hosts' + + @staticmethod + def convert_with_links(nodes, limit, url=None, + expand=False, **kwargs): + collection = HostCollection() + collection.hosts = [view.format_host(url, p) for p in nodes] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +class HostController(base.Controller): + """Host info controller""" + + @pecan.expose('json') + @base.Controller.api_version("1.3") + @exception.wrap_pecan_controller_exception + def get_all(self, **kwargs): + """Retrieve a list of hosts""" + context = pecan.request.context + policy.enforce(context, "host:get_all", + action="host: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', 'hostname') + expand = kwargs.get('expand') + filters = None + marker_obj = None + resource_url = kwargs.get('resource_url') + marker = kwargs.get('marker') + if marker: + marker_obj = objects.ComputeNode.get_by_uuid(context, marker) + nodes = objects.ComputeNode.list(context, + limit, + marker_obj, + sort_key, + sort_dir, + filters=filters) + return HostCollection.convert_with_links(nodes, limit, + url=resource_url, + expand=expand, + sort_key=sort_key, + sort_dir=sort_dir) diff --git a/zun/api/controllers/v1/views/hosts_view.py b/zun/api/controllers/v1/views/hosts_view.py new file mode 100644 index 000000000..36032bd40 --- /dev/null +++ b/zun/api/controllers/v1/views/hosts_view.py @@ -0,0 +1,45 @@ +# +# 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 = ( + 'uuid', + 'hostname', + 'mem_total', + 'cpus', + 'os', + 'labels' +) + + +def format_host(url, host): + def transform(key, value): + if key not in _basic_keys: + return + if key == 'uuid': + yield ('uuid', value) + yield ('links', [link.make_link( + 'self', url, 'hosts', value), + link.make_link( + 'bookmark', url, + 'hosts', value, + bookmark=True)]) + else: + yield (key, value) + + return dict(itertools.chain.from_iterable( + transform(k, v) for k, v in host.as_dict().items())) diff --git a/zun/api/controllers/versions.py b/zun/api/controllers/versions.py index 3d34ccdca..4f06a7c37 100644 --- a/zun/api/controllers/versions.py +++ b/zun/api/controllers/versions.py @@ -36,10 +36,11 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 1.1 - Initial version * 1.2 - Support user specify pre created networks * 1.3 - Add auto_remove to container + * 1.4 - Support list all container host """ BASE_VER = '1.1' -CURRENT_MAX_VER = '1.3' +CURRENT_MAX_VER = '1.4' class Version(object): diff --git a/zun/api/rest_api_version_history.rst b/zun/api/rest_api_version_history.rst index 87a23630b..045ddcc75 100644 --- a/zun/api/rest_api_version_history.rst +++ b/zun/api/rest_api_version_history.rst @@ -42,3 +42,9 @@ user documentation. Add 'auto_remove' field for creating a container. With this field, the container will be automatically removed if it exists. The new one will be created instead. + +1.4 +--- + + Add host list api. + Users can use this api to list all the zun compute hosts. diff --git a/zun/tests/unit/api/controllers/test_root.py b/zun/tests/unit/api/controllers/test_root.py index fbcc4230f..ef260a7c5 100644 --- a/zun/tests/unit/api/controllers/test_root.py +++ b/zun/tests/unit/api/controllers/test_root.py @@ -25,7 +25,7 @@ class TestRootController(api_base.FunctionalTest): u'default_version': {u'id': u'v1', u'links': [{u'href': u'http://localhost/v1/', u'rel': u'self'}], - u'max_version': u'1.3', + u'max_version': u'1.4', u'min_version': u'1.1', u'status': u'CURRENT'}, u'description': u'Zun is an OpenStack project which ' @@ -33,7 +33,7 @@ class TestRootController(api_base.FunctionalTest): u'versions': [{u'id': u'v1', u'links': [{u'href': u'http://localhost/v1/', u'rel': u'self'}], - u'max_version': u'1.3', + u'max_version': u'1.4', u'min_version': u'1.1', u'status': u'CURRENT'}]} @@ -56,6 +56,10 @@ class TestRootController(api_base.FunctionalTest): u'rel': u'self'}, {u'href': u'http://localhost/containers/', u'rel': u'bookmark'}], + u'hosts': [{u'href': u'http://localhost/v1/hosts/', + u'rel': u'self'}, + {u'href': u'http://localhost/hosts/', + u'rel': u'bookmark'}], u'images': [{u'href': u'http://localhost/v1/images/', u'rel': u'self'}, {u'href': u'http://localhost/images/', diff --git a/zun/tests/unit/api/controllers/v1/test_hosts.py b/zun/tests/unit/api/controllers/v1/test_hosts.py new file mode 100644 index 000000000..68963b780 --- /dev/null +++ b/zun/tests/unit/api/controllers/v1/test_hosts.py @@ -0,0 +1,89 @@ +# 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 oslo_utils import uuidutils + +from zun import objects +from zun.objects import numa +from zun.tests.unit.api import base as api_base +from zun.tests.unit.db import utils + + +class TestHostController(api_base.FunctionalTest): + + @patch('zun.objects.ComputeNode.list') + def test_get_all_hosts(self, mock_host_list): + test_host = utils.get_test_compute_node() + numat = numa.NUMATopology._from_dict(test_host['numa_topology']) + test_host['numa_topology'] = numat + hosts = [objects.ComputeNode(self.context, **test_host)] + mock_host_list.return_value = hosts + + extra_environ = {'HTTP_ACCEPT': 'application/json'} + headers = {'OpenStack-API-Version': 'container 1.3'} + response = self.app.get('/v1/hosts/', extra_environ=extra_environ, + headers=headers) + + mock_host_list.assert_called_once_with(mock.ANY, + 1000, None, 'hostname', 'asc', + filters=None) + self.assertEqual(200, response.status_int) + actual_hosts = response.json['hosts'] + self.assertEqual(1, len(actual_hosts)) + self.assertEqual(test_host['uuid'], + actual_hosts[0].get('uuid')) + + @patch('zun.objects.ComputeNode.list') + def test_get_all_hosts_with_pagination_marker(self, mock_host_list): + host_list = [] + for id_ in range(4): + test_host = utils.create_test_compute_node( + context=self.context, + uuid=uuidutils.generate_uuid()) + numat = numa.NUMATopology._from_dict(test_host['numa_topology']) + test_host['numa_topology'] = numat + host = objects.ComputeNode(self.context, **test_host) + host_list.append(host) + mock_host_list.return_value = host_list[-1:] + extra_environ = {'HTTP_ACCEPT': 'application/json'} + headers = {'OpenStack-API-Version': 'container 1.3'} + response = self.app.get('/v1/hosts/?limit=3&marker=%s' + % host_list[2].uuid, + extra_environ=extra_environ, headers=headers) + + self.assertEqual(200, response.status_int) + actual_hosts = response.json['hosts'] + self.assertEqual(1, len(actual_hosts)) + self.assertEqual(host_list[-1].uuid, + actual_hosts[0].get('uuid')) + + +class TestHostEnforcement(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): + extra_environ = {'HTTP_ACCEPT': 'application/json'} + headers = {'OpenStack-API-Version': 'container 1.3'} + self._common_policy_check( + 'host:get_all', self.get_json, '/hosts/', + expect_errors=True, extra_environ=extra_environ, headers=headers)