~veebers/juju-ci-tools/longrun_check1.25

« back to all changes in this revision

Viewing changes to jujupy.py

  • Committer: Christopher Lee
  • Date: 2016-10-25 02:49:11 UTC
  • mfrom: (1485.2.201 trunk)
  • Revision ID: chris.lee@canonical.com-20161025024911-9tfmpby5dj6h42in
Merge trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
89
89
            'Operation exceeded deadline.')
90
90
 
91
91
 
 
92
class NoProvider(Exception):
 
93
    """Raised when an environment defines no provider."""
 
94
 
 
95
 
92
96
def get_timeout_path():
93
97
    import timeout
94
98
    return os.path.abspath(timeout.__file__)
103
107
 
104
108
def get_teardown_timeout(client):
105
109
    """Return the timeout need byt the client to teardown resources."""
106
 
    if client.env.config['type'] == 'azure':
 
110
    if client.env.provider == 'azure':
107
111
        return 1800
108
112
    else:
109
113
        return 600
244
248
        self.config = config
245
249
        self.juju_home = juju_home
246
250
        if self.config is not None:
247
 
            self.local = bool(self.config.get('type') == 'local')
 
251
            try:
 
252
                provider = self.provider
 
253
            except NoProvider:
 
254
                provider = None
 
255
            self.local = bool(provider == 'local')
248
256
            self.kvm = (
249
257
                self.local and bool(self.config.get('container') == 'kvm'))
250
 
            self.maas = bool(self.config.get('type') == 'maas')
251
 
            self.joyent = bool(self.config.get('type') == 'joyent')
 
258
            self.maas = bool(provider == 'maas')
 
259
            self.joyent = bool(provider == 'joyent')
252
260
        else:
253
261
            self.local = False
254
262
            self.kvm = False
255
263
            self.maas = False
256
264
            self.joyent = False
257
265
 
 
266
    @property
 
267
    def provider(self):
 
268
        """Return the provider type for this environment.
 
269
 
 
270
        See get_cloud to determine the specific cloud.
 
271
        """
 
272
        try:
 
273
            return self.config['type']
 
274
        except KeyError:
 
275
            raise NoProvider('No provider specified.')
 
276
 
 
277
    def get_region(self):
 
278
        provider = self.provider
 
279
        if provider == 'azure':
 
280
            if 'tenant-id' not in self.config:
 
281
                return self.config['location'].replace(' ', '').lower()
 
282
            return self.config['location']
 
283
        elif provider == 'joyent':
 
284
            matcher = re.compile('https://(.*).api.joyentcloud.com')
 
285
            return matcher.match(self.config['sdc-url']).group(1)
 
286
        elif provider == 'lxd':
 
287
            return 'localhost'
 
288
        elif provider == 'manual':
 
289
            return self.config['bootstrap-host']
 
290
        elif provider in ('maas', 'manual'):
 
291
            return None
 
292
        else:
 
293
            return self.config['region']
 
294
 
 
295
    def set_region(self, region):
 
296
        try:
 
297
            provider = self.provider
 
298
        except NoProvider:
 
299
            provider = None
 
300
        if provider == 'azure':
 
301
            self.config['location'] = region
 
302
        elif provider == 'joyent':
 
303
            self.config['sdc-url'] = (
 
304
                'https://{}.api.joyentcloud.com'.format(region))
 
305
        elif provider == 'lxd':
 
306
            if region != 'localhost':
 
307
                raise ValueError('Only "localhost" allowed for lxd.')
 
308
        elif provider == 'manual':
 
309
            self.config['bootstrap-host'] = region
 
310
        elif provider == 'maas':
 
311
            if region is not None:
 
312
                raise ValueError('Only None allowed for maas.')
 
313
        else:
 
314
            self.config['region'] = region
 
315
 
258
316
    def clone(self, model_name=None):
259
317
        config = deepcopy(self.config)
260
318
        if model_name is None:
410
468
        raise LookupError('No such endpoint: {}'.format(endpoint))
411
469
 
412
470
    def get_cloud(self):
413
 
        provider = self.config['type']
 
471
        provider = self.provider
414
472
        # Separate cloud recommended by: Juju Cloud / Credentials / BootStrap /
415
473
        # Model CLI specification
416
474
        if provider == 'ec2' and self.config['region'] == 'cn-north-1':
426
484
            endpoint = self.config['auth-url']
