~andrewjbeach/juju-ci-tools/make-local-patcher

« back to all changes in this revision

Viewing changes to update_lxc_cache.py

  • Committer: Aaron Bentley
  • Date: 2015-01-19 15:32:33 UTC
  • mto: This revision was merged to the branch mainline in revision 804.
  • Revision ID: aaron.bentley@canonical.com-20150119153233-jjcvikwiw1dx2lak
Print error on missing environment.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
"""Update the lxc 'download' template cache for hosts on closed networks."""
3
 
 
4
 
from __future__ import print_function
5
 
 
6
 
from argparse import ArgumentParser
7
 
from collections import namedtuple
8
 
import errno
9
 
import os
10
 
import sys
11
 
import traceback
12
 
import shutil
13
 
import subprocess
14
 
import urllib2
15
 
 
16
 
 
17
 
SITE = 'https://images.linuxcontainers.org'
18
 
INDEX_PATH = 'meta/1.0'
19
 
INDEX = 'index-system'
20
 
ROOTFS = 'rootfs.tar.xz'
21
 
META = 'meta.tar.xz'
22
 
LXC_CACHE = '/var/cache/lxc/download'
23
 
 
24
 
 
25
 
System = namedtuple(
26
 
    'System', ['dist', 'release', 'arch', 'variant', 'version', 'path'])
27
 
 
28
 
 
29
 
PUT_SCRIPT = """\
30
 
scp {rootfs_path} {meta_path} {user_host}:~/
31
 
"""
32
 
 
33
 
INSTALL_SCRIPT = """\
34
 
ssh {user_host} bash <<"EOT"
35
 
sudo mkdir -p {lxc_cache}
36
 
sudo mv ~/{rootfs} ~/{meta} {lxc_cache}
37
 
sudo chown -R root:root  {lxc_cache}
38
 
sudo tar -C {lxc_cache} -xf {lxc_cache}/meta.tar.xz
39
 
EOT
40
 
"""
41
 
 
42
 
 
43
 
class LxcCache:
44
 
    """Manage the LXC download template cache."""
45
 
 
46
 
    def __init__(self, workspace, verbose=False, dry_run=False):
47
 
        """Set the workspace for the local cache."""
48
 
        self.workspace = os.path.abspath(workspace)
49
 
        self.verbose = verbose
50
 
        self.dry_run = dry_run
51
 
        local_path = os.path.join(self.workspace, INDEX_PATH, INDEX)
52
 
        self.systems, ignore = self.init_systems(local_path)
53
 
 
54
 
    def init_systems(self, location):
55
 
        """Return a tuple of the dict of lxc Systems and the source data.
56
 
 
57
 
        A System has these attributes: 'dist', 'release', 'arch', 'variant',
58
 
        'version', and 'path'.  The dict keys are a tuple of
59
 
        (dist, release, arch, variant).
60
 
        """
61
 
        systems = {}
62
 
        if location.startswith('http'):
63
 
            request = urllib2.Request(location)
64
 
            response = urllib2.urlopen(request)
65
 
            data = response.read()
66
 
        else:
67
 
            try:
68
 
                with open(location) as f:
69
 
                    data = f.read()
70
 
            except IOError as e:
71
 
                if e.errno == errno.ENOENT:
72
 
                    if self.verbose:
73
 
                        print('Local cache is empty.')
74
 
                    return systems, None
75
 
        for line in data.splitlines():
76
 
            system = System(*line.split(';'))
77
 
            key = (system.dist, system.release, system.arch, system.variant)
78
 
            systems[key] = system
79
 
        return systems, data
80
 
 
81
 
    def get_updates(self, dist, release, arch, variant):
82
 
        """Return a tuple of the new system and the source data that match.
83
 
 
84
 
        The new system and source data will be None when there are
85
 
        no updates. The dist, release, arch, and variant args identify the
86
 
        system to return.
87
 
        """
88
 
        key = (dist, release, arch, variant)
89
 
        old_system = self.systems.get(key)
90
 
        url = '%s/%s/%s' % (SITE, INDEX_PATH, INDEX)
91
 
        new_systems, data = self.init_systems(url)
92
 
        new_system = new_systems[key]
93
 
        if not old_system or new_system.version > old_system.version:
94
 
            if self.verbose:
95
 
                print('Found new version for %s' % str(key))
96
 
                print(new_system.version)
97
 
            return new_system, data
98
 
        if self.verbose:
99
 
            print('Version is current for %s' % str(key))
100
 
            print(old_system.version)
101
 
        return None, None
102
 
 
103
 
    def get_lxc_data(self, system):
104
 
        """Download the system image and meta data.
105
 
 
106
 
        Return a tuple of the image and meta data paths.
107
 
        """
108
 
        image_path = os.path.join(self.workspace, system.path[1:])
109
 
        if not self.dry_run:
110
 
            if self.verbose:
111
 
                print('creating %s' % image_path)
112
 
            if not os.path.isdir(image_path):
113
 
                os.makedirs(image_path)
