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

« back to all changes in this revision

Viewing changes to jujuci.py

  • Committer: Aaron Bentley
  • Date: 2014-02-28 16:40:22 UTC
  • mto: This revision was merged to the branch mainline in revision 257.
  • Revision ID: aaron.bentley@canonical.com-20140228164022-kfip2tphn9m9invi
Add juju-backup script.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
"""Access Juju CI artifacts and data."""
3
 
 
4
 
from __future__ import print_function
5
 
 
6
 
from argparse import ArgumentParser
7
 
import base64
8
 
from collections import namedtuple
9
 
import fnmatch
10
 
import json
11
 
import os
12
 
import shutil
13
 
import sys
14
 
import traceback
15
 
import urllib
16
 
import urllib2
17
 
 
18
 
from deploy_stack import destroy_environment
19
 
from jujuconfig import NoSuchEnvironment
20
 
from jujupy import (
21
 
    SimpleEnvironment,
22
 
    EnvJujuClient,
23
 
)
24
 
try:
25
 
    from lsb_release import get_distro_information
26
 
except ImportError:
27
 
    def get_distro_information():
28
 
        raise NotImplementedError('Not supported on this platform!')
29
 
 
30
 
from utility import (
31
 
    extract_deb,
32
 
    get_deb_arch,
33
 
    get_revision_build,
34
 
    print_now,
35
 
    )
36
 
 
37
 
 
38
 
__metaclass__ = type
39
 
 
40
 
 
41
 
JENKINS_URL = 'http://juju-ci.vapour.ws:8080'
42
 
BUILD_REVISION = 'build-revision'
43
 
PUBLISH_REVISION = 'publish-revision'
44
 
CERTIFY_UBUNTU_PACKAGES = 'certify-ubuntu-packages'
45
 
 
46
 
Artifact = namedtuple('Artifact', ['file_name', 'location'])
47
 
 
48
 
 
49
 
Credentials = namedtuple('Credentials', ['user', 'password'])
50
 
 
51
 
 
52
 
class CredentialsMissing(Exception):
53
 
    """Raised when no credentials are supplied."""
54
 
 
55
 
 
56
 
def get_jenkins_json(credentials, url):
57
 
    req = urllib2.Request(url)
58
 
    encoded = base64.encodestring(
59
 
        '{}:{}'.format(*credentials)).replace('\n', '')
60
 
    req.add_header('Authorization', 'Basic {}'.format(encoded))
61
 
    build_data = urllib2.urlopen(req)
62
 
    return json.load(build_data)
63
 
 
64
 
 
65
 
def get_build_data(jenkins_url, credentials, job_name,
66
 
                   build='lastSuccessfulBuild'):
67
 
    """Return a dict of the build data for a job build number."""
68
 
    url = '%s/job/%s/%s/api/json' % (jenkins_url, job_name, build)
69
 
    return get_jenkins_json(credentials, url)
70
 
 
71
 
 
72
 
def get_job_data(jenkins_url, credentials, job_name):
73
 
    """Return a dict of the job data for a job name."""
74
 
    url = '%s/job/%s/api/json' % (jenkins_url, job_name)
75
 
    return get_jenkins_json(credentials, url)
76
 
 
77
 
 
78
 
def make_artifact(build_data, artifact):
79
 
    location = '%sartifact/%s' % (build_data['url'], artifact['relativePath'])
80
 
    return Artifact(artifact['fileName'], location)
81
 
 
82
 
 
83
 
def find_artifacts(build_data, glob='*'):
84
 
    found = []
85
 
    for artifact in build_data['artifacts']:
86
 
        file_name = artifact['fileName']
87
 
        if fnmatch.fnmatch(file_name, glob):
88
 
            found.append(make_artifact(build_data, artifact))
89
 
    return found
90
 
 
91
 
 
92
 
def list_artifacts(credentials, job_name, build, glob, verbose=False):
93
 
    build_data = get_build_data(JENKINS_URL, credentials, job_name, build)
94
 
    artifacts = find_artifacts(build_data, glob)
95
 
    for artifact in artifacts:
96
 
        if verbose:
97
 
            print_now(artifact.location)
98
 
        else:
99
 
            print_now(artifact.file_name)
100
 
 
101
 
 
102
 
def retrieve_artifact(credentials, url, local_path):
103
 
    auth_location = url.replace('http://',
104
 
                                'http://{}:{}@'.format(*credentials))
105
 
    urllib.urlretrieve(auth_location, local_path)
106
 
 
107
 
 
108
 
def get_juju_bin_artifact(package_namer, version, build_data):
109
 
    file_name = package_namer.get_release_package(version)
