Update libjuju

- fix licensing quirks
- refresh against libjuju master

Signed-off-by: Adam Israel <adam.israel@canonical.com>
diff --git a/modules/libjuju/juju/application.py b/modules/libjuju/juju/application.py
index 8719a62..620e9c9 100644
--- a/modules/libjuju/juju/application.py
+++ b/modules/libjuju/juju/application.py
@@ -1,3 +1,17 @@
+# Copyright 2016 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+#     Unless required by applicable law or agreed to in writing, software
+#     distributed under the License is distributed on an "AS IS" BASIS,
+#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#     See the License for the specific language governing permissions and
+#     limitations under the License.
+
 import asyncio
 import logging
 
@@ -342,8 +356,13 @@
             raise ValueError("switch and revision are mutually exclusive")
 
         client_facade = client.ClientFacade.from_connection(self.connection)
+        resources_facade = client.ResourcesFacade.from_connection(
+            self.connection)
         app_facade = client.ApplicationFacade.from_connection(self.connection)
 
+        charmstore = self.model.charmstore
+        charmstore_entity = None
+
         if switch is not None:
             charm_url = switch
             if not charm_url.startswith('cs:'):
@@ -354,18 +373,65 @@
             if revision is not None:
                 charm_url = "%s-%d" % (charm_url, revision)
             else:
-                charmstore = self.model.charmstore
-                entity = await charmstore.entity(charm_url, channel=channel)
-                charm_url = entity['Id']
+                charmstore_entity = await charmstore.entity(charm_url,
+                                                            channel=channel)
+                charm_url = charmstore_entity['Id']
 
         if charm_url == self.data['charm-url']:
             raise JujuError('already running charm "%s"' % charm_url)
 
+        # Update charm
         await client_facade.AddCharm(
             url=charm_url,
             channel=channel
         )
 
+        # Update resources
+        if not charmstore_entity:
+            charmstore_entity = await charmstore.entity(charm_url,
+                                                        channel=channel)
+        store_resources = charmstore_entity['Meta']['resources']
+
+        request_data = [client.Entity(self.tag)]
+        response = await resources_facade.ListResources(request_data)
+        existing_resources = {
+            resource.name: resource
+            for resource in response.results[0].resources
+        }
+
+        resources_to_update = [
+            resource for resource in store_resources
+            if resource['Name'] not in existing_resources or
+            existing_resources[resource['Name']].origin != 'upload'
+        ]
+
+        if resources_to_update:
+            request_data = [
+                client.CharmResource(
+                    description=resource.get('Description'),
+                    fingerprint=resource['Fingerprint'],
+                    name=resource['Name'],
+                    path=resource['Path'],
+                    revision=resource['Revision'],
+                    size=resource['Size'],
+                    type_=resource['Type'],
+                    origin='store',
+                ) for resource in resources_to_update
+            ]
+            response = await resources_facade.AddPendingResources(
+                self.tag,
+                charm_url,
+                request_data
+            )
+            pending_ids = response.pending_ids
+            resource_ids = {
+                resource['Name']: id
+                for resource, id in zip(resources_to_update, pending_ids)
+            }
+        else:
+            resource_ids = None
+
+        # Update application
         await app_facade.SetCharm(
             application=self.entity_id,
             channel=channel,
@@ -374,7 +440,7 @@
             config_settings_yaml=None,
             force_series=force_series,
             force_units=force_units,
-            resource_ids=None,
+            resource_ids=resource_ids,
             storage_constraints=None
         )
 
diff --git a/modules/libjuju/juju/client/_client.py b/modules/libjuju/juju/client/_client.py
index d510e11..2ef0ffd 100644
--- a/modules/libjuju/juju/client/_client.py
+++ b/modules/libjuju/juju/client/_client.py
@@ -1,7 +1,7 @@
 # DO NOT CHANGE THIS FILE! This file is auto-generated by facade.py.
 # Changes will be overwritten/lost when the file is regenerated.
 
-from juju.client._definitions import *
+from juju.client._definitions import *  # noqa
 
 from juju.client import _client1, _client2, _client3, _client4, _client5
 
@@ -15,22 +15,21 @@
 }
 
 
-
 def lookup_facade(name, version):
         """
         Given a facade name and version, attempt to pull that facade out
         of the correct client<version>.py file.
 
         """
-        try:
-            facade = getattr(CLIENTS[str(version)], name)
-        except KeyError:
-            raise ImportError("No facades found for version {}".format(version))
-        except AttributeError:
-            raise ImportError(
-                "No facade with name '{}' in version {}".format(name, version))
-        return facade
-
+        for _version in range(int(version), 0, -1):
+            try:
+                facade = getattr(CLIENTS[str(_version)], name)
+                return facade
+            except (KeyError, AttributeError):
+                continue
+        else:
+            raise ImportError("No supported version for facade: "
+                              "{}".format(name))
 
 
 class TypeFactory:
@@ -363,5 +362,3 @@
 
 class VolumeAttachmentsWatcherFacade(TypeFactory):
     pass
-
-
diff --git a/modules/libjuju/juju/client/client.py b/modules/libjuju/juju/client/client.py
index 89b5248..2f3e49d 100644
--- a/modules/libjuju/juju/client/client.py
+++ b/modules/libjuju/juju/client/client.py
@@ -5,7 +5,7 @@
 
 
 for o in overrides.__all__:
-    if not "Facade" in o:
+    if "Facade" not in o:
         # Override stuff in _definitions, which is all imported
         # into _client. We Monkey patch both the original class and
         # the ref in _client (import shenanigans are fun!)
diff --git a/modules/libjuju/juju/client/connection.py b/modules/libjuju/juju/client/connection.py
index 7457391..c09468c 100644
--- a/modules/libjuju/juju/client/connection.py
+++ b/modules/libjuju/juju/client/connection.py
@@ -413,7 +413,7 @@
             endpoints.extend(new_endpoints)
         else:
             # ran out of endpoints without a successful login
-            raise Exception("Couldn't authenticate to {}".format(
+            raise JujuConnectionError("Couldn't authenticate to {}".format(
                 self._endpoint))
 
         response = result['response']
@@ -584,6 +584,38 @@
     def accounts(self):
         return self._load_yaml('accounts.yaml', 'controllers')
 
+    def credentials(self):
+        return self._load_yaml('credentials.yaml', 'credentials')
+
+    def load_credential(self, cloud, name=None):
+        """Load a local credential.
+
+        :param str cloud: Name of cloud to load credentials from.
+        :param str name: Name of credential. If None, the default credential
+            will be used, if available.
+        :returns: A CloudCredential instance, or None.
+        """
+        try:
+            cloud = tag.untag('cloud-', cloud)
+            creds_data = self.credentials()[cloud]
+            if not name:
+                default_credential = creds_data.pop('default-credential', None)
+                default_region = creds_data.pop('default-region', None)  # noqa
+                if default_credential:
+                    name = creds_data['default-credential']
+                elif len(creds_data) == 1:
+                    name = list(creds_data)[0]
+                else:
+                    return None, None
+            cred_data = creds_data[name]
+            auth_type = cred_data.pop('auth-type')
+            return name, client.CloudCredential(
+                auth_type=auth_type,
+                attrs=cred_data,
+            )
+        except (KeyError, FileNotFoundError):
+            return None, None
+
     def _load_yaml(self, filename, key):
         filepath = os.path.join(self.path, filename)
         with io.open(filepath, 'rt') as f:
diff --git a/modules/libjuju/juju/client/facade.py b/modules/libjuju/juju/client/facade.py
index c959e01..c015c5f 100644
--- a/modules/libjuju/juju/client/facade.py
+++ b/modules/libjuju/juju/client/facade.py
@@ -44,19 +44,19 @@
 # Classes and helper functions that we'll write to _client.py
 LOOKUP_FACADE = '''
 def lookup_facade(name, version):
-        """
-        Given a facade name and version, attempt to pull that facade out
-        of the correct client<version>.py file.
+    """
+    Given a facade name and version, attempt to pull that facade out
+    of the correct client<version>.py file.
 
-        """
-        try:
-            facade = getattr(CLIENTS[str(version)], name)
-        except KeyError:
-            raise ImportError("No facades found for version {}".format(version))
-        except AttributeError:
-            raise ImportError(
-                "No facade with name '{}' in version {}".format(name, version))
-        return facade
+    """
+    try:
+        facade = getattr(CLIENTS[str(version)], name)
+    except KeyError:
+        raise ImportError("No facades found for version {}".format(version))
+    except AttributeError:
+        raise ImportError(
+            "No facade with name '{}' in version {}".format(name, version))
+    return facade
 
 
 '''
@@ -127,6 +127,7 @@
 
         return self[refname]
 
+
 _types = TypeRegistry()
 _registry = KindRegistry()
 CLASSES = {}
@@ -257,7 +258,7 @@
     for kind in sorted((k for k in _types if not isinstance(k, str)),
                        key=lambda x: str(x)):
         name = _types[kind]
-        if name in capture and not name in NAUGHTY_CLASSES:
+        if name in capture and name not in NAUGHTY_CLASSES:
             continue
         args = Args(kind)
         # Write Factory class for _client.py
@@ -277,9 +278,7 @@
             pprint.pformat(args.SchemaToPyMapping(), width=999),
             ", " if args else "",
             args.as_kwargs(),
-            textwrap.indent(args.get_doc(), INDENT * 2))
-                  ]
-        assignments = args._get_arg_str(False, False)
+            textwrap.indent(args.get_doc(), INDENT * 2))]
 
         if not args:
             source.append("{}pass".format(INDENT * 2))
@@ -289,7 +288,9 @@
                 arg_type = arg[1]
                 arg_type_name = strcast(arg_type)
                 if arg_type in basic_types:
-                    source.append("{}self.{} = {}".format(INDENT * 2, arg_name, arg_name))
+                    source.append("{}self.{} = {}".format(INDENT * 2,
+                                                          arg_name,
+                                                          arg_name))
                 elif issubclass(arg_type, typing.Sequence):
                     value_type = (
                         arg_type_name.__parameters__[0]
@@ -297,10 +298,16 @@
                         else None
                     )
                     if type(value_type) is typing.TypeVar:
-                        source.append("{}self.{} = [{}.from_json(o) for o in {} or []]".format(
-                            INDENT * 2, arg_name, strcast(value_type), arg_name))
+                        source.append(
+                            "{}self.{} = [{}.from_json(o) "
+                            "for o in {} or []]".format(INDENT * 2,
+                                                        arg_name,
+                                                        strcast(value_type),
+                                                        arg_name))
                     else:
-                        source.append("{}self.{} = {}".format(INDENT * 2, arg_name, arg_name))
+                        source.append("{}self.{} = {}".format(INDENT * 2,
+                                                              arg_name,
+                                                              arg_name))
                 elif issubclass(arg_type, typing.Mapping):
                     value_type = (
                         arg_type_name.__parameters__[1]
@@ -308,15 +315,28 @@
                         else None
                     )
                     if type(value_type) is typing.TypeVar:
-                        source.append("{}self.{} = {{k: {}.from_json(v) for k, v in ({} or dict()).items()}}".format(
-                            INDENT * 2, arg_name, strcast(value_type), arg_name))
+                        source.append(
+                            "{}self.{} = {{k: {}.from_json(v) "
+                            "for k, v in ({} or dict()).items()}}".format(
+                                INDENT * 2,
+                                arg_name,
+                                strcast(value_type),
+                                arg_name))
                     else:
-                        source.append("{}self.{} = {}".format(INDENT * 2, arg_name, arg_name))
+                        source.append("{}self.{} = {}".format(INDENT * 2,
+                                                              arg_name,
+                                                              arg_name))
                 elif type(arg_type) is typing.TypeVar:
