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

« back to all changes in this revision

Viewing changes to utility.py

  • Committer: Aaron Bentley
  • Date: 2016-04-24 16:09:49 UTC
  • mto: This revision was merged to the branch mainline in revision 1372.
  • Revision ID: aaron.bentley@canonical.com-20160424160949-6x1jdnkkpkcd820m
Rename create_model to add_model.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import argparse
1
2
from contextlib import contextmanager
2
3
from datetime import (
3
4
    datetime,
18
19
    )
19
20
from tempfile import mkdtemp
20
21
import warnings
 
22
import xml.etree.ElementTree as ET
 
23
import yaml
21
24
# Export shell quoting function which has moved in newer python versions
22
25
try:
23
26
    from shlex import quote
33
36
 
34
37
 
35
38
@contextmanager
36
 
def noop_context():
37
 
    """A context manager that does nothing."""
38
 
    yield
39
 
 
40
 
 
41
 
@contextmanager
42
39
def scoped_environ(new_environ=None):
43
40
    old_environ = dict(os.environ)
44
41
    try:
92
89
        return remaining
93
90
 
94
91
 
95
 
class JujuAssertionError(AssertionError):
96
 
    """Exception for juju assertion failures."""
97
 
 
98
 
 
99
 
class JujuResourceTimeout(Exception):
100
 
    """A timeout exception for a resource not being downloaded into a unit."""
 
92
class ErrJujuPath(Exception):
 
93
    """An exception for an invalid juju binary path."""
 
94
 
 
95
 
 
96
class enforce_juju_path(argparse.Action):
 
97
    """Enforces that a path ending with juju is given."""
 
98
    def __call__(self, parser, namespace, values, option_string=None):
 
99
        if not values.endswith(('/juju', '\\juju.exe')):
 
100
            raise ErrJujuPath(
 
101
                "%s: The full path to the juju binary is required." % values)
 
102
        setattr(namespace, self.dest, values)
101
103
 
102
104
 
103
105
def _clean_dir(maybe_dir):
109
111
    try:
110
112
        contents = os.listdir(maybe_dir)
111
113
    except OSError as e:
112
 
        if e.errno == errno.ENOENT:
113
 
            # we don't raise this error due to tests abusing /tmp/logs
114
 
            warnings.warn('Not a directory {}'.format(maybe_dir))
115
 
        if e.errno == errno.EEXIST:
116
 
            warnings.warn('Directory {} already exists'.format(maybe_dir))
 
114
        if e.errno != errno.ENOENT:
 
115
            raise
 
116
        # GZ 2016-03-01: We may want to raise or just create the dir here, but
 
117
        # that confuses expectations of all existing parse_args tests.
117
118
    else:
118
119
        if contents and contents != ["empty"]:
119
 
            warnings.warn(
120
 
                'Directory {!r} has existing contents.'.format(maybe_dir))
 
120
            warnings.warn("Directory %r has existing contents." % (maybe_dir,))
121
121
    return maybe_dir
122
122
 
123
123
 
124
124
def pause(seconds):
125
 
    print_now('Sleeping for {:d} seconds.'.format(seconds))
 
125
    print_now('Sleeping for %d seconds.' % seconds)
126
126
    sleep(seconds)
127
127
 
128
128
 
201
201
        except socket.gaierror as e:
202
202
            print_now(str(e))
203
203
        except Exception as e:
204
 
            print_now('Unexpected {!r}: {}'.format((type(e), e)))
 
204
            print_now('Unexpected %r: %s' % (type(e), e))
205
205
            raise
206
206
        else:
207
207
            conn.close()
254
254
    return result
255
255
 
256
256
 
 
257
def get_auth_token(root, job):
 
258
    tree = ET.parse(os.path.join(root, 'jobs', job, 'config.xml'))
 
259
    return tree.getroot().find('authToken').text
 
260
 
 
261
 
