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 (
18
from filecmp import cmpfiles
25
from provisioningserver.config import Config
26
from provisioningserver.pxe.tftppath import (
30
from twisted.python.filepath import FilePath
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/maas/tftp/.
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, symlink=None):
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
# Normalise permissions.
109
for filepath in FilePath('%s.new' % old).walk():
115
# Start of critical window.
116
if os.path.isdir(old):
117
os.rename(old, '%s.old' % old)
118
os.rename('%s.new' % old, old)
119
# End of critical window.
121
# Now delete the old image directory at leisure.
122
rmtree('%s.old' % old, ignore_errors=True)
124
# Symlink the new image directory to 'symlink'.
125
if symlink is not None:
126
sdest = "%s/%s" % (os.path.dirname(old), symlink)
127
os.symlink(old, sdest)
130
def add_arguments(parser):
132
'--arch', dest='arch', default=None,
133
help="Main system architecture that the image is for.")
135
'--subarch', dest='subarch', default='generic',
136
help="Sub-architecture of the main architecture [%(default)s].")
138
'--release', dest='release', default=None,
139
help="Ubuntu release that the image is for.")
141
'--purpose', dest='purpose', default=None,
142
help="Purpose of the image (e.g. 'install' or 'commissioning').")
144
'--image', dest='image', default=None,
145
help="Netboot image directory, containing kernel & initrd.")
147
'--symlink', dest='symlink', default=None,
148
help="Destination directory to symlink the installed images to.")
152
"""Move a netboot image into the TFTP directory structure.
154
The image is a directory containing a kernel and an initrd. If the
155
destination location already has an image of the same name and
156
containing identical files, the new image is deleted and the old one
159
config = Config.load(args.config_file)
160
tftproot = config["tftp"]["root"]
161
destination = make_destination(
162
tftproot, args.arch, args.subarch, args.release, args.purpose)
163
if not are_identical_dirs(destination, args.image):
164
# Image has changed. Move the new version into place.
165
install_dir(args.image, destination, args.symlink)
166
rmtree(args.image, ignore_errors=True)