Merge lp:~allenap/maas/database-locks-rererevisited into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 4197
Proposed branch: lp:~allenap/maas/database-locks-rererevisited
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~allenap/maas/database-locks-rerevisited
Diff against target: 247 lines (+60/-75)
4 files modified
src/maasserver/dns/config.py (+3/-7)
src/maasserver/dns/tests/test_config.py (+16/-29)
src/maasserver/locks.py (+2/-2)
src/maasserver/security.py (+39/-37)
To merge this branch: bzr merge lp:~allenap/maas/database-locks-rererevisited
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+266448@code.launchpad.net

Commit message

Use non-transactional advisory locks for critical regions of DNS and security activity.

Previously locks were obtained after transactions had begun. This meant that, once the lock was acquired, the transaction may be working with stale data. This causes breakage when dealing with the shared-secret on the filesystem and when writing DNS zones with old data.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good as well. No bug for this either?

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

> Looks good as well. No bug for this either?

I'm going to go with bug 1459168. Thanks again!

Revision history for this message
Gavin Panella (allenap) wrote :

FTR, I QA'ed this at home first, and it seems to work as intended.

Revision history for this message
Andres Rodriguez (andreserl) wrote :

Does it make sense to backport this to 1.8 ?

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (93.9 KiB)

The attempt to merge lp:~allenap/maas/database-locks-rererevisited into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://security.ubuntu.com trusty-security Release
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm pep8 phantomjs postgresql pyflakes python-apt python-bson python-bzrlib python-convoy python-coverage python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mock python-netaddr python-netifaces python-nose python-oauth python-openssl python-paramiko python-pexpect python-pip python-pocket-lint python-psycopg2 python-pyinotify python-pyparsing python-seamicroclient python...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/dns/config.py'
--- src/maasserver/dns/config.py 2015-07-01 18:58:34 +0000
+++ src/maasserver/dns/config.py 2015-08-17 16:24:03 +0000
@@ -14,11 +14,8 @@
14__metaclass__ = type14__metaclass__ = type
15__all__ = [15__all__ = [
16 'dns_add_zones',16 'dns_add_zones',
17 'dns_add_zones_now',
18 'dns_update_all_zones',17 'dns_update_all_zones',
19 'dns_update_all_zones_now',
20 'dns_update_zones',18 'dns_update_zones',
21 'dns_update_zones_now',
22 ]19 ]
2320
24from itertools import (21from itertools import (
@@ -46,6 +43,7 @@
46from maasserver.utils.orm import (43from maasserver.utils.orm import (
47 post_commit,44 post_commit,
48 transactional,45 transactional,
46 with_connection,
49)47)
50from provisioningserver.dns.actions import (48from provisioningserver.dns.actions import (
51 bind_reconfigure,49 bind_reconfigure,
@@ -96,7 +94,6 @@
96DNS_DEFER_UPDATES = True94DNS_DEFER_UPDATES = True
9795
9896
99@synchronised(locks.dns)
100def dns_add_zones_now(clusters):97def dns_add_zones_now(clusters):
101 """Add zone files for the given cluster(s), and serve them.98 """Add zone files for the given cluster(s), and serve them.
10299
@@ -135,7 +132,6 @@
135 return None132 return None
136133
137134
138@synchronised(locks.dns)
139def dns_update_zones_now(clusters):135def dns_update_zones_now(clusters):
140 """Update the zone files for the given cluster(s).136 """Update the zone files for the given cluster(s).
141137
@@ -169,7 +165,6 @@
169 return None165 return None
170166
171167
172@synchronised(locks.dns)
173def dns_update_all_zones_now(reload_retry=False, force=False):168def dns_update_all_zones_now(reload_retry=False, force=False):
174 """Update all zone files for all clusters.169 """Update all zone files for all clusters.
175170
@@ -270,8 +265,9 @@
270 self.hook.addBoth(callOut, self.reset)265 self.hook.addBoth(callOut, self.reset)
271 return self.hook266 return self.hook
272267
268 @with_connection # Needed by the following lock.
269 @synchronised(locks.dns) # Lock before beginning transaction.
273 @transactional270 @transactional
274 @synchronised(locks.dns)
275 def apply(self):271 def apply(self):
276 """Apply all requested changes.272 """Apply all requested changes.
277273
278274
=== modified file 'src/maasserver/dns/tests/test_config.py'
--- src/maasserver/dns/tests/test_config.py 2015-07-01 19:02:36 +0000
+++ src/maasserver/dns/tests/test_config.py 2015-08-17 16:24:03 +0000
@@ -187,6 +187,22 @@
187 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,187 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,
188 status=NODEGROUP_STATUS.ENABLED)188 status=NODEGROUP_STATUS.ENABLED)
189189
190 def test__zone_changes_applied_while_holding_dns_lock(self):
191
192 def check_dns_is_locked(reload_retry, force):
193 self.assertTrue(locks.dns.is_locked(), "locks.dns isn't locked")
194 return False # Prevent dns_update_zones_now from continuing.
195
196 dns_update_all_zones_now = self.patch_autospec(
197 dns_config_module, "dns_update_all_zones_now")
198 dns_update_all_zones_now.side_effect = check_dns_is_locked
199
200 consolidator.update_all_zones()
201 post_commit_hooks.fire()
202 self.assertThat(
203 dns_update_all_zones_now, MockCalledOnceWith(
204 reload_retry=ANY, force=ANY))
205
190 def test__added_zones_applied_post_commit(self):206 def test__added_zones_applied_post_commit(self):
191 dns_add_zones_now = self.patch_autospec(207 dns_add_zones_now = self.patch_autospec(
192 dns_config_module, "dns_add_zones_now")208 dns_config_module, "dns_add_zones_now")
@@ -413,35 +429,6 @@
413 fqdn, ','.join(reverse_lookup_result)))429 fqdn, ','.join(reverse_lookup_result)))
414430
415431
416class TestDNSModificationUsesLocks(TestDNSServer):
417
418 def patch_is_dns_enabled(self):
419 def check_dns_is_locked():
420 self.assertTrue(
421 locks.dns.is_locked(), "locks.dns isn't locked")
422 return False # Prevent dns_update_zones_now from continuing.
423
424 is_dns_enabled = self.patch_autospec(
425 dns_config_module, 'is_dns_enabled')
426 is_dns_enabled.side_effect = check_dns_is_locked
427 return is_dns_enabled
428
429 def test_dns_update_zones_now_uses_lock(self):
430 is_dns_enabled = self.patch_is_dns_enabled()
431 dns_config_module.dns_update_zones_now(sentinel.nodegroup)
432 self.assertThat(is_dns_enabled, MockCalledOnceWith())
433
434 def test_dns_add_zones_now_uses_lock(self):
435 is_dns_enabled = self.patch_is_dns_enabled()
436 dns_config_module.dns_add_zones_now(sentinel.nodegroup)
437 self.assertThat(is_dns_enabled, MockCalledOnceWith())
438
439 def test_dns_update_all_zones_now_config_uses_lock(self):
440 is_dns_enabled = self.patch_is_dns_enabled()
441 dns_config_module.dns_update_all_zones_now()
442 self.assertThat(is_dns_enabled, MockCalledOnceWith())
443
444
445class TestDNSConfigModifications(TestDNSServer):432class TestDNSConfigModifications(TestDNSServer):
446433
447 def test_dns_add_zones_now_loads_dns_zone(self):434 def test_dns_add_zones_now_loads_dns_zone(self):
448435
=== modified file 'src/maasserver/locks.py'
--- src/maasserver/locks.py 2015-08-17 13:22:49 +0000
+++ src/maasserver/locks.py 2015-08-17 16:24:03 +0000
@@ -28,7 +28,7 @@
2828
29# Lock around performing critical security-related operations, like29# Lock around performing critical security-related operations, like
30# generating or signing certificates.30# generating or signing certificates.
31security = DatabaseXactLock(2)31security = DatabaseLock(2)
3232
33# Lock used when starting up the event-loop.33# Lock used when starting up the event-loop.
34eventloop = DatabaseLock(3)34eventloop = DatabaseLock(3)
@@ -38,7 +38,7 @@
38import_images = DatabaseXactLock(4)38import_images = DatabaseXactLock(4)
3939
40# Lock to prevent concurrent changes to DNS configuration.40# Lock to prevent concurrent changes to DNS configuration.
41dns = DatabaseXactLock(6)41dns = DatabaseLock(6)
4242
43# Lock to prevent concurrent acquisition of nodes.43# Lock to prevent concurrent acquisition of nodes.
44node_acquire = DatabaseXactLock(7)44node_acquire = DatabaseXactLock(7)
4545
=== modified file 'src/maasserver/security.py'
--- src/maasserver/security.py 2015-05-07 18:14:38 +0000
+++ src/maasserver/security.py 2015-08-17 16:24:03 +0000
@@ -22,7 +22,11 @@
2222
23from maasserver import locks23from maasserver import locks
24from maasserver.models.config import Config24from maasserver.models.config import Config
25from maasserver.utils.orm import transactional25from maasserver.utils import synchronised
26from maasserver.utils.orm import (
27 transactional,
28 with_connection,
29)
26from provisioningserver.security import (30from provisioningserver.security import (
27 get_shared_secret_filesystem_path,31 get_shared_secret_filesystem_path,
28 get_shared_secret_from_filesystem,32 get_shared_secret_from_filesystem,
@@ -72,52 +76,50 @@
7276
7377
74@synchronous78@synchronous
79@with_connection # Needed by the following lock.
80@synchronised(locks.security) # Region-wide lock.
75@transactional81@transactional
76def get_region_certificate():82def get_region_certificate():
77 cert = load_region_certificate()83 cert = load_region_certificate()
78 if cert is None:84 if cert is None:
79 with locks.security:85 cert = generate_region_certificate()
80 # Load again, while holding the security lock.86 save_region_certificate(cert)
81 cert = load_region_certificate()
82 if cert is None:
83 cert = generate_region_certificate()
84 save_region_certificate(cert)
85 return cert87 return cert
8688
8789
88@synchronous90@synchronous
91@with_connection # Needed by the following lock.
92@synchronised(locks.security) # Region-wide lock.
89@transactional93@transactional
90def get_shared_secret_txn():94def get_shared_secret_txn():
91 # Get a full region-wide lock.95 # Load secret from database, if it exists.
92 with locks.security:96 secret_in_db_hex = Config.objects.get_config("rpc_shared_secret")
93 # Load secret from database, if it exists.97 if secret_in_db_hex is None:
94 secret_in_db_hex = Config.objects.get_config("rpc_shared_secret")98 secret_in_db = None
95 if secret_in_db_hex is None:99 else:
96 secret_in_db = None100 secret_in_db = to_bin(secret_in_db_hex)
97 else:101 # Load secret from the filesystem, if it exists.
98 secret_in_db = to_bin(secret_in_db_hex)102 secret_on_fs = get_shared_secret_from_filesystem()
99 # Load secret from the filesystem, if it exists.103
100 secret_on_fs = get_shared_secret_from_filesystem()104 if secret_in_db is None and secret_on_fs is None:
101105 secret = os.urandom(16) # 16-bytes of crypto-standard noise.
102 if secret_in_db is None and secret_on_fs is None:106 Config.objects.set_config("rpc_shared_secret", to_hex(secret))
103 secret = os.urandom(16) # 16-bytes of crypto-standard noise.107 set_shared_secret_on_filesystem(secret)
104 Config.objects.set_config("rpc_shared_secret", to_hex(secret))108 elif secret_in_db is None:
105 set_shared_secret_on_filesystem(secret)109 secret = secret_on_fs
106 elif secret_in_db is None:110 Config.objects.set_config("rpc_shared_secret", to_hex(secret))
107 secret = secret_on_fs111 elif secret_on_fs is None:
108 Config.objects.set_config("rpc_shared_secret", to_hex(secret))112 secret = secret_in_db
109 elif secret_on_fs is None:113 set_shared_secret_on_filesystem(secret)
110 secret = secret_in_db114 elif secret_in_db == secret_on_fs:
111 set_shared_secret_on_filesystem(secret)115 secret = secret_in_db # or secret_on_fs.
112 elif secret_in_db == secret_on_fs:116 else:
113 secret = secret_in_db # or secret_on_fs.117 raise AssertionError(
114 else:118 "The secret stored in the database does not match the secret "
115 raise AssertionError(119 "stored on the filesystem at %s. Please investigate." %
116 "The secret stored in the database does not match the secret "120 get_shared_secret_filesystem_path())
117 "stored on the filesystem at %s. Please investigate." %121
118 get_shared_secret_filesystem_path())122 return secret
119
120 return secret
121123
122124
123@asynchronous(timeout=10)125@asynchronous(timeout=10)