1
# Copyright 2012 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Install a netboot image directory for TFTP download."""
6
from __future__ import (
17
from filecmp import cmpfiles
18
from optparse import make_option
25
from celeryconfig import TFTPROOT
26
from django.core.management.base import BaseCommand
27
from provisioningserver.pxe.tftppath import (
33
def make_destination(tftproot, arch, subarch, release, purpose):
34
"""Locate an image's destination. Create containing directory if needed.
36
:param tftproot: The root directory served up by the TFTP server,
37
e.g. /var/lib/tftpboot/.
38
:param arch: Main architecture to locate the destination for.
39
:param subarch: Sub-architecture of the main architecture.
40
:param release: OS release name, e.g. "precise".
41
:param purpose: Purpose of this image, e.g. "install".
42
:return: Full path describing the image directory's new location and
43
name. In other words, a file "linux" in the image directory should
44
become os.path.join(make_destination(...), 'linux').
46
dest = locate_tftp_path(
47
compose_image_path(arch, subarch, release, purpose),
49
parent = os.path.dirname(dest)
50
if not os.path.isdir(parent):
55
def are_identical_dirs(old, new):
56
"""Do directories `old` and `new` contain identical files?
58
It's OK for `old` not to exist; that is considered a difference rather
59
than an error. But `new` is assumed to exist - if it doesn't, you
60
shouldn't have come far enough to call this function.
62
assert os.path.isdir(new)
63
if os.path.isdir(old):
64
files = set(os.listdir(old) + os.listdir(new))
65
# The shallow=False is needed to make cmpfiles() compare file
66
# contents. Otherwise it only compares os.stat() results,
67
match, mismatch, errors = cmpfiles(old, new, files, shallow=False)
68
return len(match) == len(files)
73
def install_dir(new, old):
74
"""Install directory `new`, replacing directory `old` if it exists.
76
This works as atomically as possible, but isn't entirely. Moreover,
77
any TFTP downloads that are reading from the old directory during
78
the move may receive inconsistent data, with some of the files (or
79
parts of files!) coming from the old directory and some from the
82
Some temporary paths will be used that are identical to `old`, but with
83
suffixes ".old" or ".new". If either of these directories already
84
exists, it will be mercilessly deleted.
86
This function makes no promises about whether it moves or copies
87
`new` into place. The caller should make an attempt to clean it up,
88
but be prepared for it not being there.
90
# Get rid of any leftover temporary directories from potential
91
# interrupted previous runs.
92
rmtree('%s.old' % old, ignore_errors=True)
93
rmtree('%s.new' % old, ignore_errors=True)
95
# We have to move the existing directory out of the way and the new
96
# one into place. Between those steps, there is a window where
97
# neither is in place. To minimize that window, move the new one
98
# into the same location (ensuring that it no longer needs copying
99
# from one partition to another) and then swizzle the two as quickly
101
# This could be a simple "remove" if the downloaded image is on the
102
# same filesystem as the destination, but because that isn't
103
# certain, copy instead. It's not particularly fast, but the extra
104
# work happens outside the critical window so it shouldn't matter
106
copytree(new, '%s.new' % old)
108
# Start of critical window.
109
if os.path.isdir(old):
110
os.rename(old, '%s.old' % old)
111
os.rename('%s.new' % old, old)
112
# End of critical window.
114
# Now delete the old image directory at leisure.
115
rmtree('%s.old' % old, ignore_errors=True)
118
class Command(BaseCommand):
119
"""Move a netboot image into the TFTP directory structure.
121
The image is a directory containing a kernel and an initrd. If the
122
destination location already has an image of the same name and
123
containing identical files, the new image is deleted and the old one
127
option_list = BaseCommand.option_list + (
129
'--arch', dest='arch', default=None,
130
help="Main system architecture that the image is for."),
132
'--subarch', dest='subarch', default='generic',
133
help="Sub-architecture of the main architecture."),
135
'--release', dest='release', default=None,
136
help="Ubuntu release that the image is for."),
138
'--purpose', dest='purpose', default=None,
139
help="Purpose of the image (e.g. 'install' or 'commissioning')."),
141
'--image', dest='image', default=None,
142
help="Netboot image directory, containing kernel & initrd."),
144
'--tftproot', dest='tftproot', default=TFTPROOT,
145
help="Store to this TFTP directory tree instead of the default."),
148
def handle(self, arch=None, subarch=None, release=None, purpose=None,
149
image=None, tftproot=None, **kwargs):
153
dest = make_destination(tftproot, arch, subarch, release, purpose)
154
if not are_identical_dirs(dest, image):
155
# Image has changed. Move the new version into place.
156
install_dir(image, dest)
157
rmtree(image, ignore_errors=True)