Add support for getting Service Status

Change-Id: Iaf10d8486ac8015ecf9f394dfbf074bfb863fb78
This commit is contained in:
Endre Karlson 2016-02-15 16:46:55 +01:00
parent b5448804c0
commit 7abae80c61
27 changed files with 981 additions and 5 deletions

View File

@ -21,6 +21,7 @@ from designate.i18n import _LI
from designate import exceptions
from designate import utils
from designate import service
from designate import service_status
LOG = logging.getLogger(__name__)
@ -30,6 +31,23 @@ class Service(service.WSGIService, service.Service):
def __init__(self, threads=None):
super(Service, self).__init__(threads=threads)
emitter_cls = service_status.HeartBeatEmitter.get_driver(
cfg.CONF.heartbeat_emitter.emitter_type
)
self.heartbeat_emitter = emitter_cls(
self.service_name, self.tg, status_factory=self._get_status
)
def start(self):
super(Service, self).start()
self.heartbeat_emitter.start()
def _get_status(self):
status = "UP"
stats = {}
capabilities = {}
return status, stats, capabilities
@property
def service_name(self):
return 'api'

View File

@ -23,6 +23,7 @@ from designate.api.v2.controllers import tlds
from designate.api.v2.controllers import blacklists
from designate.api.v2.controllers import errors
from designate.api.v2.controllers import pools
from designate.api.v2.controllers import service_status
from designate.api.v2.controllers import zones
from designate.api.v2.controllers import tsigkeys
@ -57,4 +58,5 @@ class RootController(object):
blacklists = blacklists.BlacklistsController()
errors = errors.ErrorsController()
pools = pools.PoolsController()
service_statuses = service_status.ServiceStatusController()
tsigkeys = tsigkeys.TsigKeysController()

View File

@ -0,0 +1,61 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# 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 oslo_log import log as logging
from designate import utils
from designate.api.v2.controllers import rest
from designate.objects.adapters import DesignateAdapter
LOG = logging.getLogger(__name__)
class ServiceStatusController(rest.RestController):
SORT_KEYS = ['created_at', 'id', 'updated_at', 'hostname', 'service_name',
'status']
@pecan.expose(template='json:', content_type='application/json')
def get_all(self, **params):
request = pecan.request
context = pecan.request.environ['context']
marker, limit, sort_key, sort_dir = utils.get_paging_params(
params, self.SORT_KEYS)
accepted_filters = ["hostname", "service_name", "status"]
criterion = self._apply_filter_params(
params, accepted_filters, {})
service_statuses = self.central_api.find_service_statuses(
context, criterion, )
return DesignateAdapter.render(
'API_v2',
service_statuses,
request=request)
@pecan.expose(template='json:', content_type='application/json')
@utils.validate_uuid('service_id')
def get_one(self, service_id):
"""Get Service Status"""
request = pecan.request
context = request.environ['context']
criterion = {"id": service_id}
service_status = self.central_api.find_service_status(
context, criterion)
return DesignateAdapter.render(
'API_v2', service_status, request=request)

View File

