/usr/share/pyshared/provisioningserver/pxe/install_image.py is in python-maas-provisioningserver 1.2+bzr1373+dfsg-0ubuntu1~12.04.6.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | # Copyright 2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Install a netboot image directory for TFTP download."""
from __future__ import (
absolute_import,
print_function,
unicode_literals,
)
__metaclass__ = type
__all__ = [
"add_arguments",
"run",
]
from filecmp import cmpfiles
import os.path
from shutil import (
copytree,
rmtree,
)
from provisioningserver.config import Config
from provisioningserver.pxe.tftppath import (
compose_image_path,
locate_tftp_path,
)
from twisted.python.filepath import FilePath
def make_destination(tftproot, arch, subarch, release, purpose):
"""Locate an image's destination. Create containing directory if needed.
:param tftproot: The root directory served up by the TFTP server,
e.g. /var/lib/maas/tftp/.
:param arch: Main architecture to locate the destination for.
:param subarch: Sub-architecture of the main architecture.
:param release: OS release name, e.g. "precise".
:param purpose: Purpose of this image, e.g. "install".
:return: Full path describing the image directory's new location and
name. In other words, a file "linux" in the image directory should
become os.path.join(make_destination(...), 'linux').
"""
dest = locate_tftp_path(
compose_image_path(arch, subarch, release, purpose),
tftproot=tftproot)
parent = os.path.dirname(dest)
if not os.path.isdir(parent):
os.makedirs(parent)
return dest
def are_identical_dirs(old, new):
"""Do directories `old` and `new` contain identical files?
It's OK for `old` not to exist; that is considered a difference rather
than an error. But `new` is assumed to exist - if it doesn't, you
shouldn't have come far enough to call this function.
"""
assert os.path.isdir(new)
if os.path.isdir(old):
files = set(os.listdir(old) + os.listdir(new))
# The shallow=False is needed to make cmpfiles() compare file
# contents. Otherwise it only compares os.stat() results,
match, mismatch, errors = cmpfiles(old, new, files, shallow=False)
return len(match) == len(files)
else:
return False
def install_dir(new, old):
"""Install directory `new`, replacing directory `old` if it exists.
This works as atomically as possible, but isn't entirely. Moreover,
any TFTP downloads that are reading from the old directory during
the move may receive inconsistent data, with some of the files (or
parts of files!) coming from the old directory and some from the
new.
Some temporary paths will be used that are identical to `old`, but with
suffixes ".old" or ".new". If either of these directories already
exists, it will be mercilessly deleted.
This function makes no promises about whether it moves or copies
`new` into place. The caller should make an attempt to clean it up,
but be prepared for it not being there.
"""
# Get rid of any leftover temporary directories from potential
# interrupted previous runs.
rmtree('%s.old' % old, ignore_errors=True)
rmtree('%s.new' % old, ignore_errors=True)
# We have to move the existing directory out of the way and the new
# one into place. Between those steps, there is a window where
# neither is in place. To minimize that window, move the new one
# into the same location (ensuring that it no longer needs copying
# from one partition to another) and then swizzle the two as quickly
# as possible.
# This could be a simple "remove" if the downloaded image is on the
# same filesystem as the destination, but because that isn't
# certain, copy instead. It's not particularly fast, but the extra
# work happens outside the critical window so it shouldn't matter
# much.
copytree(new, '%s.new' % old)
# Normalise permissions.
for filepath in FilePath('%s.new' % old).walk():
if filepath.isdir():
filepath.chmod(0755)
else:
filepath.chmod(0644)
# Start of critical window.
if os.path.isdir(old):
os.rename(old, '%s.old' % old)
os.rename('%s.new' % old, old)
# End of critical window.
# Now delete the old image directory at leisure.
rmtree('%s.old' % old, ignore_errors=True)
def add_arguments(parser):
parser.add_argument(
'--arch', dest='arch', default=None,
help="Main system architecture that the image is for.")
parser.add_argument(
'--subarch', dest='subarch', default='generic',
help="Sub-architecture of the main architecture [%(default)s].")
parser.add_argument(
'--release', dest='release', default=None,
help="Ubuntu release that the image is for.")
parser.add_argument(
'--purpose', dest='purpose', default=None,
help="Purpose of the image (e.g. 'install' or 'commissioning').")
parser.add_argument(
'--image', dest='image', default=None,
help="Netboot image directory, containing kernel & initrd.")
def run(args):
"""Move a netboot image into the TFTP directory structure.
The image is a directory containing a kernel and an initrd. If the
destination location already has an image of the same name and
containing identical files, the new image is deleted and the old one
is left untouched.
"""
config = Config.load(args.config_file)
tftproot = config["tftp"]["root"]
destination = make_destination(
tftproot, args.arch, args.subarch, args.release, args.purpose)
if not are_identical_dirs(destination, args.image):
# Image has changed. Move the new version into place.
install_dir(args.image, destination)
rmtree(args.image, ignore_errors=True)
|