Open Kilda Java Documentation
model.py
Go to the documentation of this file.
1 # -*- coding:utf-8 -*-
2 # Copyright 2017 Telstra Open Source
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 implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #
16 
17 import collections
18 import datetime
19 import json
20 import logging
21 import sys
22 import uuid
23 import weakref
24 
25 import pytz
26 
27 from topologylistener import exc
28 
29 logger = logging.getLogger(__name__)
30 
31 
32 def convert_integer(raw, limit=sys.maxint):
33  if not isinstance(raw, (int, long)):
34  try:
35  value = int(raw, 0)
36  except ValueError:
37  raise exc.UnacceptableDataError(raw, 'not numeric value: {}'.format(raw))
38  else:
39  value = raw
40 
41  if limit is not None and value > limit:
43  raw, 'integer value too big {}'.format(raw))
44  return value
45 
46 
47 def grab_fields(data, fields_mapping):
48  return {
49  y: data[x]
50  for x, y in fields_mapping.items() if x in data}
51 
52 
53 LifeCycleFields = collections.namedtuple('LifeCycleFields', ('ctime', 'mtime'))
54 
55 
56 class TimeProperty(object):
57  FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
58  UNIX_EPOCH = datetime.datetime(1970, 1, 1, 0, 0, 0, 0, pytz.utc)
59 
60  @classmethod
61  def new_from_java_timestamp(cls, value):
62  value = int(value)
63  value /= 1000.0
64  return cls(datetime.datetime.utcfromtimestamp(value))
65 
66  @classmethod
67  def new_from_db(cls, value):
68  value = datetime.datetime.strptime(value, cls.FORMAT)
69  return cls(value)
70 
71  @classmethod
72  def now(cls, milliseconds_precission=False):
73  value = datetime.datetime.utcnow()
74  if milliseconds_precission:
75  microseconds = value.microsecond
76  microseconds -= microseconds % 1000
77  value = value.replace(microsecond=microseconds)
78  return cls(value)
79 
80  def __init__(self, value):
81  if value.tzinfo is None:
82  value = value.replace(tzinfo=pytz.utc)
83  self.value = value
84 
85  def __str__(self):
86  return self.value.strftime(self.FORMAT)
87 
88  def as_java_timestamp(self):
89  from_epoch = self.value - self.UNIX_EPOCH
90  seconds = int(from_epoch.total_seconds())
91  return seconds * 1000 + from_epoch.microseconds // 1000
92 
93 
94 class JsonSerializable(object):
95  pass
96 
97 
98 class Default(object):
99  def __init__(self, value, produce=False, override_none=True):
100  self._resolve_cache = weakref.WeakKeyDictionary()
101  self.value = value
102  self.produce = produce
103  self.override_none = override_none
104 
105  def __get__(self, instance, owner):
106  if instance is None:
107  return self
108 
109  value = self.value
110  if self.produce:
111  value = value()
112 
113  setattr(instance, self._resolve_name(owner), value)
114  return value
115 
116  def is_filled(self, instance):
117  name = self._resolve_name(type(instance))
118  data = vars(instance)
119  return name in data
120 
121  def _resolve_name(self, owner):
122  try:
123  return self._resolve_cache[owner]
124  except KeyError:
125  pass
126 
127  for name in dir(owner):
128  try:
129  attr = getattr(owner, name)
130  except AttributeError:
131  continue
132  if attr is not self:
133  continue
134  break
135  else:
136  raise RuntimeError(
137  '{!r} Unable to resolve bounded name (UNREACHABLE)'.format(
138  self))
139 
140  self._resolve_cache[owner] = name
141  return name
142 
143 
144 class Abstract(object):
145  pack_exclude = frozenset()
146 
147  def __init__(self, **fields):
148  cls = type(self)
149  extra = set()
150  for name in fields:
151  extra.add(name)
152  try:
153  attr = getattr(cls, name)
154  except AttributeError:
155  continue
156  if not isinstance(attr, Default):
157  continue
158 
159  extra.remove(name)
160 
161  if attr.override_none and fields[name] is None:
162  continue
163  setattr(self, name, fields[name])
164 
165  if extra:
166  raise TypeError('{!r} got unknown arguments: "{}"'.format(
167  self, '", "'.join(sorted(extra))))
168 
169  self._verify_fields()
170 
171  def __str__(self):
172  return '<{}:{}>'.format(
173  type(self).__name__,
174  json.dumps(self.pack(), sort_keys=True, cls=JSONEncoder))
175 
176  def __eq__(self, other):
177  if not isinstance(other, Abstract):
178  raise NotImplementedError
179  return self._sort_key() == other._sort_key()
180 
181  def __ne__(self, other):
182  return not self.__eq__(other)
183 
184  def pack(self):
185  fields = vars(self).copy()
186 
187  cls = type(self)
188  for name in dir(cls):
189  if name.startswith('_'):
190  continue
191  if name in fields:
192  continue
193 
194  attr = getattr(cls, name)
195  if not isinstance(attr, Default):
196  continue
197  fields[name] = getattr(self, name)
198 
199  for name in tuple(fields):
200  if not name.startswith('_') and name not in self.pack_exclude:
201  continue
202  del fields[name]
203 
204  return fields
205 
206  def _verify_fields(self):
207  pass
208 
209  def _sort_key(self):
210  raise NotImplementedError
211 
212  @classmethod
213  def _extract_fields(cls, data):
214  data = data.copy()
215  fields = {}
216  for name in dir(cls):
217  if name.startswith('_'):
218  continue
219  attr = getattr(cls, name)
220  if not isinstance(attr, Default):
221  continue
222 
223  try:
224  fields[name] = data.pop(name)
225  except KeyError:
226  pass
227 
228  return fields, data
229 
230  @classmethod
231  def decode_java_fields(cls, data):
232  return {}
233 
234  @classmethod
235  def decode_db_fields(cls, data):
236  return {}
237 
238 
240  time_create = Default(None) # type: TimeProperty
241  time_modify = Default(None) # type: TimeProperty
242 
243  def __init__(self, **fields):
244  timestamp = TimeProperty.now()
245  for name in ('time_create', 'time_modify'):
246  fields.setdefault(name, timestamp)
247  super(TimestampMixin, self).__init__(**fields)
248 
249  @classmethod
250  def decode_java_fields(cls, data):
251  decoded = super(TimestampMixin, cls).decode_java_fields(data)
252  for name in ('time_create', 'time_modify'):
253  try:
254  if data[name] is None:
255  del data[name]
256  continue
257 
258  decoded[name] = TimeProperty.new_from_java_timestamp(data[name])
259  except KeyError:
260  pass
261 
262  try:
263  decoded.setdefault('time_modify', decoded['time_create'])
264  except KeyError:
265  pass
266 
267  return decoded
268 
269  @classmethod
270  def decode_db_fields(cls, data):
271  decoded = super(TimestampMixin, cls).decode_db_fields(data)
272  for name in ('time_create', 'time_modify'):
273  try:
274  decoded[name] = TimeProperty.new_from_db(data[name])
275  except KeyError:
276  pass
277  return decoded
278 
279  def pack(self):
280  fields = super(TimestampMixin, self).pack()
281  for name in 'time_create', 'time_modify':
282  try:
283  fields[name] = fields[name].as_java_timestamp()
284  except KeyError:
285  pass
286  return fields
287 
288 
290  def __init__(self, dpid, port, **fields):
291  super(AbstractNetworkEndpoint, self).__init__(**fields)
292 
293  if isinstance(dpid, basestring):
294  dpid = dpid.lower()
295  if port is not None:
296  port = convert_integer(port)
297 
298  self.dpid = dpid
299  self.port = port
300 
301  def __str__(self):
302  return '{}-{}'.format(self.dpid, self.port)
303 
304  def _sort_key(self):
305  return self.dpid, self.port
306 
307 
309  @classmethod
310  def new_from_java(cls, data):
311  return cls(data['switch-id'], data['port-id'])
312 
313  def pack(self):
314  fields = super(NetworkEndpoint, self).pack()
315  for src, dst in [
316  ('dpid', 'switch-id'),
317  ('port', 'port-id')]:
318  fields[dst] = fields.pop(src)
319  return fields
320 
321 
323  @classmethod
324  def new_from_java(cls, data):
325  return cls(data['switch_id'], data['port_no'])
326 
327  def pack(self):
328  fields = super(IslPathNode, self).pack()
329  for src, dst in [
330  ('dpid', 'switch_id'),
331  ('port', 'port_no')]:
332  fields[dst] = fields.pop(src)
333  return fields
334 
335 
337  source = Default(None)
338  dest = Default(None)
339 
340  def __init__(self, **fields):
341  super(AbstractLink, self).__init__(**fields)
342 
343  def _verify_fields(self):
344  super(AbstractLink, self)._verify_fields()
345  if not self.source or not self.dest:
347  self, (
348  'can\'t instantiate {} without defining both '
349  'source=={!r} and dest=={!r} fields').format(
350  type(self).__name__, self.source, self.dest))
351 
352  def _sort_key(self):
353  return self.source, self.dest
354 
355 
357  isl_protected_fields = frozenset((
358  'time_create', 'time_modify',
359  'latency', 'speed', 'available_bandwidth', 'actual', 'status'))
360  props_converters = {
361  'cost': convert_integer}
362 
363  props = Default(dict, produce=True)
364  filtered = Default(set, produce=True)
365 
366  @classmethod
367  def new_from_java(cls, data):
368  data = data.copy()
369  source = NetworkEndpoint.new_from_java(data.pop('source'))
370  dest = NetworkEndpoint.new_from_java(data.pop('dest'))
371  props = data.pop('props', dict()).copy()
372  data.update(cls.decode_java_fields(data))
373  return cls(source, dest, props=props, **data)
374 
375  @classmethod
376  def new_from_db(cls, data):
377  data = data.copy()
378 
379  endpoints = []
380  for prefix in ('src_', 'dst_'):
381  dpid = data.pop(prefix + 'switch')
382  port = data.pop(prefix + 'port')
383  endpoints.append(NetworkEndpoint(dpid, port))
384  source, dest = endpoints
385 
386  data, props = cls._extract_fields(data)
387  data.update(cls.decode_db_fields(data))
388 
389  return cls(source, dest, props=props, **data)
390 
391  @classmethod
392  def new_from_isl(cls, isl):
393  return cls(isl.source, isl.dest)
394 
395  def __init__(self, source, dest, **fields):
396  super(LinkProps, self).__init__(source=source, dest=dest, **fields)
397 
398  def props_db_view(self):
399  props = self._decode_props(self.props)
400  return props
401 
403  filtered = {}
404  for field in self.isl_protected_fields:
405  try:
406  filtered[field] = self.props.pop(field)
407  except KeyError:
408  pass
409  return filtered
410 
411  @classmethod
412  def _decode_props(cls, props):
413  for field, converter in cls.props_converters.items():
414  try:
415  value = props[field]
416  except KeyError:
417  continue
418  props[field] = converter(value)
419  return props
420 
421  def pack(self):
422  fields = super(LinkProps, self).pack()
423  fields.pop('filtered')
424  return fields
425 
426 
428  state = Default(None)
429 
430  @classmethod
431  def new_from_java(cls, data):
432  try:
433  path = data['path']
434  endpoints = [
435  IslPathNode.new_from_java(x)
436  for x in path]
437  except KeyError as e:
438  raise ValueError((
439  'Invalid record format "path": is not contain key '
440  '{}').format(e))
441 
442  if 2 == len(endpoints):
443  pass
444  elif 1 == len(endpoints):
445  endpoints.append(None)
446  else:
447  raise ValueError(
448  'Invalid record format "path": expect list with 1 or 2 nodes')
449 
450  source, dest = endpoints
451  return cls(source, dest, data['state'])
452 
453  @classmethod
454  def new_from_db(cls, link):
455  source = IslPathNode(link['src_switch'], link['src_port'])
456  dest = IslPathNode(link['dst_switch'], link['dst_port'])
457  return cls(source, dest, link['status'])
458 
459  @classmethod
460  def new_from_link_props(cls, link_props):
461  endpoints = [
462  IslPathNode(x.dpid, x.port)
463  for x in link_props.source, link_props.dest]
464  return cls(*endpoints)
465 
466  def __init__(self, source, dest, state=None, **fields):
467  super(InterSwitchLink, self).__init__(
468  source=source, dest=dest, state=state, **fields)
469 
471  ends_count = len(filter(None, (self.source, self.dest)))
472  if ends_count != 2:
473  raise ValueError(
474  'ISL path not define %s/2 ends'.format(ends_count))
475 
476  def reversed(self):
477  cls = type(self)
478  return cls(self.dest, self.source, self.state)
479 
480  def __str__(self):
481  return '{} <===> {}'.format(self.source, self.dest)
482 
483 
484 class JSONEncoder(json.JSONEncoder):
485  def default(self, o):
486  if isinstance(o, TimeProperty):
487  value = str(o)
488  elif isinstance(o, uuid.UUID):
489  value = str(o)
490  elif isinstance(o, JsonSerializable):
491  value = vars(o)
492  elif isinstance(o, Abstract):
493  value = o.pack()
494  else:
495  value = super(JSONEncoder, self).default(o)
496  return value
def __init__(self, dpid, port, fields)
Definition: model.py:290
def decode_db_fields(cls, data)
Definition: model.py:235
def convert_integer(raw, limit=sys.maxint)
Definition: model.py:32
def is_filled(self, instance)
Definition: model.py:116
def __init__(self, fields)
Definition: model.py:147
def __init__(self, value, produce=False, override_none=True)
Definition: model.py:99
def new_from_db(cls, value)
Definition: model.py:67
def __init__(self, value)
Definition: model.py:80
def decode_java_fields(cls, data)
Definition: model.py:231
def __ne__(self, other)
Definition: model.py:181
def new_from_java(cls, data)
Definition: model.py:324
def grab_fields(data, fields_mapping)
Definition: model.py:47
def _extract_fields(cls, data)
Definition: model.py:213
def now(cls, milliseconds_precission=False)
Definition: model.py:72
def new_from_java_timestamp(cls, value)
Definition: model.py:61
def _resolve_name(self, owner)
Definition: model.py:121
def __get__(self, instance, owner)
Definition: model.py:105
def __eq__(self, other)
Definition: model.py:176