Add support for getting Service Status
Change-Id: Iaf10d8486ac8015ecf9f394dfbf074bfb863fb78
This commit is contained in:
parent
b5448804c0
commit
7abae80c61
@ -21,6 +21,7 @@ from designate.i18n import _LI
|
|||||||
from designate import exceptions
|
from designate import exceptions
|
||||||
from designate import utils
|
from designate import utils
|
||||||
from designate import service
|
from designate import service
|
||||||
|
from designate import service_status
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -30,6 +31,23 @@ class Service(service.WSGIService, service.Service):
|
|||||||
def __init__(self, threads=None):
|
def __init__(self, threads=None):
|
||||||
super(Service, self).__init__(threads=threads)
|
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
|
@property
|
||||||
def service_name(self):
|
def service_name(self):
|
||||||
return 'api'
|
return 'api'
|
||||||
|
@ -23,6 +23,7 @@ from designate.api.v2.controllers import tlds
|
|||||||
from designate.api.v2.controllers import blacklists
|
from designate.api.v2.controllers import blacklists
|
||||||
from designate.api.v2.controllers import errors
|
from designate.api.v2.controllers import errors
|
||||||
from designate.api.v2.controllers import pools
|
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 zones
|
||||||
from designate.api.v2.controllers import tsigkeys
|
from designate.api.v2.controllers import tsigkeys
|
||||||
|
|
||||||
@ -57,4 +58,5 @@ class RootController(object):
|
|||||||
blacklists = blacklists.BlacklistsController()
|
blacklists = blacklists.BlacklistsController()
|
||||||
errors = errors.ErrorsController()
|
errors = errors.ErrorsController()
|
||||||
pools = pools.PoolsController()
|
pools = pools.PoolsController()
|
||||||
|
service_statuses = service_status.ServiceStatusController()
|
||||||
tsigkeys = tsigkeys.TsigKeysController()
|
tsigkeys = tsigkeys.TsigKeysController()
|
||||||
|
61
designate/api/v2/controllers/service_status.py
Normal file
61
designate/api/v2/controllers/service_status.py
Normal 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)
|
@ -54,14 +54,15 @@ class CentralAPI(object):
|
|||||||
5.5 - Add deleted zone purging task
|
5.5 - Add deleted zone purging task
|
||||||
5.6 - Changed 'purge_zones' function args
|
5.6 - Changed 'purge_zones' function args
|
||||||
6.0 - Renamed domains to zones
|
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):
|
def __init__(self, topic=None):
|
||||||
topic = topic if topic else cfg.CONF.central_topic
|
topic = topic if topic else cfg.CONF.central_topic
|
||||||
|
|
||||||
target = messaging.Target(topic=topic, version=self.RPC_API_VERSION)
|
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
|
@classmethod
|
||||||
def get_instance(cls):
|
def get_instance(cls):
|
||||||
@ -578,3 +579,24 @@ class CentralAPI(object):
|
|||||||
"delete_zone_export."))
|
"delete_zone_export."))
|
||||||
return self.client.call(context, 'delete_zone_export',
|
return self.client.call(context, 'delete_zone_export',
|
||||||
zone_export_id=zone_export_id)
|
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)
|
||||||
|
@ -263,7 +263,7 @@ def notification(notification_type):
|
|||||||
|
|
||||||
|
|
||||||
class Service(service.RPCService, service.Service):
|
class Service(service.RPCService, service.Service):
|
||||||
RPC_API_VERSION = '6.0'
|
RPC_API_VERSION = '6.1'
|
||||||
|
|
||||||
target = messaging.Target(version=RPC_API_VERSION)
|
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)
|
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
|
@property
|
||||||
def scheduler(self):
|
def scheduler(self):
|
||||||
if not hasattr(self, '_scheduler'):
|
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)
|
zone_export = self.storage.delete_zone_export(context, zone_export_id)
|
||||||
|
|
||||||
return zone_export
|
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)
|
||||||
|
@ -247,6 +247,10 @@ class Duplicate(Base):
|
|||||||
error_type = 'duplicate'
|
error_type = 'duplicate'
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateServiceStatus(Duplicate):
|
||||||
|
error_type = 'duplicate_service_status'
|
||||||
|
|
||||||
|
|
||||||
class DuplicateQuota(Duplicate):
|
class DuplicateQuota(Duplicate):
|
||||||
error_type = 'duplicate_quota'
|
error_type = 'duplicate_quota'
|
||||||
|
|
||||||
@ -351,6 +355,10 @@ class NotFound(Base):
|
|||||||
error_type = 'not_found'
|
error_type = 'not_found'
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceStatusNotFound(NotFound):
|
||||||
|
error_type = 'service_status_not_found'
|
||||||
|
|
||||||
|
|
||||||
class QuotaNotFound(NotFound):
|
class QuotaNotFound(NotFound):
|
||||||
error_type = 'quota_not_found'
|
error_type = 'quota_not_found'
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ from designate.objects.quota import Quota, QuotaList # noqa
|
|||||||
from designate.objects.record import Record, RecordList # noqa
|
from designate.objects.record import Record, RecordList # noqa
|
||||||
from designate.objects.recordset import RecordSet, RecordSetList # noqa
|
from designate.objects.recordset import RecordSet, RecordSetList # noqa
|
||||||
from designate.objects.server import Server, ServerList # 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.tenant import Tenant, TenantList # noqa
|
||||||
from designate.objects.tld import Tld, TldList # noqa
|
from designate.objects.tld import Tld, TldList # noqa
|
||||||
from designate.objects.tsigkey import TsigKey, TsigKeyList # noqa
|
from designate.objects.tsigkey import TsigKey, TsigKeyList # noqa
|
||||||
|
@ -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.tld import TldAPIv2Adapter, TldListAPIv2Adapter # noqa
|
||||||
from designate.objects.adapters.api_v2.tsigkey import TsigKeyAPIv2Adapter, TsigKeyListAPIv2Adapter # 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.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_accept import ZoneTransferAcceptAPIv2Adapter, ZoneTransferAcceptListAPIv2Adapter # noqa
|
||||||
from designate.objects.adapters.api_v2.zone_transfer_request import ZoneTransferRequestAPIv2Adapter, ZoneTransferRequestListAPIv2Adapter # 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
|
from designate.objects.adapters.api_v2.validation_error import ValidationErrorAPIv2Adapter, ValidationErrorListAPIv2Adapter # noqa
|
||||||
|
65
designate/objects/adapters/api_v2/service_status.py
Normal file
65
designate/objects/adapters/api_v2/service_status.py
Normal 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',
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ import six
|
|||||||
from six.moves.urllib import parse
|
from six.moves.urllib import parse
|
||||||
import jsonschema
|
import jsonschema
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
from designate import exceptions
|
from designate import exceptions
|
||||||
from designate.schema import validators
|
from designate.schema import validators
|
||||||
@ -179,6 +180,11 @@ class DesignateObject(object):
|
|||||||
if isinstance(value, dict) and 'designate_object.name' in value:
|
if isinstance(value, dict) and 'designate_object.name' in value:
|
||||||
setattr(instance, field, DesignateObject.from_primitive(value))
|
setattr(instance, field, DesignateObject.from_primitive(value))
|
||||||
else:
|
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)
|
setattr(instance, field, value)
|
||||||
|
|
||||||
instance._obj_changes = set(primitive['designate_object.changes'])
|
instance._obj_changes = set(primitive['designate_object.changes'])
|
||||||
|
61
designate/objects/service_status.py
Normal file
61
designate/objects/service_status.py
Normal 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
|
@ -35,11 +35,13 @@ from designate.i18n import _
|
|||||||
from designate.i18n import _LE
|
from designate.i18n import _LE
|
||||||
from designate.i18n import _LI
|
from designate.i18n import _LI
|
||||||
from designate.i18n import _LW
|
from designate.i18n import _LW
|
||||||
from designate import rpc
|
|
||||||
from designate import policy
|
from designate import policy
|
||||||
|
from designate import rpc
|
||||||
|
from designate import service_status
|
||||||
from designate import version
|
from designate import version
|
||||||
from designate import utils
|
from designate import utils
|
||||||
|
|
||||||
|
|
||||||
# TODO(kiall): These options have been cut+paste from the old WSGI code, and
|
# TODO(kiall): These options have been cut+paste from the old WSGI code, and
|
||||||
# should be moved into service:api etc..
|
# should be moved into service:api etc..
|
||||||
wsgi_socket_opts = [
|
wsgi_socket_opts = [
|
||||||
@ -108,6 +110,19 @@ class RPCService(object):
|
|||||||
messaging.Target(topic=self._rpc_topic, server=self._host),
|
messaging.Target(topic=self._rpc_topic, server=self._host),
|
||||||
self._rpc_endpoints)
|
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
|
@property
|
||||||
def _rpc_endpoints(self):
|
def _rpc_endpoints(self):
|
||||||
return [self]
|
return [self]
|
||||||
@ -130,8 +145,11 @@ class RPCService(object):
|
|||||||
if e != self and hasattr(e, 'start'):
|
if e != self and hasattr(e, 'start'):
|
||||||
e.start()
|
e.start()
|
||||||
|
|
||||||
|
self.heartbeat_emitter.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
LOG.debug("Stopping RPC server on topic '%s'" % self._rpc_topic)
|
LOG.debug("Stopping RPC server on topic '%s'" % self._rpc_topic)
|
||||||
|
self.heartbeat_emitter.stop()
|
||||||
|
|
||||||
for e in self._rpc_endpoints:
|
for e in self._rpc_endpoints:
|
||||||
if e != self and hasattr(e, 'stop'):
|
if e != self and hasattr(e, 'stop'):
|
||||||
|
114
designate/service_status.py
Normal file
114
designate/service_status.py
Normal 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)
|
@ -766,3 +766,37 @@ class Storage(DriverPlugin):
|
|||||||
return {
|
return {
|
||||||
'status': None
|
'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.
|
||||||
|
"""
|
||||||
|
@ -1922,6 +1922,35 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
|
|||||||
|
|
||||||
return result[0]
|
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
|
# diagnostics
|
||||||
def ping(self, context):
|
def ping(self, context):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
@ -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()
|
@ -18,6 +18,7 @@ from sqlalchemy import (Table, MetaData, Column, String, Text, Integer,
|
|||||||
UniqueConstraint, ForeignKeyConstraint)
|
UniqueConstraint, ForeignKeyConstraint)
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
from oslo_db.sqlalchemy import types
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
from designate import utils
|
from designate import utils
|
||||||
@ -39,6 +40,9 @@ ACTIONS = ['CREATE', 'DELETE', 'UPDATE', 'NONE']
|
|||||||
ZONE_TYPES = ('PRIMARY', 'SECONDARY',)
|
ZONE_TYPES = ('PRIMARY', 'SECONDARY',)
|
||||||
ZONE_TASK_TYPES = ['IMPORT', 'EXPORT']
|
ZONE_TASK_TYPES = ['IMPORT', 'EXPORT']
|
||||||
|
|
||||||
|
SERVICE_STATES = [
|
||||||
|
"UP", "DOWN", "WARNING"
|
||||||
|
]
|
||||||
|
|
||||||
metadata = MetaData()
|
metadata = MetaData()
|
||||||
|
|
||||||
@ -51,6 +55,25 @@ def default_shard(context, id_col):
|
|||||||
return int(context.current_parameters[id_col][0:3], 16)
|
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,
|
quotas = Table('quotas', metadata,
|
||||||
Column('id', UUID, default=utils.generate_uuid, primary_key=True),
|
Column('id', UUID, default=utils.generate_uuid, primary_key=True),
|
||||||
Column('version', Integer, default=1, nullable=False),
|
Column('version', Integer, default=1, nullable=False),
|
||||||
|
@ -62,6 +62,14 @@ class TestTimeoutError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class TestCase(base.BaseTestCase):
|
class TestCase(base.BaseTestCase):
|
||||||
|
service_status_fixtures = [{
|
||||||
|
'service_name': 'foo',
|
||||||
|
'hostname': 'bar',
|
||||||
|
'status': "UP",
|
||||||
|
'stats': {},
|
||||||
|
'capabilities': {},
|
||||||
|
}]
|
||||||
|
|
||||||
quota_fixtures = [{
|
quota_fixtures = [{
|
||||||
'resource': 'zones',
|
'resource': 'zones',
|
||||||
'hard_limit': 5,
|
'hard_limit': 5,
|
||||||
@ -320,6 +328,11 @@ class TestCase(base.BaseTestCase):
|
|||||||
group='service:central'
|
group='service:central'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.config(
|
||||||
|
emitter_type="noop",
|
||||||
|
group="heartbeat_emitter"
|
||||||
|
)
|
||||||
|
|
||||||
self.config(
|
self.config(
|
||||||
auth_strategy='noauth',
|
auth_strategy='noauth',
|
||||||
group='service:api'
|
group='service:api'
|
||||||
@ -604,6 +617,23 @@ class TestCase(base.BaseTestCase):
|
|||||||
_values.update(values)
|
_values.update(values)
|
||||||
return _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):
|
def create_tld(self, **kwargs):
|
||||||
context = kwargs.pop('context', self.admin_context)
|
context = kwargs.pop('context', self.admin_context)
|
||||||
fixture = kwargs.pop('fixture', 0)
|
fixture = kwargs.pop('fixture', 0)
|
||||||
|
84
designate/tests/test_api/test_v2/test_service_status.py
Normal file
84
designate/tests/test_api/test_v2/test_service_status.py
Normal 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')
|
@ -52,6 +52,7 @@ class SqlalchemyStorageTest(StorageTestCase, TestCase):
|
|||||||
u'quotas',
|
u'quotas',
|
||||||
u'records',
|
u'records',
|
||||||
u'recordsets',
|
u'recordsets',
|
||||||
|
u'service_statuses',
|
||||||
u'tlds',
|
u'tlds',
|
||||||
u'tsigkeys',
|
u'tsigkeys',
|
||||||
u'zone_attributes',
|
u'zone_attributes',
|
||||||
|
105
designate/tests/unit/test_service_status.py
Normal file
105
designate/tests/unit/test_service_status.py
Normal 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
43
doc/service_status.rst
Normal 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.
|
||||||
|
============ ==============================================================
|
@ -68,6 +68,7 @@ Reference Documentation
|
|||||||
support-matrix
|
support-matrix
|
||||||
pools
|
pools
|
||||||
memory-caching
|
memory-caching
|
||||||
|
service_status
|
||||||
|
|
||||||
Source Documentation
|
Source Documentation
|
||||||
====================
|
====================
|
||||||
|
@ -93,6 +93,7 @@ V2 API
|
|||||||
rest/v2/pools
|
rest/v2/pools
|
||||||
rest/v2/limits
|
rest/v2/limits
|
||||||
rest/v2/reverse
|
rest/v2/reverse
|
||||||
|
rest/v2/service_status
|
||||||
rest/v2/tsigkeys
|
rest/v2/tsigkeys
|
||||||
|
|
||||||
Admin API
|
Admin API
|
||||||
|
149
doc/source/rest/v2/service_status.rst
Normal file
149
doc/source/rest/v2/service_status.rst
Normal 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
|
@ -122,5 +122,8 @@
|
|||||||
"find_zone_exports": "rule:admin_or_owner",
|
"find_zone_exports": "rule:admin_or_owner",
|
||||||
"get_zone_export": "rule:admin_or_owner",
|
"get_zone_export": "rule:admin_or_owner",
|
||||||
"update_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"
|
||||||
}
|
}
|
||||||
|
@ -122,6 +122,11 @@ designate.zone_manager_tasks =
|
|||||||
periodic_secondary_refresh = designate.zone_manager.tasks:PeriodicSecondaryRefreshTask
|
periodic_secondary_refresh = designate.zone_manager.tasks:PeriodicSecondaryRefreshTask
|
||||||
delayed_notify = designate.zone_manager.tasks:PeriodicGenerateDelayedNotifyTask
|
delayed_notify = designate.zone_manager.tasks:PeriodicGenerateDelayedNotifyTask
|
||||||
|
|
||||||
|
designate.heartbeat_emitter =
|
||||||
|
noop = designate.service_status:NoopEmitter
|
||||||
|
rpc = designate.service_status:RpcEmitter
|
||||||
|
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
all_files = 1
|
all_files = 1
|
||||||
build-dir = doc/build
|
build-dir = doc/build
|
||||||
|
Loading…
Reference in New Issue
Block a user