Merge lp:~ltrager/maas/lp1592246 into lp:~maas-committers/maas/trunk

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: no longer in the source branch.
Merged at revision: 5135
Proposed branch: lp:~ltrager/maas/lp1592246
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 651 lines (+227/-56)
11 files modified
src/maasserver/models/node.py (+2/-2)
src/maasserver/models/tests/test_node.py (+30/-7)
src/maasserver/rpc/regionservice.py (+2/-1)
src/maasserver/start_up.py (+1/-1)
src/maasserver/tests/test_start_up.py (+22/-0)
src/provisioningserver/register_command.py (+19/-0)
src/provisioningserver/rpc/clusterservice.py (+1/-1)
src/provisioningserver/rpc/tests/test_clusterservice.py (+25/-1)
src/provisioningserver/tests/test_register_command.py (+29/-2)
src/provisioningserver/utils/env.py (+18/-4)
src/provisioningserver/utils/tests/test_env.py (+78/-37)
To merge this branch: bzr merge lp:~ltrager/maas/lp1592246
Reviewer Review Type Date Requested Status
Gavin Panella (community) Needs Fixing
Blake Rouse (community) Approve
Andres Rodriguez (community) Needs Information
Review via email: mp+297729@code.launchpad.net

Commit message

Remove /var/lib/maas_id on rack register and cache contents with get_maas_id

Description of the change

This stops maas-rackd, removes /var/lib/maas_id, and restarts it after the secret key and config has been written. This ensures that a rack doesn't register using a stale system_id. Deleting a rack also stops the service and disables it. By starting and enabling the service users now have an easy way to reregister a rack that was deleted.

get_maas_id now caches /var/lib/maas_id so deleting it during registration has no effect on a region running on the same machine.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

Hi Lee,

I thought we said we were not going to do this, and decided that the random system id's would be the way to go?

That said, I have a few questions:

1. So if we are caching the maas_id, what happens if for whatever reason, maas-rack deletes /var/lib/maas/maas_id and the rack controller is restarted?
2. This also means that there's a period of time where maas_id will not be present on the disk, and we agreed not to do that.
3. Have you thought about any corner cases? What happens if you don't delete a rack from the MAAS UI, and then try to re-register the rack? For what we discussed, this would mean that a new rack gets added resulting in two racks being present, when it should have been just one?

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

I agree with Andres about the random system_id thing. The likelihood of
a collision is very small, almost certainly less risk than removing the
maas_id file unconditionally here.

review: Needs Fixing
Revision history for this message
Lee Trager (ltrager) wrote :

> Hi Lee,
>
> I thought we said we were not going to do this, and decided that the random
> system id's would be the way to go?
I had this branch mostly done before the standup yesterday so I figured I'd post it. Besides having a fix for lp:1592246 it caches maas_id so its only read once per process instead of multiple times and resolves lp:1576357.

>
> That said, I have a few questions:
>
> 1. So if we are caching the maas_id, what happens if for whatever reason,
> maas-rack deletes /var/lib/maas/maas_id and the rack controller is restarted?
Both rackd and regiond look for a node object by system_id, hostname, or MAC address. If /var/lib/maas/maas_id is missing it will fall back on the looking the host up by hostname then by MAC address. We only have /var/lib/maas/maas_id in case the user renames their hostname on a hardware platform that doesn't have unique MAC address(SystemZ)

> 2. This also means that there's a period of time where maas_id will not be
> present on the disk, and we agreed not to do that.
By caching /var/lib/maas/maas_id it doesn't matter. I tested this by starting up MAAS deleting /var/lib/maas/maas_id and running a deployment.

> 3. Have you thought about any corner cases? What happens if you don't delete a
> rack from the MAAS UI, and then try to re-register the rack? For what we
> discussed, this would mean that a new rack gets added resulting in two racks
> being present, when it should have been just one?
If you run maas-rack register on an already registered rack MAAS will find the existing node table entry from the hostname or MAC address and return the same system_id. The same goes for the region. The only way to get duplicate racks/regions is by changing the hostname and MAC addresses of an already registered host. Since this restarts maas-rackd the worst that will happen in this case is you'll have an extra rack entry which would need to be manually deleted.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good. You need to fix the two tests I commented on. Other than that this looks good.

