Merge lp:~allenap/maas/kill-all-the-configs-cluster 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: 4066
Proposed branch: lp:~allenap/maas/kill-all-the-configs-cluster
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 5182 lines (+776/-1974)
78 files modified
.bzrignore (+4/-1)
.gitignore (+6/-0)
HACKING.txt (+1/-1)
INSTALL.txt (+1/-1)
Makefile (+17/-2)
buildout.cfg (+0/-3)
docs/development/cluster-registration.rst (+3/-23)
etc/demo_maas_cluster.conf (+0/-8)
etc/maas/pserv.yaml (+0/-24)
etc/maas_cluster.conf (+0/-5)
services/clusterd/run (+10/-8)
setup.py (+1/-3)
src/maas/demo.py (+0/-3)
src/maas/development.py (+0/-8)
src/maas/settings.py (+0/-5)
src/maasserver/clusterrpc/tests/test_boot_images.py (+19/-21)
src/maasserver/config.py (+1/-10)
src/maasserver/rpc/tests/test_regionservice.py (+2/-2)
src/maasserver/tests/test_config.py (+0/-14)
src/maasserver/utils/__init__.py (+9/-19)
src/maasserver/utils/tests/test_utils.py (+4/-11)
src/maastesting/protractor/runner.py (+6/-19)
src/provisioningserver/__main__.py (+0/-2)
src/provisioningserver/boot/install_grub.py (+5/-5)
src/provisioningserver/boot/tests/test_boot.py (+3/-3)
src/provisioningserver/boot/tests/test_install_grub.py (+3/-4)
src/provisioningserver/boot/tests/test_powernv.py (+7/-4)
src/provisioningserver/boot/tests/test_pxe.py (+2/-2)
src/provisioningserver/boot/tests/test_tftppath.py (+4/-14)
src/provisioningserver/boot/tests/test_windows.py (+3/-9)
src/provisioningserver/boot/tftppath.py (+2/-6)
src/provisioningserver/boot/windows.py (+3/-2)
src/provisioningserver/cluster_config.py (+0/-88)
src/provisioningserver/cluster_config_command.py (+18/-32)
src/provisioningserver/config.py (+121/-150)
src/provisioningserver/configure_maas_url.py (+0/-123)
src/provisioningserver/dhcp/tests/test_config.py (+3/-3)
src/provisioningserver/dhcp/tests/test_detect.py (+3/-5)
src/provisioningserver/diskless.py (+15/-9)
src/provisioningserver/drivers/osystem/custom.py (+6/-5)
src/provisioningserver/drivers/osystem/tests/test_custom.py (+7/-3)
src/provisioningserver/drivers/osystem/tests/test_windows.py (+2/-6)
src/provisioningserver/drivers/osystem/windows.py (+3/-2)
src/provisioningserver/import_images/boot_resources.py (+16/-8)
src/provisioningserver/import_images/tests/test_boot_resources.py (+14/-5)
src/provisioningserver/path.py (+16/-5)
src/provisioningserver/plugin.py (+36/-51)
src/provisioningserver/pserv_services/image_download_service.py (+16/-15)
src/provisioningserver/pserv_services/service_monitor_service.py (+9/-3)
src/provisioningserver/pserv_services/tests/test_image_download_service.py (+13/-11)
src/provisioningserver/pserv_services/tests/test_service_monitor_service.py (+21/-1)
src/provisioningserver/pserv_services/tests/test_tftp.py (+36/-20)
src/provisioningserver/pserv_services/tftp.py (+7/-5)
src/provisioningserver/rpc/boot_images.py (+6/-6)
src/provisioningserver/rpc/clusterservice.py (+11/-10)
src/provisioningserver/rpc/tags.py (+6/-6)
src/provisioningserver/rpc/testing/__init__.py (+0/-6)
src/provisioningserver/rpc/tests/test_boot_images.py (+10/-4)
src/provisioningserver/rpc/tests/test_clusterservice.py (+20/-24)
src/provisioningserver/rpc/tests/test_tags.py (+7/-6)
src/provisioningserver/testing/config.py (+0/-20)
src/provisioningserver/testing/testcase.py (+0/-9)
src/provisioningserver/tests/test_cluster_config.py (+0/-48)
src/provisioningserver/tests/test_cluster_config_command.py (+36/-31)
src/provisioningserver/tests/test_config.py (+66/-454)
src/provisioningserver/tests/test_configure_maas_url.py (+0/-351)
src/provisioningserver/tests/test_diskless.py (+24/-17)
src/provisioningserver/tests/test_path.py (+8/-0)
src/provisioningserver/tests/test_plugin.py (+18/-43)
src/provisioningserver/tests/test_tags.py (+4/-2)
src/provisioningserver/tests/test_upgrade_cluster.py (+10/-9)
src/provisioningserver/upgrade_cluster.py (+10/-7)
src/provisioningserver/utils/__init__.py (+13/-28)
src/provisioningserver/utils/fs.py (+7/-2)
src/provisioningserver/utils/script.py (+2/-19)
src/provisioningserver/utils/tests/test_fs.py (+21/-5)
src/provisioningserver/utils/tests/test_script.py (+0/-25)
src/provisioningserver/utils/tests/test_utils.py (+19/-50)
To merge this branch: bzr merge lp:~allenap/maas/kill-all-the-configs-cluster
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+263494@code.launchpad.net

Commit message

Consolidate the local configuration for the cluster into a single file.

Description of the change

This is largely based on Nate's work which has already been reviewed,
but the changes I made warrant a new review from the ground up.

You can see from the diffstat - http://paste.ubuntu.com/11803857/ - that
the majority of this branch is small changes to many files, resulting in
a lot of context in the diff. The summary is:

 75 files changed, 726 insertions(+), 1968 deletions(-)

It's a fairly big change, but little of it is worth more than a moment's
contemplation, and the biggest overall change is removals.

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

Tested this, seems to be working just fine so +1

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