427
485
        return self.find_endpoint_cloud(provider, endpoint)
428
486
 
429
 
    def get_region(self):
430
 
        provider = self.config['type']
431
 
        if provider == 'azure':
432
 
            if 'tenant-id' not in self.config:
433
 
                return self.config['location'].replace(' ', '').lower()
434
 
            return self.config['location']
435
 
        elif provider == 'joyent':
436
 
            matcher = re.compile('https://(.*).api.joyentcloud.com')
437
 
            return matcher.match(self.config['sdc-url']).group(1)
438
 
        elif provider == 'lxd':
439
 
            return 'localhost'
440
 
        elif provider == 'manual':
441
 
            return self.config['bootstrap-host']
442
 
        elif provider in ('maas', 'manual'):
443
 
            return None
444
 
        else:
445
 
            return self.config['region']
446
 
 
447
487
    def get_cloud_credentials(self):
448
488
        """Return the credentials for this model's cloud."""
449
489
        cloud_name = self.get_cloud()
806
846
        pause(seconds)
807
847
 
808
848
 
809
 
class Juju2A2Backend(Juju2Backend):
810
 
    """Backend for 2A2.
811
 
 
812
 
    Uses -m to specify models, uses JUJU_HOME and JUJU_DATA to specify home
813
 
    directory.
814
 
    """
815
 
 
816
 
    def shell_environ(self, used_feature_flags, juju_home):
817
 
        """Generate a suitable shell environment.
818
 
 
819
 
        For 2.0-alpha2 set both JUJU_HOME and JUJU_DATA.
820
 
        """
821
 
        env = super(Juju2A2Backend, self).shell_environ(used_feature_flags,
822
 
                                                        juju_home)
823
 
        env['JUJU_HOME'] = juju_home
824
 
        return env
825
 
 
826
 
 
827
 
class Juju1XBackend(Juju2A2Backend):
828
 
    """Backend for Juju 1.x - 2A1.
 
849
class Juju1XBackend(Juju2Backend):
 
850
    """Backend for Juju 1.x versions.
829
851
 
830
852
    Uses -e to specify models ("environments", uses JUJU_HOME to specify home
831
853
    directory.
880
902
    elif re.match('^1\.', version):
881
903
        client_class = EnvJujuClient1X
882
904
    # Ensure alpha/beta number matches precisely
883
 
    elif re.match('^2\.0-alpha1([^\d]|$)', version):
884
 
        client_class = EnvJujuClient2A1
885
 
    elif re.match('^2\.0-alpha2([^\d]|$)', version):
886
 
        client_class = EnvJujuClient2A2
887
 
    elif re.match('^2\.0-(alpha3|beta[12])([^\d]|$)', version):
888
 
        client_class = EnvJujuClient2B2
889
 
    elif re.match('^2\.0-(beta[3-6])([^\d]|$)', version):
890
 
        client_class = EnvJujuClient2B3
891
 
    elif re.match('^2\.0-(beta7)([^\d]|$)', version):
892
 
        client_class = EnvJujuClient2B7
893
 
    elif re.match('^2\.0-beta8([^\d]|$)', version):
894
 
        client_class = EnvJujuClient2B8
 
905
    elif re.match('^2\.0-(alpha|beta)', version):
 
906
        raise VersionNotTestedError(version)
895
907
    # between beta 9-14
896
 
    elif re.match('^2\.0-beta(9|1[0-4])([^\d]|$)', version):
897
 
        client_class = EnvJujuClient2B9
898
908
    elif re.match('^2\.0-rc[1-3]', version):
899
909
        client_class = EnvJujuClientRC
900
910
    else:
962
972
 
963
973
    controller_permissions = frozenset(['login', 'addmodel', 'superuser'])
964
974
 
 
975
    reserved_spaces = frozenset([
 
976
        'endpoint-bindings-data', 'endpoint-bindings-public'])
 
977
 
965
978
    @classmethod
966
979
    def preferred_container(cls):
967
980
        for container_type in [LXD_MACHINE, LXC_MACHINE]:
1054
1067
        feature_flags = self.feature_flags.intersection(cls.used_feature_flags)
1055
1068
        backend = self._backend.clone(full_path, version, debug, feature_flags)
1056
1069
        other = cls.from_backend(backend, env)
 
1070
        other.excluded_spaces = set(self.excluded_spaces)
1057
1071
        return other
1058
1072
 
1059
1073
    @classmethod
1130
1144
                    env.juju_home = get_juju_home()
1131
1145
            else:
1132
1146
                env.juju_home = juju_home
 
1147
        self.excluded_spaces = set(self.reserved_spaces)
1133
1148
 
1134
1149
    @property
1135
1150
    def version(self):
1163
1178
        return self._backend.shell_environ(self.used_feature_flags,
1164
1179
                                           self.env.juju_home)
1165
1180
 
 
1181
    def use_reserved_spaces(self, spaces):
 
1182
        """Allow machines in given spaces to be allocated and used."""
 
1183
        if not self.reserved_spaces.issuperset(spaces):
 
1184
            raise ValueError('Space not reserved: {}'.format(spaces))
 
1185
        self.excluded_spaces.difference_update(spaces)
 
1186
 
1166
1187
    def add_ssh_machines(self, machines):
1167
1188
        for count, machine in enumerate(machines):
1168
1189
            try:
1183
1204
    def get_bootstrap_args(
1184
1205
            self, upload_tools, config_filename, bootstrap_series=None,
1185
1206
            credential=None, auto_upgrade=False, metadata_source=None,
1186
 
            to=None, agent_version=None):
 
1207
            to=None, no_gui=False, agent_version=None):
1187
1208
        """Return the bootstrap arguments for the substrate."""
1188
 
        if self.env.joyent:
1189
 
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
1190
 
            constraints = 'mem=2G cpu-cores=1'
1191
 
        else:
1192
 
            constraints = 'mem=2G'
 
1209
        constraints = self._get_substrate_constraints()
1193
1210
        cloud_region = self.get_cloud_region(self.env.get_cloud(),
1194
1211
                                             self.env.get_region())
1195
1212
        # Note cloud_region before controller name
1217
1234
            args.append('--auto-upgrade')
1218
1235
        if to is not None:
1219
1236
            args.extend(['--to', to])
 
1237
        if no_gui:
 
1238
            args.append('--no-gui')
1220
1239
        return tuple(args)
1221
1240
 
1222
1241
    def add_model(self, env):
1282
1301
 
1283
1302
    def bootstrap(self, upload_tools=False, bootstrap_series=None,
1284
1303
                  credential=None, auto_upgrade=False, metadata_source=None,
1285
 
                  to=None, agent_version=None):
 
1304
                  to=None, no_gui=False, agent_version=None):
1286
1305
        """Bootstrap a controller."""
1287
1306
        self._check_bootstrap()
1288
1307
        with self._bootstrap_config() as config_filename:
1289
1308
            args = self.get_bootstrap_args(
1290
1309
                upload_tools, config_filename, bootstrap_series, credential,
1291
 
                auto_upgrade, metadata_source, to, agent_version)
 
1310
                auto_upgrade, metadata_source, to, no_gui, agent_version)
1292
1311
            self.update_user_name()
1293
1312
            self.juju('bootstrap', args, include_e=False)
1294
1313
 
1295
1314
    @contextmanager
1296
1315
    def bootstrap_async(self, upload_tools=False, bootstrap_series=None,
1297
 
                        auto_upgrade=False, metadata_source=None, to=None):
 
1316
                        auto_upgrade=False, metadata_source=None, to=None,
 
1317
                        no_gui=False):
1298
1318
        self._check_bootstrap()
1299
1319
        with self._bootstrap_config() as config_filename:
1300
1320
            args = self.get_bootstrap_args(
1301
1321
                upload_tools, config_filename, bootstrap_series, None,
1302
 
                auto_upgrade, metadata_source, to)
 
1322
                auto_upgrade, metadata_source, to, no_gui)
1303
1323
            self.update_user_name()
1304
1324
            with self.juju_async('bootstrap', args, include_e=False):
1305
1325
                yield
1538
1558
        args = e_arg + args
1539
1559
        self.juju('deployer', args, self.env.needs_sudo(), include_e=False)
1540
1560
 
 
1561
    @staticmethod
 
1562
    def _maas_spaces_enabled():
 
1563
        return not os.environ.get("JUJU_CI_SPACELESSNESS")
 
1564
 
1541
1565
    def _get_substrate_constraints(self):
1542
1566
        if self.env.joyent:
1543
1567
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
1544
1568
            return 'mem=2G cpu-cores=1'
 
1569
        elif self.env.maas and self._maas_spaces_enabled():
 
