Merge lp:~jtv/maas/bootresources-rewrite-marker into lp:~maas-committers/maas/trunk
- bootresources-rewrite-marker
- Merge into trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~jtv/maas/bootresources-rewrite-marker |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1919 lines (+712/-430) (has conflicts) 28 files modified
contrib/maas-cluster-http.conf (+2/-2) etc/maas/bootresources.yaml (+59/-0) etc/maas/import_ephemerals (+2/-2) etc/maas/pserv.yaml (+0/-28) etc/maas/templates/pxe/config.install.armhf.template (+0/-5) scripts/maas-import-pxe-files (+27/-2) src/maasserver/models/bootimage.py (+1/-1) src/maasserver/preseed.py (+24/-3) src/maasserver/tests/test_preseed.py (+42/-5) src/metadataserver/tests/test_api.py (+6/-1) src/provisioningserver/boot_images.py (+3/-7) src/provisioningserver/config.py (+10/-8) src/provisioningserver/driver/__init__.py (+2/-2) src/provisioningserver/import_images/boot_resources.py (+330/-88) src/provisioningserver/import_images/tests/test_ephemerals_script.py (+0/-117) src/provisioningserver/kernel_opts.py (+5/-23) src/provisioningserver/pxe/config.py (+9/-6) src/provisioningserver/pxe/tests/test_config.py (+4/-43) src/provisioningserver/pxe/tests/test_tftppath.py (+32/-12) src/provisioningserver/pxe/tftppath.py (+34/-17) src/provisioningserver/rpc/tests/test_clusterservice.py (+2/-2) src/provisioningserver/testing/boot_images.py (+16/-3) src/provisioningserver/tests/test_config.py (+9/-8) src/provisioningserver/tests/test_kernel_opts.py (+5/-42) src/provisioningserver/tests/test_maas_import_pxe_files.py (+6/-0) src/provisioningserver/tests/test_upgrade_cluster.py (+56/-2) src/provisioningserver/tftp.py (+2/-0) src/provisioningserver/upgrade_cluster.py (+24/-1) Text conflict in scripts/maas-import-pxe-files |
To merge this branch: | bzr merge lp:~jtv/maas/bootresources-rewrite-marker |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Maintainers | Pending | ||
Review via email: mp+212583@code.launchpad.net |
This proposal has been superseded by a proposal from 2014-03-25.
Commit message
First step towards writing a bootresources.yaml during cluster upgrade, based on locally available old-style boot images.
Description of the change
Pre-imp was with Julian. I'm keeping this to small steps, because there's going to be quite some diff and I'm messing with production filesystems here.
In this first step, I add a config item to bootresources.yaml: "configure_me" — meaning "please either edit me manually or overwrite me automatically."
The rewrite_
Our cluster upgrade mechanism requires the upgrade code to be able to tell whether it's supposed to do anything or not. And so, when the configuration is the default (which will be just fine for most users), it needs to figure out whether it is that way because that's how we believe the user wants it, or whether it's simply because nothing has been done about it yet. To that end, I added a configure_me item. After a rewrite, that item will be gone forever.
Testing this led me down some dead ends. The ConfigFixture is not suitable for patching configurations other than pserv.yaml, and attempts to fix it led right down a rabbit hole. And so I'm solving this problem ad hoc, which turns out not to be all that bad.
Jeroen
Preview Diff
1 | === modified file 'contrib/maas-cluster-http.conf' |
2 | --- contrib/maas-cluster-http.conf 2013-10-07 19:30:44 +0000 |
3 | +++ contrib/maas-cluster-http.conf 2014-03-25 11:13:45 +0000 |
4 | @@ -1,7 +1,7 @@ |
5 | # Server static files for tftp images as FPI |
6 | # installer needs them |
7 | -Alias /MAAS/static/images/ /var/lib/maas/tftp/ |
8 | -<Directory /var/lib/maas/tftp/> |
9 | +Alias /MAAS/static/images/ /var/lib/maas/boot-resources/current/ |
10 | +<Directory /var/lib/maas/boot-resources/current/> |
11 | <IfVersion >= 2.4> |
12 | Require all granted |
13 | </IfVersion> |
14 | |
15 | === added file 'etc/maas/bootresources.yaml' |
16 | --- etc/maas/bootresources.yaml 1970-01-01 00:00:00 +0000 |
17 | +++ etc/maas/bootresources.yaml 2014-03-25 11:13:45 +0000 |
18 | @@ -0,0 +1,59 @@ |
19 | +## |
20 | +## Boot image download configuration. |
21 | +## |
22 | + |
23 | +boot: |
24 | + ## Marker: this configuration has not been initialised yet. |
25 | + # |
26 | + # If this is set to True during installation or a later upgrade, it means |
27 | + # that this configuration file has not been edited by a human or by MAAS |
28 | + # itself. When you edit this configuration to suit your needs, be sure to |
29 | + # remove this setting, or change it to False, so that a future upgrade will |
30 | + # know not to overwrite the configuration file. |
31 | + # |
32 | + # If the setting is True, the upgrade procedure will look for downloaded |
33 | + # boot images that predate the current, Simplestreams-based import |
34 | + # mechanism, and rewrite the configuration based on what it finds. The new |
35 | + # configuration will import the new-style equivalents of the images that |
36 | + # were imported in the old setup. |
37 | + configure_me = True |
38 | + |
39 | + ## Storage location for boot images. |
40 | + # |
41 | + # These images can get quite large: some files are hundreds of megabytes, |
42 | + # and you may need multiple copies for different architectures, OS releases, |
43 | + # etc. |
44 | + storage: "/var/lib/maas/boot-resources/" |
45 | + |
46 | + ## Sources of downloadable boot images. |
47 | + # |
48 | + # Each source downloads from one simplestreams URL, though it is possible to |
49 | + # have multiple sources using the same URL. For example, if you want to |
50 | + # download "release" images for one OS release and just the "beta-2" images |
51 | + # for another, specify two separate selections. Otherwise, the importer |
52 | + # would have to download both kinds of images for both releases. |
53 | + sources: |
54 | + - path: "http://maas.ubuntu.com/images/ephemeral/releases/" |
55 | + keyring: "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg" |
56 | + |
57 | + ## Filters for this source's imports. |
58 | + # |
59 | + # Be careful: these items will multiply. For example if you specify |
60 | + # architectures "i386" and "amd64", and subarchitectures "generic", |
61 | + # "hwe-s", and "hwe-t", the import script will need to import 6 images, |
62 | + # for each possible combination. If you only want some of those |
63 | + # combinations, keep them as separate sources and/or selections. |
64 | + selections: |
65 | + - release: "trusty" |
66 | + arches: ["i386", "amd64"] |
67 | + subarches: ["generic"] |
68 | + #selections: |
69 | + # - release: "*" |
70 | + # arches: ["*"] |
71 | + # subarches: ["*"] |
72 | +## other source example: |
73 | +# - path: "http://hyperscale.ubuntu.com/images/" |
74 | +# selections: |
75 | +# - release: "*" |
76 | +# arches: ["*"] |
77 | +# subarches: ['*'] |
78 | |
79 | === modified file 'etc/maas/import_ephemerals' |
80 | --- etc/maas/import_ephemerals 2014-03-18 00:29:45 +0000 |
81 | +++ etc/maas/import_ephemerals 2014-03-25 11:13:45 +0000 |
82 | @@ -1,13 +1,13 @@ |
83 | # Legacy configuration file for the maas-import-ephemerals script. |
84 | # |
85 | # This file is obsolete, but the script will still read it for compatibility. |
86 | -# Configure /etc/maas/pserv.yaml by preference. |
87 | +# Configure /etc/maas/bootresources.yaml by preference. |
88 | |
89 | ## Include settings from import_pxe_files. |
90 | [ ! -f /etc/maas/import_pxe_files ] || . /etc/maas/import_pxe_files |
91 | |
92 | # These options can be defined here, although for future compatibility they |
93 | -# should be set in pserv.yaml instead: |
94 | +# should be set in bootresources.yaml instead: |
95 | |
96 | #DATA_DIR="/var/lib/maas/ephemeral" |
97 | #ARCHES="amd64/generic i386/generic" |
98 | |
99 | === modified file 'etc/maas/pserv.yaml' |
100 | --- etc/maas/pserv.yaml 2014-02-10 22:32:17 +0000 |
101 | +++ etc/maas/pserv.yaml 2014-03-25 11:13:45 +0000 |
102 | @@ -38,31 +38,3 @@ |
103 | # generator: http://localhost/MAAS/api/1.0/pxeconfig/ |
104 | generator: http://localhost:5243/api/1.0/pxeconfig/ |
105 | |
106 | -## Boot configuration. |
107 | -boot: |
108 | - ## CPU architectures for which boot images should be downloaded from the |
109 | - ## server, e.g. ['i386/generic', 'amd64/generic']. MAAS needs these images |
110 | - ## in order to boot nodes up. |
111 | - ## |
112 | - ## Leave this option out to download images for all architectures. |
113 | - # |
114 | - # architectures: |
115 | - |
116 | - ## Settings for ephemeral boot images. These images are used when |
117 | - ## commissioning nodes, and during fast-path installation. |
118 | - # |
119 | - ephemeral: |
120 | - |
121 | - ## Directory where ephemeral boot images and related state should be |
122 | - ## stored. |
123 | - # |
124 | - # images_directory: /var/lib/maas/ephemeral |
125 | - images_directory: /var/lib/maas/ephemeral |
126 | - |
127 | - ## Releases for which ephemeral images should be downloaded. |
128 | - ## These images are quite large (about a quarter GB each), so you may want |
129 | - ## to restrict these separately even if you do want the regular install |
130 | - ## images for all releases. Leave this out to download all currently |
131 | - ## supported releases. |
132 | - # |
133 | - # releases: |
134 | |
135 | === removed symlink 'etc/maas/templates/pxe/config.commissioning.armhf.template' |
136 | === target was u'config.install.armhf.template' |
137 | === modified file 'etc/maas/templates/pxe/config.install.armhf.template' |
138 | --- etc/maas/templates/pxe/config.install.armhf.template 2013-10-24 02:46:03 +0000 |
139 | +++ etc/maas/templates/pxe/config.install.armhf.template 2014-03-25 11:13:45 +0000 |
140 | @@ -2,11 +2,6 @@ |
141 | |
142 | LABEL execute |
143 | {{# SAY is not implemented in U-Boot }} |
144 | - {{if kernel_params.release not in ("precise", "quantal")}} |
145 | - {{# Return a copy of kernel_params with an overridden subarch. |
146 | - See https://bugs.launchpad.net/maas/+bug/1166994 }} |
147 | - {{py: kernel_params=kernel_params(subarch="generic")}} |
148 | - {{endif}} |
149 | KERNEL {{kernel_params | kernel_path }} |
150 | INITRD {{kernel_params | initrd_path }} |
151 | APPEND {{kernel_params | kernel_command}} |
152 | |
153 | === modified file 'scripts/maas-import-pxe-files' |
154 | --- scripts/maas-import-pxe-files 2014-03-25 09:25:40 +0000 |
155 | +++ scripts/maas-import-pxe-files 2014-03-25 11:13:45 +0000 |
156 | @@ -1,6 +1,7 @@ |
157 | -#!/usr/bin/env bash |
158 | -# Copyright 2012-2014 Canonical Ltd. This software is licensed under the |
159 | +#!/usr/bin/env python2.7 |
160 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
161 | # GNU Affero General Public License version 3 (see the file LICENSE). |
162 | +<<<<<<< TREE |
163 | # |
164 | # Download static files needed for net-booting nodes through TFTP: |
165 | # pre-boot loader, kernels, and initrd images. |
166 | @@ -445,3 +446,27 @@ |
167 | fi |
168 | |
169 | main |
170 | +======= |
171 | + |
172 | +"""Import boot resources into MAAS cluster controller.""" |
173 | + |
174 | +from __future__ import ( |
175 | + absolute_import, |
176 | + print_function, |
177 | + unicode_literals, |
178 | + ) |
179 | + |
180 | +str = None |
181 | + |
182 | +__metaclass__ = type |
183 | + |
184 | +from provisioningserver.import_images.boot_resources import ( |
185 | + main, |
186 | + make_arg_parser, |
187 | + ) |
188 | + |
189 | +if __name__ == "__main__": |
190 | + parser = make_arg_parser(__doc__) |
191 | + args = parser.parse_args() |
192 | + main(args) |
193 | +>>>>>>> MERGE-SOURCE |
194 | |
195 | === modified file 'src/maasserver/models/bootimage.py' |
196 | --- src/maasserver/models/bootimage.py 2014-03-18 10:33:34 +0000 |
197 | +++ src/maasserver/models/bootimage.py 2014-03-25 11:13:45 +0000 |
198 | @@ -154,7 +154,7 @@ |
199 | # Boot purpose (e.g. "commissioning" or "install") that the image is for. |
200 | purpose = CharField(max_length=255, blank=False, editable=False) |
201 | |
202 | - # "Label" as in simplestreams parlance. (e.g. "release", "beta1") |
203 | + # "Label" as in simplestreams parlance. (e.g. "release", "beta-1") |
204 | label = CharField( |
205 | max_length=255, blank=False, editable=False, default="release") |
206 | |
207 | |
208 | === modified file 'src/maasserver/preseed.py' |
209 | --- src/maasserver/preseed.py 2014-01-28 02:37:29 +0000 |
210 | +++ src/maasserver/preseed.py 2014-03-25 11:13:45 +0000 |
211 | @@ -39,7 +39,9 @@ |
212 | PRESEED_TYPE, |
213 | USERDATA_TYPE, |
214 | ) |
215 | +from maasserver.exceptions import MAASAPIException |
216 | from maasserver.models import ( |
217 | + BootImage, |
218 | Config, |
219 | DHCPLease, |
220 | ) |
221 | @@ -94,10 +96,29 @@ |
222 | cluster_host = pick_cluster_controller_address(node) |
223 | # XXX rvb(?): The path shouldn't be hardcoded like this, but rather synced |
224 | # somehow with the content of contrib/maas-cluster-http.conf. |
225 | + arch, subarch = node.architecture.split('/') |
226 | + purpose = 'xinstall' |
227 | + image = BootImage.objects.get_latest_image( |
228 | + node.nodegroup, arch, subarch, series, purpose) |
229 | + if image is None: |
230 | + raise MAASAPIException( |
231 | + "Error generating the URL of curtin's root-tgz file. " |
232 | + "No image could be found for the given selection: " |
233 | + "arch=%s, subarch=%s, series=%s, purpose=%s." % ( |
234 | + arch, |
235 | + subarch, |
236 | + series, |
237 | + purpose |
238 | + )) |
239 | + dyn_uri = '/'.join([ |
240 | + arch, |
241 | + subarch, |
242 | + series, |
243 | + image.label, |
244 | + 'root-tgz' |
245 | + ]) |
246 | return ( |
247 | - "http://" + cluster_host + "/MAAS/static/images/" + |
248 | - node.architecture + "/" + series + |
249 | - "/xinstall/root.tar.gz") |
250 | + "http://" + cluster_host + "/MAAS/static/images/" + dyn_uri) |
251 | |
252 | |
253 | def get_curtin_config(node): |
254 | |
255 | === modified file 'src/maasserver/tests/test_preseed.py' |
256 | --- src/maasserver/tests/test_preseed.py 2014-03-06 05:57:48 +0000 |
257 | +++ src/maasserver/tests/test_preseed.py 2014-03-25 11:13:45 +0000 |
258 | @@ -27,6 +27,7 @@ |
259 | NODEGROUPINTERFACE_MANAGEMENT, |
260 | PRESEED_TYPE, |
261 | ) |
262 | +from maasserver.exceptions import MAASAPIException |
263 | from maasserver.models import Config |
264 | from maasserver.preseed import ( |
265 | compose_enlistment_preseed_url, |
266 | @@ -574,6 +575,11 @@ |
267 | |
268 | def test_get_curtin_userdata(self): |
269 | node = factory.make_node() |
270 | + arch, subarch = node.architecture.split('/') |
271 | + factory.make_boot_image( |
272 | + architecture=arch, subarchitecture=subarch, |
273 | + release=node.get_distro_series(), purpose='xinstall', |
274 | + nodegroup=node.nodegroup) |
275 | node.use_fastpath_installer() |
276 | user_data = get_curtin_userdata(node) |
277 | # Just check that the user data looks good. |
278 | @@ -603,20 +609,51 @@ |
279 | self.assertItemsEqual(['curtin_preseed'], context) |
280 | self.assertIn('cloud-init', context['curtin_preseed']) |
281 | |
282 | - def test_get_curtin_installer_url(self): |
283 | + def test_get_curtin_installer_url_returns_url(self): |
284 | # Exclude DISTRO_SERIES.default. It's a special value that defers |
285 | # to a run-time setting which we don't provide in this test. |
286 | series = factory.getRandomEnum( |
287 | DISTRO_SERIES, but_not=DISTRO_SERIES.default) |
288 | - arch = make_usable_architecture(self) |
289 | - node = factory.make_node(architecture=arch, distro_series=series) |
290 | + architecture = make_usable_architecture(self) |
291 | + node = factory.make_node( |
292 | + architecture=architecture, distro_series=series) |
293 | + arch, subarch = architecture.split('/') |
294 | + boot_image = factory.make_boot_image( |
295 | + architecture=arch, subarchitecture=subarch, release=series, |
296 | + purpose='xinstall', nodegroup=node.nodegroup) |
297 | + |
298 | installer_url = get_curtin_installer_url(node) |
299 | + |
300 | [interface] = node.nodegroup.get_managed_interfaces() |
301 | self.assertEqual( |
302 | - 'http://%s/MAAS/static/images/%s/%s/xinstall/root.tar.gz' % ( |
303 | - interface.ip, arch, series), |
304 | + 'http://%s/MAAS/static/images/%s/%s/%s/%s/root-tgz' % ( |
305 | + interface.ip, arch, subarch, series, boot_image.label), |
306 | installer_url) |
307 | |
308 | + def test_get_curtin_installer_url_fails_if_no_boot_image(self): |
309 | + series = factory.getRandomEnum( |
310 | + DISTRO_SERIES, but_not=DISTRO_SERIES.default) |
311 | + architecture = make_usable_architecture(self) |
312 | + node = factory.make_node( |
313 | + architecture=architecture, distro_series=series) |
314 | + # Generate a boot image with a different arch/subarch. |
315 | + factory.make_boot_image( |
316 | + architecture=factory.make_name('arch'), |
317 | + subarchitecture=factory.make_name('subarch'), release=series, |
318 | + purpose='xinstall', nodegroup=node.nodegroup) |
319 | + |
320 | + error = self.assertRaises( |
321 | + MAASAPIException, get_curtin_installer_url, node) |
322 | + arch, subarch = architecture.split('/') |
323 | + msg = ( |
324 | + "No image could be found for the given selection: " |
325 | + "arch=%s, subarch=%s, series=%s, purpose=xinstall." % ( |
326 | + arch, |
327 | + subarch, |
328 | + node.get_distro_series(), |
329 | + )) |
330 | + self.assertIn(msg, "%s" % error) |
331 | + |
332 | def test_get_preseed_type_for(self): |
333 | normal = factory.make_node() |
334 | normal.use_traditional_installer() |
335 | |
336 | === modified file 'src/metadataserver/tests/test_api.py' |
337 | --- src/metadataserver/tests/test_api.py 2014-03-05 02:08:23 +0000 |
338 | +++ src/metadataserver/tests/test_api.py 2014-03-25 11:13:45 +0000 |
339 | @@ -39,9 +39,9 @@ |
340 | Tag, |
341 | ) |
342 | from maasserver.testing import reload_object |
343 | -from maasserver.testing.testcase import MAASServerTestCase |
344 | from maasserver.testing.factory import factory |
345 | from maasserver.testing.oauthclient import OAuthAuthenticatedClient |
346 | +from maasserver.testing.testcase import MAASServerTestCase |
347 | from maastesting.djangotestcase import DjangoTestCase |
348 | from maastesting.matchers import MockCalledOnceWith |
349 | from maastesting.utils import sample_binary_data |
350 | @@ -392,6 +392,11 @@ |
351 | |
352 | def test_curtin_user_data_view_returns_curtin_data(self): |
353 | node = factory.make_node() |
354 | + arch, subarch = node.architecture.split('/') |
355 | + factory.make_boot_image( |
356 | + architecture=arch, subarchitecture=subarch, |
357 | + release=node.get_distro_series(), purpose='xinstall', |
358 | + nodegroup=node.nodegroup) |
359 | client = make_node_client(node) |
360 | response = client.get( |
361 | reverse('curtin-metadata-user-data', args=['latest'])) |
362 | |
363 | === modified file 'src/provisioningserver/boot_images.py' |
364 | --- src/provisioningserver/boot_images.py 2014-03-19 13:54:20 +0000 |
365 | +++ src/provisioningserver/boot_images.py 2014-03-25 11:13:45 +0000 |
366 | @@ -1,11 +1,7 @@ |
367 | -# Copyright 2012 Canonical Ltd. This software is licensed under the |
368 | +# Copyright 2012-2014 Canonical Ltd. This software is licensed under the |
369 | # GNU Affero General Public License version 3 (see the file LICENSE). |
370 | |
371 | -"""Dealing with boot images. |
372 | - |
373 | -Most of the lower-level logic is in the `tftppath` module, because it must |
374 | -correspond closely to the structure of the TFTP filesystem hierarchy. |
375 | -""" |
376 | +"""Dealing with boot images.""" |
377 | |
378 | from __future__ import ( |
379 | absolute_import, |
380 | @@ -69,6 +65,6 @@ |
381 | return |
382 | |
383 | images = tftppath.list_boot_images( |
384 | - Config.load_from_cache()['tftp']['root']) |
385 | + Config.load_from_cache()['boot']['storage'] + '/current/') |
386 | |
387 | submit(maas_url, api_credentials, images) |
388 | |
389 | === modified file 'src/provisioningserver/config.py' |
390 | --- src/provisioningserver/config.py 2014-03-21 08:19:20 +0000 |
391 | +++ src/provisioningserver/config.py 2014-03-25 11:13:45 +0000 |
392 | @@ -70,6 +70,7 @@ |
393 | ) |
394 | from formencode.declarative import DeclarativeMeta |
395 | from formencode.validators import ( |
396 | + Bool, |
397 | Int, |
398 | RequireIfPresent, |
399 | Set, |
400 | @@ -109,7 +110,7 @@ |
401 | |
402 | if_key_missing = None |
403 | |
404 | - root = String(if_missing="/var/lib/maas/tftp") |
405 | + root = String(if_missing="/var/lib/maas/boot-resources/current/") |
406 | port = Int(min=1, max=65535, if_missing=69) |
407 | generator = String(if_missing=b"http://localhost/MAAS/api/1.0/pxeconfig/") |
408 | |
409 | @@ -131,20 +132,16 @@ |
410 | releases = Set(if_missing=None) |
411 | |
412 | |
413 | -# XXX jtv 2014-03-21, bug=1295479: Unused until we start using the new import |
414 | -# script. |
415 | class ConfigBootSourceSelection(Schema): |
416 | """Configuration validator for boot source election onfiguration.""" |
417 | |
418 | if_key_missing = None |
419 | |
420 | release = String(if_missing="*") |
421 | - arch = String(if_missing="*") |
422 | + arches = Set(if_missing=["*"]) |
423 | subarches = Set(if_missing=['*']) |
424 | |
425 | |
426 | -# XXX jtv 2014-03-21, bug=1295479: Unused until we start using the new import |
427 | -# script. |
428 | class ConfigBootSource(Schema): |
429 | """Configuration validator for boot source configuration.""" |
430 | |
431 | @@ -152,6 +149,8 @@ |
432 | |
433 | path = String( |
434 | if_missing="http://maas.ubuntu.com/images/ephemeral/releases/") |
435 | + keyring = String( |
436 | + if_missing="/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg") |
437 | selections = ForEach( |
438 | ConfigBootSourceSelection, |
439 | if_missing=[ConfigBootSourceSelection.to_python({})]) |
440 | @@ -167,12 +166,15 @@ |
441 | ephemeral = ConfigBootEphemeral |
442 | architectures = Set(if_missing=None) |
443 | |
444 | - # XXX jtv 2014-03-21, bug=1295479: Unused until we start using the new |
445 | - # import script. |
446 | storage = String(if_missing="/var/lib/maas/boot-resources/") |
447 | sources = ForEach( |
448 | ConfigBootSource, if_missing=[ConfigBootSource.to_python({})]) |
449 | |
450 | + # Marker in the bootresources.yaml file: if True, the file has not been |
451 | + # edited yet and needs to be either configured with initial choices, or |
452 | + # rewritten based on previously downloaded boot images. |
453 | + configure_me = Bool(if_missing=False) |
454 | + |
455 | |
456 | class ConfigMeta(DeclarativeMeta): |
457 | """Metaclass for the root configuration schema.""" |
458 | |
459 | === modified file 'src/provisioningserver/driver/__init__.py' |
460 | --- src/provisioningserver/driver/__init__.py 2014-03-24 08:06:24 +0000 |
461 | +++ src/provisioningserver/driver/__init__.py 2014-03-25 11:13:45 +0000 |
462 | @@ -71,7 +71,7 @@ |
463 | :param at_location: URL to a Simplestreams index or a local path |
464 | to a directory containing boot resources. |
465 | :param filter: A simplestreams filter. |
466 | - e.g. "release=trusty label=beta2 arch=amd64" |
467 | + e.g. "release=trusty label=beta-2 arch=amd64" |
468 | This is ignored if the location is a local path, all resources |
469 | at the location will be imported. |
470 | TBD: How to provide progress information. |
471 | @@ -90,7 +90,7 @@ |
472 | { |
473 | "release": "trusty", |
474 | "arch": "amd64", |
475 | - "label": "beta2", |
476 | + "label": "beta-2", |
477 | "size": 12344556, |
478 | } |
479 | , |
480 | |
481 | === modified file 'src/provisioningserver/import_images/boot_resources.py' |
482 | --- src/provisioningserver/import_images/boot_resources.py 2014-03-17 18:42:45 +0000 |
483 | +++ src/provisioningserver/import_images/boot_resources.py 2014-03-25 11:13:45 +0000 |
484 | @@ -1,4 +1,4 @@ |
485 | -# Copyright 2013 Canonical Ltd. This software is licensed under the |
486 | +# Copyright 2013-2014 Canonical Ltd. This software is licensed under the |
487 | # GNU Affero General Public License version 3 (see the file LICENSE). |
488 | |
489 | from __future__ import ( |
490 | @@ -13,20 +13,30 @@ |
491 | __all__ = [ |
492 | 'main', |
493 | 'available_boot_resources', |
494 | + 'make_arg_parser', |
495 | ] |
496 | |
497 | +from argparse import ArgumentParser |
498 | from collections import defaultdict |
499 | from datetime import datetime |
500 | -import errno |
501 | +import functools |
502 | import glob |
503 | from gzip import GzipFile |
504 | -from json import dumps as jsondumps |
505 | +import json |
506 | +import logging |
507 | +from logging import getLogger |
508 | import os |
509 | from textwrap import dedent |
510 | |
511 | from provisioningserver.config import Config |
512 | from provisioningserver.pxe.install_bootloader import install_bootloader |
513 | -from provisioningserver.utils import call_and_check |
514 | +from provisioningserver.pxe.tftppath import list_boot_images |
515 | +from provisioningserver.utils import ( |
516 | + atomic_write, |
517 | + call_and_check, |
518 | + locate_config, |
519 | + read_text_file, |
520 | + ) |
521 | from simplestreams.contentsource import FdContentSource |
522 | from simplestreams.mirrors import ( |
523 | BasicMirrorWriter, |
524 | @@ -35,49 +45,153 @@ |
525 | from simplestreams.objectstores import FileStore |
526 | from simplestreams.util import ( |
527 | item_checksums, |
528 | + path_from_mirror_url, |
529 | + policy_read_signed, |
530 | products_exdata, |
531 | ) |
532 | |
533 | |
534 | +def init_logger(): |
535 | + logger = getLogger(__name__) |
536 | + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') |
537 | + handler = logging.StreamHandler() |
538 | + handler.setFormatter(formatter) |
539 | + logger.addHandler(handler) |
540 | + logger.setLevel(logging.INFO) |
541 | + return logger |
542 | + |
543 | + |
544 | +logger = init_logger() |
545 | + |
546 | + |
547 | def create_empty_hierarchy(): |
548 | + """Create hierarchy of dicts which supports h[key1]...[keyN] accesses. |
549 | + |
550 | + Generated object automatically creates nonexistent levels of hierarchy |
551 | + when accessed the following way: h[arch][subarch][release]=something. |
552 | + |
553 | + :return Generated hierarchy of dicts. |
554 | + """ |
555 | return defaultdict(create_empty_hierarchy) |
556 | |
557 | |
558 | def boot_walk(boot, func): |
559 | + """Walk over multi-level depth dict and call callback func for every leaf. |
560 | + |
561 | + Function walks over three level depth dictionary organized in a form of |
562 | + d[arch][subarch][release]=value and passes control to a callback function |
563 | + for each arch/subarch/release triplet available. Stored value is passed |
564 | + to a callback function as an additional parameter. |
565 | + |
566 | + :param boot: Hierarchy of dicts with a depth equals to three. |
567 | + :param func: Callback function f(arch, subarch, release, value). |
568 | + """ |
569 | for arch in boot: |
570 | for subarch in boot[arch]: |
571 | for release in boot[arch][subarch]: |
572 | - func(arch, subarch, release, boot[arch][subarch][release]) |
573 | + for label in boot[arch][subarch][release]: |
574 | + func( |
575 | + arch, subarch, release, label, |
576 | + boot[arch][subarch][release][label]) |
577 | + |
578 | + |
579 | +def value_passes_filter_list(filter_list, property_value): |
580 | + """Does the given property of a boot image pass the given filter list? |
581 | + |
582 | + The value passes if either it matches one of the entries in the list of |
583 | + filter values, or one of the filter values is an asterisk (`*`). |
584 | + """ |
585 | + return '*' in filter_list or property_value in filter_list |
586 | + |
587 | + |
588 | +def value_passes_filter(filter_value, property_value): |
589 | + """Does the given property of a boot image pass the given filter? |
590 | + |
591 | + The value passes the filter if either the filter value is an asterisk |
592 | + (`*`) or the value is equal to the filter value. |
593 | + """ |
594 | + return filter_value in ('*', property_value) |
595 | + |
596 | + |
597 | +def image_passes_filter(filters, arch, subarch, release): |
598 | + """Filter a boot image against configured import filters. |
599 | + |
600 | + :param filters: A list of dicts describing the filters, as in `boot_merge`. |
601 | + If the list is empty, or `None`, any image matches. Any entry in a |
602 | + filter may be a string containing just an asterisk (`*`) to denote that |
603 | + the entry will match any value. |
604 | + :param arch: The given boot image's architecture. |
605 | + :param subarch: The given boot image's subarchitecture. |
606 | + :param release: The given boot image's OS release. |
607 | + :return: Whether the image matches any of the dicts in `filters`. |
608 | + """ |
609 | + # XXX jtv 2014-03-24: add label parameter? |
610 | + if filters is None or len(filters) == 0: |
611 | + return True |
612 | + for filter_dict in filters: |
613 | + item_matches = ( |
614 | + value_passes_filter(filter_dict['release'], release) and |
615 | + value_passes_filter_list(filter_dict['arches'], arch) and |
616 | + value_passes_filter_list(filter_dict['subarches'], subarch) |
617 | + ) |
618 | + if item_matches: |
619 | + return True |
620 | + return False |
621 | |
622 | |
623 | def boot_merge(boot1, boot2, filters=None): |
624 | - |
625 | - def filter_func(arch, subarch, release): |
626 | - for filter in filters: |
627 | - item_matches = ( |
628 | - filter['release'] in ('*', release) and |
629 | - filter['arch'] in ('*', arch) and |
630 | - ( |
631 | - '*' in filter['subarches'] or |
632 | - subarch in filter['subarches'] |
633 | - ) |
634 | - ) |
635 | - if item_matches: |
636 | - return True |
637 | - return False |
638 | - |
639 | - def merge_func(arch, subarch, release, boot_resource): |
640 | - if filters and not filter_func(arch, subarch, release): |
641 | - return |
642 | - boot1[arch][subarch][release] = boot_resource |
643 | + """Add entries from the second multi-level dict to the first one. |
644 | + |
645 | + Function copies d[arch][subarch][release]=value chains from the second |
646 | + dictionary to the first one if they don't exist there and pass optional |
647 | + check done by filters. |
648 | + |
649 | + :param boot1: first dict which will be extended in-place. |
650 | + :param boot2: second dict which will be used as a source of new entries. |
651 | + :param filters: list of dicts each of which contains 'arch', 'subarch', |
652 | + 'release' keys; function takes d[arch][subarch][release] chain to the |
653 | + first dict only if filters contain at least one dict with |
654 | + arch in d['arches'], subarch in d['subarch'], d['release'] == release; |
655 | + dict may have '*' as a value for 'arch' and 'release' keys and as a |
656 | + member of 'subarch' list -- in that case key-specific check always |
657 | + passes. |
658 | + """ |
659 | + def merge_func(arch, subarch, release, label, boot_resource): |
660 | + """Merge a boot resource into `boot1`, if it passes filters.""" |
661 | + if image_passes_filter(filters, arch, subarch, release): |
662 | + logger.debug( |
663 | + "Merging boot resource for %s/%s/%s/%s.", |
664 | + arch, subarch, release, label) |
665 | + boot1[arch][subarch][release][label] = boot_resource |
666 | |
667 | boot_walk(boot2, merge_func) |
668 | |
669 | |
670 | def boot_reverse(boot): |
671 | + """Determine a set of subarches which should be deployed by boot resource. |
672 | + |
673 | + Function reverses h[arch][subarch][release]=boot_resource hierarchy to form |
674 | + boot resource to subarch relation. Many subarches may be deployed by a |
675 | + single boot resource (in which case boot_resource=[subarch1, subarch2] |
676 | + relation will be created). We note only subarchitectures and ignore |
677 | + architectures because boot resource is tightly coupled with architecture |
678 | + it can deploy according to metadata format. We can figure out for which |
679 | + architecture we need to use a specific boot resource by looking at its |
680 | + description in metadata. We can't do the same with subarch because we may |
681 | + want to use boot resource only for a specific subset of subarches it can be |
682 | + used for. To represent boot resource to subarch relation we generate the |
683 | + following multi-level dictionary: d[content_id][product_name]=[subarches] |
684 | + where 'content_id' and 'product_name' values come from metadata information |
685 | + and allow us to uniquely identify a specific boot resource. |
686 | + |
687 | + :param boot: Hierarchy of dicts d[arch][subarch][release]=boot_resource |
688 | + :return Hierarchy of dictionaries d[content_id][product_name]=[subarches] |
689 | + which describes boot resource to subarches relation for all available |
690 | + boot resources (products). |
691 | + """ |
692 | reverse = create_empty_hierarchy() |
693 | |
694 | - def reverse_func(arch, subarch, release, boot_resource): |
695 | + def reverse_func(arch, subarch, release, label, boot_resource): |
696 | content_id = boot_resource['content_id'] |
697 | product_name = boot_resource['product_name'] |
698 | existent = list(reverse[content_id][product_name]) |
699 | @@ -87,9 +201,29 @@ |
700 | return reverse |
701 | |
702 | |
703 | -def tgt_entry(arch, subarch, release, image): |
704 | +def tgt_entry(arch, subarch, release, label, image): |
705 | + """Generate tgt target used to commission arch/subarch with release |
706 | + |
707 | + Tgt target used to commission arch/subarch machine with a specific Ubuntu |
708 | + release should have the following name: ephemeral-arch-subarch-release. |
709 | + This function creates target description in a format used by tgt-admin. |
710 | + It uses arch, subarch and release to generate target name and image as |
711 | + a path to image file which should be shared. Tgt target is marked as |
712 | + read-only. Tgt target has 'allow-in-use' option enabled because this |
713 | + script actively uses hardlinks to do image management and root images |
714 | + in different folders may point to the same inode. Tgt doesn't allow us to |
715 | + use the same inode for different tgt targets (even read-only targets which |
716 | + looks like a bug to me) without this option enabled. |
717 | + |
718 | + :param arch: Architecture name we generate tgt target for |
719 | + :param subarch: Subarchitecture name we generate tgt target for |
720 | + :param release: Ubuntu release we generate tgt target for |
721 | + :param label: The images' label |
722 | + :param image: Path to the image which should be shared via tgt/iscsi |
723 | + :return Tgt entry which can be written to tgt-admin configuration file |
724 | + """ |
725 | prefix = 'iqn.2004-05.com.ubuntu:maas' |
726 | - target_name = 'ephemeral-%s-%s-%s' % (arch, subarch, release) |
727 | + target_name = 'ephemeral-%s-%s-%s-%s' % (arch, subarch, release, label) |
728 | entry = dedent("""\ |
729 | <target {prefix}:{target_name}> |
730 | readonly 1 |
731 | @@ -101,15 +235,28 @@ |
732 | return entry |
733 | |
734 | |
735 | +def mirror_info_for_path(path, unsigned_policy=None, keyring=None): |
736 | + if unsigned_policy is None: |
737 | + unsigned_policy = lambda content, path, keyring: content |
738 | + (mirror, rpath) = path_from_mirror_url(path, None) |
739 | + policy = policy_read_signed |
740 | + if rpath.endswith(".json"): |
741 | + policy = unsigned_policy |
742 | + if keyring: |
743 | + policy = functools.partial(policy, keyring=keyring) |
744 | + return(mirror, rpath, policy) |
745 | + |
746 | + |
747 | class RepoDumper(BasicMirrorWriter): |
748 | |
749 | def __init__(self): |
750 | super(RepoDumper, self).__init__({'max_items': 1}) |
751 | |
752 | - def dump(self, path): |
753 | + def dump(self, path, keyring=None): |
754 | self._boot = create_empty_hierarchy() |
755 | - reader = UrlMirrorReader(path) |
756 | - super(RepoDumper, self).sync(reader, 'streams/v1/index.sjson') |
757 | + (mirror, rpath, policy) = mirror_info_for_path(path, keyring=keyring) |
758 | + reader = UrlMirrorReader(mirror, policy=policy) |
759 | + super(RepoDumper, self).sync(reader, rpath) |
760 | return self._boot |
761 | |
762 | def load_products(self, path=None, content_id=None): |
763 | @@ -124,9 +271,10 @@ |
764 | item = products_exdata(src, pedigree) |
765 | arch, subarches = item['arch'], item['subarches'] |
766 | release = item['release'] |
767 | + label = item['label'] |
768 | compact_item = self.item_cleanup(item) |
769 | for subarch in subarches.split(','): |
770 | - self._boot[arch][subarch][release] = compact_item |
771 | + self._boot[arch][subarch][release][label] = compact_item |
772 | |
773 | |
774 | class RepoWriter(BasicMirrorWriter): |
775 | @@ -137,9 +285,10 @@ |
776 | self._cache = FileStore(os.path.abspath(cache_path)) |
777 | super(RepoWriter, self).__init__({'max_items': 1}) |
778 | |
779 | - def write(self, path): |
780 | - reader = UrlMirrorReader(path) |
781 | - super(RepoWriter, self).sync(reader, 'streams/v1/index.sjson') |
782 | + def write(self, path, keyring=None): |
783 | + (mirror, rpath, policy) = mirror_info_for_path(path, keyring=keyring) |
784 | + reader = UrlMirrorReader(mirror, policy=policy) |
785 | + super(RepoWriter, self).sync(reader, rpath) |
786 | |
787 | def load_products(self, path=None, content_id=None): |
788 | return |
789 | @@ -152,46 +301,58 @@ |
790 | product_name in self._info[content_id] |
791 | ) |
792 | |
793 | - def insert_uncompressed(self, tag, checksums, size, contentsource): |
794 | + def insert_file(self, name, tag, checksums, size, contentsource): |
795 | + logger.info("Inserting file %s (tag=%s, size=%s).", name, tag, size) |
796 | self._cache.insert( |
797 | tag, contentsource, checksums, mutable=False, size=size) |
798 | - return self._cache._fullpath(tag) |
799 | + return [(self._cache._fullpath(tag), name)] |
800 | |
801 | - def insert_compressed(self, tag, checksums, size, contentsource): |
802 | - # TODO: Bake root.tar.gz required by fast-path installer (uec2roottar) |
803 | - uncompressed_tag = 'uncompressed-%s' % tag |
804 | - compressed_path = self._cache._fullpath(tag) |
805 | - uncompressed_path = self._cache._fullpath(uncompressed_tag) |
806 | - if not os.path.isfile(uncompressed_path): |
807 | - self._cache.insert(tag, contentsource, checksums, |
808 | - mutable=False, size=size) |
809 | - compressed_source = FdContentSource(GzipFile(compressed_path)) |
810 | - self._cache.insert(uncompressed_tag, compressed_source, |
811 | - mutable=False) |
812 | + def insert_root_image(self, tag, checksums, size, contentsource): |
813 | + root_image_tag = 'root-image-%s' % tag |
814 | + root_image_path = self._cache._fullpath(root_image_tag) |
815 | + root_tgz_tag = 'root-tgz-%s' % tag |
816 | + root_tgz_path = self._cache._fullpath(root_tgz_tag) |
817 | + if not os.path.isfile(root_image_path): |
818 | + logger.info("New root image: %s.", root_image_path) |
819 | + self._cache.insert( |
820 | + tag, contentsource, checksums, mutable=False, size=size) |
821 | + uncompressed = FdContentSource( |
822 | + GzipFile(self._cache._fullpath(tag))) |
823 | + self._cache.insert(root_image_tag, uncompressed, mutable=False) |
824 | self._cache.remove(tag) |
825 | - return uncompressed_path |
826 | + if not os.path.isfile(root_tgz_path): |
827 | + logger.info("Converting root tarball: %s.", root_tgz_path) |
828 | + call_uec2roottar(root_image_path, root_tgz_path) |
829 | + return [(root_image_path, 'root-image'), (root_tgz_path, 'root-tgz')] |
830 | |
831 | def insert_item(self, data, src, target, pedigree, contentsource): |
832 | item = products_exdata(src, pedigree) |
833 | checksums = item_checksums(data) |
834 | - tag = checksums['md5'] |
835 | + tag = checksums['sha256'] |
836 | size = data['size'] |
837 | - if data['path'].endswith('.gz'): |
838 | - src = self.insert_compressed(tag, checksums, size, contentsource) |
839 | + ftype = item['ftype'] |
840 | + if ftype == 'root-image.gz': |
841 | + links = self.insert_root_image(tag, checksums, size, contentsource) |
842 | else: |
843 | - src = self.insert_uncompressed(tag, checksums, size, contentsource) |
844 | + links = self.insert_file( |
845 | + ftype, tag, checksums, size, contentsource) |
846 | for subarch in self._info[item['content_id']][item['product_name']]: |
847 | dst_folder = os.path.join( |
848 | - self._root_path, item['arch'], subarch, item['release']) |
849 | + self._root_path, item['arch'], subarch, item['release'], |
850 | + item['label']) |
851 | if not os.path.exists(dst_folder): |
852 | os.makedirs(dst_folder) |
853 | - os.link(src, os.path.join(dst_folder, item['ftype'])) |
854 | + for src, link_name in links: |
855 | + link_path = os.path.join(dst_folder, link_name) |
856 | + if os.path.isfile(link_path): |
857 | + os.remove(link_path) |
858 | + os.link(src, link_path) |
859 | |
860 | |
861 | def available_boot_resources(root): |
862 | - for resource_path in glob.glob(os.path.join(root, '*/*/*')): |
863 | - arch, subarch, release = resource_path.split('/')[-3:] |
864 | - yield (arch, subarch, release) |
865 | + for resource_path in glob.glob(os.path.join(root, '*/*/*/*')): |
866 | + arch, subarch, release, label = resource_path.split('/')[-4:] |
867 | + yield (arch, subarch, release, label) |
868 | |
869 | |
870 | BOOTLOADERS = ['pxelinux.0', 'chain.c32', 'ifcpu64.c32'] |
871 | @@ -212,14 +373,95 @@ |
872 | install_bootloader(bootloader_src, bootloader_dst) |
873 | |
874 | |
875 | -def main(): |
876 | - |
877 | - try: |
878 | - config = Config.load_from_cache() |
879 | - except IOError as e: |
880 | - if e.errno != errno.ENOENT: |
881 | - raise |
882 | - config = Config.get_defaults() |
883 | +def call_uec2roottar(*args): |
884 | + """Invoke `uec2roottar` with the given arguments. |
885 | + |
886 | + Here only so tests can stub it out. |
887 | + """ |
888 | + call_and_check(["uec2roottar"] + list(args)) |
889 | + |
890 | + |
891 | +def make_arg_parser(doc): |
892 | + """Create an `argparse.ArgumentParser` for this script.""" |
893 | + |
894 | + parser = ArgumentParser(description=doc) |
895 | + default_config = locate_config("bootresources.yaml") |
896 | + parser.add_argument( |
897 | + '--config-file', action="store", default=default_config, |
898 | + help="Path to config file " |
899 | + "(defaults to %s)" % default_config) |
900 | + return parser |
901 | + |
902 | + |
903 | +def compose_targets_conf(snapshot_path): |
904 | + """Produce the contents of a snapshot's tgt conf file. |
905 | + |
906 | + :param snasphot_path: Filesystem path to a snapshot of boot images. |
907 | + :return: Contents for a `targets.conf` file. |
908 | + :rtype: bytes |
909 | + """ |
910 | + # Use a set to make sure we don't register duplicate entries in tgt. |
911 | + entries = set() |
912 | + for item in list_boot_images(snapshot_path): |
913 | + arch = item['architecture'] |
914 | + subarch = item['subarchitecture'] |
915 | + release = item['release'] |
916 | + label = item['label'] |
917 | + entries.add((arch, subarch, release, label)) |
918 | + tgt_entries = [] |
919 | + for arch, subarch, release, label in sorted(entries): |
920 | + root_image = os.path.join( |
921 | + snapshot_path, arch, subarch, release, label, 'root-image') |
922 | + if os.path.isfile(root_image): |
923 | + entry = tgt_entry(arch, subarch, release, label, root_image) |
924 | + tgt_entries.append(entry) |
925 | + text = ''.join(tgt_entries) |
926 | + return text.encode('utf-8') |
927 | + |
928 | + |
929 | +def meta_contains(storage, content): |
930 | + """Does the `maas.meta` file match `content`? |
931 | + |
932 | + If the file's contents match the latest data, there is no need to update. |
933 | + """ |
934 | + current_meta = os.path.join(storage, 'current', 'maas.meta') |
935 | + return ( |
936 | + os.path.isfile(current_meta) and |
937 | + content == read_text_file(current_meta) |
938 | + ) |
939 | + |
940 | + |
941 | +def compose_snapshot_path(storage): |
942 | + """Put together a path for a new snapshot. |
943 | + |
944 | + A snapshot is a directory in `storage` containing images. The name |
945 | + contains the date in a sortable format. |
946 | + """ |
947 | + snapshot_name = 'snapshot-%s' % datetime.now().strftime('%Y%m%d-%H%M%S') |
948 | + return os.path.join(storage, snapshot_name) |
949 | + |
950 | + |
951 | +def update_current_symlink(storage, latest_snapshot): |
952 | + """Symlink `latest_snapshot` as the "current" snapshot.""" |
953 | + symlink_path = os.path.join(storage, 'current') |
954 | + if os.path.lexists(symlink_path): |
955 | + os.unlink(symlink_path) |
956 | + os.symlink(latest_snapshot, symlink_path) |
957 | + |
958 | + |
959 | +def write_snapshot_metadata(snapshot, meta_file_content, targets_conf, |
960 | + targets_conf_content): |
961 | + """Write "meta" file and tgt config for `snapshot`.""" |
962 | + meta_file = os.path.join(snapshot, 'maas.meta') |
963 | + atomic_write(meta_file_content, meta_file, mode=0644) |
964 | + atomic_write(targets_conf_content, targets_conf, mode=0644) |
965 | + |
966 | + |
967 | +def main(args): |
968 | + logger.info("Importing boot resources.") |
969 | + # The config file is required. We do not fall back to defaults if it's |
970 | + # not there. |
971 | + config = Config.load_from_cache(filename=args.config_file) |
972 | |
973 | storage = config['boot']['storage'] |
974 | |
975 | @@ -227,34 +469,34 @@ |
976 | dumper = RepoDumper() |
977 | |
978 | for source in reversed(config['boot']['sources']): |
979 | - repo_boot = dumper.dump(source['path']) |
980 | + repo_boot = dumper.dump(source['path'], keyring=source['keyring']) |
981 | boot_merge(boot, repo_boot, source['selections']) |
982 | |
983 | - meta = jsondumps(boot) |
984 | - current_meta = storage + '/current/maas.meta' |
985 | - if os.path.isfile(current_meta) and meta == open(current_meta).read(): |
986 | + meta_file_content = json.dumps(boot, sort_keys=True) |
987 | + if meta_contains(storage, meta_file_content): |
988 | + # The current maas.meta already contains the new config. No need to |
989 | + # rewrite anything. |
990 | return |
991 | |
992 | - snapshot_name = '/snapshot-%s/' % datetime.now().strftime('%d%m%Y-%H%M%S') |
993 | - snapshot_path = storage + snapshot_name |
994 | reverse_boot = boot_reverse(boot) |
995 | - writer = RepoWriter(snapshot_path, storage + '/cache/', reverse_boot) |
996 | + snapshot_path = compose_snapshot_path(storage) |
997 | + cache_path = os.path.join(storage, 'cache') |
998 | + targets_conf = os.path.join(snapshot_path, 'maas.tgt') |
999 | + writer = RepoWriter(snapshot_path, cache_path, reverse_boot) |
1000 | |
1001 | for source in config['boot']['sources']: |
1002 | - writer.write(source['path']) |
1003 | - |
1004 | - open(snapshot_path + '/maas.meta', 'w').write(meta) |
1005 | - symlink_path = storage + '/current' |
1006 | - if os.path.lexists(symlink_path): |
1007 | - os.unlink(symlink_path) |
1008 | - os.symlink(snapshot_path, symlink_path) |
1009 | - |
1010 | - with open(snapshot_path + '/maas.tgt', 'w') as output: |
1011 | - for arch, subarch, release in available_boot_resources(snapshot_path): |
1012 | - disk = os.path.join(snapshot_path, arch, subarch, release, 'disk') |
1013 | - if os.path.isfile(disk): |
1014 | - output.write(tgt_entry(arch, subarch, release, disk)) |
1015 | - |
1016 | - call_and_check(['tgt-admin', '--update', 'ALL']) |
1017 | - |
1018 | + writer.write(source['path'], source['keyring']) |
1019 | + |
1020 | + targets_conf_content = compose_targets_conf(snapshot_path) |
1021 | + |
1022 | + logger.info("Writing metadata and updating iSCSI targets.") |
1023 | + write_snapshot_metadata( |
1024 | + snapshot_path, meta_file_content, targets_conf, targets_conf_content) |
1025 | + call_and_check(['tgt-admin', '--conf', targets_conf, '--update', 'ALL']) |
1026 | + |
1027 | + logger.info("Installing boot images snapshot %s.", snapshot_path) |
1028 | install_boot_loaders(snapshot_path) |
1029 | + |
1030 | + # If we got here, all went well. This is now truly the "current" snapshot. |
1031 | + update_current_symlink(storage, snapshot_path) |
1032 | + logger.info("Import done.") |
1033 | |
1034 | === modified file 'src/provisioningserver/import_images/tests/test_ephemerals_script.py' |
1035 | --- src/provisioningserver/import_images/tests/test_ephemerals_script.py 2014-03-13 05:05:21 +0000 |
1036 | +++ src/provisioningserver/import_images/tests/test_ephemerals_script.py 2014-03-25 11:13:45 +0000 |
1037 | @@ -14,22 +14,15 @@ |
1038 | __metaclass__ = type |
1039 | __all__ = [] |
1040 | |
1041 | -from argparse import ArgumentParser |
1042 | -from copy import deepcopy |
1043 | from os import ( |
1044 | listdir, |
1045 | readlink, |
1046 | ) |
1047 | import os.path |
1048 | -from pipes import quote |
1049 | import subprocess |
1050 | -from textwrap import dedent |
1051 | |
1052 | -from fixtures import EnvironmentVariableFixture |
1053 | from maastesting.factory import factory |
1054 | -from provisioningserver.config import Config |
1055 | from provisioningserver.import_images import ( |
1056 | - config as config_module, |
1057 | ephemerals_script, |
1058 | ) |
1059 | from provisioningserver.import_images.ephemerals_script import ( |
1060 | @@ -37,7 +30,6 @@ |
1061 | create_symlinked_image_dir, |
1062 | extract_image_tarball, |
1063 | install_image_from_simplestreams, |
1064 | - make_arg_parser, |
1065 | move_file_by_glob, |
1066 | ) |
1067 | from provisioningserver.pxe.tftppath import ( |
1068 | @@ -369,112 +361,3 @@ |
1069 | temp_location=temp_location) |
1070 | |
1071 | self.assertItemsEqual([], listdir(temp_location)) |
1072 | - |
1073 | - |
1074 | -def make_legacy_config(data_dir=None, arches=None, releases=None): |
1075 | - """Create contents for a legacy, shell-script config file.""" |
1076 | - if data_dir is None: |
1077 | - data_dir = factory.make_name('datadir') |
1078 | - if arches is None: |
1079 | - arches = [factory.make_name('arch') for counter in range(2)] |
1080 | - if releases is None: |
1081 | - releases = [factory.make_name('release') for counter in range(2)] |
1082 | - return dedent("""\ |
1083 | - DATA_DIR=%s |
1084 | - ARCHES=%s |
1085 | - RELEASES=%s |
1086 | - """) % ( |
1087 | - quote(data_dir), |
1088 | - quote(' '.join(arches)), |
1089 | - quote(' '.join(releases)), |
1090 | - ) |
1091 | - |
1092 | - |
1093 | -def install_legacy_config(testcase, contents): |
1094 | - """Set up a legacy config file with the given contents. |
1095 | - |
1096 | - Returns the config file's path. |
1097 | - """ |
1098 | - legacy_file = testcase.make_file(contents=contents) |
1099 | - testcase.patch(config_module, 'EPHEMERALS_LEGACY_CONFIG', legacy_file) |
1100 | - return legacy_file |
1101 | - |
1102 | - |
1103 | -class TestMakeArgParser(PservTestCase): |
1104 | - |
1105 | - def test_creates_parser(self): |
1106 | - self.useFixture(ConfigFixture({'boot': {'ephemeral': {}}})) |
1107 | - documentation = factory.getRandomString() |
1108 | - |
1109 | - parser = make_arg_parser(documentation) |
1110 | - |
1111 | - self.assertIsInstance(parser, ArgumentParser) |
1112 | - self.assertEqual(documentation, parser.description) |
1113 | - |
1114 | - def test_defaults_to_config(self): |
1115 | - images_directory = self.make_dir() |
1116 | - arches = [factory.make_name('arch1'), factory.make_name('arch2')] |
1117 | - releases = [factory.make_name('rel1'), factory.make_name('rel2')] |
1118 | - self.useFixture(ConfigFixture({ |
1119 | - 'boot': { |
1120 | - 'architectures': arches, |
1121 | - 'ephemeral': { |
1122 | - 'images_directory': images_directory, |
1123 | - 'releases': releases, |
1124 | - }, |
1125 | - }, |
1126 | - })) |
1127 | - |
1128 | - parser = make_arg_parser(factory.getRandomString()) |
1129 | - |
1130 | - args = parser.parse_args('') |
1131 | - self.assertEqual(images_directory, args.output) |
1132 | - self.assertItemsEqual( |
1133 | - [ |
1134 | - compose_filter('arch', arches), |
1135 | - compose_filter('release', releases), |
1136 | - ], |
1137 | - args.filters) |
1138 | - |
1139 | - def test_does_not_require_config(self): |
1140 | - defaults = Config.get_defaults() |
1141 | - no_file = os.path.join(self.make_dir(), factory.make_name() + '.yaml') |
1142 | - self.useFixture( |
1143 | - EnvironmentVariableFixture('MAAS_PROVISIONING_SETTINGS', no_file)) |
1144 | - |
1145 | - parser = make_arg_parser(factory.getRandomString()) |
1146 | - |
1147 | - args = parser.parse_args('') |
1148 | - self.assertEqual( |
1149 | - defaults['boot']['ephemeral']['images_directory'], |
1150 | - args.output) |
1151 | - self.assertItemsEqual([], args.filters) |
1152 | - |
1153 | - def test_does_not_modify_config(self): |
1154 | - self.useFixture(ConfigFixture({ |
1155 | - 'boot': { |
1156 | - 'architectures': [factory.make_name('arch')], |
1157 | - 'ephemeral': { |
1158 | - 'images_directory': self.make_dir(), |
1159 | - 'releases': [factory.make_name('release')], |
1160 | - }, |
1161 | - }, |
1162 | - })) |
1163 | - original_boot_config = deepcopy(Config.load_from_cache()['boot']) |
1164 | - install_legacy_config(self, make_legacy_config()) |
1165 | - |
1166 | - make_arg_parser(factory.getRandomString()) |
1167 | - |
1168 | - self.assertEqual( |
1169 | - original_boot_config, |
1170 | - Config.load_from_cache()['boot']) |
1171 | - |
1172 | - def test_uses_legacy_config(self): |
1173 | - data_dir = self.make_dir() |
1174 | - self.useFixture(ConfigFixture({})) |
1175 | - install_legacy_config(self, make_legacy_config(data_dir=data_dir)) |
1176 | - |
1177 | - parser = make_arg_parser(factory.getRandomString()) |
1178 | - |
1179 | - args = parser.parse_args('') |
1180 | - self.assertEqual(data_dir, args.output) |
1181 | |
1182 | === modified file 'src/provisioningserver/kernel_opts.py' |
1183 | --- src/provisioningserver/kernel_opts.py 2014-03-18 03:23:55 +0000 |
1184 | +++ src/provisioningserver/kernel_opts.py 2014-03-25 11:13:45 +0000 |
1185 | @@ -21,9 +21,7 @@ |
1186 | from collections import namedtuple |
1187 | import os |
1188 | |
1189 | -from provisioningserver.config import Config |
1190 | from provisioningserver.driver import ArchitectureRegistry |
1191 | -from provisioningserver.utils import parse_key_value_file |
1192 | |
1193 | |
1194 | class EphemeralImagesDirectoryNotFound(Exception): |
1195 | @@ -92,26 +90,9 @@ |
1196 | ISCSI_TARGET_NAME_PREFIX = "iqn.2004-05.com.ubuntu:maas" |
1197 | |
1198 | |
1199 | -def get_ephemeral_name(release, arch): |
1200 | - """Return the name of the most recent ephemeral image. |
1201 | - |
1202 | - That information is read from the config file named 'info' in the |
1203 | - ephemeral directory e.g: |
1204 | - /var/lib/maas/ephemeral/precise/ephemeral/i386/20120424/info |
1205 | - """ |
1206 | - config = Config.load_from_cache() |
1207 | - root = os.path.join( |
1208 | - config["boot"]["ephemeral"]["images_directory"], |
1209 | - release, 'ephemeral', arch) |
1210 | - try: |
1211 | - filename = os.path.join(get_last_directory(root), 'info') |
1212 | - except OSError: |
1213 | - raise EphemeralImagesDirectoryNotFound( |
1214 | - "The directory containing the ephemeral images/info is missing " |
1215 | - "(%r). Make sure to run the script " |
1216 | - "'maas-import-pxe-files'." % root) |
1217 | - name = parse_key_value_file(filename, separator="=")['name'] |
1218 | - return name |
1219 | +def get_ephemeral_name(arch, subarch, release, label): |
1220 | + """Return the name of the most recent ephemeral image.""" |
1221 | + return "ephemeral-%s-%s-%s-%s" % (arch, subarch, release, label) |
1222 | |
1223 | |
1224 | def compose_hostname_opts(params): |
1225 | @@ -137,7 +118,8 @@ |
1226 | if params.purpose == "commissioning" or params.purpose == "xinstall": |
1227 | # These are kernel parameters read by the ephemeral environment. |
1228 | tname = prefix_target_name( |
1229 | - get_ephemeral_name(params.release, params.arch)) |
1230 | + get_ephemeral_name( |
1231 | + params.arch, params.subarch, params.release, params.label)) |
1232 | kernel_params = [ |
1233 | # Read by the open-iscsi initramfs code. |
1234 | "iscsi_target_name=%s" % tname, |
1235 | |
1236 | === modified file 'src/provisioningserver/pxe/config.py' |
1237 | --- src/provisioningserver/pxe/config.py 2014-03-13 05:05:21 +0000 |
1238 | +++ src/provisioningserver/pxe/config.py 2014-03-25 11:13:45 +0000 |
1239 | @@ -95,19 +95,22 @@ |
1240 | kernel_params.purpose, kernel_params.arch, |
1241 | kernel_params.subarch) |
1242 | |
1243 | - # The locations of the kernel image and the initrd are defined by |
1244 | - # update_install_files(), in scripts/maas-import-pxe-files. |
1245 | - |
1246 | def image_dir(params): |
1247 | return compose_image_path( |
1248 | params.arch, params.subarch, |
1249 | - params.release, params.label, params.purpose) |
1250 | + params.release, params.label) |
1251 | |
1252 | def initrd_path(params): |
1253 | - return "%s/initrd.gz" % image_dir(params) |
1254 | + if params.purpose == "install": |
1255 | + return "%s/di-initrd" % image_dir(params) |
1256 | + else: |
1257 | + return "%s/boot-initrd" % image_dir(params) |
1258 | |
1259 | def kernel_path(params): |
1260 | - return "%s/linux" % image_dir(params) |
1261 | + if params.purpose == "install": |
1262 | + return "%s/di-kernel" % image_dir(params) |
1263 | + else: |
1264 | + return "%s/boot-kernel" % image_dir(params) |
1265 | |
1266 | def kernel_command(params): |
1267 | return compose_kernel_command_line(params) |
1268 | |
1269 | === modified file 'src/provisioningserver/pxe/tests/test_config.py' |
1270 | --- src/provisioningserver/pxe/tests/test_config.py 2014-03-17 08:18:55 +0000 |
1271 | +++ src/provisioningserver/pxe/tests/test_config.py 2014-03-25 11:13:45 +0000 |
1272 | @@ -161,7 +161,7 @@ |
1273 | class TestRenderPXEConfig(MAASTestCase): |
1274 | """Tests for `provisioningserver.pxe.config.render_pxe_config`.""" |
1275 | |
1276 | - def test_render(self): |
1277 | + def test_render_install(self): |
1278 | # Given the right configuration options, the PXE configuration is |
1279 | # correctly rendered. |
1280 | params = make_kernel_parameters(self, purpose="install") |
1281 | @@ -174,14 +174,14 @@ |
1282 | # The PXE parameters are all set according to the options. |
1283 | image_dir = compose_image_path( |
1284 | arch=params.arch, subarch=params.subarch, |
1285 | - release=params.release, label=params.label, purpose=params.purpose) |
1286 | + release=params.release, label=params.label) |
1287 | self.assertThat( |
1288 | output, MatchesAll( |
1289 | MatchesRegex( |
1290 | - r'.*^\s+KERNEL %s/linux$' % re.escape(image_dir), |
1291 | + r'.*^\s+KERNEL %s/di-kernel$' % re.escape(image_dir), |
1292 | re.MULTILINE | re.DOTALL), |
1293 | MatchesRegex( |
1294 | - r'.*^\s+INITRD %s/initrd[.]gz$' % re.escape(image_dir), |
1295 | + r'.*^\s+INITRD %s/di-initrd$' % re.escape(image_dir), |
1296 | re.MULTILINE | re.DOTALL), |
1297 | MatchesRegex( |
1298 | r'.*^\s+APPEND .+?$', |
1299 | @@ -235,45 +235,6 @@ |
1300 | self.assertNotIn("LOCALBOOT", output) |
1301 | |
1302 | |
1303 | -class TestRenderArmhfSubarchScenarios(MAASTestCase): |
1304 | - """See bug https://bugs.launchpad.net/maas/+bug/1166994""" |
1305 | - |
1306 | - scenarios = [ |
1307 | - ("install_precise", dict( |
1308 | - arch="armhf", purpose="install", release="precise", |
1309 | - expect_in_output="highbank")), |
1310 | - ("install_quantal", dict( |
1311 | - arch="armhf", purpose="install", release="quantal", |
1312 | - expect_in_output="highbank")), |
1313 | - ("install_saucy", dict( |
1314 | - arch="armhf", purpose="install", release="saucy", |
1315 | - expect_in_output="generic")), |
1316 | - ("commission_precise", dict( |
1317 | - arch="armhf", purpose="commissioning", release="precise", |
1318 | - expect_in_output="highbank")), |
1319 | - ("commission_quantal", dict( |
1320 | - arch="armhf", purpose="commissioning", release="quantal", |
1321 | - expect_in_output="highbank")), |
1322 | - ("commission_saucy", dict( |
1323 | - arch="armhf", purpose="commissioning", release="saucy", |
1324 | - expect_in_output="generic")), |
1325 | - ] |
1326 | - |
1327 | - def test_highbank_scenarios(self): |
1328 | - # get_ephemeral_name depends on ephemeral images being present but |
1329 | - # doesn't affect the test outcome, so patch it out. |
1330 | - self.patch( |
1331 | - kernel_opts, |
1332 | - "get_ephemeral_name").return_value = "ephemeral_name" |
1333 | - options = { |
1334 | - "kernel_params": make_kernel_parameters( |
1335 | - arch=self.arch, purpose=self.purpose, subarch="highbank", |
1336 | - release=self.release), |
1337 | - } |
1338 | - output = render_pxe_config(**options) |
1339 | - self.assertIn(self.expect_in_output, output) |
1340 | - |
1341 | - |
1342 | class TestRenderPXEConfigScenarios(MAASTestCase): |
1343 | """Tests for `provisioningserver.pxe.config.render_pxe_config`.""" |
1344 | |
1345 | |
1346 | === modified file 'src/provisioningserver/pxe/tests/test_tftppath.py' |
1347 | --- src/provisioningserver/pxe/tests/test_tftppath.py 2014-03-18 03:23:55 +0000 |
1348 | +++ src/provisioningserver/pxe/tests/test_tftppath.py 2014-03-25 11:13:45 +0000 |
1349 | @@ -30,7 +30,9 @@ |
1350 | list_subdirs, |
1351 | locate_tftp_path, |
1352 | ) |
1353 | -from provisioningserver.testing.boot_images import make_boot_image_params |
1354 | +from provisioningserver.testing.boot_images import ( |
1355 | + make_boot_image_storage_params, |
1356 | + ) |
1357 | from provisioningserver.testing.config import ConfigFixture |
1358 | from testtools.matchers import ( |
1359 | Not, |
1360 | @@ -38,6 +40,16 @@ |
1361 | ) |
1362 | |
1363 | |
1364 | +def make_image(params, purpose): |
1365 | + """Describe an image as a dict similar to what `list_boot_images` returns. |
1366 | + |
1367 | + The `params` are as returned from `make_boot_image_storage_params`. |
1368 | + """ |
1369 | + image = params.copy() |
1370 | + image['purpose'] = purpose |
1371 | + return image |
1372 | + |
1373 | + |
1374 | class TestTFTPPath(MAASTestCase): |
1375 | |
1376 | def setUp(self): |
1377 | @@ -53,8 +65,7 @@ |
1378 | arch=image_params['architecture'], |
1379 | subarch=image_params['subarchitecture'], |
1380 | release=image_params['release'], |
1381 | - label=image_params['label'], |
1382 | - purpose=image_params['purpose']), |
1383 | + label=image_params['label']), |
1384 | tftproot) |
1385 | os.makedirs(image_dir) |
1386 | factory.make_file(image_dir, 'linux') |
1387 | @@ -111,23 +122,32 @@ |
1388 | self.tftproot, locate_tftp_path(None, tftproot=self.tftproot)) |
1389 | |
1390 | def test_list_boot_images_copes_with_empty_directory(self): |
1391 | - self.assertItemsEqual([], list_boot_images(self.tftproot)) |
1392 | + self.assertEqual([], list_boot_images(self.tftproot)) |
1393 | |
1394 | def test_list_boot_images_copes_with_unexpected_files(self): |
1395 | os.makedirs(os.path.join(self.tftproot, factory.make_name('empty'))) |
1396 | factory.make_file(self.tftproot) |
1397 | - self.assertItemsEqual([], list_boot_images(self.tftproot)) |
1398 | + self.assertEqual([], list_boot_images(self.tftproot)) |
1399 | |
1400 | def test_list_boot_images_finds_boot_image(self): |
1401 | - image = make_boot_image_params() |
1402 | - self.make_image_dir(image, self.tftproot) |
1403 | - self.assertItemsEqual([image], list_boot_images(self.tftproot)) |
1404 | + params = make_boot_image_storage_params() |
1405 | + self.make_image_dir(params, self.tftproot) |
1406 | + purposes = ['install', 'commissioning', 'xinstall'] |
1407 | + self.assertItemsEqual( |
1408 | + [make_image(params, purpose) for purpose in purposes], |
1409 | + list_boot_images(self.tftproot)) |
1410 | |
1411 | def test_list_boot_images_enumerates_boot_images(self): |
1412 | - images = [make_boot_image_params() for counter in range(3)] |
1413 | - for image in images: |
1414 | - self.make_image_dir(image, self.tftproot) |
1415 | - self.assertItemsEqual(images, list_boot_images(self.tftproot)) |
1416 | + params = [make_boot_image_storage_params() for counter in range(3)] |
1417 | + for param in params: |
1418 | + self.make_image_dir(param, self.tftproot) |
1419 | + self.assertItemsEqual( |
1420 | + [ |
1421 | + make_image(param, purpose) |
1422 | + for param in params |
1423 | + for purpose in ['install', 'commissioning', 'xinstall'] |
1424 | + ], |
1425 | + list_boot_images(self.tftproot)) |
1426 | |
1427 | def test_is_visible_subdir_ignores_regular_files(self): |
1428 | plain_file = self.make_file() |
1429 | |
1430 | === modified file 'src/provisioningserver/pxe/tftppath.py' |
1431 | --- src/provisioningserver/pxe/tftppath.py 2014-03-18 20:22:07 +0000 |
1432 | +++ src/provisioningserver/pxe/tftppath.py 2014-03-25 11:13:45 +0000 |
1433 | @@ -40,7 +40,7 @@ |
1434 | return "pxelinux.0" |
1435 | |
1436 | |
1437 | -# TODO: move this; it is now only used for testing. |
1438 | +# XXX allenap: move this; it is now only used for testing. |
1439 | def compose_config_path(mac): |
1440 | """Compose the TFTP path for a PXE configuration file. |
1441 | |
1442 | @@ -60,7 +60,11 @@ |
1443 | htype=ARP_HTYPE.ETHERNET, mac=mac) |
1444 | |
1445 | |
1446 | -def compose_image_path(arch, subarch, release, label, purpose): |
1447 | +# XXX rvb 2014-03-21 bug=1235479: The 'purpose' is made optional for now so |
1448 | +# that this method can cope with both the old layout (which had the 'purpose' |
1449 | +# as part of the images' path) and the new one. Once the old script is |
1450 | +# removed, this parameter should be removed as well. |
1451 | +def compose_image_path(arch, subarch, release, label, purpose=None): |
1452 | """Compose the TFTP path for a PXE kernel/initrd directory. |
1453 | |
1454 | The path returned is relative to the TFTP root, as it would be |
1455 | @@ -69,14 +73,16 @@ |
1456 | :param arch: Main machine architecture. |
1457 | :param subarch: Sub-architecture, or "generic" if there is none. |
1458 | :param release: Operating system release, e.g. "precise". |
1459 | - :param label: An image label, e.g. for a beta version, or "release" for |
1460 | - the default images. |
1461 | + :param label: Release label, e.g. "release" or "alpha-2". |
1462 | :param purpose: Purpose of the image, e.g. "install" or |
1463 | "commissioning". |
1464 | :return: Path for the corresponding image directory (containing a |
1465 | kernel and initrd) as exposed over TFTP. |
1466 | """ |
1467 | - return '/'.join([arch, subarch, release, label, purpose]) |
1468 | + elements = [arch, subarch, release, label] |
1469 | + if purpose is not None: |
1470 | + elements.append(purpose) |
1471 | + return '/'.join(elements) |
1472 | |
1473 | |
1474 | def locate_tftp_path(path, tftproot): |
1475 | @@ -143,23 +149,32 @@ |
1476 | |
1477 | |
1478 | def extract_image_params(path): |
1479 | - """Represent a list of TFTP path elements as a boot-image dict. |
1480 | + """Represent a list of TFTP path elements as a list of boot-image dicts. |
1481 | |
1482 | - The path must consist of a full [architecture, subarchitecture, release, |
1483 | - purpose] that identify a kind of boot that we may need an image for. |
1484 | + The path must consist of a full [architecture, subarchitecture, release] |
1485 | + that identify a kind of boot that we may need an image for. |
1486 | """ |
1487 | - arch, subarch, release, label, purpose = path |
1488 | - return dict( |
1489 | - architecture=arch, subarchitecture=subarch, release=release, |
1490 | - label=label, purpose=purpose) |
1491 | + arch, subarch, release, label = path |
1492 | + # XXX: rvb 2014-03-24: The images import script currently imports all the |
1493 | + # images for the configured selections (where a selection is an |
1494 | + # arch/subarch/series/label combination). When the import script grows the |
1495 | + # ability to import the images for a particular purpose, we need to change |
1496 | + # this code to report what is actually present. |
1497 | + purposes = ['commissioning', 'install', 'xinstall'] |
1498 | + return [ |
1499 | + dict( |
1500 | + architecture=arch, subarchitecture=subarch, |
1501 | + release=release, label=label, purpose=purpose) |
1502 | + for purpose in purposes |
1503 | + ] |
1504 | |
1505 | |
1506 | def list_boot_images(tftproot): |
1507 | """List the available boot images. |
1508 | |
1509 | :param tftproot: TFTP root directory. |
1510 | - :return: An iterable of dicts, describing boot images as consumed by |
1511 | - the report_boot_images API call. |
1512 | + :return: A list of dicts, describing boot images as consumed by the |
1513 | + `report_boot_images` API call. |
1514 | """ |
1515 | # The sub-directories directly under tftproot, if they contain |
1516 | # images, represent architectures. |
1517 | @@ -170,10 +185,12 @@ |
1518 | paths = [[subdir] for subdir in potential_archs] |
1519 | |
1520 | # Extend paths deeper into the filesystem, through the levels that |
1521 | - # represent sub-architecture, release, and purpose. Any directory |
1522 | + # represent sub-architecture, release, and label. Any directory |
1523 | # that doesn't extend this deep isn't a boot image. |
1524 | - for level in ['subarch', 'release', 'label', 'purpose']: |
1525 | + for level in ['subarch', 'release', 'label']: |
1526 | paths = drill_down(tftproot, paths) |
1527 | |
1528 | # Each path we find this way should be a boot image. |
1529 | - return [extract_image_params(path) for path in paths] |
1530 | + # This gets serialised to JSON, so we really have to return a list, not |
1531 | + # just any iterable. |
1532 | + return sum([extract_image_params(path) for path in paths], []) |
1533 | |
1534 | === modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py' |
1535 | --- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-03-20 22:36:32 +0000 |
1536 | +++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-03-25 11:13:45 +0000 |
1537 | @@ -175,11 +175,11 @@ |
1538 | subarchs = "generic", "special" |
1539 | releases = "precise", "trusty" |
1540 | labels = "beta-1", "release" |
1541 | - purposes = "commission", "install" |
1542 | + purposes = "commissioning", "install", "xinstall" |
1543 | |
1544 | # Create a TFTP file tree with a variety of subdirectories. |
1545 | tftpdir = self.make_dir() |
1546 | - for options in product(archs, subarchs, releases, labels, purposes): |
1547 | + for options in product(archs, subarchs, releases, labels): |
1548 | os.makedirs(os.path.join(tftpdir, *options)) |
1549 | |
1550 | # Ensure that list_boot_images() uses the above TFTP file tree. |
1551 | |
1552 | === modified file 'src/provisioningserver/testing/boot_images.py' |
1553 | --- src/provisioningserver/testing/boot_images.py 2014-03-11 07:34:00 +0000 |
1554 | +++ src/provisioningserver/testing/boot_images.py 2014-03-25 11:13:45 +0000 |
1555 | @@ -1,4 +1,4 @@ |
1556 | -# Copyright 2012 Canonical Ltd. This software is licensed under the |
1557 | +# Copyright 2012-2014 Canonical Ltd. This software is licensed under the |
1558 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1559 | |
1560 | """Test helpers for boot-image parameters.""" |
1561 | @@ -24,13 +24,26 @@ |
1562 | |
1563 | These are the parameters that together describe a kind of boot for |
1564 | which we may need a kernel and initrd: architecture, |
1565 | - sub-architecture, Ubuntu release, boot purpose and simplestreams |
1566 | - label. See the `tftppath` module for how these fit together. |
1567 | + sub-architecture, Ubuntu release, boot purpose, and release label. |
1568 | """ |
1569 | return dict( |
1570 | architecture=factory.make_name('architecture'), |
1571 | subarchitecture=factory.make_name('subarchitecture'), |
1572 | release=factory.make_name('release'), |
1573 | + label=factory.make_name('label'), |
1574 | purpose=factory.make_name('purpose'), |
1575 | + ) |
1576 | + |
1577 | + |
1578 | +def make_boot_image_storage_params(): |
1579 | + """Create a dict of boot-image parameters as used to store the image. |
1580 | + |
1581 | + These are the parameters that together describe a path to store a boot |
1582 | + image: architecture, sub-architecture, Ubuntu release, and release label. |
1583 | + """ |
1584 | + return dict( |
1585 | + architecture=factory.make_name('architecture'), |
1586 | + subarchitecture=factory.make_name('subarchitecture'), |
1587 | + release=factory.make_name('release'), |
1588 | label=factory.make_name('label'), |
1589 | ) |
1590 | |
1591 | === modified file 'src/provisioningserver/tests/test_config.py' |
1592 | --- src/provisioningserver/tests/test_config.py 2014-03-21 08:19:20 +0000 |
1593 | +++ src/provisioningserver/tests/test_config.py 2014-03-25 11:13:45 +0000 |
1594 | @@ -1,4 +1,4 @@ |
1595 | -# Copyright 2005-2013 Canonical Ltd. This software is licensed under the |
1596 | +# Copyright 2005-2014 Canonical Ltd. This software is licensed under the |
1597 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1598 | |
1599 | """Tests for provisioning configuration.""" |
1600 | @@ -136,15 +136,15 @@ |
1601 | 'images_directory': '/var/lib/maas/ephemeral', |
1602 | 'releases': None, |
1603 | }, |
1604 | - # XXX jtv 2014-03-21, bug=1295479: Unused until we start using |
1605 | - # the new import script. |
1606 | 'sources': [ |
1607 | { |
1608 | 'path': ( |
1609 | 'http://maas.ubuntu.com/images/ephemeral/releases/'), |
1610 | + 'keyring': ( |
1611 | + '/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg'), |
1612 | 'selections': [ |
1613 | { |
1614 | - 'arch': '*', |
1615 | + 'arches': ['*'], |
1616 | 'release': '*', |
1617 | 'subarches': ['*'], |
1618 | }, |
1619 | @@ -152,6 +152,7 @@ |
1620 | }, |
1621 | ], |
1622 | 'storage': '/var/lib/maas/boot-resources/', |
1623 | + 'configure_me': False, |
1624 | }, |
1625 | 'broker': { |
1626 | 'host': 'localhost', |
1627 | @@ -169,7 +170,7 @@ |
1628 | 'tftp': { |
1629 | 'generator': 'http://localhost/MAAS/api/1.0/pxeconfig/', |
1630 | 'port': 69, |
1631 | - 'root': "/var/lib/maas/tftp", |
1632 | + 'root': "/var/lib/maas/boot-resources/current/", |
1633 | }, |
1634 | } |
1635 | |
1636 | @@ -266,10 +267,10 @@ |
1637 | self.assertNotEqual(first_load['logfile'], second_load['logfile']) |
1638 | self.assertEqual(logfile, second_load['logfile']) |
1639 | self.assertIsNot(first_load['boot'], second_load['boot']) |
1640 | - first_load['boot']['architectures'] = [factory.make_name('otherarch')] |
1641 | + first_load['boot']['storage'] = [factory.make_name('otherstorage')] |
1642 | self.assertNotEqual( |
1643 | - first_load['boot']['architectures'], |
1644 | - second_load['boot']['architectures']) |
1645 | + first_load['boot']['storage'], |
1646 | + second_load['boot']['storage']) |
1647 | |
1648 | def test_oops_directory_without_reporter(self): |
1649 | # It is an error to omit the OOPS reporter if directory is specified. |
1650 | |
1651 | === modified file 'src/provisioningserver/tests/test_kernel_opts.py' |
1652 | --- src/provisioningserver/tests/test_kernel_opts.py 2014-03-17 08:48:46 +0000 |
1653 | +++ src/provisioningserver/tests/test_kernel_opts.py 2014-03-25 11:13:45 +0000 |
1654 | @@ -29,13 +29,12 @@ |
1655 | compose_arch_opts, |
1656 | compose_kernel_command_line, |
1657 | compose_preseed_opt, |
1658 | - EphemeralImagesDirectoryNotFound, |
1659 | + get_ephemeral_name, |
1660 | get_last_directory, |
1661 | ISCSI_TARGET_NAME_PREFIX, |
1662 | KernelParameters, |
1663 | prefix_target_name, |
1664 | ) |
1665 | -from provisioningserver.testing.config import ConfigFixture |
1666 | from testtools.matchers import ( |
1667 | Contains, |
1668 | ContainsAll, |
1669 | @@ -169,8 +168,6 @@ |
1670 | def test_xinstall_compose_kernel_command_line_inc_purpose_opts(self): |
1671 | # The result of compose_kernel_command_line includes the purpose |
1672 | # options for a non "xinstall" node. |
1673 | - get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name") |
1674 | - get_ephemeral_name.return_value = "RELEASE-ARCH" |
1675 | params = self.make_kernel_parameters(purpose="xinstall") |
1676 | cmdline = compose_kernel_command_line(params) |
1677 | self.assertThat( |
1678 | @@ -184,8 +181,6 @@ |
1679 | def test_commissioning_compose_kernel_command_line_inc_purpose_opts(self): |
1680 | # The result of compose_kernel_command_line includes the purpose |
1681 | # options for a non "commissioning" node. |
1682 | - get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name") |
1683 | - get_ephemeral_name.return_value = "RELEASE-ARCH" |
1684 | params = self.make_kernel_parameters(purpose="commissioning") |
1685 | cmdline = compose_kernel_command_line(params) |
1686 | self.assertThat( |
1687 | @@ -212,8 +207,6 @@ |
1688 | def test_compose_kernel_command_line_inc_common_opts(self): |
1689 | # Test that some kernel arguments appear on commissioning, install |
1690 | # and xinstall command lines. |
1691 | - get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name") |
1692 | - get_ephemeral_name.return_value = "RELEASE-ARCH" |
1693 | expected = ["nomodeset"] |
1694 | |
1695 | params = self.make_kernel_parameters( |
1696 | @@ -231,32 +224,12 @@ |
1697 | cmdline = compose_kernel_command_line(params) |
1698 | self.assertThat(cmdline, ContainsAll(expected)) |
1699 | |
1700 | - def create_ephemeral_info(self, name, arch, release): |
1701 | - """Create a pseudo-real ephemeral info file.""" |
1702 | - ephemeral_info = """ |
1703 | - release=%s |
1704 | - stream=ephemeral |
1705 | - label=release |
1706 | - serial=20120424 |
1707 | - arch=%s |
1708 | - name=%s |
1709 | - """ % (release, arch, name) |
1710 | - ephemeral_root = self.make_dir() |
1711 | - config = {"boot": {"ephemeral": {"images_directory": ephemeral_root}}} |
1712 | - self.useFixture(ConfigFixture(config)) |
1713 | - ephemeral_dir = os.path.join( |
1714 | - ephemeral_root, release, 'ephemeral', arch, release) |
1715 | - os.makedirs(ephemeral_dir) |
1716 | - factory.make_file( |
1717 | - ephemeral_dir, name='info', contents=ephemeral_info) |
1718 | - |
1719 | def test_compose_kernel_command_line_inc_purpose_opts_xinstall_node(self): |
1720 | # The result of compose_kernel_command_line includes the purpose |
1721 | # options for a "xinstall" node. |
1722 | - ephemeral_name = factory.make_name("ephemeral") |
1723 | params = self.make_kernel_parameters(purpose="xinstall") |
1724 | - self.create_ephemeral_info( |
1725 | - ephemeral_name, params.arch, params.release) |
1726 | + ephemeral_name = get_ephemeral_name( |
1727 | + params.arch, params.subarch, params.release, params.label) |
1728 | self.assertThat( |
1729 | compose_kernel_command_line(params), |
1730 | ContainsAll([ |
1731 | @@ -269,10 +242,9 @@ |
1732 | def test_compose_kernel_command_line_inc_purpose_opts_comm_node(self): |
1733 | # The result of compose_kernel_command_line includes the purpose |
1734 | # options for a "commissioning" node. |
1735 | - ephemeral_name = factory.make_name("ephemeral") |
1736 | params = self.make_kernel_parameters(purpose="commissioning") |
1737 | - self.create_ephemeral_info( |
1738 | - ephemeral_name, params.arch, params.release) |
1739 | + ephemeral_name = get_ephemeral_name( |
1740 | + params.arch, params.subarch, params.release, params.label) |
1741 | self.assertThat( |
1742 | compose_kernel_command_line(params), |
1743 | ContainsAll([ |
1744 | @@ -282,15 +254,6 @@ |
1745 | "iscsi_target_ip=%s" % params.fs_host, |
1746 | ])) |
1747 | |
1748 | - def test_compose_kernel_command_line_reports_error_about_missing_dir(self): |
1749 | - params = self.make_kernel_parameters(purpose="commissioning") |
1750 | - missing_dir = factory.make_name('missing-dir') |
1751 | - config = {"boot": {"ephemeral": {"images_directory": missing_dir}}} |
1752 | - self.useFixture(ConfigFixture(config)) |
1753 | - self.assertRaises( |
1754 | - EphemeralImagesDirectoryNotFound, |
1755 | - compose_kernel_command_line, params) |
1756 | - |
1757 | def test_compose_preseed_kernel_opt_returns_kernel_option(self): |
1758 | dummy_preseed_url = factory.make_name("url") |
1759 | self.assertEqual( |
1760 | |
1761 | === modified file 'src/provisioningserver/tests/test_maas_import_pxe_files.py' |
1762 | --- src/provisioningserver/tests/test_maas_import_pxe_files.py 2014-03-22 17:21:55 +0000 |
1763 | +++ src/provisioningserver/tests/test_maas_import_pxe_files.py 2014-03-25 11:13:45 +0000 |
1764 | @@ -16,6 +16,7 @@ |
1765 | |
1766 | import os |
1767 | from subprocess import check_call |
1768 | +import unittest |
1769 | |
1770 | from maastesting import root |
1771 | from maastesting.factory import factory |
1772 | @@ -168,6 +169,11 @@ |
1773 | |
1774 | def setUp(self): |
1775 | super(TestImportPXEFiles, self).setUp() |
1776 | + raise unittest.SkipTest( |
1777 | + "XXX rvb 2014-03-21 bug=1295479: Disabled. The " |
1778 | + "maas-import-pxe-files script has been replaced with a " |
1779 | + "new version to use simplestreams v2's data. These tests need " |
1780 | + "to be completely refactored.") |
1781 | self.tftproot = self.make_dir() |
1782 | self.config = {"tftp": {"root": self.tftproot}} |
1783 | self.config_fixture = ConfigFixture(self.config) |
1784 | |
1785 | === modified file 'src/provisioningserver/tests/test_upgrade_cluster.py' |
1786 | --- src/provisioningserver/tests/test_upgrade_cluster.py 2014-03-18 04:25:00 +0000 |
1787 | +++ src/provisioningserver/tests/test_upgrade_cluster.py 2014-03-25 11:13:45 +0000 |
1788 | @@ -25,10 +25,17 @@ |
1789 | import os.path |
1790 | |
1791 | from maastesting.factory import factory |
1792 | -from maastesting.matchers import MockCalledOnceWith |
1793 | +from maastesting.matchers import ( |
1794 | + MockCalledOnceWith, |
1795 | + MockNotCalled, |
1796 | + ) |
1797 | from maastesting.testcase import MAASTestCase |
1798 | -from mock import Mock |
1799 | +from mock import ( |
1800 | + ANY, |
1801 | + Mock, |
1802 | + ) |
1803 | from provisioningserver import upgrade_cluster |
1804 | +from provisioningserver.config import Config |
1805 | from provisioningserver.pxe.install_image import install_image |
1806 | from provisioningserver.testing.config import ConfigFixture |
1807 | from testtools.matchers import ( |
1808 | @@ -580,3 +587,50 @@ |
1809 | entries_before = listdir(tftproot) |
1810 | upgrade_cluster.add_label_directory_level_to_boot_images() |
1811 | self.assertItemsEqual(entries_before, listdir(tftproot)) |
1812 | + |
1813 | + |
1814 | +class TestGenerateBootResourcesConfig(MAASTestCase): |
1815 | + """Tests for the `generate_boot_resources_config` upgrade.""" |
1816 | + |
1817 | + def patch_rewrite_boot_resources_config(self): |
1818 | + """Patch `rewrite_boot_resources_config` with a mock.""" |
1819 | + return self.patch(upgrade_cluster, 'rewrite_boot_resources_config') |
1820 | + |
1821 | + def patch_config(self, config): |
1822 | + """Patch the `bootresources.yaml` config with a given dict.""" |
1823 | + original_load = Config.load_from_cache |
1824 | + |
1825 | + @classmethod |
1826 | + def fake_config_load(cls, filename=None): |
1827 | + """Fake `Config.load_from_cache`. |
1828 | + |
1829 | + Returns a susbtitute for `bootresources.yaml`, but defers to the |
1830 | + original implementation for other files. This means we can still |
1831 | + patch the original, and it means we'll probably get a tell-tale |
1832 | + error if any code underneath the tests accidentally tries to |
1833 | + load pserv.yaml. |
1834 | + """ |
1835 | + if os.path.basename(filename) == 'bootresources.yaml': |
1836 | + return config |
1837 | + else: |
1838 | + return original_load(Config, filename=filename) |
1839 | + |
1840 | + self.patch(Config, 'load_from_cache', fake_config_load) |
1841 | + |
1842 | + def test_does_nothing_if_configure_me_is_False(self): |
1843 | + self.patch_config({'boot': {'configure_me': False}}) |
1844 | + rewrite_config = self.patch_rewrite_boot_resources_config() |
1845 | + upgrade_cluster.generate_boot_resources_config() |
1846 | + self.assertThat(rewrite_config, MockNotCalled()) |
1847 | + |
1848 | + def test_does_nothing_if_configure_me_is_missing(self): |
1849 | + self.patch_config({'boot': {}}) |
1850 | + rewrite_config = self.patch_rewrite_boot_resources_config() |
1851 | + upgrade_cluster.generate_boot_resources_config() |
1852 | + self.assertThat(rewrite_config, MockNotCalled()) |
1853 | + |
1854 | + def test_rewrites_if_configure_me_is_True(self): |
1855 | + self.patch_config({'boot': {'configure_me': True}}) |
1856 | + rewrite_config = self.patch_rewrite_boot_resources_config() |
1857 | + upgrade_cluster.generate_boot_resources_config() |
1858 | + self.assertThat(rewrite_config, MockCalledOnceWith(ANY)) |
1859 | |
1860 | === modified file 'src/provisioningserver/tftp.py' |
1861 | --- src/provisioningserver/tftp.py 2014-03-22 17:21:55 +0000 |
1862 | +++ src/provisioningserver/tftp.py 2014-03-25 11:13:45 +0000 |
1863 | @@ -329,6 +329,8 @@ |
1864 | :param root: The root directory for this TFTP server. |
1865 | :param port: The port on which each server should be started. |
1866 | :param generator: The URL to be queried for PXE configuration. |
1867 | + This will normally point to the `pxeconfig` endpoint on the |
1868 | + region-controller API. |
1869 | """ |
1870 | super(TFTPService, self).__init__() |
1871 | self.backend, self.port = TFTPBackend(root, generator), port |
1872 | |
1873 | === modified file 'src/provisioningserver/upgrade_cluster.py' |
1874 | --- src/provisioningserver/upgrade_cluster.py 2014-03-18 04:10:37 +0000 |
1875 | +++ src/provisioningserver/upgrade_cluster.py 2014-03-25 11:13:45 +0000 |
1876 | @@ -44,7 +44,10 @@ |
1877 | from shutil import rmtree |
1878 | |
1879 | from provisioningserver.config import Config |
1880 | -from provisioningserver.utils import ensure_dir |
1881 | +from provisioningserver.utils import ( |
1882 | + ensure_dir, |
1883 | + locate_config, |
1884 | + ) |
1885 | |
1886 | |
1887 | logger = getLogger(__name__) |
1888 | @@ -246,6 +249,25 @@ |
1889 | move_real_boot_image(tftproot, image) |
1890 | |
1891 | |
1892 | +def rewrite_boot_resources_config(config_file): |
1893 | + """Rewrite the `bootresources.yaml` configuration.""" |
1894 | + |
1895 | + |
1896 | +def generate_boot_resources_config(): |
1897 | + """Upgrade hook: rewrite `bootresources.yaml` based on boot images. |
1898 | + |
1899 | + This finds boot images downloaded into the old, pre-Simplestreams tftp |
1900 | + root, and writes a boot-resources configuration to import a similar set of |
1901 | + images using Simplestreams. |
1902 | + """ |
1903 | + config_file = locate_config('bootresources.yaml') |
1904 | + boot_resources = Config.load_from_cache(config_file) |
1905 | + if not boot_resources['boot'].get('configure_me', False): |
1906 | + # Already configured. |
1907 | + return |
1908 | + rewrite_boot_resources_config(config_file) |
1909 | + |
1910 | + |
1911 | # Upgrade hooks, from oldest to newest. The hooks are callables, taking no |
1912 | # arguments. They are called in order. |
1913 | # |
1914 | @@ -253,6 +275,7 @@ |
1915 | # no record of previous upgrades. |
1916 | UPGRADE_HOOKS = [ |
1917 | add_label_directory_level_to_boot_images, |
1918 | + generate_boot_resources_config, |
1919 | ] |
1920 | |
1921 |