Merge ~blake-rouse/maas:pools-modified-tracking into maas:master
- Git
- lp:~blake-rouse/maas
- pools-modified-tracking
- Merge into master
Status: | Merged |
---|---|
Approved by: | Blake Rouse |
Approved revision: | b4b742a4529f03fbd2fd87a452cdd1d583222a6b |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~blake-rouse/maas:pools-modified-tracking |
Merge into: | maas:master |
Diff against target: |
661 lines (+558/-0) 9 files modified
src/maasserver/migrations/builtin/maasserver/0179_rbacsync.py (+30/-0) src/maasserver/models/__init__.py (+2/-0) src/maasserver/models/rbacsync.py (+95/-0) src/maasserver/models/tests/test_rbacsync.py (+42/-0) src/maasserver/triggers/system.py (+152/-0) src/maasserver/triggers/testing.py (+45/-0) src/maasserver/triggers/tests/test_init.py (+6/-0) src/maasserver/triggers/tests/test_system.py (+5/-0) src/maasserver/triggers/tests/test_system_listener.py (+181/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Lander | Needs Fixing | ||
Alberto Donato (community) | Approve | ||
Review via email: mp+356106@code.launchpad.net |
Commit message
Add tracking of when resource pools are created/
Description of the change
In a following branch the regiond will listen for the sys_rpool notification to sync the resource pools to the RBAC micro-service.
Blake Rouse (blake-rouse) : | # |
Blake Rouse (blake-rouse) wrote : | # |
I was actually thinking the same thing that I didn't make it generic enough. Because I also need to get notifications when the settings change.
I have re-done the branch to make it generic just for syncing with RBAC in general. We can easily add on to it as we add more resources to RBAC.
Alberto Donato (ack) wrote : | # |
+1, just minor comments inline
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b pools-modified-
STATUS: FAILED
LOG: http://
COMMIT: 1b10f2bc3634690
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b pools-modified-
STATUS: FAILED BUILD
LOG: http://
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b pools-modified-
STATUS: FAILED
LOG: http://
COMMIT: aad64793fd2ef8f
- b4b742a... by Blake Rouse
-
Fix tests.
Preview Diff
1 | diff --git a/src/maasserver/migrations/builtin/maasserver/0179_rbacsync.py b/src/maasserver/migrations/builtin/maasserver/0179_rbacsync.py |
2 | new file mode 100644 |
3 | index 0000000..3422aed |
4 | --- /dev/null |
5 | +++ b/src/maasserver/migrations/builtin/maasserver/0179_rbacsync.py |
6 | @@ -0,0 +1,30 @@ |
7 | +# -*- coding: utf-8 -*- |
8 | +# Generated by Django 1.11.11 on 2018-10-04 14:25 |
9 | +from __future__ import unicode_literals |
10 | + |
11 | +from django.db import ( |
12 | + migrations, |
13 | + models, |
14 | +) |
15 | + |
16 | + |
17 | +class Migration(migrations.Migration): |
18 | + |
19 | + dependencies = [ |
20 | + ('maasserver', '0178_break_apart_linked_bmcs'), |
21 | + ] |
22 | + |
23 | + operations = [ |
24 | + migrations.CreateModel( |
25 | + name='RBACSync', |
26 | + fields=[ |
27 | + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
28 | + ('action', models.CharField(blank=True, choices=[('full', 'full'), ('add', 'add'), ('update', 'update'), ('remove', 'remove')], default='full', editable=False, help_text='Action that should occur on the RBAC service.', max_length=6)), |
29 | + ('resource_type', models.CharField(blank=True, editable=False, help_text='Resource type that as been added/updated/removed.', max_length=255)), |
30 | + ('resource_id', models.IntegerField(blank=True, editable=False, help_text='Resource ID that has been added/updated/removed.', null=True)), |
31 | + ('resource_name', models.CharField(blank=True, editable=False, help_text='Resource name that has been added/updated/removed.', max_length=255)), |
32 | + ('created', models.DateTimeField(auto_now_add=True)), |
33 | + ('source', models.CharField(blank=True, editable=False, help_text='A brief explanation what changed.', max_length=255)), |
34 | + ], |
35 | + ), |
36 | + ] |
37 | diff --git a/src/maasserver/models/__init__.py b/src/maasserver/models/__init__.py |
38 | index 9177321..fc75e92 100644 |
39 | --- a/src/maasserver/models/__init__.py |
40 | +++ b/src/maasserver/models/__init__.py |
41 | @@ -60,6 +60,7 @@ __all__ = [ |
42 | 'PodStoragePool', |
43 | 'RackController', |
44 | 'RAID', |
45 | + 'RBACSync', |
46 | 'RDNS', |
47 | 'RegionController', |
48 | 'RegionControllerProcess', |
49 | @@ -167,6 +168,7 @@ from maasserver.models.partitiontable import PartitionTable |
50 | from maasserver.models.physicalblockdevice import PhysicalBlockDevice |
51 | from maasserver.models.podhints import PodHints |
52 | from maasserver.models.podstoragepool import PodStoragePool |
53 | +from maasserver.models.rbacsync import RBACSync |
54 | from maasserver.models.rdns import RDNS |
55 | from maasserver.models.regioncontrollerprocess import RegionControllerProcess |
56 | from maasserver.models.regioncontrollerprocessendpoint import ( |
57 | diff --git a/src/maasserver/models/rbacsync.py b/src/maasserver/models/rbacsync.py |
58 | new file mode 100644 |
59 | index 0000000..40905db |
60 | --- /dev/null |
61 | +++ b/src/maasserver/models/rbacsync.py |
62 | @@ -0,0 +1,95 @@ |
63 | +# Copyright 2018 Canonical Ltd. This software is licensed under the |
64 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
65 | + |
66 | +"""RBACSync objects.""" |
67 | + |
68 | +__all__ = [ |
69 | + "RBACSync", |
70 | +] |
71 | + |
72 | +from django.db.models import ( |
73 | + Manager, |
74 | + Model, |
75 | +) |
76 | +from django.db.models.fields import ( |
77 | + CharField, |
78 | + DateTimeField, |
79 | + IntegerField, |
80 | +) |
81 | +from maasserver import DefaultMeta |
82 | + |
83 | + |
84 | +class RBAC_ACTION: |
85 | + #: Perform a full sync. |
86 | + FULL = 'full' |
87 | + #: Add a new resource. |
88 | + ADD = 'add' |
89 | + #: Update a resource. |
90 | + UPDATE = 'update' |
91 | + #: Remove a resource. |
92 | + REMOVE = 'remove' |
93 | + |
94 | + |
95 | +RBAC_ACTION_CHOICES = [ |
96 | + (RBAC_ACTION.FULL, 'full'), |
97 | + (RBAC_ACTION.ADD, 'add'), |
98 | + (RBAC_ACTION.UPDATE, 'update'), |
99 | + (RBAC_ACTION.REMOVE, 'remove'), |
100 | +] |
101 | + |
102 | + |
103 | +class RBACSyncManager(Manager): |
104 | + """Manager for `RBACSync` records.""" |
105 | + |
106 | + def changes(self): |
107 | + """Returns the changes that have occurred.""" |
108 | + return list(self.order_by('id')) |
109 | + |
110 | + def clear(self): |
111 | + """Deletes all `RBACSync`.""" |
112 | + self.all().delete() |
113 | + |
114 | + |
115 | +class RBACSync(Model): |
116 | + """A row in this table denotes a change that requires information RBAC |
117 | + micro-service to be updated. |
118 | + |
119 | + Typically this will be populated by a trigger within the database. A |
120 | + listeners in regiond will be notified and consult the un-synced records |
121 | + in this table. This way we can consistently publish RBAC information to the |
122 | + RBAC service in an HA environment. |
123 | + """ |
124 | + |
125 | + class Meta(DefaultMeta): |
126 | + """Default meta.""" |
127 | + |
128 | + objects = RBACSyncManager() |
129 | + |
130 | + action = CharField( |
131 | + editable=False, max_length=6, null=False, blank=True, |
132 | + choices=RBAC_ACTION_CHOICES, default=RBAC_ACTION.FULL, |
133 | + help_text="Action that should occur on the RBAC service.") |
134 | + |
135 | + # An '' string is used when action is 'full'. |
136 | + resource_type = CharField( |
137 | + editable=False, max_length=255, null=False, blank=True, |
138 | + help_text="Resource type that as been added/updated/removed.") |
139 | + |
140 | + # A `None` is used when action is 'full'. |
141 | + resource_id = IntegerField( |
142 | + editable=False, null=True, blank=True, |
143 | + help_text="Resource ID that has been added/updated/removed.") |
144 | + |
145 | + # A '' string is used when action is 'full'. |
146 | + resource_name = CharField( |
147 | + editable=False, max_length=255, null=False, blank=True, |
148 | + help_text="Resource name that has been added/updated/removed.") |
149 | + |
150 | + # This field is informational. |
151 | + created = DateTimeField( |
152 | + editable=False, null=False, auto_now=False, auto_now_add=True) |
153 | + |
154 | + # This field is informational. |
155 | + source = CharField( |
156 | + editable=False, max_length=255, null=False, blank=True, |
157 | + help_text="A brief explanation what changed.") |
158 | diff --git a/src/maasserver/models/tests/test_rbacsync.py b/src/maasserver/models/tests/test_rbacsync.py |
159 | new file mode 100644 |
160 | index 0000000..8d1f670 |
161 | --- /dev/null |
162 | +++ b/src/maasserver/models/tests/test_rbacsync.py |
163 | @@ -0,0 +1,42 @@ |
164 | +# Copyright 2018 Canonical Ltd. This software is licensed under the |
165 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
166 | + |
167 | +"""Tests for RBACSync models.""" |
168 | + |
169 | +__all__ = [] |
170 | + |
171 | +from maasserver.models.rbacsync import RBACSync |
172 | +from maasserver.testing.testcase import MAASServerTestCase |
173 | +from testtools.matchers import ( |
174 | + Equals, |
175 | + HasLength, |
176 | +) |
177 | + |
178 | + |
179 | +class TestRBACSync(MAASServerTestCase): |
180 | + """Test `RBACSync`.""" |
181 | + |
182 | + def setUp(self): |
183 | + super(TestRBACSync, self).setUp() |
184 | + # These tests expect the RBACSync table to be empty. |
185 | + RBACSync.objects.all().delete() |
186 | + |
187 | + def test_changes(self): |
188 | + synced = [ |
189 | + RBACSync.objects.create() |
190 | + for _ in range(3) |
191 | + ] |
192 | + self.assertThat( |
193 | + RBACSync.objects.changes(), Equals(synced)) |
194 | + |
195 | + def test_clear_does_nothing_when_nothing(self): |
196 | + self.assertThat(RBACSync.objects.all(), HasLength(0)) |
197 | + RBACSync.objects.clear() |
198 | + self.assertThat(RBACSync.objects.all(), HasLength(0)) |
199 | + |
200 | + def test_clear_removes_all(self): |
201 | + for _ in range(3): |
202 | + RBACSync.objects.create() |
203 | + self.assertThat(RBACSync.objects.all(), HasLength(3)) |
204 | + RBACSync.objects.clear() |
205 | + self.assertThat(RBACSync.objects.all(), HasLength(0)) |
206 | diff --git a/src/maasserver/triggers/system.py b/src/maasserver/triggers/system.py |
207 | index ef0f07e..f73d11e 100644 |
208 | --- a/src/maasserver/triggers/system.py |
209 | +++ b/src/maasserver/triggers/system.py |
210 | @@ -1654,6 +1654,130 @@ PEER_PROXY_CONFIG_UPDATE = dedent("""\ |
211 | """) |
212 | |
213 | |
214 | +# Triggered when RBAC need to be synced. In essense this means on |
215 | +# insert into maasserver_rbacsync. |
216 | +RBAC_SYNC = dedent("""\ |
217 | + CREATE OR REPLACE FUNCTION sys_rbac_sync() |
218 | + RETURNS trigger AS $$ |
219 | + BEGIN |
220 | + PERFORM pg_notify('sys_rbac', ''); |
221 | + RETURN NEW; |
222 | + END; |
223 | + $$ LANGUAGE plpgsql; |
224 | + """) |
225 | + |
226 | + |
227 | +# Procedure to mark RBAC as needing a sync. |
228 | +RBAC_SYNC_UPDATE = dedent("""\ |
229 | + CREATE OR REPLACE FUNCTION sys_rbac_sync_update( |
230 | + reason text, |
231 | + action text DEFAULT 'full', |
232 | + resource_type text DEFAULT '', |
233 | + resource_id int DEFAULT NULL, |
234 | + resource_name text DEFAULT '') |
235 | + RETURNS void as $$ |
236 | + BEGIN |
237 | + INSERT INTO maasserver_rbacsync |
238 | + (created, source, action, resource_type, resource_id, resource_name) |
239 | + VALUES ( |
240 | + now(), substring(reason FOR 255), |
241 | + action, resource_type, resource_id, resource_name); |
242 | + END; |
243 | + $$ LANGUAGE plpgsql; |
244 | + """) |
245 | + |
246 | + |
247 | +# Triggered when a new resource pool is added. Notifies that RBAC needs |
248 | +# to be synced. |
249 | +RBAC_RPOOL_INSERT = dedent("""\ |
250 | + CREATE OR REPLACE FUNCTION sys_rbac_rpool_insert() |
251 | + RETURNS trigger as $$ |
252 | + BEGIN |
253 | + PERFORM sys_rbac_sync_update( |
254 | + 'added resource pool ' || NEW.name, |
255 | + 'add', 'resource-pool', NEW.id, NEW.name); |
256 | + RETURN NEW; |
257 | + END; |
258 | + $$ LANGUAGE plpgsql; |
259 | + """) |
260 | + |
261 | + |
262 | +# Triggered when a resource pool is updated. Notifies that RBAC needs |
263 | +# to be synced. Only watches name. |
264 | +RBAC_RPOOL_UPDATE = dedent("""\ |
265 | + CREATE OR REPLACE FUNCTION sys_rbac_rpool_update() |
266 | + RETURNS trigger as $$ |
267 | + DECLARE |
268 | + changes text[]; |
269 | + BEGIN |
270 | + IF OLD.name != NEW.name THEN |
271 | + PERFORM sys_rbac_sync_update( |
272 | + 'renamed resource pool ' || OLD.name || ' to ' || NEW.name, |
273 | + 'update', 'resource-pool', OLD.id, NEW.name); |
274 | + END IF; |
275 | + RETURN NEW; |
276 | + END; |
277 | + $$ LANGUAGE plpgsql; |
278 | + """) |
279 | + |
280 | + |
281 | +# Triggered when a resource pool is deleted. Notifies that RBAC needs |
282 | +# to be synced. |
283 | +RBAC_RPOOL_DELETE = dedent("""\ |
284 | + CREATE OR REPLACE FUNCTION sys_rbac_rpool_delete() |
285 | + RETURNS trigger as $$ |
286 | + BEGIN |
287 | + PERFORM sys_rbac_sync_update( |
288 | + 'removed resource pool ' || OLD.name, |
289 | + 'remove', 'resource-pool', OLD.id, OLD.name); |
290 | + RETURN OLD; |
291 | + END; |
292 | + $$ LANGUAGE plpgsql; |
293 | + """) |
294 | + |
295 | + |
296 | +# Triggered when the Candid/RBAC settings are inserted. Notifies that RBAC |
297 | +# needs to be synced. |
298 | +RBAC_CONFIG_INSERT = dedent("""\ |
299 | + CREATE OR REPLACE FUNCTION sys_rbac_config_insert() |
300 | + RETURNS trigger as $$ |
301 | + BEGIN |
302 | + IF (NEW.name = 'external_auth_url' OR |
303 | + NEW.name = 'external_auth_user' OR |
304 | + NEW.name = 'external_auth_key' OR |
305 | + NEW.name = 'rbac_url') THEN |
306 | + PERFORM sys_rbac_sync_update( |
307 | + 'configuration ' || NEW.name || ' set to ' || |
308 | + COALESCE(NEW.value, 'NULL')); |
309 | + END IF; |
310 | + RETURN NEW; |
311 | + END; |
312 | + $$ LANGUAGE plpgsql; |
313 | + """) |
314 | + |
315 | + |
316 | +# Triggered when the Candid/RBAC settings are updated. Notifies that RBAC |
317 | +# needs to be synced. The external_auth_* keys are included that way if |
318 | +# the agent account that MAAS uses to update RBAC is updated in MAAS, MAAS will |
319 | +# ensure that a full sync of data is performed. |
320 | +RBAC_CONFIG_UPDATE = dedent("""\ |
321 | + CREATE OR REPLACE FUNCTION sys_rbac_config_update() |
322 | + RETURNS trigger as $$ |
323 | + BEGIN |
324 | + IF (OLD.value != NEW.value AND ( |
325 | + NEW.name = 'external_auth_url' OR |
326 | + NEW.name = 'external_auth_user' OR |
327 | + NEW.name = 'external_auth_key' OR |
328 | + NEW.name = 'rbac_url')) THEN |
329 | + PERFORM sys_rbac_sync_update( |
330 | + 'configuration ' || NEW.name || ' changed to ' || NEW.value); |
331 | + END IF; |
332 | + RETURN NEW; |
333 | + END; |
334 | + $$ LANGUAGE plpgsql; |
335 | + """) |
336 | + |
337 | + |
338 | def render_sys_proxy_procedure(proc_name, on_delete=False): |
339 | """Render a database procedure with name `proc_name` that notifies that a |
340 | proxy update is needed. |
341 | @@ -1914,3 +2038,31 @@ def register_system_triggers(): |
342 | register_trigger( |
343 | "maasserver_config", "sys_proxy_config_use_peer_proxy_update", |
344 | "update") |
345 | + |
346 | + # - RBACSync |
347 | + register_procedure(RBAC_SYNC) |
348 | + register_trigger( |
349 | + "maasserver_rbacsync", |
350 | + "sys_rbac_sync", "insert") |
351 | + register_procedure(RBAC_SYNC_UPDATE) |
352 | + |
353 | + # - ResourcePool |
354 | + register_procedure(RBAC_RPOOL_INSERT) |
355 | + register_trigger( |
356 | + "maasserver_resourcepool", "sys_rbac_rpool_insert", "insert") |
357 | + register_procedure(RBAC_RPOOL_UPDATE) |
358 | + register_trigger( |
359 | + "maasserver_resourcepool", "sys_rbac_rpool_update", "update") |
360 | + register_procedure(RBAC_RPOOL_DELETE) |
361 | + register_trigger( |
362 | + "maasserver_resourcepool", "sys_rbac_rpool_delete", "delete") |
363 | + |
364 | + # - Config (Candid/RBAC) |
365 | + register_procedure(RBAC_CONFIG_INSERT) |
366 | + register_trigger( |
367 | + "maasserver_config", "sys_rbac_config_insert", |
368 | + "insert") |
369 | + register_procedure(RBAC_CONFIG_UPDATE) |
370 | + register_trigger( |
371 | + "maasserver_config", "sys_rbac_config_update", |
372 | + "update") |
373 | diff --git a/src/maasserver/triggers/testing.py b/src/maasserver/triggers/testing.py |
374 | index 73a6b48..0fec0ac 100644 |
375 | --- a/src/maasserver/triggers/testing.py |
376 | +++ b/src/maasserver/triggers/testing.py |
377 | @@ -41,6 +41,7 @@ from maasserver.models.packagerepository import PackageRepository |
378 | from maasserver.models.partition import Partition |
379 | from maasserver.models.partitiontable import PartitionTable |
380 | from maasserver.models.physicalblockdevice import PhysicalBlockDevice |
381 | +from maasserver.models.rbacsync import RBACSync |
382 | from maasserver.models.regioncontrollerprocess import RegionControllerProcess |
383 | from maasserver.models.regioncontrollerprocessendpoint import ( |
384 | RegionControllerProcessEndpoint, |
385 | @@ -847,3 +848,47 @@ class DNSHelpersMixin: |
386 | self.assertThat( |
387 | new.serial, GreaterThan(old.serial), |
388 | "DNS has not been published again.") |
389 | + |
390 | + |
391 | +class RBACHelpersMixin: |
392 | + """Helper to get the latest rbac sync.""" |
393 | + |
394 | + @transactional |
395 | + def getSynced(self): |
396 | + try: |
397 | + return RBACSync.objects.order_by('-id').first() |
398 | + except RBACSync.DoesNotExist: |
399 | + return None |
400 | + |
401 | + @inlineCallbacks |
402 | + def captureSynced(self): |
403 | + """Capture the most recent `RBACSync` record.""" |
404 | + self.__synced = yield deferToDatabase(self.getSynced) |
405 | + returnValue(self.__synced) |
406 | + |
407 | + def getCapturedSynced(self): |
408 | + """Return the captured sync record.""" |
409 | + try: |
410 | + return self.__synced |
411 | + except AttributeError: |
412 | + self.fail( |
413 | + "No reference modification has been captured; " |
414 | + "use `captureSynced` before calling " |
415 | + "`getCapturedSynced`.") |
416 | + |
417 | + @inlineCallbacks |
418 | + def assertSynced(self): |
419 | + """Assert there's a newer `RBACSync` record. |
420 | + |
421 | + Call `captureSynced` first to obtain a reference record. |
422 | + """ |
423 | + old = self.getCapturedSynced() |
424 | + new = yield self.captureSynced() |
425 | + if old is None: |
426 | + self.assertThat( |
427 | + new, Not(Is(None)), |
428 | + "RBAC sync tracking has not been modified at all.") |
429 | + else: |
430 | + self.assertThat( |
431 | + new.id, GreaterThan(old.id), |
432 | + "RBAC sync tracking has not been modified again.") |
433 | diff --git a/src/maasserver/triggers/tests/test_init.py b/src/maasserver/triggers/tests/test_init.py |
434 | index e5bce9e..d03a833 100644 |
435 | --- a/src/maasserver/triggers/tests/test_init.py |
436 | +++ b/src/maasserver/triggers/tests/test_init.py |
437 | @@ -105,6 +105,12 @@ class TestTriggersUsed(MAASServerTestCase): |
438 | "subnet_sys_proxy_subnet_insert", |
439 | "subnet_sys_proxy_subnet_update", |
440 | "vlan_sys_dhcp_vlan_update", |
441 | + "rbacsync_sys_rbac_sync", |
442 | + "resourcepool_sys_rbac_rpool_insert", |
443 | + "resourcepool_sys_rbac_rpool_update", |
444 | + "resourcepool_sys_rbac_rpool_delete", |
445 | + "config_sys_rbac_config_insert", |
446 | + "config_sys_rbac_config_update", |
447 | } |
448 | |
449 | triggers_websocket = { |
450 | diff --git a/src/maasserver/triggers/tests/test_system.py b/src/maasserver/triggers/tests/test_system.py |
451 | index 1fe4615..eeccb47 100644 |
452 | --- a/src/maasserver/triggers/tests/test_system.py |
453 | +++ b/src/maasserver/triggers/tests/test_system.py |
454 | @@ -61,6 +61,11 @@ class TestTriggers(MAASServerTestCase): |
455 | "subnet_sys_proxy_subnet_insert", |
456 | "subnet_sys_proxy_subnet_update", |
457 | "subnet_sys_proxy_subnet_delete", |
458 | + "resourcepool_sys_rbac_rpool_insert", |
459 | + "resourcepool_sys_rbac_rpool_update", |
460 | + "resourcepool_sys_rbac_rpool_delete", |
461 | + "config_sys_rbac_config_insert", |
462 | + "config_sys_rbac_config_update", |
463 | ] |
464 | sql, args = psql_array(triggers, sql_type="text") |
465 | with closing(connection.cursor()) as cursor: |
466 | diff --git a/src/maasserver/triggers/tests/test_system_listener.py b/src/maasserver/triggers/tests/test_system_listener.py |
467 | index d6e2adc..c543ca3 100644 |
468 | --- a/src/maasserver/triggers/tests/test_system_listener.py |
469 | +++ b/src/maasserver/triggers/tests/test_system_listener.py |
470 | @@ -36,6 +36,7 @@ from maasserver.testing.testcase import ( |
471 | from maasserver.triggers.system import register_system_triggers |
472 | from maasserver.triggers.testing import ( |
473 | DNSHelpersMixin, |
474 | + RBACHelpersMixin, |
475 | TransactionalHelpersMixin, |
476 | ) |
477 | from maasserver.utils.orm import transactional |
478 | @@ -4688,3 +4689,183 @@ class TestProxyListener( |
479 | yield dv.get(timeout=2) |
480 | finally: |
481 | yield listener.stopService() |
482 | + |
483 | + |
484 | +class TestRBACResourcePoolListener( |
485 | + MAASTransactionServerTestCase, TransactionalHelpersMixin, |
486 | + RBACHelpersMixin): |
487 | + """End-to-end test for the resource pool RBAC triggers code.""" |
488 | + |
489 | + @wait_for_reactor |
490 | + @inlineCallbacks |
491 | + def test_sends_message_for_resource_pool_insert(self): |
492 | + yield deferToDatabase(register_system_triggers) |
493 | + yield self.captureSynced() |
494 | + name = factory.make_name('pool') |
495 | + dv = DeferredValue() |
496 | + listener = self.make_listener_without_delay() |
497 | + listener.register( |
498 | + "sys_rbac", lambda *args: dv.set(args)) |
499 | + yield listener.startService() |
500 | + try: |
501 | + pool = yield deferToDatabase( |
502 | + self.create_resource_pool, {'name': name}) |
503 | + yield dv.get(timeout=2) |
504 | + yield self.assertSynced() |
505 | + finally: |
506 | + yield listener.stopService() |
507 | + change = self.getCapturedSynced() |
508 | + self.assertThat( |
509 | + change.source, |
510 | + Equals("added resource pool %s" % name)) |
511 | + self.assertThat( |
512 | + change.action, Equals("add")) |
513 | + self.assertThat( |
514 | + change.resource_type, Equals("resource-pool")) |
515 | + self.assertThat( |
516 | + change.resource_id, Equals(pool.id)) |
517 | + self.assertThat( |
518 | + change.resource_name, Equals(name)) |
519 | + |
520 | + @wait_for_reactor |
521 | + @inlineCallbacks |
522 | + def test_sends_message_for_resource_pool_update(self): |
523 | + yield deferToDatabase(register_system_triggers) |
524 | + pool = yield deferToDatabase( |
525 | + self.create_resource_pool, {}) |
526 | + pool_name = factory.make_name('pool') |
527 | + yield self.captureSynced() |
528 | + dv = DeferredValue() |
529 | + listener = self.make_listener_without_delay() |
530 | + listener.register( |
531 | + "sys_rbac", lambda *args: dv.set(args)) |
532 | + yield listener.startService() |
533 | + try: |
534 | + yield deferToDatabase(self.update_resource_pool, pool.id, { |
535 | + "name": pool_name, |
536 | + }) |
537 | + yield dv.get(timeout=2) |
538 | + yield self.assertSynced() |
539 | + finally: |
540 | + yield listener.stopService() |
541 | + change = self.getCapturedSynced() |
542 | + self.assertThat( |
543 | + change.source, |
544 | + Equals("renamed resource pool %s to %s" % (pool.name, pool_name))) |
545 | + self.assertThat( |
546 | + change.action, Equals("update")) |
547 | + self.assertThat( |
548 | + change.resource_type, Equals("resource-pool")) |
549 | + self.assertThat( |
550 | + change.resource_id, Equals(pool.id)) |
551 | + self.assertThat( |
552 | + change.resource_name, Equals(pool_name)) |
553 | + |
554 | + @wait_for_reactor |
555 | + @inlineCallbacks |
556 | + def test_sends_message_for_resource_pool_delete(self): |
557 | + yield deferToDatabase(register_system_triggers) |
558 | + pool = yield deferToDatabase(self.create_resource_pool) |
559 | + yield self.captureSynced() |
560 | + dv = DeferredValue() |
561 | + listener = self.make_listener_without_delay() |
562 | + listener.register( |
563 | + "sys_rbac", lambda *args: dv.set(args)) |
564 | + yield listener.startService() |
565 | + try: |
566 | + yield deferToDatabase(self.delete_resource_pool, pool.id) |
567 | + yield dv.get(timeout=2) |
568 | + yield self.assertSynced() |
569 | + finally: |
570 | + yield listener.stopService() |
571 | + change = self.getCapturedSynced() |
572 | + self.assertThat( |
573 | + change.source, |
574 | + Equals("removed resource pool %s" % pool.name)) |
575 | + self.assertThat( |
576 | + change.action, Equals("remove")) |
577 | + self.assertThat( |
578 | + change.resource_type, Equals("resource-pool")) |
579 | + self.assertThat( |
580 | + change.resource_id, Equals(pool.id)) |
581 | + self.assertThat( |
582 | + change.resource_name, Equals(pool.name)) |
583 | + |
584 | + |
585 | +class TestRBACConfigListener( |
586 | + MAASTransactionServerTestCase, TransactionalHelpersMixin, |
587 | + RBACHelpersMixin): |
588 | + """End-to-end test for the config RBAC triggers code.""" |
589 | + |
590 | + scenarios = ( |
591 | + ('external_auth_url', { |
592 | + 'config': 'external_auth_url', |
593 | + }), |
594 | + ('external_auth_user', { |
595 | + 'config': 'external_auth_user', |
596 | + }), |
597 | + ('external_auth_key', { |
598 | + 'config': 'external_auth_key', |
599 | + }), |
600 | + ('rbac_url', { |
601 | + 'config': 'rbac_url', |
602 | + }), |
603 | + ) |
604 | + |
605 | + @wait_for_reactor |
606 | + @inlineCallbacks |
607 | + def test_sends_message_for_config_insert(self): |
608 | + new_value = factory.make_name("value") |
609 | + yield deferToDatabase(register_system_triggers) |
610 | + yield self.captureSynced() |
611 | + dv = DeferredValue() |
612 | + listener = self.make_listener_without_delay() |
613 | + listener.register( |
614 | + "sys_rbac", lambda *args: dv.set(args)) |
615 | + yield listener.startService() |
616 | + try: |
617 | + yield deferToDatabase( |
618 | + Config.objects.set_config, |
619 | + self.config, new_value) |
620 | + yield dv.get(timeout=2) |
621 | + yield self.assertSynced() |
622 | + finally: |
623 | + yield listener.stopService() |
624 | + change = self.getCapturedSynced() |
625 | + self.assertThat( |
626 | + change.source, Equals( |
627 | + "configuration %s set to %s" |
628 | + % (self.config, json.dumps(new_value)))) |
629 | + self.assertThat( |
630 | + change.action, Equals("full")) |
631 | + |
632 | + @wait_for_reactor |
633 | + @inlineCallbacks |
634 | + def test_sends_message_for_config_update(self): |
635 | + old_value = factory.make_name("old") |
636 | + new_value = factory.make_name("new") |
637 | + yield deferToDatabase(register_system_triggers) |
638 | + yield deferToDatabase( |
639 | + Config.objects.set_config, |
640 | + self.config, old_value) |
641 | + yield self.captureSynced() |
642 | + dv = DeferredValue() |
643 | + listener = self.make_listener_without_delay() |
644 | + listener.register( |
645 | + "sys_rbac", lambda *args: dv.set(args)) |
646 | + yield listener.startService() |
647 | + try: |
648 | + yield deferToDatabase( |
649 | + Config.objects.set_config, |
650 | + self.config, new_value) |
651 | + yield dv.get(timeout=2) |
652 | + yield self.assertSynced() |
653 | + finally: |
654 | + yield listener.stopService() |
655 | + change = self.getCapturedSynced() |
656 | + self.assertThat( |
657 | + change.source, Equals( |
658 | + "configuration %s changed to %s" |
659 | + % (self.config, json.dumps(new_value)))) |
660 | + self.assertThat( |
661 | + change.action, Equals("full")) |
The mechanism implemented here seems quite generic.
Maybe we could add a "resource_type" field so that a single table would track all type of resources that need sync.
Manager methods would then filter on that type (and the pg notification would also carry the type).
This way we don't need a different table and notification handler for each resource we cover with rbac.