1570
            # For now only maas support spaces in a meaningful way.
 
1571
            return 'mem=2G spaces={}'.format(','.join(
 
1572
                '^' + space for space in sorted(self.excluded_spaces)))
1545
1573
        else:
1546
1574
            return 'mem=2G'
1547
1575
 
2221
2249
    def get_bootstrap_args(
2222
2250
            self, upload_tools, config_filename, bootstrap_series=None,
2223
2251
            credential=None, auto_upgrade=False, metadata_source=None,
2224
 
            to=None, agent_version=None):
 
2252
            to=None, no_gui=False, agent_version=None):
2225
2253
        """Return the bootstrap arguments for the substrate."""
2226
2254
        if self.env.joyent:
2227
2255
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
2255
2283
            args.append('--auto-upgrade')
2256
2284
        if to is not None:
2257
2285
            args.extend(['--to', to])
 
2286
        if no_gui:
 
2287
            args.append('--no-gui')
2258
2288
        return tuple(args)
2259
2289
 
2260
2290
 
2261
 
class EnvJujuClient2B9(EnvJujuClientRC):
2262
 
 
2263
 
    def update_user_name(self):
2264
 
        return
2265
 
 
2266
 
    def create_cloned_environment(
2267
 
            self, cloned_juju_home, controller_name, user_name=None):
2268
 
        """Create a cloned environment.
2269
 
 
2270
 
        `user_name` is unused in this version of beta.
2271
 
        """
2272
 
        user_client = self.clone(env=self.env.clone())
2273
 
        user_client.env.juju_home = cloned_juju_home
2274
 
        # New user names the controller.
2275
 
        user_client.env.controller = Controller(controller_name)
2276
 
        return user_client
2277
 
 
2278
 
    def get_model_uuid(self):
2279
 
        name = self.env.environment
2280
 
        output_yaml = self.get_juju_output('show-model', '--format', 'yaml')
2281
 
        output = yaml.safe_load(output_yaml)
2282
 
        return output[name]['model-uuid']
2283
 
 
2284
 
    def add_user_perms(self, username, models=None, permissions='read'):
2285
 
        """Adds provided user and return register command arguments.
2286
 
 
2287
 
        :return: Registration token provided by the add-user command.
2288
 
 
2289
 
        """
2290
 
        if models is None:
2291
 
            models = self.env.environment
2292
 
 
2293
 
        args = (username, '--models', models, '--acl', permissions,
2294
 
                '-c', self.env.controller.name)
2295
 
 
2296
 
        output = self.get_juju_output('add-user', *args, include_e=False)
2297
 
        return self._get_register_command(output)
2298
 
 
2299
 
    def grant(self, user_name, permission, model=None):
2300
 
        """Grant the user with a model."""
2301
 
        if model is None:
2302
 
            model = self.model_name
2303
 
        self.juju('grant', (user_name, model, '--acl', permission),
2304
 
                  include_e=False)
2305
 
 
2306
 
    def revoke(self, username, models=None, permissions='read'):
2307
 
        if models is None:
2308
 
            models = self.env.environment
2309
 
 
2310
 
        args = (username, models, '--acl', permissions)
2311
 
 
2312
 
        self.controller_juju('revoke', args)
2313
 
 
2314
 
    def set_config(self, service, options):
2315
 
        option_strings = self._dict_as_option_strings(options)
2316
 
        self.juju('set-config', (service,) + option_strings)
2317
 
 
2318
 
    def get_config(self, service):
2319
 
        return yaml.safe_load(self.get_juju_output('get-config', service))
2320
 
 
2321
 
    def get_model_config(self):
2322
 
        """Return the value of the environment's configured option."""
2323
 
        return yaml.safe_load(
2324
 
            self.get_juju_output('get-model-config', '--format', 'yaml'))
2325
 
 
2326
 
    def get_env_option(self, option):
2327
 
        """Return the value of the environment's configured option."""
2328
 
        return self.get_juju_output('get-model-config', option)
2329
 
 
2330
 
    def set_env_option(self, option, value):
2331
 
        """Set the value of the option in the environment."""
2332
 
        option_value = "%s=%s" % (option, value)
2333
 
        return self.juju('set-model-config', (option_value,))
2334
 
 
2335
 
    def unset_env_option(self, option):
2336
 
        """Unset the value of the option in the environment."""
2337
 
        return self.juju('unset-model-config', (option,))
