Merge ~bjornt/maas:bug-2043970-vlan-interfaces into maas:master

Proposed by Björn Tillenius
Status: Merged
Approved by: Björn Tillenius
Approved revision: a1f5f8652ff88f20b7f41b5990f594ccd02c876b
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~bjornt/maas:bug-2043970-vlan-interfaces
Merge into: maas:master
Diff against target: 464 lines (+246/-80)
2 files modified
src/metadataserver/builtin_scripts/network.py (+65/-60)
src/metadataserver/builtin_scripts/tests/test_network.py (+181/-20)
Reviewer Review Type Date Requested Status
Alexsander de Souza Approve
MAAS Lander Approve
Review via email: mp+465065@code.launchpad.net

Commit message

Bug #2043970: MAAS 3.2.9 creates for Calico Interfaces 80.000 fabrics

Redo the fix for bug #2043970.

In some cases we need to create VLANs/fabrics for interfaces that don't have
links. For example for VLAN interfaces, often it's the VLAN interface that has
an address, while the underlying physical interface often is without any
address. In that case, the physical interface should have a VLAN that is on the
same fabric as the VLAN interface.

Also removed the check whether a machine is a controller. A controller is not
special. It's a deployed machine. If Calico is installed on a controller, we
should not create useless fabrics.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b bug-2043970-vlan-interfaces lp:~bjornt/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/5386/console
COMMIT: fe814c60b331edb7c7ffaeb3185915e64463566b

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b bug-2043970-vlan-interfaces lp:~bjornt/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/5387/console
COMMIT: 00992cb182bda432edbb20d98a0a3471a27a92a9

review: Needs Fixing
Revision history for this message
Björn Tillenius (bjornt) wrote :

I put this back to work in progress, since there are currently two test failures, related to setting the boot interface.

Revision history for this message
Björn Tillenius (bjornt) wrote :

All tests should pass now.

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b bug-2043970-vlan-interfaces lp:~bjornt/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: a1f5f8652ff88f20b7f41b5990f594ccd02c876b

