Merge lp:~mpontillo/maas/beaconing-service into lp:maas/trunk

Proposed by Mike Pontillo
Status: Rejected
Rejected by: MAAS Lander
Proposed branch: lp:~mpontillo/maas/beaconing-service
Merge into: lp:maas/trunk
Prerequisite: lp:~mpontillo/maas/beaconing-monitor
Diff against target: 241 lines (+118/-5)
4 files modified
setup.py (+1/-0)
src/provisioningserver/utils/beaconing.py (+2/-0)
src/provisioningserver/utils/services.py (+114/-4)
src/provisioningserver/utils/tcpip.py (+1/-1)
To merge this branch: bzr merge lp:~mpontillo/maas/beaconing-service
Reviewer Review Type Date Requested Status
MAAS Maintainers Pending
Review via email: mp+325486@code.launchpad.net

Commit message

Initial beaconing services.

To post a comment you must log in.
lp:~mpontillo/maas/beaconing-service updated
6090. By Mike Pontillo on 2017-06-12

Join multicast group with socket layer.

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

6090. By Mike Pontillo on 2017-06-12

Join multicast group with socket layer.

6089. By Mike Pontillo on 2017-06-12

Initial beaconing services.

Preview Diff

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