2338
 
 
2339
 
    def list_disabled_commands(self):
2340
 
        """List all the commands disabled on the model."""
2341
 
        raw = self.get_juju_output('block list', '--format', 'yaml')
2342
 
        return yaml.safe_load(raw)
2343
 
 
2344
 
    def disable_command(self, args):
2345
 
        """Disable a command set."""
2346
 
        return self.juju('block', args)
2347
 
 
2348
 
    def enable_command(self, args):
2349
 
        """Enable a command set."""
2350
 
        return self.juju('unblock', args)
2351
 
 
2352
 
    def enable_ha(self):
2353
 
        self.juju('enable-ha', ('-n', '3'))
2354
 
 
2355
 
 
2356
 
class EnvJujuClient2B8(EnvJujuClient2B9):
 
2291
class EnvJujuClient1X(EnvJujuClientRC):
 
2292
    """Base for all 1.x client drivers."""
 
2293
 
 
2294
    default_backend = Juju1XBackend
 
2295
 
 
2296
    config_class = SimpleEnvironment
2357
2297
 
2358
2298
    status_class = ServiceStatus
2359
2299
 
2360
 
    def remove_service(self, service):
2361
 
        self.juju('remove-service', (service,))
2362
 
 
2363
 
    def run(self, commands, applications):
2364
 
        responses = self.get_juju_output(
2365
 
            'run', '--format', 'json', '--service', ','.join(applications),
2366
 
            *commands)
2367
 
        return json.loads(responses)
2368
 
 
2369
 
    def deployer(self, bundle_template, name=None, deploy_delay=10,
2370
 
                 timeout=3600):
2371
 
        """Deploy a bundle using deployer."""
2372
 
        bundle = self.format_bundle(bundle_template)
2373
 
        args = (
2374
 
            '--debug',
2375
 
            '--deploy-delay', str(deploy_delay),
2376
 
            '--timeout', str(timeout),
2377
 
            '--config', bundle,
2378
 
        )
2379
 
        if name:
2380
 
            args += (name,)
2381
 
        e_arg = ('-e', 'local.{}:{}'.format(
2382
 
            self.env.controller.name, self.env.environment))
2383
 
        args = e_arg + args
2384
 
        self.juju('deployer', args, self.env.needs_sudo(), include_e=False)
2385
 
 
2386
 
 
2387
 
class EnvJujuClient2B7(EnvJujuClient2B8):
2388
 
 
2389
 
    def get_controller_model_name(self):
2390
 
        """Return the name of the 'controller' model.
2391
 
 
2392
 
        Return the name of the environment when an 'controller' model does
2393
 
        not exist.
2394
 
        """
2395
 
        return 'admin'
2396
 
 
2397
 
 
2398
 
class EnvJujuClient2B3(EnvJujuClient2B7):
2399
 
 
2400
 
    def _add_model(self, model_name, config_file):
2401
 
        self.controller_juju('create-model', (
2402
 
            model_name, '--config', config_file))
2403
 
 
2404
 
 
2405
 
class EnvJujuClient2B2(EnvJujuClient2B3):
2406
 
 
2407
 
    def get_bootstrap_args(
2408
 
            self, upload_tools, config_filename, bootstrap_series=None,
2409
 
            credential=None, auto_upgrade=False, metadata_source=None,
2410
 
            to=None, agent_version=None):
2411
 
        """Return the bootstrap arguments for the substrate."""
2412
 
        err_fmt = 'EnvJujuClient2B2 does not support bootstrap argument {}'
2413
 
        if auto_upgrade:
2414
 
            raise ValueError(err_fmt.format('auto_upgrade'))
2415
 
        if metadata_source is not None:
2416
 
            raise ValueError(err_fmt.format('metadata_source'))
2417
 
        if to is not None:
2418
 
            raise ValueError(err_fmt.format('to'))
2419
 
        if agent_version is not None:
2420
 
            raise ValueError(err_fmt.format('agent_version'))
2421
 
        if self.env.joyent:
2422
 
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
2423
 
            constraints = 'mem=2G cpu-cores=1'
2424
 
        else:
2425
 
            constraints = 'mem=2G'
2426
 
        cloud_region = self.get_cloud_region(self.env.get_cloud(),
2427
 
                                             self.env.get_region())
2428
 
        args = ['--constraints', constraints, self.env.environment,
2429
 
                cloud_region, '--config', config_filename]
2430
 
        if upload_tools:
