Adding API support for magnum service
These changes implement the API level support for querying magnum services. Partially-Implements: blueprint magnum-service-list Closes-bug: #1498158 Depends-On: Ia0c09222405c87cb61e5de4a43ba345ae3405b50 Change-Id: Ib816f595ba2edef29edaec40fa940570755b10aachanges/78/226078/8
parent
3674ce278d
commit
e360cf0be2
|
@ -35,6 +35,10 @@
|
|||
# (integer value)
|
||||
#periodic_interval_max = 60
|
||||
|
||||
# Max interval size between periodic tasks execution in seconds.
|
||||
# (integer value)
|
||||
#service_down_time = 180
|
||||
|
||||
# Name of this node. This can be an opaque identifier. It is not
|
||||
# necessarily a hostname, FQDN, or IP address. However, the node name
|
||||
# must be valid within an AMQP key, and if using ZeroMQ, a valid
|
||||
|
|
|
@ -54,5 +54,7 @@
|
|||
"container:update": "rule:default",
|
||||
|
||||
"certificate:create": "rule:default",
|
||||
"certificate:get": "rule:default"
|
||||
"certificate:get": "rule:default",
|
||||
|
||||
"magnum-service:get_all": "rule:admin_api"
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ from magnum.api.controllers.v1 import bay
|
|||
from magnum.api.controllers.v1 import baymodel
|
||||
from magnum.api.controllers.v1 import certificate
|
||||
from magnum.api.controllers.v1 import container
|
||||
from magnum.api.controllers.v1 import magnum_services
|
||||
from magnum.api.controllers.v1 import node
|
||||
from magnum.api.controllers.v1 import pod
|
||||
from magnum.api.controllers.v1 import replicationcontroller as rc
|
||||
|
@ -109,6 +110,9 @@ class V1(controllers_base.APIBase):
|
|||
certificates = [link.Link]
|
||||
"""Links to the certificates resource"""
|
||||
|
||||
mservices = [link.Link]
|
||||
"""Links to the magnum-services resource"""
|
||||
|
||||
@staticmethod
|
||||
def convert():
|
||||
v1 = V1()
|
||||
|
@ -170,6 +174,12 @@ class V1(controllers_base.APIBase):
|
|||
pecan.request.host_url,
|
||||
'certificates', '',
|
||||
bookmark=True)]
|
||||
v1.mservices = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'mservices', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'mservices', '',
|
||||
bookmark=True)]
|
||||
return v1
|
||||
|
||||
|
||||
|
@ -185,6 +195,7 @@ class Controller(rest.RestController):
|
|||
services = service.ServicesController()
|
||||
x509keypairs = x509keypair.X509KeyPairController()
|
||||
certificates = certificate.CertificateController()
|
||||
mservices = magnum_services.MagnumServiceController()
|
||||
|
||||
@expose.expose(V1)
|
||||
def get(self):
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import pecan
|
||||
from pecan import rest
|
||||
import wsme
|
||||
from wsme import types as wtypes
|
||||
|
||||
from magnum.api.controllers import base
|
||||
from magnum.api.controllers.v1 import collection
|
||||
from magnum.api import expose
|
||||
from magnum.api import servicegroup as svcgrp_api
|
||||
from magnum.common import policy
|
||||
from magnum import objects
|
||||
|
||||
|
||||
class MagnumService(base.APIBase):
|
||||
|
||||
host = wtypes.StringType(min_length=1, max_length=255)
|
||||
"""Name of the host """
|
||||
|
||||
binary = wtypes.StringType(min_length=1, max_length=255)
|
||||
"""Name of the binary"""
|
||||
|
||||
state = wtypes.StringType(min_length=1, max_length=255)
|
||||
"""State of the binary"""
|
||||
|
||||
id = wsme.wsattr(wtypes.IntegerType(minimum=1))
|
||||
"""The id for the healthcheck record """
|
||||
|
||||
report_count = wsme.wsattr(wtypes.IntegerType(minimum=0))
|
||||
"""The number of times the heartbeat was reported """
|
||||
|
||||
# disabled = wsme.wsattr(wtypes.BoolType(default=False))
|
||||
"""If the service is 'disabled' administratively """
|
||||
|
||||
disabled_reason = wtypes.StringType(min_length=0, max_length=255)
|
||||
"""Reason for disabling """
|
||||
|
||||
def __init__(self, state, **kwargs):
|
||||
super(MagnumService, self).__init__()
|
||||
|
||||
self.fields = ['state']
|
||||
setattr(self, 'state', state)
|
||||
for field in objects.MagnumService.fields:
|
||||
self.fields.append(field)
|
||||
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
||||
|
||||
|
||||
class MagnumServiceCollection(collection.Collection):
|
||||
|
||||
mservices = [MagnumService]
|
||||
"""A list containing bays objects"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(MagnumServiceCollection, self).__init__()
|
||||
self._type = 'mservices'
|
||||
|
||||
@staticmethod
|
||||
def convert_db_rec_list_to_collection(servicegroup_api,
|
||||
rpc_msvcs, **kwargs):
|
||||
collection = MagnumServiceCollection()
|
||||
collection.mservices = []
|
||||
for p in rpc_msvcs:
|
||||
alive = servicegroup_api.service_is_up(p)
|
||||
state = 'up' if alive else 'down'
|
||||
msvc = MagnumService(state, **p.as_dict())
|
||||
collection.mservices.append(msvc)
|
||||
collection.next = collection.get_next(limit=None, url=None, **kwargs)
|
||||
return collection
|
||||
|
||||
|
||||
class MagnumServiceController(rest.RestController):
|
||||
"""REST controller for magnum-services."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(MagnumServiceController, self).__init__()
|
||||
self.servicegroup_api = svcgrp_api.ServiceGroup()
|
||||
|
||||
@policy.enforce_wsgi("magnum-service", "get_all")
|
||||
@expose.expose(MagnumServiceCollection)
|
||||
def get_all(self):
|
||||
"""Retrieve a list of magnum-services.
|
||||
|
||||
"""
|
||||
msvcs = pecan.request.rpcapi.magnum_services_list(
|
||||
pecan.request.context, limit=None,
|
||||
marker=None, sort_key='id',
|
||||
sort_dir='asc')
|
||||
return MagnumServiceCollection.convert_db_rec_list_to_collection(
|
||||
self.servicegroup_api, msvcs)
|
|
@ -11,14 +11,25 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import timeutils
|
||||
|
||||
from magnum.db.sqlalchemy import models
|
||||
|
||||
periodic_opts = [
|
||||
cfg.IntOpt('service_down_time',
|
||||
default=180,
|
||||
help='Max interval size between periodic tasks execution in '
|
||||
'seconds.'),
|
||||
]
|
||||
|
||||
class API(object):
|
||||
def __init__(self, conf):
|
||||
self.service_down_time = 3 * conf.periodic_interval_max
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(periodic_opts)
|
||||
|
||||
|
||||
class ServiceGroup(object):
|
||||
def __init__(self):
|
||||
self.service_down_time = CONF.service_down_time
|
||||
|
||||
def service_is_up(self, member):
|
||||
if not isinstance(member, models.MagnumService):
|
||||
|
@ -28,6 +39,7 @@ class API(object):
|
|||
|
||||
last_heartbeat = (member.get(
|
||||
'last_seen_up') or member['updated_at'] or member['created_at'])
|
||||
elapsed = timeutils.delta_seconds(last_heartbeat, timeutils.utcnow())
|
||||
now = timeutils.utcnow(True)
|
||||
elapsed = timeutils.delta_seconds(last_heartbeat, now)
|
||||
is_up = abs(elapsed) <= self.service_down_time
|
||||
return is_up
|
|
@ -171,6 +171,11 @@ class API(rpc_service.API):
|
|||
def get_ca_certificate(self, bay):
|
||||
return self._call('get_ca_certificate', bay=bay)
|
||||
|
||||
# magnum-services
|
||||
def magnum_services_list(self, context, limit, marker, sort_key, sort_dir):
|
||||
return objects.MagnumService.list(context, limit, marker, sort_key,
|
||||
sort_dir)
|
||||
|
||||
# Versioned Objects indirection API
|
||||
|
||||
def object_class_action(self, context, objname, objmethod, objver,
|
||||
|
|
|
@ -770,7 +770,7 @@ class Connection(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_magnum_service_list(self, context, filters=None, limit=None,
|
||||
def get_magnum_service_list(self, context, disabled=None, limit=None,
|
||||
marker=None, sort_key=None, sort_dir=None):
|
||||
"""Get matching magnum_service records.
|
||||
|
||||
|
@ -778,7 +778,7 @@ class Connection(object):
|
|||
those match the specified filters.
|
||||
|
||||
:param context: The security context
|
||||
:param filters: Filters to apply. Defaults to None.
|
||||
:param disabled: Filters disbaled services. Defaults to None.
|
||||
:param limit: Maximum number of magnum_services to return.
|
||||
:param marker: the last item of the previous page; we return the next
|
||||
result set.
|
||||
|
|
|
@ -77,7 +77,11 @@ class TestRootController(api_base.FunctionalTest):
|
|||
u'certificates': [{u'href': u'http://localhost/v1/certificates/',
|
||||
u'rel': u'self'},
|
||||
{u'href': u'http://localhost/certificates/',
|
||||
u'rel': u'bookmark'}]}
|
||||
u'rel': u'bookmark'}],
|
||||
u'mservices': [{u'href': u'http://localhost/v1/mservices/',
|
||||
u'rel': u'self'},
|
||||
{u'href': u'http://localhost/mservices/',
|
||||
u'rel': u'bookmark'}]}
|
||||
|
||||
response = self.app.get('/v1/')
|
||||
self.assertEqual(expected, response.json)
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
import mock
|
||||
|
||||
from magnum.api.controllers.v1 import magnum_services as mservice
|
||||
from magnum.api import servicegroup as servicegroup
|
||||
from magnum.conductor import api as rpcapi
|
||||
from magnum.tests import base
|
||||
from magnum.tests.unit.api import base as api_base
|
||||
from magnum.tests.unit.api import utils as apiutils
|
||||
|
||||
|
||||
class TestMagnumServiceObject(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestMagnumServiceObject, self).setUp()
|
||||
self.rpc_dict = apiutils.mservice_get_data()
|
||||
|
||||
def test_msvc_obj_fields_filtering(self):
|
||||
"""Test that it does filtering fields """
|
||||
self.rpc_dict['fake-key'] = 'fake-value'
|
||||
msvco = mservice.MagnumService("up", **self.rpc_dict)
|
||||
self.assertNotIn('fake-key', msvco.fields)
|
||||
|
||||
|
||||
class db_rec(object):
|
||||
|
||||
def __init__(self, d):
|
||||
self.rec_as_dict = d
|
||||
|
||||
def as_dict(self):
|
||||
return self.rec_as_dict
|
||||
|
||||
|
||||
class TestMagnumServiceController(api_base.FunctionalTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestMagnumServiceController, self).setUp()
|
||||
|
||||
def test_empty(self):
|
||||
response = self.get_json('/mservices')
|
||||
self.assertEqual([], response['mservices'])
|
||||
|
||||
def _rpc_api_reply(self, count=1):
|
||||
reclist = []
|
||||
for i in range(count):
|
||||
elem = apiutils.mservice_get_data()
|
||||
elem['id'] = i + 1
|
||||
rec = db_rec(elem)
|
||||
reclist.append(rec)
|
||||
return reclist
|
||||
|
||||
@mock.patch.object(rpcapi.API, 'magnum_services_list')
|
||||
@mock.patch.object(servicegroup.ServiceGroup, 'service_is_up')
|
||||
def test_get_one(self, svc_up, rpc_patcher):
|
||||
rpc_patcher.return_value = self._rpc_api_reply()
|
||||
svc_up.return_value = "up"
|
||||
|
||||
response = self.get_json('/mservices')
|
||||
self.assertEqual(len(response['mservices']), 1)
|
||||
self.assertEqual(response['mservices'][0]['id'], 1)
|
||||
|
||||
@mock.patch.object(rpcapi.API, 'magnum_services_list')
|
||||
@mock.patch.object(servicegroup.ServiceGroup, 'service_is_up')
|
||||
def test_get_many(self, svc_up, rpc_patcher):
|
||||
svc_num = 5
|
||||
rpc_patcher.return_value = self._rpc_api_reply(svc_num)
|
||||
svc_up.return_value = "up"
|
||||
|
||||
response = self.get_json('/mservices')
|
||||
self.assertEqual(len(response['mservices']), svc_num)
|
||||
for i in range(svc_num):
|
||||
elem = response['mservices'][i]
|
||||
self.assertEqual(elem['id'], i + 1)
|
|
@ -12,6 +12,8 @@
|
|||
"""
|
||||
Utils for testing the API service.
|
||||
"""
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
from magnum.api.controllers.v1 import bay as bay_controller
|
||||
from magnum.api.controllers.v1 import baymodel as baymodel_controller
|
||||
|
@ -148,3 +150,20 @@ def x509keypair_post_data(**kw):
|
|||
x509keypair = utils.get_test_x509keypair(**kw)
|
||||
internal = x509keypair_controller.X509KeyPairPatchType.internal_attrs()
|
||||
return remove_internal(x509keypair, internal)
|
||||
|
||||
|
||||
def mservice_get_data(**kw):
|
||||
"""Simulate what the RPC layer will get from DB """
|
||||
faketime = datetime.datetime(2001, 1, 1, tzinfo=pytz.UTC)
|
||||
return {
|
||||
'binary': kw.get('binary', 'fake-binary'),
|
||||
'host': kw.get('host', 'fake-host'),
|
||||
'id': kw.get('id', '13'),
|
||||
'report_count': kw.get('report_count', '13'),
|
||||
'disabled': kw.get('disabled', False),
|
||||
'disabled_reason': kw.get('disabled_reason', None),
|
||||
'forced_down': kw.get('forced_down', False),
|
||||
'last_seen_at': kw.get('last_seen_at', faketime),
|
||||
'created_at': kw.get('created_at', faketime),
|
||||
'updated_at': kw.get('updated_at', faketime),
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue