/usr/lib/python3/dist-packages/provisioningserver/import_images/download_resources.py is in python3-maas-provisioningserver 2.4.0~beta2-6865-gec43e47e6-0ubuntu1.
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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 | # Copyright 2014-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Simplestreams code to download boot resources."""
__all__ = [
'download_all_boot_resources',
]
from datetime import datetime
import os.path
import tarfile
from provisioningserver.import_images.helpers import (
get_os_from_product,
get_signing_policy,
maaslog,
)
from provisioningserver.logger import LegacyLogger
from simplestreams.mirrors import (
BasicMirrorWriter,
UrlMirrorReader,
)
from simplestreams.objectstores import FileStore
from simplestreams.util import (
item_checksums,
path_from_mirror_url,
products_exdata,
)
log = LegacyLogger()
DEFAULT_KEYRING_PATH = "/usr/share/keyrings"
def insert_file(store, name, tag, checksums, size, content_source):
"""Insert a file into `store`.
:param store: A simplestreams `ObjectStore`.
:param name: Logical name of the file being inserted. Only needs to be
unique within the scope of this boot image.
:param tag: UUID, or "tag," for the file. It will be inserted into
`store` under this name, not its logical name.
:param checksums: A Simplestreams checksums dict, mapping hash algorihm
names (such as `sha256`) to the file's respective checksums as
computed by those hash algorithms.
:param size: Optional size for the file, so Simplestreams knows what size
to expect.
:param content_source: A Simplestreams `ContentSource` for reading the
file.
:return: A list of inserted files (actually, only the one file in this
case) described as tuples of (path, logical name). The path lies in
the directory managed by `store` and has a filename based on `tag`,
not logical name.
"""
maaslog.debug("Inserting file %s (tag=%s, size=%s).", name, tag, size)
store.insert(tag, content_source, checksums, mutable=False, size=size)
# XXX jtv 2014-04-24 bug=1313580: Isn't _fullpath meant to be private?
return [(store._fullpath(tag), name)]
def extract_archive_tar(store, name, tag, checksums, size, content_source):
"""Extract an archive.tar.xz into `store`.
:param store: A simplestreams `ObjectStore`.
:param name: Logical name of the file being inserted. Only needs to be
unique within the scope of this boot image.
:param tag: UUID, or "tag," for the file archive.tar.xz file. The
archive.tar.xz file along with its contents will be stored in the
cache directory under names derived from this tag.
:param checksums: A Simplestreams checksums dict, mapping hash algorihm
names (such as `sha256`) to the file's respective checksums as
computed by those hash algorithms.
:param size: Optional size for the file, so Simplestreams knows what size
to expect.
:param content_source: A Simplestreams `ContentSource` for reading the
file.
:return: A list of inserted files (file and archive.tar.xz) described
as tuples of (path, logical name). The path lies in the directory
managed by `store` and has a filename based on `tag`, not logical name.
"""
maaslog.debug("Inserting archive %s (tag=%s, size=%s).", name, tag, size)
extracted_files = []
cache_dir = store._fullpath('')
# Check if the archive has already been extracted. This is done by scanning
# the cache directory for files containing the given tag. Since the tag is
# the SHA256 this will always be unique and if files are added/removed from
# the archive we'll get a new tag.
for root, dirs, files in os.walk(cache_dir):
for f in files:
if f.endswith(tag):
# Strip out the tag
filename = f[:-(len(tag) + 1)]
if root != cache_dir:
filename = os.path.join(root[len(cache_dir):], filename)
# Give full path to cached file
filepath = os.path.join(root, f)
extracted_files.append((filepath, filename))
# If no files with the given tag were found we need to extract them.
if extracted_files == []:
maaslog.debug(
"Extracting archive %s (tag=%s, size=%s).", name, tag, size)
archive_path = store._fullpath(tag)
store.insert(tag, content_source, checksums, mutable=False, size=size)
with tarfile.open(archive_path, 'r|*') as tar:
for member in tar:
if member.isfile():
filename = member.name
filepath = store._fullpath('%s-%s' % (filename, tag))
fo = tar.extractfile(member)
store.insert(filepath, fo, mutable=False)
extracted_files.append((filepath, filename))
store.remove(tag)
# Return the list of sets containing the path to the cache file and the
# real filename which should be used.
return extracted_files
def link_resources(
snapshot_path, links, osystem, arch, release, label,
subarches, bootloader_type=None):
"""Hardlink entries in the snapshot directory to resources in the cache.
This creates file entries in the snapshot directory for boot resources
that are part of a single boot image.
:param snapshot_path: Snapshot directory.
:param links: A list of links that should be created to files stored in
the cache. Each link is described as a tuple of (path, logical
name). The path points to a file in the cache directory. The logical
name will be link's filename, without path.
:param osystem: Operating system with this boot image supports.
:param arch: Architecture which this boot image supports.
:param release: OS release of which this boot image is a part.
:param label: OS release label of which this boot image is a part, e.g.
`release` or `rc`.
:param subarches: A list of sub-architectures which this boot image
supports. For example, a kernel for one Ubuntu release for a given
architecture and subarchitecture `generic` will typically also support
the `hwe-*` subarchitectures that denote hardware-enablement kernels
for older Ubuntu releases.
:param bootloader_type: If the resource is a bootloader specify the type of
bootloader(pxe, uefi, open-firmware). Bootloader resources are linked
under a base 'bootloader' directory instead of the image path.
"""
for subarch in subarches:
if bootloader_type is None:
directory = os.path.join(
snapshot_path, osystem, arch, subarch, release, label)
else:
# Subarches are only supported on Ubuntu. With the path bootloaders
# are being put in below having multiple subarches on a bootloader
# will cause the contents from one subarch to overwrite the
# contents of another.
assert(len(subarches) == 1)
directory = os.path.join(
snapshot_path, 'bootloader', bootloader_type, arch)
if not os.path.exists(directory):
os.makedirs(directory)
for cached_file, logical_name in links:
link_path = os.path.join(directory, logical_name)
if os.path.isfile(link_path):
os.remove(link_path)
base_dir = os.path.dirname(link_path)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
os.link(cached_file, link_path)
class RepoWriter(BasicMirrorWriter):
"""Download boot resources from an upstream Simplestreams repo.
:ivar root_path: Snapshot directory.
:ivar store: A simplestreams `ObjectStore` where downloaded resources
should be stored.
:ivar product_mapping: A `ProductMapping` describing the desired boot
resources.
"""
def __init__(self, root_path, store, product_mapping):
self.root_path = root_path
self.store = store
self.product_mapping = product_mapping
super(RepoWriter, self).__init__(config={
# Only download the latest version. Without this all versions
# will be downloaded from simplestreams.
'max_items': 1,
})
def load_products(self, path=None, content_id=None):
"""Overridable from `BasicMirrorWriter`."""
# It looks as if this method only makes sense for MirrorReaders, not
# for MirrorWriters. The default MirrorWriter implementation just
# raises NotImplementedError. Stop it from doing that.
return
def filter_version(self, data, src, target, pedigree):
"""Overridable from `BasicMirrorWriter`."""
return self.product_mapping.contains(products_exdata(src, pedigree))
def insert_item(self, data, src, target, pedigree, contentsource):
"""Overridable from `BasicMirrorWriter`."""
item = products_exdata(src, pedigree)
checksums = item_checksums(data)
tag = checksums['sha256']
size = data['size']
ftype = item['ftype']
filename = os.path.basename(item['path'])
if ftype == 'archive.tar.xz':
links = extract_archive_tar(
self.store, filename, tag, checksums, size, contentsource)
else:
links = insert_file(
self.store, filename, tag, checksums, size, contentsource)
osystem = get_os_from_product(item)
# link_resources creates a hardlink for every subarch. Every Ubuntu
# product in a SimpleStream contains a list of subarches which list
# what subarches are a subset of that subarch. For example Xenial
# ga-16.04 has the subarches list hwe-{p,q,r,s,t,u,v,w},ga-16.04.
# Kernel flavors are the same arch, the only difference is the kernel
# config. So ga-16.04-lowlatency has the same subarch list as ga-16.04.
# If we create hard links for all subarches a kernel flavor may
# overwrite the generic kernel hard link. This happens if a kernel
# flavor is processed after the generic kernel. Since MAAS doesn't use
# the other hard links only create hard links for the subarch of the
# product we have and a rolling link if it's a rolling kernel.
if 'subarch' in item:
# MAAS uses the 'generic' subarch when it doesn't know which
# subarch to use. This happens during enlistment and commissioning.
# Allow the 'generic' kflavor to own the 'generic' hardlink. The
# generic kernel should always be the ga kernel.
if (item['subarch'].startswith('ga-') and
item.get('kflavor') == 'generic'):
subarches = {item['subarch'], 'generic'}
else:
subarches = {item['subarch']}
else:
subarches = {'generic'}
if item.get('rolling', False):
subarch_parts = item['subarch'].split('-')
subarch_parts[1] = 'rolling'
subarches.add('-'.join(subarch_parts))
link_resources(
snapshot_path=self.root_path, links=links,
osystem=osystem, arch=item['arch'], release=item['release'],
label=item['label'], subarches=subarches,
bootloader_type=item.get('bootloader-type'))
def download_boot_resources(path, store, snapshot_path, product_mapping,
keyring_file=None):
"""Download boot resources for one simplestreams source.
:param path: The Simplestreams URL for this source.
:param store: A simplestreams `ObjectStore` where downloaded resources
should be stored.
:param snapshot_path: Filesystem path to a snapshot of current upstream
boot resources.
:param product_mapping: A `ProductMapping` describing the resources to be
downloaded.
:param keyring_file: Optional path to a keyring file for verifying
signatures.
"""
maaslog.info("Downloading boot resources from %s", path)
writer = RepoWriter(snapshot_path, store, product_mapping)
(mirror, rpath) = path_from_mirror_url(path, None)
policy = get_signing_policy(rpath, keyring_file)
reader = UrlMirrorReader(mirror, policy=policy)
writer.sync(reader, rpath)
def compose_snapshot_path(storage_path):
"""Put together a path for a new snapshot.
A snapshot is a directory in `storage_path` containing boot resources.
The snapshot's name contains the date in a sortable format.
:param storage_path: Root storage directory,
usually `/var/lib/maas/boot-resources`.
:return: Path to the snapshot directory.
"""
now = datetime.utcnow()
snapshot_name = 'snapshot-%s' % now.strftime('%Y%m%d-%H%M%S')
return os.path.join(storage_path, snapshot_name)
def download_all_boot_resources(
sources, storage_path, product_mapping, store=None):
"""Download the actual boot resources.
Local copies of boot resources are downloaded into a "cache" directory.
This is a raw, flat store of resources, with UUID-based filenames called
"tags."
In addition, the downlads are hardlinked into a "snapshot directory." This
directory, named after the date and time that the snapshot was initiated,
reflects the currently available boot resources in a proper directory
hierarchy with subdirectories for architectures, releases, and so on.
:param sources: List of dicts describing the Simplestreams sources from
which we should download.
:param storage_path: Root storage directory,
usually `/var/lib/maas/boot-resources`.
:param snapshot_path:
:param product_mapping: A `ProductMapping` describing the resources to be
downloaded.
:param store: A `FileStore` instance. Used only for testing.
:return: Path to the snapshot directory.
"""
storage_path = os.path.abspath(storage_path)
snapshot_path = compose_snapshot_path(storage_path)
# Use a FileStore as our ObjectStore implementation. It will write to the
# cache directory.
if store is None:
cache_path = os.path.join(storage_path, 'cache')
store = FileStore(cache_path)
# XXX jtv 2014-04-11: FileStore now also takes an argument called
# complete_callback, which can be used for progress reporting.
for source in sources:
download_boot_resources(
source['url'], store, snapshot_path, product_mapping,
keyring_file=source.get('keyring')),
return snapshot_path
|