The attempt to merge lp:~allenap/maas/kill-all-the-configs-cluster into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Get:2 http://security.ubuntu.com trusty-security Release [63.5 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [63.5 kB]
Get:5 http://security.ubuntu.com trusty-security/main Sources [87.4 kB]
Get:6 http://security.ubuntu.com trusty-security/universe Sources [28.1 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [304 kB]
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [111 kB]
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/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [212 kB]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [123 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [562 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [292 kB]
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
Fetched 1,848 kB in 4s (439 kB/s)
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-iscpy python-jinja2 python-jsonschema python-lockfile python-lxml python-mock python-netaddr python-netif...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2015-06-15 10:58:41 +0000
3+++ .bzrignore 2015-07-03 11:18:28 +0000
4@@ -24,8 +24,9 @@
5 ./docs/_autosummary
6 ./docs/_build
7 ./docs/api.rst
8+./eggs
9+./etc/maas/clusterd.conf
10 ./etc/maas/regiond.conf
11-./eggs
12 ./include
13 ./lib
14 ./local
15@@ -35,6 +36,8 @@
16 ./media/development
17 ./parts
18 ./run/*
19+./run/etc/*
20+./run/etc/maas/*
21 ./services/*/introspect
22 ./services/*/supervise
23 ./src/maasserver/static/js/enums.js
24
25=== modified file '.gitignore'
26--- .gitignore 2015-04-10 09:45:46 +0000
27+++ .gitignore 2015-07-03 11:18:28 +0000
28@@ -4,6 +4,8 @@
29 /.db.lock
30 /.hypothesis
31 /.idea
32+/.idea/scopes
33+/.idea/workspace.xml
34 /.installed.cfg
35 /.noseids
36 /acceptance/*.build
37@@ -23,6 +25,8 @@
38 /docs/_build
39 /docs/api.rst
40 /eggs
41+/etc/maas/clusterd.conf
42+/etc/maas/regiond.conf
43 /include
44 /lib
45 /local
46@@ -32,6 +36,8 @@
47 /media/development
48 /parts
49 /run/*
50+/run/etc/*
51+/run/etc/maas/*
52 /services/*/introspect
53 /services/*/supervise
54 /src/maasserver/static/js/enums.js
55
56=== modified file 'HACKING.txt'
57--- HACKING.txt 2015-06-10 11:49:32 +0000
58+++ HACKING.txt 2015-07-03 11:18:28 +0000
59@@ -223,7 +223,7 @@
60 you want to boot some real hardware. By default, it's set to start up on
61 port 5244 for testing purposes. Make these changes::
62
63- * Edit ``etc/maas/pserv.yaml`` to change the tftp/port setting to 69
64+ * Use ``bin/maas-provision`` to change the tftp-port setting to 69
65 * Install the ``authbind``package:
66
67 $ sudo apt-get install authbind
68
69=== modified file 'INSTALL.txt'
70--- INSTALL.txt 2015-05-22 14:59:47 +0000
71+++ INSTALL.txt 2015-07-03 11:18:28 +0000
72@@ -330,7 +330,7 @@
73
74 * Initiate TCP connections (for HTTP) to each region controller on
75 port 80 or port 5240, the choice of which depends on the setting of
76- ``MAAS_URL``.
77+ the MAAS URL.
78
79 * Initiate TCP connections (for RPC) to each region controller between
80 port 5250 and 5259 inclusive. This permits up to 10 ``maas-regiond``
81
82=== modified file 'Makefile'
83--- Makefile 2015-06-16 21:10:26 +0000
84+++ Makefile 2015-07-03 11:18:28 +0000
85@@ -297,7 +297,7 @@
86 clean-styles:
87 $(RM) $(scss_output)
88
89-clean: stop
90+clean: stop clean-run
91 $(MAKE) -C acceptance $@
92 find . -type f -name '*.py[co]' -print0 | xargs -r0 $(RM)
93 find . -type f -name '*~' -print0 | xargs -r0 $(RM)
94@@ -316,7 +316,21 @@
95 $(RM) -r build dist logs/* parts
96 $(RM) tags TAGS .installed.cfg
97 $(RM) -r *.egg *.egg-info src/*.egg-info
98- $(RM) -r run/* run-e2e/* services/*/supervise
99+ $(RM) -r services/*/supervise
100+
101+# Be selective about what to remove from run and run-e2e.
102+define clean-run-template
103+find $(1) -depth ! -type d \
104+ ! -path $(1)/etc/maas/templates \
105+ ! -path $(1)/etc/maas/drivers.yaml \
106+ -print0 | xargs -r0 $(RM)
107+find $(1) -depth -type d \
108+ -print0 | xargs -r0 rmdir --ignore-fail-on-non-empty
109+endef
110+
111+clean-run:
112+ $(call clean-run-template,run)
113+ $(call clean-run-template,run-e2e)
114
115 clean+db: clean
116 $(RM) -r db
117@@ -348,6 +362,7 @@
118 check
119 clean
120 clean+db
121+ clean-run
122 clean-styles
123 configure-buildout
124 copyright
125
126=== modified file 'buildout.cfg'
127--- buildout.cfg 2015-06-15 10:58:41 +0000
128+++ buildout.cfg 2015-07-03 11:18:28 +0000
129@@ -46,7 +46,6 @@
130 ${common:environment}
131 environment =
132 from os import environ
133- environ.setdefault("MAAS_CONFIG_DIR", "${buildout:directory}/etc/maas")
134 environ.setdefault("MAAS_ROOT", "${buildout:directory}/run")
135 warnings =
136 from warnings import filterwarnings
137@@ -77,7 +76,6 @@
138 twistd.region=twisted.scripts.twistd:run
139 initialization =
140 ${common:initialization}
141- environ.setdefault("MAAS_REGION_CONFIG", "${buildout:directory}/etc/maas/regiond.conf")
142 environ.setdefault("DJANGO_SETTINGS_MODULE", "maas.development")
143 scripts =
144 maas-region-admin
145@@ -190,7 +188,6 @@
146 twistd.cluster
147 initialization =
148 ${common:initialization}
149- environ.setdefault("MAAS_PROVISION_CMD", "${buildout:directory}/bin/maas-provision")
150 environ.setdefault("MAAS_CLUSTER_DEVELOP", "TRUE")
151
152 [cluster-test]
153
154=== modified file 'docs/development/cluster-registration.rst'
155--- docs/development/cluster-registration.rst 2015-02-19 21:49:54 +0000
156+++ docs/development/cluster-registration.rst 2015-07-03 11:18:28 +0000
157@@ -21,29 +21,9 @@
158 Region Controller Location
159 --------------------------
160
161-The cluster obviously needs to know where the region controller is, and this is
162-configured in a file ``/etc/maas/maas_cluster.conf`` (or
163-``etc/demo_maas_cluster.conf`` for development environments).
164-
165-Cluster configuration file
166---------------------------
167-
168-This config file generally contains two items like this::
169-
170- MAAS_URL=http://0.0.0.0:5240/
171- CLUSTER_UUID="adfd3977-f251-4f2c-8d61-745dbd690bf2"
172-
173-The values here are the defaults in the development environment. MAAS_URL
174-tells the cluster controller where to find the region controller.
175-
176-``CLUSTER_UUID`` is what the region uses to tell clusters apart when they
177-connect. Each cluster is free to generate its own UUID but the development
178-environment fixes it in advance. The Ubuntu packaging generates a new UUID for
179-a cluster controller each time it is installed.
180-
181-.. warning::
182- The format of this config file is very sensitive due to the code that parses
183- it. It will not accept quoting, or any kind of comments.
184+The cluster obviously needs to know where the region controller is, and
185+this is configured in a file ``/etc/maas/clusterd.conf``. This should
186+only ever be modified via the ``maas-provision`` command.
187
188 .. _first-cluster:
189
190
191=== removed file 'etc/demo_maas_cluster.conf'
192--- etc/demo_maas_cluster.conf 2015-03-12 18:13:14 +0000
193+++ etc/demo_maas_cluster.conf 1970-01-01 00:00:00 +0000
194@@ -1,8 +0,0 @@
195-# This is the version of maas_cluster.conf that's used when running from a
196-# branch. The region controller will serve HTTP on a different port from a
197-# packaged MAAS, and the master cluster controller (running on the same
198-# development machine) has a fixed UUID.
199-# It is also carefully crafted so it works as both a bash script and a
200-# Python script.
201-MAAS_URL="http://0.0.0.0:5240/MAAS/"
202-CLUSTER_UUID="adfd3977-f251-4f2c-8d61-745dbd690bf2"
203
204=== removed file 'etc/maas/pserv.yaml'
205--- etc/maas/pserv.yaml 2015-04-24 13:27:39 +0000
206+++ etc/maas/pserv.yaml 1970-01-01 00:00:00 +0000
207@@ -1,24 +0,0 @@
208-##
209-## Provisioning Server (pserv) configuration.
210-##
211-
212-## Where to log. This log can be rotated by sending SIGUSR1 to the
213-## running server.
214-#
215-# logfile: "pserv.log"
216-logfile: "/dev/null"
217-
218-## TFTP configuration.
219-#
220-tftp:
221- # The "root" setting has been replaced by "resource_root". The old setting
222- # is used one final time when upgrading a pre-14.04 cluster controller to a
223- # 14.04 version. After that upgrade, it can be removed.
224- #
225- # resource_root: /var/lib/maas/boot-resources/current/
226-
227- # port: 69
228- port: 5244
229- ## The URL to be contacted to generate PXE configurations.
230- # generator: http://localhost/MAAS/api/1.0/pxeconfig/
231- generator: http://localhost:5240/MAAS/api/1.0/pxeconfig/
232
233=== removed file 'etc/maas_cluster.conf'
234--- etc/maas_cluster.conf 2015-03-12 18:13:14 +0000
235+++ etc/maas_cluster.conf 1970-01-01 00:00:00 +0000
236@@ -1,5 +0,0 @@
237-# This file is used by the cluster controller startup script:
238-# `maas-provision start-cluster-controller`
239-# to get the URL to the MAAS region controller's API. Normally, packaging
240-# will update this file automatically but it may also be configured manually.
241-MAAS_URL=http://localhost/MAAS
242
243=== added directory 'run-e2e/etc'
244=== added directory 'run-e2e/etc/maas'
245=== added symlink 'run-e2e/etc/maas/drivers.yaml'
246=== target is u'../../../etc/maas/drivers.yaml'
247=== added symlink 'run-e2e/etc/maas/templates'
248=== target is u'../../../etc/maas/templates'
249=== added directory 'run/etc'
250=== added directory 'run/etc/maas'
251=== added symlink 'run/etc/maas/drivers.yaml'
252=== target is u'../../../etc/maas/drivers.yaml'
253=== added symlink 'run/etc/maas/templates'
254=== target is u'../../../etc/maas/templates'
255=== modified file 'services/clusterd/run'
256--- services/clusterd/run 2015-05-29 16:47:37 +0000
257+++ services/clusterd/run 2015-07-03 11:18:28 +0000
258@@ -17,16 +17,18 @@
259 # because there are race issues when restarting.
260 [ -z "${logdir:-}" ] || exec &>> "${logdir}/current"
261
262+# Configure the cluster's UUID to match sampledata, and also use a high
263+# port for TFTP to match this branch's etc/services.
264+bin/maas-provision config \
265+ --uuid adfd3977-f251-4f2c-8d61-745dbd690bf2 \
266+ --tftp-port 5244
267+
268+# Wait for the installation of the shared-secret.
269+until bin/maas-provision check-for-shared-secret; do sleep 2; done
270+
271 # Exec the Provisioning Server.
272 script="$(readlink -f bin/twistd.cluster)"
273-config="$(readlink -f etc/maas/pserv.yaml)"
274-
275-# Obtain the development setting for CLUSTER_UUID.
276-. etc/demo_maas_cluster.conf
277-export CLUSTER_UUID
278-export MAAS_URL
279
280 exec $(command -v authbind && echo --deep) \
281 "${script}" --nodaemon --pidfile="" maas-clusterd \
282- --introspect="unix:${here}/introspect:lockfile=0" \
283- --config-file "${config}"
284+ --introspect="unix:${here}/introspect:lockfile=0"
285
286=== modified file 'setup.py'
287--- setup.py 2015-05-29 16:47:37 +0000
288+++ setup.py 2015-07-03 11:18:28 +0000
289@@ -64,9 +64,7 @@
290
291 data_files=[
292 ('/etc/maas',
293- ['etc/maas/pserv.yaml',
294- 'etc/maas/drivers.yaml',
295- 'etc/maas_cluster.conf',
296+ ['etc/maas/drivers.yaml',
297 'contrib/maas-http.conf']),
298 ('/etc/maas/templates/uefi',
299 glob('etc/maas/templates/uefi/*.template')),
300
301=== modified file 'src/maas/demo.py'
302--- src/maas/demo.py 2015-06-15 10:58:41 +0000
303+++ src/maas/demo.py 2015-07-03 11:18:28 +0000
304@@ -38,9 +38,6 @@
305
306 MAAS_CLI = abspath("bin/maas-region-admin")
307
308-# Use the in-branch development version of maas_cluster.conf.
309-LOCAL_CLUSTER_CONFIG = abspath("etc/demo_maas_cluster.conf")
310-
311 # For demo purposes, give nodes unauthenticated access to their metadata
312 # even if we can't pass boot parameters. This is not safe; do not
313 # enable it on a production MAAS.
314
315=== modified file 'src/maas/development.py'
316--- src/maas/development.py 2015-06-15 10:58:41 +0000
317+++ src/maas/development.py 2015-07-03 11:18:28 +0000
318@@ -95,14 +95,6 @@
319 # Inject custom code for setting up the test database.
320 patch_db_creation(abspath('db'), abspath('schema/baseline.sql'))
321
322-# Override the default provisioning config filename.
323-import provisioningserver.config
324-provisioningserver.config.Config.DEFAULT_FILENAME = abspath(
325- "etc/maas/pserv.yaml")
326-
327-# Use the in-branch development version of maas_cluster.conf.
328-LOCAL_CLUSTER_CONFIG = abspath("etc/demo_maas_cluster.conf")
329-
330 PASSWORD_HASHERS = (
331 'django.contrib.auth.hashers.MD5PasswordHasher',
332 )
333
334=== modified file 'src/maas/settings.py'
335--- src/maas/settings.py 2015-06-10 10:08:45 +0000
336+++ src/maas/settings.py 2015-07-03 11:18:28 +0000
337@@ -68,11 +68,6 @@
338 # same model, so we silence the warnings that Piston gives.
339 PISTON_IGNORE_DUPE_MODELS = True
340
341-# Location of the local cluster config file (installed by
342-# the package maas-cluster-controller). Use to distinguish the local cluster
343-# from the others.
344-LOCAL_CLUSTER_CONFIG = "/etc/maas/maas_cluster.conf"
345-
346 TEMPLATE_DEBUG = DEBUG
347
348 YUI_DEBUG = DEBUG
349
350=== modified file 'src/maasserver/clusterrpc/tests/test_boot_images.py'
351--- src/maasserver/clusterrpc/tests/test_boot_images.py 2015-05-29 16:47:37 +0000
352+++ src/maasserver/clusterrpc/tests/test_boot_images.py 2015-07-03 11:18:28 +0000
353@@ -45,11 +45,12 @@
354 make_boot_image_storage_params,
355 make_image,
356 )
357+from provisioningserver.testing.config import ClusterConfigurationFixture
358 from twisted.internet.defer import succeed
359
360
361-def make_image_dir(image_params, tftproot):
362- """Fake a boot image matching `image_params` under `tftproot`."""
363+def make_image_dir(image_params, tftp_root):
364+ """Fake a boot image matching `image_params` under `tftp_root`."""
365 image_dir = locate_tftp_path(
366 compose_image_path(
367 osystem=image_params['osystem'],
368@@ -57,7 +58,7 @@
369 subarch=image_params['subarchitecture'],
370 release=image_params['release'],
371 label=image_params['label']),
372- tftproot)
373+ tftp_root)
374 os.makedirs(image_dir)
375 factory.make_file(image_dir, 'linux')
376 factory.make_file(image_dir, 'initrd.gz')
377@@ -136,16 +137,21 @@
378 self.assertFalse(is_import_boot_images_running_for(nodegroup))
379
380
381+def prepare_tftp_root(test):
382+ """Create a `current` directory and configure its use."""
383+ test.tftp_root = os.path.join(test.make_dir(), 'current')
384+ os.mkdir(test.tftp_root)
385+ test.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
386+ config = ClusterConfigurationFixture(tftp_root=test.tftp_root)
387+ test.useFixture(config)
388+
389+
390 class TestGetBootImages(MAASServerTestCase):
391 """Tests for `get_boot_images`."""
392
393 def setUp(self):
394 super(TestGetBootImages, self).setUp()
395- resource_dir = self.make_dir()
396- self.tftproot = os.path.join(resource_dir, 'current')
397- os.mkdir(self.tftproot)
398- self.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
399- self.patch(boot_images, 'BOOT_RESOURCES_STORAGE', resource_dir)
400+ prepare_tftp_root(self) # Sets self.tftp_root.
401
402 def test_returns_boot_images(self):
403 nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
404@@ -154,7 +160,7 @@
405 purposes = ['install', 'commissioning', 'xinstall']
406 params = [make_boot_image_storage_params() for _ in range(3)]
407 for param in params:
408- make_image_dir(param, self.tftproot)
409+ make_image_dir(param, self.tftp_root)
410 test_tftppath.make_osystem(self, param['osystem'], purposes)
411 self.assertItemsEqual(
412 [
413@@ -170,11 +176,7 @@
414
415 def setUp(self):
416 super(TestGetAvailableBootImages, self).setUp()
417- resource_dir = self.make_dir()
418- self.tftproot = os.path.join(resource_dir, 'current')
419- os.mkdir(self.tftproot)
420- self.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
421- self.patch(boot_images, 'BOOT_RESOURCES_STORAGE', resource_dir)
422+ prepare_tftp_root(self) # Sets self.tftp_root.
423
424 def test_returns_boot_images_for_one_cluster(self):
425 factory.make_NodeGroup().accept()
426@@ -183,7 +185,7 @@
427 purposes = ['install', 'commissioning', 'xinstall']
428 params = [make_boot_image_storage_params() for _ in range(3)]
429 for param in params:
430- make_image_dir(param, self.tftproot)
431+ make_image_dir(param, self.tftp_root)
432 test_tftppath.make_osystem(self, param['osystem'], purposes)
433 self.assertItemsEqual(
434 [
435@@ -260,17 +262,13 @@
436
437 def setUp(self):
438 super(TestGetBootImagesFor, self).setUp()
439- resource_dir = self.make_dir()
440- self.tftproot = os.path.join(resource_dir, 'current')
441- os.mkdir(self.tftproot)
442- self.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
443- self.patch(boot_images, 'BOOT_RESOURCES_STORAGE', resource_dir)
444+ prepare_tftp_root(self) # Sets self.tftp_root.
445
446 def make_boot_images(self):
447 purposes = ['install', 'commissioning', 'xinstall']
448 params = [make_boot_image_storage_params() for _ in range(3)]
449 for param in params:
450- make_image_dir(param, self.tftproot)
451+ make_image_dir(param, self.tftp_root)
452 test_tftppath.make_osystem(self, param['osystem'], purposes)
453 return params
454
455
456=== modified file 'src/maasserver/config.py'
457--- src/maasserver/config.py 2015-06-19 14:43:57 +0000
458+++ src/maasserver/config.py 2015-07-03 11:18:28 +0000
459@@ -25,6 +25,7 @@
460 ConfigurationMeta,
461 ConfigurationOption,
462 ExtendedURL,
463+ is_dev_environment,
464 )
465
466
467@@ -68,13 +69,3 @@
468 return path.join(path.dirname(__file__), "static")
469 else:
470 return "/usr/share/maas/web/static"
471-
472-
473-def is_dev_environment():
474- """Is this the development environment, or production?"""
475- try:
476- from maastesting import root # noqa
477- except:
478- return False
479- else:
480- return True
481
482=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
483--- src/maasserver/rpc/tests/test_regionservice.py 2015-06-15 11:14:37 +0000
484+++ src/maasserver/rpc/tests/test_regionservice.py 2015-07-03 11:18:28 +0000
485@@ -131,7 +131,7 @@
486 call_responder,
487 )
488 from provisioningserver.rpc.testing.doubles import DummyConnection
489-from provisioningserver.testing.config import set_tftp_root
490+from provisioningserver.testing.config import ClusterConfigurationFixture
491 from provisioningserver.utils import events
492 from provisioningserver.utils.twisted import asynchronous
493 from simplejson import dumps
494@@ -362,7 +362,7 @@
495 os.makedirs(os.path.join(tftpdir, *options))
496
497 # Ensure that report_boot_images() uses the above TFTP file tree.
498- self.useFixture(set_tftp_root(tftpdir))
499+ self.useFixture(ClusterConfigurationFixture(tftp_root=tftpdir))
500
501 images = [
502 {"architecture": arch, "subarchitecture": subarch,
503
504=== modified file 'src/maasserver/tests/test_config.py'
505--- src/maasserver/tests/test_config.py 2015-06-10 10:19:06 +0000
506+++ src/maasserver/tests/test_config.py 2015-07-03 11:18:28 +0000
507@@ -14,11 +14,8 @@
508 __metaclass__ = type
509 __all__ = []
510
511-
512-from maasserver import config
513 from maasserver.config import RegionConfiguration
514 from maastesting.factory import factory
515-from maastesting.fixtures import ImportErrorFixture
516 from maastesting.testcase import MAASTestCase
517
518
519@@ -95,14 +92,3 @@
520 self.assertEqual(example_value, getattr(config, self.option))
521 # It's also stored in the configuration database.
522 self.assertEqual({self.option: example_value}, config.store)
523-
524-
525-class TestConfig(MAASTestCase):
526- """Tests for `maasserver.config`."""
527-
528- def test_is_dev_environment_returns_false(self):
529- self.useFixture(ImportErrorFixture('maastesting', 'root'))
530- self.assertFalse(config.is_dev_environment())
531-
532- def test_is_dev_environment_returns_true(self):
533- self.assertTrue(config.is_dev_environment())
534
535=== modified file 'src/maasserver/utils/__init__.py'
536--- src/maasserver/utils/__init__.py 2015-06-17 14:15:11 +0000
537+++ src/maasserver/utils/__init__.py 2015-07-03 11:18:28 +0000
538@@ -25,21 +25,22 @@
539 'synchronised',
540 ]
541
542-import errno
543 from functools import wraps
544-import re
545 from urllib import urlencode
546 from urlparse import (
547 urljoin,
548 urlparse,
549 )
550
551-from django.conf import settings
552 from django.core.urlresolvers import reverse
553 from maasserver.config import RegionConfiguration
554 from maasserver.enum import NODEGROUPINTERFACE_MANAGEMENT
555 from maasserver.exceptions import NodeGroupMisconfiguration
556 from maasserver.utils.orm import get_one
557+from provisioningserver.config import (
558+ ClusterConfiguration,
559+ UUID_NOT_SET,
560+)
561 from provisioningserver.utils.text import make_bullet_list
562
563
564@@ -150,22 +151,11 @@
565
566 def get_local_cluster_UUID():
567 """Return the UUID of the local cluster (or None if it cannot be found)."""
568- try:
569- cluster_config = open(settings.LOCAL_CLUSTER_CONFIG).read()
570- match = re.search(
571- "CLUSTER_UUID=(?P<quote>[\"']?)([^\"']+)(?P=quote)",
572- cluster_config)
573- if match is not None:
574- return match.groups()[1]
575- else:
576- return None
577- except IOError as error:
578- if error.errno == errno.ENOENT:
579- # Cluster config file is not present.
580- return None
581- else:
582- # Anything else is an error.
583- raise
584+ with ClusterConfiguration.open() as config:
585+ if config.cluster_uuid == UUID_NOT_SET:
586+ return None
587+ else:
588+ return config.cluster_uuid
589
590
591 def find_nodegroup(request):
592
593=== modified file 'src/maasserver/utils/tests/test_utils.py'
594--- src/maasserver/utils/tests/test_utils.py 2015-06-10 10:08:45 +0000
595+++ src/maasserver/utils/tests/test_utils.py 2015-07-03 11:18:28 +0000
596@@ -22,7 +22,6 @@
597 urlparse,
598 )
599
600-from django.conf import settings
601 from django.core.exceptions import ValidationError
602 from django.core.urlresolvers import reverse
603 from django.http import HttpRequest
604@@ -49,6 +48,7 @@
605 from maastesting.testcase import MAASTestCase
606 from mock import sentinel
607 from netaddr import IPAddress
608+from provisioningserver.testing.config import ClusterConfigurationFixture
609
610
611 class TestAbsoluteReverse(MAASServerTestCase):
612@@ -231,20 +231,13 @@
613
614 class TestGetLocalClusterUUID(MAASTestCase):
615
616- def test_get_local_cluster_UUID_returns_None_if_no_config_file(self):
617- bogus_file_name = '/tmp/bogus/%s' % factory.make_name('name')
618- self.patch(settings, 'LOCAL_CLUSTER_CONFIG', bogus_file_name)
619- self.assertIsNone(get_local_cluster_UUID())
620-
621- def test_get_local_cluster_UUID_returns_None_if_parsing_fails(self):
622- file_name = self.make_file(contents="wrong content")
623- self.patch(settings, 'LOCAL_CLUSTER_CONFIG', file_name)
624+ def test_get_local_cluster_UUID_returns_None_if_not_set(self):
625+ self.useFixture(ClusterConfigurationFixture())
626 self.assertIsNone(get_local_cluster_UUID())
627
628 def test_get_local_cluster_UUID_returns_cluster_UUID(self):
629 uuid = factory.make_UUID()
630- file_name = self.make_file(contents='CLUSTER_UUID="%s"' % uuid)
631- self.patch(settings, 'LOCAL_CLUSTER_CONFIG', file_name)
632+ self.useFixture(ClusterConfigurationFixture(cluster_uuid=uuid))
633 self.assertEqual(uuid, get_local_cluster_UUID())
634
635
636
637=== modified file 'src/maastesting/protractor/runner.py'
638--- src/maastesting/protractor/runner.py 2015-03-25 15:33:23 +0000
639+++ src/maastesting/protractor/runner.py 2015-07-03 11:18:28 +0000
640@@ -26,15 +26,13 @@
641 from django.core.management import call_command
642 from django.db import DEFAULT_DB_ALIAS
643 from django.test.runner import setup_databases
644-from fixtures import (
645- EnvironmentVariableFixture,
646- Fixture,
647-)
648+from fixtures import Fixture
649 from maastesting.fixtures import (
650 ChromiumWebDriverFixture,
651 DisplayFixture,
652 )
653 from postgresfixture import ClusterFixture
654+from provisioningserver.testing.config import ClusterConfigurationFixture
655 from testtools.monkey import patch
656 from twisted.scripts import twistd
657
658@@ -161,17 +159,12 @@
659 class MAASClusterServiceFixture(Fixture):
660 """Starts and stops the MAAS cluster service."""
661
662- MAAS_URL = "http://0.0.0.0:5253/MAAS/"
663- CLUSTER_UUID = "adfd3977-f251-4f2c-8d61-745dbd690bf2"
664- CONFIG_FILE = "src/maastesting/protractor.yaml"
665-
666 def setUp(self):
667 """Start the clusterd service."""
668 super(MAASClusterServiceFixture, self).setUp()
669- self.useFixture(EnvironmentVariableFixture(
670- "MAAS_URL", self.MAAS_URL))
671- self.useFixture(EnvironmentVariableFixture(
672- "CLUSTER_UUID", self.CLUSTER_UUID))
673+ self.useFixture(ClusterConfigurationFixture(
674+ cluster_uuid="adfd3977-f251-4f2c-8d61-745dbd690bf2",
675+ maas_url="http://0.0.0.0:5253/MAAS/"))
676
677 # Fork the process to have clusterd run in its own process.
678 twistd_pid = os.fork()
679@@ -180,13 +173,7 @@
680 redirect_to_devnull()
681
682 # Add command line options to start twistd.
683- sys.argv[1:] = [
684- "--nodaemon",
685- "--pidfile", "",
686- "maas-clusterd",
687- "--config-file",
688- self.CONFIG_FILE,
689- ]
690+ sys.argv[1:] = ["--nodaemon", "--pidfile", "", "maas-clusterd"]
691
692 # Start twistd.
693 try:
694
695=== modified file 'src/provisioningserver/__main__.py'
696--- src/provisioningserver/__main__.py 2015-06-26 09:48:29 +0000
697+++ src/provisioningserver/__main__.py 2015-07-03 11:18:28 +0000
698@@ -18,7 +18,6 @@
699 import provisioningserver.boot.install_bootloader
700 import provisioningserver.boot.install_grub
701 import provisioningserver.cluster_config_command
702-import provisioningserver.configure_maas_url
703 import provisioningserver.dhcp.writer
704 import provisioningserver.upgrade_cluster
705 from provisioningserver.utils.script import (
706@@ -30,7 +29,6 @@
707 script_commands = {
708 'atomic-write': AtomicWriteScript,
709 'check-for-shared-secret': security.CheckForSharedSecretScript,
710- 'configure-maas-url': provisioningserver.configure_maas_url,
711 'generate-dhcp-config': provisioningserver.dhcp.writer,
712 'install-shared-secret': security.InstallSharedSecretScript,
713 'install-uefi-config': provisioningserver.boot.install_grub,
714
715=== modified file 'src/provisioningserver/boot/install_grub.py'
716--- src/provisioningserver/boot/install_grub.py 2015-05-29 16:47:37 +0000
717+++ src/provisioningserver/boot/install_grub.py 2015-07-03 11:18:28 +0000
718@@ -20,7 +20,7 @@
719 import os.path
720
721 from provisioningserver.boot.install_bootloader import make_destination
722-from provisioningserver.config import Config
723+from provisioningserver.config import ClusterConfiguration
724 from provisioningserver.utils.fs import write_text_file
725
726
727@@ -44,7 +44,7 @@
728 """Install a GRUB2 pre-boot loader config into the TFTP
729 directory structure.
730 """
731- config = Config.load(args.config_file)
732- grubroot = os.path.join(config["tftp"]["resource_root"], 'grub')
733- destination_path = make_destination(grubroot)
734- write_text_file(os.path.join(destination_path, 'grub.cfg'), CONFIG_FILE)
735+ with ClusterConfiguration.open() as config:
736+ destination_path = make_destination(config.grub_root)
737+ destination_file = os.path.join(destination_path, 'grub.cfg')
738+ write_text_file(destination_file, CONFIG_FILE)
739
740=== modified file 'src/provisioningserver/boot/tests/test_boot.py'
741--- src/provisioningserver/boot/tests/test_boot.py 2015-06-11 21:21:22 +0000
742+++ src/provisioningserver/boot/tests/test_boot.py 2015-07-03 11:18:28 +0000
743@@ -118,9 +118,9 @@
744
745 def make_fake_templates_dir(self, method):
746 """Set up a fake templates dir, and return its path."""
747- fake_etc_maas = self.make_dir()
748- self.useFixture(EnvironmentVariableFixture(
749- 'MAAS_CONFIG_DIR', fake_etc_maas))
750+ fake_root = self.make_dir()
751+ fake_etc_maas = os.path.join(fake_root, "etc", "maas")
752+ self.useFixture(EnvironmentVariableFixture('MAAS_ROOT', fake_root))
753 fake_templates = os.path.join(
754 fake_etc_maas, 'templates/%s' % method.template_subdir)
755 os.makedirs(fake_templates)
756
757=== modified file 'src/provisioningserver/boot/tests/test_install_grub.py'
758--- src/provisioningserver/boot/tests/test_install_grub.py 2015-04-09 16:21:27 +0000
759+++ src/provisioningserver/boot/tests/test_install_grub.py 2015-07-03 11:18:28 +0000
760@@ -20,7 +20,7 @@
761 from maastesting.testcase import MAASTestCase
762 import provisioningserver.boot.install_grub
763 from provisioningserver.boot.tftppath import locate_tftp_path
764-from provisioningserver.testing.config import set_tftp_root
765+from provisioningserver.testing.config import ClusterConfigurationFixture
766 from provisioningserver.utils.script import MainScript
767 from testtools.matchers import FileExists
768
769@@ -29,13 +29,12 @@
770
771 def test_integration(self):
772 tftproot = self.make_dir()
773- config_fixture = self.useFixture(set_tftp_root(tftproot))
774+ self.useFixture(ClusterConfigurationFixture(tftp_root=tftproot))
775
776 action = factory.make_name("action")
777 script = MainScript(action)
778 script.register(action, provisioningserver.boot.install_grub)
779- script.execute(
780- ("--config-file", config_fixture.filename, action))
781+ script.execute((action,))
782
783 config_filename = os.path.join('grub', 'grub.cfg')
784 self.assertThat(
785
786=== modified file 'src/provisioningserver/boot/tests/test_powernv.py'
787--- src/provisioningserver/boot/tests/test_powernv.py 2015-05-07 18:14:38 +0000
788+++ src/provisioningserver/boot/tests/test_powernv.py 2015-07-03 11:18:28 +0000
789@@ -19,6 +19,7 @@
790
791 from maastesting.factory import factory
792 from maastesting.testcase import MAASTestCase
793+from mock import sentinel
794 from provisioningserver.boot import (
795 BytesReader,
796 powernv as powernv_module,
797@@ -32,7 +33,7 @@
798 from provisioningserver.boot.tests.test_pxe import parse_pxe_config
799 from provisioningserver.boot.tftppath import compose_image_path
800 from provisioningserver.pserv_services.tftp import TFTPBackend
801-from provisioningserver.testing.config import set_tftp_root
802+from provisioningserver.testing.config import ClusterConfigurationFixture
803 from provisioningserver.tests.test_kernel_opts import make_kernel_parameters
804 from testtools.matchers import (
805 IsInstance,
806@@ -78,7 +79,7 @@
807 def make_tftp_root(self):
808 """Set, and return, a temporary TFTP root directory."""
809 tftproot = self.make_dir()
810- self.useFixture(set_tftp_root(tftproot))
811+ self.useFixture(ClusterConfigurationFixture(tftp_root=tftproot))
812 return tftproot
813
814 def test_compose_config_path_follows_maas_pxe_directory_layout(self):
815@@ -255,7 +256,8 @@
816 data = factory.make_string().encode("ascii")
817 temp_file = self.make_file(name="example", contents=data)
818 temp_dir = os.path.dirname(temp_file)
819- backend = TFTPBackend(temp_dir, "http://nowhere.example.com/")
820+ backend = TFTPBackend(
821+ temp_dir, "http://nowhere.example.com/", sentinel.uuid)
822 method = PowerNVBootMethod()
823 options = {
824 'backend': backend,
825@@ -274,7 +276,8 @@
826 temp_subdir = os.path.join(temp_dir, 'ppc64el')
827 os.mkdir(temp_subdir)
828 factory.make_file(temp_subdir, "example", data)
829- backend = TFTPBackend(temp_dir, "http://nowhere.example.com/")
830+ backend = TFTPBackend(
831+ temp_dir, "http://nowhere.example.com/", sentinel.uuid)
832 method = PowerNVBootMethod()
833 options = {
834 'backend': backend,
835
836=== modified file 'src/provisioningserver/boot/tests/test_pxe.py'
837--- src/provisioningserver/boot/tests/test_pxe.py 2015-05-07 18:14:38 +0000
838+++ src/provisioningserver/boot/tests/test_pxe.py 2015-07-03 11:18:28 +0000
839@@ -34,7 +34,7 @@
840 re_config_file,
841 )
842 from provisioningserver.boot.tftppath import compose_image_path
843-from provisioningserver.testing.config import set_tftp_root
844+from provisioningserver.testing.config import ClusterConfigurationFixture
845 from provisioningserver.tests.test_kernel_opts import make_kernel_parameters
846 from testtools.matchers import (
847 Contains,
848@@ -72,7 +72,7 @@
849 def make_tftp_root(self):
850 """Set, and return, a temporary TFTP root directory."""
851 tftproot = self.make_dir()
852- self.useFixture(set_tftp_root(tftproot))
853+ self.useFixture(ClusterConfigurationFixture(tftp_root=tftproot))
854 return tftproot
855
856 def make_dummy_bootloader_sources(self, destination, loader_names):
857
858=== modified file 'src/provisioningserver/boot/tests/test_tftppath.py'
859--- src/provisioningserver/boot/tests/test_tftppath.py 2015-05-29 16:47:37 +0000
860+++ src/provisioningserver/boot/tests/test_tftppath.py 2015-07-03 11:18:28 +0000
861@@ -18,10 +18,8 @@
862 import os.path
863
864 from maastesting.factory import factory
865-from maastesting.matchers import MockCalledOnceWith
866 from maastesting.testcase import MAASTestCase
867 from mock import Mock
868-from provisioningserver import config
869 from provisioningserver.boot import tftppath
870 from provisioningserver.boot.tftppath import (
871 compose_image_path,
872@@ -48,7 +46,7 @@
873 make_boot_image_storage_params,
874 make_image,
875 )
876-from provisioningserver.testing.config import set_tftp_root
877+from provisioningserver.testing.config import ClusterConfigurationFixture
878 from provisioningserver.testing.os import make_osystem
879 from testtools.matchers import (
880 Not,
881@@ -62,7 +60,7 @@
882 def setUp(self):
883 super(TestTFTPPath, self).setUp()
884 self.tftproot = self.make_dir()
885- self.useFixture(set_tftp_root(self.tftproot))
886+ self.useFixture(ClusterConfigurationFixture(tftp_root=self.tftproot))
887
888 def make_image_dir(self, image_params, tftproot):
889 """Fake a boot image matching `image_params` under `tftproot`."""
890@@ -101,20 +99,12 @@
891 os.path.join(self.tftproot, "maas.meta"))
892 self.assertIsNone(observed)
893
894- def test_maas_meta_last_modified_defaults_tftproot(self):
895- path = factory.make_file(self.tftproot, name="maas.meta")
896- maas_meta_file_path = self.patch(tftppath, 'maas_meta_file_path')
897- maas_meta_file_path.return_value = path
898- maas_meta_last_modified()
899- expected_path = os.path.join(
900- config.BOOT_RESOURCES_STORAGE, 'current')
901- self.assertThat(maas_meta_file_path, MockCalledOnceWith(expected_path))
902-
903 def test_maas_meta_last_modified_reraises_non_ENOENT(self):
904+ path = factory.make_file(self.tftproot, name="maas.meta")
905 oserror = OSError()
906 oserror.errno = errno.E2BIG
907 self.patch(os.path, 'getmtime').side_effect = oserror
908- self.assertRaises(OSError, maas_meta_last_modified)
909+ self.assertRaises(OSError, maas_meta_last_modified, path)
910
911 def test_compose_image_path_follows_storage_directory_layout(self):
912 osystem = factory.make_name('osystem')
913
914=== modified file 'src/provisioningserver/boot/tests/test_windows.py'
915--- src/provisioningserver/boot/tests/test_windows.py 2015-05-29 16:47:37 +0000
916+++ src/provisioningserver/boot/tests/test_windows.py 2015-07-03 11:18:28 +0000
917@@ -41,9 +41,9 @@
918 Bcd,
919 WindowsPXEBootMethod,
920 )
921-from provisioningserver.config import Config
922 from provisioningserver.rpc.exceptions import NoSuchNode
923 from provisioningserver.rpc.region import RequestNodeInfoByMACAddress
924+from provisioningserver.testing.config import ClusterConfigurationFixture
925 from provisioningserver.tests.test_kernel_opts import make_kernel_parameters
926 from testtools.deferredruntest import extract_result
927 from testtools.matchers import Is
928@@ -193,7 +193,6 @@
929 run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
930
931 def setUp(self):
932- self.patch(Config, 'load_from_cache')
933 self.patch(windows_module, 'get_hivex_module')
934 super(TestWindowsPXEBootMethod, self).setUp()
935
936@@ -372,13 +371,8 @@
937 BootMethodError, method.compose_bcd, kernel_params, local_host)
938
939 def test_get_resouce_path(self):
940- fake_tftproot = factory.make_name('tftproot')
941- mock_config = self.patch(windows_module, 'Config')
942- mock_config.load_from_cache.return_value = {
943- 'tftp': {
944- 'resource_root': fake_tftproot,
945- },
946- }
947+ fake_tftproot = self.make_dir()
948+ self.useFixture(ClusterConfigurationFixture(tftp_root=fake_tftproot))
949 method = WindowsPXEBootMethod()
950 fake_path = factory.make_name('path')
951 fake_kernelparams = make_kernel_parameters()
952
953=== modified file 'src/provisioningserver/boot/tftppath.py'
954--- src/provisioningserver/boot/tftppath.py 2015-05-29 16:47:37 +0000
955+++ src/provisioningserver/boot/tftppath.py 2015-07-03 11:18:28 +0000
956@@ -23,7 +23,6 @@
957 from itertools import chain
958 import os.path
959
960-from provisioningserver import config
961 from provisioningserver.drivers.osystem import (
962 BOOT_IMAGE_PURPOSE,
963 OperatingSystemRegistry,
964@@ -199,17 +198,14 @@
965 return os.path.join(tftproot, 'maas.meta')
966
967
968-def maas_meta_last_modified(tftproot=None):
969+def maas_meta_last_modified(tftproot):
970 """Return time of last modification of maas.meta.
971
972 The time is the same as returned from getmtime() (seconds since epoch),
973 or None if the file doesn't exist.
974
975- :param tftproot: Optional tftp root dir, defaults to
976- provisioningserver.config.BOOT_RESOURCES_STORAGE
977+ :param tftproot: The TFTP root path.
978 """
979- if tftproot is None:
980- tftproot = os.path.join(config.BOOT_RESOURCES_STORAGE, 'current')
981 meta_file = maas_meta_file_path(tftproot)
982 try:
983 return os.path.getmtime(meta_file)
984
985=== modified file 'src/provisioningserver/boot/windows.py'
986--- src/provisioningserver/boot/windows.py 2015-05-29 16:47:37 +0000
987+++ src/provisioningserver/boot/windows.py 2015-07-03 11:18:28 +0000
988@@ -28,7 +28,7 @@
989 BytesReader,
990 get_remote_mac,
991 )
992-from provisioningserver.config import Config
993+from provisioningserver.config import ClusterConfiguration
994 from provisioningserver.logger.log import get_maas_logger
995 from provisioningserver.rpc import getRegionClient
996 from provisioningserver.rpc.exceptions import NoSuchNode
997@@ -300,7 +300,8 @@
998
999 def get_resource_path(self, kernel_params, path):
1000 """Gets the resource path from the kernel param."""
1001- resources = Config.load_from_cache()['tftp']['resource_root']
1002+ with ClusterConfiguration.open() as config:
1003+ resources = config.tftp_root
1004 return os.path.join(
1005 resources, 'windows', kernel_params.arch, kernel_params.subarch,
1006 kernel_params.release, kernel_params.label, path)
1007
1008=== removed file 'src/provisioningserver/cluster_config.py'
1009--- src/provisioningserver/cluster_config.py 2015-04-09 16:21:27 +0000
1010+++ src/provisioningserver/cluster_config.py 1970-01-01 00:00:00 +0000
1011@@ -1,88 +0,0 @@
1012-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
1013-# GNU Affero General Public License version 3 (see the file LICENSE).
1014-
1015-"""Accessors for cluster configuration as set in `maas_cluster.conf`."""
1016-
1017-from __future__ import (
1018- absolute_import,
1019- print_function,
1020- unicode_literals,
1021-)
1022-
1023-str = None
1024-
1025-__metaclass__ = type
1026-__all__ = [
1027- 'get_cluster_uuid',
1028- 'get_maas_url',
1029- "get_cluster_variable",
1030- "get_config_cluster_variable",
1031- "set_config_cluster_variable", ]
1032-
1033-from os import environ
1034-
1035-from provisioningserver.config import ClusterConfiguration
1036-
1037-# List of configuration keys
1038-
1039-
1040-class CLUSTER_CONFIG:
1041- DB_cluster_uuid = 'cluster_uuid'
1042- DB_maas_url = 'maas_url'
1043- DB_tftp_resource_root = "tftp_root"
1044- DB_boot_resources_storage = "tftp_root"
1045- DB_tftpport = 'tftp_port'
1046-
1047-
1048-# New get function for ClusterConfiguration backend
1049-def get_config_cluster_variable(var):
1050- """Obtain the given environment variable from clusterd config"""
1051- with ClusterConfiguration.open() as config:
1052- return getattr(config, var)
1053-
1054-
1055-# Set function for ClusterConfiguration backend
1056-def set_config_cluster_variable(var, value):
1057- """Set the given environment variable in clusterd config"""
1058- with ClusterConfiguration.open() as config:
1059- setattr(config, var, value)
1060-
1061-
1062-def get_tftp_generator():
1063- """Return the `tftp_generator` setting, which is
1064- <maas url>/api/1.0/pxeconfig/
1065- """
1066- return '/'.join(get_cluster_variable(CLUSTER_CONFIG.DB_maas_url),
1067- 'api', '1.0', 'pxeconfig')
1068-
1069-
1070-# Old get function for config file (via env variables) backend,
1071-# to be removed in follow-up branch. This branch is a prerequisite branch for
1072-# a follow-up branch, where this function will be removed. Both
1073-# the new and old versions of this function have been included
1074-# in this intermediate branch at the request of the package
1075-# maintainers.
1076-def get_cluster_variable(var):
1077- """Obtain the given environment variable from maas_cluster.conf.
1078-
1079- If the variable is not set, it probably means that whatever script
1080- started the current process neglected to run maas_cluster.conf.
1081- In that case, fail helpfully but utterly.
1082- """
1083- value = environ.get(var)
1084- if value is None:
1085- raise AssertionError(
1086- "%s is not set. This probably means that the script which "
1087- "started this program failed to source maas_cluster.conf."
1088- % var)
1089- return value
1090-
1091-
1092-def get_cluster_uuid():
1093- """Return the `CLUSTER_UUID` setting."""
1094- return get_cluster_variable('CLUSTER_UUID')
1095-
1096-
1097-def get_maas_url():
1098- """Return the `MAAS_URL` setting."""
1099- return get_cluster_variable('MAAS_URL')
1100
1101=== modified file 'src/provisioningserver/cluster_config_command.py'
1102--- src/provisioningserver/cluster_config_command.py 2015-04-02 19:30:23 +0000
1103+++ src/provisioningserver/cluster_config_command.py 2015-07-03 11:18:28 +0000
1104@@ -21,12 +21,10 @@
1105
1106 from uuid import uuid4
1107
1108-from provisioningserver.cluster_config import (
1109- CLUSTER_CONFIG,
1110- get_config_cluster_variable,
1111- set_config_cluster_variable,
1112+from provisioningserver.config import (
1113+ ClusterConfiguration,
1114+ UUID_NOT_SET,
1115 )
1116-from provisioningserver.config import UUID_NOT_SET
1117
1118
1119 def update_maas_cluster_conf(
1120@@ -44,28 +42,20 @@
1121 and init be passed at the same time, as these are mutually exclusive
1122 parameters.
1123 """
1124-
1125- if url is not None:
1126- set_config_cluster_variable(CLUSTER_CONFIG.DB_maas_url, url)
1127-
1128- if uuid is not None:
1129- set_config_cluster_variable(
1130- CLUSTER_CONFIG.DB_cluster_uuid, uuid)
1131-
1132- if init:
1133- cur_uuid = get_config_cluster_variable(CLUSTER_CONFIG.DB_cluster_uuid)
1134- if cur_uuid == UUID_NOT_SET:
1135- set_config_cluster_variable(
1136- CLUSTER_CONFIG.DB_cluster_uuid,
1137- unicode(uuid4()))
1138-
1139- if tftp_port is not None:
1140- set_config_cluster_variable(CLUSTER_CONFIG.DB_tftpport, tftp_port)
1141-
1142- if tftp_root is not None:
1143- set_config_cluster_variable(
1144- CLUSTER_CONFIG.DB_tftp_resource_root,
1145- tftp_root)
1146+ with ClusterConfiguration.open() as config:
1147+ if url is not None:
1148+ config.maas_url = url
1149+ if uuid is not None:
1150+ config.cluster_uuid = uuid
1151+ if init:
1152+ cur_uuid = config.cluster_uuid
1153+ if cur_uuid == UUID_NOT_SET:
1154+ config.cluster_uuid = unicode(uuid4())
1155+ if tftp_port is not None:
1156+ config.tftp_port = tftp_port
1157+ if tftp_root is not None:
1158+ config.tftp_root = tftp_root
1159+
1160
1161 all_arguments = (
1162 '--region-url',
1163@@ -101,11 +91,7 @@
1164
1165
1166 def run(args):
1167- """Update MAAS_URL setting in configuration files.
1168-
1169- For use by the MAAS packaging scripts. Updates configuration data
1170- store to reflect provided settings.
1171- """
1172+ """Update configuration settings."""
1173 params = vars(args).copy()
1174 url = params.pop('region_url', None)
1175 uuid = params.pop('uuid', None)
1176
1177=== modified file 'src/provisioningserver/config.py'
1178--- src/provisioningserver/config.py 2015-06-24 13:27:05 +0000
1179+++ src/provisioningserver/config.py 2015-07-03 11:18:28 +0000
1180@@ -1,45 +1,105 @@
1181 # Copyright 2012-2015 Canonical Ltd. This software is licensed under the
1182 # GNU Affero General Public License version 3 (see the file LICENSE).
1183
1184-"""MAAS Provisioning Configuration.
1185-
1186-Configuration for most elements of a Cluster Controller can be obtained
1187-through this module's `Config` validator class. At the time of writing the
1188-exceptions are the `CLUSTER_UUID` and `MAAS_URL` environment variables (see
1189-`provisioningserver.cluster_config`).
1190-
1191-It's pretty simple. Typical usage is::
1192-
1193- >>> config = Config.load_from_cache()
1194+"""Configuration for the MAAS cluster.
1195+
1196+This module also contains the common library code that's used for
1197+configuration in both the region and the cluster.
1198+
1199+There are two styles of configuration object, one older and deprecated, and
1200+one new.
1201+
1202+
1203+The Old Way
1204+-----------
1205+
1206+Configuration can be obtained through subclassing this module's `ConfigBase`
1207+validator class. It's pretty simple. Typical usage is::
1208+
1209+ >>> config = MyConfig.load_from_cache()
1210 {...}
1211
1212-This reads in a configuration file from `Config.DEFAULT_FILENAME` (see a note
1213+This reads in a configuration file from `MyConfig.DEFAULT_FILENAME` (see a note
1214 about that later). The configuration file is parsed as YAML, and a plain `dict`
1215 is returned with configuration nested within it. The configuration is validated
1216 at load time using `formencode`. The policy for validation is laid out in this
1217 module; see the various `formencode.Schema` subclasses.
1218
1219-All configuration is optional, and a sensible default is provided in every
1220-instance. When adding or changing settings bear this policy in mind, and also
1221-that the defaults should be geared towards a system in production, and not a
1222-development environment. The defaults can be obtained by calling
1223-`Config.get_defaults()`.
1224-
1225-An alternative to `Config.load_from_cache()` is `Config.load()`, which loads
1226-and validates a configuration file while bypassing the cache. See `Config` for
1227-other useful functions.
1228-
1229-`Config.DEFAULT_FILENAME` is a class property, so does not need to be
1230-referenced via an instance of `Config`. It refers to the
1231-``MAAS_PROVISIONING_SETTINGS`` environment variable in the first instance, but
1232-has a sensible default too. You can write to this property and it will update
1233-the environment so that child processes will also use the same configuration
1234-filename. To revert to the default - i.e. erase the environment variable - you
1235-can `del Config.DEFAULT_FILENAME`.
1236-
1237-When testing, see `provisioningserver.testing.config.ConfigFixture` to
1238+Configuration should be optional, and a sensible default should be provided in
1239+every instance. The defaults can be obtained from `MyConfig.get_defaults()`.
1240+
1241+An alternative to `MyConfig.load_from_cache()` is `MyConfig.load()`, which
1242+loads and validates a configuration file while bypassing the cache. See
1243+`ConfigBase` for other useful functions.
1244+
1245+`MyConfig.DEFAULT_FILENAME` is a class property, so does not need to be
1246+referenced via an instance of `MyConfig`. It refers to an environment variable
1247+named by `MyConfig.envvar` in the first instance, but should have a sensible
1248+default too. You can write to this property and it will update the environment
1249+so that child processes will also use the same configuration filename. To
1250+revert to the default - i.e. erase the environment variable - you can `del
1251+MyConfig.DEFAULT_FILENAME`.
1252+
1253+When testing, see `provisioningserver.testing.config.ConfigFixtureBase` to
1254 temporarily use a different configuration.
1255
1256+
1257+The New Way
1258+-----------
1259+
1260+There are two subclasses of this module's `Configuration` class, one for the
1261+region (`RegionConfiguration`) and for the cluster (`ClusterConfiguration`).
1262+Each defines a set of attributes which are the configuration variables:
1263+
1264+* If an attribute is declared as a `ConfigurationOption` then it's a
1265+ read-write configuration option, and should have a sensible default if
1266+ possible.
1267+
1268+* If an attribute is declared as a standard Python `property` then it's a
1269+ read-only configuration option.
1270+
1271+A metaclass is also defined, which must inherit from `ConfigurationMeta`, to
1272+define a few other important options:
1273+
1274+* ``default`` is the default filename for the configuration database.
1275+
1276+* ``envvar`` is the name of an environment variable that, if defined, provides
1277+ the filename for the configuration database. This is used in preference to
1278+ ``default``.
1279+
1280+* ``backend`` is a factory that provides the storage mechanism. Currently you
1281+ can choose from `ConfigurationFile` or `ConfigurationDatabase`. The latter
1282+ is strongly recommended in preference to the former.
1283+
1284+An example::
1285+
1286+ class MyConfiguration(Configuration):
1287+
1288+ class __metaclass__(ConfigurationMeta):
1289+ envvar = "CONFIG_FILE"
1290+ default = "/etc/myapp.conf"
1291+ backend = ConfigurationDatabase
1292+
1293+ images_dir = ConfigurationOption(
1294+ "images_dir", "The directory in which to store images.",
1295+ Directory(if_missing="/var/lib/myapp/images"))
1296+
1297+ @property
1298+ def png_dir(self):
1299+ "The directory in which to store PNGs."
1300+ return os.path.join(self.images_dir, "png")
1301+
1302+ @property
1303+ def gif_dir(self):
1304+ "The directory in which to store GIFs."
1305+ return os.path.join(self.images_dir, "gif")
1306+
1307+It can be used like so::
1308+
1309+ with MyConfiguration.open() as config:
1310+ config.images_dir = "/var/www/example.com/images"
1311+ print(config.png_dir, config.gif_dir)
1312+
1313 """
1314
1315 from __future__ import (
1316@@ -52,11 +112,12 @@
1317
1318 __metaclass__ = type
1319 __all__ = [
1320- "BOOT_RESOURCES_STORAGE",
1321 "BootSources",
1322- "Config",
1323+ "ClusterConfiguration",
1324 "ConfigBase",
1325 "ConfigMeta",
1326+ "is_dev_environment",
1327+ "UUID_NOT_SET",
1328 ]
1329
1330 from contextlib import (
1331@@ -64,7 +125,6 @@
1332 contextmanager,
1333 )
1334 from copy import deepcopy
1335-from getpass import getuser
1336 import json
1337 import os
1338 from os import environ
1339@@ -82,11 +142,9 @@
1340 from formencode.api import NoDefault
1341 from formencode.declarative import DeclarativeMeta
1342 from formencode.validators import (
1343- Int,
1344 Invalid,
1345 is_validator,
1346 Number,
1347- RequireIfPresent,
1348 Set,
1349 String,
1350 UnicodeString,
1351@@ -100,11 +158,6 @@
1352 )
1353 import yaml
1354
1355-# Path to the directory on the cluster controller where boot resources are
1356-# stored. This used to be configurable in bootresources.yaml, and may become
1357-# configurable again in the future.
1358-BOOT_RESOURCES_STORAGE = '/var/lib/maas/boot-resources/'
1359-
1360 # Default result for cluster UUID if not set
1361 UUID_NOT_SET = '** UUID NOT SET **'
1362
1363@@ -170,93 +223,6 @@
1364 ''', re.I | re.VERBOSE)
1365
1366
1367-class ConfigOops(Schema):
1368- """Configuration validator for OOPS options.
1369-
1370- Deprecated: MAAS no longer records OOPS reports. This remains here to
1371- avoid validation failures when using old versions of the cluster's
1372- configuration file.
1373- """
1374-
1375- if_key_missing = None
1376-
1377- directory = String(if_missing=b"")
1378- reporter = String(if_missing=b"")
1379-
1380- chained_validators = (
1381- RequireIfPresent("reporter", present="directory"),
1382- )
1383-
1384-
1385-class ConfigBroker(Schema):
1386- """Configuration validator for message broker options.
1387-
1388- Deprecated: MAAS no longer uses a message broker. This remains here to
1389- avoid validation failures when using old versions of the cluster's
1390- configuration file.
1391- """
1392-
1393- if_key_missing = None
1394-
1395- host = String(if_missing=b"localhost")
1396- port = Int(min=1, max=65535, if_missing=5673)
1397- username = String(if_missing=getuser())
1398- password = String(if_missing=b"test")
1399- vhost = String(if_missing="/")
1400-
1401-
1402-class ConfigTFTP(Schema):
1403- """Configuration validator for the TFTP service."""
1404-
1405- if_key_missing = None
1406-
1407- # Obsolete: old TFTP root directory. This is retained for the purpose of
1408- # deriving new, Simplestreams-based import configuration from previously
1409- # imported boot images.
1410- # The last time this is needed is for upgrading an older cluster
1411- # controller to the Ubuntu 14.04 version of MAAS. After installation of
1412- # the 14.04 version, this setting is never used.
1413- root = String(if_missing="/var/lib/maas/tftp")
1414-
1415- # TFTP root directory, managed by the Simplestreams-based import script.
1416- # The import script maintains "current" as a symlink pointing to the most
1417- # recent images.
1418- # XXX jtv 2014-05-22: Redundant with BOOT_RESOURCES_STORAGE.
1419- resource_root = String(
1420- if_missing=os.path.join(BOOT_RESOURCES_STORAGE, 'current/'))
1421-
1422- port = Int(min=1, max=65535, if_missing=69)
1423- generator = String(if_missing=b"http://localhost/MAAS/api/1.0/pxeconfig/")
1424-
1425-
1426-class ConfigLegacyEphemeral(Schema):
1427- """Legacy `ephemeral` section in `pserv.yaml` prior to MAAS 1.5.
1428-
1429- This has been superseded by boot sources.
1430- It is still accepted in `pserv.yaml`, but not used.
1431- """
1432- if_key_missing = None
1433- images_directory = String(if_missing=None)
1434- releases = Set(if_missing=None)
1435-
1436-
1437-class ConfigLegacyBoot(Schema):
1438- """Legacy `boot` section in `pserv.yaml` prior to MAAS 1.5.
1439-
1440- This has been superseded by boot sources.
1441- It is still accepted in `pserv.yaml`, but not used.
1442- """
1443- if_key_missing = None
1444- architectures = Set(if_missing=None)
1445- ephemeral = ConfigLegacyEphemeral
1446-
1447-
1448-class ConfigRPC(Schema):
1449- """Configuration validator for the RPC service."""
1450-
1451- if_key_missing = None
1452-
1453-
1454 class BootSourceSelection(Schema):
1455 """Configuration validator for boot source selection configuration."""
1456
1457@@ -403,23 +369,6 @@
1458 "`cls.envvar` in the environment."))
1459
1460
1461-class Config(ConfigBase, Schema):
1462- """Configuration for the provisioning server."""
1463-
1464- class __metaclass__(ConfigMeta):
1465- envvar = "MAAS_PROVISIONING_SETTINGS"
1466- default = "pserv.yaml"
1467-
1468- if_key_missing = None
1469-
1470- logfile = String(if_empty=b"pserv.log", if_missing=b"pserv.log")
1471- oops = ConfigOops
1472- broker = ConfigBroker
1473- tftp = ConfigTFTP
1474- rpc = ConfigRPC
1475- boot = ConfigLegacyBoot
1476-
1477-
1478 class BootSources(ConfigBase, ForEach):
1479 """Configuration for boot sources."""
1480
1481@@ -760,20 +709,42 @@
1482 default = "/etc/maas/clusterd.conf"
1483 backend = ConfigurationFile
1484
1485- # MAAS URL options
1486 maas_url = ConfigurationOption(
1487- "maas_url", "The HTTP URL for the MAAS region.",
1488- ExtendedURL(require_tld=False,
1489- if_missing="http://localhost:5240/MAAS"))
1490+ "maas_url", "The HTTP URL for the MAAS region.", ExtendedURL(
1491+ require_tld=False, if_missing="http://localhost:5240/MAAS"))
1492+
1493 # TFTP options.
1494 tftp_port = ConfigurationOption(
1495 "tftp_port", "The UDP port on which to listen for TFTP requests.",
1496 Number(min=0, max=(2 ** 16) - 1, if_missing=69))
1497 tftp_root = ConfigurationOption(
1498 "tftp_root", "The root directory for TFTP resources.",
1499- Directory(if_missing="/var/lib/maas/boot-resources/current/"))
1500+ Directory(if_missing=get_tentative_path(
1501+ "/var/lib/maas/boot-resources/current")))
1502+
1503+ @property
1504+ def tftp_generator_url(self):
1505+ """The URL at which to obtain the TFTP options for a node."""
1506+ return "%s/api/1.0/pxeconfig/" % self.maas_url.rstrip("/")
1507+
1508+ # GRUB options.
1509+
1510+ @property
1511+ def grub_root(self):
1512+ "The root directory for GRUB resources."
1513+ return os.path.join(self.tftp_root, "grub")
1514
1515 # Cluster UUID Option
1516 cluster_uuid = ConfigurationOption(
1517 "cluster_uuid", "The UUID for this cluster controller",
1518- UUID(if_missing=unicode(UUID_NOT_SET)))
1519+ UUID(if_missing=UUID_NOT_SET))
1520+
1521+
1522+def is_dev_environment():
1523+ """Is this the development environment, or production?"""
1524+ try:
1525+ from maastesting import root # noqa
1526+ except:
1527+ return False
1528+ else:
1529+ return True
1530
1531=== removed file 'src/provisioningserver/configure_maas_url.py'
1532--- src/provisioningserver/configure_maas_url.py 2015-05-29 16:47:37 +0000
1533+++ src/provisioningserver/configure_maas_url.py 1970-01-01 00:00:00 +0000
1534@@ -1,123 +0,0 @@
1535-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
1536-# GNU Affero General Public License version 3 (see the file LICENSE).
1537-
1538-"""Management command: update `MAAS_URL`.
1539-
1540-The MAAS cluster controller packaging calls this in order to set a new
1541-"MAAS URL" (the URL where nodes and cluster controllers can reach the
1542-region controller) in the cluster controller's configuration files.
1543-"""
1544-
1545-from __future__ import (
1546- absolute_import,
1547- print_function,
1548- unicode_literals,
1549- )
1550-
1551-str = None
1552-
1553-__metaclass__ = type
1554-__all__ = [
1555- 'add_arguments',
1556- 'run',
1557- ]
1558-
1559-from functools import partial
1560-import re
1561-from urlparse import urlparse
1562-
1563-from provisioningserver.utils.fs import (
1564- atomic_write,
1565- read_text_file,
1566-)
1567-from provisioningserver.utils.url import compose_URL
1568-
1569-
1570-MAAS_CLUSTER_CONF = '/etc/maas/maas_cluster.conf'
1571-
1572-PSERV_YAML = '/etc/maas/pserv.yaml'
1573-
1574-
1575-def rewrite_config_file(path, line_filter, mode=0600):
1576- """Rewrite config file at `path` on a line-by-line basis.
1577-
1578- Reads the file at `path`, runs its lines through `line_filter`, and
1579- writes the result back to `path`.
1580-
1581- Newlines may not be exactly as they were. A trailing newline is ensured.
1582-
1583- :param path: Path to the config file to be rewritten.
1584- :param line_filter: A callable which accepts a line of input text (without
1585- trailing newline), and returns the corresponding line of output text
1586- (also without trailing newline).
1587- :param mode: File access permissions for the newly written file.
1588- """
1589- input_lines = read_text_file(path).splitlines()
1590- output_lines = [line_filter(line) for line in input_lines]
1591- result = '%s\n' % '\n'.join(output_lines)
1592- atomic_write(result, path, mode=mode)
1593-
1594-
1595-def update_maas_cluster_conf(url):
1596- """Update `MAAS_URL` in `/etc/maas/maas_cluster.conf`.
1597-
1598- This file contains a shell-style assignment of the `MAAS_URL`
1599- variable. Its assigned value will be changed to `url`.
1600- """
1601- substitute_line = lambda line: (
1602- 'MAAS_URL="%s"' % url
1603- if re.match('\s*MAAS_URL=', line)
1604- else line)
1605- rewrite_config_file(MAAS_CLUSTER_CONF, substitute_line, mode=0640)
1606-
1607-
1608-def extract_host(url):
1609- """Return just the host part of `url`."""
1610- return urlparse(url).hostname
1611-
1612-
1613-def substitute_pserv_yaml_line(new_host, line):
1614- match = re.match('(\s*generator:)\s+(\S*)(.*)$', line)
1615- if match is None:
1616- # Not the generator line. Keep as-is.
1617- return line
1618- [head, input_url, tail] = match.groups()
1619- return "%s %s%s" % (head, compose_URL(input_url, new_host), tail)
1620-
1621-
1622-def update_pserv_yaml(host):
1623- """Update `generator` in `/etc/maas/pserv.yaml`.
1624-
1625- This file contains a YAML line defining a `generator` URL. The line must
1626- look something like::
1627-
1628- generator: http://10.9.8.7/MAAS/api/1.0/pxeconfig/
1629-
1630- The host part of the URL (in this example, `10.9.8.7`) will be replaced
1631- with the new `host`. If `host` is an IPv6 address, this function will
1632- ensure that it is surrounded by square brackets.
1633- """
1634- substitute_line = partial(substitute_pserv_yaml_line, host)
1635- rewrite_config_file(PSERV_YAML, substitute_line, mode=0644)
1636-
1637-
1638-def add_arguments(parser):
1639- """Add this command's options to the `ArgumentParser`.
1640-
1641- Specified by the `ActionScript` interface.
1642- """
1643- parser.add_argument(
1644- 'maas_url', metavar='URL',
1645- help=(
1646- "URL where nodes and cluster controllers can reach the MAAS "
1647- "region controller."))
1648-
1649-
1650-def run(args):
1651- """Update MAAS_URL setting in configuration files.
1652-
1653- For use by the MAAS packaging scripts. Updates configuration files
1654- to reflect a new MAAS_URL setting.
1655- """
1656- update_maas_cluster_conf(args.maas_url)
1657- update_pserv_yaml(extract_host(args.maas_url))
1658
1659=== modified file 'src/provisioningserver/dhcp/tests/test_config.py'
1660--- src/provisioningserver/dhcp/tests/test_config.py 2015-05-07 18:14:38 +0000
1661+++ src/provisioningserver/dhcp/tests/test_config.py 2015-07-03 11:18:28 +0000
1662@@ -74,9 +74,9 @@
1663 """
1664 if name is None:
1665 name = 'dhcpd.conf.template'
1666- fake_etc_maas = self.make_dir()
1667- self.useFixture(EnvironmentVariableFixture(
1668- 'MAAS_CONFIG_DIR', fake_etc_maas))
1669+ fake_root = self.make_dir()
1670+ fake_etc_maas = path.join(fake_root, "etc", "maas")
1671+ self.useFixture(EnvironmentVariableFixture('MAAS_ROOT', fake_root))
1672 template_dir = path.join(fake_etc_maas, 'templates', 'dhcp')
1673 makedirs(template_dir)
1674 template = factory.make_file(
1675
1676=== modified file 'src/provisioningserver/dhcp/tests/test_detect.py'
1677--- src/provisioningserver/dhcp/tests/test_detect.py 2015-05-07 18:14:38 +0000
1678+++ src/provisioningserver/dhcp/tests/test_detect.py 2015-07-03 11:18:28 +0000
1679@@ -23,10 +23,7 @@
1680
1681 from apiclient.maas_client import MAASClient
1682 from apiclient.testing.credentials import make_api_credentials
1683-from fixtures import (
1684- EnvironmentVariable,
1685- FakeLogger,
1686-)
1687+from fixtures import FakeLogger
1688 from maastesting.factory import factory
1689 from maastesting.matchers import MockCalledOnceWith
1690 from maastesting.testcase import MAASTestCase
1691@@ -48,6 +45,7 @@
1692 update_region_controller,
1693 )
1694 import provisioningserver.dhcp.detect as detect_module
1695+from provisioningserver.testing.config import ClusterConfigurationFixture
1696 from provisioningserver.testing.testcase import PservTestCase
1697
1698
1699@@ -343,7 +341,7 @@
1700 factory.make_string(),
1701 )
1702 api_credentials = make_api_credentials()
1703- self.useFixture(EnvironmentVariable("MAAS_URL", maas_url))
1704+ self.useFixture(ClusterConfigurationFixture(maas_url=maas_url))
1705 self.knowledge = dict(
1706 nodegroup_uuid=uuid,
1707 api_credentials=api_credentials,
1708
1709=== modified file 'src/provisioningserver/diskless.py'
1710--- src/provisioningserver/diskless.py 2015-05-29 16:47:37 +0000
1711+++ src/provisioningserver/diskless.py 2015-07-03 11:18:28 +0000
1712@@ -20,12 +20,13 @@
1713 import os
1714 from textwrap import dedent
1715
1716-from provisioningserver import config
1717+from provisioningserver.config import ClusterConfiguration
1718 from provisioningserver.drivers.diskless import DisklessDriverRegistry
1719 from provisioningserver.drivers.osystem import (
1720 BOOT_IMAGE_PURPOSE,
1721 OperatingSystemRegistry,
1722 )
1723+from provisioningserver.import_images.boot_resources import two_dir_levels_up
1724 from provisioningserver.logger import get_maas_logger
1725 from provisioningserver.utils.fs import (
1726 atomic_symlink,
1727@@ -47,8 +48,10 @@
1728 This is the location that all diskless links exist. It holds all of the
1729 currently in use disk for diskless booting.
1730 """
1731- return os.path.join(
1732- config.BOOT_RESOURCES_STORAGE, 'diskless', 'store')
1733+ with ClusterConfiguration.open() as config:
1734+ return os.path.join(
1735+ two_dir_levels_up(config.tftp_root),
1736+ 'diskless', 'store')
1737
1738
1739 def compose_diskless_link_path(system_id):
1740@@ -99,8 +102,10 @@
1741
1742 def get_diskless_tgt_path():
1743 """Return path to maas-diskless.tgt."""
1744- return os.path.join(
1745- config.BOOT_RESOURCES_STORAGE, 'diskless', 'maas-diskless.tgt')
1746+ with ClusterConfiguration.open() as config:
1747+ return os.path.join(
1748+ two_dir_levels_up(config.tftp_root),
1749+ 'diskless', 'maas-diskless.tgt')
1750
1751
1752 def tgt_entry(system_id, image):
1753@@ -153,7 +158,7 @@
1754 symlinks in the diskless store. Reloads the tgt config."""
1755 tgt_path = get_diskless_tgt_path()
1756 tgt_config = compose_diskless_tgt_config()
1757- atomic_write(tgt_config, tgt_path, mode=0644)
1758+ atomic_write(tgt_config, tgt_path, mode=0o644)
1759 reload_diskless_tgt()
1760
1761
1762@@ -183,9 +188,10 @@
1763 raise DisklessError(
1764 "OS doesn't support diskless booting: %s" % osystem_name)
1765 root_path, _ = osystem.get_xinstall_parameters()
1766- return os.path.join(
1767- config.BOOT_RESOURCES_STORAGE, 'current',
1768- osystem_name, arch, subarch, release, label, root_path)
1769+ with ClusterConfiguration.open() as config:
1770+ return os.path.join(
1771+ config.tftp_root, osystem_name, arch, subarch, release, label,
1772+ root_path)
1773
1774
1775 def create_diskless_disk(driver, driver_options, system_id,
1776
1777=== modified file 'src/provisioningserver/drivers/osystem/custom.py'
1778--- src/provisioningserver/drivers/osystem/custom.py 2015-05-29 16:47:37 +0000
1779+++ src/provisioningserver/drivers/osystem/custom.py 2015-07-03 11:18:28 +0000
1780@@ -18,7 +18,7 @@
1781
1782 import os
1783
1784-from provisioningserver.config import BOOT_RESOURCES_STORAGE
1785+from provisioningserver.config import ClusterConfiguration
1786 from provisioningserver.drivers.osystem import (
1787 BOOT_IMAGE_PURPOSE,
1788 OperatingSystem,
1789@@ -55,10 +55,11 @@
1790
1791 def get_xinstall_parameters(self, arch, subarch, release, label):
1792 """Returns the xinstall image name and type for given image."""
1793- path = os.path.join(
1794- BOOT_RESOURCES_STORAGE, 'current', 'custom',
1795- arch, subarch, release, label)
1796- if os.path.exists(os.path.join(path, 'root-dd')):
1797+ with ClusterConfiguration.open() as config:
1798+ dd_path = os.path.join(
1799+ config.tftp_root, 'custom', arch,
1800+ subarch, release, label, 'root-dd')
1801+ if os.path.exists(dd_path):
1802 return "root-dd", "dd-tgz"
1803 else:
1804 return "root-tgz", "tgz"
1805
1806=== modified file 'src/provisioningserver/drivers/osystem/tests/test_custom.py'
1807--- src/provisioningserver/drivers/osystem/tests/test_custom.py 2015-05-29 16:47:37 +0000
1808+++ src/provisioningserver/drivers/osystem/tests/test_custom.py 2015-07-03 11:18:28 +0000
1809@@ -19,26 +19,30 @@
1810
1811 from maastesting.factory import factory
1812 from maastesting.testcase import MAASTestCase
1813-from provisioningserver.drivers.osystem import custom
1814+from provisioningserver.config import ClusterConfiguration
1815 from provisioningserver.drivers.osystem.custom import (
1816 BOOT_IMAGE_PURPOSE,
1817 CustomOS,
1818 )
1819+from provisioningserver.testing.config import ClusterConfigurationFixture
1820
1821
1822 class TestCustomOS(MAASTestCase):
1823
1824 def make_resource_path(self, filename):
1825+ self.useFixture(ClusterConfigurationFixture())
1826 tmpdir = self.make_dir()
1827 arch = factory.make_name('arch')
1828 subarch = factory.make_name('subarch')
1829 release = factory.make_name('release')
1830 label = factory.make_name('label')
1831+ current_dir = os.path.join(tmpdir, 'current') + '/'
1832 dirpath = os.path.join(
1833- tmpdir, 'current', 'custom', arch, subarch, release, label)
1834+ current_dir, 'custom', arch, subarch, release, label)
1835 os.makedirs(dirpath)
1836 factory.make_file(dirpath, filename)
1837- self.patch(custom, 'BOOT_RESOURCES_STORAGE', tmpdir)
1838+ with ClusterConfiguration.open() as config:
1839+ config.tftp_root = current_dir
1840 return arch, subarch, release, label
1841
1842 def test_get_boot_image_purposes(self):
1843
1844=== modified file 'src/provisioningserver/drivers/osystem/tests/test_windows.py'
1845--- src/provisioningserver/drivers/osystem/tests/test_windows.py 2015-05-29 16:47:37 +0000
1846+++ src/provisioningserver/drivers/osystem/tests/test_windows.py 2015-07-03 11:18:28 +0000
1847@@ -25,12 +25,12 @@
1848 )
1849 from provisioningserver.drivers.osystem.windows import (
1850 BOOT_IMAGE_PURPOSE,
1851- Config,
1852 REQUIRE_LICENSE_KEY,
1853 WINDOWS_CHOICES,
1854 WINDOWS_DEFAULT,
1855 WindowsOS,
1856 )
1857+from provisioningserver.testing.config import ClusterConfigurationFixture
1858
1859
1860 class TestWindowsOS(MAASTestCase):
1861@@ -46,11 +46,7 @@
1862 os.makedirs(dirpath)
1863 for fname in files:
1864 factory.make_file(dirpath, fname)
1865- self.patch(Config, 'load_from_cache').return_value = {
1866- 'tftp': {
1867- 'resource_root': tmpdir,
1868- },
1869- }
1870+ self.useFixture(ClusterConfigurationFixture(tftp_root=tmpdir))
1871 return arch, subarch, release, label
1872
1873 def test_get_boot_image_purposes_neither(self):
1874
1875=== modified file 'src/provisioningserver/drivers/osystem/windows.py'
1876--- src/provisioningserver/drivers/osystem/windows.py 2015-05-29 16:47:37 +0000
1877+++ src/provisioningserver/drivers/osystem/windows.py 2015-07-03 11:18:28 +0000
1878@@ -19,7 +19,7 @@
1879 import os
1880 import re
1881
1882-from provisioningserver.config import Config
1883+from provisioningserver.config import ClusterConfiguration
1884 from provisioningserver.drivers.osystem import (
1885 BOOT_IMAGE_PURPOSE,
1886 OperatingSystem,
1887@@ -51,7 +51,8 @@
1888 # is available the node will boot correctly, even if fast-path
1889 # installer is not selected.
1890 purposes = []
1891- resources = Config.load_from_cache()['tftp']['resource_root']
1892+ with ClusterConfiguration.open() as config:
1893+ resources = config.tftp_root
1894 path = os.path.join(
1895 resources, 'windows', arch, subarch, release, label)
1896 if os.path.exists(os.path.join(path, 'root-dd')):
1897
1898=== modified file 'src/provisioningserver/import_images/boot_resources.py'
1899--- src/provisioningserver/import_images/boot_resources.py 2015-05-29 16:47:37 +0000
1900+++ src/provisioningserver/import_images/boot_resources.py 2015-07-03 11:18:28 +0000
1901@@ -22,10 +22,12 @@
1902 import os
1903 from textwrap import dedent
1904
1905-import provisioningserver
1906 from provisioningserver.boot import BootMethodRegistry
1907 from provisioningserver.boot.tftppath import list_boot_images
1908-from provisioningserver.config import BootSources
1909+from provisioningserver.config import (
1910+ BootSources,
1911+ ClusterConfiguration,
1912+)
1913 from provisioningserver.import_images.cleanup import (
1914 cleanup_snapshots_and_cache,
1915 )
1916@@ -39,7 +41,6 @@
1917 from provisioningserver.import_images.keyrings import write_all_keyrings
1918 from provisioningserver.import_images.product_mapping import map_products
1919 from provisioningserver.service_monitor import service_monitor
1920-from provisioningserver.utils import get_cluster_config
1921 from provisioningserver.utils.fs import (
1922 atomic_symlink,
1923 atomic_write,
1924@@ -49,6 +50,16 @@
1925 from provisioningserver.utils.shell import call_and_check
1926
1927
1928+def two_dir_levels_up(path):
1929+ # NOTE: Syntax along the lines of:
1930+ # os.path.normpath(os.path.join(
1931+ # path, os.path.pardir, os.path.pardir))
1932+ # will NOT work here, as the classes that use these file paths
1933+ # to NOT support paths with 'os.path.pardir' segments. Thus
1934+ # we must manually resolve the path here.
1935+ return os.path.split(os.path.split(path)[0])[0]
1936+
1937+
1938 class NoConfigFile(Exception):
1939 """Raised when the config file for the script doesn't exist."""
1940
1941@@ -250,7 +261,8 @@
1942 "any boot images available.")
1943 return
1944
1945- storage = provisioningserver.config.BOOT_RESOURCES_STORAGE
1946+ with ClusterConfiguration.open() as config:
1947+ storage = two_dir_levels_up(config.tftp_root)
1948 meta_file_content = image_descriptions.dump_json()
1949 if meta_contains(storage, meta_file_content):
1950 maaslog.info(
1951@@ -359,10 +371,6 @@
1952 finally:
1953 reactor.callLater(0, reactor.stop)
1954
1955- cluster_config = get_cluster_config('/etc/maas/maas_cluster.conf')
1956- os.environ['MAAS_URL'] = cluster_config['MAAS_URL']
1957- os.environ['CLUSTER_UUID'] = cluster_config['CLUSTER_UUID']
1958-
1959 reactor.callWhenRunning(run_main)
1960 reactor.run()
1961
1962
1963=== modified file 'src/provisioningserver/import_images/tests/test_boot_resources.py'
1964--- src/provisioningserver/import_images/tests/test_boot_resources.py 2015-05-29 16:47:37 +0000
1965+++ src/provisioningserver/import_images/tests/test_boot_resources.py 2015-07-03 11:18:28 +0000
1966@@ -40,14 +40,19 @@
1967 import mock
1968 from mock import call
1969 from provisioningserver.boot import BootMethodRegistry
1970-import provisioningserver.config
1971-from provisioningserver.config import BootSources
1972+from provisioningserver.config import (
1973+ BootSources,
1974+ ClusterConfiguration,
1975+)
1976 from provisioningserver.import_images import boot_resources
1977 from provisioningserver.import_images.boot_image_mapping import (
1978 BootImageMapping,
1979 )
1980 from provisioningserver.import_images.testing.factory import make_image_spec
1981-from provisioningserver.testing.config import BootSourcesFixture
1982+from provisioningserver.testing.config import (
1983+ BootSourcesFixture,
1984+ ClusterConfigurationFixture,
1985+)
1986 from provisioningserver.utils.fs import write_text_file
1987 from testtools.content import Content
1988 from testtools.content_type import UTF8_TEXT
1989@@ -196,9 +201,13 @@
1990
1991 def setUp(self):
1992 super(TestMain, self).setUp()
1993+ self.useFixture(ClusterConfigurationFixture())
1994 self.storage = self.make_dir()
1995- self.patch(
1996- provisioningserver.config, 'BOOT_RESOURCES_STORAGE', self.storage)
1997+ current_dir = os.path.join(self.storage, 'current') + os.sep
1998+ os.makedirs(current_dir)
1999+ with ClusterConfiguration.open() as config:
2000+ config.tftp_root = current_dir
2001+ os.rmdir(current_dir)
2002 # Forcing arch to amd64 causes pxelinux.0 to be installed, giving more
2003 # test coverage.
2004 self.image = make_image_spec(arch='amd64')
2005
2006=== modified file 'src/provisioningserver/path.py'
2007--- src/provisioningserver/path.py 2015-05-07 18:14:38 +0000
2008+++ src/provisioningserver/path.py 2015-07-03 11:18:28 +0000
2009@@ -22,6 +22,17 @@
2010 from provisioningserver.utils.fs import ensure_dir
2011
2012
2013+def get_root():
2014+ """Return ``MAAS_ROOT`` if set, else "/"."""
2015+ root = os.getenv('MAAS_ROOT')
2016+ if root is None:
2017+ return "/"
2018+ elif len(root) == 0:
2019+ return "/"
2020+ else:
2021+ return root
2022+
2023+
2024 def get_tentative_path(*path_elements):
2025 """Return an absolute path based on the `MAAS_ROOT` environment variable.
2026
2027@@ -44,12 +55,12 @@
2028 holds your path, export a getter function that returns your path. Add
2029 caching if it becomes a performance problem.
2030 """
2031- maas_root = os.getenv('MAAS_ROOT', '/')
2032- # Strip off a leading slash, if any. If left in, it would override any
2033- # preceding path elements. The MAAS_ROOT would be ignored.
2034- # The dot is there to make the call work even with zero path elements.
2035+ # Strip off a leading slash from the given path, if any. If it were left
2036+ # in, it would override preceding path elements and MAAS_ROOT would be
2037+ # ignored later on. The dot is there to make the call work even with zero
2038+ # path elements.
2039 path = os.path.join('.', *path_elements).lstrip('/')
2040- path = os.path.join(maas_root, path)
2041+ path = os.path.join(get_root(), path)
2042 return os.path.abspath(path)
2043
2044
2045
2046=== modified file 'src/provisioningserver/plugin.py'
2047--- src/provisioningserver/plugin.py 2015-06-25 14:48:14 +0000
2048+++ src/provisioningserver/plugin.py 2015-07-03 11:18:28 +0000
2049@@ -17,10 +17,10 @@
2050 ]
2051
2052 from errno import ENOPROTOOPT
2053-import os
2054 import socket
2055 from socket import error as socket_error
2056
2057+from provisioningserver.config import ClusterConfiguration
2058 from provisioningserver.monkey import (
2059 add_term_error_code_to_tftp,
2060 force_simplestreams_to_use_urllib2,
2061@@ -48,7 +48,6 @@
2062 """Command line options for the provisioning server."""
2063
2064 optParameters = [
2065- ["config-file", "c", "pserv.yaml", "Configuration file to load."],
2066 ["introspect", None, None,
2067 ("Allow introspection, allowing unhindered access to the internals "
2068 "of MAAS. This should probably only be used for debugging. Supply "
2069@@ -79,12 +78,10 @@
2070 site_service.setName("site")
2071 return site_service
2072
2073- def _makeImageService(self):
2074+ def _makeImageService(self, resource_root):
2075 from provisioningserver.pserv_services.image import (
2076 BootImageEndpointService)
2077 from twisted.internet.endpoints import AdoptedStreamServerEndpoint
2078- from provisioningserver import config
2079-
2080 port = 5248 # config["port"]
2081 # Make a socket with SO_REUSEPORT set so that we can run multiple we
2082 # applications. This is easier to do from outside of Twisted as there's
2083@@ -112,59 +109,54 @@
2084 site_endpoint.socket = s # Prevent garbage collection.
2085
2086 image_service = BootImageEndpointService(
2087- resource_root=os.path.join(
2088- config.BOOT_RESOURCES_STORAGE, "current"),
2089- endpoint=site_endpoint)
2090+ resource_root=resource_root, endpoint=site_endpoint)
2091 image_service.setName("image_service")
2092 return image_service
2093
2094- def _makeTFTPService(self, tftp_config):
2095+ def _makeTFTPService(
2096+ self, cluster_uuid, tftp_root, tftp_port, tftp_generator):
2097 """Create the dynamic TFTP service."""
2098 from provisioningserver.pserv_services.tftp import TFTPService
2099 tftp_service = TFTPService(
2100- resource_root=tftp_config['resource_root'],
2101- port=tftp_config['port'], generator=tftp_config['generator'])
2102+ resource_root=tftp_root, port=tftp_port, generator=tftp_generator,
2103+ uuid=cluster_uuid)
2104 tftp_service.setName("tftp")
2105 return tftp_service
2106
2107- def _makeImageDownloadService(self, rpc_service):
2108- from provisioningserver.cluster_config import get_cluster_uuid
2109+ def _makeImageDownloadService(self, rpc_service, cluster_uuid, tftp_root):
2110 from provisioningserver.pserv_services.image_download_service \
2111 import ImageDownloadService
2112 image_download_service = ImageDownloadService(
2113- rpc_service, reactor, get_cluster_uuid())
2114+ rpc_service, cluster_uuid, tftp_root, reactor)
2115 image_download_service.setName("image_download")
2116 return image_download_service
2117
2118- def _makeLeaseUploadService(self, rpc_service):
2119- from provisioningserver.cluster_config import get_cluster_uuid
2120+ def _makeLeaseUploadService(self, rpc_service, cluster_uuid):
2121 from provisioningserver.pserv_services.lease_upload_service \
2122 import LeaseUploadService
2123 lease_upload_service = LeaseUploadService(
2124- rpc_service, reactor, get_cluster_uuid())
2125+ rpc_service, reactor, cluster_uuid)
2126 lease_upload_service.setName("lease_upload")
2127 return lease_upload_service
2128
2129- def _makeNodePowerMonitorService(self):
2130- from provisioningserver.cluster_config import get_cluster_uuid
2131+ def _makeNodePowerMonitorService(self, cluster_uuid):
2132 from provisioningserver.pserv_services.node_power_monitor_service \
2133 import NodePowerMonitorService
2134- node_monitor = NodePowerMonitorService(get_cluster_uuid(), reactor)
2135+ node_monitor = NodePowerMonitorService(cluster_uuid, reactor)
2136 node_monitor.setName("node_monitor")
2137 return node_monitor
2138
2139- def _makeRPCService(self, rpc_config):
2140+ def _makeRPCService(self):
2141 from provisioningserver.rpc.clusterservice import ClusterClientService
2142 rpc_service = ClusterClientService(reactor)
2143 rpc_service.setName("rpc")
2144 return rpc_service
2145
2146- def _makeDHCPProbeService(self, rpc_service):
2147- from provisioningserver.cluster_config import get_cluster_uuid
2148+ def _makeDHCPProbeService(self, rpc_service, cluster_uuid):
2149 from provisioningserver.pserv_services.dhcp_probe_service \
2150 import DHCPProbeService
2151 dhcp_probe_service = DHCPProbeService(
2152- rpc_service, reactor, get_cluster_uuid())
2153+ rpc_service, reactor, cluster_uuid)
2154 dhcp_probe_service.setName("dhcp_probe")
2155 return dhcp_probe_service
2156
2157@@ -183,6 +175,23 @@
2158 introspect_service.setName("introspect")
2159 return introspect_service
2160
2161+ def _makeServices(self, config):
2162+ # Several services need to make use of the RPC service.
2163+ rpc_service = self._makeRPCService()
2164+ yield rpc_service
2165+ # Other services that make up the MAAS Region Controller.
2166+ yield self._makeDHCPProbeService(rpc_service, config.cluster_uuid)
2167+ yield self._makeLeaseUploadService(rpc_service, config.cluster_uuid)
2168+ yield self._makeNodePowerMonitorService(config.cluster_uuid)
2169+ yield self._makeServiceMonitorService()
2170+ yield self._makeImageDownloadService(
2171+ rpc_service, config.cluster_uuid, config.tftp_root)
2172+ # The following are network-accessible services.
2173+ yield self._makeImageService(config.tftp_root)
2174+ yield self._makeTFTPService(
2175+ config.cluster_uuid, config.tftp_root, config.tftp_port,
2176+ config.tftp_generator_url)
2177+
2178 def makeService(self, options):
2179 """Construct the MAAS Cluster service."""
2180 register_sigusr2_thread_dump_handler()
2181@@ -190,33 +199,9 @@
2182 add_term_error_code_to_tftp()
2183
2184 from provisioningserver import services
2185- from provisioningserver.config import Config
2186-
2187- config = Config.load(options["config-file"])
2188-
2189- image_service = self._makeImageService()
2190- image_service.setServiceParent(services)
2191-
2192- tftp_service = self._makeTFTPService(config["tftp"])
2193- tftp_service.setServiceParent(services)
2194-
2195- rpc_service = self._makeRPCService(config["rpc"])
2196- rpc_service.setServiceParent(services)
2197-
2198- node_monitor = self._makeNodePowerMonitorService()
2199- node_monitor.setServiceParent(services)
2200-
2201- image_download_service = self._makeImageDownloadService(rpc_service)
2202- image_download_service.setServiceParent(services)
2203-
2204- dhcp_probe_service = self._makeDHCPProbeService(rpc_service)
2205- dhcp_probe_service.setServiceParent(services)
2206-
2207- lease_upload_service = self._makeLeaseUploadService(rpc_service)
2208- lease_upload_service.setServiceParent(services)
2209-
2210- service_monitor_service = self._makeServiceMonitorService()
2211- service_monitor_service.setServiceParent(services)
2212+ with ClusterConfiguration.open() as config:
2213+ for service in self._makeServices(config):
2214+ service.setServiceParent(services)
2215
2216 if options["introspect"] is not None:
2217 introspect = self._makeIntrospectionService(options["introspect"])
2218
2219=== modified file 'src/provisioningserver/pserv_services/image_download_service.py'
2220--- src/provisioningserver/pserv_services/image_download_service.py 2015-05-07 18:14:38 +0000
2221+++ src/provisioningserver/pserv_services/image_download_service.py 2015-07-03 11:18:28 +0000
2222@@ -45,22 +45,24 @@
2223
2224
2225 class ImageDownloadService(TimerService, object):
2226- """Twisted service to periodically refresh ephemeral images.
2227-
2228- :param client_service: A `ClusterClientService` instance for talking
2229- to the region controller.
2230- :param reactor: An `IReactor` instance.
2231- """
2232+ """Twisted service to periodically refresh ephemeral images."""
2233
2234 check_interval = timedelta(minutes=5).total_seconds()
2235
2236- def __init__(self, client_service, reactor, cluster_uuid):
2237- # Call self.check() every self.check_interval.
2238+ def __init__(self, client_service, cluster_uuid, tftp_root, reactor):
2239+ """Twisted service to periodically refresh ephemeral images.
2240+
2241+ :param client_service: A `ClusterClientService` instance.
2242+ :param cluster_uuid: The UUID for this cluster, as a string.
2243+ :param tftp_root: The path to the TFTP root directory.
2244+ :param reactor: An `IReactor` instance.
2245+ """
2246 super(ImageDownloadService, self).__init__(
2247 self.check_interval, self.try_download)
2248- self.clock = reactor
2249 self.client_service = client_service
2250 self.uuid = cluster_uuid
2251+ self.tftp_root = tftp_root
2252+ self.clock = reactor
2253
2254 def try_download(self):
2255 """Wrap download attempts in something that catches Failures.
2256@@ -125,11 +127,10 @@
2257 """Check the time the last image refresh happened and initiate a new
2258 one if older than 15 minutes.
2259 """
2260- last_modified = tftppath.maas_meta_last_modified()
2261+ last_modified = tftppath.maas_meta_last_modified(self.tftp_root)
2262 if last_modified is None:
2263 yield self._start_download()
2264- return
2265-
2266- age_in_seconds = self.clock.seconds() - last_modified
2267- if age_in_seconds >= timedelta(minutes=15).total_seconds():
2268- yield self._start_download()
2269+ else:
2270+ age_in_seconds = self.clock.seconds() - last_modified
2271+ if age_in_seconds >= timedelta(minutes=15).total_seconds():
2272+ yield self._start_download()
2273
2274=== modified file 'src/provisioningserver/pserv_services/service_monitor_service.py'
2275--- src/provisioningserver/pserv_services/service_monitor_service.py 2015-05-19 19:02:22 +0000
2276+++ src/provisioningserver/pserv_services/service_monitor_service.py 2015-07-03 11:18:28 +0000
2277@@ -20,6 +20,7 @@
2278
2279 from datetime import timedelta
2280
2281+from provisioningserver.config import is_dev_environment
2282 from provisioningserver.service_monitor import service_monitor
2283 from twisted.application.internet import TimerService
2284 from twisted.internet.threads import deferToThread
2285@@ -41,6 +42,11 @@
2286 """Monitors all of the external services and makes sure they
2287 stay running.
2288 """
2289- d = deferToThread(service_monitor.ensure_all_services)
2290- d.addErrback(log.err, "Failed to monitor services.")
2291- return d
2292+ if is_dev_environment():
2293+ log.msg(
2294+ "Skipping check of services; they're not running under "
2295+ "the supervision of Upstart or systemd.")
2296+ else:
2297+ d = deferToThread(service_monitor.ensure_all_services)
2298+ d.addErrback(log.err, "Failed to monitor services.")
2299+ return d
2300
2301=== modified file 'src/provisioningserver/pserv_services/tests/test_image_download_service.py'
2302--- src/provisioningserver/pserv_services/tests/test_image_download_service.py 2015-05-22 15:52:13 +0000
2303+++ src/provisioningserver/pserv_services/tests/test_image_download_service.py 2015-07-03 11:18:28 +0000
2304@@ -57,11 +57,13 @@
2305
2306 def test_init(self):
2307 service = ImageDownloadService(
2308- sentinel.service, sentinel.clock, sentinel.uuid)
2309+ sentinel.service, sentinel.uuid, sentinel.tftp_root,
2310+ sentinel.clock)
2311 self.assertIsInstance(service, TimerService)
2312 self.assertIs(service.clock, sentinel.clock)
2313 self.assertIs(service.uuid, sentinel.uuid)
2314 self.assertIs(service.client_service, sentinel.service)
2315+ self.assertIs(service.tftp_root, sentinel.tftp_root)
2316
2317 def patch_download(self, service, return_value):
2318 patched = self.patch(service, '_start_download')
2319@@ -71,7 +73,7 @@
2320 def test_is_called_every_interval(self):
2321 clock = Clock()
2322 service = ImageDownloadService(
2323- sentinel.service, clock, sentinel.uuid)
2324+ sentinel.service, sentinel.uuid, sentinel.tftp_root, clock)
2325 # Avoid actual downloads:
2326 self.patch_download(service, None)
2327 maas_meta_last_modified = self.patch(
2328@@ -100,7 +102,7 @@
2329 def test_initiates_download_if_no_meta_file(self):
2330 clock = Clock()
2331 service = ImageDownloadService(
2332- sentinel.service, clock, sentinel.uuid)
2333+ sentinel.service, sentinel.uuid, sentinel.tftp_root, clock)
2334 _start_download = self.patch_download(service, None)
2335 self.patch(
2336 tftppath,
2337@@ -111,7 +113,7 @@
2338 def test_initiates_download_if_15_minutes_has_passed(self):
2339 clock = Clock()
2340 service = ImageDownloadService(
2341- sentinel.service, clock, sentinel.uuid)
2342+ sentinel.service, sentinel.uuid, sentinel.tftp_root, clock)
2343 _start_download = self.patch_download(service, None)
2344 one_week_ago = clock.seconds() - timedelta(minutes=15).total_seconds()
2345 self.patch(
2346@@ -123,7 +125,7 @@
2347 def test_no_download_if_15_minutes_has_not_passed(self):
2348 clock = Clock()
2349 service = ImageDownloadService(
2350- sentinel.service, clock, sentinel.uuid)
2351+ sentinel.service, sentinel.uuid, sentinel.tftp_root, clock)
2352 _start_download = self.patch_download(service, None)
2353 one_week = timedelta(minutes=15).total_seconds()
2354 self.patch(
2355@@ -159,7 +161,7 @@
2356 deferToThread = self.patch(boot_images, 'deferToThread')
2357 deferToThread.return_value = defer.succeed(None)
2358 service = ImageDownloadService(
2359- rpc_client, clock, sentinel.uuid)
2360+ rpc_client, sentinel.uuid, sentinel.tftp_root, clock)
2361 service.startService()
2362 self.assertThat(
2363 deferToThread, MockCalledOnceWith(
2364@@ -173,13 +175,13 @@
2365
2366 deferToThread = self.patch(boot_images, 'deferToThread')
2367 service = ImageDownloadService(
2368- rpc_client, Clock(), sentinel.uuid)
2369+ rpc_client, sentinel.uuid, self.make_dir(), Clock())
2370 service.startService()
2371 self.assertThat(deferToThread, MockNotCalled())
2372
2373 def test_logs_other_errors(self):
2374 service = ImageDownloadService(
2375- sentinel.rpc, Clock(), sentinel.uuid)
2376+ sentinel.rpc, sentinel.uuid, sentinel.tftp_root, Clock())
2377
2378 maybe_start_download = self.patch(service, "maybe_start_download")
2379 maybe_start_download.return_value = defer.fail(
2380@@ -215,7 +217,7 @@
2381 ]
2382
2383 service = ImageDownloadService(
2384- sentinel.rpc, clock, sentinel.uuid)
2385+ sentinel.rpc, sentinel.uuid, sentinel.tftp_root, clock)
2386 sources = yield service._get_boot_sources(client_call)
2387 self.assertEqual(sources.get('sources'), sentinel.sources)
2388 self.assertThat(
2389@@ -232,7 +234,7 @@
2390 ]
2391
2392 service = ImageDownloadService(
2393- sentinel.rpc, clock, sentinel.uuid)
2394+ sentinel.rpc, sentinel.uuid, sentinel.tftp_root, clock)
2395 yield service._get_boot_sources(client_call)
2396 self.assertThat(
2397 client_call,
2398@@ -270,7 +272,7 @@
2399 ]
2400
2401 service = ImageDownloadService(
2402- sentinel.rpc, clock, sentinel.uuid)
2403+ sentinel.rpc, sentinel.uuid, sentinel.tftp_root, clock)
2404 sources = yield service._get_boot_sources(client_call)
2405 os_selections = [
2406 selection.get('os')
2407
2408=== modified file 'src/provisioningserver/pserv_services/tests/test_service_monitor_service.py'
2409--- src/provisioningserver/pserv_services/tests/test_service_monitor_service.py 2015-06-10 11:35:04 +0000
2410+++ src/provisioningserver/pserv_services/tests/test_service_monitor_service.py 2015-07-03 11:18:28 +0000
2411@@ -15,11 +15,15 @@
2412 __metaclass__ = type
2413 __all__ = []
2414
2415-from maastesting.matchers import MockCalledOnceWith
2416+from maastesting.matchers import (
2417+ MockCalledOnceWith,
2418+ MockNotCalled,
2419+)
2420 from maastesting.testcase import (
2421 MAASTestCase,
2422 MAASTwistedRunTest,
2423 )
2424+from maastesting.twisted import TwistedLoggerFixture
2425 from provisioningserver.pserv_services import service_monitor_service as sms
2426 from provisioningserver.service_monitor import service_monitor
2427 from testtools.matchers import MatchesStructure
2428@@ -40,7 +44,23 @@
2429 service = sms.ServiceMonitorService(Clock())
2430 return service
2431
2432+ def test_monitor_services_does_not_do_anything_in_dev_environment(self):
2433+ # Belt-n-braces make sure we're in a development environment.
2434+ self.assertTrue(sms.is_dev_environment())
2435+
2436+ service = self.make_monitor_service()
2437+ mock_deferToThread = self.patch(sms, "deferToThread")
2438+ with TwistedLoggerFixture() as logger:
2439+ service.monitor_services()
2440+ self.assertThat(mock_deferToThread, MockNotCalled())
2441+ self.assertDocTestMatches(
2442+ "Skipping check of services; they're not running under the "
2443+ "supervision of Upstart or systemd.", logger.output)
2444+
2445 def test_monitor_services_defers_ensure_all_services_to_thread(self):
2446+ # Pretend we're in a production environment.
2447+ self.patch(sms, "is_dev_environment").return_value = False
2448+
2449 service = self.make_monitor_service()
2450 mock_deferToThread = self.patch(sms, "deferToThread")
2451 service.monitor_services()
2452
2453=== modified file 'src/provisioningserver/pserv_services/tests/test_tftp.py'
2454--- src/provisioningserver/pserv_services/tests/test_tftp.py 2015-05-29 16:47:37 +0000
2455+++ src/provisioningserver/pserv_services/tests/test_tftp.py 2015-07-03 11:18:28 +0000
2456@@ -30,7 +30,6 @@
2457 urlparse,
2458 )
2459
2460-from fixtures import EnvironmentVariable
2461 from maastesting.factory import factory
2462 from maastesting.matchers import (
2463 MockCalledOnceWith,
2464@@ -54,6 +53,7 @@
2465 from provisioningserver.boot import BytesReader
2466 from provisioningserver.boot.pxe import PXEBootMethod
2467 from provisioningserver.boot.tests.test_pxe import compose_config_path
2468+from provisioningserver.config import ClusterConfiguration
2469 from provisioningserver.events import EVENT_TYPES
2470 from provisioningserver.pserv_services import tftp as tftp_module
2471 from provisioningserver.pserv_services.tftp import (
2472@@ -63,6 +63,7 @@
2473 TFTPService,
2474 UDPServer,
2475 )
2476+from provisioningserver.testing.config import ClusterConfigurationFixture
2477 from provisioningserver.tests.test_kernel_opts import make_kernel_parameters
2478 from testtools import ExpectedException
2479 from testtools.matchers import (
2480@@ -70,6 +71,7 @@
2481 AllMatch,
2482 Equals,
2483 HasLength,
2484+ Is,
2485 IsInstance,
2486 MatchesAll,
2487 MatchesStructure,
2488@@ -127,6 +129,7 @@
2489
2490 def setUp(self):
2491 super(TestTFTPBackend, self).setUp()
2492+ self.useFixture(ClusterConfigurationFixture())
2493 from provisioningserver import boot
2494 self.patch(boot, "find_mac_via_arp")
2495 self.patch(tftp_module, 'log_request')
2496@@ -135,10 +138,11 @@
2497 temp_dir = self.make_dir()
2498 generator_url = "http://%s.example.com/%s" % (
2499 factory.make_name("domain"), factory.make_name("path"))
2500- backend = TFTPBackend(temp_dir, generator_url)
2501+ backend = TFTPBackend(temp_dir, generator_url, sentinel.uuid)
2502 self.assertEqual((True, False), (backend.can_read, backend.can_write))
2503 self.assertEqual(temp_dir, backend.base.path)
2504 self.assertEqual(generator_url, backend.generator_url.geturl())
2505+ self.assertIs(backend.cluster_uuid, sentinel.uuid)
2506
2507 def test_get_generator_url(self):
2508 # get_generator_url() merges the parameters obtained from the request
2509@@ -146,7 +150,7 @@
2510 mac = factory.make_mac_address("-")
2511 dummy = factory.make_name("dummy").encode("ascii")
2512 backend_url = b"http://example.com/?" + urlencode({b"dummy": dummy})
2513- backend = TFTPBackend(self.make_dir(), backend_url)
2514+ backend = TFTPBackend(self.make_dir(), backend_url, sentinel.uuid)
2515 # params is an example of the parameters obtained from a request.
2516 params = {"mac": mac}
2517 generator_url = urlparse(backend.get_generator_url(params))
2518@@ -161,7 +165,8 @@
2519 def get_reader(self, data):
2520 temp_file = self.make_file(name="example", contents=data)
2521 temp_dir = os.path.dirname(temp_file)
2522- backend = TFTPBackend(temp_dir, "http://nowhere.example.com/")
2523+ backend = TFTPBackend(
2524+ temp_dir, "http://nowhere.example.com/", sentinel.uuid)
2525 return backend.get_reader("example")
2526
2527 @inlineCallbacks
2528@@ -188,7 +193,8 @@
2529 factory.make_file(os.path.join(temp_dir, subdir), filename, data)
2530
2531 path = '\\%s\\%s' % (subdir, filename)
2532- backend = TFTPBackend(temp_dir, "http://nowhere.example.com/")
2533+ backend = TFTPBackend(
2534+ temp_dir, "http://nowhere.example.com/", sentinel.uuid)
2535 reader = yield backend.get_reader(path)
2536
2537 self.addCleanup(reader.finish)
2538@@ -219,9 +225,11 @@
2539
2540 @inlineCallbacks
2541 def test_get_reader_converts_404s_to_tftp_error(self):
2542- self.useFixture(EnvironmentVariable("CLUSTER_UUID", "foobar"))
2543+ with ClusterConfiguration.open() as config:
2544+ config.cluster_uuid = factory.make_UUID()
2545
2546- backend = TFTPBackend(self.make_dir(), "http://example.com/")
2547+ backend = TFTPBackend(
2548+ self.make_dir(), "http://example.com/", sentinel.uuid)
2549 get_page = self.patch(backend, 'get_page')
2550 get_page.side_effect = twisted.web.error.Error(httplib.NOT_FOUND)
2551
2552@@ -230,11 +238,13 @@
2553
2554 @inlineCallbacks
2555 def test_get_reader_converts_other_exceptions_to_tftp_error(self):
2556- self.useFixture(EnvironmentVariable("CLUSTER_UUID", "foobar"))
2557+ with ClusterConfiguration.open() as config:
2558+ config.cluster_uuid = factory.make_UUID()
2559
2560 exception_type = factory.make_exception_type()
2561 exception_message = factory.make_string()
2562- backend = TFTPBackend(self.make_dir(), "http://example.com/")
2563+ backend = TFTPBackend(
2564+ self.make_dir(), "http://example.com/", sentinel.uuid)
2565 get_page = self.patch(backend, 'get_page')
2566 get_page.side_effect = exception_type(exception_message)
2567
2568@@ -257,11 +267,12 @@
2569 # For paths matching PXEBootMethod.match_path, TFTPBackend.get_reader()
2570 # returns a Deferred that will yield a BytesReader.
2571 cluster_uuid = factory.make_UUID()
2572- get_cluster_uuid = self.patch(tftp_module, 'get_cluster_uuid')
2573- get_cluster_uuid.return_value = cluster_uuid
2574+ with ClusterConfiguration.open() as config:
2575+ config.cluster_uuid = cluster_uuid
2576 mac = factory.make_mac_address("-")
2577 config_path = compose_config_path(mac)
2578- backend = TFTPBackend(self.make_dir(), b"http://example.com/")
2579+ backend = TFTPBackend(
2580+ self.make_dir(), b"http://example.com/", cluster_uuid)
2581 # python-tx-tftp sets up call context so that backends can discover
2582 # more about the environment in which they're running.
2583 call_context = {"local": local, "remote": remote}
2584@@ -318,7 +329,8 @@
2585 # get_boot_method_reader() takes a dict() of parameters and returns an
2586 # `IReader` of a PXE configuration, rendered by
2587 # `PXEBootMethod.get_reader`.
2588- backend = TFTPBackend(self.make_dir(), b"http://example.com/")
2589+ backend = TFTPBackend(
2590+ self.make_dir(), b"http://example.com/", sentinel.uuid)
2591 # Fake configuration parameters, as discovered from the file path.
2592 fake_params = {"mac": factory.make_mac_address("-")}
2593 # Fake kernel configuration parameters, as returned from the API call.
2594@@ -356,10 +368,11 @@
2595 # arch field of the parameters (mapping from pxe to maas
2596 # namespace).
2597 cluster_uuid = factory.make_UUID()
2598- get_cluster_uuid = self.patch(tftp_module, 'get_cluster_uuid')
2599- get_cluster_uuid.return_value = cluster_uuid
2600+ with ClusterConfiguration.open() as config:
2601+ config.cluster_uuid = cluster_uuid
2602 config_path = "pxelinux.cfg/default-arm"
2603- backend = TFTPBackend(self.make_dir(), b"http://example.com/")
2604+ backend = TFTPBackend(
2605+ self.make_dir(), b"http://example.com/", cluster_uuid)
2606 # python-tx-tftp sets up call context so that backends can discover
2607 # more about the environment in which they're running.
2608 call_context = {
2609@@ -400,7 +413,7 @@
2610 example_port = factory.pick_port()
2611 tftp_service = TFTPService(
2612 resource_root=example_root, generator=example_generator,
2613- port=example_port)
2614+ port=example_port, uuid=sentinel.uuid)
2615 tftp_service.updateServers()
2616 # The "tftp" service is a multi-service containing UDP servers for
2617 # each interface defined by get_all_interface_addresses().
2618@@ -418,7 +431,10 @@
2619 Equals(example_root)),
2620 AfterPreprocessing(
2621 lambda backend: backend.generator_url.geturl(),
2622- Equals(example_generator)))
2623+ Equals(example_generator)),
2624+ AfterPreprocessing(
2625+ lambda backend: backend.cluster_uuid,
2626+ Is(sentinel.uuid)))
2627 expected_protocol = MatchesAll(
2628 IsInstance(TFTP),
2629 AfterPreprocessing(
2630@@ -452,7 +468,7 @@
2631
2632 tftp_service = TFTPService(
2633 resource_root=self.make_dir(), generator="http://mighty/wind",
2634- port=factory.pick_port())
2635+ port=factory.pick_port(), uuid=sentinel.uuid)
2636 tftp_service.updateServers()
2637
2638 # The child services of tftp_services are named after the
2639@@ -494,7 +510,7 @@
2640
2641 tftp_service = TFTPService(
2642 resource_root=self.make_dir(), generator="http://mighty/wind",
2643- port=factory.pick_port())
2644+ port=factory.pick_port(), uuid=sentinel.uuid)
2645 tftp_service.updateServers()
2646
2647 # Only the "normal" addresses have been used.
2648
2649=== modified file 'src/provisioningserver/pserv_services/tftp.py'
2650--- src/provisioningserver/pserv_services/tftp.py 2015-05-01 13:30:13 +0000
2651+++ src/provisioningserver/pserv_services/tftp.py 2015-07-03 11:18:28 +0000
2652@@ -35,7 +35,6 @@
2653 BootMethodRegistry,
2654 get_remote_mac,
2655 )
2656-from provisioningserver.cluster_config import get_cluster_uuid
2657 from provisioningserver.drivers import ArchitectureRegistry
2658 from provisioningserver.events import (
2659 EVENT_TYPES,
2660@@ -109,16 +108,18 @@
2661 fetch files at many similar paths which must not be passed on.
2662 """
2663
2664- def __init__(self, base_path, generator_url):
2665+ def __init__(self, base_path, generator_url, cluster_uuid):
2666 """
2667 :param base_path: The root directory for this TFTP server.
2668 :param generator_url: The URL which can be queried for the PXE
2669 config. See `get_generator_url` for the types of queries it is
2670 expected to accept.
2671+ :param cluster_uuid: The cluster's UUID, as a string.
2672 """
2673 super(TFTPBackend, self).__init__(
2674 base_path, can_read=True, can_write=False)
2675 self.generator_url = urlparse(generator_url)
2676+ self.cluster_uuid = cluster_uuid
2677 self.fetcher = PageFetcher(agent=self.__class__)
2678 self.get_page = self.fetcher.get
2679
2680@@ -223,7 +224,7 @@
2681 params["local"] = local_host
2682 remote_host, remote_port = tftp.get_remote_address()
2683 params["remote"] = remote_host
2684- params["cluster_uuid"] = get_cluster_uuid()
2685+ params["cluster_uuid"] = self.cluster_uuid
2686 d = self.get_boot_method_reader(boot_method, params)
2687 return d
2688
2689@@ -314,16 +315,17 @@
2690
2691 """
2692
2693- def __init__(self, resource_root, port, generator):
2694+ def __init__(self, resource_root, port, generator, uuid):
2695 """
2696 :param resource_root: The root directory for this TFTP server.
2697 :param port: The port on which each server should be started.
2698 :param generator: The URL to be queried for PXE configuration.
2699 This will normally point to the `pxeconfig` endpoint on the
2700 region-controller API.
2701+ :param uuid: The cluster's UUID, as a string.
2702 """
2703 super(TFTPService, self).__init__()
2704- self.backend = TFTPBackend(resource_root, generator)
2705+ self.backend = TFTPBackend(resource_root, generator, uuid)
2706 self.port = port
2707 # Establish a periodic call to self.updateServers() every 45
2708 # seconds, so that this service eventually converges on truth.
2709
2710=== modified file 'src/provisioningserver/rpc/boot_images.py'
2711--- src/provisioningserver/rpc/boot_images.py 2015-05-29 16:47:37 +0000
2712+++ src/provisioningserver/rpc/boot_images.py 2015-07-03 11:18:28 +0000
2713@@ -18,13 +18,12 @@
2714 "is_import_boot_images_running",
2715 ]
2716
2717-import os
2718 from urlparse import urlparse
2719
2720 from provisioningserver import concurrency
2721 from provisioningserver.auth import get_maas_user_gpghome
2722 from provisioningserver.boot import tftppath
2723-from provisioningserver.config import BOOT_RESOURCES_STORAGE
2724+from provisioningserver.config import ClusterConfiguration
2725 from provisioningserver.import_images import boot_resources
2726 from provisioningserver.utils.env import environment_variables
2727 from provisioningserver.utils.twisted import synchronous
2728@@ -41,10 +40,11 @@
2729 of IO, as this function is called often. To update the cache call
2730 `reload_boot_images`.
2731 """
2732+
2733 global CACHED_BOOT_IMAGES
2734 if CACHED_BOOT_IMAGES is None:
2735- CACHED_BOOT_IMAGES = tftppath.list_boot_images(
2736- os.path.join(BOOT_RESOURCES_STORAGE, 'current'))
2737+ with ClusterConfiguration.open() as config:
2738+ CACHED_BOOT_IMAGES = tftppath.list_boot_images(config.tftp_root)
2739 return CACHED_BOOT_IMAGES
2740
2741
2742@@ -52,8 +52,8 @@
2743 """Update the cached boot images so `list_boot_images` returns the
2744 most up-to-date boot images list."""
2745 global CACHED_BOOT_IMAGES
2746- CACHED_BOOT_IMAGES = tftppath.list_boot_images(
2747- os.path.join(BOOT_RESOURCES_STORAGE, 'current'))
2748+ with ClusterConfiguration.open() as config:
2749+ CACHED_BOOT_IMAGES = tftppath.list_boot_images(config.tftp_root)
2750
2751
2752 def get_hosts_from_sources(sources):
2753
2754=== modified file 'src/provisioningserver/rpc/clusterservice.py'
2755--- src/provisioningserver/rpc/clusterservice.py 2015-05-29 16:47:37 +0000
2756+++ src/provisioningserver/rpc/clusterservice.py 2015-07-03 11:18:28 +0000
2757@@ -26,10 +26,7 @@
2758 from apiclient.creds import convert_string_to_tuple
2759 from apiclient.utils import ascii_url
2760 from provisioningserver import concurrency
2761-from provisioningserver.cluster_config import (
2762- get_cluster_uuid,
2763- get_maas_url,
2764-)
2765+from provisioningserver.config import ClusterConfiguration
2766 from provisioningserver.drivers import (
2767 ArchitectureRegistry,
2768 PowerTypeRegistry,
2769@@ -135,7 +132,8 @@
2770 Implementation of
2771 :py:class:`~provisioningserver.rpc.cluster.Identify`.
2772 """
2773- return {b"ident": get_cluster_uuid().decode("ascii")}
2774+ with ClusterConfiguration.open() as config:
2775+ return {b"ident": config.cluster_uuid}
2776
2777 @cluster.Authenticate.responder
2778 def authenticate(self, message):
2779@@ -508,9 +506,11 @@
2780 returnValue(digest == digest_local)
2781
2782 def registerWithRegion(self):
2783- uuid = get_cluster_uuid()
2784+ with ClusterConfiguration.open() as config:
2785+ uuid = config.cluster_uuid
2786+ url = config.maas_url
2787+
2788 networks = discover_networks()
2789- url = get_maas_url()
2790
2791 def cb_register(_):
2792 log.msg(
2793@@ -760,9 +760,10 @@
2794 @staticmethod
2795 def _get_rpc_info_url():
2796 """Return the URL to the RPC infomation page on the region."""
2797- url = urlparse(get_maas_url())
2798- url = url._replace(path="%s/rpc/" % url.path.rstrip("/"))
2799- url = url.geturl()
2800+ with ClusterConfiguration.open() as config:
2801+ url = urlparse(config.maas_url)
2802+ url = url._replace(path="%s/rpc/" % url.path.rstrip("/"))
2803+ url = url.geturl()
2804 return ascii_url(url)
2805
2806 @classmethod
2807
2808=== modified file 'src/provisioningserver/rpc/tags.py'
2809--- src/provisioningserver/rpc/tags.py 2015-05-29 16:47:37 +0000
2810+++ src/provisioningserver/rpc/tags.py 2015-07-03 11:18:28 +0000
2811@@ -21,10 +21,7 @@
2812 MAASDispatcher,
2813 MAASOAuth,
2814 )
2815-from provisioningserver.cluster_config import (
2816- get_cluster_uuid,
2817- get_maas_url,
2818-)
2819+from provisioningserver.config import ClusterConfiguration
2820 from provisioningserver.tags import process_node_tags
2821 from provisioningserver.utils.twisted import synchronous
2822
2823@@ -38,9 +35,12 @@
2824 :param tag_nsmap: The namespace map as used by LXML's ETree library.
2825 :param credentials: A 3-tuple of OAuth credentials.
2826 """
2827+ with ClusterConfiguration.open() as config:
2828+ cluster_uuid = config.cluster_uuid
2829+ maas_url = config.maas_url
2830 client = MAASClient(
2831 auth=MAASOAuth(*credentials), dispatcher=MAASDispatcher(),
2832- base_url=get_maas_url())
2833+ base_url=maas_url)
2834 process_node_tags(
2835 tag_name=tag_name, tag_definition=tag_definition, tag_nsmap=tag_nsmap,
2836- client=client, nodegroup_uuid=get_cluster_uuid())
2837+ client=client, nodegroup_uuid=cluster_uuid)
2838
2839=== modified file 'src/provisioningserver/rpc/testing/__init__.py'
2840--- src/provisioningserver/rpc/testing/__init__.py 2015-05-22 15:36:53 +0000
2841+++ src/provisioningserver/rpc/testing/__init__.py 2015-07-03 11:18:28 +0000
2842@@ -29,7 +29,6 @@
2843 from os import path
2844
2845 import fixtures
2846-from fixtures import EnvironmentVariable
2847 from maastesting.factory import factory
2848 from maastesting.fixtures import TempDirectory
2849 from maastesting.twisted import always_succeed_with
2850@@ -176,11 +175,6 @@
2851 @asynchronous(timeout=15)
2852 def setUp(self):
2853 super(MockClusterToRegionRPCFixtureBase, self).setUp()
2854- # Ensure that we have MAAS_URL and CLUSTER_UUID set.
2855- self.useFixture(EnvironmentVariable(
2856- "MAAS_URL", factory.make_simple_http_url()))
2857- self.useFixture(EnvironmentVariable(
2858- "CLUSTER_UUID", factory.make_UUID().encode("ascii")))
2859 # Use an inert clock with ClusterClientService so it doesn't update
2860 # itself except when we ask it to.
2861 self.rpc_service = ClusterClientService(Clock())
2862
2863=== modified file 'src/provisioningserver/rpc/tests/test_boot_images.py'
2864--- src/provisioningserver/rpc/tests/test_boot_images.py 2015-05-29 16:47:37 +0000
2865+++ src/provisioningserver/rpc/tests/test_boot_images.py 2015-07-03 11:18:28 +0000
2866@@ -29,7 +29,6 @@
2867 )
2868 from provisioningserver import concurrency
2869 from provisioningserver.boot import tftppath
2870-from provisioningserver.config import BOOT_RESOURCES_STORAGE
2871 from provisioningserver.import_images import boot_resources
2872 from provisioningserver.rpc import boot_images
2873 from provisioningserver.rpc.boot_images import (
2874@@ -40,7 +39,10 @@
2875 list_boot_images,
2876 reload_boot_images,
2877 )
2878-from provisioningserver.testing.config import BootSourcesFixture
2879+from provisioningserver.testing.config import (
2880+ BootSourcesFixture,
2881+ ClusterConfigurationFixture,
2882+)
2883 from provisioningserver.testing.testcase import PservTestCase
2884 from provisioningserver.utils.twisted import pause
2885 from testtools.matchers import Equals
2886@@ -64,14 +66,18 @@
2887
2888 class TestListBootImages(PservTestCase):
2889
2890+ def setUp(self):
2891+ super(TestListBootImages, self).setUp()
2892+ self.tftp_root = self.make_dir()
2893+ self.useFixture(ClusterConfigurationFixture(tftp_root=self.tftp_root))
2894+
2895 def test__calls_list_boot_images_with_boot_resource_storage(self):
2896 self.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
2897 mock_list_boot_images = self.patch(tftppath, 'list_boot_images')
2898 list_boot_images()
2899 self.assertThat(
2900 mock_list_boot_images,
2901- MockCalledOnceWith(
2902- os.path.join(BOOT_RESOURCES_STORAGE, "current")))
2903+ MockCalledOnceWith(self.tftp_root))
2904
2905 def test__calls_list_boot_images_when_cache_is_None(self):
2906 self.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
2907
2908=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
2909--- src/provisioningserver/rpc/tests/test_clusterservice.py 2015-06-16 21:10:26 +0000
2910+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2015-07-03 11:18:28 +0000
2911@@ -29,7 +29,6 @@
2912 from urlparse import urlparse
2913
2914 from apiclient.creds import convert_tuple_to_string
2915-from fixtures import EnvironmentVariable
2916 from maastesting.factory import factory
2917 from maastesting.matchers import (
2918 IsUnfiredDeferred,
2919@@ -57,10 +56,6 @@
2920 from provisioningserver import concurrency
2921 from provisioningserver.boot import tftppath
2922 from provisioningserver.boot.tests.test_tftppath import make_osystem
2923-from provisioningserver.cluster_config import (
2924- get_cluster_uuid,
2925- get_maas_url,
2926-)
2927 from provisioningserver.dhcp.testing.config import make_subnet_config
2928 from provisioningserver.drivers.osystem import (
2929 OperatingSystem,
2930@@ -108,6 +103,7 @@
2931 StubOS,
2932 )
2933 from provisioningserver.security import set_shared_secret_on_filesystem
2934+from provisioningserver.testing.config import ClusterConfigurationFixture
2935 from testtools import ExpectedException
2936 from testtools.deferredruntest import extract_result
2937 from testtools.matchers import (
2938@@ -154,9 +150,8 @@
2939
2940 def test_identify_reports_cluster_uuid(self):
2941 example_uuid = factory.make_UUID()
2942-
2943- get_cluster_uuid = self.patch(clusterservice, "get_cluster_uuid")
2944- get_cluster_uuid.return_value = example_uuid
2945+ self.useFixture(ClusterConfigurationFixture(
2946+ cluster_uuid=example_uuid))
2947
2948 d = call_responder(Cluster(), cluster.Identify, {})
2949
2950@@ -232,6 +227,7 @@
2951
2952 @inlineCallbacks
2953 def test_list_boot_images_can_be_called(self):
2954+ self.useFixture(ClusterConfigurationFixture())
2955 self.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
2956 list_boot_images = self.patch(tftppath, "list_boot_images")
2957 list_boot_images.return_value = []
2958@@ -254,14 +250,16 @@
2959 labels = "beta-1", "release"
2960 purposes = "commissioning", "install", "xinstall"
2961
2962- # Create a TFTP file tree with a variety of subdirectories.
2963+ # Populate a TFTP file tree with a variety of subdirectories.
2964 tftpdir = self.make_dir()
2965 current_dir = os.path.join(tftpdir, 'current')
2966 os.makedirs(current_dir)
2967 for options in product(osystems, archs, subarchs, releases, labels):
2968 os.makedirs(os.path.join(current_dir, *options))
2969 make_osystem(self, options[0], purposes)
2970- self.patch(boot_images, 'BOOT_RESOURCES_STORAGE', tftpdir)
2971+
2972+ self.useFixture(ClusterConfigurationFixture(
2973+ tftp_root=os.path.join(tftpdir, "current")))
2974 self.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
2975
2976 expected_images = [
2977@@ -470,7 +468,7 @@
2978 def test__get_rpc_info_url(self):
2979 maas_url = "http://%s/%s/" % (
2980 factory.make_hostname(), factory.make_name("path"))
2981- self.useFixture(EnvironmentVariable("MAAS_URL", maas_url))
2982+ self.useFixture(ClusterConfigurationFixture(maas_url=maas_url))
2983 expected_rpc_info_url = maas_url + "rpc/"
2984 observed_rpc_info_url = ClusterClientService._get_rpc_info_url()
2985 self.assertThat(observed_rpc_info_url, Equals(expected_rpc_info_url))
2986@@ -550,7 +548,7 @@
2987 def test_update_calls__update_connections(self):
2988 maas_url = "http://%s/%s/" % (
2989 factory.make_hostname(), factory.make_name("path"))
2990- self.useFixture(EnvironmentVariable("MAAS_URL", maas_url))
2991+ self.useFixture(ClusterConfigurationFixture(maas_url=maas_url))
2992 getPage = self.patch(clusterservice, "getPage")
2993 getPage.return_value = succeed(self.example_rpc_info_view_response)
2994 service = ClusterClientService(Clock())
2995@@ -817,10 +815,9 @@
2996
2997 def setUp(self):
2998 super(TestClusterClient, self).setUp()
2999- self.useFixture(EnvironmentVariable(
3000- "MAAS_URL", factory.make_simple_http_url()))
3001- self.useFixture(EnvironmentVariable(
3002- "CLUSTER_UUID", factory.make_UUID().encode("ascii")))
3003+ self.useFixture(ClusterConfigurationFixture(
3004+ maas_url=factory.make_simple_http_url(),
3005+ cluster_uuid=factory.make_UUID()))
3006
3007 def make_running_client(self):
3008 client = clusterservice.ClusterClient(
3009@@ -1220,15 +1217,18 @@
3010
3011 @inlineCallbacks
3012 def test_registerWithRegion_end_to_end(self):
3013+ maas_url = factory.make_simple_http_url()
3014+ cluster_uuid = factory.make_UUID()
3015+ self.useFixture(ClusterConfigurationFixture(
3016+ maas_url=maas_url, cluster_uuid=cluster_uuid))
3017 fixture = self.useFixture(MockLiveClusterToRegionRPCFixture())
3018 protocol, connecting = fixture.makeEventLoop()
3019 self.addCleanup((yield connecting))
3020 yield getRegionClient()
3021 self.assertThat(
3022 protocol.Register, MockCalledOnceWith(
3023- protocol, uuid=get_cluster_uuid(),
3024- networks=discover_networks(),
3025- url=urlparse(get_maas_url())))
3026+ protocol, uuid=cluster_uuid, networks=discover_networks(),
3027+ url=urlparse(maas_url)))
3028
3029
3030 class TestClusterProtocol_ListSupportedArchitectures(MAASTestCase):
3031@@ -1859,11 +1859,7 @@
3032
3033 @inlineCallbacks
3034 def test_happy_path(self):
3035- get_maas_url = self.patch_autospec(tags, "get_maas_url")
3036- get_maas_url.return_value = sentinel.maas_url
3037- get_cluster_uuid = self.patch_autospec(tags, "get_cluster_uuid")
3038- get_cluster_uuid.return_value = sentinel.cluster_uuid
3039-
3040+ self.useFixture(ClusterConfigurationFixture())
3041 # Prevent real work being done, which would involve HTTP calls.
3042 self.patch_autospec(tags, "process_node_tags")
3043
3044
3045=== modified file 'src/provisioningserver/rpc/tests/test_tags.py'
3046--- src/provisioningserver/rpc/tests/test_tags.py 2015-05-29 16:47:37 +0000
3047+++ src/provisioningserver/rpc/tests/test_tags.py 2015-07-03 11:18:28 +0000
3048@@ -27,16 +27,17 @@
3049 sentinel,
3050 )
3051 from provisioningserver.rpc import tags
3052+from provisioningserver.testing.config import ClusterConfigurationFixture
3053
3054
3055 class TestEvaluateTag(MAASTestCase):
3056
3057 def setUp(self):
3058 super(TestEvaluateTag, self).setUp()
3059- get_maas_url = self.patch_autospec(tags, "get_maas_url")
3060- get_maas_url.return_value = sentinel.maas_url
3061- get_cluster_uuid = self.patch_autospec(tags, "get_cluster_uuid")
3062- get_cluster_uuid.return_value = sentinel.cluster_uuid
3063+ self.mock_cluster_uuid = factory.make_UUID()
3064+ self.mock_url = factory.make_simple_http_url()
3065+ self.useFixture(ClusterConfigurationFixture(
3066+ cluster_uuid=self.mock_cluster_uuid, maas_url=self.mock_url))
3067
3068 def test__calls_process_node_tags(self):
3069 credentials = "aaa", "bbb", "ccc"
3070@@ -49,7 +50,7 @@
3071 tag_name=sentinel.tag_name,
3072 tag_definition=sentinel.tag_definition,
3073 tag_nsmap=sentinel.tag_nsmap, client=ANY,
3074- nodegroup_uuid=sentinel.cluster_uuid))
3075+ nodegroup_uuid=self.mock_cluster_uuid))
3076
3077 def test__constructs_client_with_credentials(self):
3078 consumer_key = factory.make_name("ckey")
3079@@ -66,7 +67,7 @@
3080
3081 client = tags.process_node_tags.call_args[1]["client"]
3082 self.assertIsInstance(client, MAASClient)
3083- self.assertEqual(sentinel.maas_url, client.url)
3084+ self.assertEqual(self.mock_url, client.url)
3085 self.assertIsInstance(client.dispatcher, MAASDispatcher)
3086 self.assertIsInstance(client.auth, MAASOAuth)
3087 self.assertThat(tags.MAASOAuth, MockCalledOnceWith(
3088
3089=== modified file 'src/provisioningserver/testing/config.py'
3090--- src/provisioningserver/testing/config.py 2015-05-29 16:47:37 +0000
3091+++ src/provisioningserver/testing/config.py 2015-07-03 11:18:28 +0000
3092@@ -15,10 +15,8 @@
3093 __all__ = [
3094 "BootSourcesFixture",
3095 "ClusterConfigurationFixture",
3096- "ConfigFixture",
3097 "ConfigFixtureBase",
3098 "ConfigurationFixtureBase",
3099- "set_tftp_root",
3100 ]
3101
3102 from os import path
3103@@ -31,7 +29,6 @@
3104 from provisioningserver.config import (
3105 BootSources,
3106 ClusterConfiguration,
3107- Config,
3108 )
3109 import yaml
3110
3111@@ -69,15 +66,6 @@
3112 self.useFixture(EnvironmentVariableFixture(name, value))
3113
3114
3115-class ConfigFixture(ConfigFixtureBase):
3116- """Fixture to substitute for :class:`Config` in tests."""
3117-
3118- schema = Config
3119-
3120- def __init__(self, config=None, name='pserv.yaml'):
3121- super(ConfigFixture, self).__init__(config=config, name=name)
3122-
3123-
3124 class BootSourcesFixture(ConfigFixtureBase):
3125 """Fixture to substitute for :class:`BootSources` in tests.
3126
3127@@ -93,14 +81,6 @@
3128 super(BootSourcesFixture, self).__init__(config=sources, name=name)
3129
3130
3131-def set_tftp_root(tftproot):
3132- """Create a `ConfigFixture` fixture that sets the TFTP root directory.
3133-
3134- Add the resulting fixture to your test using `self.useFixture`.
3135- """
3136- return ConfigFixture({'tftp': {'resource_root': tftproot}})
3137-
3138-
3139 class ConfigurationFixtureBase(Fixture):
3140 """Base class for new-style configuration testing fixtures.
3141
3142
3143=== modified file 'src/provisioningserver/testing/testcase.py'
3144--- src/provisioningserver/testing/testcase.py 2015-05-29 16:47:37 +0000
3145+++ src/provisioningserver/testing/testcase.py 2015-07-03 11:18:28 +0000
3146@@ -16,22 +16,13 @@
3147 'PservTestCase',
3148 ]
3149
3150-from fixtures import EnvironmentVariableFixture
3151 from maastesting import testcase
3152-from maastesting.factory import factory
3153 from twisted.internet import reactor
3154 from twisted.python import threadable
3155
3156
3157 class PservTestCase(testcase.MAASTestCase):
3158
3159- def make_maas_url(self):
3160- return 'http://127.0.0.1/%s' % factory.make_name('path')
3161-
3162- def set_maas_url(self):
3163- self.useFixture(
3164- EnvironmentVariableFixture("MAAS_URL", self.make_maas_url()))
3165-
3166 def register_as_io_thread(self):
3167 """Make the current thread the IO thread.
3168
3169
3170=== removed file 'src/provisioningserver/tests/test_cluster_config.py'
3171--- src/provisioningserver/tests/test_cluster_config.py 2015-05-29 16:47:37 +0000
3172+++ src/provisioningserver/tests/test_cluster_config.py 1970-01-01 00:00:00 +0000
3173@@ -1,48 +0,0 @@
3174-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
3175-# GNU Affero General Public License version 3 (see the file LICENSE).
3176-
3177-"""Tests for `provisioningserver.cluster_config`."""
3178-
3179-from __future__ import (
3180- absolute_import,
3181- print_function,
3182- unicode_literals,
3183- )
3184-
3185-str = None
3186-
3187-__metaclass__ = type
3188-__all__ = []
3189-
3190-from fixtures import EnvironmentVariableFixture
3191-from maastesting.factory import factory
3192-from maastesting.testcase import MAASTestCase
3193-from provisioningserver.cluster_config import (
3194- get_cluster_uuid,
3195- get_cluster_variable,
3196- get_maas_url,
3197-)
3198-
3199-
3200-class TestClusterConfig(MAASTestCase):
3201-
3202- def test_get_cluster_variable_reads_env(self):
3203- var = factory.make_name('variable')
3204- value = factory.make_name('value')
3205- self.useFixture(EnvironmentVariableFixture(var, value))
3206- self.assertEqual(value, get_cluster_variable(var))
3207-
3208- def test_get_cluster_variable_fails_if_not_set(self):
3209- self.assertRaises(
3210- AssertionError,
3211- get_cluster_variable, factory.make_name('nonexistent-variable'))
3212-
3213- def test_get_cluster_uuid_reads_CLUSTER_UUID(self):
3214- uuid = factory.make_name('uuid')
3215- self.useFixture(EnvironmentVariableFixture('CLUSTER_UUID', uuid))
3216- self.assertEqual(uuid, get_cluster_uuid())
3217-
3218- def test_get_maas_url_reads_MAAS_URL(self):
3219- maas_url = factory.make_name('maas_url')
3220- self.useFixture(EnvironmentVariableFixture('MAAS_URL', maas_url))
3221- self.assertEqual(maas_url, get_maas_url())
3222
3223=== modified file 'src/provisioningserver/tests/test_cluster_config_command.py'
3224--- src/provisioningserver/tests/test_cluster_config_command.py 2015-04-09 16:21:27 +0000
3225+++ src/provisioningserver/tests/test_cluster_config_command.py 2015-07-03 11:18:28 +0000
3226@@ -1,7 +1,7 @@
3227 # Copyright 2015 Canonical Ltd. This software is licensed under the
3228 # GNU Affero General Public License version 3 (see the file LICENSE).
3229
3230-"""Tests for `MAAS_URL` configuration update code."""
3231+"""Tests for configuration update code."""
3232
3233 from __future__ import (
3234 absolute_import,
3235@@ -28,12 +28,10 @@
3236 patch,
3237 )
3238 from provisioningserver import cluster_config_command
3239-from provisioningserver.cluster_config import (
3240- CLUSTER_CONFIG,
3241- get_config_cluster_variable,
3242- set_config_cluster_variable,
3243+from provisioningserver.config import (
3244+ ClusterConfiguration,
3245+ UUID_NOT_SET,
3246 )
3247-from provisioningserver.config import UUID_NOT_SET
3248 from provisioningserver.testing.config import ClusterConfigurationFixture
3249 from testtools import ExpectedException
3250
3251@@ -134,23 +132,28 @@
3252 def test_config_set_maas_url_sets_url(self):
3253 expected = factory.make_simple_http_url()
3254 cluster_config_command.run(self.make_args(region_url=expected))
3255- observed = get_config_cluster_variable(CLUSTER_CONFIG.DB_maas_url)
3256+ with ClusterConfiguration.open() as config:
3257+ observed = config.maas_url
3258 self.assertEqual(expected, observed)
3259
3260 def test_config_set_maas_url_without_setting_does_nothing(self):
3261- expected = get_config_cluster_variable(CLUSTER_CONFIG.DB_maas_url)
3262+ with ClusterConfiguration.open() as config:
3263+ expected = config.maas_url
3264 cluster_config_command.run(self.make_args(region_url=None))
3265- observed = get_config_cluster_variable(CLUSTER_CONFIG.DB_maas_url)
3266+ with ClusterConfiguration.open() as config:
3267+ observed = config.maas_url
3268 self.assertEqual(expected, observed)
3269
3270 def test_config_set_cluster_uuid_sets_cluster_uuid(self):
3271 expected = unicode(uuid.uuid4())
3272 cluster_config_command.run(self.make_args(uuid=expected))
3273- observed = get_config_cluster_variable(CLUSTER_CONFIG.DB_cluster_uuid)
3274+ with ClusterConfiguration.open() as config:
3275+ observed = config.cluster_uuid
3276 self.assertEqual(expected, observed)
3277
3278 def get_parsed_uuid_from_config(self):
3279- observed = get_config_cluster_variable(CLUSTER_CONFIG.DB_cluster_uuid)
3280+ with ClusterConfiguration.open() as config:
3281+ observed = config.cluster_uuid
3282 try:
3283 parsed_observed = unicode(uuid.UUID(observed))
3284 except:
3285@@ -160,10 +163,10 @@
3286
3287 def test_config_set_cluster_uuid_without_setting_does_nothing(self):
3288 expected_previous_value = unicode(uuid.uuid4())
3289- set_config_cluster_variable(
3290- CLUSTER_CONFIG.DB_cluster_uuid, expected_previous_value)
3291- observed_previous_value = get_config_cluster_variable(
3292- CLUSTER_CONFIG.DB_cluster_uuid)
3293+ with ClusterConfiguration.open() as config:
3294+ config.cluster_uuid = expected_previous_value
3295+ with ClusterConfiguration.open() as config:
3296+ observed_previous_value = config.cluster_uuid
3297 self.assertEqual(expected_previous_value, observed_previous_value)
3298
3299 cluster_config_command.run(self.make_args(uuid=None))
3300@@ -173,8 +176,8 @@
3301 self.assertEqual(parsed_observed, expected_previous_value)
3302
3303 def test_config_init_creates_initial_cluster_id(self):
3304- observed_default = get_config_cluster_variable(
3305- CLUSTER_CONFIG.DB_cluster_uuid)
3306+ with ClusterConfiguration.open() as config:
3307+ observed_default = config.cluster_uuid
3308 self.assertEqual(UUID_NOT_SET, observed_default)
3309
3310 cluster_config_command.run(self.make_args(init=True))
3311@@ -184,10 +187,10 @@
3312
3313 def test_config_init_when_already_configured_does_nothing(self):
3314 expected_previous_value = unicode(uuid.uuid4())
3315- set_config_cluster_variable(
3316- CLUSTER_CONFIG.DB_cluster_uuid, expected_previous_value)
3317- observed_previous_value = get_config_cluster_variable(
3318- CLUSTER_CONFIG.DB_cluster_uuid)
3319+ with ClusterConfiguration.open() as config:
3320+ config.cluster_uuid = expected_previous_value
3321+ with ClusterConfiguration.open() as config:
3322+ observed_previous_value = config.cluster_uuid
3323 self.assertEqual(expected_previous_value, observed_previous_value)
3324
3325 cluster_config_command.run(self.make_args(init=True))
3326@@ -199,27 +202,29 @@
3327 def test_config_set_tftp_port_sets_tftp_port(self):
3328 expected = factory.pick_port()
3329 cluster_config_command.run(self.make_args(tftp_port=expected))
3330- observed = get_config_cluster_variable(CLUSTER_CONFIG.DB_tftpport)
3331+ with ClusterConfiguration.open() as config:
3332+ observed = config.tftp_port
3333 self.assertEqual(expected, observed)
3334
3335 def test_config_set_tftp_port_without_setting_does_nothing(self):
3336- expected = get_config_cluster_variable(CLUSTER_CONFIG.DB_tftpport)
3337+ with ClusterConfiguration.open() as config:
3338+ expected = config.tftp_port
3339 cluster_config_command.run(self.make_args(tftp_port=None))
3340- observed = get_config_cluster_variable(CLUSTER_CONFIG.DB_tftpport)
3341+ with ClusterConfiguration.open() as config:
3342+ observed = config.tftp_port
3343 self.assertEqual(expected, observed)
3344
3345 def test_config_set_tftp_port_sets_tftp_root(self):
3346 expected = self.make_dir()
3347 cluster_config_command.run(self.make_args(tftp_root=expected))
3348- observed = get_config_cluster_variable(
3349- CLUSTER_CONFIG.DB_tftp_resource_root)
3350+ with ClusterConfiguration.open() as config:
3351+ observed = config.tftp_root
3352 self.assertEqual(expected, observed)
3353
3354 def test_config_set_tftp_root_without_setting_does_nothing(self):
3355- expected = get_config_cluster_variable(
3356- CLUSTER_CONFIG.DB_tftp_resource_root)
3357+ with ClusterConfiguration.open() as config:
3358+ expected = config.tftp_root
3359 cluster_config_command.run(self.make_args(tftp_root=None))
3360- observed = get_config_cluster_variable(
3361- CLUSTER_CONFIG.DB_tftp_resource_root)
3362-
3363+ with ClusterConfiguration.open() as config:
3364+ observed = config.tftp_root
3365 self.assertEqual(expected, observed)
3366
3367=== modified file 'src/provisioningserver/tests/test_config.py'
3368--- src/provisioningserver/tests/test_config.py 2015-06-24 13:27:05 +0000
3369+++ src/provisioningserver/tests/test_config.py 2015-07-03 11:18:28 +0000
3370@@ -15,25 +15,19 @@
3371 __all__ = []
3372
3373 import contextlib
3374-from copy import deepcopy
3375-import errno
3376-from functools import partial
3377-from getpass import getuser
3378-from io import BytesIO
3379 from operator import methodcaller
3380 import os.path
3381 import random
3382 import re
3383 import sqlite3
3384-from textwrap import dedent
3385 from uuid import uuid4
3386
3387 from fixtures import EnvironmentVariableFixture
3388 import formencode
3389 import formencode.validators
3390 from formencode.validators import Invalid
3391-from maastesting import root
3392 from maastesting.factory import factory
3393+from maastesting.fixtures import ImportErrorFixture
3394 from maastesting.matchers import (
3395 MockCalledOnceWith,
3396 MockNotCalled,
3397@@ -41,11 +35,7 @@
3398 from maastesting.testcase import MAASTestCase
3399 from mock import sentinel
3400 from provisioningserver.config import (
3401- BootSources,
3402 ClusterConfiguration,
3403- Config,
3404- ConfigBase,
3405- ConfigMeta,
3406 Configuration,
3407 ConfigurationDatabase,
3408 ConfigurationFile,
3409@@ -53,20 +43,18 @@
3410 ConfigurationOption,
3411 Directory,
3412 ExtendedURL,
3413+ is_dev_environment,
3414 UUID,
3415 )
3416 from provisioningserver.path import get_path
3417-from provisioningserver.testing.config import ConfigFixtureBase
3418+from provisioningserver.testing.config import ClusterConfigurationFixture
3419 from provisioningserver.utils.fs import FileLockProxy
3420 from testtools import ExpectedException
3421 from testtools.matchers import (
3422- DirExists,
3423 FileContains,
3424 FileExists,
3425 Is,
3426- MatchesException,
3427 MatchesStructure,
3428- Raises,
3429 )
3430 from twisted.python.filepath import FilePath
3431 import yaml
3432@@ -224,444 +212,6 @@
3433 "url: %s" % url)
3434
3435
3436-class ExampleConfig(ConfigBase, formencode.Schema):
3437- """An example configuration schema.
3438-
3439- It derives from :class:`ConfigBase` and has a metaclass derived from
3440- :class:`ConfigMeta`, just as a "real" schema must.
3441- """
3442-
3443- class __metaclass__(ConfigMeta):
3444- envvar = "MAAS_TESTING_SETTINGS"
3445- default = "example.yaml"
3446-
3447- something = formencode.validators.String(if_missing="*missing*")
3448-
3449-
3450-class ExampleConfigFixture(ConfigFixtureBase):
3451- """A fixture to help with testing :class:`ExampleConfig`."""
3452-
3453- schema = ExampleConfig
3454-
3455-
3456-class TestConfigFixtureBase(MAASTestCase):
3457- """Tests for `ConfigFixtureBase`."""
3458-
3459- def exercise_fixture(self, fixture):
3460- # ConfigFixtureBase arranges a minimal configuration on disk,
3461- # and exports the configuration filename to the environment so
3462- # that subprocesses can find it.
3463- with fixture:
3464- self.assertThat(fixture.dir, DirExists())
3465- self.assertThat(fixture.filename, FileExists())
3466- self.assertEqual(
3467- {fixture.schema.envvar: fixture.filename},
3468- fixture.environ)
3469- self.assertEqual(
3470- fixture.filename, os.environ[fixture.schema.envvar])
3471- with open(fixture.filename, "rb") as stream:
3472- self.assertEqual(fixture.config, yaml.safe_load(stream))
3473-
3474- def test_use_minimal(self):
3475- # With no arguments, ConfigFixtureBase arranges a minimal
3476- # configuration.
3477- fixture = ExampleConfigFixture()
3478- self.exercise_fixture(fixture)
3479-
3480- def test_use_with_config(self):
3481- # Given a configuration, ConfigFixtureBase can arrange a minimal
3482- # global configuration with the additional options merged in.
3483- something = self.getUniqueString("something")
3484- fixture = ExampleConfigFixture({"something": something})
3485- self.assertEqual(something, fixture.config["something"])
3486- self.exercise_fixture(fixture)
3487-
3488-
3489-class TestConfigMeta_DEFAULT_FILENAME(MAASTestCase):
3490- """Tests for `provisioningserver.config.ConfigBase.DEFAULT_FILENAME`."""
3491-
3492- def set_envvar(self, filepath=None):
3493- """Continue this test with a given environment variable."""
3494- self.useFixture(EnvironmentVariableFixture(
3495- ExampleConfig.envvar, filepath))
3496-
3497- def set_MAAS_CONFIG_DIR(self, dirpath=None):
3498- """Continue this test with a given `MAAS_CONFIG_DIR`."""
3499- self.useFixture(EnvironmentVariableFixture("MAAS_CONFIG_DIR", dirpath))
3500-
3501- def make_config(self):
3502- """Create a config file in a directory of its own."""
3503- return self.make_file(name=ExampleConfig.default)
3504-
3505- def test_gets_filename_from_MAAS_PROVISIONING_SETTNGS(self):
3506- dummy_filename = factory.make_name("config")
3507- self.set_MAAS_CONFIG_DIR(None)
3508- self.set_envvar(dummy_filename)
3509- self.assertEqual(dummy_filename, ExampleConfig.DEFAULT_FILENAME)
3510-
3511- def test_falls_back_to_MAAS_CONFIG_DIR(self):
3512- config_file = self.make_config()
3513- self.set_MAAS_CONFIG_DIR(os.path.dirname(config_file))
3514- self.set_envvar(None)
3515- self.assertEqual(config_file, ExampleConfig.DEFAULT_FILENAME)
3516-
3517- def test_MAAS_PROVISIONING_SETTINGS_trumps_MAAS_CONFIG_DIR(self):
3518- provisioning_settings = factory.make_name("config")
3519- self.set_MAAS_CONFIG_DIR(os.path.dirname(self.make_config()))
3520- self.set_envvar(provisioning_settings)
3521- self.assertEqual(
3522- provisioning_settings,
3523- ExampleConfig.DEFAULT_FILENAME)
3524-
3525- def test_defaults_to_global_config(self):
3526- self.set_MAAS_CONFIG_DIR(None)
3527- self.set_envvar(None)
3528- self.assertEqual(
3529- '/etc/maas/%s' % ExampleConfig.default,
3530- ExampleConfig.DEFAULT_FILENAME)
3531-
3532- def test_set(self):
3533- dummy_filename = factory.make_name("config")
3534- ExampleConfig.DEFAULT_FILENAME = dummy_filename
3535- self.assertEqual(dummy_filename, ExampleConfig.DEFAULT_FILENAME)
3536-
3537- def test_delete(self):
3538- self.set_MAAS_CONFIG_DIR(None)
3539- self.set_envvar(None)
3540- ExampleConfig.DEFAULT_FILENAME = factory.make_name("config")
3541- del ExampleConfig.DEFAULT_FILENAME
3542- # The filename reverts; see test_get_with_environment_empty.
3543- self.assertEqual(
3544- "/etc/maas/%s" % ExampleConfig.default,
3545- ExampleConfig.DEFAULT_FILENAME)
3546- # The delete does not fail when called multiple times.
3547- del ExampleConfig.DEFAULT_FILENAME
3548-
3549-
3550-class TestConfigBase(MAASTestCase):
3551- """Tests for `provisioningserver.config.ConfigBase`."""
3552-
3553- def make_config_data(self):
3554- """Return random config data for `ExampleConfig`."""
3555- return {'something': factory.make_name('value')}
3556-
3557- def make_config_file(self, name=None, data=None):
3558- """Write a YAML config file, and return its path."""
3559- if name is None:
3560- name = factory.make_name('config') + '.yaml'
3561- if data is None:
3562- data = self.make_config_data()
3563- return self.make_file(name=name, contents=yaml.safe_dump(data))
3564-
3565- def test_get_defaults_returns_default_config(self):
3566- # The default configuration is production-ready.
3567- observed = ExampleConfig.get_defaults()
3568- self.assertEqual({"something": "*missing*"}, observed)
3569-
3570- def test_get_defaults_ignores_settings(self):
3571- self.useFixture(ExampleConfigFixture(self.make_config_data()))
3572- observed = ExampleConfig.get_defaults()
3573- self.assertEqual({"something": "*missing*"}, observed)
3574-
3575- def test_parse(self):
3576- # Configuration can be parsed from a snippet of YAML.
3577- observed = ExampleConfig.parse(b'something: "important"\n')
3578- self.assertEqual("important", observed["something"])
3579-
3580- def test_load(self):
3581- # Configuration can be loaded and parsed from a file.
3582- config = dedent("""
3583- something: "important"
3584- """)
3585- filename = self.make_file(contents=config)
3586- observed = ExampleConfig.load(filename)
3587- self.assertEqual({"something": "important"}, observed)
3588-
3589- def test_load_defaults_to_default_filename(self):
3590- data = self.make_config_data()
3591- filename = self.make_config_file(name='config.yaml', data=data)
3592- self.patch(ExampleConfig, 'DEFAULT_FILENAME', filename)
3593- self.assertEqual(data, ExampleConfig.load())
3594-
3595- def test_load_from_cache_loads_config(self):
3596- data = self.make_config_data()
3597- filename = self.make_config_file(data=data)
3598- self.assertEqual(data, ExampleConfig.load_from_cache(filename))
3599-
3600- def test_load_from_cache_uses_defaults(self):
3601- filename = self.make_file(contents='')
3602- self.assertEqual(
3603- ExampleConfig.get_defaults(),
3604- ExampleConfig.load_from_cache(filename))
3605-
3606- def test_load_from_cache_caches_each_file_separately(self):
3607- config1 = self.make_file(contents=yaml.safe_dump({'something': "1"}))
3608- config2 = self.make_file(contents=yaml.safe_dump({'something': "2"}))
3609-
3610- self.assertEqual(
3611- {"something": "1"},
3612- ExampleConfig.load_from_cache(config1))
3613- self.assertEqual(
3614- {"something": "2"},
3615- ExampleConfig.load_from_cache(config2))
3616-
3617- def test_load_from_cache_reloads_from_cache_not_from_file(self):
3618- # A config loaded by Config.load_from_cache() is never reloaded.
3619- filename = self.make_config_file()
3620- config_before = ExampleConfig.load_from_cache(filename)
3621- os.unlink(filename)
3622- config_after = ExampleConfig.load_from_cache(filename)
3623- self.assertEqual(config_before, config_after)
3624-
3625- def test_load_from_cache_caches_immutable_copy(self):
3626- filename = self.make_config_file()
3627-
3628- first_load = ExampleConfig.load_from_cache(filename)
3629- second_load = ExampleConfig.load_from_cache(filename)
3630-
3631- self.assertEqual(first_load, second_load)
3632- self.assertIsNot(first_load, second_load)
3633- first_load['something'] = factory.make_name('newthing')
3634- self.assertNotEqual(first_load['something'], second_load['something'])
3635-
3636- def test_flush_cache_without_filename_empties_cache(self):
3637- filename = self.make_config_file()
3638- ExampleConfig.load_from_cache(filename)
3639- os.unlink(filename)
3640- ExampleConfig.flush_cache()
3641- error = self.assertRaises(
3642- IOError,
3643- ExampleConfig.load_from_cache, filename)
3644- self.assertEqual(errno.ENOENT, error.errno)
3645-
3646- def test_flush_cache_flushes_specific_file(self):
3647- filename = self.make_config_file()
3648- ExampleConfig.load_from_cache(filename)
3649- os.unlink(filename)
3650- ExampleConfig.flush_cache(filename)
3651- error = self.assertRaises(
3652- IOError,
3653- ExampleConfig.load_from_cache, filename)
3654- self.assertEqual(errno.ENOENT, error.errno)
3655-
3656- def test_flush_cache_retains_other_files(self):
3657- flushed_file = self.make_config_file()
3658- cached_file = self.make_config_file()
3659- ExampleConfig.load_from_cache(flushed_file)
3660- cached_config = ExampleConfig.load_from_cache(cached_file)
3661- os.unlink(cached_file)
3662- ExampleConfig.flush_cache(flushed_file)
3663- self.assertEqual(
3664- cached_config,
3665- ExampleConfig.load_from_cache(cached_file))
3666-
3667- def test_flush_cache_ignores_uncached_files(self):
3668- data = self.make_config_data()
3669- filename = self.make_config_file(data=data)
3670- ExampleConfig.flush_cache(filename)
3671- self.assertEqual(data, ExampleConfig.load_from_cache(filename))
3672-
3673- def test_field(self):
3674- self.assertIs(ExampleConfig, ExampleConfig.field())
3675- self.assertIs(
3676- ExampleConfig.fields["something"],
3677- ExampleConfig.field("something"))
3678-
3679- def test_save_and_load_interoperate(self):
3680- something = self.getUniqueString()
3681- saved_file = self.make_file()
3682-
3683- ExampleConfig.save({'something': something}, saved_file)
3684- loaded_config = ExampleConfig.load(saved_file)
3685- self.assertEqual(something, loaded_config['something'])
3686-
3687- def test_save_saves_yaml_file(self):
3688- config = {'something': self.getUniqueString()}
3689- saved_file = self.make_file()
3690-
3691- ExampleConfig.save(config, saved_file)
3692-
3693- with open(saved_file, 'rb') as written_file:
3694- loaded_config = yaml.safe_load(written_file)
3695- self.assertEqual(config, loaded_config)
3696-
3697- def test_save_defaults_to_default_filename(self):
3698- something = self.getUniqueString()
3699- filename = self.make_file(name="config.yaml")
3700- self.patch(ExampleConfig, 'DEFAULT_FILENAME', filename)
3701-
3702- ExampleConfig.save({'something': something})
3703-
3704- self.assertEqual(
3705- {'something': something},
3706- ExampleConfig.load(filename))
3707-
3708- def test_create_backup_creates_backup(self):
3709- something = self.getUniqueString()
3710- filename = self.make_file(name="config.yaml")
3711- config = {'something': something}
3712- yaml_config = yaml.safe_dump(config)
3713- self.patch(ExampleConfig, 'DEFAULT_FILENAME', filename)
3714- ExampleConfig.save(config)
3715-
3716- ExampleConfig.create_backup('test')
3717-
3718- backup_name = "%s.%s.bak" % (filename, 'test')
3719- self.assertThat(backup_name, FileContains(yaml_config))
3720-
3721-
3722-class TestConfig(MAASTestCase):
3723- """Tests for `provisioningserver.config.Config`."""
3724-
3725- default_production_config = {
3726- 'broker': {
3727- 'host': 'localhost',
3728- 'port': 5673,
3729- 'username': getuser(),
3730- 'password': 'test',
3731- 'vhost': '/',
3732- },
3733- 'logfile': 'pserv.log',
3734- 'oops': {
3735- 'directory': '',
3736- 'reporter': '',
3737- },
3738- 'rpc': {},
3739- 'tftp': {
3740- 'generator': 'http://localhost/MAAS/api/1.0/pxeconfig/',
3741- 'port': 69,
3742- # The "root" setting is obsolete; resource_root replaces it.
3743- 'root': "/var/lib/maas/tftp",
3744- 'resource_root': "/var/lib/maas/boot-resources/current/",
3745- },
3746- # Legacy section. Became unused in MAAS 1.5.
3747- 'boot': {
3748- 'architectures': None,
3749- 'ephemeral': {
3750- 'images_directory': None,
3751- 'releases': None,
3752- },
3753- },
3754- }
3755-
3756- default_development_config = deepcopy(default_production_config)
3757- default_development_config.update(logfile="/dev/null")
3758- default_development_config["tftp"].update(
3759- port=5244, generator="http://localhost:5240/MAAS/api/1.0/pxeconfig/")
3760-
3761- def test_get_defaults_returns_default_config(self):
3762- # The default configuration is production-ready.
3763- observed = Config.get_defaults()
3764- self.assertEqual(self.default_production_config, observed)
3765-
3766- def test_load_example(self):
3767- # The example configuration is designed for development.
3768- filename = os.path.join(root, "etc", "maas", "pserv.yaml")
3769- self.assertEqual(
3770- self.default_development_config,
3771- Config.load(filename))
3772-
3773- def test_oops_directory_without_reporter(self):
3774- # It is an error to omit the OOPS reporter if directory is specified.
3775- config = (
3776- 'oops:\n'
3777- ' directory: /tmp/oops\n'
3778- )
3779- expected = MatchesException(
3780- formencode.Invalid, "oops: You must give a value for reporter")
3781- self.assertThat(
3782- partial(Config.parse, config),
3783- Raises(expected))
3784-
3785- def test_accepts_1_4_config_file(self):
3786- # A config file that was valid with MAAS 1.4 still loads, even though
3787- # its "boot" section is no longer used.
3788- broker_password = factory.make_name('pass')
3789- config = Config.parse(dedent("""\
3790- logfile: "/dev/null"
3791- oops:
3792- directory: "logs/oops"
3793- reporter: "maas-pserv"
3794- broker:
3795- host: "localhost"
3796- port: 5673
3797- username: brokeruser
3798- password: "%s"
3799- vhost: "/"
3800- tftp:
3801- root: /var/lib/maas/tftp
3802- port: 5244
3803- generator: http://localhost:5240/api/1.0/pxeconfig/
3804- boot:
3805- architectures: ['i386', 'armhf']
3806- ephemeral:
3807- images_directory: /var/lib/maas/ephemeral
3808- releases: ['precise', 'saucy']
3809- """) % broker_password)
3810- # This does not fail.
3811- self.assertEqual(broker_password, config['broker']['password'])
3812-
3813-
3814-class TestBootSources(MAASTestCase):
3815- """Tests for `provisioningserver.config.BootSources`."""
3816-
3817- default_source = {
3818- 'url': (
3819- 'http://maas.ubuntu.com/images/ephemeral-v2/releases/'
3820- ),
3821- 'keyring': (
3822- '/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg'),
3823- 'keyring_data': None,
3824- 'selections': [
3825- {
3826- 'os': '*',
3827- 'release': '*',
3828- 'labels': ['*'],
3829- 'arches': ['*'],
3830- 'subarches': ['*'],
3831- },
3832- ],
3833- }
3834-
3835- def make_source(self):
3836- """Create a dict defining an arbitrary `BootSource`."""
3837- return {
3838- 'url': 'http://example.com/' + factory.make_name('path'),
3839- 'keyring': factory.make_name('keyring'),
3840- 'keyring_data': factory.make_string(),
3841- 'selections': [{
3842- 'os': factory.make_name('os'),
3843- 'release': factory.make_name('release'),
3844- 'labels': [factory.make_name('label')],
3845- 'arches': [factory.make_name('arch')],
3846- 'subarches': [factory.make_name('sub') for _ in range(3)],
3847- }],
3848- }
3849-
3850- def test_parse_parses_source(self):
3851- sources = [self.make_source()]
3852- self.assertEqual(
3853- sources,
3854- BootSources.parse(BytesIO(yaml.safe_dump(sources))))
3855-
3856- def test_parse_parses_multiple_sources(self):
3857- sources = [self.make_source() for _ in range(2)]
3858- self.assertEqual(
3859- sources,
3860- BootSources.parse(BytesIO(yaml.safe_dump(sources))))
3861-
3862- def test_parse_uses_defaults(self):
3863- self.assertEqual(
3864- [self.default_source],
3865- BootSources.parse(BytesIO(b'[{}]')))
3866-
3867- def test_load_parses_file(self):
3868- sources = [self.make_source()]
3869- self.assertEqual(
3870- sources,
3871- BootSources.load(self.make_file(contents=yaml.safe_dump(sources))))
3872-
3873-
3874 ###############################################################################
3875 # New configuration API follows.
3876 ###############################################################################
3877@@ -1153,9 +703,12 @@
3878 self.assertEqual({"tftp_port": example_port}, config.store)
3879
3880 def test_default_tftp_root(self):
3881+ maas_root = os.getenv("MAAS_ROOT")
3882+ self.assertIsNotNone(maas_root)
3883 config = ClusterConfiguration({})
3884 self.assertEqual(
3885- "/var/lib/maas/boot-resources/current/", config.tftp_root)
3886+ os.path.join(maas_root, "var/lib/maas/boot-resources/current"),
3887+ config.tftp_root)
3888
3889 def test_set_and_get_tftp_root(self):
3890 config = ClusterConfiguration({})
3891@@ -1164,3 +717,62 @@
3892 self.assertEqual(example_dir, config.tftp_root)
3893 # It's also stored in the configuration database.
3894 self.assertEqual({"tftp_root": example_dir}, config.store)
3895+
3896+ def test_default_cluster_uuid(self):
3897+ config = ClusterConfiguration({})
3898+ self.assertEqual("** UUID NOT SET **", config.cluster_uuid)
3899+
3900+ def test_set_and_get_cluster_uuid(self):
3901+ example_uuid = uuid4()
3902+ config = ClusterConfiguration({})
3903+ config.cluster_uuid = example_uuid
3904+ self.assertEqual(unicode(example_uuid), config.cluster_uuid)
3905+ # It's also stored in the configuration database.
3906+ self.assertEqual({"cluster_uuid": unicode(example_uuid)}, config.store)
3907+
3908+
3909+class TestClusterConfigurationTFTPGeneratorURL(MAASTestCase):
3910+ """Tests for `ClusterConfiguration.tftp_generator_url`."""
3911+
3912+ def test__is_relative_to_maas_url(self):
3913+ random_url = factory.make_simple_http_url()
3914+ self.useFixture(ClusterConfigurationFixture(maas_url=random_url))
3915+ with ClusterConfiguration.open() as configuration:
3916+ self.assertEqual(
3917+ random_url + "/api/1.0/pxeconfig/",
3918+ configuration.tftp_generator_url)
3919+
3920+ def test__strips_trailing_slashes_from_maas_url(self):
3921+ random_url = factory.make_simple_http_url(path="foobar/")
3922+ self.useFixture(ClusterConfigurationFixture(maas_url=random_url))
3923+ with ClusterConfiguration.open() as configuration:
3924+ self.assertEqual(
3925+ random_url.rstrip("/") + "/api/1.0/pxeconfig/",
3926+ configuration.tftp_generator_url)
3927+
3928+
3929+class TestClusterConfigurationGRUBRoot(MAASTestCase):
3930+ """Tests for `ClusterConfiguration.grub_root`."""
3931+
3932+ def test__is_relative_to_tftp_root_without_trailing_slash(self):
3933+ random_dir = self.make_dir().rstrip("/")
3934+ self.useFixture(ClusterConfigurationFixture(tftp_root=random_dir))
3935+ with ClusterConfiguration.open() as configuration:
3936+ self.assertEqual(random_dir + "/grub", configuration.grub_root)
3937+
3938+ def test__is_relative_to_tftp_root_with_trailing_slash(self):
3939+ random_dir = self.make_dir().rstrip("/") + "/"
3940+ self.useFixture(ClusterConfigurationFixture(tftp_root=random_dir))
3941+ with ClusterConfiguration.open() as configuration:
3942+ self.assertEqual(random_dir + "grub", configuration.grub_root)
3943+
3944+
3945+class TestConfig(MAASTestCase):
3946+ """Tests for `maasserver.config`."""
3947+
3948+ def test_is_dev_environment_returns_false(self):
3949+ self.useFixture(ImportErrorFixture('maastesting', 'root'))
3950+ self.assertFalse(is_dev_environment())
3951+
3952+ def test_is_dev_environment_returns_true(self):
3953+ self.assertTrue(is_dev_environment())
3954
3955=== removed file 'src/provisioningserver/tests/test_configure_maas_url.py'
3956--- src/provisioningserver/tests/test_configure_maas_url.py 2015-05-29 16:47:37 +0000
3957+++ src/provisioningserver/tests/test_configure_maas_url.py 1970-01-01 00:00:00 +0000
3958@@ -1,351 +0,0 @@
3959-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
3960-# GNU Affero General Public License version 3 (see the file LICENSE).
3961-
3962-"""Tests for `MAAS_URL` configuration update code."""
3963-
3964-from __future__ import (
3965- absolute_import,
3966- print_function,
3967- unicode_literals,
3968- )
3969-
3970-str = None
3971-
3972-__metaclass__ = type
3973-__all__ = []
3974-
3975-from argparse import ArgumentParser
3976-from random import randint
3977-from textwrap import dedent
3978-
3979-from maastesting.factory import factory
3980-from maastesting.matchers import (
3981- MockAnyCall,
3982- MockCalledOnceWith,
3983-)
3984-from maastesting.testcase import MAASTestCase
3985-from mock import (
3986- ANY,
3987- Mock,
3988-)
3989-from provisioningserver import configure_maas_url
3990-from provisioningserver.configure_maas_url import substitute_pserv_yaml_line
3991-from testtools.matchers import (
3992- FileContains,
3993- StartsWith,
3994-)
3995-
3996-
3997-class TestRewriteConfigFile(MAASTestCase):
3998-
3999- def test__rewrites_file(self):
4000- path = self.make_file(contents='foo\n')
4001- configure_maas_url.rewrite_config_file(path, lambda line: 'bar')
4002- self.assertThat(path, FileContains('bar\n'))
4003-
4004- def test__sets_access_permissions(self):
4005- writer = self.patch(configure_maas_url, 'atomic_write')
4006- mode = 0215
4007- path = self.make_file()
4008- configure_maas_url.rewrite_config_file(
4009- path, lambda line: line, mode=mode)
4010- self.assertThat(writer, MockCalledOnceWith(ANY, path, mode=mode))
4011-
4012- def test__preserves_trailing_newline(self):
4013- path = self.make_file(contents='x\n')
4014- configure_maas_url.rewrite_config_file(path, lambda line: line)
4015- self.assertThat(path, FileContains('x\n'))
4016-
4017- def test__ensures_trailing_newline(self):
4018- path = self.make_file(contents='x')
4019- configure_maas_url.rewrite_config_file(path, lambda line: line)
4020- self.assertThat(path, FileContains('x\n'))
4021-
4022-
4023-class TestUpdateMAASClusterConf(MAASTestCase):
4024-
4025- def patch_file(self, content):
4026- """Inject a fake `/etc/maas/maas_cluster.conf`."""
4027- path = self.make_file(name='maas_cluster.conf', contents=content)
4028- self.patch(configure_maas_url, 'MAAS_CLUSTER_CONF', path)
4029- return path
4030-
4031- def test__updates_realistic_file(self):
4032- config_file = self.patch_file(dedent("""\
4033- # Leading comments.
4034- MAAS_URL="http://10.9.8.7/MAAS"
4035- CLUSTER_UUID="5d02950e-6318-8195-ac3e-e6ccb12673c5"
4036- """))
4037- configure_maas_url.update_maas_cluster_conf('http://1.2.3.4/MAAS')
4038- self.assertThat(
4039- config_file,
4040- FileContains(dedent("""\
4041- # Leading comments.
4042- MAAS_URL="http://1.2.3.4/MAAS"
4043- CLUSTER_UUID="5d02950e-6318-8195-ac3e-e6ccb12673c5"
4044- """)))
4045-
4046- def test__updates_quoted_value(self):
4047- old_url = factory.make_url()
4048- new_url = factory.make_url()
4049- config_file = self.patch_file('MAAS_URL="%s"\n' % old_url)
4050- configure_maas_url.update_maas_cluster_conf(new_url)
4051- self.assertThat(
4052- config_file,
4053- FileContains('MAAS_URL="%s"\n' % new_url))
4054-
4055- def test__updates_unquoted_value(self):
4056- old_url = factory.make_url()
4057- new_url = factory.make_url()
4058- config_file = self.patch_file('MAAS_URL=%s\n' % old_url)
4059- configure_maas_url.update_maas_cluster_conf(new_url)
4060- self.assertThat(
4061- config_file,
4062- FileContains('MAAS_URL="%s"\n' % new_url))
4063-
4064- def test__leaves_other_lines_unchanged(self):
4065- old_content = '#MAAS_URL="%s"\n' % factory.make_url()
4066- config_file = self.patch_file(old_content)
4067- configure_maas_url.update_maas_cluster_conf(factory.make_url())
4068- self.assertThat(config_file, FileContains(old_content))
4069-
4070-
4071-class TestExtractHost(MAASTestCase):
4072-
4073- def test__extracts_hostname(self):
4074- host = factory.make_name('host').lower()
4075- port = factory.pick_port()
4076- self.assertEqual(
4077- host,
4078- configure_maas_url.extract_host('http://%s/path' % host))
4079- self.assertEqual(
4080- host,
4081- configure_maas_url.extract_host('http://%s:%d' % (host, port)))
4082-
4083- def test__extracts_IPv4_address(self):
4084- host = factory.make_ipv4_address()
4085- port = factory.pick_port()
4086- self.assertEqual(
4087- host,
4088- configure_maas_url.extract_host('http://%s' % host))
4089- self.assertEqual(
4090- host,
4091- configure_maas_url.extract_host('http://%s:%d' % (host, port)))
4092-
4093- def test__extracts_IPv6_address(self):
4094- host = factory.make_ipv6_address()
4095- port = factory.pick_port()
4096- self.assertEqual(
4097- host,
4098- configure_maas_url.extract_host('http://[%s]' % host))
4099- self.assertEqual(
4100- host,
4101- configure_maas_url.extract_host('http://[%s]:%d' % (host, port)))
4102-
4103- def test__extracts_IPv6_address_with_zone_index(self):
4104- host = (
4105- factory.make_ipv6_address() +
4106- '%25' +
4107- factory.make_name('zone').lower())
4108- port = factory.pick_port()
4109- self.assertEqual(
4110- host,
4111- configure_maas_url.extract_host('http://[%s]' % host))
4112- self.assertEqual(
4113- host,
4114- configure_maas_url.extract_host('http://[%s]:%d' % (host, port)))
4115-
4116-
4117-class TestSubstitutePservYamlLine(MAASTestCase):
4118-
4119- def make_generator_line(self, url):
4120- return " generator: %s" % url
4121-
4122- def test__replaces_hostname_generator_URL(self):
4123- old_host = factory.make_name('old-host')
4124- new_host = factory.make_name('new-host')
4125- input_line = self.make_generator_line('http://%s' % old_host)
4126- self.assertEqual(
4127- self.make_generator_line('http://%s' % new_host),
4128- substitute_pserv_yaml_line(new_host, input_line))
4129-
4130- def test__replaces_IPv4_generator_URL(self):
4131- old_host = factory.make_ipv4_address()
4132- new_host = factory.make_name('new-host')
4133- input_line = self.make_generator_line('http://%s' % old_host)
4134- self.assertEqual(
4135- self.make_generator_line('http://%s' % new_host),
4136- substitute_pserv_yaml_line(new_host, input_line))
4137-
4138- def test__replaces_IPv6_generator_URL(self):
4139- old_host = factory.make_ipv6_address()
4140- new_host = factory.make_name('new-host')
4141- input_line = self.make_generator_line('http://[%s]' % old_host)
4142- self.assertEqual(
4143- self.make_generator_line('http://%s' % new_host),
4144- substitute_pserv_yaml_line(new_host, input_line))
4145-
4146- def test__replaces_IPv6_generator_URL_with_zone_index(self):
4147- old_host = (
4148- factory.make_ipv6_address() +
4149- '%25' +
4150- factory.make_name('zone')
4151- )
4152- new_host = factory.make_name('new-host')
4153- input_line = self.make_generator_line('http://[%s]' % old_host)
4154- self.assertEqual(
4155- self.make_generator_line('http://%s' % new_host),
4156- substitute_pserv_yaml_line(new_host, input_line))
4157-
4158- def test__inserts_IPv6_with_brackets(self):
4159- old_host = factory.make_name('old-host')
4160- new_host = '[%s]' % factory.make_ipv6_address()
4161- input_line = self.make_generator_line('http://%s' % old_host)
4162- self.assertEqual(
4163- self.make_generator_line('http://%s' % new_host),
4164- substitute_pserv_yaml_line(new_host, input_line))
4165-
4166- def test__inserts_IPv6_without_brackets(self):
4167- old_host = factory.make_name('old-host')
4168- new_host = factory.make_ipv6_address()
4169- input_line = self.make_generator_line('http://%s' % old_host)
4170- self.assertEqual(
4171- self.make_generator_line('http://[%s]' % new_host),
4172- substitute_pserv_yaml_line(new_host, input_line))
4173-
4174- def test__preserves_port_after_simple_host(self):
4175- port = factory.pick_port()
4176- old_host = factory.make_name('old-host')
4177- new_host = factory.make_name('new-host')
4178- input_line = self.make_generator_line(
4179- 'http://%s:%d' % (old_host, port))
4180- self.assertEqual(
4181- self.make_generator_line('http://%s:%d' % (new_host, port)),
4182- substitute_pserv_yaml_line(new_host, input_line))
4183-
4184- def test__preserves_port_with_IPv6(self):
4185- port = factory.pick_port()
4186- old_host = factory.make_ipv6_address()
4187- new_host = factory.make_name('new-host')
4188- input_line = self.make_generator_line(
4189- 'http://[%s]:%d' % (old_host, port))
4190- self.assertEqual(
4191- self.make_generator_line('http://%s:%d' % (new_host, port)),
4192- substitute_pserv_yaml_line(new_host, input_line))
4193-
4194- def test__preserves_port_with_IPv6_and_zone_index(self):
4195- port = factory.pick_port()
4196- old_host = (
4197- factory.make_ipv6_address() +
4198- '%25' +
4199- factory.make_name('zone')
4200- )
4201- new_host = factory.make_name('new-host')
4202- input_line = self.make_generator_line(
4203- 'http://[%s]:%d' % (old_host, port))
4204- self.assertEqual(
4205- self.make_generator_line('http://%s:%d' % (new_host, port)),
4206- substitute_pserv_yaml_line(new_host, input_line))
4207-
4208- def test__preserves_other_line(self):
4209- line = '#' + self.make_generator_line(factory.make_url())
4210- self.assertEqual(
4211- line,
4212- substitute_pserv_yaml_line(factory.make_name('host'), line))
4213-
4214- def test__preserves_indentation(self):
4215- spaces = ' ' * randint(0, 10)
4216- input_line = spaces + 'generator: %s' % factory.make_url()
4217- output_line = substitute_pserv_yaml_line(
4218- factory.make_name('host'), input_line)
4219- self.assertThat(output_line, StartsWith(spaces + 'generator:'))
4220-
4221- def test__preserves_trailing_comments(self):
4222- comment = " # Trailing comment."
4223- old_host = factory.make_name('old-host')
4224- new_host = factory.make_name('new-host')
4225- input_line = self.make_generator_line('http://%s' % old_host) + comment
4226- self.assertEqual(
4227- self.make_generator_line('http://%s' % new_host) + comment,
4228- substitute_pserv_yaml_line(new_host, input_line))
4229-
4230-
4231-class TestUpdatePservYaml(MAASTestCase):
4232-
4233- def patch_file(self, content):
4234- """Inject a fake `/etc/maas/pserv.yaml`."""
4235- path = self.make_file(name='pserv.yaml', contents=content)
4236- self.patch(configure_maas_url, 'PSERV_YAML', path)
4237- return path
4238-
4239- def test__updates_realistic_file(self):
4240- old_host = factory.make_name('old-host')
4241- new_host = factory.make_name('new-host')
4242- config_file = self.patch_file(dedent("""\
4243- ## TFTP configuration.
4244- tftp:
4245- ## The URL to be contacted to generate PXE configurations.
4246- generator: http://%s/MAAS/api/1.0/pxeconfig/
4247- """) % old_host)
4248- configure_maas_url.update_pserv_yaml(new_host)
4249- self.assertThat(
4250- config_file,
4251- FileContains(dedent("""\
4252- ## TFTP configuration.
4253- tftp:
4254- ## The URL to be contacted to generate PXE configurations.
4255- generator: http://%s/MAAS/api/1.0/pxeconfig/
4256- """) % new_host))
4257-
4258-
4259-class TestAddArguments(MAASTestCase):
4260-
4261- def test__accepts_maas_url(self):
4262- url = factory.make_url()
4263- parser = ArgumentParser()
4264- configure_maas_url.add_arguments(parser)
4265- args = parser.parse_args([url])
4266- self.assertEqual(url, args.maas_url)
4267-
4268-
4269-class TestRun(MAASTestCase):
4270-
4271- def make_args(self, maas_url):
4272- args = Mock()
4273- args.maas_url = maas_url
4274- return args
4275-
4276- def patch_read_file(self):
4277- return self.patch(configure_maas_url, 'read_text_file')
4278-
4279- def patch_write_file(self):
4280- return self.patch(configure_maas_url, 'atomic_write')
4281-
4282- def test__updates_maas_cluster_conf(self):
4283- reader = self.patch_read_file()
4284- writer = self.patch_write_file()
4285- url = factory.make_url()
4286- configure_maas_url.run(self.make_args(url))
4287- self.assertThat(reader, MockAnyCall('/etc/maas/maas_cluster.conf'))
4288- self.assertThat(
4289- writer,
4290- MockAnyCall(ANY, '/etc/maas/maas_cluster.conf', mode=0640))
4291-
4292- def test__updates_pserv_yaml(self):
4293- reader = self.patch_read_file()
4294- writer = self.patch_write_file()
4295- url = factory.make_url()
4296- configure_maas_url.run(self.make_args(url))
4297- self.assertThat(reader, MockAnyCall('/etc/maas/pserv.yaml'))
4298- self.assertThat(
4299- writer,
4300- MockAnyCall(ANY, '/etc/maas/pserv.yaml', mode=0644))
4301-
4302- def test__passes_host_to_update_pserv_yaml(self):
4303- self.patch_read_file()
4304- self.patch_write_file()
4305- update_pserv_yaml = self.patch(configure_maas_url, 'update_pserv_yaml')
4306- host = factory.make_name('host').lower()
4307- url = factory.make_url(netloc=host)
4308- configure_maas_url.run(self.make_args(url))
4309- self.assertThat(update_pserv_yaml, MockCalledOnceWith(host))
4310
4311=== modified file 'src/provisioningserver/tests/test_diskless.py'
4312--- src/provisioningserver/tests/test_diskless.py 2015-05-29 16:47:37 +0000
4313+++ src/provisioningserver/tests/test_diskless.py 2015-07-03 11:18:28 +0000
4314@@ -24,10 +24,8 @@
4315 )
4316 from maastesting.testcase import MAASTestCase
4317 from mock import sentinel
4318-from provisioningserver import (
4319- config,
4320- diskless,
4321-)
4322+from provisioningserver import diskless
4323+from provisioningserver.config import ClusterConfiguration
4324 from provisioningserver.diskless import (
4325 compose_diskless_link_path,
4326 compose_diskless_tgt_config,
4327@@ -54,6 +52,7 @@
4328 BOOT_IMAGE_PURPOSE,
4329 OperatingSystemRegistry,
4330 )
4331+from provisioningserver.testing.config import ClusterConfigurationFixture
4332 from provisioningserver.testing.os import FakeOS
4333 from provisioningserver.utils.testing import RegistryFixture
4334 from testtools.matchers import (
4335@@ -71,13 +70,17 @@
4336
4337 def setUp(self):
4338 super(DisklessTestMixin, self).setUp()
4339+ self.useFixture(ClusterConfigurationFixture())
4340 # Ensure the global registry is empty for each test run.
4341 self.useFixture(RegistryFixture())
4342
4343 def configure_resource_storage(self):
4344 resource_dir = self.make_dir()
4345 os.mkdir(os.path.join(resource_dir, 'diskless'))
4346- self.patch(config, 'BOOT_RESOURCES_STORAGE', resource_dir)
4347+ current_dir = os.path.join(resource_dir, 'current') + '/'
4348+ os.mkdir(current_dir)
4349+ with ClusterConfiguration.open() as config:
4350+ config.tftp_root = current_dir
4351 return resource_dir
4352
4353 def configure_diskless_storage(self):
4354@@ -113,11 +116,14 @@
4355 self.patch(diskless, 'reload_diskless_tgt')
4356
4357
4358-class TestHelpers(MAASTestCase, DisklessTestMixin):
4359+class TestHelpers(DisklessTestMixin, MAASTestCase):
4360
4361 def test_get_diskless_store(self):
4362- storage_dir = factory.make_name('storage')
4363- self.patch(config, 'BOOT_RESOURCES_STORAGE', storage_dir)
4364+ storage_dir = self.make_dir()
4365+ current_dir = os.path.join(storage_dir, 'current') + '/'
4366+ os.mkdir(current_dir)
4367+ with ClusterConfiguration.open() as config:
4368+ config.tftp_root = current_dir
4369 self.assertEqual(
4370 os.path.join(storage_dir, 'diskless', 'store'),
4371 get_diskless_store())
4372@@ -199,7 +205,7 @@
4373 self.assertRaises(DisklessError, get_diskless_driver, invalid_name)
4374
4375
4376-class TestTgtHelpers(MAASTestCase, DisklessTestMixin):
4377+class TestTgtHelpers(DisklessTestMixin, MAASTestCase):
4378
4379 def test_get_diskless_target(self):
4380 system_id = factory.make_name('system_id')
4381@@ -269,10 +275,10 @@
4382 update_diskless_tgt()
4383 self.assertThat(
4384 mock_write,
4385- MockCalledOnceWith(tgt_config, tgt_path, mode=0644))
4386-
4387-
4388-class TestComposeSourcePath(MAASTestCase, DisklessTestMixin):
4389+ MockCalledOnceWith(tgt_config, tgt_path, mode=0o644))
4390+
4391+
4392+class TestComposeSourcePath(DisklessTestMixin, MAASTestCase):
4393
4394 def test__raises_error_on_missing_os_from_registry(self):
4395 self.assertRaises(
4396@@ -297,14 +303,15 @@
4397 osystem = OperatingSystemRegistry[os_name]
4398 mock_xi_params = self.patch(osystem, 'get_xinstall_parameters')
4399 mock_xi_params.return_value = (root_path, 'tgz')
4400+ with ClusterConfiguration.open() as config:
4401+ tftp_root = config.tftp_root
4402 self.assertEqual(
4403 os.path.join(
4404- config.BOOT_RESOURCES_STORAGE, 'current', os_name,
4405- arch, subarch, release, label, root_path),
4406+ tftp_root, os_name, arch, subarch, release, label, root_path),
4407 compose_source_path(os_name, arch, subarch, release, label))
4408
4409
4410-class TestCreateDisklessDisk(MAASTestCase, DisklessTestMixin):
4411+class TestCreateDisklessDisk(DisklessTestMixin, MAASTestCase):
4412
4413 def test__raises_error_on_doesnt_exist_source_path(self):
4414 self.configure_compose_source_path(factory.make_name('invalid_path'))
4415@@ -408,7 +415,7 @@
4416 self.assertThat(mock_update_tgt, MockCalledOnceWith())
4417
4418
4419-class TestDeleteDisklessDisk(MAASTestCase, DisklessTestMixin):
4420+class TestDeleteDisklessDisk(DisklessTestMixin, MAASTestCase):
4421
4422 def test__exits_early_on_missing_link(self):
4423 self.configure_diskless_storage()
4424
4425=== modified file 'src/provisioningserver/tests/test_path.py'
4426--- src/provisioningserver/tests/test_path.py 2015-05-07 18:14:38 +0000
4427+++ src/provisioningserver/tests/test_path.py 2015-07-03 11:18:28 +0000
4428@@ -73,6 +73,14 @@
4429 os.path.join(root, path),
4430 self.get_path_function(path))
4431
4432+ def test__assumes_MAAS_ROOT_is_unset_if_empty(self):
4433+ self.set_root("")
4434+ self.patch(provisioningserver.path, 'ensure_dir')
4435+ path = factory.make_name('path')
4436+ self.assertEqual(
4437+ os.path.join("/", path),
4438+ self.get_path_function(path))
4439+
4440 def test__returns_absolute_path(self):
4441 self.set_root('.')
4442 self.patch(provisioningserver.path, 'ensure_dir')
4443
4444=== modified file 'src/provisioningserver/tests/test_plugin.py'
4445--- src/provisioningserver/tests/test_plugin.py 2015-06-25 14:48:14 +0000
4446+++ src/provisioningserver/tests/test_plugin.py 2015-07-03 11:18:28 +0000
4447@@ -14,10 +14,6 @@
4448 __metaclass__ = type
4449 __all__ = []
4450
4451-import os
4452-
4453-from fixtures import EnvironmentVariableFixture
4454-from maastesting.factory import factory
4455 from maastesting.matchers import MockCalledOnceWith
4456 from maastesting.testcase import (
4457 MAASTestCase,
4458@@ -25,6 +21,7 @@
4459 )
4460 import provisioningserver
4461 from provisioningserver import plugin as plugin_module
4462+from provisioningserver.config import ClusterConfiguration
4463 from provisioningserver.plugin import (
4464 Options,
4465 ProvisioningServiceMaker,
4466@@ -46,6 +43,7 @@
4467 TFTPBackend,
4468 TFTPService,
4469 )
4470+from provisioningserver.testing.config import ClusterConfigurationFixture
4471 from testtools.matchers import (
4472 AfterPreprocessing,
4473 Equals,
4474@@ -54,7 +52,8 @@
4475 MatchesStructure,
4476 )
4477 from twisted.application.service import MultiService
4478-import yaml
4479+from twisted.python.filepath import FilePath
4480+from twisted.web.server import Site
4481
4482
4483 class TestOptions(MAASTestCase):
4484@@ -62,7 +61,7 @@
4485
4486 def test_defaults(self):
4487 options = Options()
4488- expected = {"config-file": "pserv.yaml", "introspect": None}
4489+ expected = {"introspect": None}
4490 self.assertEqual(expected, options.defaults)
4491
4492 def test_parse_minimal_options(self):
4493@@ -79,17 +78,9 @@
4494
4495 def setUp(self):
4496 super(TestProvisioningServiceMaker, self).setUp()
4497+ self.useFixture(ClusterConfigurationFixture())
4498 self.patch(provisioningserver, "services", MultiService())
4499 self.tempdir = self.make_dir()
4500- self.useFixture(
4501- EnvironmentVariableFixture(
4502- "CLUSTER_UUID", factory.make_UUID()))
4503-
4504- def write_config(self, config):
4505- config_filename = os.path.join(self.tempdir, "config.yaml")
4506- with open(config_filename, "wb") as stream:
4507- yaml.safe_dump(config, stream)
4508- return config_filename
4509
4510 def test_init(self):
4511 service_maker = ProvisioningServiceMaker("Harry", "Hill")
4512@@ -101,7 +92,6 @@
4513 Only the site service is created when no options are given.
4514 """
4515 options = Options()
4516- options["config-file"] = self.write_config({})
4517 service_maker = ProvisioningServiceMaker("Harry", "Hill")
4518 service = service_maker.makeService(options)
4519 self.assertIsInstance(service, MultiService)
4520@@ -120,7 +110,6 @@
4521 mock_simplestreams_patch = (
4522 self.patch(plugin_module, 'force_simplestreams_to_use_urllib2'))
4523 options = Options()
4524- options["config-file"] = self.write_config({})
4525 service_maker = ProvisioningServiceMaker("Harry", "Hill")
4526 service_maker.makeService(options)
4527 self.assertThat(mock_simplestreams_patch, MockCalledOnceWith())
4528@@ -129,14 +118,12 @@
4529 mock_tftp_patch = (
4530 self.patch(plugin_module, 'add_term_error_code_to_tftp'))
4531 options = Options()
4532- options["config-file"] = self.write_config({})
4533 service_maker = ProvisioningServiceMaker("Harry", "Hill")
4534 service_maker.makeService(options)
4535 self.assertThat(mock_tftp_patch, MockCalledOnceWith())
4536
4537 def test_image_download_service(self):
4538 options = Options()
4539- options["config-file"] = self.write_config({})
4540 service_maker = ProvisioningServiceMaker("Harry", "Hill")
4541 service = service_maker.makeService(options)
4542 image_service = service.getServiceNamed("image_download")
4543@@ -144,7 +131,6 @@
4544
4545 def test_node_monitor_service(self):
4546 options = Options()
4547- options["config-file"] = self.write_config({})
4548 service_maker = ProvisioningServiceMaker("Harry", "Hill")
4549 service = service_maker.makeService(options)
4550 node_monitor = service.getServiceNamed("node_monitor")
4551@@ -152,7 +138,6 @@
4552
4553 def test_dhcp_probe_service(self):
4554 options = Options()
4555- options["config-file"] = self.write_config({})
4556 service_maker = ProvisioningServiceMaker("Spike", "Milligan")
4557 service = service_maker.makeService(options)
4558 dhcp_probe = service.getServiceNamed("dhcp_probe")
4559@@ -160,7 +145,6 @@
4560
4561 def test_service_monitor_service(self):
4562 options = Options()
4563- options["config-file"] = self.write_config({})
4564 service_maker = ProvisioningServiceMaker("Harry", "Hill")
4565 service = service_maker.makeService(options)
4566 service_monitor = service.getServiceNamed("service_monitor")
4567@@ -168,53 +152,44 @@
4568
4569 def test_tftp_service(self):
4570 # A TFTP service is configured and added to the top-level service.
4571- config = {
4572- "tftp": {
4573- "generator": "http://candlemass/solitude",
4574- "resource_root": self.tempdir,
4575- "port": factory.pick_port(),
4576- },
4577- }
4578 options = Options()
4579- options["config-file"] = self.write_config(config)
4580 service_maker = ProvisioningServiceMaker("Harry", "Hill")
4581 service = service_maker.makeService(options)
4582 tftp_service = service.getServiceNamed("tftp")
4583 self.assertIsInstance(tftp_service, TFTPService)
4584
4585+ with ClusterConfiguration.open() as config:
4586+ tftp_generator_url = config.tftp_generator_url
4587+ tftp_root = config.tftp_root
4588+ tftp_port = config.tftp_port
4589+
4590 expected_backend = MatchesAll(
4591 IsInstance(TFTPBackend),
4592 AfterPreprocessing(
4593 lambda backend: backend.base.path,
4594- Equals(config["tftp"]["resource_root"])),
4595+ Equals(tftp_root)),
4596 AfterPreprocessing(
4597 lambda backend: backend.generator_url.geturl(),
4598- Equals(config["tftp"]["generator"])))
4599+ Equals(tftp_generator_url)))
4600
4601 self.assertThat(
4602 tftp_service, MatchesStructure(
4603 backend=expected_backend,
4604- port=Equals(config["tftp"]["port"]),
4605+ port=Equals(tftp_port),
4606 ))
4607
4608 def test_image_service(self):
4609- from provisioningserver import config
4610- from twisted.web.server import Site
4611- from twisted.python.filepath import FilePath
4612-
4613 options = Options()
4614- options["config-file"] = self.write_config({})
4615 service_maker = ProvisioningServiceMaker("Harry", "Hill")
4616 service = service_maker.makeService(options)
4617 image_service = service.getServiceNamed("image_service")
4618 self.assertIsInstance(image_service, BootImageEndpointService)
4619 self.assertIsInstance(image_service.site, Site)
4620 resource = image_service.site.resource
4621- root = resource.getChildWithDefault(
4622- "images", request=None)
4623+ root = resource.getChildWithDefault("images", request=None)
4624 self.assertThat(root, IsInstance(FilePath))
4625
4626- resource_root = os.path.join(
4627- config.BOOT_RESOURCES_STORAGE, "current")
4628+ with ClusterConfiguration.open() as config:
4629+ resource_root = FilePath(config.tftp_root)
4630
4631- self.assertEqual(resource_root, root.path)
4632+ self.assertEqual(resource_root, root)
4633
4634=== modified file 'src/provisioningserver/tests/test_tags.py'
4635--- src/provisioningserver/tests/test_tags.py 2015-05-07 18:14:38 +0000
4636+++ src/provisioningserver/tests/test_tags.py 2015-07-03 11:18:28 +0000
4637@@ -37,6 +37,7 @@
4638 sentinel,
4639 )
4640 from provisioningserver import tags
4641+from provisioningserver.testing.config import ClusterConfigurationFixture
4642 from provisioningserver.testing.testcase import PservTestCase
4643 from testtools.matchers import (
4644 DocTestMatches,
4645@@ -484,7 +485,7 @@
4646 self.useFixture(FakeLogger())
4647
4648 def fake_client(self):
4649- return MAASClient(None, None, self.make_maas_url())
4650+ return MAASClient(None, None, factory.make_simple_http_url())
4651
4652 def fake_cached_knowledge(self):
4653 nodegroup_uuid = factory.make_name('nodegroupuuid')
4654@@ -592,7 +593,8 @@
4655 tags.classify(xpath, node_details))
4656
4657 def test_process_node_tags_integration(self):
4658- self.set_maas_url()
4659+ self.useFixture(ClusterConfigurationFixture(
4660+ maas_url=factory.make_simple_http_url()))
4661 get_nodes = FakeMethod(
4662 result=factory.make_response(
4663 httplib.OK,
4664
4665=== modified file 'src/provisioningserver/tests/test_upgrade_cluster.py'
4666--- src/provisioningserver/tests/test_upgrade_cluster.py 2015-05-29 16:47:37 +0000
4667+++ src/provisioningserver/tests/test_upgrade_cluster.py 2015-07-03 11:18:28 +0000
4668@@ -27,11 +27,9 @@
4669 from maastesting.testcase import MAASTestCase
4670 from maastesting.utils import sample_binary_data
4671 from mock import Mock
4672-from provisioningserver import (
4673- config,
4674- upgrade_cluster,
4675-)
4676+from provisioningserver import upgrade_cluster
4677 from provisioningserver.boot.tftppath import list_subdirs
4678+from provisioningserver.testing.config import ClusterConfigurationFixture
4679 from provisioningserver.utils.fs import read_text_file
4680 from testtools.matchers import (
4681 DirExists,
4682@@ -88,7 +86,7 @@
4683
4684 def configure_storage(self, storage_dir):
4685 """Create a storage config."""
4686- self.patch(config, 'BOOT_RESOURCES_STORAGE', storage_dir)
4687+ self.useFixture(ClusterConfigurationFixture(tftp_root=storage_dir))
4688
4689 def test__calls_chown_if_boot_resources_dir_exists(self):
4690 self.patch(upgrade_cluster, 'check_call')
4691@@ -102,7 +100,9 @@
4692 def test__skips_chown_if_boot_resources_dir_does_not_exist(self):
4693 self.patch(upgrade_cluster, 'check_call')
4694 storage_dir = os.path.join(self.make_dir(), factory.make_name('none'))
4695+ os.mkdir(storage_dir)
4696 self.configure_storage(storage_dir)
4697+ os.rmdir(storage_dir)
4698 upgrade_cluster.make_maas_own_boot_resources()
4699 self.assertThat(upgrade_cluster.check_call, MockNotCalled())
4700
4701@@ -205,10 +205,11 @@
4702
4703 def configure_storage(self, storage_dir, make_current_dir=True):
4704 """Create a storage config."""
4705- if make_current_dir:
4706- current_dir = os.path.join(storage_dir, "current")
4707- os.mkdir(current_dir)
4708- self.patch(config, 'BOOT_RESOURCES_STORAGE', storage_dir)
4709+ current_dir = os.path.join(storage_dir, "current")
4710+ os.makedirs(current_dir)
4711+ self.useFixture(ClusterConfigurationFixture(tftp_root=current_dir))
4712+ if not make_current_dir:
4713+ os.rmdir(current_dir)
4714
4715 def test__list_subdirs_under_current_directory(self):
4716 self.patch(upgrade_cluster, 'list_subdirs').return_value = ['ubuntu']
4717
4718=== modified file 'src/provisioningserver/upgrade_cluster.py'
4719--- src/provisioningserver/upgrade_cluster.py 2015-05-29 16:47:37 +0000
4720+++ src/provisioningserver/upgrade_cluster.py 2015-07-03 11:18:28 +0000
4721@@ -38,12 +38,12 @@
4722 from subprocess import check_call
4723 from textwrap import dedent
4724
4725-from provisioningserver import config
4726 from provisioningserver.auth import get_maas_user_gpghome
4727 from provisioningserver.boot.tftppath import (
4728 drill_down,
4729 list_subdirs,
4730 )
4731+from provisioningserver.config import ClusterConfiguration
4732 from provisioningserver.import_images.boot_resources import (
4733 update_targets_conf,
4734 write_targets_conf,
4735@@ -57,8 +57,10 @@
4736 def make_maas_own_boot_resources():
4737 """Upgrade hook: make the `maas` user the owner of the boot resources."""
4738 # This reduces the privileges required for importing and managing images.
4739- if os.path.isdir(config.BOOT_RESOURCES_STORAGE):
4740- check_call(['chown', '-R', 'maas', config.BOOT_RESOURCES_STORAGE])
4741+ with ClusterConfiguration.open() as config:
4742+ boot_resources_storage = config.tftp_root
4743+ if os.path.isdir(boot_resources_storage):
4744+ check_call(['chown', '-R', 'maas', boot_resources_storage])
4745
4746
4747 def create_gnupg_home():
4748@@ -145,10 +147,10 @@
4749 def filter_out_directories_with_extra_levels(paths):
4750 """Remove paths that contain directories with more levels. We don't want
4751 to move other operating systems under the ubuntu directory."""
4752+ with ClusterConfiguration.open() as config:
4753+ tftp_root = config.tftp_root
4754 for arch, subarch, release, label in paths:
4755- path = os.path.join(
4756- config.BOOT_RESOURCES_STORAGE, 'current',
4757- arch, subarch, release, label)
4758+ path = os.path.join(tftp_root, arch, subarch, release, label)
4759 if len(list_subdirs(path)) == 0:
4760 yield (arch, subarch, release, label)
4761
4762@@ -165,7 +167,8 @@
4763 folders have structure arch/subarch/release/label and move them into
4764 ubuntu folder. Making the final path ubuntu/arch/subarch/release/label.
4765 """
4766- current_dir = os.path.join(config.BOOT_RESOURCES_STORAGE, "current")
4767+ with ClusterConfiguration.open() as config:
4768+ current_dir = config.tftp_root
4769 if not os.path.isdir(current_dir):
4770 return
4771 # If ubuntu folder already exists, then no reason to continue
4772
4773=== modified file 'src/provisioningserver/utils/__init__.py'
4774--- src/provisioningserver/utils/__init__.py 2015-05-29 16:47:37 +0000
4775+++ src/provisioningserver/utils/__init__.py 2015-07-03 11:18:28 +0000
4776@@ -18,7 +18,6 @@
4777 "commission_node",
4778 "filter_dict",
4779 "flatten",
4780- "get_cluster_config",
4781 "import_settings",
4782 "locate_config",
4783 "parse_key_value_file",
4784@@ -116,7 +115,10 @@
4785 """
4786 # Avoid circular dependencies.
4787 from provisioningserver.rpc.region import CreateNode
4788- from provisioningserver.cluster_config import get_cluster_uuid
4789+ from provisioningserver.config import ClusterConfiguration
4790+
4791+ with ClusterConfiguration.open() as config:
4792+ cluster_uuid = config.cluster_uuid
4793
4794 if hostname is not None:
4795 hostname = coerce_to_valid_hostname(hostname)
4796@@ -138,7 +140,7 @@
4797 try:
4798 response = yield client(
4799 CreateNode,
4800- cluster_uuid=get_cluster_uuid(),
4801+ cluster_uuid=cluster_uuid,
4802 architecture=arch,
4803 power_type=power_type,
4804 power_parameters=json.dumps(power_parameters),
4805@@ -216,24 +218,16 @@
4806 def locate_config(*path):
4807 """Return the location of a given config file or directory.
4808
4809- Defaults to `/etc/maas` (followed by any further path elements you
4810- specify), but can be overridden using the `MAAS_CONFIG_DIR` environment
4811- variable. (When running from a branch, this variable will point to the
4812- `etc/maas` inside the branch.)
4813-
4814- The result is absolute and normalized.
4815+ :param path: Path elements to resolve relative to `${MAAS_ROOT}/etc/maas`.
4816 """
4817- # Check for MAAS_CONFIG_DIR. Count empty string as "not set."
4818- env_setting = os.getenv('MAAS_CONFIG_DIR', '')
4819- if env_setting == '':
4820- # Running from installed package. Config is in /etc/maas.
4821- config_dir = '/etc/maas'
4822+ # The `os.curdir` avoids a crash when `path` is empty.
4823+ path = os.path.join(os.curdir, *path)
4824+ if os.path.isabs(path):
4825+ return path
4826 else:
4827- # Running from branch or other customized setup. Config is at
4828- # $MAAS_CONFIG_DIR/etc/maas.
4829- config_dir = env_setting
4830-
4831- return os.path.abspath(os.path.join(config_dir, *path))
4832+ # Avoid circular imports.
4833+ from provisioningserver.path import get_tentative_path
4834+ return get_tentative_path("etc", "maas", path)
4835
4836
4837 setting_expression = r"""
4838@@ -245,15 +239,6 @@
4839 """
4840
4841
4842-def get_cluster_config(config_path):
4843- contents = open(config_path).read()
4844-
4845- results = re.findall(
4846- setting_expression, contents, re.MULTILINE | re.VERBOSE)
4847-
4848- return dict(results)
4849-
4850-
4851 def find_settings(whence):
4852 """Return settings from `whence`, which is assumed to be a module."""
4853 # XXX 2012-10-11 JeroenVermeulen, bug=1065456: Put this in a shared
4854
4855=== modified file 'src/provisioningserver/utils/fs.py'
4856--- src/provisioningserver/utils/fs.py 2015-05-29 16:47:37 +0000
4857+++ src/provisioningserver/utils/fs.py 2015-07-03 11:18:28 +0000
4858@@ -30,7 +30,6 @@
4859 from itertools import count
4860 import os
4861 from os import (
4862- environ,
4863 rename,
4864 stat,
4865 chown,
4866@@ -58,7 +57,13 @@
4867 development mode it will return the path for the current development
4868 environment.
4869 """
4870- return environ.get("MAAS_PROVISION_CMD", "maas-provision")
4871+ # Avoid circular imports.
4872+ from provisioningserver.config import is_dev_environment
4873+ if is_dev_environment():
4874+ from maastesting import root
4875+ return os.path.join(root, "bin", "maas-provision")
4876+ else:
4877+ return "maas-provision"
4878
4879
4880 def _write_temp_file(content, filename):
4881
4882=== modified file 'src/provisioningserver/utils/script.py'
4883--- src/provisioningserver/utils/script.py 2015-05-29 16:47:37 +0000
4884+++ src/provisioningserver/utils/script.py 2015-07-03 11:18:28 +0000
4885@@ -88,24 +88,7 @@
4886
4887
4888 class MainScript(ActionScript):
4889- """An `ActionScript` that always accepts a `--config-file` option.
4890-
4891- The `--config-file` option defaults to the value of
4892- `MAAS_PROVISIONING_SETTINGS` in the process's environment, or absent
4893- that, `$MAAS_CONFIG_DIR/pserv.yaml` (normally /etc/maas/pserv.yaml for
4894- packaged installations, or when running from branch, the equivalent
4895- inside that branch).
4896- """
4897-
4898- def __init__(self, description):
4899- # Avoid circular imports.
4900- from provisioningserver.config import Config
4901-
4902- super(MainScript, self).__init__(description)
4903- self.parser.add_argument(
4904- "-c", "--config-file", metavar="FILENAME",
4905- help="Configuration file to load [%(default)s].",
4906- default=Config.DEFAULT_FILENAME)
4907+ """An `ActionScript` denoting the main script in an application."""
4908
4909
4910 class AtomicWriteScript:
4911@@ -141,7 +124,7 @@
4912 if args.mode is not None:
4913 mode = int(args.mode, 8)
4914 else:
4915- mode = 0600
4916+ mode = 0o600
4917 atomic_write(
4918 content, args.filename, overwrite=not args.no_overwrite,
4919 mode=mode)
4920
4921=== modified file 'src/provisioningserver/utils/tests/test_fs.py'
4922--- src/provisioningserver/utils/tests/test_fs.py 2015-05-29 16:47:37 +0000
4923+++ src/provisioningserver/utils/tests/test_fs.py 2015-07-03 11:18:28 +0000
4924@@ -28,6 +28,7 @@
4925 import time
4926
4927 from lockfile import FileLock
4928+from maastesting import root
4929 from maastesting.factory import factory
4930 from maastesting.fakemethod import FakeMethod
4931 from maastesting.matchers import MockCalledOnceWith
4932@@ -37,11 +38,13 @@
4933 Mock,
4934 sentinel,
4935 )
4936+import provisioningserver.config
4937 from provisioningserver.utils.fs import (
4938 atomic_symlink,
4939 atomic_write,
4940 ensure_dir,
4941 FileLockProxy,
4942+ get_maas_provision_command,
4943 get_mtime,
4944 incremental_write,
4945 pick_new_mtime,
4946@@ -284,6 +287,21 @@
4947 self.assertEqual(now, pick_new_mtime(now))
4948
4949
4950+class TestGetMAASProvisionCommand(MAASTestCase):
4951+
4952+ def test__returns_just_command_for_production(self):
4953+ self.patch(provisioningserver.config, "is_dev_environment")
4954+ provisioningserver.config.is_dev_environment.return_value = False
4955+ self.assertEqual("maas-provision", get_maas_provision_command())
4956+
4957+ def test__returns_full_path_for_development(self):
4958+ self.patch(provisioningserver.config, "is_dev_environment")
4959+ provisioningserver.config.is_dev_environment.return_value = True
4960+ self.assertEqual(
4961+ root.rstrip("/") + "/bin/maas-provision",
4962+ get_maas_provision_command())
4963+
4964+
4965 class TestSudoWriteFile(MAASTestCase):
4966 """Testing for `sudo_write_file`."""
4967
4968@@ -301,11 +319,9 @@
4969
4970 sudo_write_file(path, contents)
4971
4972- self.assertThat(fs_module.Popen, MockCalledOnceWith([
4973- 'sudo', '-n', 'maas-provision', 'atomic-write',
4974- '--filename', path, '--mode', '0644',
4975- ],
4976- stdin=PIPE))
4977+ self.assertThat(fs_module.Popen, MockCalledOnceWith(
4978+ ['sudo', '-n', get_maas_provision_command(), 'atomic-write',
4979+ '--filename', path, '--mode', '0644'], stdin=PIPE))
4980
4981 def test_encodes_contents(self):
4982 process = self.patch_popen()
4983
4984=== modified file 'src/provisioningserver/utils/tests/test_script.py'
4985--- src/provisioningserver/utils/tests/test_script.py 2015-05-29 16:47:37 +0000
4986+++ src/provisioningserver/utils/tests/test_script.py 2015-07-03 11:18:28 +0000
4987@@ -38,7 +38,6 @@
4988 from provisioningserver.utils.script import (
4989 ActionScript,
4990 AtomicWriteScript,
4991- MainScript,
4992 )
4993 from testtools.matchers import (
4994 FileContains,
4995@@ -158,30 +157,6 @@
4996 self.assertEqual(1, error.code)
4997
4998
4999-class TestMainScript(TestActionScript):
5000-
The diff has been truncated for viewing.