2431
 
            args.insert(0, '--upload-tools')
2432
 
        else:
2433
 
            args.extend(['--agent-version', self.get_matching_agent_version()])
2434
 
 
2435
 
        if bootstrap_series is not None:
2436
 
            args.extend(['--bootstrap-series', bootstrap_series])
2437
 
 
2438
 
        if credential is not None:
2439
 
            args.extend(['--credential', credential])
2440
 
 
2441
 
        return tuple(args)
2442
 
 
2443
 
    def get_controller_client(self):
2444
 
        """Return a client for the controller model.  May return self."""
2445
 
        return self
2446
 
 
2447
 
    def get_controller_model_name(self):
2448
 
        """Return the name of the 'controller' model.
2449
 
 
2450
 
        Return the name of the environment when an 'controller' model does
2451
 
        not exist.
2452
 
        """
2453
 
        models = self.get_models()
2454
 
        # The dict can be empty because 1.x does not support the models.
2455
 
        # This is an ambiguous case for the jes feature flag which supports
2456
 
        # multiple models, but none is named 'admin' by default. Since the
2457
 
        # jes case also uses '-e' for models, the env is the controller model.
2458
 
        for model in models.get('models', []):
2459
 
            if 'admin' in model['name']:
2460
 
                return 'admin'
2461
 
        return self.env.environment
2462
 
 
2463
 
 
2464
 
class EnvJujuClient2A2(EnvJujuClient2B2):
2465
 
    """Drives Juju 2.0-alpha2 clients."""
2466
 
 
2467
 
    default_backend = Juju2A2Backend
2468
 
 
2469
 
    config_class = SimpleEnvironment
 
2300
    # The environments.yaml options that are replaced by bootstrap options.
 
2301
    # For Juju 1.x, no bootstrap options are used.
 
2302
    bootstrap_replaces = frozenset()
 
2303
 
 
2304
    destroy_model_command = 'destroy-environment'
 
2305
 
 
2306
    supported_container_types = frozenset([KVM_MACHINE, LXC_MACHINE])
 
2307
 
 
2308
    agent_metadata_url = 'tools-metadata-url'
 
2309
 
 
2310
    _show_status = 'status'
2470
2311
 
2471
2312
    @classmethod
2472
2313
    def _get_env(cls, env):
2475
2316
                'JujuData cannot be used with {}'.format(cls.__name__))
2476
2317
        return env
2477
2318
 
2478
 
    def bootstrap(self, upload_tools=False, bootstrap_series=None):
2479
 
        """Bootstrap a controller."""
2480
 
        self._check_bootstrap()
2481
 
        args = self.get_bootstrap_args(upload_tools, bootstrap_series)
2482
 
        self.juju('bootstrap', args, self.env.needs_sudo())
2483
 
 
2484
 
    @contextmanager
2485
 
    def bootstrap_async(self, upload_tools=False):
2486
 
        self._check_bootstrap()
2487
 
        args = self.get_bootstrap_args(upload_tools)
2488
 
        with self.juju_async('bootstrap', args):
2489
 
            yield
2490
 
            log.info('Waiting for bootstrap of {}.'.format(
2491
 
                self.env.environment))
2492
 
 
2493
 
    def get_bootstrap_args(self, upload_tools, bootstrap_series=None,
2494
 
                           credential=None):
2495
 
        """Return the bootstrap arguments for the substrate."""
2496
 
        if credential is not None:
2497
 
            raise ValueError(
2498
 
                '--credential is not supported by this juju version.')
2499
 
        constraints = self._get_substrate_constraints()
2500
 
        args = ('--constraints', constraints,
2501
 
                '--agent-version', self.get_matching_agent_version())
2502
 
        if upload_tools:
2503
 
            args = ('--upload-tools',) + args
2504
 
        if bootstrap_series is not None:
2505
 
            args = args + ('--bootstrap-series', bootstrap_series)
2506
 
        return args
2507
 
 
2508
 
    def deploy(self, charm, repository=None, to=None, series=None,
2509
 
               service=None, force=False, storage=None, constraints=None):
2510
 
        args = [charm]
2511
 
        if repository is not None:
2512
 
            args.extend(['--repository', repository])
2513
 
        if to is not None:
2514
 
            args.extend(['--to', to])
2515
 
        if service is not None:
2516
 
            args.extend([service])
2517
 
        if storage is not None:
