Add service management API to Karbor
Change-Id: Ie0b83fadd8581b85fec6e2e59ffbfe6b9fadecc7 Implements: blueprint karbor-service-management
This commit is contained in:
parent
22ed8a9d9f
commit
8fa5d82894
|
@ -19,6 +19,7 @@ from karbor.api.v1 import protectables
|
|||
from karbor.api.v1 import providers
|
||||
from karbor.api.v1 import restores
|
||||
from karbor.api.v1 import scheduled_operations
|
||||
from karbor.api.v1 import services
|
||||
from karbor.api.v1 import triggers
|
||||
from karbor.api.v1 import verifications
|
||||
|
||||
|
@ -37,6 +38,7 @@ class APIRouter(base_wsgi.Router):
|
|||
scheduled_operation_resources = scheduled_operations.create_resource()
|
||||
operation_log_resources = operation_logs.create_resource()
|
||||
verification_resources = verifications.create_resource()
|
||||
service_resources = services.create_resource()
|
||||
|
||||
mapper.resource("plan", "plans",
|
||||
controller=plans_resources,
|
||||
|
@ -104,4 +106,8 @@ class APIRouter(base_wsgi.Router):
|
|||
controller=verification_resources,
|
||||
collection={},
|
||||
member={'action': 'POST'})
|
||||
mapper.resource("os-service", "os-services",
|
||||
controller=service_resources,
|
||||
collection={},
|
||||
member={'action': 'POST'})
|
||||
super(APIRouter, self).__init__(mapper)
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
# 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.
|
||||
|
||||
"""The service management api."""
|
||||
from oslo_log import log as logging
|
||||
from webob import exc
|
||||
|
||||
from karbor.api import common
|
||||
from karbor.api.openstack import wsgi
|
||||
from karbor import exception
|
||||
from karbor.i18n import _
|
||||
from karbor import objects
|
||||
from karbor.policies import services as service_policy
|
||||
from karbor import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
SERVICES_CAN_BE_UPDATED = ['karbor-operationengine']
|
||||
|
||||
|
||||
class ServiceViewBuilder(common.ViewBuilder):
|
||||
"""Model a server API response as a python dictionary."""
|
||||
|
||||
_collection_name = "services"
|
||||
|
||||
def detail(self, request, service):
|
||||
"""Detailed view of a single service."""
|
||||
service_ref = {
|
||||
'service': {
|
||||
'id': service.get('id'),
|
||||
'binary': service.get('binary'),
|
||||
'host': service.get('host'),
|
||||
'status': 'disabled' if service.get('disabled') else 'enabled',
|
||||
'state': 'up' if utils.service_is_up(service) else 'down',
|
||||
'updated_at': service.get('updated_at'),
|
||||
'disabled_reason': service.get('disabled_reason')
|
||||
}
|
||||
}
|
||||
return service_ref
|
||||
|
||||
def detail_list(self, request, services, service_count=None):
|
||||
"""Detailed view of a list of services."""
|
||||
return self._list_view(self.detail, request, services)
|
||||
|
||||
def _list_view(self, func, request, services):
|
||||
"""Provide a view for a list of service.
|
||||
|
||||
:param func: Function used to format the service data
|
||||
:param request: API request
|
||||
:param services: List of services in dictionary format
|
||||
:returns: Service data in dictionary format
|
||||
"""
|
||||
services_list = [func(request, service)['service']
|
||||
for service in services]
|
||||
services_dict = {
|
||||
"services": services_list
|
||||
}
|
||||
|
||||
return services_dict
|
||||
|
||||
|
||||
class ServiceController(wsgi.Controller):
|
||||
"""The Service Management API controller for the OpenStack API."""
|
||||
|
||||
_view_builder_class = ServiceViewBuilder
|
||||
|
||||
def __init__(self):
|
||||
super(ServiceController, self).__init__()
|
||||
|
||||
def index(self, req):
|
||||
"""Returns a list of services
|
||||
|
||||
transformed through view builder.
|
||||
"""
|
||||
context = req.environ['karbor.context']
|
||||
context.can(service_policy.GET_ALL_POLICY)
|
||||
host = req.GET['host'] if 'host' in req.GET else None
|
||||
binary = req.GET['binary'] if 'binary' in req.GET else None
|
||||
try:
|
||||
services = objects.ServiceList.get_all_by_args(
|
||||
context, host, binary)
|
||||
except exception as e:
|
||||
LOG.error('List service failed, reason: %s' % e)
|
||||
raise
|
||||
return self._view_builder.detail_list(req, services)
|
||||
|
||||
def update(self, req, id, body):
|
||||
"""Enable/Disable scheduling for a service"""
|
||||
|
||||
context = req.environ['karbor.context']
|
||||
context.can(service_policy.UPDATE_POLICY)
|
||||
try:
|
||||
service = objects.Service.get_by_id(context, id)
|
||||
except exception.ServiceNotFound as e:
|
||||
raise exc.HTTPNotFound(explanation=e.message)
|
||||
|
||||
if service.binary not in SERVICES_CAN_BE_UPDATED:
|
||||
msg = (_('Updating a %(binary)s service is not supported. Only '
|
||||
'karbor-operationengine services can be updated.') %
|
||||
{'binary': service.binary})
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
if 'status' in body:
|
||||
if body['status'] == 'enabled':
|
||||
if body.get('disabled_reason'):
|
||||
msg = _("Specifying 'disabled_reason' with status "
|
||||
"'enabled' is invalid.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
service.disabled = False
|
||||
service.disabled_reason = None
|
||||
elif body['status'] == 'disabled':
|
||||
service.disabled = True
|
||||
service.disabled_reason = body.get('disabled_reason')
|
||||
service.save()
|
||||
return self._view_builder.detail(req, service)
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(ServiceController())
|
|
@ -100,6 +100,11 @@ def service_get_all_by_topic(context, topic, disabled=None):
|
|||
return IMPL.service_get_all_by_topic(context, topic, disabled=disabled)
|
||||
|
||||
|
||||
def service_get_all_by_args(context, host, binary):
|
||||
"""Get all services for a given host and binary."""
|
||||
return IMPL.service_get_all_by_args(context, host, binary)
|
||||
|
||||
|
||||
def service_get_by_args(context, host, binary):
|
||||
"""Get the state of an service by node name and binary."""
|
||||
return IMPL.service_get_by_args(context, host, binary)
|
||||
|
|
|
@ -252,6 +252,20 @@ def service_get_all(context, disabled=None):
|
|||
return query.all()
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def service_get_all_by_args(context, host, binary):
|
||||
results = model_query(
|
||||
context,
|
||||
models.Service
|
||||
)
|
||||
if host is not None:
|
||||
results = results.filter_by(host=host)
|
||||
if binary is not None:
|
||||
results = results.filter_by(binary=binary)
|
||||
|
||||
return results.all()
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def service_get_all_by_topic(context, topic, disabled=None):
|
||||
query = model_query(
|
||||
|
@ -340,7 +354,7 @@ def service_update(context, service_id, values):
|
|||
session = get_session()
|
||||
with session.begin():
|
||||
service_ref = _service_get(context, service_id, session=session)
|
||||
if ('disabled' in values):
|
||||
if 'disabled' in values:
|
||||
service_ref['modified_at'] = timeutils.utcnow()
|
||||
service_ref['updated_at'] = literal_column('updated_at')
|
||||
service_ref.update(values)
|
||||
|
|
|
@ -63,6 +63,11 @@ class Service(base.KarborPersistentObject, base.KarborObject,
|
|||
db_service = db.service_get_by_args(context, host, binary_key)
|
||||
return cls._from_db_object(context, cls(context), db_service)
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_by_id(cls, context, id):
|
||||
db_service = db.service_get(context, id)
|
||||
return cls._from_db_object(context, cls(context), db_service)
|
||||
|
||||
@base.remotable
|
||||
def create(self):
|
||||
if self.obj_attr_is_set('id'):
|
||||
|
@ -102,6 +107,12 @@ class ServiceList(base.ObjectListBase, base.KarborObject):
|
|||
return base.obj_make_list(context, cls(context), objects.Service,
|
||||
services)
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_all_by_args(cls, context, host, binary):
|
||||
services = db.service_get_all_by_args(context, host, binary)
|
||||
return base.obj_make_list(context, cls(context), objects.Service,
|
||||
services)
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get_all_by_topic(cls, context, topic, disabled=None):
|
||||
services = db.service_get_all_by_topic(context, topic,
|
||||
|
|
|
@ -21,6 +21,7 @@ from karbor.policies import protectables
|
|||
from karbor.policies import providers
|
||||
from karbor.policies import restores
|
||||
from karbor.policies import scheduled_operations
|
||||
from karbor.policies import services
|
||||
from karbor.policies import triggers
|
||||
from karbor.policies import verifications
|
||||
|
||||
|
@ -35,5 +36,6 @@ def list_rules():
|
|||
triggers.list_rules(),
|
||||
scheduled_operations.list_rules(),
|
||||
operation_logs.list_rules(),
|
||||
verifications.list_rules()
|
||||
verifications.list_rules(),
|
||||
services.list_rules(),
|
||||
)
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# Copyright (c) 2017 Huawei Technologies Co., Ltd.
|
||||
# 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 karbor.policies import base
|
||||
|
||||
GET_ALL_POLICY = 'service:get_all'
|
||||
UPDATE_POLICY = 'service:update'
|
||||
|
||||
service_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=GET_ALL_POLICY,
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='List services.',
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/os-services'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=UPDATE_POLICY,
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
description='Update service status',
|
||||
operations=[
|
||||
{
|
||||
'method': 'PUT',
|
||||
'path': '/os-services/{service_id}'
|
||||
}
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return service_policies
|
|
@ -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 mock import mock
|
||||
from webob import exc
|
||||
|
||||
from karbor.api.v1 import services
|
||||
from karbor import exception
|
||||
from karbor.tests import base
|
||||
from karbor.tests.unit.api import fakes
|
||||
|
||||
|
||||
class ServiceApiTest(base.TestCase):
|
||||
def setUp(self):
|
||||
super(ServiceApiTest, self).setUp()
|
||||
self.controller = services.ServiceController()
|
||||
|
||||
@mock.patch('karbor.objects.service.ServiceList.get_all_by_args')
|
||||
def test_service_list_with_admin_context(self, mock_get_all_by_args):
|
||||
req = fakes.HTTPRequest.blank('/v1/services?host=host1',
|
||||
use_admin_context=True)
|
||||
self.controller.index(req)
|
||||
self.assertTrue(mock_get_all_by_args.called)
|
||||
|
||||
def test_service_list_with_non_admin_context(self):
|
||||
req = fakes.HTTPRequest.blank('/v1/services', use_admin_context=False)
|
||||
self.assertRaises(
|
||||
exception.PolicyNotAuthorized, self.controller.index, req)
|
||||
|
||||
@mock.patch('karbor.utils.service_is_up')
|
||||
@mock.patch('karbor.objects.service.Service.get_by_id')
|
||||
def test_service_update_with_admin_context(
|
||||
self, mock_get_by_id, mock_service_is_up):
|
||||
req = fakes.HTTPRequest.blank('/v1/services/1', use_admin_context=True)
|
||||
body = {
|
||||
"status": 'disabled',
|
||||
'disabled_reason': 'reason'
|
||||
}
|
||||
mock_service = mock.MagicMock(
|
||||
binary='karbor-operationengine', save=mock.MagicMock())
|
||||
mock_get_by_id.return_value = mock_service
|
||||
mock_service_is_up.return_value = True
|
||||
self.controller.update(req, "fake_id", body)
|
||||
self.assertTrue(mock_get_by_id.called)
|
||||
self.assertTrue(mock_service.save.called)
|
||||
|
||||
def test_service_update_with_non_admin_context(self):
|
||||
req = fakes.HTTPRequest.blank('/v1/services/1',
|
||||
use_admin_context=False)
|
||||
body = {
|
||||
"status": 'disabled',
|
||||
'disabled_reason': 'reason'
|
||||
}
|
||||
self.assertRaises(
|
||||
exception.PolicyNotAuthorized,
|
||||
self.controller.update,
|
||||
req,
|
||||
"fake_id",
|
||||
body
|
||||
)
|
||||
|
||||
@mock.patch('karbor.objects.service.Service.get_by_id')
|
||||
def test_update_protection_services(self, mock_get_by_id):
|
||||
req = fakes.HTTPRequest.blank('/v1/services/1', use_admin_context=True)
|
||||
body = {
|
||||
"status": 'disabled',
|
||||
'disabled_reason': 'reason'
|
||||
}
|
||||
mock_service = mock.MagicMock(binary='karbor-protection')
|
||||
mock_get_by_id.return_value = mock_service
|
||||
self.assertRaises(
|
||||
exc.HTTPBadRequest,
|
||||
self.controller.update,
|
||||
req,
|
||||
"fake_id",
|
||||
body
|
||||
)
|
|
@ -98,3 +98,14 @@ class TestServiceList(test_objects.BaseObjectsTestCase):
|
|||
self.context, 'foo', disabled='bar')
|
||||
self.assertEqual(1, len(services))
|
||||
TestService._compare(self, db_service, services[0])
|
||||
|
||||
@mock.patch('karbor.db.service_get_all_by_args')
|
||||
def test_get_all_by_args(self, service_get_all_by_args):
|
||||
db_service = fake_service.fake_db_service()
|
||||
service_get_all_by_args.return_value = [db_service]
|
||||
services = objects.ServiceList.get_all_by_args(
|
||||
self.context, 'fake-host', 'fake-service')
|
||||
service_get_all_by_args.assert_called_once_with(
|
||||
self.context, 'fake-host', 'fake-service')
|
||||
self.assertEqual(1, len(services))
|
||||
TestService._compare(self, db_service, services[0])
|
||||
|
|
Loading…
Reference in New Issue