257
262
def check_free_disk_space(path, required, purpose):
258
263
    df_result = subprocess.check_output(["df", "-k", path])
259
264
    df_result = df_result.split('\n')[1]
292
297
        return subprocess.check_output(command)
293
298
 
294
299
 
295
 
def _get_test_name_from_filename():
296
 
    try:
297
 
        calling_file = sys._getframe(2).f_back.f_globals['__file__']
298
 
        return os.path.splitext(os.path.basename(calling_file))[0]
299
 
    except:
300
 
        return 'unknown_test'
301
 
 
302
 
 
303
 
def _generate_default_temp_env_name():
304
 
    """Creates a new unique name for environment and returns the name"""
305
 
    # we need to sanitize the name
306
 
    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
307
 
    test_name = re.sub('[^a-zA-Z]', '', _get_test_name_from_filename())
308
 
    return '{}-{}-temp-env'.format(test_name, timestamp)
309
 
 
310
 
 
311
 
def _generate_default_binary():
312
 
    """Returns GOPATH juju binary if it exists, otherwise /usr/bin/juju"""
313
 
    if os.getenv('GOPATH'):
314
 
        go_bin = os.getenv('GOPATH') + '/bin/juju'
315
 
        if os.path.isfile(go_bin):
316
 
            return go_bin
317
 
 
318
 
    return '/usr/bin/juju'
319
 
 
320
 
 
321
 
def _to_deadline(timeout):
322
 
    return datetime.utcnow() + timedelta(seconds=int(timeout))
323
 
 
324
 
 
325
 
def add_basic_testing_arguments(parser, using_jes=False, deadline=True):
 
300
def add_basic_testing_arguments(parser, using_jes=False):
326
301
    """Returns the parser loaded with basic testing arguments.
327
302
 
328
303
    The basic testing arguments, used in conjuction with boot_context ensures
329
304
    a test can be run in any supported substrate in parallel.
330
305
 
331
 
    This helper adds 4 positional arguments that defines the minimum needed
332
 
    to run a test script.
333
 
 
334
 
    These arguments (env, juju_bin, logs, temp_env_name) allow you to specify
335
 
    specifics for which env, juju binary, which folder for logging and an
336
 
    environment name for your test respectively.
 
306
    This helper adds 4 positional arguments that define the minimum needed
 
307
    to run a test script: env, juju_bin, logs, and temp_env_name.
337
308
 
338
309
    There are many optional args that either update the env's config or
339
310
    manipulate the juju command line options to test in controlled situations
343
314
 
344
315
    :param parser: an ArgumentParser.
345
316
    :param using_jes: whether args should be tailored for JES testing.
346
 
    :param deadline: If true, support the --timeout option and convert to a
347
 
        deadline.
348
317
    """
349
 
 
350
 
    # Optional postional arguments
351
 
    parser.add_argument(
352
 
        'env', nargs='?',
353
 
        help='The juju environment to base the temp test environment on.',
354
 
        default='lxd')
355
 
    parser.add_argument('juju_bin', nargs='?',
356
 
                        help='Full path to the Juju binary. By default, this'
357
 
                        ' will use $GOPATH/bin/juju or /usr/bin/juju in that'
358
 
                        ' order.',
359
 
                        default=_generate_default_binary())
360
 
    parser.add_argument('logs', nargs='?', type=_clean_dir,
361
 
                        help='A directory in which to store logs. By default,'
362
 
                        ' this will use the current directory',
363
 
                        default=None)
364
 
    parser.add_argument('temp_env_name', nargs='?',
365
 
                        help='A temporary test environment name. By default, '
366
 
                        ' this will generate an enviroment name using the '
367
 
                        ' timestamp and testname. '
368
 
                        ' test_name_timestamp_temp_env',
369
 
                        default=_generate_default_temp_env_name())
370
 
 
 
318
    # Required positional arguments.
 