2518
 
            args.extend(['--storage', storage])
2519
 
        if constraints is not None:
2520
 
            args.extend(['--constraints', constraints])
2521
 
        return self.juju('deploy', tuple(args))
2522
 
 
2523
 
 
2524
 
class EnvJujuClient2A1(EnvJujuClient2A2):
2525
 
    """Drives Juju 2.0-alpha1 clients."""
2526
 
 
2527
 
    _show_status = 'status'
2528
 
 
2529
 
    default_backend = Juju1XBackend
2530
 
 
2531
2319
    def get_cache_path(self):
2532
2320
        return get_cache_path(self.env.juju_home, models=False)
2533
2321
 
2586
2374
        """return a models dict with a 'models': [] key-value pair."""
2587
2375
        return {}
2588
2376
 
2589
 
    def _get_models(self):
2590
 
        """return a list of model dicts."""
2591
 
        # In 2.0-alpha1, 'list-models' produced a yaml list rather than a
2592
 
        # dict, but the command and parsing are the same.
2593
 
        return super(EnvJujuClient2A1, self).get_models()
2594
 
 
2595
2377
    def list_controllers(self):
2596
2378
        """List the controllers."""
2597
2379
        log.info(
2641
2423
                            output)
2642
2424
        return match.group(1)
2643
2425
 
 
2426
    def run(self, commands, applications):
 
2427
        responses = self.get_juju_output(
 
2428
            'run', '--format', 'json', '--service', ','.join(applications),
 
2429
            *commands)
 
2430
        return json.loads(responses)
 
2431
 
2644
2432
    def list_space(self):
2645
2433
        return yaml.safe_load(self.get_juju_output('space list'))
2646
2434
 
2650
2438
    def add_subnet(self, subnet, space):
2651
2439
        self.juju('subnet add', (subnet, space))
2652
2440
 
 
2441
    def add_user_perms(self, username, models=None, permissions='read'):
 
2442
        raise JESNotSupported()
 
2443
 
 
2444
    def grant(self, user_name, permission, model=None):
 
2445
        raise JESNotSupported()
 
2446
 
 
2447
    def revoke(self, username, models=None, permissions='read'):
 
2448
        raise JESNotSupported()
 
2449
 
2653
2450
    def set_model_constraints(self, constraints):
2654
2451
        constraint_strings = self._dict_as_option_strings(constraints)
2655
2452
        return self.juju('set-constraints', constraint_strings)
2674
2471
        option_value = "%s=%s" % (option, value)
2675
2472
        return self.juju('set-env', (option_value,))
2676
2473
 
2677
 
 
2678
 
class EnvJujuClient1X(EnvJujuClient2A1):
2679
 
    """Base for all 1.x client drivers."""
2680
 
 
2681
 
    # The environments.yaml options that are replaced by bootstrap options.
2682
 
    # For Juju 1.x, no bootstrap options are used.
2683
 
    bootstrap_replaces = frozenset()
2684
 
 
2685
 
    destroy_model_command = 'destroy-environment'
2686
 
 
2687
 
    supported_container_types = frozenset([KVM_MACHINE, LXC_MACHINE])
2688
 
 
2689
 
    agent_metadata_url = 'tools-metadata-url'
 
2474
    def unset_env_option(self, option):
 
2475
        """Unset the value of the option in the environment."""
 
2476
        return self.juju('set-env', ('{}='.format(option),))
2690
2477
 
2691
2478
    def _cmd_model(self, include_e, controller):
2692
2479
        if controller:
2696
2483
        else:
2697
2484
            return unqualified_model_name(self.model_name)
2698
2485
 
 
2486
    def update_user_name(self):
 
2487
        return
 
2488
 
 
2489
    def _get_substrate_constraints(self):
 
2490
        if self.env.joyent:
 
2491
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
 
2492
            return 'mem=2G cpu-cores=1'
 
2493
        else:
 
2494
            return 'mem=2G'
 
2495
 
2699
2496
    def get_bootstrap_args(self, upload_tools, bootstrap_series=None,
2700
2497
                           credential=None):
2701
2498
        """Return the bootstrap arguments for the substrate."""
2714
2511
                    env_val)
2715
2512
        return args
2716
2513
 
 
2514
    def bootstrap(self, upload_tools=False, bootstrap_series=None):
 
2515
        """Bootstrap a controller."""
 
2516
        self._check_bootstrap()
 
