diff --git a/etc/magnum/magnum.conf.sample b/etc/magnum/magnum.conf.sample index ea088efed1..f60a4f2d9b 100644 --- a/etc/magnum/magnum.conf.sample +++ b/etc/magnum/magnum.conf.sample @@ -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 diff --git a/etc/magnum/policy.json b/etc/magnum/policy.json index 24f0425a7f..8a534f1079 100644 --- a/etc/magnum/policy.json +++ b/etc/magnum/policy.json @@ -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" } diff --git a/magnum/api/controllers/v1/__init__.py b/magnum/api/controllers/v1/__init__.py index 9364872fe9..5b79048baa 100644 --- a/magnum/api/controllers/v1/__init__.py +++ b/magnum/api/controllers/v1/__init__.py @@ -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): diff --git a/magnum/api/controllers/v1/magnum_services.py b/magnum/api/controllers/v1/magnum_services.py new file mode 100644 index 0000000000..4019fb8f07 --- /dev/null +++ b/magnum/api/controllers/v1/magnum_services.py @@ -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) diff --git a/magnum/servicegroup/api.py b/magnum/api/servicegroup.py similarity index 67% rename from magnum/servicegroup/api.py rename to magnum/api/servicegroup.py index f2d06c8370..4058621e22 100644 --- a/magnum/servicegroup/api.py +++ b/magnum/api/servicegroup.py @@ -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 diff --git a/magnum/conductor/api.py b/magnum/conductor/api.py index a4641a0452..360704809d 100644 --- a/magnum/conductor/api.py +++ b/magnum/conductor/api.py @@ -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, diff --git a/magnum/db/api.py b/magnum/db/api.py index 7181de1671..660f076838 100644 --- a/magnum/db/api.py +++ b/magnum/db/api.py @@ -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. diff --git a/magnum/tests/unit/api/controllers/test_root.py b/magnum/tests/unit/api/controllers/test_root.py index 5f5a205c0f..d1a1aa86b2 100644 --- a/magnum/tests/unit/api/controllers/test_root.py +++ b/magnum/tests/unit/api/controllers/test_root.py @@ -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) diff --git a/magnum/tests/unit/api/controllers/v1/test_magnum_service.py b/magnum/tests/unit/api/controllers/v1/test_magnum_service.py new file mode 100644 index 0000000000..ee3d56f877 --- /dev/null +++ b/magnum/tests/unit/api/controllers/v1/test_magnum_service.py @@ -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) diff --git a/magnum/tests/unit/api/utils.py b/magnum/tests/unit/api/utils.py index 74b14eec5e..57c932dab7 100644 --- a/magnum/tests/unit/api/utils.py +++ b/magnum/tests/unit/api/utils.py @@ -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), + }