Give a way to save why a service has been disabled.

Implements blueprint record-reason-for-disabling-service

We added a field to the service table to log a reason when a service has
been disabled.
We added a new API extension called os-extended-services. The new extension
will extend the os-services extension adding:
- A method for disabling a service and specify a reason for that.

PUT /v2/{tenant_id}/os-services/disable-log-reason

When the os-extended-extension is loaded the call:

GET /V2/{tenant_id}/os-services
will return the list of services with reason information it that exists.

DocImpact
Change-Id: I87a4affc45160796ff11c7b03e591e6aba73d62a
This commit is contained in:
Andrea Rosa
2013-02-20 10:10:04 +00:00
committed by Michael Still
parent d7f898eab9
commit c741e862fd
25 changed files with 540 additions and 65 deletions

View File

@@ -496,6 +496,14 @@
"namespace": "http://docs.openstack.org/compute/ext/services/api/v2", "namespace": "http://docs.openstack.org/compute/ext/services/api/v2",
"updated": "2012-10-28T00:00:00-00:00" "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", "alias": "os-simple-tenant-usage",
"description": "Simple tenant usage extension.", "description": "Simple tenant usage extension.",

View File

@@ -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"> <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> <description>Services support.</description>
</extension> </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"> <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> <description>Simple tenant usage extension.</description>
</extension> </extension>

View File

@@ -0,0 +1,5 @@
{
"host": "host1",
"binary": "nova-compute",
"disabled_reason": "test2"
}

View File

@@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<service host="host1" binary="nova-compute" disabled_reason="test2"/>

View File

@@ -0,0 +1,8 @@
{
"service": {
"binary": "nova-compute",
"host": "host1",
"disabled_reason": "test2",
"status": "disabled"
}
}

View File

@@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<service host="host1" binary="nova-compute" status="disabled" disabled_reason="test2" />

View File

@@ -1,4 +1,4 @@
{ {
"host": "host1", "host": "host1",
"service": "nova-compute" "binary": "nova-compute"
} }

View File

