Merge lp:~mpontillo/maas/beaconing--replies-from-maas into lp:maas/trunk

Proposed by Mike Pontillo on 2017-06-14
Status: Rejected
Rejected by: MAAS Lander on 2017-06-22
Proposed branch: lp:~mpontillo/maas/beaconing--replies-from-maas
Merge into: lp:maas/trunk
Prerequisite: lp:~mpontillo/maas/maas-rack--send-beacons
Diff against target: 247 lines (+139/-5)
2 files modified
src/provisioningserver/utils/beaconing.py (+1/-1)
src/provisioningserver/utils/services.py (+138/-4)
To merge this branch: bzr merge lp:~mpontillo/maas/beaconing--replies-from-maas
Reviewer Review Type Date Requested Status
MAAS Maintainers 2017-06-14 Pending
Review via email: mp+325607@code.launchpad.net

Commit message

Add network monitoring service which replies to beacon packets.

To post a comment you must log in.
6091. By Mike Pontillo on 2017-06-14

Merge branch: lp:~mpontillo/maas/maas-rack--send-beacons

6092. By Mike Pontillo on 2017-06-14

Fix comment.

6093. By Mike Pontillo on 2017-06-19

Merge trunk.

MAAS Lander (maas-lander) wrote :

Transitioned to Git.

lp:maas has now moved from Bzr to Git.
Please propose your branches with Launchpad using Git.

git clone https://git.launchpad.net/maas

Unmerged revisions

6093. By Mike Pontillo on 2017-06-19

Merge trunk.

6092. By Mike Pontillo on 2017-06-14

Fix comment.

6091. By Mike Pontillo on 2017-06-14

Merge branch: lp:~mpontillo/maas/maas-rack--send-beacons

6090. By Mike Pontillo on 2017-06-14

Add beacon services.

6089. By Mike Pontillo on 2017-06-14

