Merge "Add service management API to Karbor"

This commit is contained in:
Zuul 2017-10-25 06:24:06 +00:00 committed by Gerrit Code Review
commit 7b2cb7c405
9 changed files with 311 additions and 2 deletions

View File

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

128
karbor/api/v1/services.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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(),
)

View File

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

View File

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

View File

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