110
 
    return get_filename_artifact(file_name, build_data)
111
 
 
112
 
 
113
 
def get_filename_artifact(file_name, build_data):
114
 
    by_filename = dict((a['fileName'], a) for a in build_data['artifacts'])
115
 
    bin_artifact = by_filename[file_name]
116
 
    return make_artifact(build_data, bin_artifact)
117
 
 
118
 
 
119
 
def retrieve_buildvars(credentials, build_number):
120
 
    build_data = get_build_data(JENKINS_URL, credentials, BUILD_REVISION,
121
 
                                build_number)
122
 
    artifact = get_filename_artifact('buildvars.json', build_data)
123
 
    return get_jenkins_json(credentials, artifact.location)
124
 
 
125
 
 
126
 
def get_buildvars(credentials, build_number, summary=False, env='unknown',
127
 
                  revision_build=False, version=False, branch=False,
128
 
                  short_branch=False, revision=False, short_revision=False):
129
 
    """Return requested information as text.
130
 
 
131
 
    The 'summary' kwarg returns intelligible information about the build.
132
 
    'env' is included in the summary text. Otherwise, a space-separated
133
 
    string composed of the each True kwarg is returned in this order:
134
 
        revision_build version short_branch short_revision branch revision
135
 
    Note that revision_build is always the same as build number;
136
 
    build_number can be an alias like 'lastBuild' or 'lastSuccessfulBuild'.
137
 
    """
138
 
    buildvars = retrieve_buildvars(credentials, build_number)
139
 
    buildvars['short_revision_id'] = buildvars['revision_id'][0:7]
140
 
    buildvars['short_branch'] = buildvars['branch'].split(':')[1]
141
 
    buildvars['env'] = env
142
 
    template = (
143
 
        'Testing {branch} {short_revision_id} on {env} for {revision_build}')
144
 
    if summary:
145
 
        text = template.format(**buildvars)
146
 
    else:
147
 
        data = []
148
 
        if revision_build:
149
 
            data.append(buildvars['revision_build'])
150
 
        if version:
151
 
            data.append(buildvars['version'])
152
 
        if short_branch:
153
 
            data.append(buildvars['short_branch'])
154
 
        if short_revision:
155
 
            data.append(buildvars['short_revision_id'])
156
 
        if branch:
157
 
            data.append(buildvars['branch'])
158
 
        if revision:
159
 
            data.append(buildvars['revision_id'])
160
 
        text = ' '.join(data)
161
 
    return text
162
 
 
163
 
 
164
 
def get_release_package_filename(credentials, build_data):
165
 
    revision_build = get_revision_build(build_data)
166
 
    version = retrieve_buildvars(credentials, revision_build)['version']
167
 
    return PackageNamer.factory().get_release_package(version)
168
 
 
169
 
 
170
 
def get_juju_bin(credentials, workspace):
171
 
    binary_job = JobNamer.factory().get_build_binary_job()
172
 
    build_data = get_build_data(JENKINS_URL, credentials, binary_job,
173
 
                                'lastBuild')
174
 
    file_name = get_release_package_filename(credentials, build_data)
175
 
    return get_juju_binary(credentials, file_name, build_data, workspace)
176
 
 
177
 
 
178
 
def get_certification_bin(credentials, version, workspace):
179
 
    build_data = get_build_data(JENKINS_URL, credentials,
180
 
                                CERTIFY_UBUNTU_PACKAGES, 'lastBuild')
181
 
    file_name = PackageNamer.factory().get_certification_package(version)
182
 
    return get_juju_binary(credentials, file_name, build_data, workspace)
183
 
 
184
 
 
185
 
def get_juju_binary(credentials, file_name, build_data, workspace):
186
 
    artifact = get_filename_artifact(file_name, build_data)
187
 
    target_path = os.path.join(workspace, artifact.file_name)
188
 
    retrieve_artifact(credentials, artifact.location, target_path)
189
 
    return acquire_binary(target_path, workspace)
190
 
 
191
 
 
192
 
def acquire_binary(package_path, workspace):
193
 
    bin_dir = os.path.join(workspace, 'extracted-bin')
194
 
    extract_deb(package_path, bin_dir)
195
 
    for root, dirs, files in os.walk(bin_dir):
196
 
        if 'juju' in files:
197
 
            return os.path.join(root, 'juju')
198
 
 
199
 
 
200
 
def get_artifacts(credentials, job_name, build, glob, path,
201
 
                  archive=False, dry_run=False, verbose=False):
202
 
    full_path = os.path.expanduser(path)
203
 
    if archive:
204
 
        if verbose:
205
 
            print_now('Cleaning %s' % full_path)