review: Approve
Revision history for this message
Alexsander de Souza (alexsander-souza) wrote :

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/metadataserver/builtin_scripts/network.py b/src/metadataserver/builtin_scripts/network.py
2index 9282986..58f8120 100644
3--- a/src/metadataserver/builtin_scripts/network.py
4+++ b/src/metadataserver/builtin_scripts/network.py
5@@ -13,7 +13,7 @@ from maasserver.models.interface import (
6 )
7 from maasserver.models.staticipaddress import StaticIPAddress
8 from maasserver.models.subnet import Subnet
9-from maasserver.models.vlan import VLAN
10+from maasserver.models.vlan import DEFAULT_VID, VLAN
11 from maasserver.utils.orm import transactional
12 from provisioningserver.events import EVENT_TYPES
13 from provisioningserver.logger import get_maas_logger
14@@ -266,7 +266,7 @@ def update_physical_interface(
15 if hints is not None:
16 new_vlan = guess_vlan_from_hints(node, name, hints)
17 if new_vlan is None:
18- new_vlan = guess_vlan_for_interface(node, links)
19+ new_vlan = guess_vlan_for_interface(node, interface, links)
20 if new_vlan is not None:
21 interface.vlan = new_vlan
22 update_fields.add("vlan")
23@@ -362,16 +362,19 @@ def update_vlan_interface(node, name, network, links):
24 parent_nic = Interface.objects.get(
25 node_config=node.current_config, name=parent_name
26 )
27+ parent_fabric = None
28+ if parent_nic.vlan is not None:
29+ parent_fabric = parent_nic.vlan.fabric
30 links_vlan = get_interface_vlan_from_links(links)
31+ interface = VLANInterface.objects.filter(
32+ node_config=node.current_config,
33+ name=name,
34+ parents__id=parent_nic.id,
35+ vlan__vid=vid,
36+ ).first()
37 if links_vlan:
38 vlan = links_vlan
39- if parent_nic.vlan.fabric_id != vlan.fabric_id:
40- # XXX: We should surface this error to the API and UI, since
41- # it's something the user needs to fix.
42- maaslog.error(
43- f"Interface '{parent_nic.name}' on controller '{node.hostname}' "
44- f"is not on the same fabric as VLAN interface '{name}'."
45- )
46+ parent_fabric = vlan.fabric
47 if links_vlan.vid != vid:
48 maaslog.error(
49 f"VLAN interface '{name}' reports VLAN {vid} "
50@@ -379,41 +382,31 @@ def update_vlan_interface(node, name, network, links):
51 )
52 else:
53 vlan = None
54+ if interface is not None and interface.vlan is not None:
55+ vlan = interface.vlan
56+ parent_fabric = vlan.fabric
57+ elif parent_fabric is None:
58+ maaslog.info(
59+ f"Unable to detect fabric VLAN Interface '{name}', using default"
60+ )
61+ parent_fabric = Fabric.objects.get_default_fabric()
62+
63+ if parent_nic.vlan_id is None:
64+ parent_vlan, _ = VLAN.objects.get_or_create(
65+ fabric=parent_fabric,
66+ vid=DEFAULT_VID,
67+ )
68+ parent_nic.vlan = parent_vlan
69+ parent_nic.save()
70
71 from metadataserver.builtin_scripts.hooks import HARDWARE_SYNC_ACTIONS
72
73- interface = VLANInterface.objects.filter(
74- node_config=node.current_config,
75- name=name,
76- parents__id=parent_nic.id,
77- vlan__vid=vid,
78- ).first()
79- if interface is None:
80- if vlan is None:
81- # Since no suitable VLAN is found, create a new one in the same
82- # fabric as the parent interface.
83- if parent_nic.vlan:
84- vlan, _ = VLAN.objects.get_or_create(
85- fabric=parent_nic.vlan.fabric, vid=vid
86- )
87- elif network["addresses"]: # use IP address to find VLAN
88- address = network["addresses"][0]["address"]
89- try:
90- subnet = Subnet.objects.get_subnet_for_ip(address)
91- except Subnet.DoesNotExist:
92- pass
93- else:
94- vlan, _ = VLAN.objects.get_or_create(
95- fabric=subnet.vlan.fabric, vid=vid
96- )
97- if vlan is None:
98- maaslog.error(
99- f"Unable to detect fabric VLAN Interface '{name}', using default"
100- )
101- vlan, _ = VLAN.objects.get_or_create(
102- fabric=Fabric.objects.get_default_fabric(), vid=vid
103- )
104+ if vlan is None:
105+ # Since no suitable VLAN is found, create a new one in the same
106+ # fabric as the parent interface.
107+ vlan, _ = VLAN.objects.get_or_create(fabric=parent_fabric, vid=vid)
108
109+ if interface is None:
110 interface, created = VLANInterface.objects.get_or_create(
111 node_config=node.current_config,
112 name=name,
113@@ -435,6 +428,14 @@ def update_vlan_interface(node, name, network, links):
114 node, interface, HARDWARE_SYNC_ACTIONS.UPDATED
115 )
116
117+ if parent_nic.vlan.fabric_id != vlan.fabric_id:
118+ # XXX: We should surface this error to the API and UI, since
119+ # it's something the user needs to fix.
120+ maaslog.error(
121+ f"Interface '{parent_nic.name}' on controller '{node.hostname}' "
122+ f"is not on the same fabric as VLAN interface '{name}'."
123+ )
124+
125 update_links(node, interface, links, force_vlan=True)
126 return interface
127
128@@ -507,10 +508,6 @@ def update_parent_vlans(node, interface, parent_nics, update_ip_addresses):
129
130
131 def auto_vlan_creation(node) -> bool:
132- if node.is_controller:
133- # Always create controller's vlans
134- return True
135-
136 if node.status == NODE_STATUS.DEPLOYED:
137 # don't create empty vlan/fabric for interfaces discovered by HW sync,
138 # they cannot be used.
139@@ -519,25 +516,26 @@ def auto_vlan_creation(node) -> bool:
140 return Config.objects.get_config("auto_vlan_creation", True)
141
142
143-def guess_vlan_for_interface(node, links):
144+def guess_vlan_for_interface(node, interface, links):
145 # Make sure that the VLAN on the interface is correct. When
146 # links exists on this interface we place it into the correct
147 # VLAN. If it cannot be determined and its a new interface it
148 # gets placed on its own fabric.
149 new_vlan = get_interface_vlan_from_links(links)
150- if new_vlan is None and auto_vlan_creation(node):
151- # If the default VLAN on the default fabric has no interfaces
152- # associated with it, the first interface will be placed there
153- # (rather than creating a new fabric).
154- default_vlan = VLAN.objects.get_default_vlan()
155- interfaces_on_default_vlan = Interface.objects.filter(
156- vlan=default_vlan
157- ).exists()
158- if not interfaces_on_default_vlan:
159- new_vlan = default_vlan
160- else:
161- new_fabric = Fabric.objects.create()
162- new_vlan = new_fabric.get_default_vlan()
163+ if new_vlan is None:
164+ if links:
165+ update_links(node, interface, links)
166+ new_vlan = get_interface_vlan_from_links(links)
167+ elif auto_vlan_creation(node):
168+ default_vlan = VLAN.objects.get_default_vlan()
169+ interfaces_on_default_vlan = Interface.objects.filter(
170+ vlan=default_vlan
171+ ).exists()
172+ if not interfaces_on_default_vlan:
173+ new_vlan = default_vlan
174+ else:
175+ new_fabric = Fabric.objects.create()
176+ new_vlan = new_fabric.get_default_vlan()
177 return new_vlan
178
179
180@@ -636,8 +634,15 @@ def update_links(
181 if use_interface_vlan and interface.vlan is not None:
182 vlan = interface.vlan
183 elif links:
184- fabric = Fabric.objects.create()
185- vlan = fabric.get_default_vlan()
186+ default_vlan = VLAN.objects.get_default_vlan()
187+ interfaces_on_default_vlan = Interface.objects.filter(
188+ vlan=default_vlan
189+ ).exists()
190+ if not interfaces_on_default_vlan:
191+ vlan = default_vlan
192+ else:
193+ new_fabric = Fabric.objects.create()
194+ vlan = new_fabric.get_default_vlan()
195 interface.vlan = vlan
196 interface.save()
197
198diff --git a/src/metadataserver/builtin_scripts/tests/test_network.py b/src/metadataserver/builtin_scripts/tests/test_network.py
199index 3d93887..0ebf607 100644
200--- a/src/metadataserver/builtin_scripts/tests/test_network.py
201+++ b/src/metadataserver/builtin_scripts/tests/test_network.py
202@@ -90,7 +90,9 @@ class TestUpdateInterfaces(MAASServerTestCase, UpdateInterfacesMixin):
203 eth0 = node.current_config.interface_set.get(name="eth0")
204 self.assertEqual(eth0.numa_node, node.default_numanode)
205
206- def test_dont_create_default_vlan_for_deployed_machines(self):
207+ def test_dont_create_default_vlan_for_missing_links_deployed_machines(
208+ self,
209+ ):
210 node = factory.make_Machine(status=NODE_STATUS.DEPLOYED)
211 data = FakeCommissioningData()
212 data_eth0 = data.create_physical_network(
213@@ -104,6 +106,174 @@ class TestUpdateInterfaces(MAASServerTestCase, UpdateInterfacesMixin):
214 )
215 self.assertIsNone(eth0.vlan)
216
217+ def test_vlan_interfaces_with_known_link_deployed_machine(self):
218+ node = factory.make_Machine(status=NODE_STATUS.DEPLOYED)
219+ vlan123 = factory.make_VLAN(vid=123)
220+ vlan456 = factory.make_VLAN(vid=456, fabric=vlan123.fabric)
221+ factory.make_Subnet(cidr="10.10.10.0/24", vlan=vlan123)
222+ factory.make_Subnet(cidr="10.10.20.0/24", vlan=vlan456)
223+ data = FakeCommissioningData()
224+ data_eth0 = data.create_physical_network(
225+ "eth0",
226+ mac_address="11:11:11:11:11:11",
227+ )
228+ data_eth0.state = "up"
229+ vlan_network123 = data.create_vlan_network(
230+ "eth0.123", parent=data_eth0, vid=123
231+ )
232+ vlan_network123.addresses = [LXDAddress("10.10.10.10", 24)]
233+ vlan_network456 = data.create_vlan_network(
234+ "eth0.456", parent=data_eth0, vid=456
235+ )
236+ vlan_network456.addresses = [LXDAddress("10.10.20.10", 24)]
237+ self.update_interfaces(node, data)
238+ eth0 = PhysicalInterface.objects.get(
239+ name="eth0", node_config=node.current_config
240+ )
241+ eth0_vlan123 = Interface.objects.get(
242+ name="eth0.123", node_config=node.current_config
243+ )
244+ eth0_vlan456 = Interface.objects.get(
245+ name="eth0.456", node_config=node.current_config
246+ )
247+ self.assertEqual(INTERFACE_TYPE.VLAN, eth0_vlan123.type)
248+ self.assertEqual(123, eth0_vlan123.vlan.vid)
249+ self.assertEqual(INTERFACE_TYPE.VLAN, eth0_vlan456.type)
250+ self.assertEqual(456, eth0_vlan456.vlan.vid)
251+ self.assertEqual(
252+ eth0_vlan123.vlan.fabric_id, eth0_vlan456.vlan.fabric_id
253+ )
254+ self.assertIsNotNone(eth0.vlan_id)
255+ self.assertNotIn(
256+ eth0.vlan_id, [eth0_vlan456.vlan_id, eth0_vlan123.vlan_id]
257+ )
258+ self.assertEqual(eth0.vlan.fabric_id, eth0_vlan123.vlan.fabric_id)
259+ self.assertEqual(eth0.vlan.fabric_id, eth0_vlan456.vlan.fabric_id)
260+
261+ def test_vlan_interfaces_with_new_known_link_deployed_machine(self):
262+ node = factory.make_Machine(status=NODE_STATUS.DEPLOYED)
263+ vlan456 = factory.make_VLAN(vid=456)
264+ factory.make_Subnet(cidr="10.10.20.0/24", vlan=vlan456)
265+ data = FakeCommissioningData()
266+ data_eth0 = data.create_physical_network(
267+ "eth0",
268+ mac_address="11:11:11:11:11:11",
269+ )
270+ data_eth0.state = "up"
271+ vlan_network123 = data.create_vlan_network(
272+ "eth0.123", parent=data_eth0, vid=123
273+ )
274+ vlan_network123.addresses = [LXDAddress("10.10.10.10", 24)]
275+ vlan_network456 = data.create_vlan_network(
276+ "eth0.456", parent=data_eth0, vid=456
277+ )
278+ vlan_network456.addresses = [LXDAddress("10.10.20.10", 24)]
279+ self.update_interfaces(node, data)
280+ eth0 = PhysicalInterface.objects.get(
281+ name="eth0", node_config=node.current_config
282+ )
283+ eth0_vlan123 = Interface.objects.get(
284+ name="eth0.123", node_config=node.current_config
285+ )
286+ eth0_vlan456 = Interface.objects.get(
287+ name="eth0.456", node_config=node.current_config
288+ )
289+ self.assertEqual(INTERFACE_TYPE.VLAN, eth0_vlan123.type)
290+ self.assertEqual(123, eth0_vlan123.vlan.vid)
291+ self.assertEqual(INTERFACE_TYPE.VLAN, eth0_vlan456.type)
292+ self.assertEqual(456, eth0_vlan456.vlan.vid)
293+ self.assertEqual(
294+ eth0_vlan123.vlan.fabric_id, eth0_vlan456.vlan.fabric_id
295+ )
296+ self.assertIsNotNone(eth0.vlan_id)
297+ self.assertNotIn(
298+ eth0.vlan_id, [eth0_vlan456.vlan_id, eth0_vlan123.vlan_id]
299+ )
300+ self.assertEqual(eth0.vlan.fabric_id, eth0_vlan123.vlan.fabric_id)
301+ self.assertEqual(eth0.vlan.fabric_id, eth0_vlan456.vlan.fabric_id)
302+
303+ def test_vlan_interfaces_with_new_unknown_link_deployed_machine(self):
304+ node = factory.make_Machine(status=NODE_STATUS.DEPLOYED)
305+ vlan123 = factory.make_VLAN(vid=123)
306+ factory.make_Subnet(cidr="10.10.10.0/24", vlan=vlan123)
307+ data = FakeCommissioningData()
308+ data_eth0 = data.create_physical_network(
309+ "eth0",
310+ mac_address="11:11:11:11:11:11",
311+ )
312+ data_eth0.state = "up"
313+ vlan_network123 = data.create_vlan_network(
314+ "eth0.123", parent=data_eth0, vid=123
315+ )
316+ vlan_network123.addresses = [LXDAddress("10.10.10.10", 24)]
317+ vlan_network456 = data.create_vlan_network(
318+ "eth0.456", parent=data_eth0, vid=456
319+ )
320+ vlan_network456.addresses = [LXDAddress("10.10.20.10", 24)]
321+ self.update_interfaces(node, data)
322+ eth0 = PhysicalInterface.objects.get(
323+ name="eth0", node_config=node.current_config
324+ )
325+ eth0_vlan123 = Interface.objects.get(
326+ name="eth0.123", node_config=node.current_config
327+ )
328+ eth0_vlan456 = Interface.objects.get(
329+ name="eth0.456", node_config=node.current_config
330+ )
331+ self.assertEqual(INTERFACE_TYPE.VLAN, eth0_vlan123.type)
332+ self.assertEqual(123, eth0_vlan123.vlan.vid)
333+ self.assertEqual(INTERFACE_TYPE.VLAN, eth0_vlan456.type)
334+ self.assertEqual(456, eth0_vlan456.vlan.vid)
335+ self.assertEqual(
336+ eth0_vlan123.vlan.fabric_id, eth0_vlan456.vlan.fabric_id
337+ )
338+ self.assertIsNotNone(eth0.vlan_id)
339+ self.assertNotIn(
340+ eth0.vlan_id, [eth0_vlan456.vlan_id, eth0_vlan123.vlan_id]
341+ )
342+ self.assertEqual(eth0.vlan.fabric_id, eth0_vlan123.vlan.fabric_id)
343+ self.assertEqual(eth0.vlan.fabric_id, eth0_vlan456.vlan.fabric_id)
344+
345+ def test_vlan_interfaces_with_unknown_link_deployed_machine(self):
346+ node = factory.make_Machine(status=NODE_STATUS.DEPLOYED)
347+ data = FakeCommissioningData()
348+ data_eth0 = data.create_physical_network(
349+ "eth0",
350+ mac_address="11:11:11:11:11:11",
351+ )
352+ data_eth0.state = "up"
353+ vlan_network123 = data.create_vlan_network(
354+ "eth0.123", parent=data_eth0, vid=123
355+ )
356+ vlan_network123.addresses = [LXDAddress("10.10.10.10", 24)]
357+ vlan_network456 = data.create_vlan_network(
358+ "eth0.456", parent=data_eth0, vid=456
359+ )
360+ vlan_network456.addresses = [LXDAddress("10.10.20.10", 24)]
361+ self.update_interfaces(node, data)
362+ eth0 = PhysicalInterface.objects.get(
363+ name="eth0", node_config=node.current_config
364+ )
365+ eth0_vlan123 = Interface.objects.get(
366+ name="eth0.123", node_config=node.current_config
367+ )
368+ eth0_vlan456 = Interface.objects.get(
369+ name="eth0.456", node_config=node.current_config
370+ )
371+ self.assertEqual(INTERFACE_TYPE.VLAN, eth0_vlan123.type)
372+ self.assertEqual(123, eth0_vlan123.vlan.vid)
373+ self.assertEqual(INTERFACE_TYPE.VLAN, eth0_vlan456.type)
374+ self.assertEqual(456, eth0_vlan456.vlan.vid)
375+ self.assertEqual(
376+ eth0_vlan123.vlan.fabric_id, eth0_vlan456.vlan.fabric_id
377+ )
378+ self.assertIsNotNone(eth0.vlan_id)
379+ self.assertNotIn(
380+ eth0.vlan_id, [eth0_vlan456.vlan_id, eth0_vlan123.vlan_id]
381+ )
382+ self.assertEqual(eth0.vlan.fabric_id, eth0_vlan123.vlan.fabric_id)
383+ self.assertEqual(eth0.vlan.fabric_id, eth0_vlan456.vlan.fabric_id)
384+
385 def test_dont_create_default_vlan_if_disabled(self):
386 Config.objects.set_config(name="auto_vlan_creation", value=False)
387 node = factory.make_Machine(status=NODE_STATUS.NEW)
388@@ -141,9 +311,7 @@ class TestUpdateInterfaces(MAASServerTestCase, UpdateInterfacesMixin):
389 self.assertEqual("11:11:11:11:11:11", eth0.mac_address)
390 self.assertTrue(eth0.enabled)
391 self.assertTrue(eth0.link_connected)
392- self.assertEqual(
393- Fabric.objects.get_default_fabric().get_default_vlan(), eth0.vlan
394- )
395+ self.assertIsNone(eth0.vlan)
396 self.assertEqual([], list(eth0.parents.all()))
397 eth1 = PhysicalInterface.objects.get(
398 name="eth1", node_config=controller.current_config
399@@ -550,10 +718,9 @@ class TestUpdateInterfaces(MAASServerTestCase, UpdateInterfacesMixin):
400 name="eth0", node_config=controller.current_config
401 )
402
403- default_vlan = Fabric.objects.get_default_fabric().get_default_vlan()
404 self.assertEqual(INTERFACE_TYPE.PHYSICAL, eth0.type)
405 self.assertEqual("11:11:11:11:11:11", eth0.mac_address)
406- self.assertEqual(default_vlan, eth0.vlan)
407+ self.assertIsNone(eth0.vlan)
408 self.assertTrue(eth0.enabled)
409
410 eth0_addresses = list(eth0.ip_addresses.all())
411@@ -1106,13 +1273,13 @@ class TestUpdateInterfaces(MAASServerTestCase, UpdateInterfacesMixin):
412 maaslog.method_calls,
413 [
414 call.error(
415- f"Interface 'eth0' on controller '{controller.hostname}' "
416- f"is not on the same fabric as VLAN interface '{vlan_interface.name}'."
417- ),
418- call.error(
419 f"VLAN interface '{vlan_interface.name}' reports VLAN {vid_on_fabric} "
420 f"but links are on VLAN {other_vlan.vid}"
421 ),
422+ call.error(
423+ f"Interface 'eth0' on controller '{controller.hostname}' "
424+ f"is not on the same fabric as VLAN interface '{vlan_interface.name}'."
425+ ),
426 ]
427 * passes,
428 )
429@@ -1273,13 +1440,13 @@ class TestUpdateInterfaces(MAASServerTestCase, UpdateInterfacesMixin):
430 maaslog.method_calls,
431 [
432 call.error(
433- f"Interface 'eth0' on controller '{controller.hostname}' "
434- f"is not on the same fabric as VLAN interface '{vlan_interface.name}'."
435- ),
436- call.error(
437 f"VLAN interface '{vlan_interface.name}' reports VLAN {other_vlan.vid} "
438 f"but links are on VLAN {new_vlan.vid}"
439 ),
440+ call.error(
441+ f"Interface 'eth0' on controller '{controller.hostname}' "
442+ f"is not on the same fabric as VLAN interface '{vlan_interface.name}'."
443+ ),
444 ]
445 * passes,
446 )
447@@ -2227,17 +2394,11 @@ class TestUpdateInterfaces(MAASServerTestCase, UpdateInterfacesMixin):
448
449 self.update_interfaces(controller, data)
450
451- eth0 = get_one(
452- PhysicalInterface.objects.filter(
453- node_config=controller.current_config, name="eth0"
454- )
455- )
456 eth1 = get_one(
457 PhysicalInterface.objects.filter(
458 node_config=controller.current_config, name="eth1"
459 )
460 )
461- self.assertIsNotNone(eth0.vlan)
462 self.assertIsNone(eth1.vlan)
463
464 def test_subnet_seen_on_second_controller_does_not_create_fabric(self):

Subscribers

People subscribed via source and target branches