114
 
        rootfs_path = os.path.join(image_path, ROOTFS)
115
 
        rootfs_url = '%s%s%s' % (SITE, system.path, ROOTFS)
116
 
        self.download(rootfs_url, rootfs_path)
117
 
        meta_path = os.path.join(image_path, META)
118
 
        meta_url = '%s%s%s' % (SITE, system.path, META)
119
 
        self.download(meta_url, meta_path)
120
 
        return rootfs_path, meta_path
121
 
 
122
 
    def download(self, location, path):
123
 
        """Download a large binary from location to the specified path."""
124
 
        chunk = 16 * 1024
125
 
        if not self.dry_run:
126
 
            request = urllib2.Request(location)
127
 
            response = urllib2.urlopen(request)
128
 
            if response.getcode() == 200:
129
 
                with open(path, 'wb') as f:
130
 
                    shutil.copyfileobj(response, f, chunk)
131
 
                if self.verbose:
132
 
                    print('Downloaded %s' % location)
133
 
 
134
 
    def put_lxc_data(self, user_host, system, rootfs_path, meta_path):
135
 
        """Install the lxc image and meta data on the host.
136
 
 
137
 
        The user on the host must have password-less sudo.
138
 
        """
139
 
        lxc_cache = os.path.join(
140
 
            LXC_CACHE, system.dist, system.release, system.arch,
141
 
            system.variant)
142
 
        put_script = PUT_SCRIPT.format(
143
 
            user_host=user_host, rootfs_path=rootfs_path, meta_path=meta_path)
144
 
        if not self.dry_run:
145
 
            subprocess.check_call([put_script], shell=True)
146
 
            if self.verbose:
147
 
                print("Uploaded %s and %s" % (ROOTFS, META))
148
 
        install_script = INSTALL_SCRIPT.format(
149
 
            user_host=user_host, lxc_cache=lxc_cache, rootfs=ROOTFS, meta=META)
150
 
        if not self.dry_run:
151
 
            subprocess.check_call([install_script], shell=True)
152
 
            if self.verbose:
153
 
                print("Installed %s and %s" % (ROOTFS, META))
154
 
 
155
 
    def save_index(self, data):
156
 
        "Save the (current) index data for future calls to get_updates()."
157
 
        index_dir = os.path.join(self.workspace, INDEX_PATH)
158
 
        if not os.path.isdir(index_dir):
159
 
            os.makedirs(index_dir)
160
 
        index_path = os.path.join(self.workspace, INDEX_PATH, INDEX)
161
 
        with open(index_path, 'w') as f:
162
 
            f.write(data)
163
 
        if self.verbose:
164
 
            print('saved index: %s' % INDEX)
165
 
 
166
 
 
167
 
def parse_args(argv=None):
168
 
    """Return the argument parser for this program."""
169
 
    parser = ArgumentParser(
170
 
        "Update a remote host's download lxc template cache.")
171
 
    parser.add_argument(
172
 
        '-d', '--dry-run', action='store_true', default=False,
173
 
        help='Do not make changes.')
174
 
    parser.add_argument(
175
 
        '-v', '--verbose', action='store_true', default=False,
176
 
        help='Increase verbosity.')
177
 
    parser.add_argument(
178
 
        '--dist', default="ubuntu", help="The distribution to update.")
179
 
    parser.add_argument(
180
 
        '--variant', default="default", help="The variant to update.")
181
 
    parser.add_argument(
182
 
        'user_host', help='The user@host to update.')
183
 
    parser.add_argument(
184
 
        'release', help='The release to update.')
185
 
    parser.add_argument(
186
 
        'arch', help='The architecture of the remote host')
187
 
    parser.add_argument(
188
 
        'workspace', help='The path to the local dir to stage the update.')
189
 
    args = parser.parse_args(argv)
190
 
    return args
191
 
 
192
 
 
193
 
def main(argv):
194
 
    """Update the lxc download template cache for hosts on closed networks."""
195
 
    args = parse_args(argv)
196
 
    try:
197
 
        lxc_cache = LxcCache(
198
 
            args.workspace, verbose=args.verbose, dry_run=args.dry_run)
199
 
        new_system, data = lxc_cache.get_updates(
200
 
            args.dist, args.release, args.arch, args.variant)
201
 
        if new_system:
202
 
            rootfs_path, meta_path = lxc_cache.get_lxc_data(new_system)
203
 
            lxc_cache.put_lxc_data(
204
 
                args.user_host, new_system, rootfs_path, meta_path)
205
 
            lxc_cache.save_index(data)
206
 
    except Exception as e:
207
 
        print(e)
208
 
        print(getattr(e, 'output', ''))
209
 
        if args.verbose:
210
 
            traceback.print_tb(sys.exc_info()[2])
211
 
        return 2
212
 
    if args.verbose:
213
 
        print("Done.")
214
 
    return 0
215
 
 
216
 
 
217
 
if __name__ == '__main__':
218
 
    sys.exit(main(sys.argv[1:]))