206
 
        if not os.path.isdir(full_path):
207
 
            raise ValueError('%s does not exist' % full_path)
208
 
        shutil.rmtree(full_path)
209
 
        os.makedirs(full_path)
210
 
    build_data = get_build_data(JENKINS_URL, credentials, job_name, build)
211
 
    artifacts = find_artifacts(build_data, glob)
212
 
    for artifact in artifacts:
213
 
        local_path = os.path.abspath(
214
 
            os.path.join(full_path, artifact.file_name))
215
 
        if verbose:
216
 
            print_now('Retrieving %s => %s' % (artifact.location, local_path))
217
 
        else:
218
 
            print_now(artifact.file_name)
219
 
        if not dry_run:
220
 
            retrieve_artifact(credentials, artifact.location, local_path)
221
 
    return artifacts
222
 
 
223
 
 
224
 
def clean_environment(env_name, verbose=False):
225
 
    try:
226
 
        env = SimpleEnvironment.from_config(env_name)
227
 
    except NoSuchEnvironment as e:
228
 
        # Nothing to do.
229
 
        if verbose:
230
 
            print_now(str(e))
231
 
        return False
232
 
    client = EnvJujuClient.by_version(env)
233
 
    if verbose:
234
 
        print_now("Destroying %s" % env_name)
235
 
    destroy_environment(client, env_name)
236
 
    return True
237
 
 
238
 
 
239
 
def setup_workspace(workspace_dir, env=None, dry_run=False, verbose=False):
240
 
    """Clean the workspace directory and create an artifacts sub directory."""
241
 
    for root, dirs, files in os.walk(workspace_dir):
242
 
        for name in files:
243
 
            print_now('Removing %s' % name)
244
 
            if not dry_run:
245
 
                os.remove(os.path.join(root, name))
246
 
        for name in dirs:
247
 
            print_now('Removing %s' % name)
248
 
            if not dry_run:
249
 
                shutil.rmtree(os.path.join(root, name))
250
 
    artifacts_path = os.path.join(workspace_dir, 'artifacts')
251
 
    print_now('Creating artifacts dir.')
252
 
    if not dry_run:
253
 
        os.mkdir(artifacts_path)
254
 
    # "touch empty" to convince jenkins there is an archive.
255
 
    empty_path = os.path.join(artifacts_path, 'empty')
256
 
    if not dry_run:
257
 
        with open(empty_path, 'w'):
258
 
            pass
259
 
    if env is not None and not dry_run:
260
 
        clean_environment(env, verbose=verbose)
261
 
 
262
 
 
263
 
def add_artifacts(workspace_dir, globs, dry_run=False, verbose=False):
264
 
    """Find files beneath the workspace_dir and move them to the artifacts.
265
 
 
266
 
    The list of globs can match the full file name, part of a name, or
267
 
    a sub directory: eg: buildvars.json, *.deb, tmp/*.deb.
268
 
    """
269
 
    workspace_dir = os.path.realpath(workspace_dir)
270
 
    artifacts_dir = os.path.join(workspace_dir, 'artifacts')
271
 
    for root, dirs, files in os.walk(workspace_dir):
272
 
        # create a pseudo-relative path to make glob matches easy.
273
 
        relative = os.path.relpath(root, workspace_dir)
274
 
        if relative == '.':
275
 
            relative = ''
276
 
        if 'artifacts' in dirs:
277
 
            dirs.remove('artifacts')
278
 
        for file_name in files:
279
 
            file_path = os.path.join(root, file_name)
280
 
            file_relative_path = os.path.join(relative, file_name)
281
 
            for glob in globs:
282
 
                if fnmatch.fnmatch(file_relative_path, glob):
283
 
                    if verbose:
284
 
                        print_now("Adding artifact %s" % file_relative_path)
285
 
                    if not dry_run:
286
 
                        shutil.move(file_path, artifacts_dir)
287
 
                    break
288
 
 
289
 
 
290
 
def add_build_job_glob(parser):
291
 
    """Added the --build, job, and glob arguments to the parser."""
292
 
    parser.add_argument(
293
 
        '-b', '--build', default='lastSuccessfulBuild',
294
 
        help="The specific build to examine (default: lastSuccessfulBuild).")
295
 
    parser.add_argument(
296
 
        'job', help="The job that collected the artifacts.")
297
 
    parser.add_argument(
298
 
        'glob', nargs='?', default='*',
299
 
        help="The glob pattern to match artifact file names.")
300
 
 
301
 
 
302
 
def add_credential_args(parser):
303
 
    parser.add_argument(
304
 
        '--user', default=os.environ.get('JENKINS_USER'))
