Merge lp:~maas-maintainers/maas/new-import-script-integration into lp:~maas-committers/maas/trunk
- new-import-script-integration
- Merge into 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 |
Related bugs: |
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-
Description of the change
To post a comment you must log in.
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 |
Approving on behalf of all who sailed the merry seas of this here branch.