blob: b71ea6abc30bf83a486beaabcbecaea96537ee76 [file] [log] [blame]
Brad Bishop63f59a72016-07-25 12:05:57 -04001#!/usr/bin/env python
2
3# Contributors Listed Below - COPYRIGHT 2016
4# [+] International Business Machines Corp.
5#
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16# implied. See the License for the specific language governing
17# permissions and limitations under the License.
18
19import dbus
20import dbus.service
21import dbus.exceptions
22import dbus.mainloop.glib
23import gobject
24import xml.etree.ElementTree as ET
25import obmc.utils.pathtree
26import obmc.utils.misc
27import obmc.mapper
28import obmc.dbuslib.bindings
29import obmc.dbuslib.enums
30
31
32class MapperNotFoundException(dbus.exceptions.DBusException):
33 _dbus_error_name = obmc.mapper.MAPPER_NOT_FOUND
34
35 def __init__(self, path):
36 super(MapperNotFoundException, self).__init__(
37 "path or object not found: %s" % path)
38
39
40def find_dbus_interfaces(conn, service, path, match):
41 class _FindInterfaces(object):
42 def __init__(self):
43 self.results = {}
44
45 @staticmethod
46 def _get_object(path):
47 try:
48 return conn.get_object(service, path, introspect=False)
49 except dbus.exceptions.DBusException, e:
50 if e.get_dbus_name() in [
51 obmc.dbuslib.enums.DBUS_UNKNOWN_SERVICE,
52 obmc.dbuslib.enums.DBUS_NO_REPLY]:
53 print "Warning: Introspection failure: " \
54 "service `%s` is not running" % (service)
55 return None
56 raise
57
58 @staticmethod
59 def _invoke_method(path, iface, method, *args):
60 obj = _FindInterfaces._get_object(path)
61 if not obj:
62 return None
63
64 iface = dbus.Interface(obj, iface)
65 try:
66 f = getattr(iface, method)
67 return f(*args)
68 except dbus.exceptions.DBusException, e:
69 if e.get_dbus_name() in [
70 obmc.dbuslib.enums.DBUS_UNKNOWN_SERVICE,
71 obmc.dbuslib.enums.DBUS_NO_REPLY]:
72 print "Warning: Introspection failure: " \
73 "service `%s` did not reply to "\
74 "method call on %s" % (service, path)
75 return None
76 raise
77
78 @staticmethod
79 def _introspect(path):
80 return _FindInterfaces._invoke_method(
81 path,
82 dbus.INTROSPECTABLE_IFACE,
83 'Introspect')
84
85 @staticmethod
86 def _get_managed_objects(om):
87 return _FindInterfaces._invoke_method(
88 om,
89 dbus.BUS_DAEMON_IFACE + '.ObjectManager',
90 'GetManagedObjects')
91
92 @staticmethod
93 def _to_path(elements):
94 return '/' + '/'.join(elements)
95
96 @staticmethod
97 def _to_path_elements(path):
98 return filter(bool, path.split('/'))
99
100 def __call__(self, path):
101 self.results = {}
102 self._find_interfaces(path)
103 return self.results
104
105 @staticmethod
106 def _match(iface):
107 return iface == dbus.BUS_DAEMON_IFACE + '.ObjectManager' \
108 or match(iface)
109
110 def _find_interfaces(self, path):
111 path_elements = self._to_path_elements(path)
112 path = self._to_path(path_elements)
113 data = self._introspect(path)
114 if data is None:
115 return
116
117 root = ET.fromstring(data)
118 ifaces = filter(
119 self._match,
120 [x.attrib.get('name') for x in root.findall('interface')])
121 self.results[path] = ifaces
122
123 if dbus.BUS_DAEMON_IFACE + '.ObjectManager' in ifaces:
124 objs = self._get_managed_objects(path)
125 for k, v in objs.iteritems():
126 self.results[k] = v
127 else:
128 children = filter(
129 bool,
130 [x.attrib.get('name') for x in root.findall('node')])
131 children = [
132 self._to_path(
133 path_elements + self._to_path_elements(x))
134 for x in sorted(children)]
135 for child in children:
136 if child not in self.results:
137 self._find_interfaces(child)
138
139 return _FindInterfaces()(path)
140
141
142class Association(dbus.service.Object):
143 def __init__(self, bus, path, endpoints):
144 super(Association, self).__init__(bus, path)
145 self.endpoints = endpoints
146
147 def __getattr__(self, name):
148 if name == 'properties':
149 return {
150 obmc.dbuslib.enums.OBMC_ASSOC_IFACE: {
151 'endpoints': self.endpoints}}
152 return super(Association, self).__getattr__(name)
153
154 def emit_signal(self, old):
155 if old != self.endpoints:
156 self.PropertiesChanged(
157 obmc.dbuslib.enums.OBMC_ASSOC_IFACE,
158 {'endpoints': self.endpoints}, ['endpoints'])
159
160 def append(self, endpoints):
161 old = self.endpoints
162 self.endpoints = list(set(endpoints).union(self.endpoints))
163 self.emit_signal(old)
164
165 def remove(self, endpoints):
166 old = self.endpoints
167 self.endpoints = list(set(self.endpoints).difference(endpoints))
168 self.emit_signal(old)
169
170 @dbus.service.method(dbus.PROPERTIES_IFACE, 'ss', 'as')
171 def Get(self, interface_name, property_name):
172 if property_name != 'endpoints':
173 raise dbus.exceptions.DBusException(name=DBUS_UNKNOWN_PROPERTY)
174 return self.GetAll(interface_name)[property_name]
175
176 @dbus.service.method(dbus.PROPERTIES_IFACE, 's', 'a{sas}')
177 def GetAll(self, interface_name):
178 if interface_name != obmc.dbuslib.enums.OBMC_ASSOC_IFACE:
179 raise dbus.exceptions.DBusException(DBUS_UNKNOWN_INTERFACE)
180 return {'endpoints': self.endpoints}
181
182 @dbus.service.signal(
183 dbus.PROPERTIES_IFACE, signature='sa{sas}as')
184 def PropertiesChanged(
185 self, interface_name, changed_properties, invalidated_properties):
186 pass
187
188
189class Manager(obmc.dbuslib.bindings.DbusObjectManager):
190 def __init__(self, bus, path):
191 obmc.dbuslib.bindings.DbusObjectManager.__init__(self)
192 dbus.service.Object.__init__(self, bus, path)
193
194
195class ObjectMapper(dbus.service.Object):
196 def __init__(self, bus, path,
197 name_match=obmc.utils.misc.org_dot_openbmc_match,
198 intf_match=obmc.utils.misc.org_dot_openbmc_match):
199 super(ObjectMapper, self).__init__(bus, path)
200 self.cache = obmc.utils.pathtree.PathTree()
201 self.bus = bus
202 self.name_match = name_match
203 self.intf_match = intf_match
204 self.tag_match = obmc.utils.misc.ListMatch(['children', 'interface'])
205 self.service = None
206 self.index = {}
207 self.manager = Manager(bus, obmc.dbuslib.bindings.OBJ_PREFIX)
208 self.unique = bus.get_unique_name()
209 self.bus_map = {}
210
211 gobject.idle_add(self.discover)
212 self.bus.add_signal_receiver(
213 self.bus_handler,
214 dbus_interface=dbus.BUS_DAEMON_IFACE,
215 signal_name='NameOwnerChanged')
216 self.bus.add_signal_receiver(
217 self.interfaces_added_handler,
218 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
219 signal_name='InterfacesAdded',
220 sender_keyword='sender',
221 path_keyword='sender_path')
222 self.bus.add_signal_receiver(
223 self.interfaces_removed_handler,
224 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
225 signal_name='InterfacesRemoved',
226 sender_keyword='sender',
227 path_keyword='sender_path')
228 self.bus.add_signal_receiver(
229 self.properties_changed_handler,
230 dbus_interface=dbus.PROPERTIES_IFACE,
231 signal_name='PropertiesChanged',
232 path_keyword='path',
233 sender_keyword='sender')
234
235 def bus_match(self, owner):
236 # Ignore my own signals
237 return owner != obmc.mapper.MAPPER_NAME and \
238 self.name_match(owner)
239
240 def discovery_pending(self):
241 return not bool(self.service)
242
243 def cache_get(self, path):
244 cache_entry = self.cache.get(path, {})
245 if cache_entry is None:
246 # hide path elements without any interfaces
247 cache_entry = {}
248 return cache_entry
249
250 def add_new_objmgr(self, path, owner):
251 # We don't get a signal for the ObjectManager
252 # interface itself, so if we see a signal from
253 # make sure its in our cache, and add it if not.
254 cache_entry = self.cache_get(path)
255 old = self.interfaces_get(cache_entry, owner)
256 new = list(set(old).union([dbus.BUS_DAEMON_IFACE + '.ObjectManager']))
257 self.update_interfaces(path, owner, old, new)
258
259 def interfaces_added_handler(self, path, iprops, **kw):
260 path = str(path)
261 owner = str(kw['sender'])
262 interfaces = self.get_signal_interfaces(owner, iprops.iterkeys())
263 if interfaces:
264 self.add_new_objmgr(str(kw['sender_path']), owner)
265 cache_entry = self.cache_get(path)
266 old = self.interfaces_get(cache_entry, owner)
267 new = list(set(interfaces).union(old))
268 self.update_interfaces(path, owner, old, new)
269
270 def interfaces_removed_handler(self, path, interfaces, **kw):
271 path = str(path)
272 owner = str(kw['sender'])
273 interfaces = self.get_signal_interfaces(owner, interfaces)
274 if interfaces:
275 self.add_new_objmgr(str(kw['sender_path']), owner)
276 cache_entry = self.cache_get(path)
277 old = self.interfaces_get(cache_entry, owner)
278 new = list(set(old).difference(interfaces))
279 self.update_interfaces(path, owner, old, new)
280
281 def properties_changed_handler(self, interface, new, old, **kw):
282 owner = str(kw['sender'])
283 path = str(kw['path'])
284 interfaces = self.get_signal_interfaces(owner, [interface])
285 if not self.is_association(interfaces):
286 return
287 associations = new.get('associations', None)
288 if associations is None:
289 return
290
291 associations = [
292 (str(x), str(y), str(z)) for x, y, z in associations]
293 self.update_associations(
294 path, owner,
295 self.index_get_associations(path, [owner]),
296 associations)
297
298 def process_new_owner(self, owned_name, owner):
299 # unique name
300 try:
301 return self.discover([(owned_name, owner)])
302 except dbus.exceptions.DBusException, e:
303 if obmc.dbuslib.enums.DBUS_UNKNOWN_SERVICE \
304 not in e.get_dbus_name():
305 raise
306
307 def process_old_owner(self, owned_name, owner):
308 if owner in self.bus_map:
309 del self.bus_map[owner]
310
311 for path, item in self.cache.dataitems():
312 old = self.interfaces_get(item, owner)
313 # remove all interfaces for this service
314 self.update_interfaces(
315 path, owner, old=old, new=[])
316
317 def bus_handler(self, owned_name, old, new):
318 valid = False
319 if not obmc.dbuslib.bindings.is_unique(owned_name):
320 valid = self.valid_signal(owned_name)
321
322 if valid and new:
323 self.process_new_owner(owned_name, new)
324 if valid and old:
325 self.process_old_owner(owned_name, old)
326
327 def update_interfaces(self, path, owner, old, new):
328 # __xx -> intf list
329 # xx -> intf dict
330 if isinstance(old, dict):
331 __old = old.keys()
332 else:
333 __old = old
334 old = {x: {} for x in old}
335 if isinstance(new, dict):
336 __new = new.keys()
337 else:
338 __new = new
339 new = {x: {} for x in new}
340
341 cache_entry = self.cache.setdefault(path, {})
342 created = [] if self.has_interfaces(cache_entry) else [path]
343 added = list(set(__new).difference(__old))
344 removed = list(set(__old).difference(__new))
345 self.interfaces_append(cache_entry, owner, added)
346 self.interfaces_remove(cache_entry, owner, removed, path)
347 destroyed = [] if self.has_interfaces(cache_entry) else [path]
348
349 # react to anything that requires association updates
350 new_assoc = []
351 old_assoc = []
352 if self.is_association(added):
353 new_assoc = self.dbus_get_associations(path, owner, new)
354 if self.is_association(removed):
355 old_assoc = self.index_get_associations(path, [owner])
356 self.update_associations(
357 path, owner, old_assoc, new_assoc, created, destroyed)
358
359 def add_items(self, owner, bus_items):
360 for path, items in bus_items.iteritems():
361 self.update_interfaces(path, str(owner), old=[], new=items)
362
363 def discover(self, owners=[]):
364 def match(iface):
365 return iface == dbus.BUS_DAEMON_IFACE + '.ObjectManager' or \
366 self.intf_match(iface)
367 if not owners:
368 owned_names = [
369 x for x in self.bus.list_names() if self.bus_match(x)]
370 owners = [self.bus.get_name_owner(x) for x in owned_names]
371 owners = zip(owned_names, owners)
372 for owned_name, o in owners:
373 self.add_items(
374 o,
375 find_dbus_interfaces(self.bus, o, '/', self.intf_match))
376 self.bus_map[o] = owned_name
377
378 if self.discovery_pending():
379 # add my object mananger instance
380 self.bus_map[self.unique] = obmc.mapper.MAPPER_NAME
381 self.add_items(
382 self.unique,
383 {obmc.dbuslib.bindings.OBJ_PREFIX:
384 {'interfaces':
385 [dbus.BUS_DAEMON_IFACE + '.ObjectManager']}})
386
387 print "ObjectMapper discovery complete..."
388 self.service = dbus.service.BusName(
389 obmc.mapper.MAPPER_NAME, self.bus)
390
391 def valid_signal(self, name):
392 if self.discovery_pending():
393 return False
394
395 if obmc.dbuslib.bindings.is_unique(name):
396 name = self.bus_map.get(name)
397
398 return name is not None and \
399 self.bus_match(name)
400
401 def get_signal_interfaces(self, owner, interfaces):
402 filtered = []
403 if self.valid_signal(owner):
404 filtered = [str(x) for x in interfaces if self.intf_match(x)]
405
406 return filtered
407
408 @staticmethod
409 def interfaces_get(item, owner, default=[]):
410 return item.get(owner, default)
411
412 @staticmethod
413 def interfaces_append(item, owner, append):
414 interfaces = item.setdefault(owner, [])
415 item[owner] = list(set(append).union(interfaces))
416
417 def interfaces_remove(self, item, owner, remove, path):
418 interfaces = item.get(owner, [])
419 item[owner] = list(set(interfaces).difference(remove))
420
421 if not item[owner]:
422 # remove the owner if there aren't any interfaces left
423 del item[owner]
424
425 if item:
426 # other owners remain
427 return
428
429 if self.cache.get_children(path):
430 # there are still references to this path
431 # from objects further down the tree.
432 # mark it for removal if that changes
433 self.cache.demote(path)
434 else:
435 # delete the entire path if everything is gone
436 del self.cache[path]
437
438 @dbus.service.method(obmc.mapper.MAPPER_IFACE, 's', 'a{sas}')
439 def GetObject(self, path):
440 o = self.cache_get(path)
441 if not o:
442 raise MapperNotFoundException(path)
443 return o
444
445 @dbus.service.method(obmc.mapper.MAPPER_IFACE, 'si', 'as')
446 def GetSubTreePaths(self, path, depth):
447 try:
448 return self.cache.iterkeys(path, depth)
449 except KeyError:
450 raise MapperNotFoundException(path)
451
452 @dbus.service.method(obmc.mapper.MAPPER_IFACE, 'si', 'a{sa{sas}}')
453 def GetSubTree(self, path, depth):
454 try:
455 return {x: y for x, y in self.cache.dataitems(path, depth)}
456 except KeyError:
457 raise MapperNotFoundException(path)
458
459 @staticmethod
460 def has_interfaces(item):
461 for owner in item.iterkeys():
462 if ObjectMapper.interfaces_get(item, owner):
463 return True
464 return False
465
466 @staticmethod
467 def is_association(interfaces):
468 return obmc.dbuslib.enums.OBMC_ASSOCIATIONS_IFACE in interfaces
469
470 def index_get(self, index, path, owners):
471 items = []
472 item = self.index.get(index, {})
473 item = item.get(path, {})
474 for o in owners:
475 items.extend(item.get(o, []))
476 return items
477
478 def index_append(self, index, path, owner, assoc):
479 item = self.index.setdefault(index, {})
480 item = item.setdefault(path, {})
481 item = item.setdefault(owner, [])
482 item.append(assoc)
483
484 def index_remove(self, index, path, owner, assoc):
485 index = self.index.get(index, {})
486 owners = index.get(path, {})
487 items = owners.get(owner, [])
488 if assoc in items:
489 items.remove(assoc)
490 if not items:
491 del owners[owner]
492 if not owners:
493 del index[path]
494
495 def dbus_get_associations(self, path, owner, obj):
496 iface = obmc.dbuslib.enums.OBMC_ASSOCIATIONS_IFACE
497 if 'associations' in obj[iface]:
498 return obj[iface]['associations']
499
500 # fallback to dbus
501 obj = self.bus.get_object(owner, path, introspect=False)
502 iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
503 return [(str(f), str(r), str(e)) for f, r, e in iface.Get(
504 obmc.dbuslib.enums.OBMC_ASSOCIATIONS_IFACE,
505 'associations')]
506
507 def index_get_associations(self, path, owners=[], direction='forward'):
508 forward = 'forward' if direction == 'forward' else 'reverse'
509 reverse = 'reverse' if direction == 'forward' else 'forward'
510
511 associations = []
512 if not owners:
513 index = self.index.get(forward, {})
514 owners = index.get(path, {}).keys()
515
516 # f: forward
517 # r: reverse
518 for rassoc in self.index_get(forward, path, owners):
519 elements = rassoc.split('/')
520 rtype = ''.join(elements[-1:])
521 fendpoint = '/'.join(elements[:-1])
522 for fassoc in self.index_get(reverse, fendpoint, owners):
523 elements = fassoc.split('/')
524 ftype = ''.join(elements[-1:])
525 rendpoint = '/'.join(elements[:-1])
526 if rendpoint != path:
527 continue
528 associations.append((ftype, rtype, fendpoint))
529
530 return associations
531
532 def update_association(self, path, removed, added):
533 iface = obmc.dbuslib.enums.OBMC_ASSOC_IFACE
534 create = [] if self.manager.get(path, False) else [iface]
535
536 if added and create:
537 self.manager.add(
538 path, Association(self.bus, path, added))
539 elif added:
540 self.manager.get(path).append(added)
541
542 obj = self.manager.get(path, None)
543 if obj and removed:
544 obj.remove(removed)
545
546 if obj and not obj.endpoints:
547 self.manager.remove(path)
548
549 delete = [] if self.manager.get(path, False) else [iface]
550
551 if create != delete:
552 self.update_interfaces(
553 path, self.unique, delete, create)
554
555 def update_associations(
556 self, path, owner, old, new, created=[], destroyed=[]):
557 added = list(set(new).difference(old))
558 removed = list(set(old).difference(new))
559 for forward, reverse, endpoint in added:
560 # update the index
561 forward_path = str(path + '/' + forward)
562 reverse_path = str(endpoint + '/' + reverse)
563 self.index_append(
564 'forward', path, owner, reverse_path)
565 self.index_append(
566 'reverse', endpoint, owner, forward_path)
567
568 # create the association if the endpoint exists
569 if not self.cache_get(endpoint):
570 continue
571
572 self.update_association(forward_path, [], [endpoint])
573 self.update_association(reverse_path, [], [path])
574
575 for forward, reverse, endpoint in removed:
576 # update the index
577 forward_path = str(path + '/' + forward)
578 reverse_path = str(endpoint + '/' + reverse)
579 self.index_remove(
580 'forward', path, owner, reverse_path)
581 self.index_remove(
582 'reverse', endpoint, owner, forward_path)
583
584 # destroy the association if it exists
585 self.update_association(forward_path, [endpoint], [])
586 self.update_association(reverse_path, [path], [])
587
588 # If the associations interface endpoint comes
589 # or goes create or destroy the appropriate
590 # associations
591 for path in created:
592 for forward, reverse, endpoint in \
593 self.index_get_associations(path, direction='reverse'):
594 forward_path = str(path + '/' + forward)
595 reverse_path = str(endpoint + '/' + reverse)
596 self.update_association(forward_path, [], [endpoint])
597 self.update_association(reverse_path, [], [path])
598
599 for path in destroyed:
600 for forward, reverse, endpoint in \
601 self.index_get_associations(path, direction='reverse'):
602 forward_path = str(path + '/' + forward)
603 reverse_path = str(endpoint + '/' + reverse)
604 self.update_association(forward_path, [endpoint], [])
605 self.update_association(reverse_path, [path], [])
606
607 @dbus.service.method(obmc.mapper.MAPPER_IFACE, 's', 'a{sa{sas}}')
608 def GetAncestors(self, path):
609 elements = filter(bool, path.split('/'))
610 paths = []
611 objs = {}
612 while elements:
613 elements.pop()
614 paths.append('/' + '/'.join(elements))
615 if path != '/':
616 paths.append('/')
617
618 for path in paths:
619 obj = self.cache_get(path)
620 if not obj:
621 continue
622 objs[path] = obj
623
624 return objs
625
626
627def server_main():
628 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
629 bus = dbus.SystemBus()
630 o = ObjectMapper(bus, obmc.mapper.MAPPER_PATH)
631 loop = gobject.MainLoop()
632
633 loop.run()