Merge lp:~jtv/maas/bootresources-rewrite-marker into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
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
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_boot_resources_config stub will look for old-style boot images on the cluster's filesystem (reviving the old code for list_boot_images as a special-purpose migration helper) and rewrite bootresources.yaml with sources/selections to get similar downloads from the new script.

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

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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