Merge lp:~jtv/maas/import-to-tftp into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 649
Proposed branch: lp:~jtv/maas/import-to-tftp
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 426 lines (+396/-3)
3 files modified
HACKING.txt (+3/-3)
scripts/maas-import-pxe-files (+191/-0)
src/maas/tests/test_maas_import_pxe_files.py (+202/-0)
To merge this branch: bzr merge lp:~jtv/maas/import-to-tftp
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+110451@code.launchpad.net

Commit message

Script to import kernels/initrds for install netboots.

Description of the change

See the “PXE boot requirements” document for the reasoning and conventions behind this branch. The branch adds a script that downloads the files that MAAS needs to provide over TFTP for a node to boot into the installer.

(Only the kernel and initrd are needed for this use-case; the minimal netbooted system will download the installer from an Ubuntu archive and start executing it.)

In a Cobblerless world this script replaces most, but not quite all, of what the existing maas-import-isos script does. There's still the preseeds and kernel parameters to be dealt with, but I think those will need to be done with knowledge from the database. My guess right now is that it can be done separately from the downloads.

There is still one problem with this script: a race condition. Updates are skipped when there are no changes, and the files are replaced as near-atomically as possible, but that doesn't help downloads that are ongoing at the time. Even if tftpd keeps file handles open for ongoing downloads (not too likely for a stateless UDP-based protocol!) so that each file will produce a consistent byte stream, the download is still 2 files, so a node could still see a mismatch between the kernel and the initrd. We agreed to tolerate this for now, but I'll file a bug.

For testing purposes I had to introduce a test dependency on curl. Amazingly, wget does not support the “file://” URLs I needed for my test. So for testing (and testing only) I substitute curl. The script is written such that it should work with either.

Jeroen

To post a comment you must log in.
Revision history for this message
Robie Basak (racb) wrote :

This is really readable and shouldn't be too difficult to add ARM support to cleanly - thanks!

Can we please use "generic" instead of "base"? "base" sounds like it is used as the foundation for something else which I think is a bit misleading. The armhf subarchitectures correspond to kernel flavours and "generic" is the standard i386/amd64 flavour that we are moving to so I think this makes sense.