@ -54,14 +54,15 @@ class CentralAPI(object):
5.5 - Add deleted zone purging task
5.6 - Changed 'purge_zones' function args
6.0 - Renamed domains to zones
6.1 - Add ServiceStatus methods
"""
RPC_API_VERSION = '6.0'
RPC_API_VERSION = '6.1'
def __init__(self, topic=None):
topic = topic if topic else cfg.CONF.central_topic
target = messaging.Target(topic=topic, version=self.RPC_API_VERSION)
self.client = rpc.get_client(target, version_cap='6.0')
self.client = rpc.get_client(target, version_cap='6.1')
@classmethod
def get_instance(cls):
@ -578,3 +579,24 @@ class CentralAPI(object):
"delete_zone_export."))
return self.client.call(context, 'delete_zone_export',
zone_export_id=zone_export_id)
def find_service_status(self, context, criterion=None):
LOG.info(_LI("find_service_status: Calling central's "
"find_service_status."))
return self.client.call(context, 'find_service_status',
criterion=criterion)
def find_service_statuses(self, context, criterion=None, marker=None,
limit=None, sort_key=None, sort_dir=None):
LOG.info(_LI("find_service_statuses: Calling central's "
"find_service_statuses."))
return self.client.call(context, 'find_service_statuses',
criterion=criterion, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir)
def update_service_status(self, context, service_status):
LOG.info(_LI("update_service_status: Calling central's "
"update_service_status."))
self.client.cast(context, 'update_service_status',
service_status=service_status)

View File

@ -263,7 +263,7 @@ def notification(notification_type):
class Service(service.RPCService, service.Service):
RPC_API_VERSION = '6.0'
RPC_API_VERSION = '6.1'
target = messaging.Target(version=RPC_API_VERSION)
@ -272,6 +272,10 @@ class Service(service.RPCService, service.Service):
self.network_api = network_api.get_network_api(cfg.CONF.network_api)
# update_service_status needs is called by the emitter so we pass
# ourselves as the rpc_api.
self.heartbeat_emitter.rpc_api = self
@property
def scheduler(self):
if not hasattr(self, '_scheduler'):
@ -2880,3 +2884,38 @@ class Service(service.RPCService, service.Service):
zone_export = self.storage.delete_zone_export(context, zone_export_id)
return zone_export
def find_service_statuses(self, context, criterion=None, marker=None,
limit=None, sort_key=None, sort_dir=None):
"""List service statuses.
"""
policy.check('find_service_statuses', context)
return self.storage.find_service_statuses(
context, criterion, marker, limit, sort_key, sort_dir)
def find_service_status(self, context, criterion=None):
policy.check('find_service_status', context)
return self.storage.find_service_status(context, criterion)
def update_service_status(self, context, service_status):
policy.check('update_service_status', context)
criterion = {
"service_name": service_status.service_name,
"hostname": service_status.hostname
}
if service_status.obj_attr_is_set('id'):
criterion["id"] = service_status.id
try:
db_status = self.storage.find_service_status(
context, criterion)
db_status.update(dict(service_status))
return self.storage.update_service_status(context, db_status)
except exceptions.ServiceStatusNotFound:
return self.storage.create_service_status(
context, service_status)

View File

@ -247,6 +247,10 @@ class Duplicate(Base):
error_type = 'duplicate'
class DuplicateServiceStatus(Duplicate):
error_type = 'duplicate_service_status'
class DuplicateQuota(Duplicate):
error_type = 'duplicate_quota'
@ -351,6 +355,10 @@ class NotFound(Base):
error_type = 'not_found'
class ServiceStatusNotFound(NotFound):
error_type = 'service_status_not_found'
class QuotaNotFound(NotFound):
error_type = 'quota_not_found'

View File

@ -34,6 +34,7 @@ from designate.objects.quota import Quota, QuotaList # noqa
from designate.objects.record import Record, RecordList # noqa
from designate.objects.recordset import RecordSet, RecordSetList # noqa
from designate.objects.server import Server, ServerList # noqa
from designate.objects.service_status import ServiceStatus, ServiceStatusList # noqa
from designate.objects.tenant import Tenant, TenantList # noqa
from designate.objects.tld import Tld, TldList # noqa
from designate.objects.tsigkey import TsigKey, TsigKeyList # noqa

View File

@ -27,6 +27,7 @@ from designate.objects.adapters.api_v2.pool_ns_record import PoolNsRecordAPIv2Ad
from designate.objects.adapters.api_v2.tld import TldAPIv2Adapter, TldListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.tsigkey import TsigKeyAPIv2Adapter, TsigKeyListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.quota import QuotaAPIv2Adapter, QuotaListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.service_status import ServiceStatusAPIv2Adapter, ServiceStatusListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.zone_transfer_accept import ZoneTransferAcceptAPIv2Adapter, ZoneTransferAcceptListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.zone_transfer_request import ZoneTransferRequestAPIv2Adapter, ZoneTransferRequestListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.validation_error import ValidationErrorAPIv2Adapter, ValidationErrorListAPIv2Adapter # noqa

View File

@ -0,0 +1,65 @@
# Copyright 2016 Hewlett-Packard Development Company, L.P.
#
# 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_log import log as logging
from designate.objects.adapters.api_v2 import base
from designate import objects
LOG = logging.getLogger(__name__)
class ServiceStatusAPIv2Adapter(base.APIv2Adapter):
ADAPTER_OBJECT = objects.ServiceStatus
MODIFICATIONS = {
'fields': {
"id": {},
"hostname": {},
"service_name": {},
"status": {},
"stats": {},
"capabilities": {},
"heartbeated_at": {},
"created_at": {},
"updated_at": {},
},
'options': {
'links': True,
'resource_name': 'service_status',
'collection_name': 'service_statuses',
}
}
@classmethod
def _render_object(cls, object, *args, **kwargs):
obj = super(ServiceStatusAPIv2Adapter, cls)._render_object(
object, *args, **kwargs)
obj['links']['self'] = \
'%s/v2/%s/%s' % (cls.BASE_URI, 'service_statuses', obj['id'])
return obj
class ServiceStatusListAPIv2Adapter(base.APIv2Adapter):
ADAPTER_OBJECT = objects.ServiceStatusList
MODIFICATIONS = {
'options': {
'links': True,
'resource_name': 'service_status',
'collection_name': 'service_statuses',
}
}

View File

@ -18,6 +18,7 @@ import six
from six.moves.urllib import parse
import jsonschema
from oslo_log import log as logging
from oslo_utils import timeutils
from designate import exceptions
from designate.schema import validators
@ -179,6 +180,11 @@ class DesignateObject(object):
if isinstance(value, dict) and 'designate_object.name' in value:
setattr(instance, field, DesignateObject.from_primitive(value))
else:
# data typically doesn't have a schema..
schema = cls.FIELDS[field].get("schema", None)
if schema is not None and value is not None:
if "format" in schema and schema["format"] == "date-time":
value = timeutils.parse_strtime(value)
setattr(instance, field, value)
instance._obj_changes = set(primitive['designate_object.changes'])

View File

@ -0,0 +1,61 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# 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 designate.objects import base
class ServiceStatus(base.PersistentObjectMixin,
base.DictObjectMixin,
base.DesignateObject):
FIELDS = {
"service_name": {
"schema": {
"type": "string"
}
},
"hostname": {
"schema": {
"type": "string"
}
},
"heartbeated_at": {
"schema": {
'type': ['string', 'null'],
'format': 'date-time'
}
},
"status": {
"schema": {
"type": "string",
"enum": ["UP", "DOWN", "WARNING"]
}
},
"stats": {
"schema": {
"type": "object",
}
},
"capabilities": {
"schema": {
"type": "object"
}
}
}
STRING_FIELDS = [
'service_name', 'hostname', 'status'
]
class ServiceStatusList(base.ListObjectMixin, base.DesignateObject):
LIST_ITEM_TYPE = ServiceStatus

View File

@ -35,11 +35,13 @@ from designate.i18n import _
from designate.i18n import _LE
from designate.i18n import _LI
from designate.i18n import _LW
from designate import rpc
from designate import policy
from designate import rpc
from designate import service_status
from designate import version
from designate import utils
# TODO(kiall): These options have been cut+paste from the old WSGI code, and
# should be moved into service:api etc..
wsgi_socket_opts = [
@ -108,6 +110,19 @@ class RPCService(object):
messaging.Target(topic=self._rpc_topic, server=self._host),
self._rpc_endpoints)
emitter_cls = service_status.HeartBeatEmitter.get_driver(
cfg.CONF.heartbeat_emitter.emitter_type
)
self.heartbeat_emitter = emitter_cls(
self.service_name, self.tg, status_factory=self._get_status
)
def _get_status(self):
status = "UP"
stats = {}
capabilities = {}
return status, stats, capabilities
@property
def _rpc_endpoints(self):
return [self]
@ -130,8 +145,11 @@ class RPCService(object):
if e != self and hasattr(e, 'start'):
e.start()
self.heartbeat_emitter.start()
def stop(self):
LOG.debug("Stopping RPC server on topic '%s'" % self._rpc_topic)
self.heartbeat_emitter.stop()
for e in self._rpc_endpoints:
if e != self and hasattr(e, 'stop'):

114
designate/service_status.py Normal file
View File

@ -0,0 +1,114 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# 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 abc
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import timeutils
from designate import context
from designate import objects
from designate import plugin
from designate.central import rpcapi as central_rpcapi
heartbeat_opts = [
cfg.FloatOpt('heartbeat_interval',
default=5.0,
help='Number of seconds between heartbeats for reporting '
'state'),
cfg.StrOpt('emitter_type', default="rpc", help="Emitter to use")
]
CONF = cfg.CONF
CONF.register_opts(heartbeat_opts, group="heartbeat_emitter")
LOG = logging.getLogger(__name__)
class HeartBeatEmitter(plugin.DriverPlugin):
__plugin_ns__ = 'designate.heartbeat_emitter'
__plugin_type__ = 'heartbeat_emitter'
def __init__(self, service, threadgroup, status_factory=None):
super(HeartBeatEmitter, self).__init__()
self._service = service
self._hostname = CONF.host
self._running = False
self._tg = threadgroup
self._tg.add_timer(
cfg.CONF.heartbeat_emitter.heartbeat_interval,
self._emit_heartbeat)
self._status_factory = status_factory
def _get_status(self):
if self._status_factory is not None:
return self._status_factory()
return True, {}, {}
def _emit_heartbeat(self):
"""
Returns Status, Stats, Capabilities
"""
if not self._running:
return
LOG.debug("Emitting heartbeat...")
status, stats, capabilities = self._get_status()
service_status = objects.ServiceStatus(
service_name=self._service,
hostname=self._hostname,
status=status,
stats=stats,
capabilities=capabilities,
heartbeated_at=timeutils.utcnow()
)
self._transmit(service_status)
@abc.abstractmethod
def _transmit(self, status):
pass
def start(self):
self._running = True
def stop(self):
self._running = False
class NoopEmitter(HeartBeatEmitter):
__plugin_name__ = 'noop'
def _transmit(self, status):
LOG.debug(status)
class RpcEmitter(HeartBeatEmitter):
__plugin_name__ = 'rpc'
def __init__(self, service, thread_group, rpc_api=None, *args, **kwargs):
super(RpcEmitter, self).__init__(
service, thread_group, *args, **kwargs)
self.rpc_api = rpc_api
def _transmit(self, status):
admin_context = context.DesignateContext.get_admin_context()
api = self.rpc_api or central_rpcapi.CentralAPI.get_instance()
api.update_service_status(admin_context, status)

View File

@ -766,3 +766,37 @@ class Storage(DriverPlugin):
return {
'status': None
}
@abc.abstractmethod
def find_service_statuses(self, context, criterion=None, marker=None,
limit=None, sort_key=None, sort_dir=None):
"""
Retrieve status for services
:param context: RPC Context.
:param criterion: Criteria to filter by.
:param marker: Resource ID from which after the requested page will
start after
:param limit: Integer limit of objects of the page size after the
marker
:param sort_key: Key from which to sort after.
:param sort_dir: Direction to sort after using sort_key.
"""
@abc.abstractmethod
def find_service_status(self, context, criterion):
"""
Find a single Service Status.
:param context: RPC Context.
:param criterion: Criteria to filter by.
"""
@abc.abstractmethod
def update_service_status(self, context, service_status):
"""
Update the Service status for a service.
:param context: RPC Context.
:param service_status: Set the status for a service.
"""

View File

@ -1922,6 +1922,35 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
return result[0]
# Service Status Methods
def _find_service_statuses(self, context, criterion, one=False,
marker=None, limit=None, sort_key=None,
sort_dir=None):
return self._find(
context, tables.service_status, objects.ServiceStatus,
objects.ServiceStatusList, exceptions.ServiceStatusNotFound,
criterion, one, marker, limit, sort_key, sort_dir)
def find_service_status(self, context, criterion):
return self._find_service_statuses(context, criterion, one=True)
def find_service_statuses(self, context, criterion=None, marker=None,
limit=None, sort_key=None, sort_dir=None):
return self._find_service_statuses(context, criterion, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir)
def create_service_status(self, context, service_status):
return self._create(
tables.service_status, service_status,
exceptions.DuplicateServiceStatus)
def update_service_status(self, context, service_status):
return self._update(
context, tables.service_status, service_status,
exceptions.DuplicateServiceStatus,
exceptions.ServiceStatusNotFound)
# diagnostics
def ping(self, context):
start_time = time.time()

View File

@ -0,0 +1,52 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# 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.
"""Add Service Status tables"""
from oslo_log import log as logging
from sqlalchemy import String, DateTime, Enum, Text
from sqlalchemy.schema import Table, Column, MetaData
from designate import utils
from designate.sqlalchemy.types import UUID
LOG = logging.getLogger()
meta = MetaData()
SERVICE_STATES = [
"UP", "DOWN", "WARNING"
]
def upgrade(migrate_engine):
meta.bind = migrate_engine
status_enum = Enum(name='service_statuses', metadata=meta, *SERVICE_STATES)
status_enum.create()
service_status_table = Table('service_statuses', meta,
Column('id', UUID(), default=utils.generate_uuid, primary_key=True),
Column('created_at', DateTime),
Column('updated_at', DateTime),
Column('service_name', String(40), nullable=False),
Column('hostname', String(255), nullable=False),
Column('heartbeated_at', DateTime, nullable=True),
Column('status', status_enum, nullable=False),
Column('stats', Text, nullable=False),
Column('capabilities', Text, nullable=False),
)
service_status_table.create()

View File

@ -18,6 +18,7 @@ from sqlalchemy import (Table, MetaData, Column, String, Text, Integer,
UniqueConstraint, ForeignKeyConstraint)
from oslo_config import cfg
from oslo_db.sqlalchemy import types
from oslo_utils import timeutils
from designate import utils
@ -39,6 +40,9 @@ ACTIONS = ['CREATE', 'DELETE', 'UPDATE', 'NONE']
ZONE_TYPES = ('PRIMARY', 'SECONDARY',)
ZONE_TASK_TYPES = ['IMPORT', 'EXPORT']
SERVICE_STATES = [
"UP", "DOWN", "WARNING"
]
metadata = MetaData()
@ -51,6 +55,25 @@ def default_shard(context, id_col):
return int(context.current_parameters[id_col][0:3], 16)
service_status = Table("service_statuses", metadata,
Column('id', UUID, default=utils.generate_uuid, primary_key=True),
Column('created_at', DateTime, default=lambda: timeutils.utcnow()),
Column('updated_at', DateTime, onupdate=lambda: timeutils.utcnow()),
Column('service_name', String(40), nullable=False),
Column('hostname', String(255), nullable=False),
Column('heartbeated_at', DateTime, nullable=True),
Column('status', Enum(name='service_statuses', *SERVICE_STATES),
nullable=False),
Column('stats', types.JsonEncodedDict, nullable=False),
Column('capabilities', types.JsonEncodedDict, nullable=False),
mysql_engine='InnoDB',
mysql_charset='utf8',
)
quotas = Table('quotas', metadata,
Column('id', UUID, default=utils.generate_uuid, primary_key=True),
Column('version', Integer, default=1, nullable=False),

View File

@ -62,6 +62,14 @@ class TestTimeoutError(Exception):
class TestCase(base.BaseTestCase):
service_status_fixtures = [{
'service_name': 'foo',
'hostname': 'bar',
'status': "UP",
'stats': {},
'capabilities': {},
}]
quota_fixtures = [{
'resource': 'zones',
'hard_limit': 5,
@ -320,6 +328,11 @@ class TestCase(base.BaseTestCase):
group='service:central'
)
self.config(
emitter_type="noop",
group="heartbeat_emitter"
)
self.config(
auth_strategy='noauth',
group='service:api'
@ -604,6 +617,23 @@ class TestCase(base.BaseTestCase):
_values.update(values)
return _values
def get_service_status_fixture(self, fixture=0, values=None):
values = values or {}
_values = copy.copy(self.service_status_fixtures[fixture])
_values.update(values)
return _values
def update_service_status(self, **kwargs):
context = kwargs.pop('context', self.admin_context)
fixture = kwargs.pop('fixture', 0)
values = self.get_service_status_fixture(
fixture=fixture, values=kwargs)
return self.central_service.update_service_status(
context, objects.ServiceStatus.from_dict(values))
def create_tld(self, **kwargs):
context = kwargs.pop('context', self.admin_context)
fixture = kwargs.pop('fixture', 0)

View File

@ -0,0 +1,84 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# 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_log import log as logging
from designate.tests.test_api.test_v2 import ApiV2TestCase
LOG = logging.getLogger(__name__)
class ApiV2ServiceStatusTest(ApiV2TestCase):
def setUp(self):
super(ApiV2ServiceStatusTest, self).setUp()
def test_get_service_statuses(self):
# Set the policy file as this is an admin-only API
self.policy({'find_service_statuses': '@'})
response = self.client.get('/service_statuses/')
# Check the headers are what we expect
self.assertEqual(200, response.status_int)
self.assertEqual('application/json', response.content_type)
# Check the body structure is what we expect
self.assertIn('service_statuses', response.json)
self.assertIn('links', response.json)
self.assertIn('self', response.json['links'])
# Test with 0 service_statuses
# Seeing that Central is started there will be 1 here already..
self.assertEqual(0, len(response.json['service_statuses']))
data = [self.update_service_status(
hostname="foo%s" % i, service_name="bar") for i in range(0, 10)]
self._assert_paging(data, '/service_statuses', key='service_statuses')
def test_get_service_status(self):
service_status = self.update_service_status(fixture=0)
# Set the policy file as this is an admin-only API
self.policy({'find_service_status': '@'})
response = self.client.get(
'/service_statuses/%s' % service_status['id'],
headers=[('Accept', 'application/json')])
# Verify the headers
self.assertEqual(200, response.status_int)
self.assertEqual('application/json', response.content_type)
# Verify the body structure
self.assertIn('links', response.json)
self.assertIn('self', response.json['links'])
# Verify the returned values
self.assertIn('id', response.json)
self.assertIn('created_at', response.json)
self.assertIsNone(response.json['updated_at'])
fixture = self.get_service_status_fixture(0)
self.assertEqual(fixture['hostname'], response.json['hostname'])
self.assertEqual(fixture['service_name'],
response.json['service_name'])
self.assertEqual(fixture['capabilities'],
response.json['capabilities'])
self.assertEqual(fixture['stats'], response.json['stats'])
self.assertEqual(fixture['status'], response.json['status'])
self.assertEqual(None, response.json['heartbeated_at'])
def test_get_service_status_invalid_id(self):
self.policy({'find_service_status': '@'})
self._assert_invalid_uuid(self.client.get, '/service_statuses/%s')

View File

@ -52,6 +52,7 @@ class SqlalchemyStorageTest(StorageTestCase, TestCase):
u'quotas',
u'records',
u'recordsets',
u'service_statuses',
u'tlds',
u'tsigkeys',
u'zone_attributes',

View File

@ -0,0 +1,105 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# 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
import oslotest.base
from oslo_config import cfg
from designate import objects
from designate import service_status
class NoopEmitterTest(oslotest.base.BaseTestCase):
def setUp(self):
super(NoopEmitterTest, self).setUp()
self.mock_tg = mock.Mock()
def test_init(self):
service_status.NoopEmitter("svc", self.mock_tg)
def test_start(self):
emitter = service_status.NoopEmitter("svc", self.mock_tg)
emitter.start()
self.mock_tg.add_timer.assert_called_once_with(
5.0, emitter._emit_heartbeat)
def test_stop(self):
mock_pulse = mock.Mock()
self.mock_tg.add_timer.return_value = mock_pulse
emitter = service_status.NoopEmitter("svc", self.mock_tg)
emitter.start()
emitter.stop()
self.assertFalse(emitter._running)
class RpcEmitterTest(oslotest.base.BaseTestCase):
def setUp(self):
super(RpcEmitterTest, self).setUp()
self.mock_tg = mock.Mock()
@mock.patch.object(objects, "ServiceStatus")
@mock.patch("designate.context.DesignateContext.get_admin_context")
def test_emit_no_status_factory(self, mock_context, mock_service_status):
emitter = service_status.RpcEmitter("svc", self.mock_tg)
emitter.start()
central = mock.Mock()
with mock.patch("designate.central.rpcapi.CentralAPI.get_instance",
return_value=central):
emitter._emit_heartbeat()
mock_service_status.assert_called_once_with(
service_name="svc",
hostname=cfg.CONF.host,
status=True,
stats={},
capabilities={},
heartbeated_at=mock.ANY
)
central.update_service_status.assert_called_once_with(
mock_context.return_value, mock_service_status.return_value
)
@mock.patch.object(objects, "ServiceStatus")
@mock.patch("designate.context.DesignateContext.get_admin_context")
def test_emit_status_factory(self, mock_context, mock_service_status):
status = False
stats = {"a": 1}
capabilities = {"b": 2}
status_factory = mock.Mock(return_value=(status, stats, capabilities,))
emitter = service_status.RpcEmitter("svc", self.mock_tg,
status_factory=status_factory)
emitter.start()
central = mock.Mock()
with mock.patch("designate.central.rpcapi.CentralAPI.get_instance",
return_value=central):
emitter._emit_heartbeat()
mock_service_status.assert_called_once_with(
service_name="svc",
hostname=cfg.CONF.host,
status=status,
stats=stats,
capabilities=capabilities,
heartbeated_at=mock.ANY
)
central.update_service_status.assert_called_once_with(
mock_context.return_value, mock_service_status.return_value
)

43
doc/service_status.rst Normal file
View File

@ -0,0 +1,43 @@
..
Copyright 2016 Hewlett Packard Enterprise Development Company LP
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.
Service Statuses
================
Overview
-----------------------
The Service Status entries are used to track the health state of the services
in the Designate system. Each service will report in it's health via RPC or
using HTTP.
Explanation
-----------
============ ==============================================================
Attribute Description
============ ==============================================================
service_name The name of the service, typically `central` or alike.
hostname The hostname where the service is running.
capabilities Service capabilities, used to tell a service of the same type
apart.
stats Statistics are optional per service metrics.
status An enum describing the status of the service.
UP for health and ok, DOWN for down (Ie the service hasn't
reported in for a while) and WARNING if the service is having
issues.
============ ==============================================================

View File

@ -68,6 +68,7 @@ Reference Documentation
support-matrix
pools
memory-caching
service_status
Source Documentation
====================

View File

@ -93,6 +93,7 @@ V2 API
rest/v2/pools
rest/v2/limits
rest/v2/reverse
rest/v2/service_status
rest/v2/tsigkeys
Admin API

View File

@ -0,0 +1,149 @@
..
Copyright 2016 Hewlett Packard Enterprise Development Company LP
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.
Service Statuses
================
Overview
-----------------------
The Service Status entries are used to track the health state of the services
in the Designate system.
Get a Service Status
--------------------
.. http:get:: /service_statuses/(uuid:id)
Lists a particular Service Status
**Example request**:
.. sourcecode:: http
GET /service_statuses/5abe514c-9fb5-41e8-ab73-5ed25f8a73e9 HTTP/1.1
Host: example.com
Accept: application/json
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"capabilities": {},
"created_at": "2016-03-08T09:20:23.000000",
"heartbeated_at": "2016-03-08T09:26:18.000000",
"hostname": "vagrant-ubuntu-trusty-64",
"id": "769e8ca2-f71e-48be-8dee-631492c91e41",
"links": {
"self": "http://192.168.27.100:9001/v2/service_statuses/769e8ca2-f71e-48be-8dee-631492c91e41",
"service_status": "http://192.168.27.100:9001/v2/service_statuses/769e8ca2-f71e-48be-8dee-631492c91e41"
},
"service_name": "pool_manager",
"stats": {},
"status": "UP",
"updated_at": "2016-03-08T09:26:18.000000"
}
:form created_at: timestamp
:form updated_at: timestamp
:form id: uuid
:form description: UTF-8 text field
:form links: links to traverse the list
:form service_name: Service name
:form hostname: Service hostname
:form capabilities: Service capabilities - dict of capabilities
:form stats: Service stats - dict of stats
:form status: Service status - UP, DOWN or WARNING
:statuscode 200: OK
:statuscode 401: Access Denied
:statuscode 404: Service Status not found
List Service Statuses
---------------------
.. http:get:: /service_statuses
Lists all Service Statuses
**Example request**:
.. sourcecode:: http
GET /service_statuses HTTP/1.1
Host: example.com
Accept: application/json
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"service_statuses":[
{
"capabilities": {},
"created_at": "2016-03-08T09:20:23.000000",
"heartbeated_at": "2016-03-08T09:26:18.000000",
"hostname": "vagrant-ubuntu-trusty-64",
"id": "769e8ca2-f71e-48be-8dee-631492c91e41",
"links": {
"self": "http://192.168.27.100:9001/v2/service_statuses/769e8ca2-f71e-48be-8dee-631492c91e41",
"service_status": "http://192.168.27.100:9001/v2/service_statuses/769e8ca2-f71e-48be-8dee-631492c91e41"
},
"service_name": "pool_manager",
"stats": {},
"status": "UP",
"updated_at": "2016-03-08T09:26:18.000000"
},
{
"capabilities": {},
"created_at": "2016-03-08T09:20:26.000000",
"heartbeated_at": "2016-03-08T09:26:16.000000",
"hostname": "vagrant-ubuntu-trusty-64",
"id": "adcf580b-ea1c-4ebc-8a95-37ccdeed11ae",
"links": {
"self": "http://192.168.27.100:9001/v2/service_statuses/adcf580b-ea1c-4ebc-8a95-37ccdeed11ae",
"service_status": "http://192.168.27.100:9001/v2/service_statuses/adcf580b-ea1c-4ebc-8a95-37ccdeed11ae"
},
"service_name": "zone_manager",
"stats": {},
"status": "UP",
"updated_at": "2016-03-08T09:26:17.000000"
}
],
"links":{
"self":"http://127.0.0.1:9001/v2/service_statuses"
}
}
:form created_at: timestamp
:form updated_at: timestamp
:form id: uuid
:form description: UTF-8 text field
:form links: links to traverse the list
:form service_name: Service name
:form hostname: Service hostname
:form capabilities: Service capabilities - dict of capabilities
:form stats: Service stats - dict of stats
:form status: Service status - UP, DOWN or WARNING
:statuscode 200: OK
:statuscode 401: Access Denied

View File

@ -122,5 +122,8 @@
"find_zone_exports": "rule:admin_or_owner",
"get_zone_export": "rule:admin_or_owner",
"update_zone_export": "rule:admin_or_owner",
"delete_zone_export": "rule:admin_or_owner"
"find_service_status": "rule:admin",
"find_service_statuses": "rule:admin",
"update_service_service_status": "rule:admin"
}

View File

@ -122,6 +122,11 @@ designate.zone_manager_tasks =
periodic_secondary_refresh = designate.zone_manager.tasks:PeriodicSecondaryRefreshTask
delayed_notify = designate.zone_manager.tasks:PeriodicGenerateDelayedNotifyTask
designate.heartbeat_emitter =
noop = designate.service_status:NoopEmitter
rpc = designate.service_status:RpcEmitter
[build_sphinx]
all_files = 1
build-dir = doc/build