1
# Copyright 2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Management command: upgrade the cluster.
6
This module implements the `ActionScript` interface for pserv commands.
8
Use the upgrade-cluster command when the MAAS code has been updated (e.g. while
9
installing a package ugprade, from the packaging) to perform any data
10
migrations that the new version may require.
12
This maintains a list of upgrade hooks, each representing a data migration
13
that was needed at some point in development of the MAAS cluster codebase.
14
All these hooks get run, in chronological order. There is no record of
15
updates that have already been performed; each hook figures out for itself
16
whether its migration is needed.
18
Backwards migrations are not supported.
21
from __future__ import (
35
from logging import getLogger
38
from provisioningserver.boot.tftppath import drill_down
39
from provisioningserver.config import (
43
from provisioningserver.utils import (
51
logger = getLogger(__name__)
54
def find_old_imports(tftproot):
55
"""List pre-Simplestreams boot images.
57
Supports the `generate_boot_resources_config` upgrade hook. Returns a set
58
of tuples (arch, subarch, release) describing all of the images found.
60
if not os.path.isdir(tftproot):
63
for level in ['arch', 'subarch', 'release', 'purpose']:
64
paths = drill_down(tftproot, paths)
66
(arch, subarch, release)
67
for [root, arch, subarch, release, purpose] in paths
71
def generate_selections(images):
72
"""Generate `selections` stanzas to match pre-existing boot images.
74
Supports the `generate_boot_resources_config` upgrade hook.
76
:param images: An iterable of (arch, subarch, release) tuples as returned
77
by `find_old_imports`.
78
:return: A list of dicts, each describing one `selections` stanza for the
79
`bootresources.yaml` file.
82
# No old images found.
85
# Return one "selections" stanza per image. This could be cleverer
86
# and combine multiple architectures/subarchitectures, but there would
87
# have to be a clear gain. Simple is good.
92
'subarches': [subarch],
94
for arch, subarch, release in sorted(images)
98
def generate_updated_config(config, old_images):
99
"""Return an updated version of a config dict.
101
Supports the `generate_boot_resources_config` upgrade hook.
103
This clears the `configure_me` flag, and replaces all sources'
104
`selections` stanzas with ones based on the old boot images.
106
:param config: A config dict, as loaded from `bootresources.yaml`.
107
:param old_images: Old-style boot images, as returned by
108
`find_old_imports`. If `None`, the existing `selections` are left
110
:return: An updated version of `config` with the above changes.
112
config = config.copy()
113
# Remove the configure_me item. It's there exactly to tell us that we
114
# haven't done this rewrite yet.
115
del config['boot']['configure_me']
116
if old_images is None:
119
# If we found old images, rewrite the selections.
120
if len(old_images) != 0:
121
new_selections = generate_selections(old_images)
122
for source in config['boot']['sources']:
123
source['selections'] = new_selections
127
def extract_top_comment(input_file):
128
"""Return just the comment at the top of `input_file`.
130
Supports the `generate_boot_resources_config` upgrade hook.
133
for line in read_text_file(input_file).splitlines():
134
stripped_line = line.lstrip()
135
if stripped_line != '' and not stripped_line.startswith('#'):
136
# Not an empty line or comment any more. Stop.
139
return '\n'.join(lines) + '\n'
142
def update_config_file(config_file, new_config):
143
"""Replace configuration data in `config_file` with `new_config`.
145
Supports the `generate_boot_resources_config` upgrade hook.
147
The first part of the config file, up to the first text that isn't a
148
comment, is kept intact. The part after that is overwritten with YAML
149
for the new configuration.
151
header = extract_top_comment(config_file)
152
data = yaml.safe_dump(new_config, default_flow_style=False)
153
content = (header + data).encode('utf-8')
154
atomic_write(content, config_file, mode=0644)
155
BootConfig.flush_cache(config_file)
158
def rewrite_boot_resources_config(config_file):
159
"""Rewrite the `bootresources.yaml` configuration.
161
Supports the `generate_boot_resources_config` upgrade hook.
163
# Look for images using the old tftp root setting, not the tftp
164
# resource_root setting. The latter points to where the newer,
165
# Simplestreams-based boot images live.
166
# This should be the final use of the old tftp root setting. After this
167
# has run, it serves no more purpose.
168
tftproot = Config.load_from_cache()['tftp']['root']
169
config = BootConfig.load_from_cache(config_file)
170
old_images = find_old_imports(tftproot)
171
new_config = generate_updated_config(config, old_images)
172
update_config_file(config_file, new_config)
175
def generate_boot_resources_config():
176
"""Upgrade hook: rewrite `bootresources.yaml` based on boot images.
178
This finds boot images downloaded into the old, pre-Simplestreams tftp
179
root, and writes a boot-resources configuration to import a similar set of
180
images using Simplestreams.
182
config_file = locate_config('bootresources.yaml')
183
boot_resources = BootConfig.load_from_cache(config_file)
184
if boot_resources['boot'].get('configure_me', False):
185
rewrite_boot_resources_config(config_file)
188
# Upgrade hooks, from oldest to newest. The hooks are callables, taking no
189
# arguments. They are called in order.
191
# Each hook figures out for itself whether its changes are needed. There is
192
# no record of previous upgrades.
194
generate_boot_resources_config,
198
def add_arguments(parser):
199
"""Add this command's options to the `ArgumentParser`.
201
Specified by the `ActionScript` interface.
203
# This command accepts no arguments.
206
# The docstring for the "run" function is also the command's documentation.
208
"""Perform any data migrations needed for upgrading this cluster."""
209
for hook in UPGRADE_HOOKS: