diff options
25 files changed, 537 insertions, 62 deletions
diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index 3752aedf5..09ede77dc 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -497,6 +497,14 @@ "updated": "2012-10-28T00:00:00-00:00" }, { + "alias": "os-extended-services", + "description": "Extended services support.", + "links": [], + "name": "ExtendedServices", + "namespace": "http://docs.openstack.org/compute/ext/extended_services/api/v2", + "updated": "2013-05-17T00:00:00-00:00" + }, + { "alias": "os-simple-tenant-usage", "description": "Simple tenant usage extension.", "links": [], diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index 97cfd4abd..1c6a9a58b 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -204,6 +204,9 @@ <extension alias="os-services" updated="2012-10-28T00:00:00-00:00" namespace="http://docs.openstack.org/compute/ext/services/api/v2" name="Services"> <description>Services support.</description> </extension> + <extension alias="os-extended-services" updated="2013-05-17T00:00:00-00:00" namespace="http://docs.openstack.org/compute/ext/extended_services/api/v2" name="ExtendedServices"> + <description>Extended services support.</description> + </extension> <extension alias="os-simple-tenant-usage" updated="2011-08-19T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/os-simple-tenant-usage/api/v1.1" name="SimpleTenantUsage"> <description>Simple tenant usage extension.</description> </extension> diff --git a/doc/api_samples/os-services/service-disable-log-put-req.json b/doc/api_samples/os-services/service-disable-log-put-req.json new file mode 100644 index 000000000..6ccc5524e --- /dev/null +++ b/doc/api_samples/os-services/service-disable-log-put-req.json @@ -0,0 +1,5 @@ +{ + "host": "host1", + "binary": "nova-compute", + "disabled_reason": "test2" +} diff --git a/doc/api_samples/os-services/service-disable-log-put-req.xml b/doc/api_samples/os-services/service-disable-log-put-req.xml new file mode 100644 index 000000000..f8eec8173 --- /dev/null +++ b/doc/api_samples/os-services/service-disable-log-put-req.xml @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<service host="host1" binary="nova-compute" disabled_reason="test2"/> diff --git a/doc/api_samples/os-services/service-disable-log-put-resp.json b/doc/api_samples/os-services/service-disable-log-put-resp.json new file mode 100644 index 000000000..c82bc54c3 --- /dev/null +++ b/doc/api_samples/os-services/service-disable-log-put-resp.json @@ -0,0 +1,8 @@ +{ + "service": { + "binary": "nova-compute", + "host": "host1", + "disabled_reason": "test2", + "status": "disabled" + } +} diff --git a/doc/api_samples/os-services/service-disable-log-put-resp.xml b/doc/api_samples/os-services/service-disable-log-put-resp.xml new file mode 100644 index 000000000..1ddf273b1 --- /dev/null +++ b/doc/api_samples/os-services/service-disable-log-put-resp.xml @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<service host="host1" binary="nova-compute" status="disabled" disabled_reason="test2" /> diff --git a/doc/api_samples/os-services/service-enable-put-req.json b/doc/api_samples/os-services/service-enable-put-req.json index ffe896999..f96d34536 100644 --- a/doc/api_samples/os-services/service-enable-put-req.json +++ b/doc/api_samples/os-services/service-enable-put-req.json @@ -1,4 +1,4 @@ { "host": "host1", - "service": "nova-compute" + "binary": "nova-compute" } diff --git a/doc/api_samples/os-services/services-get-resp.json b/doc/api_samples/os-services/services-get-resp.json new file mode 100644 index 000000000..4c873d636 --- /dev/null +++ b/doc/api_samples/os-services/services-get-resp.json @@ -0,0 +1,40 @@ +{ + "services": [ + { + "binary": "nova-scheduler", + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "2012-10-29T13:42:02.000000", + "zone": "internal", + "disabled_reason": "test1" + }, + { + "binary": "nova-compute", + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "2012-10-29T13:42:05.000000", + "zone": "nova", + "disabled_reason": "test2" + }, + { + "binary": "nova-scheduler", + "host": "host2", + "state": "down", + "status": "enabled", + "updated_at": "2012-09-19T06:55:34.000000", + "zone": "internal", + "disabled_reason": "" + }, + { + "binary": "nova-compute", + "host": "host2", + "state": "down", + "status": "disabled", + "updated_at": "2012-09-18T08:03:38.000000", + "zone": "nova", + "disabled_reason": "test4" + } + ] +} diff --git a/doc/api_samples/os-services/services-get-resp.xml b/doc/api_samples/os-services/services-get-resp.xml new file mode 100644 index 000000000..997fc3945 --- /dev/null +++ b/doc/api_samples/os-services/services-get-resp.xml @@ -0,0 +1,6 @@ +<services> + <service status="disabled" binary="nova-scheduler" zone="internal" state="up" host="host1" updated_at="2012-10-29T13:42:02.000000" disabled_reason="test1"/> + <service status="disabled" binary="nova-compute" zone="nova" state="up" host="host1" updated_at="2012-10-29T13:42:05.000000" disabled_reason="test2"/> + <service status="enabled" binary="nova-scheduler" zone="internal" state="down" host="host2" updated_at="2012-09-19T06:55:34.000000" disabled_reason=""/> + <service status="disabled" binary="nova-compute" zone="nova" state="down" host="host2" updated_at="2012-09-18T08:03:38.000000" disabled_reason="test4"/> +</services> diff --git a/nova/api/openstack/compute/contrib/extended_services.py b/nova/api/openstack/compute/contrib/extended_services.py new file mode 100644 index 000000000..f9f1a4d86 --- /dev/null +++ b/nova/api/openstack/compute/contrib/extended_services.py @@ -0,0 +1,11 @@ +from nova.api.openstack import extensions + + +class Extended_services(extensions.ExtensionDescriptor): + """Extended services support.""" + + name = "ExtendedServices" + alias = "os-extended-services" + namespace = ("http://docs.openstack.org/compute/ext/" + "extended_services/api/v2") + updated = "2013-05-17T00:00:00-00:00" diff --git a/nova/api/openstack/compute/contrib/services.py b/nova/api/openstack/compute/contrib/services.py index bc5f60b64..3a637010a 100644 --- a/nova/api/openstack/compute/contrib/services.py +++ b/nova/api/openstack/compute/contrib/services.py @@ -23,6 +23,7 @@ from nova.api.openstack import xmlutil from nova import compute from nova import exception from nova import servicegroup +from nova import utils authorize = extensions.extension_authorizer('compute', 'services') CONF = cfg.CONF @@ -39,6 +40,7 @@ class ServicesIndexTemplate(xmlutil.TemplateBuilder): elem.set('status') elem.set('state') elem.set('updated_at') + elem.set('disabled_reason') return xmlutil.MasterTemplate(root, 1) @@ -49,6 +51,7 @@ class ServiceUpdateTemplate(xmlutil.TemplateBuilder): root.set('host') root.set('binary') root.set('status') + root.set('disabled_reason') return xmlutil.MasterTemplate(root, 1) @@ -62,21 +65,20 @@ class ServiceUpdateDeserializer(wsgi.XMLDeserializer): return service service['host'] = service_node.getAttribute('host') service['binary'] = service_node.getAttribute('binary') + service['disabled_reason'] = service_node.getAttribute( + 'disabled_reason') return dict(body=service) class ServiceController(object): - def __init__(self): + def __init__(self, ext_mgr=None, *args, **kwargs): self.host_api = compute.HostAPI() self.servicegroup_api = servicegroup.API() + self.ext_mgr = ext_mgr - @wsgi.serializers(xml=ServicesIndexTemplate) - def index(self, req): - """ - Return a list of all running services. Filter by host & service name. - """ + def _get_services(self, req): context = req.environ['nova.context'] authorize(context) services = self.host_api.service_get_all( @@ -93,18 +95,49 @@ class ServiceController(object): if binary: services = [s for s in services if s['binary'] == binary] + return services + + def _get_service_detail(self, svc, detailed): + alive = self.servicegroup_api.service_is_up(svc) + state = (alive and "up") or "down" + active = 'enabled' + if svc['disabled']: + active = 'disabled' + service_detail = {'binary': svc['binary'], 'host': svc['host'], + 'zone': svc['availability_zone'], + 'status': active, 'state': state, + 'updated_at': svc['updated_at']} + if detailed: + service_detail['disabled_reason'] = svc['disabled_reason'] + + return service_detail + + def _get_services_list(self, req, detailed): + services = self._get_services(req) svcs = [] for svc in services: - alive = self.servicegroup_api.service_is_up(svc) - art = (alive and "up") or "down" - active = 'enabled' - if svc['disabled']: - active = 'disabled' - svcs.append({"binary": svc['binary'], 'host': svc['host'], - 'zone': svc['availability_zone'], - 'status': active, 'state': art, - 'updated_at': svc['updated_at']}) - return {'services': svcs} + svcs.append(self._get_service_detail(svc, detailed)) + + return svcs + + def _is_valid_as_reason(self, reason): + try: + utils.check_string_length(reason.strip(), 'Disabled reason', + min_length=1, max_length=255) + except exception.InvalidInput: + return False + + return True + + @wsgi.serializers(xml=ServicesIndexTemplate) + def index(self, req): + """ + Return a list of all running services. Filter by host & service name. + """ + detailed = self.ext_mgr.is_loaded('os-extended-services') + services = self._get_services_list(req, detailed) + + return {'services': services} @wsgi.deserializers(xml=ServiceUpdateDeserializer) @wsgi.serializers(xml=ServiceUpdateTemplate) @@ -113,28 +146,49 @@ class ServiceController(object): context = req.environ['nova.context'] authorize(context) + ext_loaded = self.ext_mgr.is_loaded('os-extended-services') if id == "enable": disabled = False - elif id == "disable": + status = "enabled" + elif (id == "disable" or + (id == "disable-log-reason" and ext_loaded)): disabled = True + status = "disabled" else: - raise webob.exc.HTTPNotFound(_("Unknown action")) - - status = id + 'd' - + raise webob.exc.HTTPNotFound("Unknown action") try: host = body['host'] binary = body['binary'] + ret_value = { + 'service': { + 'host': host, + 'binary': binary, + 'status': status, + }, + } + status_detail = {'disabled': disabled} + if id == "disable-log-reason": + reason = body['disabled_reason'] + if not self._is_valid_as_reason(reason): + msg = _('Disabled reason contains invalid characters ' + 'or is too long') + raise webob.exc.HTTPUnprocessableEntity(detail=msg) + + status_detail['disabled_reason'] = reason + ret_value['service']['disabled_reason'] = reason except (TypeError, KeyError): - raise webob.exc.HTTPUnprocessableEntity() + msg = _('Invalid attribute in the request') + if 'host' in body and 'binary' in body: + msg = _('Missing disabled reason field') + raise webob.exc.HTTPUnprocessableEntity(detail=msg) try: svc = self.host_api.service_update(context, host, binary, - {'disabled': disabled}) - except exception.ServiceNotFound as exc: + status_detail) + except exception.ServiceNotFound: raise webob.exc.HTTPNotFound(_("Unknown service")) - return {'service': {'host': host, 'binary': binary, 'status': status}} + return ret_value class Services(extensions.ExtensionDescriptor): @@ -148,6 +202,7 @@ class Services(extensions.ExtensionDescriptor): def get_resources(self): resources = [] resource = extensions.ResourceExtension('os-services', - ServiceController()) + ServiceController(self.ext_mgr)) + resources.append(resource) return resources diff --git a/nova/db/sqlalchemy/migrate_repo/versions/188_add_reason_column_to_service.py b/nova/db/sqlalchemy/migrate_repo/versions/188_add_reason_column_to_service.py new file mode 100644 index 000000000..ed87bcfeb --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/188_add_reason_column_to_service.py @@ -0,0 +1,36 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation. +# +# 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 sqlalchemy import Column, MetaData, String, Table + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + services = Table('services', meta, autoload=True) + reason = Column('disabled_reason', String(255)) + services.create_column(reason) + shadow_services = Table('shadow_services', meta, autoload=True) + shadow_services.create_column(reason.copy()) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + services = Table('services', meta, autoload=True) + services.drop_column('disabled_reason') + shadow_services = Table('shadow_services', meta, autoload=True) + shadow_services.drop_column('disabled_reason') diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 28fe36a0d..d12e47246 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -53,6 +53,7 @@ class Service(BASE, NovaBase): topic = Column(String(255), nullable=True) report_count = Column(Integer, nullable=False, default=0) disabled = Column(Boolean, default=False) + disabled_reason = Column(String(255)) class ComputeNode(BASE, NovaBase): diff --git a/nova/tests/api/openstack/compute/contrib/test_services.py b/nova/tests/api/openstack/compute/contrib/test_services.py index 387a5eb21..c9d0c76e5 100644 --- a/nova/tests/api/openstack/compute/contrib/test_services.py +++ b/nova/tests/api/openstack/compute/contrib/test_services.py @@ -14,8 +14,10 @@ import datetime +import webob.exc from nova.api.openstack.compute.contrib import services +from nova.api.openstack import extensions from nova import availability_zones from nova import context from nova import db @@ -33,28 +35,32 @@ fake_services_list = [ 'disabled': True, 'topic': 'scheduler', 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), - 'created_at': datetime.datetime(2012, 9, 18, 2, 46, 27)}, + 'created_at': datetime.datetime(2012, 9, 18, 2, 46, 27), + 'disabled_reason': 'test1'}, {'binary': 'nova-compute', 'host': 'host1', 'id': 2, 'disabled': True, 'topic': 'compute', 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), - 'created_at': datetime.datetime(2012, 9, 18, 2, 46, 27)}, + 'created_at': datetime.datetime(2012, 9, 18, 2, 46, 27), + 'disabled_reason': 'test2'}, {'binary': 'nova-scheduler', 'host': 'host2', 'id': 3, 'disabled': False, 'topic': 'scheduler', 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34), - 'created_at': datetime.datetime(2012, 9, 18, 2, 46, 28)}, + 'created_at': datetime.datetime(2012, 9, 18, 2, 46, 28), + 'disabled_reason': ''}, {'binary': 'nova-compute', 'host': 'host2', 'id': 4, 'disabled': True, 'topic': 'compute', 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38), - 'created_at': datetime.datetime(2012, 9, 18, 2, 46, 28)}, + 'created_at': datetime.datetime(2012, 9, 18, 2, 46, 28), + 'disabled_reason': 'test4'}, ] @@ -106,9 +112,6 @@ def fake_service_update(context, service_id, values): service = fake_service_get_by_id(service_id) if service is None: raise exception.ServiceNotFound(service_id=service_id) - else: - {'host': 'host1', 'service': 'nova-compute', - 'disabled': values['disabled']} def fake_utcnow(): @@ -121,8 +124,9 @@ class ServicesTest(test.TestCase): super(ServicesTest, self).setUp() self.context = context.get_admin_context() - self.controller = services.ServiceController() - + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = services.ServiceController(self.ext_mgr) self.stubs.Set(self.controller.host_api, "service_get_all", fake_host_api_service_get_all) self.stubs.Set(timeutils, "utcnow", fake_utcnow) @@ -134,21 +138,30 @@ class ServicesTest(test.TestCase): req = FakeRequest() res_dict = self.controller.index(req) - response = {'services': [{'binary': 'nova-scheduler', - 'host': 'host1', 'zone': 'internal', - 'status': 'disabled', 'state': 'up', + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)}, {'binary': 'nova-compute', - 'host': 'host1', 'zone': 'nova', - 'status': 'disabled', 'state': 'up', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}, - {'binary': 'nova-scheduler', 'host': 'host2', + {'binary': 'nova-scheduler', + 'host': 'host2', 'zone': 'internal', - 'status': 'enabled', 'state': 'down', + 'status': 'enabled', + 'state': 'down', 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34)}, - {'binary': 'nova-compute', 'host': 'host2', + {'binary': 'nova-compute', + 'host': 'host2', 'zone': 'nova', - 'status': 'disabled', 'state': 'down', + 'status': 'disabled', + 'state': 'down', 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]} self.assertEqual(res_dict, response) @@ -156,13 +169,18 @@ class ServicesTest(test.TestCase): req = FakeRequestWithHost() res_dict = self.controller.index(req) - response = {'services': [{'binary': 'nova-scheduler', 'host': 'host1', + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', 'zone': 'internal', - 'status': 'disabled', 'state': 'up', + 'status': 'disabled', + 'state': 'up', 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)}, - {'binary': 'nova-compute', 'host': 'host1', + {'binary': 'nova-compute', + 'host': 'host1', 'zone': 'nova', - 'status': 'disabled', 'state': 'up', + 'status': 'disabled', + 'state': 'up', 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}]} self.assertEqual(res_dict, response) @@ -170,13 +188,18 @@ class ServicesTest(test.TestCase): req = FakeRequestWithService() res_dict = self.controller.index(req) - response = {'services': [{'binary': 'nova-compute', 'host': 'host1', + response = {'services': [ + {'binary': 'nova-compute', + 'host': 'host1', 'zone': 'nova', - 'status': 'disabled', 'state': 'up', + 'status': 'disabled', + 'state': 'up', 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}, - {'binary': 'nova-compute', 'host': 'host2', + {'binary': 'nova-compute', + 'host': 'host2', 'zone': 'nova', - 'status': 'disabled', 'state': 'down', + 'status': 'disabled', + 'state': 'down', 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]} self.assertEqual(res_dict, response) @@ -184,25 +207,125 @@ class ServicesTest(test.TestCase): req = FakeRequestWithHostService() res_dict = self.controller.index(req) - response = {'services': [{'binary': 'nova-compute', 'host': 'host1', + response = {'services': [ + {'binary': 'nova-compute', + 'host': 'host1', 'zone': 'nova', - 'status': 'disabled', 'state': 'up', + 'status': 'disabled', + 'state': 'up', 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}]} self.assertEqual(res_dict, response) + def test_services_detail(self): + self.ext_mgr.extensions['os-extended-services'] = True + self.controller = services.ServiceController(self.ext_mgr) + self.stubs.Set(self.controller.host_api, "service_get_all", + fake_host_api_service_get_all) + req = FakeRequest() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'disabled_reason': 'test1'}, + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}, + {'binary': 'nova-scheduler', + 'host': 'host2', + 'zone': 'internal', + 'status': 'enabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34), + 'disabled_reason': ''}, + {'binary': 'nova-compute', + 'host': 'host2', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38), + 'disabled_reason': 'test4'}]} + self.assertEqual(res_dict, response) + + def test_service_detail_with_host(self): + self.ext_mgr.extensions['os-extended-services'] = True + self.controller = services.ServiceController(self.ext_mgr) + self.stubs.Set(self.controller.host_api, "service_get_all", + fake_host_api_service_get_all) + req = FakeRequestWithHost() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'disabled_reason': 'test1'}, + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}]} + self.assertEqual(res_dict, response) + + def test_service_detail_with_service(self): + self.ext_mgr.extensions['os-extended-services'] = True + self.controller = services.ServiceController(self.ext_mgr) + self.stubs.Set(self.controller.host_api, "service_get_all", + fake_host_api_service_get_all) + req = FakeRequestWithService() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}, + {'binary': 'nova-compute', + 'host': 'host2', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38), + 'disabled_reason': 'test4'}]} + self.assertEqual(res_dict, response) + + def test_service_detail_with_host_service(self): + self.ext_mgr.extensions['os-extended-services'] = True + self.controller = services.ServiceController(self.ext_mgr) + self.stubs.Set(self.controller.host_api, "service_get_all", + fake_host_api_service_get_all) + req = FakeRequestWithHostService() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}]} + self.assertEqual(res_dict, response) + def test_services_enable(self): body = {'host': 'host1', 'binary': 'nova-compute'} req = fakes.HTTPRequest.blank('/v2/fake/os-services/enable') res_dict = self.controller.update(req, "enable", body) self.assertEqual(res_dict['service']['status'], 'enabled') - - def test_services_disable(self): - req = fakes.HTTPRequest.blank('/v2/fake/os-services/disable') - body = {'host': 'host1', 'binary': 'nova-compute'} - res_dict = self.controller.update(req, "disable", body) - - self.assertEqual(res_dict['service']['status'], 'disabled') + self.assertFalse('disabled_reason' in res_dict['service']) # This test is just to verify that the servicegroup API gets used when # calling this API. @@ -213,3 +336,44 @@ class ServicesTest(test.TestCase): self.stubs.Set(db_driver.DbDriver, 'is_up', dummy_is_up) req = FakeRequestWithHostService() self.assertRaises(KeyError, self.controller.index, req) + + def test_services_disable(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-services/disable') + body = {'host': 'host1', 'binary': 'nova-compute'} + res_dict = self.controller.update(req, "disable", body) + + self.assertEqual(res_dict['service']['status'], 'disabled') + self.assertFalse('disabled_reason' in res_dict['service']) + + def test_services_disable_log_reason(self): + self.ext_mgr.extensions['os-extended-services'] = True + self.controller = services.ServiceController(self.ext_mgr) + req = \ + fakes.HTTPRequest.blank('v2/fakes/os-services/disable-log-reason') + body = {'host': 'host1', + 'binary': 'nova-compute', + 'disabled_reason': 'test-reason', + } + res_dict = self.controller.update(req, "disable-log-reason", body) + + self.assertEqual(res_dict['service']['status'], 'disabled') + self.assertEqual(res_dict['service']['disabled_reason'], 'test-reason') + + def test_mandatory_reason_field(self): + self.ext_mgr.extensions['os-extended-services'] = True + self.controller = services.ServiceController(self.ext_mgr) + req = \ + fakes.HTTPRequest.blank('v2/fakes/os-services/disable-log-reason') + body = {'host': 'host1', + 'binary': 'nova-compute', + } + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller.update, req, "disable-log-reason", body) + + def test_invalid_reason_field(self): + reason = ' ' + self.assertFalse(self.controller._is_valid_as_reason(reason)) + reason = 'a' * 256 + self.assertFalse(self.controller._is_valid_as_reason(reason)) + reason = 'it\'s a valid reason.' + self.assertTrue(self.controller._is_valid_as_reason(reason)) diff --git a/nova/tests/cells/test_cells_messaging.py b/nova/tests/cells/test_cells_messaging.py index 9aae11201..d4d9b052e 100644 --- a/nova/tests/cells/test_cells_messaging.py +++ b/nova/tests/cells/test_cells_messaging.py @@ -784,6 +784,7 @@ class CellsTargetedMethodsTestCase(test.TestCase): result = response.value_or_raise() result.pop('created_at', None) result.pop('updated_at', None) + result.pop('disabled_reason', None) expected_result = dict( deleted=0, deleted_at=None, binary=fake_service['binary'], diff --git a/nova/tests/db/test_migrations.py b/nova/tests/db/test_migrations.py index c73093d7d..3efa8c6bc 100644 --- a/nova/tests/db/test_migrations.py +++ b/nova/tests/db/test_migrations.py @@ -1626,6 +1626,16 @@ class TestNovaMigrations(BaseMigrationTestCase, CommonTestsMixIn): # check that groups does not exist self._check_no_group_instance_tables(engine) + def _check_188(self, engine, data): + services = db_utils.get_table(engine, 'services') + rows = services.select().execute().fetchall() + self.assertEqual(rows[0]['disabled_reason'], None) + + def _post_downgrade_188(self, engine): + services = db_utils.get_table(engine, 'services') + rows = services.select().execute().fetchall() + self.assertFalse('disabled_reason' in rows[0]) + class TestBaremetalMigrations(BaseMigrationTestCase, CommonTestsMixIn): """Test sqlalchemy-migrate migrations.""" diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index 1fbe71a90..33ddb3fab 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -361,6 +361,14 @@ "updated": "%(timestamp)s" }, { + "alias": "os-extended-services", + "description": "%(text)s", + "links": [], + "name": "ExtendedServices", + "namespace": "http://docs.openstack.org/compute/ext/extended_services/api/v2", + "updated": "%(timestamp)s" + }, + { "alias": "os-fping", "description": "%(text)s", "links": [], diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index f0a802f30..bbbef2c2c 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -135,6 +135,9 @@ <extension alias="os-services" name="Services" namespace="http://docs.openstack.org/compute/ext/services/api/v2" updated="%(timestamp)s"> <description>%(text)s</description> </extension> + <extension alias="os-extended-services" name="ExtendedServices" namespace="http://docs.openstack.org/compute/ext/extended_services/api/v2" updated="%(timestamp)s"> + <description>%(text)s</description> + </extension> <extension alias="os-fping" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/fping/api/v1.1" name="Fping"> <description>%(text)s</description> </extension> diff --git a/nova/tests/integrated/api_samples/os-services/service-disable-log-put-req.json.tpl b/nova/tests/integrated/api_samples/os-services/service-disable-log-put-req.json.tpl new file mode 100644 index 000000000..13ba2f11c --- /dev/null +++ b/nova/tests/integrated/api_samples/os-services/service-disable-log-put-req.json.tpl @@ -0,0 +1,5 @@ +{ + "host": "%(host)s", + "binary": "%(binary)s", + "disabled_reason": "%(disabled_reason)s" +} diff --git a/nova/tests/integrated/api_samples/os-services/service-disable-log-put-req.xml.tpl b/nova/tests/integrated/api_samples/os-services/service-disable-log-put-req.xml.tpl new file mode 100644 index 000000000..a1ffd7e20 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-services/service-disable-log-put-req.xml.tpl @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<service host="%(host)s" binary="%(binary)s" disabled_reason="%(disabled_reason)s"/> diff --git a/nova/tests/integrated/api_samples/os-services/service-disable-log-put-resp.json.tpl b/nova/tests/integrated/api_samples/os-services/service-disable-log-put-resp.json.tpl new file mode 100644 index 000000000..5266b0b62 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-services/service-disable-log-put-resp.json.tpl @@ -0,0 +1,8 @@ +{ + "service": { + "binary": "%(binary)s", + "host": "%(host)s", + "disabled_reason": "%(disabled_reason)s", + "status": "disabled" + } +} diff --git a/nova/tests/integrated/api_samples/os-services/service-disable-log-put-resp.xml.tpl b/nova/tests/integrated/api_samples/os-services/service-disable-log-put-resp.xml.tpl new file mode 100644 index 000000000..f7255d385 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-services/service-disable-log-put-resp.xml.tpl @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?> +<service status="disabled" binary="%(binary)s" host="%(host)s" disabled_reason="%(disabled_reason)s"/> diff --git a/nova/tests/integrated/api_samples/os-services/services-get-resp.json.tpl b/nova/tests/integrated/api_samples/os-services/services-get-resp.json.tpl new file mode 100644 index 000000000..39eda4e72 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-services/services-get-resp.json.tpl @@ -0,0 +1,40 @@ +{ + "services": [ + { + "binary": "nova-scheduler", + "host": "host1", + "disabled_reason": "test1", + "state": "up", + "status": "disabled", + "updated_at": "%(timestamp)s", + "zone": "internal" + }, + { + "binary": "nova-compute", + "host": "host1", + "disabled_reason": "test2", + "state": "up", + "status": "disabled", + "updated_at": "%(timestamp)s", + "zone": "nova" + }, + { + "binary": "nova-scheduler", + "host": "host2", + "disabled_reason": "", + "state": "down", + "status": "enabled", + "updated_at": "%(timestamp)s", + "zone": "internal" + }, + { + "binary": "nova-compute", + "host": "host2", + "disabled_reason": "test4", + "state": "down", + "status": "disabled", + "updated_at": "%(timestamp)s", + "zone": "nova" + } + ] +} diff --git a/nova/tests/integrated/api_samples/os-services/services-get-resp.xml.tpl b/nova/tests/integrated/api_samples/os-services/services-get-resp.xml.tpl new file mode 100644 index 000000000..53f8541ad --- /dev/null +++ b/nova/tests/integrated/api_samples/os-services/services-get-resp.xml.tpl @@ -0,0 +1,7 @@ +<?xml version='1.0' encoding='UTF-8'?> +<services> + <service status="disabled" binary="nova-scheduler" zone="internal" state="up" updated_at="%(timestamp)s" host="host1" disabled_reason="test1"/> + <service status="disabled" binary="nova-compute" zone="nova" state="up" updated_at="%(timestamp)s" host="host1" disabled_reason="test2"/> + <service status="enabled" binary="nova-scheduler" zone="internal" state="down" updated_at="%(timestamp)s" host="host2" disabled_reason=""/> + <service status="disabled" binary="nova-compute" zone="nova" state="down" updated_at="%(timestamp)s" host="host2" disabled_reason="test4"/> +</services> diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index 0d8c869f2..b929949d3 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -30,6 +30,7 @@ from oslo.config import cfg from nova.api.metadata import password from nova.api.openstack.compute.contrib import coverage_ext from nova.api.openstack.compute.contrib import fping +from nova.api.openstack.compute.extensions import ExtensionManager as ext_mgr # Import extensions to pull in osapi_compute_extension CONF option used below. from nova.cells import state from nova.cloudpipe import pipelib @@ -1965,6 +1966,9 @@ class ServicesJsonTest(ApiSampleTestBase): super(ServicesJsonTest, self).tearDown() timeutils.clear_time_override() + def fake_load(self, *args): + return True + def test_services_list(self): """Return a list of all agent builds.""" response = self._do_get('os-services') @@ -1996,11 +2000,55 @@ class ServicesJsonTest(ApiSampleTestBase): "binary": "nova-compute"} self._verify_response('service-disable-put-resp', subs, response, 200) + def test_service_detail(self): + """ + Return a list of all running services with the disable reason + information if that exists. + """ + self.stubs.Set(ext_mgr, "is_loaded", self.fake_load) + response = self._do_get('os-services') + self.assertEqual(response.status, 200) + subs = {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up'} + subs.update(self._get_regexes()) + return self._verify_response('services-get-resp', + subs, response, 200) + + def test_service_disable_log_reason(self): + """Disable an existing service and log the reason.""" + self.stubs.Set(ext_mgr, "is_loaded", self.fake_load) + subs = {"host": "host1", + 'binary': 'nova-compute', + 'disabled_reason': 'test2'} + response = self._do_put('os-services/disable-log-reason', + 'service-disable-log-put-req', subs) + return self._verify_response('service-disable-log-put-resp', + subs, response, 200) + class ServicesXmlTest(ServicesJsonTest): ctype = 'xml' +class ExtendedServicesJsonTest(ApiSampleTestBase): + """ + This extension is extending the functionalities of the + Services extension so the funcionalities introduced by this extension + are tested in the ServicesJsonTest and ServicesXmlTest classes. + """ + + extension_name = ("nova.api.openstack.compute.contrib." + "extended_services.Extended_services") + + +class ExtendedServicesXmlTest(ExtendedServicesJsonTest): + """This extension is tested in the ServicesXmlTest class.""" + ctype = 'xml' + + class SimpleTenantUsageSampleJsonTest(ServersSampleBase): extension_name = ("nova.api.openstack.compute.contrib.simple_tenant_usage." "Simple_tenant_usage") |