@@ -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"
}
]
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -23,6 +23,7 @@ from nova.api.openstack import xmlutil
from nova import compute from nova import compute
from nova import exception from nova import exception
from nova import servicegroup from nova import servicegroup
from nova import utils
authorize = extensions.extension_authorizer('compute', 'services') authorize = extensions.extension_authorizer('compute', 'services')
CONF = cfg.CONF CONF = cfg.CONF
@@ -39,6 +40,7 @@ class ServicesIndexTemplate(xmlutil.TemplateBuilder):
elem.set('status') elem.set('status')
elem.set('state') elem.set('state')
elem.set('updated_at') elem.set('updated_at')
elem.set('disabled_reason')
return xmlutil.MasterTemplate(root, 1) return xmlutil.MasterTemplate(root, 1)
@@ -49,6 +51,7 @@ class ServiceUpdateTemplate(xmlutil.TemplateBuilder):
root.set('host') root.set('host')
root.set('binary') root.set('binary')
root.set('status') root.set('status')
root.set('disabled_reason')
return xmlutil.MasterTemplate(root, 1) return xmlutil.MasterTemplate(root, 1)
@@ -62,21 +65,20 @@ class ServiceUpdateDeserializer(wsgi.XMLDeserializer):
return service return service
service['host'] = service_node.getAttribute('host') service['host'] = service_node.getAttribute('host')
service['binary'] = service_node.getAttribute('binary') service['binary'] = service_node.getAttribute('binary')
service['disabled_reason'] = service_node.getAttribute(
'disabled_reason')
return dict(body=service) return dict(body=service)
class ServiceController(object): class ServiceController(object):
def __init__(self): def __init__(self, ext_mgr=None, *args, **kwargs):
self.host_api = compute.HostAPI() self.host_api = compute.HostAPI()
self.servicegroup_api = servicegroup.API() self.servicegroup_api = servicegroup.API()
self.ext_mgr = ext_mgr
@wsgi.serializers(xml=ServicesIndexTemplate) def _get_services(self, req):
def index(self, req):
"""
Return a list of all running services. Filter by host & service name.
"""
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context) authorize(context)
services = self.host_api.service_get_all( services = self.host_api.service_get_all(
@@ -93,18 +95,49 @@ class ServiceController(object):
if binary: if binary:
services = [s for s in services if s['binary'] == 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 = [] svcs = []
for svc in services: for svc in services:
alive = self.servicegroup_api.service_is_up(svc) svcs.append(self._get_service_detail(svc, detailed))
art = (alive and "up") or "down"
active = 'enabled' return svcs
if svc['disabled']:
active = 'disabled' def _is_valid_as_reason(self, reason):
svcs.append({"binary": svc['binary'], 'host': svc['host'], try:
'zone': svc['availability_zone'], utils.check_string_length(reason.strip(), 'Disabled reason',
'status': active, 'state': art, min_length=1, max_length=255)
'updated_at': svc['updated_at']}) except exception.InvalidInput:
return {'services': svcs} 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.deserializers(xml=ServiceUpdateDeserializer)
@wsgi.serializers(xml=ServiceUpdateTemplate) @wsgi.serializers(xml=ServiceUpdateTemplate)
@@ -113,28 +146,49 @@ class ServiceController(object):
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context) authorize(context)
ext_loaded = self.ext_mgr.is_loaded('os-extended-services')
if id == "enable": if id == "enable":
disabled = False disabled = False
elif id == "disable": status = "enabled"
elif (id == "disable" or
(id == "disable-log-reason" and ext_loaded)):
disabled = True disabled = True
status = "disabled"
else: else:
raise webob.exc.HTTPNotFound(_("Unknown action")) raise webob.exc.HTTPNotFound("Unknown action")
status = id + 'd'
try: try:
host = body['host'] host = body['host']
binary = body['binary'] 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): 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: try:
svc = self.host_api.service_update(context, host, binary, svc = self.host_api.service_update(context, host, binary,
{'disabled': disabled}) status_detail)
except exception.ServiceNotFound as exc: except exception.ServiceNotFound:
raise webob.exc.HTTPNotFound(_("Unknown service")) raise webob.exc.HTTPNotFound(_("Unknown service"))
return {'service': {'host': host, 'binary': binary, 'status': status}} return ret_value
class Services(extensions.ExtensionDescriptor): class Services(extensions.ExtensionDescriptor):
@@ -148,6 +202,7 @@ class Services(extensions.ExtensionDescriptor):
def get_resources(self): def get_resources(self):
resources = [] resources = []
resource = extensions.ResourceExtension('os-services', resource = extensions.ResourceExtension('os-services',
ServiceController()) ServiceController(self.ext_mgr))
resources.append(resource) resources.append(resource)
return resources return resources

View File

@@ -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')

View File

@@ -53,6 +53,7 @@ class Service(BASE, NovaBase):
topic = Column(String(255), nullable=True) topic = Column(String(255), nullable=True)
report_count = Column(Integer, nullable=False, default=0) report_count = Column(Integer, nullable=False, default=0)
disabled = Column(Boolean, default=False) disabled = Column(Boolean, default=False)
disabled_reason = Column(String(255))
class ComputeNode(BASE, NovaBase): class ComputeNode(BASE, NovaBase):

View File

@@ -14,8 +14,10 @@
import datetime import datetime
import webob.exc
from nova.api.openstack.compute.contrib import services from nova.api.openstack.compute.contrib import services
from nova.api.openstack import extensions
from nova import availability_zones from nova import availability_zones
from nova import context from nova import context
from nova import db from nova import db
@@ -33,28 +35,32 @@ fake_services_list = [
'disabled': True, 'disabled': True,
'topic': 'scheduler', 'topic': 'scheduler',
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), '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', {'binary': 'nova-compute',
'host': 'host1', 'host': 'host1',
'id': 2, 'id': 2,
'disabled': True, 'disabled': True,
'topic': 'compute', 'topic': 'compute',
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), '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', {'binary': 'nova-scheduler',
'host': 'host2', 'host': 'host2',
'id': 3, 'id': 3,
'disabled': False, 'disabled': False,
'topic': 'scheduler', 'topic': 'scheduler',
'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34), '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', {'binary': 'nova-compute',
'host': 'host2', 'host': 'host2',
'id': 4, 'id': 4,
'disabled': True, 'disabled': True,
'topic': 'compute', 'topic': 'compute',
'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38), '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) service = fake_service_get_by_id(service_id)
if service is None: if service is None:
raise exception.ServiceNotFound(service_id=service_id) raise exception.ServiceNotFound(service_id=service_id)
else:
{'host': 'host1', 'service': 'nova-compute',
'disabled': values['disabled']}
def fake_utcnow(): def fake_utcnow():
@@ -121,8 +124,9 @@ class ServicesTest(test.TestCase):
super(ServicesTest, self).setUp() super(ServicesTest, self).setUp()
self.context = context.get_admin_context() 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", self.stubs.Set(self.controller.host_api, "service_get_all",
fake_host_api_service_get_all) fake_host_api_service_get_all)
self.stubs.Set(timeutils, "utcnow", fake_utcnow) self.stubs.Set(timeutils, "utcnow", fake_utcnow)
@@ -134,21 +138,30 @@ class ServicesTest(test.TestCase):
req = FakeRequest() req = FakeRequest()
res_dict = self.controller.index(req) res_dict = self.controller.index(req)
response = {'services': [{'binary': 'nova-scheduler', response = {'services': [
'host': 'host1', 'zone': 'internal', {'binary': 'nova-scheduler',
'status': 'disabled', 'state': 'up', 'host': 'host1',
'zone': 'internal',
'status': 'disabled',
'state': 'up',
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)}, 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)},
{'binary': 'nova-compute', {'binary': 'nova-compute',
'host': 'host1', 'zone': 'nova', 'host': 'host1',
'status': 'disabled', 'state': 'up',
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)},
{'binary': 'nova-scheduler', 'host': 'host2',
'zone': 'internal',
'status': 'enabled', 'state': 'down',
'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34)},
{'binary': 'nova-compute', 'host': 'host2',
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'state': 'down', 'status': 'disabled',
'state': 'up',
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)},
{'binary': 'nova-scheduler',
'host': 'host2',
'zone': 'internal',
'status': 'enabled',
'state': 'down',
'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34)},
{'binary': 'nova-compute',
'host': 'host2',
'zone': 'nova',
'status': 'disabled',
'state': 'down',
'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]} 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]}
self.assertEqual(res_dict, response) self.assertEqual(res_dict, response)
@@ -156,13 +169,18 @@ class ServicesTest(test.TestCase):
req = FakeRequestWithHost() req = FakeRequestWithHost()
res_dict = self.controller.index(req) res_dict = self.controller.index(req)
response = {'services': [{'binary': 'nova-scheduler', 'host': 'host1', response = {'services': [
{'binary': 'nova-scheduler',
'host': 'host1',
'zone': 'internal', 'zone': 'internal',
'status': 'disabled', 'state': 'up', 'status': 'disabled',
'state': 'up',
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)}, 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)},
{'binary': 'nova-compute', 'host': 'host1', {'binary': 'nova-compute',
'host': 'host1',
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'state': 'up', 'status': 'disabled',
'state': 'up',
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}]} 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}]}
self.assertEqual(res_dict, response) self.assertEqual(res_dict, response)
@@ -170,13 +188,18 @@ class ServicesTest(test.TestCase):
req = FakeRequestWithService() req = FakeRequestWithService()
res_dict = self.controller.index(req) res_dict = self.controller.index(req)
response = {'services': [{'binary': 'nova-compute', 'host': 'host1', response = {'services': [
{'binary': 'nova-compute',
'host': 'host1',
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'state': 'up', 'status': 'disabled',
'state': 'up',
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}, 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)},
{'binary': 'nova-compute', 'host': 'host2', {'binary': 'nova-compute',
'host': 'host2',
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'state': 'down', 'status': 'disabled',
'state': 'down',
'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]} 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]}
self.assertEqual(res_dict, response) self.assertEqual(res_dict, response)
@@ -184,25 +207,125 @@ class ServicesTest(test.TestCase):
req = FakeRequestWithHostService() req = FakeRequestWithHostService()
res_dict = self.controller.index(req) res_dict = self.controller.index(req)
response = {'services': [{'binary': 'nova-compute', 'host': 'host1', response = {'services': [
{'binary': 'nova-compute',
'host': 'host1',
'zone': 'nova', 'zone': 'nova',
'status': 'disabled', 'state': 'up', 'status': 'disabled',
'state': 'up',
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}]} 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}]}
self.assertEqual(res_dict, response) 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): def test_services_enable(self):
body = {'host': 'host1', 'binary': 'nova-compute'} body = {'host': 'host1', 'binary': 'nova-compute'}
req = fakes.HTTPRequest.blank('/v2/fake/os-services/enable') req = fakes.HTTPRequest.blank('/v2/fake/os-services/enable')
res_dict = self.controller.update(req, "enable", body) res_dict = self.controller.update(req, "enable", body)
self.assertEqual(res_dict['service']['status'], 'enabled') self.assertEqual(res_dict['service']['status'], 'enabled')
self.assertFalse('disabled_reason' in res_dict['service'])
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')
# This test is just to verify that the servicegroup API gets used when # This test is just to verify that the servicegroup API gets used when
# calling this API. # calling this API.
@@ -213,3 +336,44 @@ class ServicesTest(test.TestCase):
self.stubs.Set(db_driver.DbDriver, 'is_up', dummy_is_up) self.stubs.Set(db_driver.DbDriver, 'is_up', dummy_is_up)
req = FakeRequestWithHostService() req = FakeRequestWithHostService()
self.assertRaises(KeyError, self.controller.index, req) 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))

View File

@@ -784,6 +784,7 @@ class CellsTargetedMethodsTestCase(test.TestCase):
result = response.value_or_raise() result = response.value_or_raise()
result.pop('created_at', None) result.pop('created_at', None)
result.pop('updated_at', None) result.pop('updated_at', None)
result.pop('disabled_reason', None)
expected_result = dict( expected_result = dict(
deleted=0, deleted_at=None, deleted=0, deleted_at=None,
binary=fake_service['binary'], binary=fake_service['binary'],

View File

@@ -1626,6 +1626,16 @@ class TestNovaMigrations(BaseMigrationTestCase, CommonTestsMixIn):
# check that groups does not exist # check that groups does not exist
self._check_no_group_instance_tables(engine) 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): class TestBaremetalMigrations(BaseMigrationTestCase, CommonTestsMixIn):
"""Test sqlalchemy-migrate migrations.""" """Test sqlalchemy-migrate migrations."""

View File

@@ -360,6 +360,14 @@
"namespace": "http://docs.openstack.org/compute/ext/services/api/v2", "namespace": "http://docs.openstack.org/compute/ext/services/api/v2",
"updated": "%(timestamp)s" "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", "alias": "os-fping",
"description": "%(text)s", "description": "%(text)s",

View File

@@ -135,6 +135,9 @@
<extension alias="os-services" name="Services" namespace="http://docs.openstack.org/compute/ext/services/api/v2" updated="%(timestamp)s"> <extension alias="os-services" name="Services" namespace="http://docs.openstack.org/compute/ext/services/api/v2" updated="%(timestamp)s">
<description>%(text)s</description> <description>%(text)s</description>
</extension> </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"> <extension alias="os-fping" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/fping/api/v1.1" name="Fping">
<description>%(text)s</description> <description>%(text)s</description>
</extension> </extension>

View File

@@ -0,0 +1,5 @@
{
"host": "%(host)s",
"binary": "%(binary)s",
"disabled_reason": "%(disabled_reason)s"
}

View File

@@ -0,0 +1,2 @@
<?xml version='1.0' encoding='UTF-8'?>
<service host="%(host)s" binary="%(binary)s" disabled_reason="%(disabled_reason)s"/>

View File

@@ -0,0 +1,8 @@
{
"service": {
"binary": "%(binary)s",
"host": "%(host)s",
"disabled_reason": "%(disabled_reason)s",
"status": "disabled"
}
}

View File

@@ -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"/>

View File

@@ -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"
}
]
}

View File

@@ -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>

View File

@@ -30,6 +30,7 @@ from oslo.config import cfg
from nova.api.metadata import password from nova.api.metadata import password
from nova.api.openstack.compute.contrib import coverage_ext from nova.api.openstack.compute.contrib import coverage_ext
from nova.api.openstack.compute.contrib import fping 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. # Import extensions to pull in osapi_compute_extension CONF option used below.
from nova.cells import state from nova.cells import state
from nova.cloudpipe import pipelib from nova.cloudpipe import pipelib
@@ -1965,6 +1966,9 @@ class ServicesJsonTest(ApiSampleTestBase):
super(ServicesJsonTest, self).tearDown() super(ServicesJsonTest, self).tearDown()
timeutils.clear_time_override() timeutils.clear_time_override()
def fake_load(self, *args):
return True
def test_services_list(self): def test_services_list(self):
"""Return a list of all agent builds.""" """Return a list of all agent builds."""
response = self._do_get('os-services') response = self._do_get('os-services')
@@ -1996,11 +2000,55 @@ class ServicesJsonTest(ApiSampleTestBase):
"binary": "nova-compute"} "binary": "nova-compute"}
self._verify_response('service-disable-put-resp', subs, response, 200) 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): class ServicesXmlTest(ServicesJsonTest):
ctype = 'xml' 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): class SimpleTenantUsageSampleJsonTest(ServersSampleBase):
extension_name = ("nova.api.openstack.compute.contrib.simple_tenant_usage." extension_name = ("nova.api.openstack.compute.contrib.simple_tenant_usage."
"Simple_tenant_usage") "Simple_tenant_usage")