305
 
    parser.add_argument(
306
 
        '--password', default=os.environ.get('JENKINS_PASSWORD'))
307
 
 
308
 
 
309
 
def parse_args(args=None):
310
 
    """Return the argument parser for this program."""
311
 
    parser = ArgumentParser("List and get artifacts from Juju CI.")
312
 
    parser.add_argument(
313
 
        '-d', '--dry-run', action='store_true', default=False,
314
 
        help='Do not make changes.')
315
 
    parser.add_argument(
316
 
        '-v', '--verbose', action='store_true', default=False,
317
 
        help='Increase verbosity.')
318
 
    subparsers = parser.add_subparsers(help='sub-command help', dest="command")
319
 
    parser_list = subparsers.add_parser(
320
 
        'list', help='list artifacts for a job build')
321
 
    add_build_job_glob(parser_list)
322
 
    parser_get = subparsers.add_parser(
323
 
        'get', help='get artifacts for a job build')
324
 
    add_build_job_glob(parser_get)
325
 
    parser_get.add_argument(
326
 
        '-a', '--archive', action='store_true', default=False,
327
 
        help='Ensure the download path exists and remove older files.')
328
 
    parser_get.add_argument(
329
 
        'path', nargs='?', default='.',
330
 
        help="The path to download the files to.")
331
 
    add_credential_args(parser_list)
332
 
    add_credential_args(parser_get)
333
 
    parser_workspace = subparsers.add_parser(
334
 
        'setup-workspace', help='Setup and clean a workspace for building.')
335
 
    parser_workspace.add_argument(
336
 
        '-e', '--clean-env', dest='clean_env', default=None,
337
 
        help='Ensure the env resources are freed or deleted.')
338
 
    parser_workspace.add_argument(
339
 
        'path', help="The path to the existing workspace directory.")
340
 
    parser_get_juju_bin = subparsers.add_parser(
341
 
        'get-juju-bin', help='Retrieve and extract juju binaries.')
342
 
    parser_get_juju_bin.add_argument('workspace', nargs='?', default='.',
343
 
                                     help='The place to store binaries.')
344
 
    add_credential_args(parser_get_juju_bin)
345
 
    parser_get_certification_bin = subparsers.add_parser(
346
 
        'get-certification-bin',
347
 
        help='Retrieve and extract juju binaries for certification.')
348
 
    parser_get_certification_bin.add_argument(
349
 
        'version', help='The version to get certification for.')
350
 
    parser_get_certification_bin.add_argument(
351
 
        'workspace', nargs='?', default='.',
352
 
        help='The place to store binaries.')
353
 
    add_credential_args(parser_get_certification_bin)
354
 
    parser_get_buildvars = subparsers.add_parser(
355
 
        'get-build-vars',
356
 
        help='Retrieve the build-vars for a build-revision.')
357
 
    parser_get_buildvars.add_argument(
358
 
        '--env', default='Unknown',
359
 
        help='The env name to include in the summary')
360
 
    parser_get_buildvars.add_argument(
361
 
        '--summary', action='store_true', default=False,
362
 
        help='Summarise the build var test data')
363
 
    parser_get_buildvars.add_argument(
364
 
        '--revision-build', action='store_true', default=False,
365
 
        help='Print the test revision build number')
366
 
    parser_get_buildvars.add_argument(
367
 
        '--version', action='store_true', default=False,
368
 
        help='Print the test juju version')
369
 
    parser_get_buildvars.add_argument(
370
 
        '--short-branch', action='store_true', default=False,
371
 
        help='Print the short name of the branch')
372
 
    parser_get_buildvars.add_argument(
373
 
        '--short-revision', action='store_true', default=False,
374
 
        help='Print the short revision of the branch')
375
 
    parser_get_buildvars.add_argument(
376
 
        '--branch', action='store_true', default=False,
377
 
        help='Print the test branch')
378
 
    parser_get_buildvars.add_argument(
379
 
        '--revision', action='store_true', default=False,
380
 
        help='Print the test revision')
381
 
    parser_get_buildvars.add_argument(
382
 
        'build', help='The build-revision build number')
383
 
    add_credential_args(parser_get_buildvars)
384
 
    parser_get_package_name = subparsers.add_parser(
385
 
        'get-package-name',
386
 
        help='Determine the package name for the current machine.')
387
 
    parser_get_package_name.add_argument('version', help='The version to use.')
388
 
    parsed_args = parser.parse_args(args)
389
 
    if parsed_args.command == 'get-build-vars' and True not in (
390
 
            parsed_args.summary, parsed_args.revision_build,
391
 
            parsed_args.version, parsed_args.revision,
392
 
            parsed_args.short_branch, parsed_args.short_revision,
393
 
            parsed_args.branch, parsed_args.revision):