Add test command to send beacons out.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/provisioningserver/utils/beaconing.py'
2--- src/provisioningserver/utils/beaconing.py 2017-06-19 22:31:03 +0000
3+++ src/provisioningserver/utils/beaconing.py 2017-06-19 22:31:03 +0000
4@@ -156,7 +156,7 @@
5
6 def __init__(self, pkt_bytes: bytes):
7 """
8- Create a beaconing packet, given the specified upper-layer packet.
9+ Create a beaconing packet, given the specified payload bytes.
10
11 :param pkt_bytes: The input bytes of the beaconing packet.
12 :type pkt_bytes: bytes
13
14=== modified file 'src/provisioningserver/utils/services.py'
15--- src/provisioningserver/utils/services.py 2017-04-04 07:56:40 +0000
16+++ src/provisioningserver/utils/services.py 2017-06-19 22:31:03 +0000
17@@ -23,6 +23,7 @@
18 LegacyLogger,
19 )
20 from provisioningserver.rpc.exceptions import NoConnectionsAvailable
21+from provisioningserver.utils.beaconing import create_beacon_payload
22 from provisioningserver.utils.fs import (
23 get_maas_provision_command,
24 NamedLock,
25@@ -47,7 +48,10 @@
26 ProcessDone,
27 ProcessTerminated,
28 )
29-from twisted.internet.protocol import ProcessProtocol
30+from twisted.internet.protocol import (
31+ DatagramProtocol,
32+ ProcessProtocol,
33+)
34 from twisted.internet.threads import deferToThread
35
36
37@@ -129,9 +133,6 @@
38 The difference between `JSONPerLineProtocol` and `ProtocolForObserveARP`
39 is that the neighbour observation protocol needs to insert the interface
40 metadata into the resultant object before the callback.
41-
42- This also ensures that the spawned process is configured as a process
43- group leader for its own process group.
44 """
45
46 def __init__(self, interface, *args, **kwargs):
47@@ -147,6 +148,27 @@
48 log.msg("observe-arp[%s]:" % self.interface, line)
49
50
51+class ProtocolForObserveBeacons(JSONPerLineProtocol):
52+ """Protocol used when spawning `maas-rack observe-beacons`.
53+
54+ The difference between `JSONPerLineProtocol` and `ProtocolForObserveARP`
55+ is that the neighbour observation protocol needs to insert the interface
56+ metadata into the resultant object before the callback.
57+ """
58+
59+ def __init__(self, interface, *args, **kwargs):
60+ super().__init__(*args, **kwargs)
61+ self.interface = interface
62+
63+ def objectReceived(self, obj):
64+ obj['interface'] = self.interface
65+ super().objectReceived(obj)
66+
67+ def errLineReceived(self, line):
68+ line = line.decode("utf-8").rstrip()
69+ log.msg("observe-beacons[%s]:" % self.interface, line)
70+
71+
72 class ProtocolForObserveMDNS(JSONPerLineProtocol):
73 """Protocol used when spawning `maas-rack observe-mdns`.
74
75@@ -257,6 +279,30 @@
76 self.ifname, callback=self.callback)
77
78
79+class BeaconingService(ProcessProtocolService):
80+ """Service to spawn the per-interface device discovery subprocess."""
81+
82+ def __init__(self, ifname: str, callback: callable):
83+ self.ifname = ifname
84+ self.callback = callback
85+ super().__init__()
86+
87+ def getDescription(self) -> str:
88+ return "Beaconing process for %s" % self.ifname
89+
90+ def getProcessParameters(self):
91+ maas_rack_cmd = get_maas_provision_command().encode("utf-8")
92+ return [
93+ maas_rack_cmd,
94+ b"observe-beacons",
95+ self.ifname.encode("utf-8")
96+ ]
97+
98+ def createProcessProtocol(self):
99+ return ProtocolForObserveBeacons(
100+ self.ifname, callback=self.callback)
101+
102+
103 class MDNSResolverService(ProcessProtocolService):
104 """Service to spawn the per-interface device discovery subprocess."""
105
106@@ -278,6 +324,17 @@
107 return ProtocolForObserveMDNS(callback=self.callback)
108
109
110+class BeaconingSocketProtocol(DatagramProtocol):
111+ """Protocol to handle beaconing packets received from the socket layer."""
112+
113+ def datagramReceived(self, datagram, addr):
114+ # Note: Packets aren't processed in this path; we need this in order
115+ # to tell the socket layer we're listening to this port. Otherwise, the
116+ # stack will send ICMP destination (port) unreachable replies to anyone
117+ # trying to send us beacons.
118+ pass
119+
120+
121 class NetworksMonitoringLock(NamedLock):
122 """Host scoped lock to ensure only one network monitoring service runs."""
123
124@@ -324,6 +381,13 @@
125 self.interface_monitor.setName("updateInterfaces")
126 self.interface_monitor.clock = self.clock
127 self.interface_monitor.setServiceParent(self)
128+ self.beaconing_socket_protocol = BeaconingSocketProtocol()
129+ # Note: This returns a Twisted IListeningPort.
130+ self.beaconing_listen_port = reactor.listenMulticast(
131+ 5240, self.beaconing_socket_protocol, interface='::',
132+ listenMultiple=True)
133+ self.beacon_transport = self.beaconing_socket_protocol.transport
134+ self.beacon_transport.joinGroup("224.0.0.118")
135
136 def updateInterfaces(self):
137 """Update interfaces, catching and logging errors.
138@@ -388,6 +452,38 @@
139 This MUST be overridden in subclasses.
140 """
141
142+ def processBeacon(self, beacon, reply_address):
143+ payload = beacon['payload']
144+ beacon_type = payload['type']
145+ if beacon_type == "solicitation":
146+ receive_interface_info = {
147+ "name": beacon['interface'],
148+ "source_ip": beacon['source_ip'],
149+ "destination_ip": beacon['destination_ip'],
150+ "source_mac": beacon['source_mac'],
151+ "destination_mac": beacon['destination_mac'],
152+ }
153+ if 'vid' in beacon:
154+ receive_interface_info['vid'] = beacon['vid']
155+ data = {
156+ "interface": receive_interface_info
157+ }
158+ reply_bytes, _ = create_beacon_payload("advertisement", data)
159+ self.beacon_transport.write(reply_bytes, reply_address)
160+
161+ def reportBeacons(self, beacons):
162+ """Receives a report of an observed beacon packet."""
163+ for beacon in beacons:
164+ log.msg("Received beacon: %r" % beacon)
165+ reply_ip = beacon['source_ip']
166+ reply_port = beacon['source_port']
167+ if ':' not in reply_ip:
168+ # Since we opened an IPv6-compatible socket, need IPv6 syntax
169+ # here to send to IPv4 addresses.
170+ reply_ip = '::ffff:' + reply_ip
171+ reply_address = (reply_ip, reply_port)
172+ self.processBeacon(beacon, reply_address)
173+
174 def stopService(self):
175 """Stop the service.
176
177@@ -395,6 +491,7 @@
178 """
179 d = super().stopService()
180 d.addBoth(callOut, self._releaseSoleResponsibility)
181+ self.beaconing_listen_port.stopListening()
182 return d
183
184 def _assumeSoleResponsibility(self):
185@@ -473,6 +570,13 @@
186 service.setName("neighbour_discovery:" + ifname)
187 service.setServiceParent(self)
188
189+ def _startBeaconing(self, ifname):
190+ """"Start neighbour discovery service on the specified interface."""
191+ service = BeaconingService(ifname, self.reportBeacons)
192+ service.clock = self.clock
193+ service.setName("beaconing:" + ifname)
194+ service.setServiceParent(self)
195+
196 def _startMDNSDiscoveryService(self):
197 """Start resolving mDNS entries on attached networks."""
198 try:
199@@ -520,6 +624,30 @@
200 maaslog.info(
201 "Stopped neighbour observation service for %s." % ifname)
202
203+ def _startBeaconingServices(self, new_interfaces):
204+ """Start monitoring services for the specified set of interfaces."""
205+ for ifname in new_interfaces:
206+ # Sanity check to ensure the service isn't already started.
207+ try:
208+ self.getServiceNamed("beaconing:" + ifname)
209+ except KeyError:
210+ # This is an expected exception. (The call inside the `try`
211+ # is only necessary to ensure the service doesn't exist.)
212+ self._startBeaconing(ifname)
213+
214+ def _stopBeaconingServices(self, deleted_interfaces):
215+ """Stop monitoring services for the specified set of interfaces."""
216+ for ifname in deleted_interfaces:
217+ try:
218+ service = self.getServiceNamed("beaconing:" + ifname)
219+ except KeyError:
220+ # Service doesn't exist, so no need to stop it.
221+ pass
222+ else:
223+ service.disownServiceParent()
224+ maaslog.info(
225+ "Stopped beaconing service for %s." % ifname)
226+
227 def _shouldMonitorMDNS(self, monitoring_state):
228 # If any interface is configured for mDNS, we must start the monitoring
229 # process. (You cannot select interfaces when using `avahi-browse`.)
230@@ -591,11 +719,17 @@
231 log.msg("Starting neighbour discovery for interfaces: %r" % (
232 new_interfaces))
233 self._startNeighbourDiscoveryServices(new_interfaces)
234+ # XXX mpontillo 2017-07-12: for testing, just start beaconing
235+ # services on all the interfaces enabled for active discovery.
236+ self._startBeaconingServices(new_interfaces)
237 if len(deleted_interfaces) > 0:
238 log.msg(
239 "Stopping neighbour discovery for interfaces: %r" % (
240 deleted_interfaces))
241 self._stopNeighbourDiscoveryServices(deleted_interfaces)
242+ # XXX mpontillo 2017-07-12: this should be separately configured.
243+ # (see similar comment in the 'start' path above.)
244+ self._stopBeaconingServices(deleted_interfaces)
245 self._monitored = monitored_interfaces
246
247 def _interfacesRecorded(self, interfaces):