Make sure you have tested this on an installed version of MAAS as well.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.3 MiB)

The attempt to merge lp:~ltrager/maas/lp1592246 into lp:maas failed. Below is the output from the failed tests.

Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [94.5 kB]
Hit:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [94.5 kB]
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates/universe amd64 Packages [97.1 kB]
Fetched 286 kB in 0s (584 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bash bind9 bind9utils build-essential bzr 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 postgresql pxelinux python3-all python3-apt python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu3).
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
build-essential is already the newest version (12.1ubuntu2).
bzr is already the newest version (2.7.0-2ubuntu1).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu12).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
...

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

Minor comment.

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

I don't understand why the read_cache parameter is necessary, plus a few other things still need fixing.

review: Needs Fixing
Revision history for this message
Lee Trager (ltrager) wrote :

I landed this on Friday to solve a couple of bugs for RC1 that we wanted in. The read_cache parameter is used to force get_maas_id to read from the disk even if something is cached. The main use case is for start_up. Right now the region object is created on the master process while the other processes wait for the region object to be created. If the user has a stale value(e.g from a previous install) in /var/lib/maas/maas_id it will be read and cached. The other processes will never come up because they'll have the cached a stale value. By using read_cache=False we keep rereading /var/lib/maas/maas_id until we get a value that has a corresponding row in the database.

I've posted a separate review for the corrections you've suggested at lp:~ltrager/maas/cleanup_lp1592246

