~lutostag/ubuntu/trusty/maas/1.5.2+packagefix

« back to all changes in this revision

Viewing changes to src/provisioningserver/upgrade_cluster.py

  • Committer: Package Import Robot
  • Author(s): Andres Rodriguez
  • Date: 2014-03-28 10:43:53 UTC
  • mto: This revision was merged to the branch mainline in revision 57.
  • Revision ID: package-import@ubuntu.com-20140328104353-ekpolg0pm5xnvq2s
Tags: upstream-1.5+bzr2204
ImportĀ upstreamĀ versionĀ 1.5+bzr2204

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
"""Management command: upgrade the cluster.
 
5
 
 
6
This module implements the `ActionScript` interface for pserv commands.
 
7
 
 
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.
 
11
 
 
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.
 
17
 
 
18
Backwards migrations are not supported.
 
19
"""
 
20
 
 
21
from __future__ import (
 
22
    absolute_import,
 
23
    print_function,
 
24
    unicode_literals,
 
25
    )
 
26
 
 
27
str = None
 
28
 
 
29
__metaclass__ = type
 
30
__all__ = [
 
31
    'add_arguments',
 
32
    'run',
 
33
    ]
 
34
 
 
35
from logging import getLogger
 
36
import os.path
 
37
 
 
38
from provisioningserver.boot.tftppath import drill_down
 
39
from provisioningserver.config import (
 
40
    BootConfig,
 
41
    Config,
 
42
    )
 
43
from provisioningserver.utils import (
 
44
    atomic_write,
 
45
    locate_config,
 
46
    read_text_file,
 
47
    )
 
48
import yaml
 
49
 
 
50
 
 
51
logger = getLogger(__name__)
 
52
 
 
53
 
 
54
def find_old_imports(tftproot):
 
55
    """List pre-Simplestreams boot images.
 
56
 
 
57
    Supports the `generate_boot_resources_config` upgrade hook.  Returns a set
 
58
    of tuples (arch, subarch, release) describing all of the images found.
 
59
    """
 
60
    if not os.path.isdir(tftproot):
 
61
        return set()
 
62
    paths = [[tftproot]]
 
63
    for level in ['arch', 'subarch', 'release', 'purpose']:
 
64
        paths = drill_down(tftproot, paths)
 
65
    return {
 
66
        (arch, subarch, release)
 
67
        for [root, arch, subarch, release, purpose] in paths
 
68
        }
 
69
 
 
70
 
 
71
def generate_selections(images):
 
72
    """Generate `selections` stanzas to match pre-existing boot images.
 
73
 
 
74
    Supports the `generate_boot_resources_config` upgrade hook.
 
75
 
 
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.
 
80
    """
 
81
    if len(images) == 0:
 
82
        # No old images found.
 
83
        return None
 
84
    else:
 
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.
 
88
        return [
 
89
            {
 
90
                'release': release,
 
91
                'arches': [arch],
 
92
                'subarches': [subarch],
 
93
            }
 
94
            for arch, subarch, release in sorted(images)
 
95
            ]
 
96
 
 
97
 
 
98
def generate_updated_config(config, old_images):
 
99
    """Return an updated version of a config dict.
 
100
 
 
101
    Supports the `generate_boot_resources_config` upgrade hook.
 
102
 
 
103
    This clears the `configure_me` flag, and replaces all sources'
 
104
    `selections` stanzas with ones based on the old boot images.
 
105
 
 
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
 
109
        unchanged.
 
110
    :return: An updated version of `config` with the above changes.
 
111
    """
 
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:
 
117
        return config
 
118
 
 
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
 
124
    return config
 
125
 
 
126
 
 
127
def extract_top_comment(input_file):
 
128
    """Return just the comment at the top of `input_file`.
 
129
 
 
130
    Supports the `generate_boot_resources_config` upgrade hook.
 
131
    """
 
132
    lines = []
 
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.
 
137
            break
 
138
        lines.append(line)
 
139
    return '\n'.join(lines) + '\n'
 
140
 
 
141
 
 
142
def update_config_file(config_file, new_config):
 
143
    """Replace configuration data in `config_file` with `new_config`.
 
144
 
 
145
    Supports the `generate_boot_resources_config` upgrade hook.
 
146
 
 
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.
 
150
    """
 
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)
 
156
 
 
157
 
 
158
def rewrite_boot_resources_config(config_file):
 
159
    """Rewrite the `bootresources.yaml` configuration.
 
160
 
 
161
    Supports the `generate_boot_resources_config` upgrade hook.
 
162
    """
 
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)
 
173
 
 
174
 
 
175
def generate_boot_resources_config():
 
176
    """Upgrade hook: rewrite `bootresources.yaml` based on boot images.
 
177
 
 
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.
 
181
    """
 
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)
 
186
 
 
187
 
 
188
# Upgrade hooks, from oldest to newest.  The hooks are callables, taking no
 
189
# arguments.  They are called in order.
 
190
#
 
191
# Each hook figures out for itself whether its changes are needed.  There is
 
192
# no record of previous upgrades.
 
193
UPGRADE_HOOKS = [
 
194
    generate_boot_resources_config,
 
195
    ]
 
196
 
 
197
 
 
198
def add_arguments(parser):
 
199
    """Add this command's options to the `ArgumentParser`.
 
200
 
 
201
    Specified by the `ActionScript` interface.
 
202
    """
 
203
    # This command accepts no arguments.
 
204
 
 
205
 
 
206
# The docstring for the "run" function is also the command's documentation.
 
207
def run(args):
 
208
    """Perform any data migrations needed for upgrading this cluster."""
 
209
    for hook in UPGRADE_HOOKS:
 
210
        hook()