Coverage for watcher/api/controllers/v1/service.py: 91%
126 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
1# -*- encoding: utf-8 -*-
2# Copyright (c) 2016 Servionica
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
17"""
18Service mechanism provides ability to monitor Watcher services state.
19"""
21import datetime
22from oslo_config import cfg
23from oslo_log import log
24from oslo_utils import timeutils
25import pecan
26from pecan import rest
27from wsme import types as wtypes
28import wsmeext.pecan as wsme_pecan
30from watcher.api.controllers import base
31from watcher.api.controllers import link
32from watcher.api.controllers.v1 import collection
33from watcher.api.controllers.v1 import utils as api_utils
34from watcher.common import context
35from watcher.common import exception
36from watcher.common import policy
37from watcher import objects
40CONF = cfg.CONF
41LOG = log.getLogger(__name__)
44def hide_fields_in_newer_versions(obj):
45 """This method hides fields that were added in newer API versions.
47 Certain node fields were introduced at certain API versions.
48 These fields are only made available when the request's API version
49 matches or exceeds the versions when these fields were introduced.
50 """
51 pass
54class Service(base.APIBase):
55 """API representation of a service.
57 This class enforces type checking and value constraints, and converts
58 between the internal object model and the API representation of a service.
59 """
61 _status = None
62 _context = context.RequestContext(is_admin=True)
64 def _get_status(self):
65 return self._status
67 def _set_status(self, id):
68 service = objects.Service.get(pecan.request.context, id)
69 last_heartbeat = (service.last_seen_up or service.updated_at or
70 service.created_at)
71 if isinstance(last_heartbeat, str): 71 ↛ 75line 71 didn't jump to line 75 because the condition on line 71 was never true
72 # NOTE(russellb) If this service came in over rpc via
73 # conductor, then the timestamp will be a string and needs to be
74 # converted back to a datetime.
75 last_heartbeat = timeutils.parse_strtime(last_heartbeat)
76 else:
77 # Objects have proper UTC timezones, but the timeutils comparison
78 # below does not (and will fail)
79 last_heartbeat = last_heartbeat.replace(tzinfo=None)
80 elapsed = timeutils.delta_seconds(last_heartbeat, timeutils.utcnow())
81 is_up = abs(elapsed) <= CONF.service_down_time
82 if not is_up: 82 ↛ 91line 82 didn't jump to line 91 because the condition on line 82 was always true
83 LOG.warning('Seems service %(name)s on host %(host)s is down. '
84 'Last heartbeat was %(lhb)s.'
85 'Elapsed time is %(el)s',
86 {'name': service.name,
87 'host': service.host,
88 'lhb': str(last_heartbeat), 'el': str(elapsed)})
89 self._status = objects.service.ServiceStatus.FAILED
90 else:
91 self._status = objects.service.ServiceStatus.ACTIVE
93 id = wtypes.wsattr(int, readonly=True)
94 """ID for this service."""
96 name = wtypes.text
97 """Name of the service."""
99 host = wtypes.text
100 """Host where service is placed on."""
102 last_seen_up = wtypes.wsattr(datetime.datetime, readonly=True)
103 """Time when Watcher service sent latest heartbeat."""
105 status = wtypes.wsproperty(wtypes.text, _get_status, _set_status,
106 mandatory=True)
108 links = wtypes.wsattr([link.Link], readonly=True)
109 """A list containing a self link."""
111 def __init__(self, **kwargs):
112 super(Service, self).__init__()
114 fields = list(objects.Service.fields) + ['status']
115 self.fields = []
116 for field in fields:
117 self.fields.append(field)
118 setattr(self, field, kwargs.get(
119 field if field != 'status' else 'id', wtypes.Unset))
121 @staticmethod
122 def _convert_with_links(service, url, expand=True):
123 if not expand:
124 service.unset_fields_except(
125 ['id', 'name', 'host', 'status'])
127 service.links = [
128 link.Link.make_link('self', url, 'services', str(service.id)),
129 link.Link.make_link('bookmark', url, 'services', str(service.id),
130 bookmark=True)]
131 return service
133 @classmethod
134 def convert_with_links(cls, service, expand=True):
135 service = Service(**service.as_dict())
136 hide_fields_in_newer_versions(service)
137 return cls._convert_with_links(
138 service, pecan.request.host_url, expand)
140 @classmethod
141 def sample(cls, expand=True):
142 sample = cls(id=1,
143 name='watcher-applier',
144 host='Controller',
145 last_seen_up=datetime.datetime(2016, 1, 1))
146 return cls._convert_with_links(sample, 'http://localhost:9322', expand)
149class ServiceCollection(collection.Collection):
150 """API representation of a collection of services."""
152 services = [Service]
153 """A list containing services objects"""
155 def __init__(self, **kwargs):
156 super(ServiceCollection, self).__init__()
157 self._type = 'services'
159 @staticmethod
160 def convert_with_links(services, limit, url=None, expand=False,
161 **kwargs):
162 service_collection = ServiceCollection()
163 service_collection.services = [
164 Service.convert_with_links(g, expand) for g in services]
165 service_collection.next = service_collection.get_next(
166 limit, url=url, marker_field='id', **kwargs)
167 return service_collection
169 @classmethod
170 def sample(cls):
171 sample = cls()
172 sample.services = [Service.sample(expand=False)]
173 return sample
176class ServicesController(rest.RestController):
177 """REST controller for Services."""
179 def __init__(self):
180 super(ServicesController, self).__init__()
182 from_services = False
183 """A flag to indicate if the requests to this controller are coming
184 from the top-level resource Services."""
186 _custom_actions = {
187 'detail': ['GET'],
188 }
190 def _get_services_collection(self, marker, limit, sort_key, sort_dir,
191 expand=False, resource_url=None):
192 api_utils.validate_sort_key(
193 sort_key, list(objects.Service.fields))
194 limit = api_utils.validate_limit(limit)
195 api_utils.validate_sort_dir(sort_dir)
197 marker_obj = None
198 if marker: 198 ↛ 199line 198 didn't jump to line 199 because the condition on line 198 was never true
199 marker_obj = objects.Service.get(
200 pecan.request.context, marker)
202 sort_db_key = (sort_key if sort_key in objects.Service.fields
203 else None)
205 services = objects.Service.list(
206 pecan.request.context, limit, marker_obj,
207 sort_key=sort_db_key, sort_dir=sort_dir)
209 return ServiceCollection.convert_with_links(
210 services, limit, url=resource_url, expand=expand,
211 sort_key=sort_key, sort_dir=sort_dir)
213 @wsme_pecan.wsexpose(ServiceCollection, int, int, wtypes.text, wtypes.text)
214 def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
215 """Retrieve a list of services.
217 :param marker: pagination marker for large data sets.
218 :param limit: maximum number of resources to return in a single result.
219 :param sort_key: column to sort results by. Default: id.
220 :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
221 """
222 context = pecan.request.context
223 policy.enforce(context, 'service:get_all',
224 action='service:get_all')
226 return self._get_services_collection(marker, limit, sort_key, sort_dir)
228 @wsme_pecan.wsexpose(ServiceCollection, int, int, wtypes.text, wtypes.text)
229 def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
230 """Retrieve a list of services with detail.
232 :param marker: pagination marker for large data sets.
233 :param limit: maximum number of resources to return in a single result.
234 :param sort_key: column to sort results by. Default: id.
235 :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
236 """
237 context = pecan.request.context
238 policy.enforce(context, 'service:detail',
239 action='service:detail')
240 # NOTE(lucasagomes): /detail should only work against collections
241 parent = pecan.request.path.split('/')[:-1][-1]
242 if parent != "services":
243 raise exception.HTTPNotFound
244 expand = True
245 resource_url = '/'.join(['services', 'detail'])
247 return self._get_services_collection(
248 marker, limit, sort_key, sort_dir, expand, resource_url)
250 @wsme_pecan.wsexpose(Service, wtypes.text)
251 def get_one(self, service):
252 """Retrieve information about the given service.
254 :param service: ID or name of the service.
255 """
256 if self.from_services: 256 ↛ 257line 256 didn't jump to line 257 because the condition on line 256 was never true
257 raise exception.OperationNotPermitted
259 context = pecan.request.context
260 rpc_service = api_utils.get_resource('Service', service)
261 policy.enforce(context, 'service:get', rpc_service,
262 action='service:get')
264 return Service.convert_with_links(rpc_service)