Merge lp:~maas-maintainers/maas/new-import-script-integration into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 2171
Proposed branch: lp:~maas-maintainers/maas/new-import-script-integration
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 2453 lines (+793/-872)
30 files modified
contrib/maas-cluster-http.conf (+2/-2)
etc/maas/bootresources.yaml (+62/-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 (+39/-446)
setup.py (+1/-0)
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 (+341/-86)
src/provisioningserver/import_images/tests/test_boot_resources.py (+54/-0)
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)
To merge this branch: bzr merge lp:~maas-maintainers/maas/new-import-script-integration
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Graham Binns (community) Approve
Review via email: mp+211470@code.launchpad.net

Commit message

Merge Oleg Strikov's new version of maas-import-pxe-files into trunk. Previously, m-i-p-f was a bash script; now it's a much more functional python script.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Approving on behalf of all who sailed the merry seas of this here branch.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Re-landing this with the latest changes to the feature branch. After that, let's mark this branch as Merged so it doesn't stick around as an open development branch!

review: Needs Fixing
Revision history for this message
Jeroen T. Vermeulen (jtv) :
review: Approve

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 17:45:57 +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 17:45:57 +0000
18@@ -0,0 +1,62 @@
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-v2/daily/"
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+ - release: "precise"
69+ arches: ["i386", "amd64"]
70+ subarches: ["generic"]
71+ #selections:
72+ # - release: "*"
73+ # arches: ["*"]
74+ # subarches: ["*"]
75+## other source example:
76+# - path: "http://hyperscale.ubuntu.com/images/"
77+# selections:
78+# - release: "*"
79+# arches: ["*"]
80+# subarches: ['*']
81
82=== modified file 'etc/maas/import_ephemerals'
83--- etc/maas/import_ephemerals 2014-03-18 00:29:45 +0000
84+++ etc/maas/import_ephemerals 2014-03-25 17:45:57 +0000
85@@ -1,13 +1,13 @@
86 # Legacy configuration file for the maas-import-ephemerals script.
87 #
88 # This file is obsolete, but the script will still read it for compatibility.
89-# Configure /etc/maas/pserv.yaml by preference.
90+# Configure /etc/maas/bootresources.yaml by preference.
91
92 ## Include settings from import_pxe_files.
93 [ ! -f /etc/maas/import_pxe_files ] || . /etc/maas/import_pxe_files
94
95 # These options can be defined here, although for future compatibility they
96-# should be set in pserv.yaml instead:
97+# should be set in bootresources.yaml instead:
98
99 #DATA_DIR="/var/lib/maas/ephemeral"
100 #ARCHES="amd64/generic i386/generic"
101
102=== modified file 'etc/maas/pserv.yaml'
103--- etc/maas/pserv.yaml 2014-02-10 22:32:17 +0000
104+++ etc/maas/pserv.yaml 2014-03-25 17:45:57 +0000
105@@ -38,31 +38,3 @@
106 # generator: http://localhost/MAAS/api/1.0/pxeconfig/
107 generator: http://localhost:5243/api/1.0/pxeconfig/
108
109-## Boot configuration.
110-boot:
111- ## CPU architectures for which boot images should be downloaded from the
112- ## server, e.g. ['i386/generic', 'amd64/generic']. MAAS needs these images
113- ## in order to boot nodes up.
114- ##
115- ## Leave this option out to download images for all architectures.
116- #
117- # architectures:
118-
119- ## Settings for ephemeral boot images. These images are used when
120- ## commissioning nodes, and during fast-path installation.
121- #
122- ephemeral:
123-
124- ## Directory where ephemeral boot images and related state should be
125- ## stored.
126- #
127- # images_directory: /var/lib/maas/ephemeral
128- images_directory: /var/lib/maas/ephemeral
129-
130- ## Releases for which ephemeral images should be downloaded.
131- ## These images are quite large (about a quarter GB each), so you may want
132- ## to restrict these separately even if you do want the regular install
133- ## images for all releases. Leave this out to download all currently
134- ## supported releases.
135- #
136- # releases:
137
138=== removed symlink 'etc/maas/templates/pxe/config.commissioning.armhf.template'
139=== target was u'config.install.armhf.template'
140=== modified file 'etc/maas/templates/pxe/config.install.armhf.template'
141--- etc/maas/templates/pxe/config.install.armhf.template 2013-10-24 02:46:03 +0000
142+++ etc/maas/templates/pxe/config.install.armhf.template 2014-03-25 17:45:57 +0000
143@@ -2,11 +2,6 @@
144
145 LABEL execute
146 {{# SAY is not implemented in U-Boot }}
147- {{if kernel_params.release not in ("precise", "quantal")}}
148- {{# Return a copy of kernel_params with an overridden subarch.
149- See https://bugs.launchpad.net/maas/+bug/1166994 }}
150- {{py: kernel_params=kernel_params(subarch="generic")}}
151- {{endif}}
152 KERNEL {{kernel_params | kernel_path }}
153 INITRD {{kernel_params | initrd_path }}
154 APPEND {{kernel_params | kernel_command}}
155
156=== modified file 'scripts/maas-import-pxe-files'
157--- scripts/maas-import-pxe-files 2014-03-25 09:25:40 +0000
158+++ scripts/maas-import-pxe-files 2014-03-25 17:45:57 +0000
159@@ -1,447 +1,40 @@
160-#!/usr/bin/env bash
161-# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
162+#!/usr/bin/env python2.7
163+# Copyright 2014 Canonical Ltd. This software is licensed under the
164 # GNU Affero General Public License version 3 (see the file LICENSE).
165-#
166-# Download static files needed for net-booting nodes through TFTP:
167-# pre-boot loader, kernels, and initrd images.
168-#
169-# This script downloads the required files into the TFTP home directory
170-# (by default, /var/lib/maas/tftp). Run it with the necessarily privileges
171-# to write them there.
172-
173-# Exit immediately if a command exits with a non-zero status.
174-set -o errexit
175-# Treat unset variables as an error when substituting.
176-set -o nounset
177-
178-# Load settings if available.
179-settings="/etc/maas/import_pxe_files"
180-[ -r $settings ] && . $settings
181-
182-# Location of the GPG keyring for the Ubuntu archive.
183-GPG_KEYRING="${GPG_KEYRING:-/usr/share/keyrings/ubuntu-archive-keyring.gpg}"
184-
185-# Whether to skip checking GPG keys (necessary for testing of this script).
186-# Set this to a nonempty string to skip checking. This is needed for test
187-# programs, because they can't possibly sign with the right key.
188-IGNORE_GPG="${IGNORE_GPG:-}"
189-
190-# Download locations for Ubuntu releases. When the cluster controller runs
191-# the import scripts, it provides settings from the server side.
192-MAIN_ARCHIVE=${MAIN_ARCHIVE:-http://archive.ubuntu.com/ubuntu/}
193-PORTS_ARCHIVE=${PORTS_ARCHIVE:-http://ports.ubuntu.com/ubuntu-ports/}
194-
195-# Ubuntu releases that are to be downloaded.
196-SUPPORTED_RELEASES=$(distro-info --supported)
197-RELEASES=${RELEASES:-$SUPPORTED_RELEASES}
198-
199-# The current Ubuntu release.
200-STABLE_RELEASE=${STABLE_RELEASE:-$(distro-info --stable)}
201-
202-# Supported architectures.
203-ARCHES=${ARCHES:-amd64/generic i386/generic armhf/highbank armhf/generic}
204-
205-# Command line to download a resource at a given URL into the current
206-# directory. A wget command line will work here, but curl will do as well.
207-DOWNLOAD=${DOWNLOAD:-wget --no-verbose}
208-
209-# Whether to download ephemeral images as well: "1" for yes, "0" for no.
210-# Default is yes.
211-IMPORT_EPHEMERALS=${IMPORT_EPHEMERALS:-1}
212-
213-# Whether to check for broken grub2 efinet: "1" for yes, "0" for no.
214-# Default is yes.
215-CHECK_BROKEN_EFINET=${CHECK_BROKEN_EFINET:-1}
216-
217-# Whether to skip getting shim-signed from amd64-binary archive: "1" for yes, "0" for no.
218-# This is used for testing, Default is no.
219-SKIP_SHIM_SIGNED=${SKIP_SHIM_SIGNED:-0}
220-
221-fail() {
222- local msg=$1
223-
224- echo $msg >&2
225- exit 1
226-}
227-
228-# Show script usage/summary.
229-show_usage() {
230- echo "Usage: ${0##*/}"
231- echo
232- echo "This helper script downloads the relevant boot images from an "
233- echo "Ubuntu archive and uses 'maas' to provision them for PXE booting "
234- echo "from TFTP."
235- echo
236- echo "This script takes no arguments, but you can adjust some parameters "
237- echo -e "by editing the config file found at \033[1m$settings\033[0m."
238- echo
239- echo "MAAS homepage:<http://maas.ubuntu.com>"
240- echo
241-}
242-
243-# Return a URL that points to the images directory for the relevant
244-# release/arch.
245-compose_installer_base_url() {
246- local arch=$1 release=$2
247-
248- case $arch in
249- amd64/*|i386/*)
250- local installer_url="$MAIN_ARCHIVE/dists/$release/main/installer-${arch%%/*}"
251- echo "$installer_url/current/images/"
252- ;;
253- armhf/*)
254- # No ARM server installers were available in precise, so always go for -updates for now
255- # A better general fix is LP: #1052397
256- if [ "$release" = "precise" ]; then
257- updates=-updates
258- else
259- updates=
260- fi
261- local installer_url="$PORTS_ARCHIVE/dists/${release}${updates}/main/installer-${arch%%/*}"
262- echo "$installer_url/current/images/"
263- ;;
264- *)
265- echo "Unknown architecture: $arch" >&2
266- exit 1
267- ;;
268- esac
269-}
270-
271-# Return the URL part that is appended to the base url that gives the location
272-# of the images.
273-compose_installer_download_url_postfix() {
274- local arch=$1
275-
276- case $arch in
277- amd64/*|i386/*)
278- echo "netboot/ubuntu-installer/${arch%%/*}/"
279- ;;
280- armhf/*)
281- echo "${arch#*/}/netboot/"
282- ;;
283- *)
284- echo "Unknown architecture: $arch" >&2
285- exit 1
286- ;;
287- esac
288-}
289-
290-# Put together a full URL for where the installer files for architecture $1
291-# and release $2 can be downloaded.
292-compose_installer_download_url() {
293- local arch=$1 release=$2
294-
295- base_url=$(compose_installer_base_url $arch $release)
296- postfix=$(compose_installer_download_url_postfix $arch)
297-
298- echo "$base_url/$postfix"
299-}
300-
301-# Fetch MD5SUMS file. This returns false (i.e. nonzero) in the case of
302-# survivable failure, so that the caller can skip this image and move on.
303-fetch_server_md5sums() {
304- local base_url=$1
305-
306- if ! $DOWNLOAD "$base_url/MD5SUMS"
307- then
308- echo "Unable to download $base_url/MD5SUMS" >&2
309- return 1
310- fi
311-
312- if [ "x$IGNORE_GPG" == "x" ]
313- then
314- if ! $DOWNLOAD "$base_url/MD5SUMS.gpg"
315- then
316- echo "Unable to download $base_url/MD5SUMS.gpg" >&2
317- return 1
318- fi
319-
320- if ! gpg --keyring=$GPG_KEYRING --verify MD5SUMS.gpg MD5SUMS >/dev/null 2>/dev/null
321- then
322- echo "Failed to verify MD5SUMS via $GPG_KEYRING ($base_url/MD5SUMS)" >&2
323- return 1
324- fi
325- fi
326- return 0
327-}
328-
329-get_md5sum_for_file() {
330- local filename=$1
331-
332- # The filename supplied in $1 must be the full path as seen in the
333- # MD5SUMS file. The files are rooted from a single place so the grepped
334- # string will only match once.
335- server_md5sum=$(grep $filename MD5SUMS|awk '{print $1}') ||
336- fail "failed to find checksum for $filename"
337- echo $server_md5sum
338-}
339-
340-check_checksum() {
341- local server_md5sum=$1 file_on_disk=$2
342- local md5sum
343-
344- md5sum=$(md5sum $file_on_disk|awk '{print $1}')
345-
346- if [ "$md5sum" != "$server_md5sum" ]; then
347- fail "md5 checksum mismatch for $file_on_disk: expected $server_md5sum, got $md5sum"
348- fi
349-}
350-
351-# Return a list of files for architecture $1 and release $2 that need to be
352-# downloaded
353-compose_installer_download_files() {
354- local arch=$1 release=$2
355-
356- case $arch in
357- amd64/*|i386/*)
358- echo "linux initrd.gz"
359- ;;
360- armhf/*)
361- echo "vmlinuz initrd.gz"
362- ;;
363- *)
364- echo "Unknown architecture: $arch" >&2
365- exit 1
366- ;;
367- esac
368-}
369-
370-
371-# Rename downloaded files for architecture $1 and release $2 into the form that
372-# MAAS expects them
373-rename_installer_download_files() {
374- local arch=$1 release=$2
375-
376- case $arch in
377- amd64/*|i386/*)
378- # do nothing
379- ;;
380- armhf/*)
381- mv vmlinuz linux
382- ;;
383- *)
384- echo "Unknown architecture: $arch" >&2
385- exit 1
386- ;;
387- esac
388-}
389-
390-
391-# Copy the pre-boot loader pxelinux.0, and modules we need, from the
392-# installed syslinux version. Install it into the TFTP tree for
393-# netbooting.
394-update_pre_boot_loader() {
395- for loader_file in pxelinux.0 chain.c32 ifcpu64.c32
396- do
397- maas-provision install-pxe-bootloader \
398- --loader="/usr/lib/syslinux/$loader_file"
399- done
400-}
401-
402-# Download load the archive package list, and check that it contains
403-# the needed package.
404-download_archive_package_list() {
405- local archive_url="$MAIN_ARCHIVE/dists/$1/main/binary-amd64/Packages.gz"
406- if $DOWNLOAD $archive_url
407- then
408- gunzip "Packages.gz"
409- if grep -Fxq "Package: $2" "Packages"
410- then
411- return 0
412- fi
413- fi
414- return 1
415-}
416-
417-# Copy the signed efi and grubnet loader. Install it into the TFTP tree
418-# for UEFI netbooting.
419-update_uefi_boot_loader() {
420- local download_dir=$(mktemp -d)
421- pushd "$download_dir" >/dev/null
422-
423- # Resolves bug=1295644 - Download the package manually from the
424- # archive giving support for other architectures.
425- if test "$SKIP_SHIM_SIGNED" != "1"
426- then
427- if ! download_archive_package_list "$STABLE_RELEASE-updates" "shim-signed"
428- then
429- if ! download_archive_package_list "$STABLE_RELEASE" "shim-signed"
430- then
431- echo "Failed to obtain shim-signed." >&2
432- popd >/dev/null
433- rm -rf -- "$download_dir"
434- return 1
435- fi
436- fi
437-
438- # Extract the shim-signed package information.
439- local package="$(awk "/^Package: shim-signed$/{f=1}f;/MD5sum: .+$/{f=0}" Packages)"
440- local filepath="$(echo "${package}" | sed -n "s/^Filename: \(.*\)$/\1/p")"
441- local filename="${filepath##*/}"
442- local md5sum="$(echo "${package}" | sed -n "s/^MD5sum: \(.*\)$/\1/p")"
443- local url="${MAIN_ARCHIVE}/${filepath}"
444-
445- # Download the package from the pool and check that md5sum match.
446- trap 'popd >/dev/null; rm -rf -- "$download_dir"' RETURN
447- if ! $DOWNLOAD $url
448- then
449- echo "Failed to download ${filename}." >&2
450- return 1
451- fi
452- check_checksum "$md5sum" "$filename"
453-
454- # Extract the shim.efi.signed file from the package, and install it for maas to use.
455- dpkg -x "$filename" .
456- mv "usr/lib/shim/shim.efi.signed" "bootx64.efi"
457- maas-provision install-pxe-bootloader --loader="bootx64.efi"
458- fi
459-
460- local grub_version=$STABLE_RELEASE
461- if test "$CHECK_BROKEN_EFINET" != "0"
462- then
463- # grubnetx64 will not work below version trusty, as efinet is broken
464- # when loading kernel, warn and force trusty
465- if [ $grub_version == "saucy" -o $grub_version == "quantal" -o $grub_version == "precise" ];
466- then
467- echo "Warning: ${grub_version} and ${grub_version}-updates doesn't support UEFI, switching to trusty."
468- grub_version="trusty"
469- fi
470- fi
471-
472- local grubnet_url="$MAIN_ARCHIVE/dists/$grub_version-updates/main/uefi/grub2-amd64/current/grubnetx64.efi.signed"
473- if ! $DOWNLOAD $grubnet_url
474- then
475- echo "$grub_version-updates not available, falling back to $grub_version"
476- grubnet_url="$MAIN_ARCHIVE/dists/$grub_version/main/uefi/grub2-amd64/current/grubnetx64.efi.signed"
477- if ! $DOWNLOAD $grubnet_url
478- then
479- echo "Failed to download $grubnet_url" >&2
480- return 1
481- fi
482- fi
483- mv "grubnetx64.efi.signed" "grubx64.efi"
484- maas-provision install-pxe-bootloader --loader="grubx64.efi"
485- maas-provision install-uefi-config
486-
487- trap - RETURN
488- popd >/dev/null
489- rm -rf -- "$download_dir"
490-}
491-
492-
493-# Download kernel/initrd for installing Ubuntu release $2 for
494-# architecture $1.
495-download_install_files() {
496- local arch=$1 release=$2
497- local files file url file_prefix filename_in_md5sums_file md5sum
498-
499- files=$(compose_installer_download_files $arch $release)
500- url=$(compose_installer_download_url $arch $release)
501-
502- mkdir "install"
503- pushd "install" >/dev/null
504- if ! fetch_server_md5sums $(compose_installer_base_url $arch $release)
505- then
506- echo "Failed to download MD5 sums for $arch $release." >&2
507- popd >/dev/null
508- rm -rf "install"
509- return 1
510- fi
511- echo "MD5SUMS GPG signature OK for $arch $release"
512- for file in $files
513- do
514- if ! $DOWNLOAD $url/$file
515- then
516- # Download failed. Log error, and skip this image.
517- echo "Failed to download $url/$file" >&2
518- popd >/dev/null
519- rm -rf "install"
520- return 1
521- fi
522- file_prefix=$(compose_installer_download_url_postfix $arch)
523- filename_in_md5sums_file=./$file_prefix$file
524- md5sum=$(get_md5sum_for_file $filename_in_md5sums_file)
525- check_checksum $md5sum $file
526- echo "'$file' md5sum OK"
527- done
528- rename_installer_download_files $arch $release
529- popd >/dev/null
530- return 0
531-}
532-
533-# Download kernel/initrd for installing Ubuntu release $2 for
534-# architecture $1, and install them into the TFTP directory hierarchy.
535-update_install_files() {
536- local arch=$1 release=$2
537-
538- # Try the -updates pocket first, and fall back to release if it failed.
539- if ! download_install_files $arch ${release}-updates
540- then
541- echo "$release-updates not available, falling back to release"
542- if ! download_install_files $arch $release
543- then
544- return
545- fi
546- fi
547-
548- maas-provision install-pxe-image \
549- --arch="${arch%%/*}" --subarch="${arch#*/}" \
550- --release=$release --purpose="install" \
551- --image="install"
552-}
553-
554-
555-# Download and install the "install" images.
556-import_install_images() {
557- local arch release DOWNLOAD_DIR
558-
559- DOWNLOAD_DIR=$(mktemp -d)
560- echo "Downloading to temporary location $DOWNLOAD_DIR."
561- pushd -- $DOWNLOAD_DIR
562-
563- for arch in $ARCHES
564- do
565- for release in $RELEASES
566- do
567- update_install_files $arch $release
568- done
569- done
570-
571- popd
572- rm -rf -- $DOWNLOAD_DIR
573-}
574-
575-
576-# Download and install the ephemeral images.
577-import_ephemeral_images() {
578- if test "$IMPORT_EPHEMERALS" != "0"
579- then
580- maas-import-ephemerals
581- fi
582-}
583-
584-
585-main() {
586- # All files we create here are public. The TFTP user will need to be
587- # able to read them.
588- umask a+r
589-
590- update_pre_boot_loader
591- update_uefi_boot_loader
592- import_install_images
593- import_ephemeral_images
594-}
595-
596-# check for commandline arguments
597-if [ $# -gt 0 ]
598- then
599- case $1 in
600- "-h"|"--help") show_usage ; exit ;;
601- esac
602-fi
603-
604-if [ ! -f "$GPG_KEYRING" ]; then
605- fail "gpg keyring $GPG_KEYRING is not a file"
606-fi
607-
608-main
609+
610+"""Import boot resources into MAAS cluster controller."""
611+
612+from __future__ import (
613+ absolute_import,
614+ print_function,
615+ unicode_literals,
616+ )
617+
618+str = None
619+
620+__metaclass__ = type
621+
622+import sys
623+from provisioningserver.import_images.boot_resources import (
624+ main,
625+ make_arg_parser,
626+ NoConfigFile,
627+ logger,
628+ )
629+
630+if __name__ == "__main__":
631+ parser = make_arg_parser(__doc__)
632+ args = parser.parse_args()
633+ try:
634+ main(args)
635+ except NoConfigFile:
636+ logger.error("Config file %s not found." % args.config_file)
637+ sys.exit(1)
638+ except Exception:
639+ # logger.exception() will log the error message, followed by the
640+ # exception's stack trace. Using it here rather than allowing
641+ # the exception to kill the process unhandled means we can be
642+ # sure about where the exception ends up, rather than it
643+ # potentially vanishing.
644+ logger.exception("Unhandled exception; unable to continue.")
645+ sys.exit(1)
646
647=== modified file 'setup.py'
648--- setup.py 2014-02-19 21:28:38 +0000
649+++ setup.py 2014-03-25 17:45:57 +0000
650@@ -65,6 +65,7 @@
651 data_files=[
652 ('/etc/maas',
653 ['etc/maas/pserv.yaml',
654+ 'etc/maas/bootresources.yaml',
655 'etc/maas_cluster.conf',
656 'etc/txlongpoll.yaml',
657 'contrib/maas_local_celeryconfig.py',
658
659=== modified file 'src/maasserver/models/bootimage.py'
660--- src/maasserver/models/bootimage.py 2014-03-18 10:33:34 +0000
661+++ src/maasserver/models/bootimage.py 2014-03-25 17:45:57 +0000
662@@ -154,7 +154,7 @@
663 # Boot purpose (e.g. "commissioning" or "install") that the image is for.
664 purpose = CharField(max_length=255, blank=False, editable=False)
665
666- # "Label" as in simplestreams parlance. (e.g. "release", "beta1")
667+ # "Label" as in simplestreams parlance. (e.g. "release", "beta-1")
668 label = CharField(
669 max_length=255, blank=False, editable=False, default="release")
670
671
672=== modified file 'src/maasserver/preseed.py'
673--- src/maasserver/preseed.py 2014-01-28 02:37:29 +0000
674+++ src/maasserver/preseed.py 2014-03-25 17:45:57 +0000
675@@ -39,7 +39,9 @@
676 PRESEED_TYPE,
677 USERDATA_TYPE,
678 )
679+from maasserver.exceptions import MAASAPIException
680 from maasserver.models import (
681+ BootImage,
682 Config,
683 DHCPLease,
684 )
685@@ -94,10 +96,29 @@
686 cluster_host = pick_cluster_controller_address(node)
687 # XXX rvb(?): The path shouldn't be hardcoded like this, but rather synced
688 # somehow with the content of contrib/maas-cluster-http.conf.
689+ arch, subarch = node.architecture.split('/')
690+ purpose = 'xinstall'
691+ image = BootImage.objects.get_latest_image(
692+ node.nodegroup, arch, subarch, series, purpose)
693+ if image is None:
694+ raise MAASAPIException(
695+ "Error generating the URL of curtin's root-tgz file. "
696+ "No image could be found for the given selection: "
697+ "arch=%s, subarch=%s, series=%s, purpose=%s." % (
698+ arch,
699+ subarch,
700+ series,
701+ purpose
702+ ))
703+ dyn_uri = '/'.join([
704+ arch,
705+ subarch,
706+ series,
707+ image.label,
708+ 'root-tgz'
709+ ])
710 return (
711- "http://" + cluster_host + "/MAAS/static/images/" +
712- node.architecture + "/" + series +
713- "/xinstall/root.tar.gz")
714+ "http://" + cluster_host + "/MAAS/static/images/" + dyn_uri)
715
716
717 def get_curtin_config(node):
718
719=== modified file 'src/maasserver/tests/test_preseed.py'
720--- src/maasserver/tests/test_preseed.py 2014-03-06 05:57:48 +0000
721+++ src/maasserver/tests/test_preseed.py 2014-03-25 17:45:57 +0000
722@@ -27,6 +27,7 @@
723 NODEGROUPINTERFACE_MANAGEMENT,
724 PRESEED_TYPE,
725 )
726+from maasserver.exceptions import MAASAPIException
727 from maasserver.models import Config
728 from maasserver.preseed import (
729 compose_enlistment_preseed_url,
730@@ -574,6 +575,11 @@
731
732 def test_get_curtin_userdata(self):
733 node = factory.make_node()
734+ arch, subarch = node.architecture.split('/')
735+ factory.make_boot_image(
736+ architecture=arch, subarchitecture=subarch,
737+ release=node.get_distro_series(), purpose='xinstall',
738+ nodegroup=node.nodegroup)
739 node.use_fastpath_installer()
740 user_data = get_curtin_userdata(node)
741 # Just check that the user data looks good.
742@@ -603,20 +609,51 @@
743 self.assertItemsEqual(['curtin_preseed'], context)
744 self.assertIn('cloud-init', context['curtin_preseed'])
745
746- def test_get_curtin_installer_url(self):
747+ def test_get_curtin_installer_url_returns_url(self):
748 # Exclude DISTRO_SERIES.default. It's a special value that defers
749 # to a run-time setting which we don't provide in this test.
750 series = factory.getRandomEnum(
751 DISTRO_SERIES, but_not=DISTRO_SERIES.default)
752- arch = make_usable_architecture(self)
753- node = factory.make_node(architecture=arch, distro_series=series)
754+ architecture = make_usable_architecture(self)
755+ node = factory.make_node(
756+ architecture=architecture, distro_series=series)
757+ arch, subarch = architecture.split('/')
758+ boot_image = factory.make_boot_image(
759+ architecture=arch, subarchitecture=subarch, release=series,
760+ purpose='xinstall', nodegroup=node.nodegroup)
761+
762 installer_url = get_curtin_installer_url(node)
763+
764 [interface] = node.nodegroup.get_managed_interfaces()
765 self.assertEqual(
766- 'http://%s/MAAS/static/images/%s/%s/xinstall/root.tar.gz' % (
767- interface.ip, arch, series),
768+ 'http://%s/MAAS/static/images/%s/%s/%s/%s/root-tgz' % (
769+ interface.ip, arch, subarch, series, boot_image.label),
770 installer_url)
771
772+ def test_get_curtin_installer_url_fails_if_no_boot_image(self):
773+ series = factory.getRandomEnum(
774+ DISTRO_SERIES, but_not=DISTRO_SERIES.default)
775+ architecture = make_usable_architecture(self)
776+ node = factory.make_node(
777+ architecture=architecture, distro_series=series)
778+ # Generate a boot image with a different arch/subarch.
779+ factory.make_boot_image(
780+ architecture=factory.make_name('arch'),
781+ subarchitecture=factory.make_name('subarch'), release=series,
782+ purpose='xinstall', nodegroup=node.nodegroup)
783+
784+ error = self.assertRaises(
785+ MAASAPIException, get_curtin_installer_url, node)
786+ arch, subarch = architecture.split('/')
787+ msg = (
788+ "No image could be found for the given selection: "
789+ "arch=%s, subarch=%s, series=%s, purpose=xinstall." % (
790+ arch,
791+ subarch,
792+ node.get_distro_series(),
793+ ))
794+ self.assertIn(msg, "%s" % error)
795+
796 def test_get_preseed_type_for(self):
797 normal = factory.make_node()
798 normal.use_traditional_installer()
799
800=== modified file 'src/metadataserver/tests/test_api.py'
801--- src/metadataserver/tests/test_api.py 2014-03-05 02:08:23 +0000
802+++ src/metadataserver/tests/test_api.py 2014-03-25 17:45:57 +0000
803@@ -39,9 +39,9 @@
804 Tag,
805 )
806 from maasserver.testing import reload_object
807-from maasserver.testing.testcase import MAASServerTestCase
808 from maasserver.testing.factory import factory
809 from maasserver.testing.oauthclient import OAuthAuthenticatedClient
810+from maasserver.testing.testcase import MAASServerTestCase
811 from maastesting.djangotestcase import DjangoTestCase
812 from maastesting.matchers import MockCalledOnceWith
813 from maastesting.utils import sample_binary_data
814@@ -392,6 +392,11 @@
815
816 def test_curtin_user_data_view_returns_curtin_data(self):
817 node = factory.make_node()
818+ arch, subarch = node.architecture.split('/')
819+ factory.make_boot_image(
820+ architecture=arch, subarchitecture=subarch,
821+ release=node.get_distro_series(), purpose='xinstall',
822+ nodegroup=node.nodegroup)
823 client = make_node_client(node)
824 response = client.get(
825 reverse('curtin-metadata-user-data', args=['latest']))
826
827=== modified file 'src/provisioningserver/boot_images.py'
828--- src/provisioningserver/boot_images.py 2014-03-19 13:54:20 +0000
829+++ src/provisioningserver/boot_images.py 2014-03-25 17:45:57 +0000
830@@ -1,11 +1,7 @@
831-# Copyright 2012 Canonical Ltd. This software is licensed under the
832+# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
833 # GNU Affero General Public License version 3 (see the file LICENSE).
834
835-"""Dealing with boot images.
836-
837-Most of the lower-level logic is in the `tftppath` module, because it must
838-correspond closely to the structure of the TFTP filesystem hierarchy.
839-"""
840+"""Dealing with boot images."""
841
842 from __future__ import (
843 absolute_import,
844@@ -69,6 +65,6 @@
845 return
846
847 images = tftppath.list_boot_images(
848- Config.load_from_cache()['tftp']['root'])
849+ Config.load_from_cache()['boot']['storage'] + '/current/')
850
851 submit(maas_url, api_credentials, images)
852
853=== modified file 'src/provisioningserver/config.py'
854--- src/provisioningserver/config.py 2014-03-21 08:19:20 +0000
855+++ src/provisioningserver/config.py 2014-03-25 17:45:57 +0000
856@@ -70,6 +70,7 @@
857 )
858 from formencode.declarative import DeclarativeMeta
859 from formencode.validators import (
860+ Bool,
861 Int,
862 RequireIfPresent,
863 Set,
864@@ -109,7 +110,7 @@
865
866 if_key_missing = None
867
868- root = String(if_missing="/var/lib/maas/tftp")
869+ root = String(if_missing="/var/lib/maas/boot-resources/current/")
870 port = Int(min=1, max=65535, if_missing=69)
871 generator = String(if_missing=b"http://localhost/MAAS/api/1.0/pxeconfig/")
872
873@@ -131,20 +132,16 @@
874 releases = Set(if_missing=None)
875
876
877-# XXX jtv 2014-03-21, bug=1295479: Unused until we start using the new import
878-# script.
879 class ConfigBootSourceSelection(Schema):
880 """Configuration validator for boot source election onfiguration."""
881
882 if_key_missing = None
883
884 release = String(if_missing="*")
885- arch = String(if_missing="*")
886+ arches = Set(if_missing=["*"])
887 subarches = Set(if_missing=['*'])
888
889
890-# XXX jtv 2014-03-21, bug=1295479: Unused until we start using the new import
891-# script.
892 class ConfigBootSource(Schema):
893 """Configuration validator for boot source configuration."""
894
895@@ -152,6 +149,8 @@
896
897 path = String(
898 if_missing="http://maas.ubuntu.com/images/ephemeral/releases/")
899+ keyring = String(
900+ if_missing="/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg")
901 selections = ForEach(
902 ConfigBootSourceSelection,
903 if_missing=[ConfigBootSourceSelection.to_python({})])
904@@ -167,12 +166,15 @@
905 ephemeral = ConfigBootEphemeral
906 architectures = Set(if_missing=None)
907
908- # XXX jtv 2014-03-21, bug=1295479: Unused until we start using the new
909- # import script.
910 storage = String(if_missing="/var/lib/maas/boot-resources/")
911 sources = ForEach(
912 ConfigBootSource, if_missing=[ConfigBootSource.to_python({})])
913
914+ # Marker in the bootresources.yaml file: if True, the file has not been
915+ # edited yet and needs to be either configured with initial choices, or
916+ # rewritten based on previously downloaded boot images.
917+ configure_me = Bool(if_missing=False)
918+
919
920 class ConfigMeta(DeclarativeMeta):
921 """Metaclass for the root configuration schema."""
922
923=== modified file 'src/provisioningserver/driver/__init__.py'
924--- src/provisioningserver/driver/__init__.py 2014-03-24 08:06:24 +0000
925+++ src/provisioningserver/driver/__init__.py 2014-03-25 17:45:57 +0000
926@@ -71,7 +71,7 @@
927 :param at_location: URL to a Simplestreams index or a local path
928 to a directory containing boot resources.
929 :param filter: A simplestreams filter.
930- e.g. "release=trusty label=beta2 arch=amd64"
931+ e.g. "release=trusty label=beta-2 arch=amd64"
932 This is ignored if the location is a local path, all resources
933 at the location will be imported.
934 TBD: How to provide progress information.
935@@ -90,7 +90,7 @@
936 {
937 "release": "trusty",
938 "arch": "amd64",
939- "label": "beta2",
940+ "label": "beta-2",
941 "size": 12344556,
942 }
943 ,
944
945=== modified file 'src/provisioningserver/import_images/boot_resources.py'
946--- src/provisioningserver/import_images/boot_resources.py 2014-03-17 18:42:45 +0000
947+++ src/provisioningserver/import_images/boot_resources.py 2014-03-25 17:45:57 +0000
948@@ -1,4 +1,4 @@
949-# Copyright 2013 Canonical Ltd. This software is licensed under the
950+# Copyright 2013-2014 Canonical Ltd. This software is licensed under the
951 # GNU Affero General Public License version 3 (see the file LICENSE).
952
953 from __future__ import (
954@@ -13,20 +13,30 @@
955 __all__ = [
956 'main',
957 'available_boot_resources',
958+ 'make_arg_parser',
959 ]
960
961+from argparse import ArgumentParser
962 from collections import defaultdict
963 from datetime import datetime
964-import errno
965+import functools
966 import glob
967 from gzip import GzipFile
968-from json import dumps as jsondumps
969+import json
970+import logging
971+from logging import getLogger
972 import os
973 from textwrap import dedent
974
975 from provisioningserver.config import Config
976 from provisioningserver.pxe.install_bootloader import install_bootloader
977-from provisioningserver.utils import call_and_check
978+from provisioningserver.pxe.tftppath import list_boot_images
979+from provisioningserver.utils import (
980+ atomic_write,
981+ call_and_check,
982+ locate_config,
983+ read_text_file,
984+ )
985 from simplestreams.contentsource import FdContentSource
986 from simplestreams.mirrors import (
987 BasicMirrorWriter,
988@@ -35,49 +45,158 @@
989 from simplestreams.objectstores import FileStore
990 from simplestreams.util import (
991 item_checksums,
992+ path_from_mirror_url,
993+ policy_read_signed,
994 products_exdata,
995 )
996
997
998+def init_logger():
999+ logger = getLogger(__name__)
1000+ formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
1001+ handler = logging.StreamHandler()
1002+ handler.setFormatter(formatter)
1003+ logger.addHandler(handler)
1004+ logger.setLevel(logging.INFO)
1005+ return logger
1006+import errno
1007+
1008+
1009+logger = init_logger()
1010+
1011+
1012+class NoConfigFile(Exception):
1013+ """Raised when the config file for the script doesn't exist."""
1014+
1015+
1016 def create_empty_hierarchy():
1017+ """Create hierarchy of dicts which supports h[key1]...[keyN] accesses.
1018+
1019+ Generated object automatically creates nonexistent levels of hierarchy
1020+ when accessed the following way: h[arch][subarch][release]=something.
1021+
1022+ :return Generated hierarchy of dicts.
1023+ """
1024 return defaultdict(create_empty_hierarchy)
1025
1026
1027 def boot_walk(boot, func):
1028+ """Walk over multi-level depth dict and call callback func for every leaf.
1029+
1030+ Function walks over three level depth dictionary organized in a form of
1031+ d[arch][subarch][release]=value and passes control to a callback function
1032+ for each arch/subarch/release triplet available. Stored value is passed
1033+ to a callback function as an additional parameter.
1034+
1035+ :param boot: Hierarchy of dicts with a depth equals to three.
1036+ :param func: Callback function f(arch, subarch, release, value).
1037+ """
1038 for arch in boot:
1039 for subarch in boot[arch]:
1040 for release in boot[arch][subarch]:
1041- func(arch, subarch, release, boot[arch][subarch][release])
1042+ for label in boot[arch][subarch][release]:
1043+ func(
1044+ arch, subarch, release, label,
1045+ boot[arch][subarch][release][label])
1046+
1047+
1048+def value_passes_filter_list(filter_list, property_value):
1049+ """Does the given property of a boot image pass the given filter list?
1050+
1051+ The value passes if either it matches one of the entries in the list of
1052+ filter values, or one of the filter values is an asterisk (`*`).
1053+ """
1054+ return '*' in filter_list or property_value in filter_list
1055+
1056+
1057+def value_passes_filter(filter_value, property_value):
1058+ """Does the given property of a boot image pass the given filter?
1059+
1060+ The value passes the filter if either the filter value is an asterisk
1061+ (`*`) or the value is equal to the filter value.
1062+ """
1063+ return filter_value in ('*', property_value)
1064+
1065+
1066+def image_passes_filter(filters, arch, subarch, release):
1067+ """Filter a boot image against configured import filters.
1068+
1069+ :param filters: A list of dicts describing the filters, as in `boot_merge`.
1070+ If the list is empty, or `None`, any image matches. Any entry in a
1071+ filter may be a string containing just an asterisk (`*`) to denote that
1072+ the entry will match any value.
1073+ :param arch: The given boot image's architecture.
1074+ :param subarch: The given boot image's subarchitecture.
1075+ :param release: The given boot image's OS release.
1076+ :return: Whether the image matches any of the dicts in `filters`.
1077+ """
1078+ # XXX jtv 2014-03-24: add label parameter?
1079+ if filters is None or len(filters) == 0:
1080+ return True
1081+ for filter_dict in filters:
1082+ item_matches = (
1083+ value_passes_filter(filter_dict['release'], release) and
1084+ value_passes_filter_list(filter_dict['arches'], arch) and
1085+ value_passes_filter_list(filter_dict['subarches'], subarch)
1086+ )
1087+ if item_matches:
1088+ return True
1089+ return False
1090
1091
1092 def boot_merge(boot1, boot2, filters=None):
1093-
1094- def filter_func(arch, subarch, release):
1095- for filter in filters:
1096- item_matches = (
1097- filter['release'] in ('*', release) and
1098- filter['arch'] in ('*', arch) and
1099- (
1100- '*' in filter['subarches'] or
1101- subarch in filter['subarches']
1102- )
1103- )
1104- if item_matches:
1105- return True
1106- return False
1107-
1108- def merge_func(arch, subarch, release, boot_resource):
1109- if filters and not filter_func(arch, subarch, release):
1110- return
1111- boot1[arch][subarch][release] = boot_resource
1112+ """Add entries from the second multi-level dict to the first one.
1113+
1114+ Function copies d[arch][subarch][release]=value chains from the second
1115+ dictionary to the first one if they don't exist there and pass optional
1116+ check done by filters.
1117+
1118+ :param boot1: first dict which will be extended in-place.
1119+ :param boot2: second dict which will be used as a source of new entries.
1120+ :param filters: list of dicts each of which contains 'arch', 'subarch',
1121+ 'release' keys; function takes d[arch][subarch][release] chain to the
1122+ first dict only if filters contain at least one dict with
1123+ arch in d['arches'], subarch in d['subarch'], d['release'] == release;
1124+ dict may have '*' as a value for 'arch' and 'release' keys and as a
1125+ member of 'subarch' list -- in that case key-specific check always
1126+ passes.
1127+ """
1128+ def merge_func(arch, subarch, release, label, boot_resource):
1129+ """Merge a boot resource into `boot1`, if it passes filters."""
1130+ if image_passes_filter(filters, arch, subarch, release):
1131+ logger.debug(
1132+ "Merging boot resource for %s/%s/%s/%s.",
1133+ arch, subarch, release, label)
1134+ boot1[arch][subarch][release][label] = boot_resource
1135
1136 boot_walk(boot2, merge_func)
1137
1138
1139 def boot_reverse(boot):
1140+ """Determine a set of subarches which should be deployed by boot resource.
1141+
1142+ Function reverses h[arch][subarch][release]=boot_resource hierarchy to form
1143+ boot resource to subarch relation. Many subarches may be deployed by a
1144+ single boot resource (in which case boot_resource=[subarch1, subarch2]
1145+ relation will be created). We note only subarchitectures and ignore
1146+ architectures because boot resource is tightly coupled with architecture
1147+ it can deploy according to metadata format. We can figure out for which
1148+ architecture we need to use a specific boot resource by looking at its
1149+ description in metadata. We can't do the same with subarch because we may
1150+ want to use boot resource only for a specific subset of subarches it can be
1151+ used for. To represent boot resource to subarch relation we generate the
1152+ following multi-level dictionary: d[content_id][product_name]=[subarches]
1153+ where 'content_id' and 'product_name' values come from metadata information
1154+ and allow us to uniquely identify a specific boot resource.
1155+
1156+ :param boot: Hierarchy of dicts d[arch][subarch][release]=boot_resource
1157+ :return Hierarchy of dictionaries d[content_id][product_name]=[subarches]
1158+ which describes boot resource to subarches relation for all available
1159+ boot resources (products).
1160+ """
1161 reverse = create_empty_hierarchy()
1162
1163- def reverse_func(arch, subarch, release, boot_resource):
1164+ def reverse_func(arch, subarch, release, label, boot_resource):
1165 content_id = boot_resource['content_id']
1166 product_name = boot_resource['product_name']
1167 existent = list(reverse[content_id][product_name])
1168@@ -87,9 +206,29 @@
1169 return reverse
1170
1171
1172-def tgt_entry(arch, subarch, release, image):
1173+def tgt_entry(arch, subarch, release, label, image):
1174+ """Generate tgt target used to commission arch/subarch with release
1175+
1176+ Tgt target used to commission arch/subarch machine with a specific Ubuntu
1177+ release should have the following name: ephemeral-arch-subarch-release.
1178+ This function creates target description in a format used by tgt-admin.
1179+ It uses arch, subarch and release to generate target name and image as
1180+ a path to image file which should be shared. Tgt target is marked as
1181+ read-only. Tgt target has 'allow-in-use' option enabled because this
1182+ script actively uses hardlinks to do image management and root images
1183+ in different folders may point to the same inode. Tgt doesn't allow us to
1184+ use the same inode for different tgt targets (even read-only targets which
1185+ looks like a bug to me) without this option enabled.
1186+
1187+ :param arch: Architecture name we generate tgt target for
1188+ :param subarch: Subarchitecture name we generate tgt target for
1189+ :param release: Ubuntu release we generate tgt target for
1190+ :param label: The images' label
1191+ :param image: Path to the image which should be shared via tgt/iscsi
1192+ :return Tgt entry which can be written to tgt-admin configuration file
1193+ """
1194 prefix = 'iqn.2004-05.com.ubuntu:maas'
1195- target_name = 'ephemeral-%s-%s-%s' % (arch, subarch, release)
1196+ target_name = 'ephemeral-%s-%s-%s-%s' % (arch, subarch, release, label)
1197 entry = dedent("""\
1198 <target {prefix}:{target_name}>
1199 readonly 1
1200@@ -101,15 +240,28 @@
1201 return entry
1202
1203
1204+def mirror_info_for_path(path, unsigned_policy=None, keyring=None):
1205+ if unsigned_policy is None:
1206+ unsigned_policy = lambda content, path, keyring: content
1207+ (mirror, rpath) = path_from_mirror_url(path, None)
1208+ policy = policy_read_signed
1209+ if rpath.endswith(".json"):
1210+ policy = unsigned_policy
1211+ if keyring:
1212+ policy = functools.partial(policy, keyring=keyring)
1213+ return(mirror, rpath, policy)
1214+
1215+
1216 class RepoDumper(BasicMirrorWriter):
1217
1218 def __init__(self):
1219 super(RepoDumper, self).__init__({'max_items': 1})
1220
1221- def dump(self, path):
1222+ def dump(self, path, keyring=None):
1223 self._boot = create_empty_hierarchy()
1224- reader = UrlMirrorReader(path)
1225- super(RepoDumper, self).sync(reader, 'streams/v1/index.sjson')
1226+ (mirror, rpath, policy) = mirror_info_for_path(path, keyring=keyring)
1227+ reader = UrlMirrorReader(mirror, policy=policy)
1228+ super(RepoDumper, self).sync(reader, rpath)
1229 return self._boot
1230
1231 def load_products(self, path=None, content_id=None):
1232@@ -124,9 +276,10 @@
1233 item = products_exdata(src, pedigree)
1234 arch, subarches = item['arch'], item['subarches']
1235 release = item['release']
1236+ label = item['label']
1237 compact_item = self.item_cleanup(item)
1238 for subarch in subarches.split(','):
1239- self._boot[arch][subarch][release] = compact_item
1240+ self._boot[arch][subarch][release][label] = compact_item
1241
1242
1243 class RepoWriter(BasicMirrorWriter):
1244@@ -137,9 +290,10 @@
1245 self._cache = FileStore(os.path.abspath(cache_path))
1246 super(RepoWriter, self).__init__({'max_items': 1})
1247
1248- def write(self, path):
1249- reader = UrlMirrorReader(path)
1250- super(RepoWriter, self).sync(reader, 'streams/v1/index.sjson')
1251+ def write(self, path, keyring=None):
1252+ (mirror, rpath, policy) = mirror_info_for_path(path, keyring=keyring)
1253+ reader = UrlMirrorReader(mirror, policy=policy)
1254+ super(RepoWriter, self).sync(reader, rpath)
1255
1256 def load_products(self, path=None, content_id=None):
1257 return
1258@@ -152,46 +306,58 @@
1259 product_name in self._info[content_id]
1260 )
1261
1262- def insert_uncompressed(self, tag, checksums, size, contentsource):
1263+ def insert_file(self, name, tag, checksums, size, contentsource):
1264+ logger.info("Inserting file %s (tag=%s, size=%s).", name, tag, size)
1265 self._cache.insert(
1266 tag, contentsource, checksums, mutable=False, size=size)
1267- return self._cache._fullpath(tag)
1268+ return [(self._cache._fullpath(tag), name)]
1269
1270- def insert_compressed(self, tag, checksums, size, contentsource):
1271- # TODO: Bake root.tar.gz required by fast-path installer (uec2roottar)
1272- uncompressed_tag = 'uncompressed-%s' % tag
1273- compressed_path = self._cache._fullpath(tag)
1274- uncompressed_path = self._cache._fullpath(uncompressed_tag)
1275- if not os.path.isfile(uncompressed_path):
1276- self._cache.insert(tag, contentsource, checksums,
1277- mutable=False, size=size)
1278- compressed_source = FdContentSource(GzipFile(compressed_path))
1279- self._cache.insert(uncompressed_tag, compressed_source,
1280- mutable=False)
1281+ def insert_root_image(self, tag, checksums, size, contentsource):
1282+ root_image_tag = 'root-image-%s' % tag
1283+ root_image_path = self._cache._fullpath(root_image_tag)
1284+ root_tgz_tag = 'root-tgz-%s' % tag
1285+ root_tgz_path = self._cache._fullpath(root_tgz_tag)
1286+ if not os.path.isfile(root_image_path):
1287+ logger.info("New root image: %s.", root_image_path)
1288+ self._cache.insert(
1289+ tag, contentsource, checksums, mutable=False, size=size)
1290+ uncompressed = FdContentSource(
1291+ GzipFile(self._cache._fullpath(tag)))
1292+ self._cache.insert(root_image_tag, uncompressed, mutable=False)
1293 self._cache.remove(tag)
1294- return uncompressed_path
1295+ if not os.path.isfile(root_tgz_path):
1296+ logger.info("Converting root tarball: %s.", root_tgz_path)
1297+ call_uec2roottar(root_image_path, root_tgz_path)
1298+ return [(root_image_path, 'root-image'), (root_tgz_path, 'root-tgz')]
1299
1300 def insert_item(self, data, src, target, pedigree, contentsource):
1301 item = products_exdata(src, pedigree)
1302 checksums = item_checksums(data)
1303- tag = checksums['md5']
1304+ tag = checksums['sha256']
1305 size = data['size']
1306- if data['path'].endswith('.gz'):
1307- src = self.insert_compressed(tag, checksums, size, contentsource)
1308+ ftype = item['ftype']
1309+ if ftype == 'root-image.gz':
1310+ links = self.insert_root_image(tag, checksums, size, contentsource)
1311 else:
1312- src = self.insert_uncompressed(tag, checksums, size, contentsource)
1313+ links = self.insert_file(
1314+ ftype, tag, checksums, size, contentsource)
1315 for subarch in self._info[item['content_id']][item['product_name']]:
1316 dst_folder = os.path.join(
1317- self._root_path, item['arch'], subarch, item['release'])
1318+ self._root_path, item['arch'], subarch, item['release'],
1319+ item['label'])
1320 if not os.path.exists(dst_folder):
1321 os.makedirs(dst_folder)
1322- os.link(src, os.path.join(dst_folder, item['ftype']))
1323+ for src, link_name in links:
1324+ link_path = os.path.join(dst_folder, link_name)
1325+ if os.path.isfile(link_path):
1326+ os.remove(link_path)
1327+ os.link(src, link_path)
1328
1329
1330 def available_boot_resources(root):
1331- for resource_path in glob.glob(os.path.join(root, '*/*/*')):
1332- arch, subarch, release = resource_path.split('/')[-3:]
1333- yield (arch, subarch, release)
1334+ for resource_path in glob.glob(os.path.join(root, '*/*/*/*')):
1335+ arch, subarch, release, label = resource_path.split('/')[-4:]
1336+ yield (arch, subarch, release, label)
1337
1338
1339 BOOTLOADERS = ['pxelinux.0', 'chain.c32', 'ifcpu64.c32']
1340@@ -212,14 +378,103 @@
1341 install_bootloader(bootloader_src, bootloader_dst)
1342
1343
1344-def main():
1345-
1346+def call_uec2roottar(*args):
1347+ """Invoke `uec2roottar` with the given arguments.
1348+
1349+ Here only so tests can stub it out.
1350+ """
1351+ call_and_check(["uec2roottar"] + list(args))
1352+
1353+
1354+def make_arg_parser(doc):
1355+ """Create an `argparse.ArgumentParser` for this script."""
1356+
1357+ parser = ArgumentParser(description=doc)
1358+ default_config = locate_config("bootresources.yaml")
1359+ parser.add_argument(
1360+ '--config-file', action="store", default=default_config,
1361+ help="Path to config file "
1362+ "(defaults to %s)" % default_config)
1363+ return parser
1364+
1365+
1366+def compose_targets_conf(snapshot_path):
1367+ """Produce the contents of a snapshot's tgt conf file.
1368+
1369+ :param snasphot_path: Filesystem path to a snapshot of boot images.
1370+ :return: Contents for a `targets.conf` file.
1371+ :rtype: bytes
1372+ """
1373+ # Use a set to make sure we don't register duplicate entries in tgt.
1374+ entries = set()
1375+ for item in list_boot_images(snapshot_path):
1376+ arch = item['architecture']
1377+ subarch = item['subarchitecture']
1378+ release = item['release']
1379+ label = item['label']
1380+ entries.add((arch, subarch, release, label))
1381+ tgt_entries = []
1382+ for arch, subarch, release, label in sorted(entries):
1383+ root_image = os.path.join(
1384+ snapshot_path, arch, subarch, release, label, 'root-image')
1385+ if os.path.isfile(root_image):
1386+ entry = tgt_entry(arch, subarch, release, label, root_image)
1387+ tgt_entries.append(entry)
1388+ text = ''.join(tgt_entries)
1389+ return text.encode('utf-8')
1390+
1391+
1392+def meta_contains(storage, content):
1393+ """Does the `maas.meta` file match `content`?
1394+
1395+ If the file's contents match the latest data, there is no need to update.
1396+ """
1397+ current_meta = os.path.join(storage, 'current', 'maas.meta')
1398+ return (
1399+ os.path.isfile(current_meta) and
1400+ content == read_text_file(current_meta)
1401+ )
1402+
1403+
1404+def compose_snapshot_path(storage):
1405+ """Put together a path for a new snapshot.
1406+
1407+ A snapshot is a directory in `storage` containing images. The name
1408+ contains the date in a sortable format.
1409+ """
1410+ snapshot_name = 'snapshot-%s' % datetime.now().strftime('%Y%m%d-%H%M%S')
1411+ return os.path.join(storage, snapshot_name)
1412+
1413+
1414+def update_current_symlink(storage, latest_snapshot):
1415+ """Symlink `latest_snapshot` as the "current" snapshot."""
1416+ symlink_path = os.path.join(storage, 'current')
1417+ if os.path.lexists(symlink_path):
1418+ os.unlink(symlink_path)
1419+ os.symlink(latest_snapshot, symlink_path)
1420+
1421+
1422+def write_snapshot_metadata(snapshot, meta_file_content, targets_conf,
1423+ targets_conf_content):
1424+ """Write "meta" file and tgt config for `snapshot`."""
1425+ meta_file = os.path.join(snapshot, 'maas.meta')
1426+ atomic_write(meta_file_content, meta_file, mode=0644)
1427+ atomic_write(targets_conf_content, targets_conf, mode=0644)
1428+
1429+
1430+def main(args):
1431+ logger.info("Importing boot resources.")
1432+ # The config file is required. We do not fall back to defaults if it's
1433+ # not there.
1434 try:
1435- config = Config.load_from_cache()
1436- except IOError as e:
1437- if e.errno != errno.ENOENT:
1438+ config = Config.load_from_cache(filename=args.config_file)
1439+ except IOError as ex:
1440+ if ex.errno == errno.ENOENT:
1441+ # No config file. We have helpful error output for this.
1442+ raise NoConfigFile(ex)
1443+ else:
1444+ # Unexpected error.
1445 raise
1446- config = Config.get_defaults()
1447
1448 storage = config['boot']['storage']
1449
1450@@ -227,34 +482,34 @@
1451 dumper = RepoDumper()
1452
1453 for source in reversed(config['boot']['sources']):
1454- repo_boot = dumper.dump(source['path'])
1455+ repo_boot = dumper.dump(source['path'], keyring=source['keyring'])
1456 boot_merge(boot, repo_boot, source['selections'])
1457
1458- meta = jsondumps(boot)
1459- current_meta = storage + '/current/maas.meta'
1460- if os.path.isfile(current_meta) and meta == open(current_meta).read():
1461+ meta_file_content = json.dumps(boot, sort_keys=True)
1462+ if meta_contains(storage, meta_file_content):
1463+ # The current maas.meta already contains the new config. No need to
1464+ # rewrite anything.
1465 return
1466
1467- snapshot_name = '/snapshot-%s/' % datetime.now().strftime('%d%m%Y-%H%M%S')
1468- snapshot_path = storage + snapshot_name
1469 reverse_boot = boot_reverse(boot)
1470- writer = RepoWriter(snapshot_path, storage + '/cache/', reverse_boot)
1471+ snapshot_path = compose_snapshot_path(storage)
1472+ cache_path = os.path.join(storage, 'cache')
1473+ targets_conf = os.path.join(snapshot_path, 'maas.tgt')
1474+ writer = RepoWriter(snapshot_path, cache_path, reverse_boot)
1475
1476 for source in config['boot']['sources']:
1477- writer.write(source['path'])
1478-
1479- open(snapshot_path + '/maas.meta', 'w').write(meta)
1480- symlink_path = storage + '/current'
1481- if os.path.lexists(symlink_path):
1482- os.unlink(symlink_path)
1483- os.symlink(snapshot_path, symlink_path)
1484-
1485- with open(snapshot_path + '/maas.tgt', 'w') as output:
1486- for arch, subarch, release in available_boot_resources(snapshot_path):
1487- disk = os.path.join(snapshot_path, arch, subarch, release, 'disk')
1488- if os.path.isfile(disk):
1489- output.write(tgt_entry(arch, subarch, release, disk))
1490-
1491- call_and_check(['tgt-admin', '--update', 'ALL'])
1492-
1493+ writer.write(source['path'], source['keyring'])
1494+
1495+ targets_conf_content = compose_targets_conf(snapshot_path)
1496+
1497+ logger.info("Writing metadata and updating iSCSI targets.")
1498+ write_snapshot_metadata(
1499+ snapshot_path, meta_file_content, targets_conf, targets_conf_content)
1500+ call_and_check(['tgt-admin', '--conf', targets_conf, '--update', 'ALL'])
1501+
1502+ logger.info("Installing boot images snapshot %s.", snapshot_path)
1503 install_boot_loaders(snapshot_path)
1504+
1505+ # If we got here, all went well. This is now truly the "current" snapshot.
1506+ update_current_symlink(storage, snapshot_path)
1507+ logger.info("Import done.")
1508
1509=== added file 'src/provisioningserver/import_images/tests/test_boot_resources.py'
1510--- src/provisioningserver/import_images/tests/test_boot_resources.py 1970-01-01 00:00:00 +0000
1511+++ src/provisioningserver/import_images/tests/test_boot_resources.py 2014-03-25 17:45:57 +0000
1512@@ -0,0 +1,54 @@
1513+# Copyright 2014 Canonical Ltd. This software is licensed under the
1514+# GNU Affero General Public License version 3 (see the file LICENSE).
1515+
1516+"""Tests for the boot_resources module."""
1517+
1518+from __future__ import (
1519+ absolute_import,
1520+ print_function,
1521+ unicode_literals,
1522+ )
1523+
1524+str = None
1525+
1526+__metaclass__ = type
1527+__all__ = []
1528+
1529+import errno
1530+import os
1531+from random import randint
1532+
1533+from maastesting.factory import factory
1534+from maastesting.testcase import MAASTestCase
1535+from mock import MagicMock
1536+from provisioningserver.config import Config
1537+from provisioningserver.import_images import boot_resources
1538+from provisioningserver.import_images.boot_resources import (
1539+ main,
1540+ NoConfigFile,
1541+ )
1542+
1543+
1544+class TestMain(MAASTestCase):
1545+
1546+ def test_raises_ioerror_when_no_config_file_found(self):
1547+ # Suppress log output.
1548+ self.logger = self.patch(boot_resources, 'logger')
1549+ filename = "/tmp/%s" % factory.make_name("config")
1550+ self.assertFalse(os.path.exists(filename))
1551+ args = MagicMock()
1552+ args.config_file = filename
1553+ self.assertRaises(NoConfigFile, main, args)
1554+
1555+ def test_raises_non_ENOENT_IOErrors(self):
1556+ # main() will raise a NoConfigFile error when it encounters an
1557+ # ENOENT IOError, but will otherwise just re-raise the original
1558+ # IOError.
1559+ args = MagicMock()
1560+ mock_load_from_cache = self.patch(Config, 'load_from_cache')
1561+ other_error = IOError(randint(errno.ENOENT + 1, 1000))
1562+ mock_load_from_cache.side_effect = other_error
1563+ # Suppress log output.
1564+ self.logger = self.patch(boot_resources, 'logger')
1565+ raised_error = self.assertRaises(IOError, main, args)
1566+ self.assertEqual(other_error, raised_error)
1567
1568=== modified file 'src/provisioningserver/import_images/tests/test_ephemerals_script.py'
1569--- src/provisioningserver/import_images/tests/test_ephemerals_script.py 2014-03-13 05:05:21 +0000
1570+++ src/provisioningserver/import_images/tests/test_ephemerals_script.py 2014-03-25 17:45:57 +0000
1571@@ -14,22 +14,15 @@
1572 __metaclass__ = type
1573 __all__ = []
1574
1575-from argparse import ArgumentParser
1576-from copy import deepcopy
1577 from os import (
1578 listdir,
1579 readlink,
1580 )
1581 import os.path
1582-from pipes import quote
1583 import subprocess
1584-from textwrap import dedent
1585
1586-from fixtures import EnvironmentVariableFixture
1587 from maastesting.factory import factory
1588-from provisioningserver.config import Config
1589 from provisioningserver.import_images import (
1590- config as config_module,
1591 ephemerals_script,
1592 )
1593 from provisioningserver.import_images.ephemerals_script import (
1594@@ -37,7 +30,6 @@
1595 create_symlinked_image_dir,
1596 extract_image_tarball,
1597 install_image_from_simplestreams,
1598- make_arg_parser,
1599 move_file_by_glob,
1600 )
1601 from provisioningserver.pxe.tftppath import (
1602@@ -369,112 +361,3 @@
1603 temp_location=temp_location)
1604
1605 self.assertItemsEqual([], listdir(temp_location))
1606-
1607-
1608-def make_legacy_config(data_dir=None, arches=None, releases=None):
1609- """Create contents for a legacy, shell-script config file."""
1610- if data_dir is None:
1611- data_dir = factory.make_name('datadir')
1612- if arches is None:
1613- arches = [factory.make_name('arch') for counter in range(2)]
1614- if releases is None:
1615- releases = [factory.make_name('release') for counter in range(2)]
1616- return dedent("""\
1617- DATA_DIR=%s
1618- ARCHES=%s
1619- RELEASES=%s
1620- """) % (
1621- quote(data_dir),
1622- quote(' '.join(arches)),
1623- quote(' '.join(releases)),
1624- )
1625-
1626-
1627-def install_legacy_config(testcase, contents):
1628- """Set up a legacy config file with the given contents.
1629-
1630- Returns the config file's path.
1631- """
1632- legacy_file = testcase.make_file(contents=contents)
1633- testcase.patch(config_module, 'EPHEMERALS_LEGACY_CONFIG', legacy_file)
1634- return legacy_file
1635-
1636-
1637-class TestMakeArgParser(PservTestCase):
1638-
1639- def test_creates_parser(self):
1640- self.useFixture(ConfigFixture({'boot': {'ephemeral': {}}}))
1641- documentation = factory.getRandomString()
1642-
1643- parser = make_arg_parser(documentation)
1644-
1645- self.assertIsInstance(parser, ArgumentParser)
1646- self.assertEqual(documentation, parser.description)
1647-
1648- def test_defaults_to_config(self):
1649- images_directory = self.make_dir()
1650- arches = [factory.make_name('arch1'), factory.make_name('arch2')]
1651- releases = [factory.make_name('rel1'), factory.make_name('rel2')]
1652- self.useFixture(ConfigFixture({
1653- 'boot': {
1654- 'architectures': arches,
1655- 'ephemeral': {
1656- 'images_directory': images_directory,
1657- 'releases': releases,
1658- },
1659- },
1660- }))
1661-
1662- parser = make_arg_parser(factory.getRandomString())
1663-
1664- args = parser.parse_args('')
1665- self.assertEqual(images_directory, args.output)
1666- self.assertItemsEqual(
1667- [
1668- compose_filter('arch', arches),
1669- compose_filter('release', releases),
1670- ],
1671- args.filters)
1672-
1673- def test_does_not_require_config(self):
1674- defaults = Config.get_defaults()
1675- no_file = os.path.join(self.make_dir(), factory.make_name() + '.yaml')
1676- self.useFixture(
1677- EnvironmentVariableFixture('MAAS_PROVISIONING_SETTINGS', no_file))
1678-
1679- parser = make_arg_parser(factory.getRandomString())
1680-
1681- args = parser.parse_args('')
1682- self.assertEqual(
1683- defaults['boot']['ephemeral']['images_directory'],
1684- args.output)
1685- self.assertItemsEqual([], args.filters)
1686-
1687- def test_does_not_modify_config(self):
1688- self.useFixture(ConfigFixture({
1689- 'boot': {
1690- 'architectures': [factory.make_name('arch')],
1691- 'ephemeral': {
1692- 'images_directory': self.make_dir(),
1693- 'releases': [factory.make_name('release')],
1694- },
1695- },
1696- }))
1697- original_boot_config = deepcopy(Config.load_from_cache()['boot'])
1698- install_legacy_config(self, make_legacy_config())
1699-
1700- make_arg_parser(factory.getRandomString())
1701-
1702- self.assertEqual(
1703- original_boot_config,
1704- Config.load_from_cache()['boot'])
1705-
1706- def test_uses_legacy_config(self):
1707- data_dir = self.make_dir()
1708- self.useFixture(ConfigFixture({}))
1709- install_legacy_config(self, make_legacy_config(data_dir=data_dir))
1710-
1711- parser = make_arg_parser(factory.getRandomString())
1712-
1713- args = parser.parse_args('')
1714- self.assertEqual(data_dir, args.output)
1715
1716=== modified file 'src/provisioningserver/kernel_opts.py'
1717--- src/provisioningserver/kernel_opts.py 2014-03-18 03:23:55 +0000
1718+++ src/provisioningserver/kernel_opts.py 2014-03-25 17:45:57 +0000
1719@@ -21,9 +21,7 @@
1720 from collections import namedtuple
1721 import os
1722
1723-from provisioningserver.config import Config
1724 from provisioningserver.driver import ArchitectureRegistry
1725-from provisioningserver.utils import parse_key_value_file
1726
1727
1728 class EphemeralImagesDirectoryNotFound(Exception):
1729@@ -92,26 +90,9 @@
1730 ISCSI_TARGET_NAME_PREFIX = "iqn.2004-05.com.ubuntu:maas"
1731
1732
1733-def get_ephemeral_name(release, arch):
1734- """Return the name of the most recent ephemeral image.
1735-
1736- That information is read from the config file named 'info' in the
1737- ephemeral directory e.g:
1738- /var/lib/maas/ephemeral/precise/ephemeral/i386/20120424/info
1739- """
1740- config = Config.load_from_cache()
1741- root = os.path.join(
1742- config["boot"]["ephemeral"]["images_directory"],
1743- release, 'ephemeral', arch)
1744- try:
1745- filename = os.path.join(get_last_directory(root), 'info')
1746- except OSError:
1747- raise EphemeralImagesDirectoryNotFound(
1748- "The directory containing the ephemeral images/info is missing "
1749- "(%r). Make sure to run the script "
1750- "'maas-import-pxe-files'." % root)
1751- name = parse_key_value_file(filename, separator="=")['name']
1752- return name
1753+def get_ephemeral_name(arch, subarch, release, label):
1754+ """Return the name of the most recent ephemeral image."""
1755+ return "ephemeral-%s-%s-%s-%s" % (arch, subarch, release, label)
1756
1757
1758 def compose_hostname_opts(params):
1759@@ -137,7 +118,8 @@
1760 if params.purpose == "commissioning" or params.purpose == "xinstall":
1761 # These are kernel parameters read by the ephemeral environment.
1762 tname = prefix_target_name(
1763- get_ephemeral_name(params.release, params.arch))
1764+ get_ephemeral_name(
1765+ params.arch, params.subarch, params.release, params.label))
1766 kernel_params = [
1767 # Read by the open-iscsi initramfs code.
1768 "iscsi_target_name=%s" % tname,
1769
1770=== modified file 'src/provisioningserver/pxe/config.py'
1771--- src/provisioningserver/pxe/config.py 2014-03-13 05:05:21 +0000
1772+++ src/provisioningserver/pxe/config.py 2014-03-25 17:45:57 +0000
1773@@ -95,19 +95,22 @@
1774 kernel_params.purpose, kernel_params.arch,
1775 kernel_params.subarch)
1776
1777- # The locations of the kernel image and the initrd are defined by
1778- # update_install_files(), in scripts/maas-import-pxe-files.
1779-
1780 def image_dir(params):
1781 return compose_image_path(
1782 params.arch, params.subarch,
1783- params.release, params.label, params.purpose)
1784+ params.release, params.label)
1785
1786 def initrd_path(params):
1787- return "%s/initrd.gz" % image_dir(params)
1788+ if params.purpose == "install":
1789+ return "%s/di-initrd" % image_dir(params)
1790+ else:
1791+ return "%s/boot-initrd" % image_dir(params)
1792
1793 def kernel_path(params):
1794- return "%s/linux" % image_dir(params)
1795+ if params.purpose == "install":
1796+ return "%s/di-kernel" % image_dir(params)
1797+ else:
1798+ return "%s/boot-kernel" % image_dir(params)
1799
1800 def kernel_command(params):
1801 return compose_kernel_command_line(params)
1802
1803=== modified file 'src/provisioningserver/pxe/tests/test_config.py'
1804--- src/provisioningserver/pxe/tests/test_config.py 2014-03-17 08:18:55 +0000
1805+++ src/provisioningserver/pxe/tests/test_config.py 2014-03-25 17:45:57 +0000
1806@@ -161,7 +161,7 @@
1807 class TestRenderPXEConfig(MAASTestCase):
1808 """Tests for `provisioningserver.pxe.config.render_pxe_config`."""
1809
1810- def test_render(self):
1811+ def test_render_install(self):
1812 # Given the right configuration options, the PXE configuration is
1813 # correctly rendered.
1814 params = make_kernel_parameters(self, purpose="install")
1815@@ -174,14 +174,14 @@
1816 # The PXE parameters are all set according to the options.
1817 image_dir = compose_image_path(
1818 arch=params.arch, subarch=params.subarch,
1819- release=params.release, label=params.label, purpose=params.purpose)
1820+ release=params.release, label=params.label)
1821 self.assertThat(
1822 output, MatchesAll(
1823 MatchesRegex(
1824- r'.*^\s+KERNEL %s/linux$' % re.escape(image_dir),
1825+ r'.*^\s+KERNEL %s/di-kernel$' % re.escape(image_dir),
1826 re.MULTILINE | re.DOTALL),
1827 MatchesRegex(
1828- r'.*^\s+INITRD %s/initrd[.]gz$' % re.escape(image_dir),
1829+ r'.*^\s+INITRD %s/di-initrd$' % re.escape(image_dir),
1830 re.MULTILINE | re.DOTALL),
1831 MatchesRegex(
1832 r'.*^\s+APPEND .+?$',
1833@@ -235,45 +235,6 @@
1834 self.assertNotIn("LOCALBOOT", output)
1835
1836
1837-class TestRenderArmhfSubarchScenarios(MAASTestCase):
1838- """See bug https://bugs.launchpad.net/maas/+bug/1166994"""
1839-
1840- scenarios = [
1841- ("install_precise", dict(
1842- arch="armhf", purpose="install", release="precise",
1843- expect_in_output="highbank")),
1844- ("install_quantal", dict(
1845- arch="armhf", purpose="install", release="quantal",
1846- expect_in_output="highbank")),
1847- ("install_saucy", dict(
1848- arch="armhf", purpose="install", release="saucy",
1849- expect_in_output="generic")),
1850- ("commission_precise", dict(
1851- arch="armhf", purpose="commissioning", release="precise",
1852- expect_in_output="highbank")),
1853- ("commission_quantal", dict(
1854- arch="armhf", purpose="commissioning", release="quantal",
1855- expect_in_output="highbank")),
1856- ("commission_saucy", dict(
1857- arch="armhf", purpose="commissioning", release="saucy",
1858- expect_in_output="generic")),
1859- ]
1860-
1861- def test_highbank_scenarios(self):
1862- # get_ephemeral_name depends on ephemeral images being present but
1863- # doesn't affect the test outcome, so patch it out.
1864- self.patch(
1865- kernel_opts,
1866- "get_ephemeral_name").return_value = "ephemeral_name"
1867- options = {
1868- "kernel_params": make_kernel_parameters(
1869- arch=self.arch, purpose=self.purpose, subarch="highbank",
1870- release=self.release),
1871- }
1872- output = render_pxe_config(**options)
1873- self.assertIn(self.expect_in_output, output)
1874-
1875-
1876 class TestRenderPXEConfigScenarios(MAASTestCase):
1877 """Tests for `provisioningserver.pxe.config.render_pxe_config`."""
1878
1879
1880=== modified file 'src/provisioningserver/pxe/tests/test_tftppath.py'
1881--- src/provisioningserver/pxe/tests/test_tftppath.py 2014-03-18 03:23:55 +0000
1882+++ src/provisioningserver/pxe/tests/test_tftppath.py 2014-03-25 17:45:57 +0000
1883@@ -30,7 +30,9 @@
1884 list_subdirs,
1885 locate_tftp_path,
1886 )
1887-from provisioningserver.testing.boot_images import make_boot_image_params
1888+from provisioningserver.testing.boot_images import (
1889+ make_boot_image_storage_params,
1890+ )
1891 from provisioningserver.testing.config import ConfigFixture
1892 from testtools.matchers import (
1893 Not,
1894@@ -38,6 +40,16 @@
1895 )
1896
1897
1898+def make_image(params, purpose):
1899+ """Describe an image as a dict similar to what `list_boot_images` returns.
1900+
1901+ The `params` are as returned from `make_boot_image_storage_params`.
1902+ """
1903+ image = params.copy()
1904+ image['purpose'] = purpose
1905+ return image
1906+
1907+
1908 class TestTFTPPath(MAASTestCase):
1909
1910 def setUp(self):
1911@@ -53,8 +65,7 @@
1912 arch=image_params['architecture'],
1913 subarch=image_params['subarchitecture'],
1914 release=image_params['release'],
1915- label=image_params['label'],
1916- purpose=image_params['purpose']),
1917+ label=image_params['label']),
1918 tftproot)
1919 os.makedirs(image_dir)
1920 factory.make_file(image_dir, 'linux')
1921@@ -111,23 +122,32 @@
1922 self.tftproot, locate_tftp_path(None, tftproot=self.tftproot))
1923
1924 def test_list_boot_images_copes_with_empty_directory(self):
1925- self.assertItemsEqual([], list_boot_images(self.tftproot))
1926+ self.assertEqual([], list_boot_images(self.tftproot))
1927
1928 def test_list_boot_images_copes_with_unexpected_files(self):
1929 os.makedirs(os.path.join(self.tftproot, factory.make_name('empty')))
1930 factory.make_file(self.tftproot)
1931- self.assertItemsEqual([], list_boot_images(self.tftproot))
1932+ self.assertEqual([], list_boot_images(self.tftproot))
1933
1934 def test_list_boot_images_finds_boot_image(self):
1935- image = make_boot_image_params()
1936- self.make_image_dir(image, self.tftproot)
1937- self.assertItemsEqual([image], list_boot_images(self.tftproot))
1938+ params = make_boot_image_storage_params()
1939+ self.make_image_dir(params, self.tftproot)
1940+ purposes = ['install', 'commissioning', 'xinstall']
1941+ self.assertItemsEqual(
1942+ [make_image(params, purpose) for purpose in purposes],
1943+ list_boot_images(self.tftproot))
1944
1945 def test_list_boot_images_enumerates_boot_images(self):
1946- images = [make_boot_image_params() for counter in range(3)]
1947- for image in images:
1948- self.make_image_dir(image, self.tftproot)
1949- self.assertItemsEqual(images, list_boot_images(self.tftproot))
1950+ params = [make_boot_image_storage_params() for counter in range(3)]
1951+ for param in params:
1952+ self.make_image_dir(param, self.tftproot)
1953+ self.assertItemsEqual(
1954+ [
1955+ make_image(param, purpose)
1956+ for param in params
1957+ for purpose in ['install', 'commissioning', 'xinstall']
1958+ ],
1959+ list_boot_images(self.tftproot))
1960
1961 def test_is_visible_subdir_ignores_regular_files(self):
1962 plain_file = self.make_file()
1963
1964=== modified file 'src/provisioningserver/pxe/tftppath.py'
1965--- src/provisioningserver/pxe/tftppath.py 2014-03-18 20:22:07 +0000
1966+++ src/provisioningserver/pxe/tftppath.py 2014-03-25 17:45:57 +0000
1967@@ -40,7 +40,7 @@
1968 return "pxelinux.0"
1969
1970
1971-# TODO: move this; it is now only used for testing.
1972+# XXX allenap: move this; it is now only used for testing.
1973 def compose_config_path(mac):
1974 """Compose the TFTP path for a PXE configuration file.
1975
1976@@ -60,7 +60,11 @@
1977 htype=ARP_HTYPE.ETHERNET, mac=mac)
1978
1979
1980-def compose_image_path(arch, subarch, release, label, purpose):
1981+# XXX rvb 2014-03-21 bug=1235479: The 'purpose' is made optional for now so
1982+# that this method can cope with both the old layout (which had the 'purpose'
1983+# as part of the images' path) and the new one. Once the old script is
1984+# removed, this parameter should be removed as well.
1985+def compose_image_path(arch, subarch, release, label, purpose=None):
1986 """Compose the TFTP path for a PXE kernel/initrd directory.
1987
1988 The path returned is relative to the TFTP root, as it would be
1989@@ -69,14 +73,16 @@
1990 :param arch: Main machine architecture.
1991 :param subarch: Sub-architecture, or "generic" if there is none.
1992 :param release: Operating system release, e.g. "precise".
1993- :param label: An image label, e.g. for a beta version, or "release" for
1994- the default images.
1995+ :param label: Release label, e.g. "release" or "alpha-2".
1996 :param purpose: Purpose of the image, e.g. "install" or
1997 "commissioning".
1998 :return: Path for the corresponding image directory (containing a
1999 kernel and initrd) as exposed over TFTP.
2000 """
2001- return '/'.join([arch, subarch, release, label, purpose])
2002+ elements = [arch, subarch, release, label]
2003+ if purpose is not None:
2004+ elements.append(purpose)
2005+ return '/'.join(elements)
2006
2007
2008 def locate_tftp_path(path, tftproot):
2009@@ -143,23 +149,32 @@
2010
2011
2012 def extract_image_params(path):
2013- """Represent a list of TFTP path elements as a boot-image dict.
2014+ """Represent a list of TFTP path elements as a list of boot-image dicts.
2015
2016- The path must consist of a full [architecture, subarchitecture, release,
2017- purpose] that identify a kind of boot that we may need an image for.
2018+ The path must consist of a full [architecture, subarchitecture, release]
2019+ that identify a kind of boot that we may need an image for.
2020 """
2021- arch, subarch, release, label, purpose = path
2022- return dict(
2023- architecture=arch, subarchitecture=subarch, release=release,
2024- label=label, purpose=purpose)
2025+ arch, subarch, release, label = path
2026+ # XXX: rvb 2014-03-24: The images import script currently imports all the
2027+ # images for the configured selections (where a selection is an
2028+ # arch/subarch/series/label combination). When the import script grows the
2029+ # ability to import the images for a particular purpose, we need to change
2030+ # this code to report what is actually present.
2031+ purposes = ['commissioning', 'install', 'xinstall']
2032+ return [
2033+ dict(
2034+ architecture=arch, subarchitecture=subarch,
2035+ release=release, label=label, purpose=purpose)
2036+ for purpose in purposes
2037+ ]
2038
2039
2040 def list_boot_images(tftproot):
2041 """List the available boot images.
2042
2043 :param tftproot: TFTP root directory.
2044- :return: An iterable of dicts, describing boot images as consumed by
2045- the report_boot_images API call.
2046+ :return: A list of dicts, describing boot images as consumed by the
2047+ `report_boot_images` API call.
2048 """
2049 # The sub-directories directly under tftproot, if they contain
2050 # images, represent architectures.
2051@@ -170,10 +185,12 @@
2052 paths = [[subdir] for subdir in potential_archs]
2053
2054 # Extend paths deeper into the filesystem, through the levels that
2055- # represent sub-architecture, release, and purpose. Any directory
2056+ # represent sub-architecture, release, and label. Any directory
2057 # that doesn't extend this deep isn't a boot image.
2058- for level in ['subarch', 'release', 'label', 'purpose']:
2059+ for level in ['subarch', 'release', 'label']:
2060 paths = drill_down(tftproot, paths)
2061
2062 # Each path we find this way should be a boot image.
2063- return [extract_image_params(path) for path in paths]
2064+ # This gets serialised to JSON, so we really have to return a list, not
2065+ # just any iterable.
2066+ return sum([extract_image_params(path) for path in paths], [])
2067
2068=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
2069--- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-03-20 22:36:32 +0000
2070+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-03-25 17:45:57 +0000
2071@@ -175,11 +175,11 @@
2072 subarchs = "generic", "special"
2073 releases = "precise", "trusty"
2074 labels = "beta-1", "release"
2075- purposes = "commission", "install"
2076+ purposes = "commissioning", "install", "xinstall"
2077
2078 # Create a TFTP file tree with a variety of subdirectories.
2079 tftpdir = self.make_dir()
2080- for options in product(archs, subarchs, releases, labels, purposes):
2081+ for options in product(archs, subarchs, releases, labels):
2082 os.makedirs(os.path.join(tftpdir, *options))
2083
2084 # Ensure that list_boot_images() uses the above TFTP file tree.
2085
2086=== modified file 'src/provisioningserver/testing/boot_images.py'
2087--- src/provisioningserver/testing/boot_images.py 2014-03-11 07:34:00 +0000
2088+++ src/provisioningserver/testing/boot_images.py 2014-03-25 17:45:57 +0000
2089@@ -1,4 +1,4 @@
2090-# Copyright 2012 Canonical Ltd. This software is licensed under the
2091+# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2092 # GNU Affero General Public License version 3 (see the file LICENSE).
2093
2094 """Test helpers for boot-image parameters."""
2095@@ -24,13 +24,26 @@
2096
2097 These are the parameters that together describe a kind of boot for
2098 which we may need a kernel and initrd: architecture,
2099- sub-architecture, Ubuntu release, boot purpose and simplestreams
2100- label. See the `tftppath` module for how these fit together.
2101+ sub-architecture, Ubuntu release, boot purpose, and release label.
2102 """
2103 return dict(
2104 architecture=factory.make_name('architecture'),
2105 subarchitecture=factory.make_name('subarchitecture'),
2106 release=factory.make_name('release'),
2107+ label=factory.make_name('label'),
2108 purpose=factory.make_name('purpose'),
2109+ )
2110+
2111+
2112+def make_boot_image_storage_params():
2113+ """Create a dict of boot-image parameters as used to store the image.
2114+
2115+ These are the parameters that together describe a path to store a boot
2116+ image: architecture, sub-architecture, Ubuntu release, and release label.
2117+ """
2118+ return dict(
2119+ architecture=factory.make_name('architecture'),
2120+ subarchitecture=factory.make_name('subarchitecture'),
2121+ release=factory.make_name('release'),
2122 label=factory.make_name('label'),
2123 )
2124
2125=== modified file 'src/provisioningserver/tests/test_config.py'
2126--- src/provisioningserver/tests/test_config.py 2014-03-21 08:19:20 +0000
2127+++ src/provisioningserver/tests/test_config.py 2014-03-25 17:45:57 +0000
2128@@ -1,4 +1,4 @@
2129-# Copyright 2005-2013 Canonical Ltd. This software is licensed under the
2130+# Copyright 2005-2014 Canonical Ltd. This software is licensed under the
2131 # GNU Affero General Public License version 3 (see the file LICENSE).
2132
2133 """Tests for provisioning configuration."""
2134@@ -136,15 +136,15 @@
2135 'images_directory': '/var/lib/maas/ephemeral',
2136 'releases': None,
2137 },
2138- # XXX jtv 2014-03-21, bug=1295479: Unused until we start using
2139- # the new import script.
2140 'sources': [
2141 {
2142 'path': (
2143 'http://maas.ubuntu.com/images/ephemeral/releases/'),
2144+ 'keyring': (
2145+ '/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg'),
2146 'selections': [
2147 {
2148- 'arch': '*',
2149+ 'arches': ['*'],
2150 'release': '*',
2151 'subarches': ['*'],
2152 },
2153@@ -152,6 +152,7 @@
2154 },
2155 ],
2156 'storage': '/var/lib/maas/boot-resources/',
2157+ 'configure_me': False,
2158 },
2159 'broker': {
2160 'host': 'localhost',
2161@@ -169,7 +170,7 @@
2162 'tftp': {
2163 'generator': 'http://localhost/MAAS/api/1.0/pxeconfig/',
2164 'port': 69,
2165- 'root': "/var/lib/maas/tftp",
2166+ 'root': "/var/lib/maas/boot-resources/current/",
2167 },
2168 }
2169
2170@@ -266,10 +267,10 @@
2171 self.assertNotEqual(first_load['logfile'], second_load['logfile'])
2172 self.assertEqual(logfile, second_load['logfile'])
2173 self.assertIsNot(first_load['boot'], second_load['boot'])
2174- first_load['boot']['architectures'] = [factory.make_name('otherarch')]
2175+ first_load['boot']['storage'] = [factory.make_name('otherstorage')]
2176 self.assertNotEqual(
2177- first_load['boot']['architectures'],
2178- second_load['boot']['architectures'])
2179+ first_load['boot']['storage'],
2180+ second_load['boot']['storage'])
2181
2182 def test_oops_directory_without_reporter(self):
2183 # It is an error to omit the OOPS reporter if directory is specified.
2184
2185=== modified file 'src/provisioningserver/tests/test_kernel_opts.py'
2186--- src/provisioningserver/tests/test_kernel_opts.py 2014-03-17 08:48:46 +0000
2187+++ src/provisioningserver/tests/test_kernel_opts.py 2014-03-25 17:45:57 +0000
2188@@ -29,13 +29,12 @@
2189 compose_arch_opts,
2190 compose_kernel_command_line,
2191 compose_preseed_opt,
2192- EphemeralImagesDirectoryNotFound,
2193+ get_ephemeral_name,
2194 get_last_directory,
2195 ISCSI_TARGET_NAME_PREFIX,
2196 KernelParameters,
2197 prefix_target_name,
2198 )
2199-from provisioningserver.testing.config import ConfigFixture
2200 from testtools.matchers import (
2201 Contains,
2202 ContainsAll,
2203@@ -169,8 +168,6 @@
2204 def test_xinstall_compose_kernel_command_line_inc_purpose_opts(self):
2205 # The result of compose_kernel_command_line includes the purpose
2206 # options for a non "xinstall" node.
2207- get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")
2208- get_ephemeral_name.return_value = "RELEASE-ARCH"
2209 params = self.make_kernel_parameters(purpose="xinstall")
2210 cmdline = compose_kernel_command_line(params)
2211 self.assertThat(
2212@@ -184,8 +181,6 @@
2213 def test_commissioning_compose_kernel_command_line_inc_purpose_opts(self):
2214 # The result of compose_kernel_command_line includes the purpose
2215 # options for a non "commissioning" node.
2216- get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")
2217- get_ephemeral_name.return_value = "RELEASE-ARCH"
2218 params = self.make_kernel_parameters(purpose="commissioning")
2219 cmdline = compose_kernel_command_line(params)
2220 self.assertThat(
2221@@ -212,8 +207,6 @@
2222 def test_compose_kernel_command_line_inc_common_opts(self):
2223 # Test that some kernel arguments appear on commissioning, install
2224 # and xinstall command lines.
2225- get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")
2226- get_ephemeral_name.return_value = "RELEASE-ARCH"
2227 expected = ["nomodeset"]
2228
2229 params = self.make_kernel_parameters(
2230@@ -231,32 +224,12 @@
2231 cmdline = compose_kernel_command_line(params)
2232 self.assertThat(cmdline, ContainsAll(expected))
2233
2234- def create_ephemeral_info(self, name, arch, release):
2235- """Create a pseudo-real ephemeral info file."""
2236- ephemeral_info = """
2237- release=%s
2238- stream=ephemeral
2239- label=release
2240- serial=20120424
2241- arch=%s
2242- name=%s
2243- """ % (release, arch, name)
2244- ephemeral_root = self.make_dir()
2245- config = {"boot": {"ephemeral": {"images_directory": ephemeral_root}}}
2246- self.useFixture(ConfigFixture(config))
2247- ephemeral_dir = os.path.join(
2248- ephemeral_root, release, 'ephemeral', arch, release)
2249- os.makedirs(ephemeral_dir)
2250- factory.make_file(
2251- ephemeral_dir, name='info', contents=ephemeral_info)
2252-
2253 def test_compose_kernel_command_line_inc_purpose_opts_xinstall_node(self):
2254 # The result of compose_kernel_command_line includes the purpose
2255 # options for a "xinstall" node.
2256- ephemeral_name = factory.make_name("ephemeral")
2257 params = self.make_kernel_parameters(purpose="xinstall")
2258- self.create_ephemeral_info(
2259- ephemeral_name, params.arch, params.release)
2260+ ephemeral_name = get_ephemeral_name(
2261+ params.arch, params.subarch, params.release, params.label)
2262 self.assertThat(
2263 compose_kernel_command_line(params),
2264 ContainsAll([
2265@@ -269,10 +242,9 @@
2266 def test_compose_kernel_command_line_inc_purpose_opts_comm_node(self):
2267 # The result of compose_kernel_command_line includes the purpose
2268 # options for a "commissioning" node.
2269- ephemeral_name = factory.make_name("ephemeral")
2270 params = self.make_kernel_parameters(purpose="commissioning")
2271- self.create_ephemeral_info(
2272- ephemeral_name, params.arch, params.release)
2273+ ephemeral_name = get_ephemeral_name(
2274+ params.arch, params.subarch, params.release, params.label)
2275 self.assertThat(
2276 compose_kernel_command_line(params),
2277 ContainsAll([
2278@@ -282,15 +254,6 @@
2279 "iscsi_target_ip=%s" % params.fs_host,
2280 ]))
2281
2282- def test_compose_kernel_command_line_reports_error_about_missing_dir(self):
2283- params = self.make_kernel_parameters(purpose="commissioning")
2284- missing_dir = factory.make_name('missing-dir')
2285- config = {"boot": {"ephemeral": {"images_directory": missing_dir}}}
2286- self.useFixture(ConfigFixture(config))
2287- self.assertRaises(
2288- EphemeralImagesDirectoryNotFound,
2289- compose_kernel_command_line, params)
2290-
2291 def test_compose_preseed_kernel_opt_returns_kernel_option(self):
2292 dummy_preseed_url = factory.make_name("url")
2293 self.assertEqual(
2294
2295=== modified file 'src/provisioningserver/tests/test_maas_import_pxe_files.py'
2296--- src/provisioningserver/tests/test_maas_import_pxe_files.py 2014-03-22 17:21:55 +0000
2297+++ src/provisioningserver/tests/test_maas_import_pxe_files.py 2014-03-25 17:45:57 +0000
2298@@ -16,6 +16,7 @@
2299
2300 import os
2301 from subprocess import check_call
2302+import unittest
2303
2304 from maastesting import root
2305 from maastesting.factory import factory
2306@@ -168,6 +169,11 @@
2307
2308 def setUp(self):
2309 super(TestImportPXEFiles, self).setUp()
2310+ raise unittest.SkipTest(
2311+ "XXX rvb 2014-03-21 bug=1295479: Disabled. The "
2312+ "maas-import-pxe-files script has been replaced with a "
2313+ "new version to use simplestreams v2's data. These tests need "
2314+ "to be completely refactored.")
2315 self.tftproot = self.make_dir()
2316 self.config = {"tftp": {"root": self.tftproot}}
2317 self.config_fixture = ConfigFixture(self.config)
2318
2319=== modified file 'src/provisioningserver/tests/test_upgrade_cluster.py'
2320--- src/provisioningserver/tests/test_upgrade_cluster.py 2014-03-18 04:25:00 +0000
2321+++ src/provisioningserver/tests/test_upgrade_cluster.py 2014-03-25 17:45:57 +0000
2322@@ -25,10 +25,17 @@
2323 import os.path
2324
2325 from maastesting.factory import factory
2326-from maastesting.matchers import MockCalledOnceWith
2327+from maastesting.matchers import (
2328+ MockCalledOnceWith,
2329+ MockNotCalled,
2330+ )
2331 from maastesting.testcase import MAASTestCase
2332-from mock import Mock
2333+from mock import (
2334+ ANY,
2335+ Mock,
2336+ )
2337 from provisioningserver import upgrade_cluster
2338+from provisioningserver.config import Config
2339 from provisioningserver.pxe.install_image import install_image
2340 from provisioningserver.testing.config import ConfigFixture
2341 from testtools.matchers import (
2342@@ -580,3 +587,50 @@
2343 entries_before = listdir(tftproot)
2344 upgrade_cluster.add_label_directory_level_to_boot_images()
2345 self.assertItemsEqual(entries_before, listdir(tftproot))
2346+
2347+
2348+class TestGenerateBootResourcesConfig(MAASTestCase):
2349+ """Tests for the `generate_boot_resources_config` upgrade."""
2350+
2351+ def patch_rewrite_boot_resources_config(self):
2352+ """Patch `rewrite_boot_resources_config` with a mock."""
2353+ return self.patch(upgrade_cluster, 'rewrite_boot_resources_config')
2354+
2355+ def patch_config(self, config):
2356+ """Patch the `bootresources.yaml` config with a given dict."""
2357+ original_load = Config.load_from_cache
2358+
2359+ @classmethod
2360+ def fake_config_load(cls, filename=None):
2361+ """Fake `Config.load_from_cache`.
2362+
2363+ Returns a susbtitute for `bootresources.yaml`, but defers to the
2364+ original implementation for other files. This means we can still
2365+ patch the original, and it means we'll probably get a tell-tale
2366+ error if any code underneath the tests accidentally tries to
2367+ load pserv.yaml.
2368+ """
2369+ if os.path.basename(filename) == 'bootresources.yaml':
2370+ return config
2371+ else:
2372+ return original_load(Config, filename=filename)
2373+
2374+ self.patch(Config, 'load_from_cache', fake_config_load)
2375+
2376+ def test_does_nothing_if_configure_me_is_False(self):
2377+ self.patch_config({'boot': {'configure_me': False}})
2378+ rewrite_config = self.patch_rewrite_boot_resources_config()
2379+ upgrade_cluster.generate_boot_resources_config()
2380+ self.assertThat(rewrite_config, MockNotCalled())
2381+
2382+ def test_does_nothing_if_configure_me_is_missing(self):
2383+ self.patch_config({'boot': {}})
2384+ rewrite_config = self.patch_rewrite_boot_resources_config()
2385+ upgrade_cluster.generate_boot_resources_config()
2386+ self.assertThat(rewrite_config, MockNotCalled())
2387+
2388+ def test_rewrites_if_configure_me_is_True(self):
2389+ self.patch_config({'boot': {'configure_me': True}})
2390+ rewrite_config = self.patch_rewrite_boot_resources_config()
2391+ upgrade_cluster.generate_boot_resources_config()
2392+ self.assertThat(rewrite_config, MockCalledOnceWith(ANY))
2393
2394=== modified file 'src/provisioningserver/tftp.py'
2395--- src/provisioningserver/tftp.py 2014-03-22 17:21:55 +0000
2396+++ src/provisioningserver/tftp.py 2014-03-25 17:45:57 +0000
2397@@ -329,6 +329,8 @@
2398 :param root: The root directory for this TFTP server.
2399 :param port: The port on which each server should be started.
2400 :param generator: The URL to be queried for PXE configuration.
2401+ This will normally point to the `pxeconfig` endpoint on the
2402+ region-controller API.
2403 """
2404 super(TFTPService, self).__init__()
2405 self.backend, self.port = TFTPBackend(root, generator), port
2406
2407=== modified file 'src/provisioningserver/upgrade_cluster.py'
2408--- src/provisioningserver/upgrade_cluster.py 2014-03-18 04:10:37 +0000
2409+++ src/provisioningserver/upgrade_cluster.py 2014-03-25 17:45:57 +0000
2410@@ -44,7 +44,10 @@
2411 from shutil import rmtree
2412
2413 from provisioningserver.config import Config
2414-from provisioningserver.utils import ensure_dir
2415+from provisioningserver.utils import (
2416+ ensure_dir,
2417+ locate_config,
2418+ )
2419
2420
2421 logger = getLogger(__name__)
2422@@ -246,6 +249,25 @@
2423 move_real_boot_image(tftproot, image)
2424
2425
2426+def rewrite_boot_resources_config(config_file):
2427+ """Rewrite the `bootresources.yaml` configuration."""
2428+
2429+
2430+def generate_boot_resources_config():
2431+ """Upgrade hook: rewrite `bootresources.yaml` based on boot images.
2432+
2433+ This finds boot images downloaded into the old, pre-Simplestreams tftp
2434+ root, and writes a boot-resources configuration to import a similar set of
2435+ images using Simplestreams.
2436+ """
2437+ config_file = locate_config('bootresources.yaml')
2438+ boot_resources = Config.load_from_cache(config_file)
2439+ if not boot_resources['boot'].get('configure_me', False):
2440+ # Already configured.
2441+ return
2442+ rewrite_boot_resources_config(config_file)
2443+
2444+
2445 # Upgrade hooks, from oldest to newest. The hooks are callables, taking no
2446 # arguments. They are called in order.
2447 #
2448@@ -253,6 +275,7 @@
2449 # no record of previous upgrades.
2450 UPGRADE_HOOKS = [
2451 add_label_directory_level_to_boot_images,
2452+ generate_boot_resources_config,
2453 ]
2454
2455