-                    source.append("{}self.{} = {}.from_json({}) if {} else None".format(
-                        INDENT * 2, arg_name, arg_type_name, arg_name, arg_name))
+                    source.append("{}self.{} = {}.from_json({}) "
+                                  "if {} else None".format(INDENT * 2,
+                                                           arg_name,
+                                                           arg_type_name,
+                                                           arg_name,
+                                                           arg_name))
                 else:
-                    source.append("{}self.{} = {}".format(INDENT * 2, arg_name, arg_name))
+                    source.append("{}self.{} = {}".format(INDENT * 2,
+                                                          arg_name,
+                                                          arg_name))
 
         source = "\n".join(source)
         capture.clear(name)
@@ -435,7 +455,10 @@
     '''
     # map input types to rpc msg
     _params = dict()
-    msg = dict(type='{cls.name}', request='{name}', version={cls.version}, params=_params)
+    msg = dict(type='{cls.name}',
+               request='{name}',
+               version={cls.version},
+               params=_params)
 {assignments}
     reply = {await}self.rpc(msg)
     return reply
@@ -539,7 +562,7 @@
         return d
 
     def to_json(self):
-        return json.dumps(self.serialize())
+        return json.dumps(self.serialize(), cls=TypeEncoder, sort_keys=True)
 
 
 class Schema(dict):
@@ -576,7 +599,7 @@
         if not defs:
             return
         for d, data in defs.items():
-            if d in _registry and not d in NAUGHTY_CLASSES:
+            if d in _registry and d not in NAUGHTY_CLASSES:
                 continue
             node = self.deref(data, d)
             kind = node.get("type")
@@ -762,6 +785,7 @@
 
     return captures
 
+
 def setup():
     parser = argparse.ArgumentParser()
     parser.add_argument("-s", "--schema", default="juju/client/schemas*")
@@ -769,6 +793,7 @@
     options = parser.parse_args()
     return options
 
+
 def main():
     options = setup()
 
@@ -780,5 +805,6 @@
     write_definitions(captures, options, last_version)
     write_client(captures, options)
 
+
 if __name__ == '__main__':
     main()
diff --git a/modules/libjuju/juju/client/overrides.py b/modules/libjuju/juju/client/overrides.py
index f439adb..5e98e56 100644
--- a/modules/libjuju/juju/client/overrides.py
+++ b/modules/libjuju/juju/client/overrides.py
@@ -11,6 +11,7 @@
     'Number',
     'Binary',
     'ConfigValue',
+    'Resource',
 ]
 
 __patches__ = [
@@ -273,3 +274,47 @@
         return '<{} source={} value={}>'.format(type(self).__name__,
                                                 repr(self.source),
                                                 repr(self.value))
+
+
+class Resource(Type):
+    _toSchema = {'application': 'application',
+                 'charmresource': 'CharmResource',
+                 'id_': 'id',
+                 'pending_id': 'pending-id',
+                 'timestamp': 'timestamp',
+                 'username': 'username',
+                 'name': 'name',
+                 'origin': 'origin'}
+    _toPy = {'CharmResource': 'charmresource',
+             'application': 'application',
+             'id': 'id_',
+             'pending-id': 'pending_id',
+             'timestamp': 'timestamp',
+             'username': 'username',
+             'name': 'name',
+             'origin': 'origin'}
+
+    def __init__(self, charmresource=None, application=None, id_=None,
+                 pending_id=None, timestamp=None, username=None, name=None,
+                 origin=None, **unknown_fields):
+        '''
+        charmresource : CharmResource
+        application : str
+        id_ : str
+        pending_id : str
+        timestamp : str
+        username : str
+        name: str
+        origin : str
+        '''
+        if charmresource:
+            self.charmresource = _client.CharmResource.from_json(charmresource)
+        else:
+            self.charmresource = None
+        self.application = application
+        self.id_ = id_
+        self.pending_id = pending_id
+        self.timestamp = timestamp
+        self.username = username
+        self.name = name
+        self.origin = origin
diff --git a/modules/libjuju/juju/client/runner.py b/modules/libjuju/juju/client/runner.py
index 61f2963..6545bc4 100644
--- a/modules/libjuju/juju/client/runner.py
+++ b/modules/libjuju/juju/client/runner.py
@@ -1,6 +1,4 @@
 
-
-
 class AsyncRunner:
     async def __call__(self, facade_method, *args, **kwargs):
         await self.connection.rpc(facade_method(*args, **kwargs))
@@ -15,14 +13,9 @@
 # This could let us fake the protocol we want
 # while decoupling the protocol from the RPC and the IO/Process context
 
-# The problem is leaking the runtime impl details to the top levels of the API with
-# async def
-# By handling the Marshal/Unmarshal side of RPC as a protocol we can leave the RPC running to a specific
-# delegate without altering the method signatures.
-# This still isn't quite right though as async is co-op multitasking and the methods still need to know
-# not to block or they will pause other execution
-
-
-
-
-
+# The problem is leaking the runtime impl details to the top levels of the API
+# with async def By handling the Marshal/Unmarshal side of RPC as a protocol we
+# can leave the RPC running to a specific delegate without altering the method
+# signatures.  This still isn't quite right though as async is co-op
+# multitasking and the methods still need to know not to block or they will
+# pause other execution
diff --git a/modules/libjuju/juju/controller.py b/modules/libjuju/juju/controller.py
index 9b452c7..55ea55e 100644
--- a/modules/libjuju/juju/controller.py
+++ b/modules/libjuju/juju/controller.py
@@ -1,11 +1,13 @@
 import asyncio
 import logging
 
+from . import errors
 from . import tag
 from . import utils
 from .client import client
 from .client import connection
 from .model import Model
+from .user import User
 
 log = logging.getLogger(__name__)
 
@@ -39,9 +41,11 @@
         """Connect to the current Juju controller.
 
         """
-        self.connection = (
-            await connection.Connection.connect_current_controller(
-                max_frame_size=self.max_frame_size))
+        jujudata = connection.JujuData()
+        controller_name = jujudata.current_controller()
+        if not controller_name:
+            raise errors.JujuConnectionError('No current controller')
+        return await self.connect_controller(controller_name)
 
     async def connect_controller(self, controller_name):
         """Connect to a Juju controller by name.
@@ -61,6 +65,47 @@
             await self.connection.close()
             self.connection = None
 
+    async def add_credential(self, name=None, credential=None, cloud=None,
+                             owner=None):
+        """Add or update a credential to the controller.
+
+        :param str name: Name of new credential. If None, the default
+            local credential is used.  Name must be provided if a credential
+            is given.
+        :param CloudCredential credential: Credential to add. If not given,
+            it will attempt to read from local data, if available.
+        :param str cloud: Name of cloud to associate the credential with.
+            Defaults to the same cloud as the controller.
+        :param str owner: Username that will own the credential. Defaults to
+            the current user.
+        :returns: Name of credential that was uploaded.
+        """
+        if not cloud:
+            cloud = await self.get_cloud()
+
+        if not owner:
+            owner = self.connection.info['user-info']['identity']
+
+        if credential and not name:
+            raise errors.JujuError('Name must be provided for credential')
+
+        if not credential:
+            name, credential = connection.JujuData().load_credential(cloud,
+                                                                     name)
+            if credential is None:
+                raise errors.JujuError('Unable to find credential: '
+                                       '{}'.format(name))
+
+        log.debug('Uploading credential %s', name)
+        cloud_facade = client.CloudFacade.from_connection(self.connection)
+        await cloud_facade.UpdateCredentials([
+            client.UpdateCloudCredential(
+                tag=tag.credential(cloud, tag.untag('user-', owner), name),
+                credential=credential,
+            )])
+
+        return name
+
     async def add_model(
             self, model_name, cloud_name=None, credential_name=None,
             owner=None, config=None, region=None):
@@ -70,9 +115,8 @@
         :param str cloud_name: Name of the cloud in which to create the
             model, e.g. 'aws'. Defaults to same cloud as controller.
         :param str credential_name: Name of the credential to use when
-            creating the model. Defaults to current credential. If you
-            pass a credential_name, you must also pass a cloud_name,
-            even if it's the default cloud.
+            creating the model. If not given, it will attempt to find a
+            default credential.
         :param str owner: Username that will own the model. Defaults to
             the current user.
         :param dict config: Model configuration.
@@ -85,6 +129,16 @@
         owner = owner or self.connection.info['user-info']['identity']
         cloud_name = cloud_name or await self.get_cloud()
 
+        try:
+            # attempt to add/update the credential from local data if available
+            credential_name = await self.add_credential(
+                name=credential_name,
+                cloud=cloud_name,
+                owner=owner)
+        except errors.JujuError:
+            # if it's not available locally, assume it's on the controller
+            pass
+
         if credential_name:
             credential = tag.credential(
                 cloud_name,
@@ -96,6 +150,11 @@
 
         log.debug('Creating model %s', model_name)
 
+        if not config or 'authorized-keys' not in config:
+            config = config or {}
+            config['authorized-keys'] = await utils.read_ssh_key(
+                loop=self.loop)
+
         model_info = await model_facade.CreateModel(
             tag.cloud(cloud_name),
             config,
@@ -105,24 +164,6 @@
             region
         )
 
-        # Add our ssh key to the model, to work around
-        # https://bugs.launchpad.net/juju/+bug/1643076
-        try:
-            ssh_key = await utils.read_ssh_key(loop=self.loop)
-
-            if self.controller_name:
-                model_name = "{}:{}".format(self.controller_name, model_name)
-
-            cmd = ['juju', 'add-ssh-key', '-m', model_name, ssh_key]
-
-            await utils.execute_process(*cmd, log=log, loop=self.loop)
-        except Exception:
-            log.exception(
-                "Could not add ssh key to model. You will not be able "
-                "to ssh into machines in this model. "
-                "Manually running `juju add-ssh-key <key>` in the cli "
-                "may fix this problem.")
-
         model = Model()
         await model.connect(
             self.connection.endpoint,
@@ -136,24 +177,28 @@
 
         return model
 
-    async def destroy_models(self, *uuids):
+    async def destroy_models(self, *models):
         """Destroy one or more models.
 
-        :param str \*uuids: UUIDs of models to destroy
+        :param str \*models: Names or UUIDs of models to destroy
 
         """
+        uuids = await self._model_uuids()
+        models = [uuids[model] if model in uuids else model
+                  for model in models]
+
         model_facade = client.ModelManagerFacade.from_connection(
             self.connection)
 
         log.debug(
             'Destroying model%s %s',
-            '' if len(uuids) == 1 else 's',
-            ', '.join(uuids)
+            '' if len(models) == 1 else 's',
+            ', '.join(models)
         )
 
         await model_facade.DestroyModels([
-            client.Entity(tag.model(uuid))
-            for uuid in uuids
+            client.Entity(tag.model(model))
+            for model in models
         ])
     destroy_model = destroy_models
 
@@ -161,18 +206,26 @@
         """Add a user to this controller.
 
         :param str username: Username
+        :param str password: Password
         :param str display_name: Display name
-        :param str acl: Access control, e.g. 'read'
-        :param list models: Models to which the user is granted access
-
+        :returns: A :class:`~juju.user.User` instance
         """
         if not display_name:
             display_name = username
         user_facade = client.UserManagerFacade.from_connection(self.connection)
-        users = [{'display_name': display_name,
-                  'password': password,
-                  'username': username}]
-        return await user_facade.AddUser(users)
+        users = [client.AddUser(display_name=display_name,
+                                username=username,
+                                password=password)]
+        await user_facade.AddUser(users)
+        return await self.get_user(username)
+
+    async def remove_user(self, username):
+        """Remove a user from this controller.
+        """
+        client_facade = client.UserManagerFacade.from_connection(
+            self.connection)
+        user = tag.user(username)
+        await client_facade.RemoveUser([client.Entity(user)])
 
     async def change_user_password(self, username, password):
         """Change the password for a user in this controller.
@@ -231,17 +284,31 @@
         cloud = list(result.clouds.keys())[0]  # only lives on one cloud
         return tag.untag('cloud-', cloud)
 
-    async def get_models(self, all_=False, username=None):
-        """Return list of available models on this controller.
+    async def _model_uuids(self, all_=False, username=None):
+        controller_facade = client.ControllerFacade.from_connection(
+            self.connection)
+        for attempt in (1, 2, 3):
+            try:
+                response = await controller_facade.AllModels()
+                return {um.model.name: um.model.uuid
+                        for um in response.user_models}
+            except errors.JujuAPIError as e:
+                # retry concurrency error until resolved in Juju
+                # see: https://bugs.launchpad.net/juju/+bug/1721786
+                if 'has been removed' not in e.message or attempt == 3:
+                    raise
+                await asyncio.sleep(attempt, loop=self.loop)
+
+    async def list_models(self, all_=False, username=None):
+        """Return list of names of the available models on this controller.
 
         :param bool all_: List all models, regardless of user accessibilty
             (admin use only)
         :param str username: User for which to list models (admin use only)
 
         """
-        controller_facade = client.ControllerFacade.from_connection(
-            self.connection)
-        return await controller_facade.AllModels()
+        uuids = await self._model_uuids(all_, username)
+        return sorted(uuids.keys())
 
     def get_payloads(self, *patterns):
         """Return list of known payloads.
@@ -261,14 +328,6 @@
         """
         raise NotImplementedError()
 
-    def get_users(self, all_=False):
-        """Return list of users that can connect to this controller.
-
-        :param bool all_: Include disabled users
-
-        """
-        raise NotImplementedError()
-
     def login(self):
         """Log in to this controller.
 
@@ -284,25 +343,62 @@
         """
         raise NotImplementedError()
 
-    def get_model(self, name):
-        """Get a model by name.
+    async def get_model(self, model):
+        """Get a model by name or UUID.
 
-        :param str name: Model name
+        :param str model: Model name or UUID
 
         """
-        raise NotImplementedError()
+        uuids = await self._model_uuids()
+        if model in uuids:
+            name_or_uuid = uuids[model]
+        else:
+            name_or_uuid = model
 
-    async def get_user(self, username, include_disabled=False):
+        model = Model()
+        await model.connect(
+            self.connection.endpoint,
+            name_or_uuid,
+            self.connection.username,
+            self.connection.password,
+            self.connection.cacert,
+            self.connection.macaroons,
+            loop=self.loop,
+        )
+        return model
+
+    async def get_user(self, username):
         """Get a user by name.
 
         :param str username: Username
-
+        :returns: A :class:`~juju.user.User` instance
         """
         client_facade = client.UserManagerFacade.from_connection(
             self.connection)
         user = tag.user(username)
-        return await client_facade.UserInfo([client.Entity(user)],
-                                            include_disabled)
+        args = [client.Entity(user)]
+        try:
+            response = await client_facade.UserInfo(args, True)
+        except errors.JujuError as e:
+            if 'permission denied' in e.errors:
+                # apparently, trying to get info for a nonexistent user returns
+                # a "permission denied" error rather than an empty result set
+                return None
+            raise
+        if response.results and response.results[0].result:
+            return User(self, response.results[0].result)
+        return None
+
+    async def get_users(self, include_disabled=False):
+        """Return list of users that can connect to this controller.
+
+        :param bool include_disabled: Include disabled users
+        :returns: A list of :class:`~juju.user.User` instances
+        """
+        client_facade = client.UserManagerFacade.from_connection(
+            self.connection)
+        response = await client_facade.UserInfo(None, include_disabled)
+        return [User(self, r.result) for r in response.results]
 
     async def grant(self, username, acl='login'):
         """Set access level of the given user on the controller
diff --git a/modules/libjuju/juju/errors.py b/modules/libjuju/juju/errors.py
index de52174..ecd1c0d 100644
--- a/modules/libjuju/juju/errors.py
+++ b/modules/libjuju/juju/errors.py
@@ -1,5 +1,16 @@
 class JujuError(Exception):
-    pass
+    def __init__(self, *args, **kwargs):
+        self.message = ''
+        self.errors = []
+        if args:
+            self.message = str(args[0])
+            if isinstance(args[0], (list, tuple)):
+                self.errors = args[0]
+            elif len(args) > 1:
+                self.errors = list(args)
+            else:
+                self.errors = [self.message]
+        super().__init__(*args, **kwargs)
 
 
 class JujuAPIError(JujuError):
diff --git a/modules/libjuju/juju/loop.py b/modules/libjuju/juju/loop.py
index 4abedfc..aca726b 100644
--- a/modules/libjuju/juju/loop.py
+++ b/modules/libjuju/juju/loop.py
@@ -24,7 +24,7 @@
     try:
         loop.add_signal_handler(signal.SIGINT, abort)
         added = True
-    except ValueError as e:
+    except (ValueError, OSError, RuntimeError) as e:
         # add_signal_handler doesn't work in a thread
         if 'main thread' not in str(e):
             raise
diff --git a/modules/libjuju/juju/model.py b/modules/libjuju/juju/model.py
index bd8709a..fc8d5e9 100644
--- a/modules/libjuju/juju/model.py
+++ b/modules/libjuju/juju/model.py
@@ -1402,7 +1402,7 @@
         key_facade = client.KeyManagerFacade.from_connection(self.connection)
         key = base64.b64decode(bytes(key.strip().split()[1].encode('ascii')))
         key = hashlib.md5(key).hexdigest()
-        key = ':'.join(a+b for a, b in zip(key[::2], key[1::2]))
+        key = ':'.join(a + b for a, b in zip(key[::2], key[1::2]))
         await key_facade.DeleteKeys([key], user)
     remove_ssh_keys = remove_ssh_key
 
@@ -1658,8 +1658,9 @@
         apps, args = [], []
 
         default_series = bundle.get('series')
+        apps_dict = bundle.get('applications', bundle.get('services', {}))
         for app_name in self.applications:
-            app_dict = bundle['services'][app_name]
+            app_dict = apps_dict[app_name]
             charm_dir = os.path.abspath(os.path.expanduser(app_dict['charm']))
             if not os.path.isdir(charm_dir):
                 continue
@@ -1688,7 +1689,7 @@
             ], loop=self.model.loop)
             # Update the 'charm:' entry for each app with the new 'local:' url.
             for app_name, charm_url in zip(apps, charm_urls):
-                bundle['services'][app_name]['charm'] = charm_url
+                apps_dict[app_name]['charm'] = charm_url
 
         return bundle
 
@@ -1714,7 +1715,9 @@
 
     @property
     def applications(self):
-        return list(self.bundle['services'].keys())
+        apps_dict = self.bundle.get('applications',
+                                    self.bundle.get('services', {}))
+        return list(apps_dict.keys())
 
     def resolve(self, reference):
         if reference and reference.startswith('$'):
diff --git a/modules/libjuju/juju/utils.py b/modules/libjuju/juju/utils.py
index 1d1b24e..1d9bc1c 100644
--- a/modules/libjuju/juju/utils.py
+++ b/modules/libjuju/juju/utils.py
@@ -11,11 +11,11 @@
 
     '''
     p = await asyncio.create_subprocess_exec(
-            *cmd,
-            stdin=asyncio.subprocess.PIPE,
-            stdout=asyncio.subprocess.PIPE,
-            stderr=asyncio.subprocess.PIPE,
-            loop=loop)
+        *cmd,
+        stdin=asyncio.subprocess.PIPE,
+        stdout=asyncio.subprocess.PIPE,
+        stderr=asyncio.subprocess.PIPE,
+        loop=loop)
     stdout, stderr = await p.communicate()
     if log:
         log.debug("Exec %s -> %d", cmd, p.returncode)