Revision history for this message
Gavin Panella (allenap) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/node.py'
2--- src/maasserver/models/node.py 2016-06-16 17:20:19 +0000
3+++ src/maasserver/models/node.py 2016-06-18 01:12:20 +0000
4@@ -551,8 +551,8 @@
5 NODE_TYPE.REGION_AND_RACK_CONTROLLER,
6 ]}
7
8- def get_running_controller(self):
9- return self.get(system_id=get_maas_id())
10+ def get_running_controller(self, read_cache=True):
11+ return self.get(system_id=get_maas_id(read_cache))
12
13
14 class RackControllerManager(ControllerManager):
15
16=== modified file 'src/maasserver/models/tests/test_node.py'
17--- src/maasserver/models/tests/test_node.py 2016-06-14 04:22:21 +0000
18+++ src/maasserver/models/tests/test_node.py 2016-06-18 01:12:20 +0000
19@@ -6,6 +6,7 @@
20 __all__ = []
21
22 from datetime import datetime
23+import os
24 import random
25 from unittest.mock import (
26 ANY,
27@@ -53,6 +54,7 @@
28 BootResource,
29 BridgeInterface,
30 Config,
31+ Controller,
32 Device,
33 Domain,
34 Fabric,
35@@ -136,6 +138,7 @@
36 EVENT_DETAILS,
37 EVENT_TYPES,
38 )
39+from provisioningserver.path import get_path
40 from provisioningserver.power import QUERY_POWER_TYPES
41 from provisioningserver.power.schema import JSON_POWER_TYPE_PARAMETERS
42 from provisioningserver.rpc.cluster import (
43@@ -156,7 +159,12 @@
44 map_enum,
45 map_enum_reverse,
46 )
47+from provisioningserver.utils.env import (
48+ get_maas_id,
49+ set_maas_id,
50+)
51 from provisioningserver.utils.fs import NamedLock
52+from provisioningserver.utils.testing import MAASIDFixture
53 from testtools import ExpectedException
54 from testtools.matchers import (
55 AfterPreprocessing,
56@@ -301,7 +309,13 @@
57 list(Machine.objects.get_available_machines_for_acquisition(user)))
58
59
60-class TestControllerManager(MAASServerTestCase):
61+class TestControllerManaer(MAASServerTestCase):
62+
63+ def tearDown(self):
64+ super().tearDown()
65+ # Make sure the maas_id is cleared when done
66+ set_maas_id(None)
67+
68 def test_controller_lists_node_type_rack_and_region(self):
69 racks_and_regions = set()
70 for _ in range(3):
71@@ -311,13 +325,23 @@
72 NODE_TYPE.RACK_CONTROLLER, NODE_TYPE.REGION_CONTROLLER,
73 NODE_TYPE.REGION_AND_RACK_CONTROLLER):
74 racks_and_regions.add(factory.make_Node(node_type=node_type))
75- self.assertItemsEqual(racks_and_regions, Node.controllers.all())
76+ self.assertItemsEqual(racks_and_regions, Controller.objects.all())
77
78 def test_get_running_controller(self):
79 rack = factory.make_RackController()
80- get_maas_id = self.patch(node_module, 'get_maas_id')
81- get_maas_id.return_value = rack.system_id
82- self.assertEquals(rack, Node.controllers.get_running_controller())
83+ self.useFixture(MAASIDFixture(rack.system_id))
84+ self.assertEquals(rack, Controller.objects.get_running_controller())
85+
86+ def test_get_running_controller_can_ignore_cache(self):
87+ set_maas_id(factory.make_string())
88+ rack = factory.make_RackController()
89+ maas_id_path = get_path('/var/lib/maas/maas_id')
90+ os.unlink(maas_id_path)
91+ with open(maas_id_path, 'w') as fd:
92+ fd.write(rack.system_id)
93+ self.assertEquals(
94+ rack, Controller.objects.get_running_controller(read_cache=False))
95+ self.assertEquals(rack.system_id, get_maas_id())
96
97
98 class TestRackControllerManager(MAASServerTestCase):
99@@ -418,8 +442,7 @@
100 mock_getaddr_info = self.patch(node_module.socket, "getaddrinfo")
101 ip = random.choice(['127.0.0.1', '::1'])
102 mock_getaddr_info.return_value = (('', '', '', '', (ip,)),)
103- get_maas_id = self.patch(node_module, 'get_maas_id')
104- get_maas_id.return_value = rack.system_id
105+ self.useFixture(MAASIDFixture(rack.system_id))
106 self.assertEquals(
107 [rack, ],
108 RackController.objects.filter_by_url_accessible(ip, False))
109
110=== modified file 'src/maasserver/rpc/regionservice.py'
111--- src/maasserver/rpc/regionservice.py 2016-06-14 03:04:05 +0000
112+++ src/maasserver/rpc/regionservice.py 2016-06-18 01:12:20 +0000
113@@ -1171,7 +1171,8 @@
114
115 :return: An instance of `RegionAdvertising`.
116 """
117- region_obj = RegionController.objects.get_running_controller()
118+ region_obj = RegionController.objects.get_running_controller(
119+ read_cache=False)
120 # Create the process for this region. This process object will be
121 # updated by calls to `update`, which is the responsibility of the
122 # rpc-advertise service. Calls to `update` also remove old process
123
124=== modified file 'src/maasserver/start_up.py'
125--- src/maasserver/start_up.py 2016-06-16 01:30:23 +0000
126+++ src/maasserver/start_up.py 2016-06-18 01:12:20 +0000
127@@ -140,7 +140,7 @@
128
129
130 def create_region_obj():
131- region_id = get_maas_id()
132+ region_id = get_maas_id(read_cache=False)
133 hostname = gethostname()
134 update_fields = []
135 region_filter = Q() if region_id is None else Q(system_id=region_id)
136
137=== modified file 'src/maasserver/tests/test_start_up.py'
138--- src/maasserver/tests/test_start_up.py 2016-06-16 01:30:23 +0000
139+++ src/maasserver/tests/test_start_up.py 2016-06-18 01:12:20 +0000
140@@ -5,6 +5,7 @@
141
142 __all__ = []
143
144+import os
145 from unittest.mock import call
146
147 from maasserver import (
148@@ -29,6 +30,8 @@
149 MockCallsMatch,
150 MockNotCalled,
151 )
152+from provisioningserver.path import get_path
153+from provisioningserver.utils.env import set_maas_id
154 from provisioningserver.utils.testing import MAASIDFixture
155
156
157@@ -61,6 +64,7 @@
158 super(TestStartUp, self).tearDown()
159 # start_up starts the Twisted event loop, so we need to stop it.
160 eventloop.reset().wait(5)
161+ set_maas_id(None)
162
163 def test_inner_start_up_runs_in_exclusion(self):
164 # Disable boot source cache signals.
165@@ -112,6 +116,11 @@
166 self.addCleanup(bootsources.signals.enable)
167 bootsources.signals.disable()
168
169+ def tearDown(self):
170+ super().tearDown()
171+ # Clear maas_id cache
172+ set_maas_id(None)
173+
174 def test__calls_dns_kms_setting_changed_if_master(self):
175 self.patch(start_up, "is_master_process").return_value = True
176 self.patch(start_up, "post_commit_do")
177@@ -162,12 +171,25 @@
178
179 """Tests for the actual work done in `create_region_obj`."""
180
181+ def tearDown(self):
182+ super().tearDown()
183+ # Clear maas_id cache
184+ set_maas_id(None)
185+
186 def test__creates_obj(self):
187 region = start_up.create_region_obj()
188 self.assertIsNotNone(region)
189 self.assertIsNotNone(
190 RegionController.objects.get(system_id=region.system_id))
191
192+ def test__doesnt_read_maas_id_from_cache(self):
193+ set_maas_id(factory.make_string())
194+ os.unlink(get_path('/var/lib/maas/maas_id'))
195+ region = start_up.create_region_obj()
196+ self.assertIsNotNone(region)
197+ self.assertIsNotNone(
198+ RegionController.objects.get(system_id=region.system_id))
199+
200 def test__finds_region_by_maas_id(self):
201 region = factory.make_RegionController()
202 self.useFixture(MAASIDFixture(region.system_id))
203
204=== modified file 'src/provisioningserver/register_command.py'
205--- src/provisioningserver/register_command.py 2016-06-03 22:32:14 +0000
206+++ src/provisioningserver/register_command.py 2016-06-18 01:12:20 +0000
207@@ -30,6 +30,11 @@
208 set_shared_secret_on_filesystem,
209 to_bin,
210 )
211+from provisioningserver.utils.env import set_maas_id
212+from provisioningserver.utils.shell import (
213+ call_and_check,
214+ ExternalProcessError,
215+)
216
217
218 all_arguments = (
219@@ -103,6 +108,14 @@
220 "MAAS region controller URL must be given when supplying the "
221 "shared secret via stdin with a non-interactive shell.")
222 raise SystemExit(1)
223+ try:
224+ call_and_check(['systemctl', 'stop', 'maas-rackd'])
225+ except ExternalProcessError as e:
226+ print("Unable to stop maas-rackd service.")
227+ print("Failed with error %s." % e.output_as_unicode)
228+ return
229+ # maas_id could be stale so remove it
230+ set_maas_id(None)
231 if args.url is not None:
232 with ClusterConfiguration.open_for_update() as config:
233 config.maas_url = args.url
234@@ -122,3 +135,9 @@
235 set_shared_secret_on_filesystem(to_bin(args.secret))
236 else:
237 InstallSharedSecretScript.run(args)
238+ try:
239+ call_and_check(['systemctl', 'enable', 'maas-rackd'])
240+ call_and_check(['systemctl', 'start', 'maas-rackd'])
241+ except ExternalProcessError as e:
242+ print("Unable to enable and start the maas-rackd service")
243+ print("Failed with error %s." % e.output_as_unicode)
244
245=== modified file 'src/provisioningserver/rpc/clusterservice.py'
246--- src/provisioningserver/rpc/clusterservice.py 2016-06-14 04:39:10 +0000
247+++ src/provisioningserver/rpc/clusterservice.py 2016-06-18 01:12:20 +0000
248@@ -531,7 +531,7 @@
249 cluster_uuid = config.cluster_uuid
250
251 # Grab the set system_id if already set for this controller.
252- system_id = get_maas_id()
253+ system_id = get_maas_id(read_cache=False)
254 if system_id is None:
255 # Cannot send None over RPC when the system_id is not set.
256 system_id = ''
257
258=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
259--- src/provisioningserver/rpc/tests/test_clusterservice.py 2016-06-14 04:39:10 +0000
260+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2016-06-18 01:12:20 +0000
261@@ -61,6 +61,7 @@
262 power_drivers_by_name,
263 PowerError,
264 )
265+from provisioningserver.path import get_path
266 from provisioningserver.power import QUERY_POWER_TYPES
267 from provisioningserver.power.schema import JSON_POWER_TYPE_PARAMETERS
268 from provisioningserver.rpc import (
269@@ -95,6 +96,7 @@
270 from provisioningserver.security import set_shared_secret_on_filesystem
271 from provisioningserver.service_monitor import service_monitor
272 from provisioningserver.testing.config import ClusterConfigurationFixture
273+from provisioningserver.utils.env import set_maas_id
274 from provisioningserver.utils.fs import NamedLock
275 from provisioningserver.utils.network import get_all_interfaces_definition
276 from provisioningserver.utils.shell import ExternalProcessError
277@@ -844,7 +846,7 @@
278 self.set_maas_id = self.patch(clusterservice, "set_maas_id")
279 self.set_maas_id.side_effect = set_maas_id
280
281- def get_maas_id():
282+ def get_maas_id(read_cache=True):
283 return self.maas_id
284
285 self.get_maas_id = self.patch(clusterservice, "get_maas_id")
286@@ -1247,6 +1249,28 @@
287 self.assertThat(self.set_maas_id, MockCalledOnceWith(system_id))
288
289 @inlineCallbacks
290+ def test_registerRackWithRegion_doesnt_read_maas_id_from_cache(self):
291+ set_maas_id(factory.make_string())
292+ os.unlink(get_path('/var/lib/maas/maas_id'))
293+
294+ maas_url = factory.make_simple_http_url()
295+ hostname = platform.node().split('.')[0]
296+ interfaces = get_all_interfaces_definition()
297+ self.useFixture(ClusterConfigurationFixture(
298+ maas_url=maas_url))
299+ fixture = self.useFixture(MockLiveClusterToRegionRPCFixture())
300+ protocol, connecting = fixture.makeEventLoop()
301+ self.addCleanup((yield connecting))
302+ yield getRegionClient()
303+ self.assertThat(
304+ protocol.RegisterRackController, MockCalledOnceWith(
305+ protocol, system_id='', hostname=hostname,
306+ interfaces=interfaces, url=urlparse(maas_url),
307+ nodegroup_uuid=None))
308+ # Clear cache for the next test
309+ set_maas_id(None)
310+
311+ @inlineCallbacks
312 def test_registerRackWithRegion_returns_False_when_rejected(self):
313 client = self.make_running_client()
314
315
316=== modified file 'src/provisioningserver/tests/test_register_command.py'
317--- src/provisioningserver/tests/test_register_command.py 2016-06-03 18:48:32 +0000
318+++ src/provisioningserver/tests/test_register_command.py 2016-06-18 01:12:20 +0000
319@@ -9,10 +9,16 @@
320 import io
321 from itertools import combinations
322 import pprint
323-from unittest.mock import Mock
324+from unittest.mock import (
325+ call,
326+ Mock,
327+)
328
329 from maastesting.factory import factory
330-from maastesting.matchers import MockCalledOnceWith
331+from maastesting.matchers import (
332+ MockCalledOnceWith,
333+ MockCallsMatch,
334+)
335 from maastesting.testcase import MAASTestCase
336 from provisioningserver import register_command
337 from provisioningserver.config import ClusterConfiguration
338@@ -22,6 +28,8 @@
339 to_hex,
340 )
341 from provisioningserver.testing.config import ClusterConfigurationFixture
342+from provisioningserver.utils.env import get_maas_id
343+from provisioningserver.utils.testing import MAASIDFixture
344 from testtools.matchers import Equals
345
346
347@@ -92,6 +100,8 @@
348 def setUp(self):
349 super(TestRegisterMAASRack, self).setUp()
350 self.useFixture(ClusterConfigurationFixture())
351+ self.mock_call_and_check = self.patch_autospec(
352+ register_command, 'call_and_check')
353
354 def make_args(self, **kwargs):
355 args = Mock()
356@@ -170,3 +180,20 @@
357 input.side_effect = KeyboardInterrupt
358 self.assertRaises(
359 KeyboardInterrupt, register_command.run, args)
360+
361+ def test__restarts_maas_rackd_service(self):
362+ url = factory.make_simple_http_url()
363+ secret = factory.make_bytes()
364+ register_command.run(self.make_args(url=url, secret=to_hex(secret)))
365+ self.assertThat(self.mock_call_and_check, MockCallsMatch(
366+ call(['systemctl', 'stop', 'maas-rackd']),
367+ call(['systemctl', 'enable', 'maas-rackd']),
368+ call(['systemctl', 'start', 'maas-rackd'])
369+ ))
370+
371+ def test__deletes_maas_id_file(self):
372+ self.useFixture(MAASIDFixture(factory.make_string()))
373+ url = factory.make_simple_http_url()
374+ secret = factory.make_bytes()
375+ register_command.run(self.make_args(url=url, secret=to_hex(secret)))
376+ self.assertIsNone(get_maas_id())
377
378=== modified file 'src/provisioningserver/utils/env.py'
379--- src/provisioningserver/utils/env.py 2016-06-07 11:34:46 +0000
380+++ src/provisioningserver/utils/env.py 2016-06-18 01:12:20 +0000
381@@ -35,10 +35,19 @@
382 os.environ.update(prior_environ)
383
384
385-def get_maas_id():
386+# Cache the MAAS ID so we don't have to keep reading it from the filesystem.
387+# This avoids errors when running maas-rack register while regiond is running.
388+_maas_id = None
389+
390+
391+def get_maas_id(read_cache=True):
392 """Return the system_id for this rack/region controller that is created
393 when either the rack or region first starts.
394 """
395+ global _maas_id
396+ if read_cache and _maas_id is not None:
397+ return _maas_id
398+
399 maas_id_path = get_path('/var/lib/maas/maas_id')
400 try:
401 with open(maas_id_path, "r", encoding="ascii") as fp:
402@@ -46,19 +55,24 @@
403 except FileNotFoundError:
404 return None
405 else:
406- return _normalise_maas_id(contents)
407+ _maas_id = _normalise_maas_id(contents)
408+ return _maas_id
409
410
411 def set_maas_id(system_id):
412 """Set the system_id for this rack/region permanently for MAAS."""
413+ global _maas_id
414 maas_id_path = get_path('/var/lib/maas/maas_id')
415- if _normalise_maas_id(system_id) is None:
416+ maas_id = _normalise_maas_id(system_id)
417+ if maas_id is None:
418 try:
419 atomic_delete(maas_id_path)
420 except FileNotFoundError:
421 pass # Job done already.
422+ _maas_id = None
423 else:
424- atomic_write(system_id.encode("ascii"), maas_id_path)
425+ atomic_write(maas_id.encode("ascii"), maas_id_path)
426+ _maas_id = maas_id
427
428
429 def _normalise_maas_id(system_id):
430
431=== modified file 'src/provisioningserver/utils/tests/test_env.py'
432--- src/provisioningserver/utils/tests/test_env.py 2016-06-07 11:34:46 +0000
433+++ src/provisioningserver/utils/tests/test_env.py 2016-06-18 01:12:20 +0000
434@@ -11,11 +11,8 @@
435 from maastesting.factory import factory
436 from maastesting.testcase import MAASTestCase
437 from provisioningserver.path import get_path
438-from provisioningserver.utils.env import (
439- environment_variables,
440- get_maas_id,
441- set_maas_id,
442-)
443+from provisioningserver.utils import env
444+from testtools import ExpectedException
445 from testtools.matchers import (
446 Equals,
447 FileContains,
448@@ -23,50 +20,49 @@
449 Is,
450 Not,
451 )
452-from testtools.testcase import ExpectedException
453
454
455 class TestEnvironmentVariables(MAASTestCase):
456- """Tests for `environment_variables`."""
457+ """Tests for `env.environment_variables`."""
458
459 def make_variable(self):
460 return factory.make_name('testvar'), factory.make_name('value')
461
462 def test__sets_variables(self):
463 var, value = self.make_variable()
464- with environment_variables({var: value}):
465- env = os.environ.copy()
466- self.assertEqual(value, env[var])
467+ with env.environment_variables({var: value}):
468+ environment = os.environ.copy()
469+ self.assertEqual(value, environment[var])
470
471 def test__overrides_prior_values(self):
472 var, prior_value = self.make_variable()
473 temp_value = factory.make_name('temp-value')
474- with environment_variables({var: prior_value}):
475- with environment_variables({var: temp_value}):
476- env = os.environ.copy()
477- self.assertEqual(temp_value, env[var])
478+ with env.environment_variables({var: prior_value}):
479+ with env.environment_variables({var: temp_value}):
480+ environment = os.environ.copy()
481+ self.assertEqual(temp_value, environment[var])
482
483 def test__leaves_other_variables_intact(self):
484 untouched_var, untouched_value = self.make_variable()
485 var, value = self.make_variable()
486- with environment_variables({untouched_var: untouched_value}):
487- with environment_variables({var: value}):
488- env = os.environ.copy()
489- self.assertEqual(untouched_value, env[untouched_var])
490+ with env.environment_variables({untouched_var: untouched_value}):
491+ with env.environment_variables({var: value}):
492+ environment = os.environ.copy()
493+ self.assertEqual(untouched_value, environment[untouched_var])
494
495 def test__restores_variables_to_previous_values(self):
496 var, prior_value = self.make_variable()
497 temp_value = factory.make_name('temp-value')
498- with environment_variables({var: prior_value}):
499- with environment_variables({var: temp_value}):
500+ with env.environment_variables({var: prior_value}):
501+ with env.environment_variables({var: temp_value}):
502 pass
503- env = os.environ.copy()
504- self.assertEqual(prior_value, env[var])
505+ environment = os.environ.copy()
506+ self.assertEqual(prior_value, environment[var])
507
508 def test__restores_previously_unset_variables_to_being_unset(self):
509 var, value = self.make_variable()
510 self.assertNotIn(var, os.environ)
511- with environment_variables({var: value}):
512+ with env.environment_variables({var: value}):
513 pass
514 self.assertNotIn(var, os.environ)
515
516@@ -78,7 +74,7 @@
517 pass
518
519 with ExpectedException(DeliberateException):
520- with environment_variables({var: value}):
521+ with env.environment_variables({var: value}):
522 raise DeliberateException()
523
524 self.assertNotIn(var, os.environ)
525@@ -93,32 +89,36 @@
526
527
528 class TestMAASID(MAASTestCase):
529- """Tests for `get_maas_id` and `set_maas_id`."""
530+ """Tests for `env.get_maas_id` and `env.set_maas_id`."""
531
532 def setUp(self):
533 super(TestMAASID, self).setUp()
534 self.maas_id_path = get_path('/var/lib/maas/maas_id')
535 self.addCleanup(unlink_if_exists, self.maas_id_path)
536
537+ def tearDown(self):
538+ super(TestMAASID, self).tearDown()
539+ env.set_maas_id(None)
540+
541 def test_get_returns_None_if_maas_id_file_does_not_exist(self):
542 self.assertThat(self.maas_id_path, Not(FileExists()))
543- self.assertThat(get_maas_id(), Is(None))
544+ self.assertThat(env.get_maas_id(), Is(None))
545
546 def test_get_returns_None_if_maas_id_file_is_empty(self):
547 with open(self.maas_id_path, "w"):
548 pass # Write nothing.
549- self.assertThat(get_maas_id(), Is(None))
550+ self.assertThat(env.get_maas_id(), Is(None))
551
552 def test_get_returns_None_if_maas_id_file_is_whitespace(self):
553 with open(self.maas_id_path, "w") as fd:
554 fd.write(string.whitespace)
555- self.assertThat(get_maas_id(), Is(None))
556+ self.assertThat(env.get_maas_id(), Is(None))
557
558 def test_get_returns_contents_if_maas_id_file_contains_something(self):
559 contents = factory.make_name("contents")
560 with open(self.maas_id_path, "w") as fd:
561 fd.write(contents)
562- self.assertThat(get_maas_id(), Equals(contents))
563+ self.assertThat(env.get_maas_id(), Equals(contents))
564
565 def test_get_strips_contents_if_maas_id_file_contains_something(self):
566 contents = factory.make_name("contents")
567@@ -126,35 +126,76 @@
568 fd.write(string.whitespace)
569 fd.write(contents)
570 fd.write(string.whitespace)
571- self.assertThat(get_maas_id(), Equals(contents))
572+ self.assertThat(env.get_maas_id(), Equals(contents))
573
574 def test_get_rejects_non_ASCII_content(self):
575 contents = factory.make_unicode_non_ascii_string()
576 with open(self.maas_id_path, "w") as fd:
577 fd.write(contents)
578- self.assertRaises(UnicodeDecodeError, get_maas_id)
579+ self.assertRaises(UnicodeDecodeError, env.get_maas_id)
580+
581+ def test_get_caches_result(self):
582+ contents = factory.make_name("contents")
583+ with open(self.maas_id_path, "w") as fd:
584+ fd.write(contents)
585+ self.assertEquals(contents, env.get_maas_id())
586+ os.unlink(self.maas_id_path)
587+ self.assertEquals(contents, env.get_maas_id())
588+
589+ def test_get_can_ignore_cache(self):
590+ contents = factory.make_name("contents")
591+ with open(self.maas_id_path, "w") as fd:
592+ fd.write(contents)
593+ self.assertEquals(contents, env.get_maas_id())
594+ contents = factory.make_name("contents")
595+ os.unlink(self.maas_id_path)
596+ with open(self.maas_id_path, "w") as fd:
597+ fd.write(contents)
598+ self.assertEquals(contents, env.get_maas_id(read_cache=False))
599
600 def test_set_writes_argument_to_maas_id_file(self):
601 contents = factory.make_name("contents")
602- set_maas_id(contents)
603+ env.set_maas_id(contents)
604 self.assertThat(self.maas_id_path, FileContains(contents))
605
606 def test_set_deletes_maas_id_file_if_argument_is_None(self):
607 with open(self.maas_id_path, "w") as fd:
608 fd.write("This file will be deleted.")
609- set_maas_id(None)
610+ env.set_maas_id(None)
611 self.assertThat(self.maas_id_path, Not(FileExists()))
612+ self.assertIsNone(env.get_maas_id())
613
614 def test_set_deletes_maas_id_file_if_argument_is_whitespace(self):
615 with open(self.maas_id_path, "w") as fd:
616 fd.write("This file will be deleted.")
617- set_maas_id(string.whitespace)
618+ env.set_maas_id(string.whitespace)
619 self.assertThat(self.maas_id_path, Not(FileExists()))
620+ self.assertIsNone(env.get_maas_id())
621
622- def test_set_None_does_nothing_if_maas_id_file_idoes_not_exist(self):
623+ def test_set_None_does_nothing_if_maas_id_file_does_not_exist(self):
624 self.assertThat(self.maas_id_path, Not(FileExists()))
625- set_maas_id(None)
626+ env.set_maas_id(None)
627
628 def test_set_rejects_non_ASCII_content(self):
629 contents = factory.make_unicode_non_ascii_string()
630- self.assertRaises(UnicodeEncodeError, set_maas_id, contents)
631+ self.assertRaises(UnicodeEncodeError, env.set_maas_id, contents)
632+
633+ def test_set_caches(self):
634+ contents = factory.make_name("contents")
635+ env.set_maas_id(contents)
636+ os.unlink(self.maas_id_path)
637+ self.assertEquals(contents, env.get_maas_id())
638+
639+ def test_set_doesnt_cache_when_write_fails(self):
640+ mock_atomic_write = self.patch_autospec(env, 'atomic_write')
641+ exception = factory.make_exception()
642+ mock_atomic_write.side_effect = exception
643+ contents = factory.make_name("contents")
644+ with ExpectedException(type(exception)):
645+ env.set_maas_id(contents)
646+ self.assertIsNone(env.get_maas_id())
647+
648+ def test_set_caches_to_normalized_value(self):
649+ contents = " %s " % factory.make_name("contents")
650+ env.set_maas_id(contents)
651+ self.assertEquals(env._normalise_maas_id(contents), env.get_maas_id())