394
 
        parser_get_buildvars.error(
395
 
            'Expected --summary or one or more of: --revision-build, '
396
 
            '--version, --revision, --short-branch, --short-revision, '
397
 
            '--branch, --revision')
398
 
    credentials = get_credentials(parsed_args)
399
 
    return parsed_args, credentials
400
 
 
401
 
 
402
 
def get_credentials(args):
403
 
    if 'user' not in args:
404
 
        return None
405
 
    if None in (args.user, args.password):
406
 
        raise CredentialsMissing(
407
 
            'Jenkins username and/or password not supplied.')
408
 
        return None
409
 
    return Credentials(args.user, args.password)
410
 
 
411
 
 
412
 
class Namer:
413
 
    """A base class that has distro and arch info used to name things."""
414
 
 
415
 
    @classmethod
416
 
    def factory(cls):
417
 
        dist_info = get_distro_information()
418
 
        return cls(get_deb_arch(), dist_info['RELEASE'], dist_info['CODENAME'])
419
 
 
420
 
    def __init__(self, arch, distro_release, distro_series):
421
 
        self.arch = arch
422
 
        self.distro_release = distro_release
423
 
        self.distro_series = distro_series
424
 
 
425
 
 
426
 
class PackageNamer(Namer):
427
 
    """A class knows the names of packages."""
428
 
 
429
 
    def get_release_package_suffix(self):
430
 
        return '-0ubuntu1~{distro_release}.1~juju1_{arch}.deb'.format(
431
 
            distro_release=self.distro_release, arch=self.arch)
432
 
 
433
 
    def get_release_package(self, version):
434
 
        return (
435
 
            'juju-core_{version}{suffix}'
436
 
            ).format(version=version, suffix=self.get_release_package_suffix())
437
 
 
438
 
    def get_certification_package(self, version):
439
 
        return (
440
 
            'juju-core_{version}~{distro_release}.1_{arch}.deb'
441
 
            ).format(version=version, distro_release=self.distro_release,
442
 
                     arch=self.arch)
443
 
 
444
 
 
445
 
class JobNamer(Namer):
446
 
    """A class knows the names of jobs."""
447
 
 
448
 
    def get_build_binary_job(self):
449
 
        return 'build-binary-{distro_series}-{arch}'.format(
450
 
            distro_series=self.distro_series, arch=self.arch)
451
 
 
452
 
 
453
 
def main(argv):
454
 
    """Manage list and get files from Juju CI builds."""
455
 
    try:
456
 
        args, credentials = parse_args(argv)
457
 
    except CredentialsMissing as e:
458
 
        print_now(e)
459
 
        sys.exit(2)
460
 
    try:
461
 
        if args.command == 'list':
462
 
            list_artifacts(
463
 
                credentials, args.job, args.build, args.glob,
464
 
                verbose=args.verbose)
465
 
        elif args.command == 'get':
466
 
            get_artifacts(
467
 
                credentials, args.job, args.build, args.glob, args.path,
468
 
                archive=args.archive, dry_run=args.dry_run,
469
 
                verbose=args.verbose)
470
 
        elif args.command == 'setup-workspace':
471
 
            setup_workspace(
472
 
                args.path, env=args.clean_env,
473
 
                dry_run=args.dry_run, verbose=args.verbose)
474
 
        elif args.command == 'get-juju-bin':
475
 
            print_now(get_juju_bin(credentials, args.workspace))
476
 
        elif args.command == 'get-certification-bin':
477
 
            path = get_certification_bin(credentials, args.version,
478
 
                                         args.workspace)
479
 
            print_now(path)
480
 
        elif args.command == 'get-build-vars':
481
 
            text = get_buildvars(
482
 
                credentials, args.build, env=args.env,
483
 
                summary=args.summary, revision_build=args.revision_build,
484
 
                version=args.version, short_branch=args.short_branch,
485
 
                short_revision=args.short_revision,
486
 
                branch=args.branch, revision=args.revision)
487
 
            print_now(text)
488
 
        elif args.command == 'get-package-name':
489
 
            print_now(PackageNamer.factory().get_release_package(args.version))
490
 
 
491
 
    except Exception as e:
492
 
        print_now(e)
493
 
        if args.verbose:
494
 
            traceback.print_tb(sys.exc_info()[2])
495
 
        return 2
496
 
    if args.verbose:
497
 
        print_now("Done.")
498
 
    return 0
499
 
 
500
 
 
501
 
if __name__ == '__main__':
502
 
    sys.exit(main(sys.argv[1:]))