ARCHIVE=${ARCHIVE:-http://archive.ubuntu.com/ubuntu/dists/} should skip the dists at the end - the standard way of specifying an archive is one level higher than this, without the dists. Then compose_installer_download_url would insert "dists" into the URL instead.

ARM support:

I'm happy to work on these as a separate merge request. Here are the changes I think we'll need:

The list of supported arches will need to grow from amd64 and i386 to armhf/highbank, armhf/armadaxp etc. At this point, it might be cleanest to specify ARCHES as "amd64/generic i386/generic armhf/highbank" etc.

The archive needs to be selected based on the architecture in use, in order to switch from archive.u.c/ubuntu to ports.u.c/ubuntu-ports. Can this be made into a function, rather than directly a variable?

compose_installer_download_url will need to use a lookup table as the exact URL differs based on arch and release. The needed URLs don't necessary end in pxelinux.0, linux and initrd.gz though, so I think this needs to be a bit more specialised and return the actual URL of the kernel image, initrd image and pxelinux.0 instead. For example: the armadaxp kernel image is called uImage because it is in a different format.

Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good. A few remarks:

[0]

99 + if [ -f $dest/pxelinux.0 ] && cmp pxelinux.0 $dest/pxelinux.0

I think you want to add "--quiet" to "cmp pxelinux.0 $dest/pxelinux.0" since you only care about the return code.

[1]

124 + rm -rf -- $dest/$name.old $dest/$name.new

How about using '-f' when moving files instead of cleaning up the files?

[2]

125 + # Put new downloads next to the old. If any file copying is needed

You've written the doc for this method without mentioning 'downloads'. I realize in practice this is used to move downloaded files but still, for consistency, I think you should s/downloads/directory/ here.

[3]

192 + cd -- $DOWNLOAD_DIR

Please consider using pushd/popd to restore go back to the original directory just before the script exits.

[4]

200 + # Replace the "base" with sub-architecture once we start supporting
201 + # those.
202 + arch_dir="$TFTPROOT/maas/$arch/base"
203 +
204 + mkdir -p -- $arch_dir
[…]
209 + rel_dir="$arch_dir/$release"
210 + mkdir -p -- $rel_dir
211 + update_install_files $rel_dir $arch $release

There is an additional white space on lines 202, 209 and 210.

[5]

259 + with open(os.path.join(path, name)) as infile:

Please consider using "open(os.path.join(path, name), 'rb')" for maximum compatibility.

[6]

196 + umask a+r

That looks like an important feature of the script. Don't you think it's worth a test?

[7]

66 +TFTPROOT=${TFTPROOT:-/var/lib/tftpboot/}
and
202 + arch_dir="$TFTPROOT/maas/$arch/base"

arch_dir will be /var/lib/tftpboot//maas/$arch/base. I know it's not a problem but maybe you'll want to remove that '//'.

[8]

Same "problem" with the download path: it downloads http://archive.ubuntu.com/ubuntu/dists//lucid/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64//linux

[9]

54 +# Ubuntu releases that are to be downloaded.
55 +SUPPORTED_RELEASES=$(distro-info --supported)

Just a question here, is MAAS really supporting pre-precise releases?

[10]

70 +DOWNLOAD=${DOWNLOAD:-wget --no-verbose}

You've used --no-verbose and not --quiet here. This means that the output of this script looks like this: http://paste.ubuntu.com/1042263/. I think it's fine but I just wanted to make sure that this was your intention. The only weird thing is that it says "Updating files for oneiric-i386." after the log of the download is displayed. No biggie though.

[11]

I've noticed you've left the existing 'maas-import-isos' untouched. I understand we probably want to keep it around until the Cobbler replacement is done. But don't you think we should file bugs to make sure we don't forget to cleanup things when the replacement will be completed? I'd like your opinion on this because I've got the exact same dilemma about preseed templates/snippets.

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

Thanks for the unexpected additional review. I've made the changes you asked for:

> Can we please use "generic" instead of "base"? "base" sounds like it is used
> as the foundation for something else which I think is a bit misleading. The
> armhf subarchitectures correspond to kernel flavours and "generic" is the
> standard i386/amd64 flavour that we are moving to so I think this makes sense.

Just goes to show that no matter how many people you sanity-check with, there's always something you don't know about. I updated the code, and the PXE requirements document as well.

> ARCHIVE=${ARCHIVE:-http://archive.ubuntu.com/ubuntu/dists/} should skip the
> dists at the end - the standard way of specifying an archive is one level
> higher than this, without the dists. Then compose_installer_download_url would
> insert "dists" into the URL instead.

Done. Gratifying to see how few changes that took.

You also mentioned some changes you might want to undertake yourself:

> The list of supported arches will need to grow from amd64 and i386 to
> armhf/highbank, armhf/armadaxp etc. At this point, it might be cleanest to
> specify ARCHES as "amd64/generic i386/generic armhf/highbank" etc.

Yes, something like that. Right now we can't quite do that because these settings are shared with the old, Cobbler-based scripts.

> The archive needs to be selected based on the architecture in use, in order to
> switch from archive.u.c/ubuntu to ports.u.c/ubuntu-ports. Can this be made
> into a function, rather than directly a variable?

I suppose. But it's a worry for later... Other solutions may become apparent in the meantime.

> compose_installer_download_url will need to use a lookup table as the exact
> URL differs based on arch and release. The needed URLs don't necessary end in
> pxelinux.0, linux and initrd.gz though, so I think this needs to be a bit more
> specialised and return the actual URL of the kernel image, initrd image and
> pxelinux.0 instead. For example: the armadaxp kernel image is called uImage
> because it is in a different format.

A bit disheartening, but I guess we'll just have to deal with it when the time comes.

Jeroen

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (6.0 KiB)

And on to Raphaël's review!

> 99 + if [ -f $dest/pxelinux.0 ] && cmp pxelinux.0 $dest/pxelinux.0
>
> I think you want to add "--quiet" to "cmp pxelinux.0 $dest/pxelinux.0" since
> you only care about the return code.

Oh yes, absolutely. That also means I no longer need to check for the file's existence. I only did that to suppress the output — the documentation for --quiet suggested that the error message for a missing file would still be there, as you'd normally expect anyway, so I didn't think to use it.

> [1]
>
> 124 + rm -rf -- $dest/$name.old $dest/$name.new
>
> How about using '-f' when moving files instead of cleaning up the files?

Won't work for (non-empty) directories. There's even an option to make “mv” act as if the destination were a file, but I couldn't find a way to make that do what I wanted either.

> [2]
>
> 125 + # Put new downloads next to the old. If any file copying is
> needed
>
> You've written the doc for this method without mentioning 'downloads'. I
> realize in practice this is used to move downloaded files but still, for
> consistency, I think you should s/downloads/directory/ here.

Oops. Older incarnation of the code, when this wasn't extracted yet. Fixed.

> [3]
>
> 192 + cd -- $DOWNLOAD_DIR
>
> Please consider using pushd/popd to restore go back to the original directory
> just before the script exits.

I did consider it at the time, but bear in mind that “cd” will only affect the process of the shell that was forked for the purpose of running the script. The caller isn't affected.

> [4]
>
> 200 + # Replace the "base" with sub-architecture once we start
> supporting
> 201 + # those.
> 202 + arch_dir="$TFTPROOT/maas/$arch/base"
> 203 +
> 204 + mkdir -p -- $arch_dir
> […]
> 209 + rel_dir="$arch_dir/$release"
> 210 + mkdir -p -- $rel_dir
> 211 + update_install_files $rel_dir $arch $release
>
> There is an additional white space on lines 202, 209 and 210.

Took me a while to figure it out, since I didn't find any extra space in my editor: I got tabs I never asked for! All fixed now.

> [5]
>
> 259 + with open(os.path.join(path, name)) as infile:
>
> Please consider using "open(os.path.join(path, name), 'rb')" for maximum
> compatibility.

Compatibility with what? Windows? Python 3? Unusual encodings? Given that this is for ASCII text files written from the same tests, what's the benefit of treating them as binary files?

> [6]
>
> 196 + umask a+r
>
> That looks like an important feature of the script. Don't you think it's
> worth a test?

Not particularly, no. It just means that the script will write world-readable files even if your umask doesn't normally do that. It doesn't affect its environment in any other way.

> [7]
>
> 66 +TFTPROOT=${TFTPROOT:-/var/lib/tftpboot/}
> and
> 202 + arch_dir="$TFTPROOT/maas/$arch/base"
>
> arch_dir will be /var/lib/tftpboot//maas/$arch/base. I know it's not a
> problem but maybe you'll want to remove that '//'.

I felt the trailing slash on $TFTPROOT was clearer, but if you have a problem...

Read more...

Revision history for this message
MAAS Lander (maas-lander) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Revision history for this message
Raphaël Badin (rvb) wrote :

> Compatibility with what? Windows? Python 3? Unusual encodings? Given that this is for ASCII text files written from
> the same tests, what's the benefit of treating them as binary files?

Yes, Windows. I guess it's a little bit stupid in this instance as you mention but nonetheless, it's probably a good habit.

> Remember: cleanup is a noun, clean up is a verb!

Rarg!

> I don't like to worry too much about these small things in advance: if cleanup fits in naturally with the other work,
> we'll just do it and not worry about it. If we discover that it doesn't, then that is a good time to file a bug
> or a card so we don't remember. Until then, piling up detailed bugs or cards will just cloud our view of the work
> ahead. And as always: no matter how clear-cut a remote problem seems now, expect our view of it to change by the
> time it becomes relevant.

Fair enough.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'HACKING.txt'
--- HACKING.txt 2012-06-08 12:27:18 +0000
+++ HACKING.txt 2012-06-15 13:33:19 +0000
@@ -44,16 +44,16 @@
44 python-formencode python-oauth python-oops python-oops-datedir-repo \44 python-formencode python-oauth python-oops python-oops-datedir-repo \
45 python-twisted python-oops-wsgi python-oops-twisted \45 python-twisted python-oops-wsgi python-oops-twisted \
46 python-psycopg2 python-yaml python-convoy python-django-south \46 python-psycopg2 python-yaml python-convoy python-django-south \
47 python-avahi python-dbus python-celery python-tempita47 python-avahi python-dbus python-celery python-tempita distro-info
4848
49Additionally, you need to install the following python libraries49Additionally, you need to install the following python libraries
50for development convenience::50for development convenience::
5151
52 $ sudo apt-get install python-sphinx python-lxml python-pocket-lint52 $ sudo apt-get install python-sphinx python-lxml python-pocket-lint
5353
54If you intend to run the test suite, you also need xvfb and firefox::54If you intend to run the test suite, you also need a few other things:
5555
56 $ sudo apt-get install xvfb firefox avahi-utils56 $ sudo apt-get install xvfb firefox avahi-utils curl
5757
58All other development dependencies are pulled automatically from `PyPI`_58All other development dependencies are pulled automatically from `PyPI`_
59when buildout runs.59when buildout runs.
6060
=== added file 'scripts/maas-import-pxe-files'
--- scripts/maas-import-pxe-files 1970-01-01 00:00:00 +0000
+++ scripts/maas-import-pxe-files 2012-06-15 13:33:19 +0000
@@ -0,0 +1,191 @@
1#!/usr/bin/env bash
2# Copyright 2012 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).
4#
5# Download static files needed for net-booting nodes through TFTP:
6# pre-boot loader, kernels, and initrd images.
7#
8# This script downloads the required files into the TFTP home directory
9# (by default, /var/lib/tftpboot). Run it with the necessarily privileges
10# to write them there.
11
12# Exit immediately if a command exits with a non-zero status.
13set -o errexit
14# Treat unset variables as an error when substituting.
15set -o nounset
16
17# Load settings if available.
18settings="/etc/maas/import_isos"
19[ -r $settings ] && . $settings
20local_settings="$(pwd)/$settings"
21[ -r $local_settings ] && . $local_settings
22
23# Download location for Ubuntu releases.
24ARCHIVE=${ARCHIVE:-http://archive.ubuntu.com/ubuntu/}
25
26# Ubuntu releases that are to be downloaded.
27SUPPORTED_RELEASES=$(distro-info --supported)
28RELEASES=${RELEASES:-$SUPPORTED_RELEASES}
29
30# The current Ubuntu release.
31STABLE_RELEASE=$(distro-info --stable)
32CURRENT_RELEASE=${CURRENT_RELEASE:-$STABLE_RELEASE}
33
34# Supported architectures.
35ARCHES=${ARCHES:-amd64 i386}
36
37# TFTP root directory. (Don't let the "root" vs. "boot" confuse you.)
38TFTPROOT=${TFTPROOT:-/var/lib/tftpboot}
39
40# Command line to download a resource at a given URL into the current
41# directory. A wget command line will work here, but curl will do as well.
42DOWNLOAD=${DOWNLOAD:-wget --no-verbose}
43
44
45# Put together a full URL for where the installer files for architecture $1
46# and release $2 can be downloaded.
47compose_installer_download_url() {
48 local arch=$1 release=$2
49 local installer_url="$ARCHIVE/dists/$release/main/installer-$arch"
50 echo "$installer_url/current/images/netboot/ubuntu-installer/$arch/"
51}
52
53
54# Download the pre-boot loader, pxelinux.0, for architecture $2 if it exists,
55# and if so, install it into directory $1. (Not all architectures need this
56# file, and there's rarely an urgent need for the very latest file, so if
57# the download fails this function just skips it.)
58update_pre_boot_loader() {
59 local dest=$1 arch=$2
60 local url=$(compose_installer_download_url $arch $CURRENT_RELEASE)
61 if ! $DOWNLOAD $url/pxelinux.0
62 then
63 echo "No pre-boot loader for $arch; skipping."
64 return
65 fi
66
67 # If the file has changed, move it into place (replacing any previous
68 # version). Otherwise, leave the filesystem alone.
69 if [ -f pxelinux.0 ]
70 then
71 if cmp --quiet pxelinux.0 $dest/pxelinux.0
72 then
73 rm -f -- pxelinux.0
74 else
75 echo "Updating pre-boot loader for $arch."
76 mv -- pxelinux.0 $dest/
77 fi
78 fi
79}
80
81
82# Move local directory $1 into directory $2, so that it becomes $2/$1.
83# If a directory of the same name already existed in $2, remove it along
84# with all its contents.
85# This will use "$2/$1.new" and "$2/$1.old" as temporary locations, which
86# will also be deleted.
87install_dir() {
88 local name=$1 dest=$2
89 # Use the "old"/"new" directories to maximize speed and minimize
90 # inconvenience: ensure that the new directory contents are on the
91 # partition they will ultimately need to be on, then move the old
92 # ones out of the way and immediately replace them with the new, and
93 # finally remove the old ones outside of the critical path.
94 # This will not resolve races with ongoing downloads by booting nodes.
95 # It merely minimizes this script's side of the race window.
96 rm -rf -- $dest/$name.old $dest/$name.new
97 # Put new downloads next to the old. If any file copying is needed
98 # because directories are on different partitions, it happens here.
99 mv -- $name $dest/$name.new
100
101 # Start of the critical window.
102
103 # Move existing directory (if any) out of the way.
104 [ -d $dest/$name ] && mv -- $dest/$name $dest/$name.old
105
106 # Move new directory into place.
107 mv -- $dest/$name.new $dest/$name
108
109 # End of the critical window.
110
111 # Clean up the old directory (if any).
112 rm -rf -- $dest/$name.old
113}
114
115
116# Compare given file names between directories $1 and $2. Print "yes" if
117# they are all identical, or "no" if any of them are different or missing.
118identical_files() {
119 local lhs=$1 rhs=$2
120 local file
121 shift 2
122 for file in $*
123 do
124 if ! cmp --quiet $lhs/$file $rhs/$file
125 then
126 echo "no"
127 return
128 fi
129 done
130 echo "yes"
131}
132
133
134# Download kernel/initrd for installing Ubuntu release $3 for
135# architecture $2 into directory $1/install.
136update_install_files() {
137 local dest=$1 arch=$2 release=$3
138 local files="initrd.gz linux"
139 local url=$(compose_installer_download_url $arch $release)
140 local file
141
142 mkdir "install"
143 pushd "install" >/dev/null
144 for file in $files
145 do
146 $DOWNLOAD $url/$file
147 done
148 popd >/dev/null
149
150 # TODO: Prevent this change from upsetting any currently booting nodes.
151 if [ $(identical_files "install" $dest/"install" $files) != 'yes' ]
152 then
153 echo "Updating files for $release-$arch."
154 install_dir "install" $dest
155 fi
156}
157
158
159main() {
160 local arch release arch_dir rel_dir
161
162 DOWNLOAD_DIR=$(mktemp -d)
163 echo "Downloading to temporary location $DOWNLOAD_DIR."
164 cd -- $DOWNLOAD_DIR
165
166 # All files we create here are public. The TFTP user will need to be
167 # able to read them.
168 umask a+r
169
170 for arch in $ARCHES
171 do
172 # Replace the "generic" with sub-architecture once we start
173 # supporting those.
174 arch_dir="$TFTPROOT/maas/$arch/generic"
175
176 mkdir -p -- $arch_dir
177 update_pre_boot_loader $arch_dir $arch
178
179 for release in $RELEASES
180 do
181 rel_dir="$arch_dir/$release"
182 mkdir -p -- $rel_dir
183 update_install_files $rel_dir $arch $release
184 done
185 done
186
187 rm -rf -- $DOWNLOAD_DIR
188}
189
190
191main
0192
=== added file 'src/maas/tests/test_maas_import_pxe_files.py'
--- src/maas/tests/test_maas_import_pxe_files.py 1970-01-01 00:00:00 +0000
+++ src/maas/tests/test_maas_import_pxe_files.py 2012-06-15 13:33:19 +0000
@@ -0,0 +1,202 @@
1# Copyright 2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for the maas-import-pxe-files script."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12__metaclass__ = type
13__all__ = []
14
15import os
16from stat import ST_MTIME
17from subprocess import check_call
18
19from maastesting.factory import factory
20from maastesting.testcase import TestCase
21from testtools.matchers import (
22 FileContains,
23 FileExists,
24 Not,
25 )
26
27
28def make_name(prefix, separator='-'):
29 """Return an arbitrary name, with the given prefix."""
30 return separator.join([prefix, factory.getRandomString(4)])
31
32
33def read_file(path, name):
34 """Return the contents of the file at `path/name`."""
35 with open(os.path.join(path, name)) as infile:
36 return infile.read()
37
38
39def get_write_time(path):
40 """Return last modification time of file at `path`."""
41 return os.stat(path)[ST_MTIME]
42
43
44def backdate(path):
45 """Set the last modification time for the file at `path` to the past."""
46 os.utime(path, (99999, 99999))
47
48
49def compose_download_dir(archive, arch, release):
50 """Locate a bootloader, initrd, and kernel in an archive.
51
52 :param archive: Archive directory (corresponding to the script's ARCHIVE
53 setting, except here it's a filesystem path not a URL).
54 :param arch: Architecture.
55 :param release: Ubuntu release name.
56 :return: Full absolute path to the directory holding the requisite files
57 for this archive, arch, and release.
58 """
59 return os.path.join(
60 archive, 'dists', release, 'main', 'installer-%s' % arch, 'current',
61 'images', 'netboot', 'ubuntu-installer', arch)
62
63
64def compose_tftp_path(tftproot, arch, *path):
65 """Compose path for MAAS TFTP files for given architecture.
66
67 After the TFTP root directory and the architecture, just append any path
68 components you want to drill deeper, e.g. the release name to get to the
69 files for a specific release.
70 """
71 return os.path.join(tftproot, 'maas', arch, 'generic', *path)
72
73
74class TestImportPXEFiles(TestCase):
75
76 def make_downloads(self, release=None, arch=None):
77 """Set up a directory with an image for "download" by the script.
78
79 Returns an "ARCHIVE" URL for the download.
80 """
81 if release is None:
82 release = make_name('release')
83 if arch is None:
84 arch = make_name('arch')
85 archive = self.make_dir()
86 download = compose_download_dir(archive, arch, release)
87 os.makedirs(download)
88 for filename in ['initrd.gz', 'linux', 'pxelinux.0']:
89 factory.make_file(download, filename)
90 return archive
91
92 def call_script(self, archive_dir, tftproot, arch=None, release=None):
93 """Call the maas-download-pxe-files script with given settings.
94
95 The ARCHIVE URL and TFTPROOT path must be set, or the script will try
96 to download from the Ubuntu server and store into the system's real
97 TFTP root directory, respectively. Both bad ideas.
98 """
99 script = './scripts/maas-import-pxe-files'
100 env = {
101 'ARCHIVE': 'file://%s' % archive_dir,
102 # Substitute curl for wget; it accepts file:// URLs.
103 'DOWNLOAD': 'curl -O --silent',
104 'TFTPROOT': tftproot,
105 }
106 if arch is not None:
107 env['ARCHES'] = arch
108 if release is not None:
109 env['RELEASES'] = release
110 env['CURRENT_RELEASE'] = release
111 with open('/dev/null', 'w') as dev_null:
112 check_call(script, env=env, stdout=dev_null)
113
114 def test_downloads_pre_boot_loader(self):
115 arch = make_name('arch')
116 release = 'precise'
117 archive = self.make_downloads(arch=arch, release=release)
118 tftproot = self.make_dir()
119 self.call_script(archive, tftproot, arch=arch, release=release)
120 tftp_path = compose_tftp_path(tftproot, arch, 'pxelinux.0')
121 download_path = compose_download_dir(archive, arch, release)
122 expected_contents = read_file(download_path, 'pxelinux.0')
123 self.assertThat(tftp_path, FileContains(expected_contents))
124
125 def test_ignores_missing_pre_boot_loader(self):
126 arch = make_name('arch')
127 release = 'precise'
128 archive = self.make_downloads(arch=arch, release=release)
129 download_path = compose_download_dir(archive, arch, release)
130 os.remove(os.path.join(download_path, 'pxelinux.0'))
131 tftproot = self.make_dir()
132 self.call_script(archive, tftproot, arch=arch, release=release)
133 tftp_path = compose_tftp_path(tftproot, arch, 'pxelinux.0')
134 self.assertThat(tftp_path, Not(FileExists()))
135
136 def test_updates_pre_boot_loader(self):
137 arch = make_name('arch')
138 release = 'precise'
139 tftproot = self.make_dir()
140 tftp_path = compose_tftp_path(tftproot, arch, 'pxelinux.0')
141 os.makedirs(os.path.dirname(tftp_path))
142 with open(tftp_path, 'w') as existing_file:
143 existing_file.write(factory.getRandomString())
144 archive = self.make_downloads(arch=arch, release=release)
145 self.call_script(archive, tftproot, arch=arch, release=release)
146 download_path = compose_download_dir(archive, arch, release)
147 expected_contents = read_file(download_path, 'pxelinux.0')
148 self.assertThat(tftp_path, FileContains(expected_contents))
149
150 def test_leaves_pre_boot_loader_untouched_if_unchanged(self):
151 # If pxelinux.0 has not changed between script runs, the old
152 # copy stays in place.
153 arch = make_name('arch')
154 release = 'precise'
155 archive = self.make_downloads(arch=arch, release=release)
156 tftproot = self.make_dir()
157 self.call_script(archive, tftproot, arch=arch, release=release)
158 tftp_path = compose_tftp_path(tftproot, arch, 'pxelinux.0')
159 backdate(tftp_path)
160 original_timestamp = get_write_time(tftp_path)
161 self.call_script(archive, tftproot, arch=arch, release=release)
162 self.assertEqual(original_timestamp, get_write_time(tftp_path))
163
164 def test_downloads_install_image(self):
165 arch = make_name('arch')
166 release = 'precise'
167 archive = self.make_downloads(arch=arch, release=release)
168 tftproot = self.make_dir()
169 self.call_script(archive, tftproot, arch=arch, release=release)
170 tftp_path = compose_tftp_path(
171 tftproot, arch, release, 'install', 'linux')
172 download_path = compose_download_dir(archive, arch, release)
173 expected_contents = read_file(download_path, 'linux')
174 self.assertThat(tftp_path, FileContains(expected_contents))
175
176 def test_updates_install_image(self):
177 arch = make_name('arch')
178 release = 'precise'
179 tftproot = self.make_dir()
180 tftp_path = compose_tftp_path(
181 tftproot, arch, release, 'install', 'linux')
182 os.makedirs(os.path.dirname(tftp_path))
183 with open(tftp_path, 'w') as existing_file:
184 existing_file.write(factory.getRandomString())
185 archive = self.make_downloads(arch=arch, release=release)
186 self.call_script(archive, tftproot, arch=arch, release=release)
187 download_path = compose_download_dir(archive, arch, release)
188 expected_contents = read_file(download_path, 'linux')
189 self.assertThat(tftp_path, FileContains(expected_contents))
190
191 def test_leaves_install_image_untouched_if_unchanged(self):
192 arch = make_name('arch')
193 release = 'precise'
194 archive = self.make_downloads(arch=arch, release=release)
195 tftproot = self.make_dir()
196 self.call_script(archive, tftproot, arch=arch, release=release)
197 tftp_path = compose_tftp_path(
198 tftproot, arch, release, 'install', 'linux')
199 backdate(tftp_path)
200 original_timestamp = get_write_time(tftp_path)
201 self.call_script(archive, tftproot, arch=arch, release=release)
202 self.assertEqual(original_timestamp, get_write_time(tftp_path))