2517
        args = self.get_bootstrap_args(upload_tools, bootstrap_series)
 
2518
        self.juju('bootstrap', args, self.env.needs_sudo())
 
2519
 
 
2520
    @contextmanager
 
2521
    def bootstrap_async(self, upload_tools=False):
 
2522
        self._check_bootstrap()
 
2523
        args = self.get_bootstrap_args(upload_tools)
 
2524
        with self.juju_async('bootstrap', args):
 
2525
            yield
 
2526
            log.info('Waiting for bootstrap of {}.'.format(
 
2527
                self.env.environment))
 
2528
 
2717
2529
    def get_jes_command(self):
2718
2530
        raise JESNotSupported()
2719
2531
 
2775
2587
            log.info('Call to JES juju environments failed, falling back.')
2776
2588
            return []
2777
2589
 
 
2590
    def get_model_uuid(self):
 
2591
        raise JESNotSupported()
 
2592
 
2778
2593
    def deploy_bundle(self, bundle, timeout=_DEFAULT_BUNDLE_TIMEOUT):
2779
2594
        """Deploy bundle using deployer for Juju 1.X version."""
2780
2595
        self.deployer(bundle, timeout=timeout)
2793
2608
            args += (name,)
2794
2609
        self.juju('deployer', args, self.env.needs_sudo())
2795
2610
 
 
2611
    def deploy(self, charm, repository=None, to=None, series=None,
 
2612
               service=None, force=False, storage=None, constraints=None):
 
2613
        args = [charm]
 
2614
        if repository is not None:
 
2615
            args.extend(['--repository', repository])
 
2616
        if to is not None:
 
2617
            args.extend(['--to', to])
 
2618
        if service is not None:
 
2619
            args.extend([service])
 
2620
        if storage is not None:
 
2621
            args.extend(['--storage', storage])
 
2622
        if constraints is not None:
 
2623
            args.extend(['--constraints', constraints])
 
2624
        return self.juju('deploy', tuple(args))
 
2625
 
2796
2626
    def upgrade_charm(self, service, charm_path=None):
2797
2627
        args = (service,)
2798
2628
        if charm_path is not None:
2800
2630
            args = args + ('--repository', repository)
2801
2631
        self.juju('upgrade-charm', args)
2802
2632
 
 
2633
    def get_controller_client(self):
 
2634
        """Return a client for the controller model.  May return self."""
 
2635
        return self
 
2636
 
 
2637
    def get_controller_model_name(self):
 
2638
        """Return the name of the 'controller' model.
 
2639
 
 
2640
        Return the name of the 1.x environment."""
 
2641
        return self.env.environment
 
2642
 
2803
2643
    def get_controller_endpoint(self):
2804
2644
        """Return the address of the state-server leader."""
2805
2645
        endpoint = self.get_juju_output('api-endpoints')
2809
2649
    def upgrade_mongo(self):
2810
2650
        raise UpgradeMongoNotSupported()
2811
2651
 
 
2652
    def create_cloned_environment(
 
2653
            self, cloned_juju_home, controller_name, user_name=None):
 
2654
        """Create a cloned environment.
 
2655
 
 
2656
        `user_name` is unused in this version of juju.
 
2657
        """
 
2658
        user_client = self.clone(env=self.env.clone())
 
2659
        user_client.env.juju_home = cloned_juju_home
 
2660
        # New user names the controller.
 
2661
        user_client.env.controller = Controller(controller_name)
 
2662
        return user_client
 
2663
 
2812
2664
    def add_storage(self, unit, storage_type, amount="1"):
2813
2665
        """Add storage instances to service.
2814
2666
 
2852
2704
        return self.get_juju_output('authorized-keys import', *keys,
2853
2705
                                    merge_stderr=True)
2854
2706
 
 
2707
    def list_disabled_commands(self):
 
2708
        """List all the commands disabled on the model."""
 
2709
        raw = self.get_juju_output('block list', '--format', 'yaml')
 
2710
        return yaml.safe_load(raw)
 
2711
 
 
2712
    def disable_command(self, args):
 
2713
        """Disable a command set."""
 
2714
        return self.juju('block', args)
 
2715
 
 
2716
    def enable_command(self, args):
 
2717
        """Enable a command set."""
 
2718
        return self.juju('unblock', args)
 
2719
 
2855
2720
 
2856
2721
class EnvJujuClient22(EnvJujuClient1X):
2857
2722