1# Copyright 2015 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import collections
6import dbus
7import dbus.bus
8import dbus.service
9import logging
10import uuid
11
12
13from autotest_lib.client.cros import dbus_util
14from autotest_lib.client.cros.tendo import peerd_dbus_helper
15from autotest_lib.client.cros.tendo.n_faced_peerd import dbus_property_exposer
16from autotest_lib.client.cros.tendo.n_faced_peerd import peer
17from autotest_lib.client.cros.tendo.n_faced_peerd import service
18
19# A tuple of a bus name that sent us an ExposeService message, and an
20# object responsible for watching for the death of that bus name's
21# DBus connection.
22SenderWatch = collections.namedtuple('SenderWatch', ['sender', 'watch'])
23
24
25IGNORED_MONITORING_TOKEN_VALUE = 'This is a monitoring token.'
26class InvalidMonitoringTokenException(Exception):
27    """Self explanatory."""
28
29
30class Manager(dbus_property_exposer.DBusPropertyExposer):
31    """Represents an instance of org.chromium.peerd.Manager."""
32
33    def __init__(self, bus, ip_address, on_service_modified, unique_name,
34                 object_manager):
35        """Construct an instance of Manager.
36
37        @param bus: dbus.Bus object to export this object on.
38        @param ip_address: string IP address (e.g. '127.0.01').
39        @param on_service_modified: callback that takes a Manager instance and
40                a service ID.  We'll call this whenever we Expose/Remove a
41                service via the DBus API.
42        @param unique_name: string DBus well known name to claim on DBus.
43        @param object_manager: an instance of ObjectManager.
44
45        """
46        super(Manager, self).__init__(bus,
47                                      peerd_dbus_helper.DBUS_PATH_MANAGER,
48                                      peerd_dbus_helper.DBUS_INTERFACE_MANAGER)
49        self._bus = bus
50        self._object_manager = object_manager
51        self._peer_counter = 0
52        self._peers = dict()
53        self._ip_address = ip_address
54        self._on_service_modified = on_service_modified
55        # A map from service_ids to dbus.bus.NameOwnerWatch objects.
56        self._watches = dict()
57        self.self_peer = peer.Peer(self._bus,
58                                   peerd_dbus_helper.DBUS_PATH_SELF,
59                                   uuid.uuid4(),
60                                   self._object_manager,
61                                   is_self=True)
62        # TODO(wiley) Expose monitored technologies property
63        self._object_manager.claim_interface(
64                peerd_dbus_helper.DBUS_PATH_MANAGER,
65                peerd_dbus_helper.DBUS_INTERFACE_MANAGER,
66                self.property_getter)
67        if (self._bus.request_name(unique_name) !=
68                dbus.bus.REQUEST_NAME_REPLY_PRIMARY_OWNER):
69            raise Exception('Failed to claim name %s' % unique_name)
70
71
72    def _on_name_owner_changed(self, service_id, owner):
73        """Callback that removes a service when the owner disconnects from DBus.
74
75        @param service_id: string service_id of service to remove.
76        @param owner: dbus.String identifier of service owner.
77
78        """
79        owner = dbus_util.dbus2primitive(owner)
80        logging.debug('Name owner for service=%s changed to "%s".',
81                      service_id, owner)
82        if not owner:
83            self.RemoveExposedService(service_id)
84
85
86    def close(self):
87        """Release resources held by this object and child objects."""
88        # TODO(wiley) call close on self and remote peers.
89        raise NotImplementedError('Manager.close() does not work properly')
90
91
92    def add_remote_peer(self, remote_peer):
93        """Add a remote peer to this object.
94
95        For any given face of NFacedPeerd, the other N - 1 faces are treated
96        as "remote peers" that we instantly discover changes on.
97
98        @param remote_peer: Peer object.  Should be the |self_peer| of another
99                instance of Manager.
100
101        """
102        logging.info('Adding remote peer %s', remote_peer.uuid)
103        self._peer_counter += 1
104        peer_path = '%s%d' % (peerd_dbus_helper.PEER_PATH_PREFIX,
105                              self._peer_counter)
106        self._peers[remote_peer.uuid] = peer.Peer(
107                self._bus, peer_path, remote_peer.uuid, self._object_manager)
108
109
110    def on_remote_service_modified(self, remote_peer, service_id):
111        """Cause this face to update its view of a remote peer.
112
113        @param remote_peer: Peer object.  Should be the |self_peer| of another
114                instance of Manager.
115        @param service_id: string service ID of remote service that has changed.
116                Note that this service may have been removed.
117
118        """
119        local_peer = self._peers[remote_peer.uuid]
120        remote_service = remote_peer.services.get(service_id)
121        if remote_service is not None:
122            logging.info('Exposing remote service: %s', service_id)
123            local_peer.update_service(remote_service.service_id,
124                                      remote_service.service_info,
125                                      remote_service.ip_info)
126        else:
127            logging.info('Removing remote service: %s', service_id)
128            local_peer.remove_service(service_id)
129
130
131    @dbus.service.method(
132            dbus_interface=peerd_dbus_helper.DBUS_INTERFACE_MANAGER,
133            in_signature='sa{ss}a{sv}', out_signature='',
134            sender_keyword='sender')
135    def ExposeService(self, service_id, service_info, options, sender=None):
136        """Implementation of org.chromium.peerd.Manager.ExposeService().
137
138        @param service_id: see DBus API documentation.
139        @param service_info: see DBus API documentation.
140        @param options: see DBus API documentation.
141        @param sender: string DBus connection ID of caller.  Must match
142                |sender_keyword| value in decorator.
143
144        """
145        if (service_id in self._watches and
146                sender != self._watches[service_id].sender):
147            logging.error('Asked to advertise a duplicate service by %s. '
148                          'Expected %s.',
149                          sender, self._watches[service_id].sender)
150            raise Exception('Will not advertise duplicate services.')
151        logging.info('Exposing service %s', service_id)
152        port = options.get('mdns', dict()).get('port', 0)
153        self.self_peer.update_service(
154                service_id, service_info,
155                service.IpInfo(addr=self._ip_address, port=port))
156        if service_id not in self._watches:
157            cb = lambda owner: self._on_name_owner_changed(service_id, owner)
158            watch = dbus.bus.NameOwnerWatch(self._bus, sender, cb)
159            self._watches[service_id] = SenderWatch(sender, watch)
160        self._on_service_modified(self, service_id)
161
162
163    @dbus.service.method(
164            dbus_interface=peerd_dbus_helper.DBUS_INTERFACE_MANAGER,
165            in_signature='s', out_signature='')
166    def RemoveExposedService(self, service_id):
167        """Implementation of org.chromium.peerd.Manager.RemoveExposedService().
168
169        @param service_id: see DBus API documentation.
170
171        """
172        logging.info('Removing service %s', service_id)
173        self._watches.pop(service_id, None)
174        self.self_peer.remove_service(service_id)
175        self._on_service_modified(self, service_id)
176
177
178    @dbus.service.method(
179            dbus_interface=peerd_dbus_helper.DBUS_INTERFACE_MANAGER,
180            in_signature='asa{sv}', out_signature='s')
181    def StartMonitoring(self, technologies, options):
182        """Fake implementation of org.chromium.peerd.Manager.StartMonitoring().
183
184        We do not monitor anything in NFacedPeerd.
185
186        @param technologies: See DBus API documentation.
187        @param options: See DBus API documentation.
188
189        """
190        return IGNORED_MONITORING_TOKEN_VALUE
191
192
193    @dbus.service.method(
194            dbus_interface=peerd_dbus_helper.DBUS_INTERFACE_MANAGER,
195            in_signature='s', out_signature='')
196    def StopMonitoring(self, monitoring_token):
197        """Fake implementation of org.chromium.peerd.Manager.StopMonitoring().
198
199        We do not monitor anything in NFacedPeerd
200
201        @param monitoring_token: See DBus API documentation.
202
203        """
204        if monitoring_token != IGNORED_MONITORING_TOKEN_VALUE:
205            raise InvalidMonitoringTokenException()
206
207
208    @dbus.service.method(
209            dbus_interface=peerd_dbus_helper.DBUS_INTERFACE_MANAGER,
210            in_signature='', out_signature='s')
211    def Ping(self):
212        """Implementation of org.chromium.peerd.Manager.Ping()."""
213        return 'Hello world!'
214