319
    parser.add_argument(
 
320
        'env',
 
321
        help='The juju environment to base the temp test environment on.')
 
322
    parser.add_argument(
 
323
        'juju_bin', action=enforce_juju_path,
 
324
        help='Full path to the Juju binary.')
 
325
    parser.add_argument(
 
326
        'logs', type=_clean_dir, help='A directory in which to store logs.')
 
327
    parser.add_argument(
 
328
        'temp_env_name', help='A temporary test environment name.')
371
329
    # Optional keyword arguments.
372
330
    parser.add_argument('--debug', action='store_true',
373
331
                        help='Pass --debug to Juju.')
392
350
    parser.add_argument('--keep-env', action='store_true',
393
351
                        help='Keep the Juju environment after the test'
394
352
                        ' completes.')
395
 
    if deadline:
396
 
        parser.add_argument('--timeout', dest='deadline', type=_to_deadline,
397
 
                            help="The script timeout, in seconds.")
398
353
    return parser
399
354
 
400
355
 
489
444
            print_now(output)
490
445
 
491
446
 
492
 
def wait_for_removed_services(client, charm):
493
 
    """Timeout until the remove process ends"""
494
 
    for ignored in until_timeout(60):
495
 
        status = client.get_status()
496
 
        if charm not in status.get_applications():
497
 
            break
498
 
 
499
 
 
500
 
def unqualified_model_name(model_name):
501
 
    """Return the model name with the owner qualifier stripped if present."""
502
 
    return model_name.split('/', 1)[-1]
503
 
 
504
 
 
505
 
def qualified_model_name(model_name, owner_name):
506
 
    """Return the model name qualified with the given owner name."""
507
 
    if model_name == '' or owner_name == '':
508
 
        raise ValueError(
509
 
            'Neither model_name nor owner_name can be blank strings')
510
 
 
511
 
    parts = model_name.split('/', 1)
512
 
    if len(parts) == 2 and parts[0] != owner_name:
513
 
        raise ValueError(
514
 
            'qualified model name {} with owner not matching {}'.format(
515
 
                model_name, owner_name))
516
 
    return '{}/{}'.format(owner_name, parts[-1])
 
447
def local_charm_path(charm, juju_ver, series=None, repository=None,
 
448
                     platform='ubuntu'):
 
449
    """Create either Juju 1.x or 2.x local charm path."""
 
450
    if juju_ver.startswith('1.'):
 
451
        if series:
 
452
            series = '{}/'.format(series)
 
453
        else:
 
454
            series = ''
 
455
        local_path = 'local:{}{}'.format(series, charm)
 
456
        return local_path
 
457
    else:
 
458
        charm_dir = {
 
459
            'ubuntu': 'charms',
 
460
            'win': 'charms-win',
 
461
            'centos': 'charms-centos'}
 
462
        abs_path = charm
 
463
        if repository:
 
464
            abs_path = os.path.join(repository, charm)
 
465
        elif os.environ.get('JUJU_REPOSITORY'):
 
466
            repository = os.path.join(
 
467
                os.environ['JUJU_REPOSITORY'], charm_dir[platform])
 
468
            abs_path = os.path.join(repository, charm)
 
469
        return abs_path
 
470
 
 
471
 
 
472
def make_charm(charm_dir, min_ver='1.25.0', name='dummy',
 
473
               description='description', summary='summary', series='trusty'):
 
474
    metadata = os.path.join(charm_dir, 'metadata.yaml')
 
475
    content = {}
 
476
    content['name'] = name
 
477
    if min_ver is not None:
 
478
        content['min-juju-version'] = min_ver
 
479
    content['summary'] = summary
 
480
    content['description'] = description
 
481
    if series is not None:
 
482
        content['series'] = [series] if isinstance(series, str) else series
 
483
    with open(metadata, 'w') as f:
 
484
        yaml.safe_dump(content, f, default_flow_style=False)