[lxc-devel] [pylxd/master] Add support for the instances and virtual-machines endpoints.

ltrager on Github lxc-bot at linuxcontainers.org
Wed Feb 19 08:36:23 UTC 2020


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 302 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20200219/a7432af2/attachment-0001.bin>
-------------- next part --------------
From 256cff77fc466c6bc5451a07a3910e03b07089b6 Mon Sep 17 00:00:00 2001
From: Lee Trager <lee at trager.us>
Date: Wed, 19 Feb 2020 08:29:07 +0000
Subject: [PATCH] Add support for the instances and virtual-machines endpoints.

---
 pylxd/client.py                               |  22 +-
 pylxd/managers.py                             |   8 +
 pylxd/models/__init__.py                      |   4 +-
 pylxd/models/container.py                     | 791 +----------------
 pylxd/models/instance.py                      | 803 ++++++++++++++++++
 pylxd/models/storage_pool.py                  |   4 +
 pylxd/models/virtual_machine.py               |  19 +
 pylxd/tests/mock_lxd.py                       |  94 +-
 .../{test_container.py => test_instance.py}   | 404 ++++-----
 pylxd/tests/models/test_storage.py            |   2 +-
 10 files changed, 1108 insertions(+), 1043 deletions(-)
 create mode 100644 pylxd/models/instance.py
 create mode 100644 pylxd/models/virtual_machine.py
 rename pylxd/tests/models/{test_container.py => test_instance.py} (59%)

diff --git a/pylxd/client.py b/pylxd/client.py
index aa5118ca..ee9cc802 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -80,9 +80,9 @@ def __getattr__(self, name):
         :returns: new _APINode with /<name> on the end
         :rtype: _APINode
         """
-        # Special case for storage_pools which needs to become 'storage-pools'
-        if name == 'storage_pools':
-            name = 'storage-pools'
+        # '-' can't be used in variable names
+        if name in ('storage_pools', 'virtual_machines'):
+            name = name.replace('_', '-')
         return self.__class__('{}/{}'.format(self._api_endpoint, name),
                               cert=self.session.cert,
                               verify=self.session.verify,
@@ -222,11 +222,21 @@ class Client(object):
     This client wraps all the functionality required to interact with
     LXD, and is meant to be the sole entry point.
 
+    .. attribute:: instances
+
+        Instance of :class:`Client.Instances
+        <pylxd.client.Client.Instances>`:
+
     .. attribute:: containers
 
         Instance of :class:`Client.Containers
         <pylxd.client.Client.Containers>`:
 
+    .. attribute:: virtual_machines
+
+        Instance of :class:`Client.VirtualMachines
+        <pylxd.client.Client.VirtualMachines>`:
+
     .. attribute:: images
 
         Instance of :class:`Client.Images <pylxd.client.Client.Images>`.
@@ -253,8 +263,8 @@ class Client(object):
             >>> response = api.get()
             # Check status code and response
             >>> print response.status_code, response.json()
-            # /containers/test/
-            >>> print api.containers['test'].get().json()
+            # /instances/test/
+            >>> print api.instances['test'].get().json()
 
     """
 
@@ -316,7 +326,9 @@ def __init__(
 
         self.cluster = managers.ClusterManager(self)
         self.certificates = managers.CertificateManager(self)
+        self.instances = managers.InstanceManager(self)
         self.containers = managers.ContainerManager(self)
+        self.virtual_machines = managers.VirtualMachineManager(self)
         self.images = managers.ImageManager(self)
         self.networks = managers.NetworkManager(self)
         self.operations = managers.OperationManager(self)
diff --git a/pylxd/managers.py b/pylxd/managers.py
index 1b6cd478..6369e50c 100644
--- a/pylxd/managers.py
+++ b/pylxd/managers.py
@@ -31,10 +31,18 @@ class CertificateManager(BaseManager):
     manager_for = 'pylxd.models.Certificate'
 
 
+class InstanceManager(BaseManager):
+    manager_for = 'pylxd.models.Instance'
+
+
 class ContainerManager(BaseManager):
     manager_for = 'pylxd.models.Container'
 
 
+class VirtualMachineManager(BaseManager):
+    manager_for = 'pylxd.models.VirtualMachine'
+
+
 class ImageManager(BaseManager):
     manager_for = 'pylxd.models.Image'
 
diff --git a/pylxd/models/__init__.py b/pylxd/models/__init__.py
index c71504cc..eb2ca532 100644
--- a/pylxd/models/__init__.py
+++ b/pylxd/models/__init__.py
@@ -1,6 +1,8 @@
 from pylxd.models.cluster import (Cluster, ClusterMember)  # NOQA
 from pylxd.models.certificate import Certificate  # NOQA
-from pylxd.models.container import Container, Snapshot  # NOQA
+from pylxd.models.instance import Instance, Snapshot  # NOQA
+from pylxd.models.container import Container  # NOQA
+from pylxd.models.virtual_machine import VirtualMachine  # NOQA
 from pylxd.models.image import Image  # NOQA
 from pylxd.models.network import Network  # NOQA
 from pylxd.models.operation import Operation  # NOQA
diff --git a/pylxd/models/container.py b/pylxd/models/container.py
index 47a72ded..2bfbf205 100644
--- a/pylxd/models/container.py
+++ b/pylxd/models/container.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2016 Canonical Ltd
+# Copyright (c) 2020 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
@@ -11,792 +11,9 @@
 #    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 collections
-import os
-import stat
-import time
+from pylxd.models import Instance
 
-import six
-from six.moves.urllib import parse
-try:
-    from ws4py.client import WebSocketBaseClient
-    from ws4py.manager import WebSocketManager
-    from ws4py.messaging import BinaryMessage
-    _ws4py_installed = True
-except ImportError:  # pragma: no cover
-    WebSocketBaseClient = object
-    _ws4py_installed = False
 
-from pylxd import managers
-from pylxd.exceptions import LXDAPIException
-from pylxd.models import _model as model
-from pylxd.models.operation import Operation
+class Container(Instance):
 
-if six.PY2:
-    # Python2.7 doesn't have this natively
-    from pylxd.exceptions import NotADirectoryError
-
-
-class ContainerState(object):
-    """A simple object for representing container state."""
-
-    def __init__(self, **kwargs):
-        for key, value in six.iteritems(kwargs):
-            setattr(self, key, value)
-
-
-_ContainerExecuteResult = collections.namedtuple(
-    'ContainerExecuteResult',
-    ['exit_code', 'stdout', 'stderr'])
-
-
-class Container(model.Model):
-    """An LXD Container.
-
-    This class is not intended to be used directly, but rather to be used
-    via `Client.containers.create`.
-    """
-
-    architecture = model.Attribute()
-    config = model.Attribute()
-    created_at = model.Attribute()
-    devices = model.Attribute()
-    ephemeral = model.Attribute()
-    expanded_config = model.Attribute()
-    expanded_devices = model.Attribute()
-    name = model.Attribute(readonly=True)
-    description = model.Attribute()
-    profiles = model.Attribute()
-    status = model.Attribute(readonly=True)
-    last_used_at = model.Attribute(readonly=True)
-    location = model.Attribute(readonly=True)
-
-    status_code = model.Attribute(readonly=True)
-    stateful = model.Attribute(readonly=True)
-
-    snapshots = model.Manager()
-    files = model.Manager()
-
-    @property
-    def api(self):
-        return self.client.api.containers[self.name]
-
-    class FilesManager(object):
-        """A pseudo-manager for namespacing file operations."""
-
-        def __init__(self, client, container):
-            self._client = client
-            self._container = container
-
-        def put(self, filepath, data, mode=None, uid=None, gid=None):
-            """Push a file to the container.
-
-            This pushes a single file to the containers file system named by
-            the `filepath`.
-
-            :param filepath: The path in the container to to store the data in.
-            :type filepath: str
-            :param data: The data to store in the file.
-            :type data: bytes or str
-            :param mode: The unit mode to store the file with.  The default of
-                None stores the file with the current mask of 0700, which is
-                the lxd default.
-            :type mode: Union[oct, int, str]
-            :param uid: The uid to use inside the container. Default of None
-                results in 0 (root).
-            :type uid: int
-            :param gid: The gid to use inside the container.  Default of None
-                results in 0 (root).
-            :type gid: int
-            :raises: LXDAPIException if something goes wrong
-            """
-            headers = self._resolve_headers(mode=mode, uid=uid, gid=gid)
-            response = (self._client.api.containers[self._container.name]
-                        .files.post(params={'path': filepath},
-                                    data=data,
-                                    headers=headers or None))
-            if response.status_code == 200:
-                return
-            raise LXDAPIException(response)
-
-        @staticmethod
-        def _resolve_headers(headers=None, mode=None, uid=None, gid=None):
-            if headers is None:
-                headers = {}
-            if mode is not None:
-                if isinstance(mode, int):
-                    mode = format(mode, 'o')
-                if not isinstance(mode, six.string_types):
-                    raise ValueError("'mode' parameter must be int or string")
-                if not mode.startswith('0'):
-                    mode = '0{}'.format(mode)
-                headers['X-LXD-mode'] = mode
-            if uid is not None:
-                headers['X-LXD-uid'] = str(uid)
-            if gid is not None:
-                headers['X-LXD-gid'] = str(gid)
-            return headers
-
-        def delete_available(self):
-            """File deletion is an extension API and may not be available.
-            https://github.com/lxc/lxd/blob/master/doc/api-extensions.md#file_delete
-            """
-            return self._client.has_api_extension('file_delete')
-
-        def delete(self, filepath):
-            self._client.assert_has_api_extension('file_delete')
-            response = self._client.api.containers[
-                self._container.name].files.delete(
-                params={'path': filepath})
-            if response.status_code != 200:
-                raise LXDAPIException(response)
-
-        def get(self, filepath):
-            response = (self._client.api.containers[self._container.name]
-                        .files.get(params={'path': filepath}, is_api=False))
-            return response.content
-
-        def recursive_put(self, src, dst, mode=None, uid=None, gid=None):
-            """Recursively push directory to the container.
-
-            Recursively pushes directory to the containers
-            named by the `dst`
-
-            :param src: The source path of directory to copy.
-            :type src: str
-            :param dst: The destination path in the container
-                    of directory to copy
-            :type dst: str
-            :param mode: The unit mode to store the file with.  The default of
-                None stores the file with the current mask of 0700, which is
-                the lxd default.
-            :type mode: Union[oct, int, str]
-            :param uid: The uid to use inside the container. Default of None
-                results in 0 (root).
-            :type uid: int
-            :param gid: The gid to use inside the container.  Default of None
-                results in 0 (root).
-            :type gid: int
-            :raises: NotADirectoryError if src is not a directory
-            :raises: LXDAPIException if an error occurs
-            """
-            norm_src = os.path.normpath(src)
-            if not os.path.isdir(norm_src):
-                raise NotADirectoryError(
-                    "'src' parameter must be a directory "
-                )
-
-            idx = len(norm_src)
-            dst_items = set()
-
-            for path, dirname, files in os.walk(norm_src):
-                dst_path = os.path.normpath(
-                    os.path.join(dst, path[idx:].lstrip(os.path.sep)))
-                # create directory or symlink (depending on what's there)
-                if path not in dst_items:
-                    dst_items.add(path)
-                    headers = self._resolve_headers(mode=mode,
-                                                    uid=uid, gid=gid)
-                    # determine what the file is: a directory or a symlink
-                    fmode = os.stat(path).st_mode
-                    if stat.S_ISLNK(fmode):
-                        headers['X-LXD-type'] = 'symlink'
-                    else:
-                        headers['X-LXD-type'] = 'directory'
-                    (self._client.api.containers[self._container.name]
-                     .files.post(params={'path': dst_path},
-                                 headers=headers))
-
-                # copy files
-                for f in files:
-                    src_file = os.path.join(path, f)
-                    with open(src_file, 'rb') as fp:
-                        filepath = os.path.join(dst_path, f)
-                        headers = self._resolve_headers(mode=mode,
-                                                        uid=uid,
-                                                        gid=gid)
-                        response = (
-                            self._client.api.containers[self._container.name]
-                            .files.post(params={'path': filepath},
-                                        data=fp.read(),
-                                        headers=headers or None))
-                        if response.status_code != 200:
-                            raise LXDAPIException(response)
-
-    @classmethod
-    def exists(cls, client, name):
-        """Determine whether a container exists."""
-        try:
-            client.containers.get(name)
-            return True
-        except cls.NotFound:
-            return False
-
-    @classmethod
-    def get(cls, client, name):
-        """Get a container by name."""
-        response = client.api.containers[name].get()
-
-        container = cls(client, **response.json()['metadata'])
-        return container
-
-    @classmethod
-    def all(cls, client):
-        """Get all containers.
-
-        Containers returned from this method will only have the name
-        set, as that is the only property returned from LXD. If more
-        information is needed, `Container.sync` is the method call
-        that should be used.
-        """
-        response = client.api.containers.get()
-
-        containers = []
-        for url in response.json()['metadata']:
-            name = url.split('/')[-1]
-            containers.append(cls(client, name=name))
-        return containers
-
-    @classmethod
-    def create(cls, client, config, wait=False, target=None):
-        """Create a new container config.
-
-        :param client: client instance
-        :type client: Client
-        :param config: The configuration for the new container.
-        :type config: dict
-        :param wait: Whether to wait for async operations to complete.
-        :type wait: bool
-        :param target: If in cluster mode, the target member.
-        :type target: str
-        :raises LXDAPIException: if something goes wrong.
-        :returns: a container if successful
-        :rtype: :class:`Container`
-        """
-        response = client.api.containers.post(json=config, target=target)
-
-        if wait:
-            client.operations.wait_for_operation(response.json()['operation'])
-        return cls(client, name=config['name'])
-
-    def __init__(self, *args, **kwargs):
-        super(Container, self).__init__(*args, **kwargs)
-
-        self.snapshots = managers.SnapshotManager(self.client, self)
-        self.files = self.FilesManager(self.client, self)
-
-    def rename(self, name, wait=False):
-        """Rename a container."""
-        response = self.api.post(json={'name': name})
-
-        if wait:
-            self.client.operations.wait_for_operation(
-                response.json()['operation'])
-        self.name = name
-
-    def _set_state(self, state, timeout=30, force=True, wait=False):
-        response = self.api.state.put(json={
-            'action': state,
-            'timeout': timeout,
-            'force': force
-        })
-        if wait:
-            self.client.operations.wait_for_operation(
-                response.json()['operation'])
-            if 'status' in self.__dirty__:
-                del self.__dirty__[self.__dirty__.index('status')]
-            if self.ephemeral and state == 'stop':
-                self.client = None
-            else:
-                self.sync()
-
-    def state(self):
-        response = self.api.state.get()
-        state = ContainerState(**response.json()['metadata'])
-        return state
-
-    def start(self, timeout=30, force=True, wait=False):
-        """Start the container."""
-        return self._set_state('start',
-                               timeout=timeout,
-                               force=force,
-                               wait=wait)
-
-    def stop(self, timeout=30, force=True, wait=False):
-        """Stop the container."""
-        return self._set_state('stop',
-                               timeout=timeout,
-                               force=force,
-                               wait=wait)
-
-    def restart(self, timeout=30, force=True, wait=False):
-        """Restart the container."""
-        return self._set_state('restart',
-                               timeout=timeout,
-                               force=force,
-                               wait=wait)
-
-    def freeze(self, timeout=30, force=True, wait=False):
-        """Freeze the container."""
-        return self._set_state('freeze',
-                               timeout=timeout,
-                               force=force,
-                               wait=wait)
-
-    def unfreeze(self, timeout=30, force=True, wait=False):
-        """Unfreeze the container."""
-        return self._set_state('unfreeze',
-                               timeout=timeout,
-                               force=force,
-                               wait=wait)
-
-    def execute(
-            self, commands, environment=None, encoding=None, decode=True,
-            stdin_payload=None, stdin_encoding="utf-8",
-            stdout_handler=None, stderr_handler=None):
-        """Execute a command on the container. stdout and stderr are buffered if
-        no handler is given.
-
-        :param commands: The command and arguments as a list of strings
-        :type commands: [str]
-        :param environment: The environment variables to pass with the command
-        :type environment: {str: str}
-        :param encoding: The encoding to use for stdout/stderr if the param
-            decode is True.  If encoding is None, then no override is
-            performed and whatever the existing encoding from LXD is used.
-        :type encoding: str
-        :param decode: Whether to decode the stdout/stderr or just return the
-            raw buffers.
-        :type decode: bool
-        :param stdin_payload: Payload to pass via stdin
-        :type stdin_payload: Can be a file, string, bytearray, generator or
-            ws4py Message object
-        :param stdin_encoding: Encoding to pass text to stdin (default utf-8)
-        :param stdout_handler: Callable than receive as first parameter each
-            message received via stdout
-        :type stdout_handler: Callable[[str], None]
-        :param stderr_handler: Callable than receive as first parameter each
-            message received via stderr
-        :type stderr_handler: Callable[[str], None]
-        :raises ValueError: if the ws4py library is not installed.
-        :returns: A tuple of `(exit_code, stdout, stderr)`
-        :rtype: _ContainerExecuteResult() namedtuple
-        """
-        if not _ws4py_installed:
-            raise ValueError(
-                'This feature requires the optional ws4py library.')
-        if isinstance(commands, six.string_types):
-            raise TypeError("First argument must be a list.")
-        if environment is None:
-            environment = {}
-
-        response = self.api['exec'].post(json={
-            'command': commands,
-            'environment': environment,
-            'wait-for-websocket': True,
-            'interactive': False,
-        })
-
-        fds = response.json()['metadata']['metadata']['fds']
-        operation_id = \
-            Operation.extract_operation_id(response.json()['operation'])
-        parsed = parse.urlparse(
-            self.client.api.operations[operation_id].websocket._api_endpoint)
-
-        with managers.web_socket_manager(WebSocketManager()) as manager:
-            stdin = _StdinWebsocket(
-                self.client.websocket_url, payload=stdin_payload,
-                encoding=stdin_encoding
-            )
-            stdin.resource = '{}?secret={}'.format(parsed.path, fds['0'])
-            stdin.connect()
-            stdout = _CommandWebsocketClient(
-                manager, self.client.websocket_url,
-                encoding=encoding, decode=decode, handler=stdout_handler)
-            stdout.resource = '{}?secret={}'.format(parsed.path, fds['1'])
-            stdout.connect()
-            stderr = _CommandWebsocketClient(
-                manager, self.client.websocket_url,
-                encoding=encoding, decode=decode, handler=stderr_handler)
-            stderr.resource = '{}?secret={}'.format(parsed.path, fds['2'])
-            stderr.connect()
-
-            manager.start()
-
-            # watch for the end of the command:
-            while True:
-                operation = self.client.operations.get(operation_id)
-                if 'return' in operation.metadata:
-                    break
-                time.sleep(.5)  # pragma: no cover
-
-            try:
-                stdin.close()
-            except BrokenPipeError:
-                pass
-
-            stdout.finish_soon()
-            stderr.finish_soon()
-            manager.close_all()
-
-            while not stdout.finished or not stderr.finished:
-                time.sleep(.1)  # progma: no cover
-
-            manager.stop()
-            manager.join()
-
-            return _ContainerExecuteResult(
-                operation.metadata['return'], stdout.data, stderr.data)
-
-    def raw_interactive_execute(self, commands, environment=None):
-        """Execute a command on the container interactively and returns
-        urls to websockets. The urls contain a secret uuid, and can be accesses
-        without further authentication. The caller has to open and manage
-        the websockets themselves.
-
-        :param commands: The command and arguments as a list of strings
-           (most likely a shell)
-        :type commands: [str]
-        :param environment: The environment variables to pass with the command
-        :type environment: {str: str}
-        :returns: Two urls to an interactive websocket and a control socket
-        :rtype: {'ws':str,'control':str}
-        """
-        if isinstance(commands, six.string_types):
-            raise TypeError("First argument must be a list.")
-
-        if environment is None:
-            environment = {}
-
-        response = self.api['exec'].post(json={
-            'command': commands,
-            'environment': environment,
-            'wait-for-websocket': True,
-            'interactive': True,
-        })
-
-        fds = response.json()['metadata']['metadata']['fds']
-        operation_id = response.json()['operation']\
-            .split('/')[-1].split('?')[0]
-        parsed = parse.urlparse(
-            self.client.api.operations[operation_id].websocket._api_endpoint)
-
-        return {'ws': '{}?secret={}'.format(parsed.path, fds['0']),
-                'control': '{}?secret={}'.format(parsed.path, fds['control'])}
-
-    def migrate(self, new_client, live=False, wait=False):
-        """Migrate a container.
-
-        Destination host information is contained in the client
-        connection passed in.
-
-        If the `live` param is True, then a live migration is attempted,
-        otherwise a non live one is running.
-
-        If the container is running for live migration, it either must be shut
-        down first or criu must be installed on the source and destination
-        machines and the `live` param should be True.
-
-        :param new_client: the pylxd client connection to migrate the container
-            to.
-        :type new_client: :class:`pylxd.client.Client`
-        :param live: whether to perform a live migration
-        :type live: bool
-        :param wait: if True, wait for the migration to complete
-        :type wait: bool
-        :raises: LXDAPIException if any of the API calls fail.
-        :raises: ValueError if source of target are local connections
-        :returns: the response from LXD of the new container (the target of the
-            migration and not the operation if waited on.)
-        :rtype: :class:`requests.Response`
-        """
-        if self.api.scheme in ('http+unix',):
-            raise ValueError('Cannot migrate from a local client connection')
-
-        if self.status_code == 103:
-            try:
-                res = new_client.containers.create(
-                    self.generate_migration_data(live), wait=wait)
-            except LXDAPIException as e:
-                if e.response.status_code == 103:
-                    self.delete()
-                    return new_client.containers.get(self.name)
-                else:
-                    raise e
-        else:
-            res = new_client.containers.create(
-                self.generate_migration_data(live), wait=wait)
-        self.delete()
-        return res
-
-    def generate_migration_data(self, live=False):
-        """Generate the migration data.
-
-        This method can be used to handle migrations where the client
-        connection uses the local unix socket. For more information on
-        migration, see `Container.migrate`.
-
-        :param live: Whether to include "live": "true" in the migration
-        :type live: bool
-        :raises: LXDAPIException if the request to migrate fails
-        :returns: dictionary of migration data suitable to send to an new
-            client to complete a migration.
-        :rtype: Dict[str, ANY]
-        """
-        self.sync()  # Make sure the object isn't stale
-        _json = {'migration': True}
-        if live:
-            _json['live'] = True
-        response = self.api.post(json=_json)
-        operation = self.client.operations.get(response.json()['operation'])
-        operation_url = self.client.api.operations[operation.id]._api_endpoint
-        secrets = response.json()['metadata']['metadata']
-        cert = self.client.host_info['environment']['certificate']
-
-        return {
-            'name': self.name,
-            'architecture': self.architecture,
-            'config': self.config,
-            'devices': self.devices,
-            'epehemeral': self.ephemeral,
-            'default': self.profiles,
-            'source': {
-                'type': 'migration',
-                'operation': operation_url,
-                'mode': 'pull',
-                'certificate': cert,
-                'secrets': secrets,
-            }
-        }
-
-    def publish(self, public=False, wait=False):
-        """Publish a container as an image.
-
-        The container must be stopped in order publish it as an image. This
-        method does not enforce that constraint, so a LXDAPIException may be
-        raised if this method is called on a running container.
-
-        If wait=True, an Image is returned.
-        """
-        data = {
-            'public': public,
-            'source': {
-                'type': 'container',
-                'name': self.name,
-            }
-        }
-
-        response = self.client.api.images.post(json=data)
-        if wait:
-            operation = self.client.operations.wait_for_operation(
-                response.json()['operation'])
-
-            return self.client.images.get(operation.metadata['fingerprint'])
-
-    def restore_snapshot(self, snapshot_name, wait=False):
-        """Restore a snapshot using its name.
-
-        Attempts to restore a container using a snapshot previously made.  The
-        container should be stopped, but the method does not enforce this
-        constraint, so an LXDAPIException may be raised if this method fails.
-
-        :param snapshot_name: the name of the snapshot to restore from
-        :type snapshot_name: str
-        :param wait: wait until the operation is completed.
-        :type wait: boolean
-        :raises: LXDAPIException if the the operation fails.
-        :returns: the original response from the restore operation (not the
-            operation result)
-        :rtype: :class:`requests.Response`
-        """
-        response = self.api.put(json={"restore": snapshot_name})
-        if wait:
-            self.client.operations.wait_for_operation(
-                response.json()['operation'])
-        return response
-
-
-class _CommandWebsocketClient(WebSocketBaseClient):  # pragma: no cover
-    """Handle a websocket for container.execute(...) and manage decoding of the
-    returned values depending on 'decode' and 'encoding' parameters.
-    """
-
-    def __init__(self, manager, *args, **kwargs):
-        self.manager = manager
-        self.decode = kwargs.pop('decode', True)
-        self.encoding = kwargs.pop('encoding', None)
-        self.handler = kwargs.pop('handler', None)
-        self.message_encoding = None
-        self.finish_off = False
-        self.finished = False
-        self.last_message_empty = False
-        self.buffer = []
-        super(_CommandWebsocketClient, self).__init__(*args, **kwargs)
-
-    def handshake_ok(self):
-        self.manager.add(self)
-        self.buffer = []
-
-    def received_message(self, message):
-        if message.data is None or len(message.data) == 0:
-            self.last_message_empty = True
-            if self.finish_off:
-                self.finished = True
-            return
-        else:
-            self.last_message_empty = False
-        if message.encoding and self.message_encoding is None:
-            self.message_encoding = message.encoding
-        if self.handler:
-            self.handler(self._maybe_decode(message.data))
-        else:
-            self.buffer.append(message.data)
-        if self.finish_off and isinstance(message, BinaryMessage):
-            self.finished = True
-
-    def closed(self, code, reason=None):
-        self.finished = True
-
-    def finish_soon(self):
-        self.finish_off = True
-        if self.last_message_empty:
-            self.finished = True
-
-    def _maybe_decode(self, buffer):
-        if self.decode and buffer is not None:
-            if self.encoding:
-                return buffer.decode(self.encoding)
-            if self.message_encoding:
-                return buffer.decode(self.message_encoding)
-            # This is the backwards compatible "always decode to utf-8"
-            return buffer.decode('utf-8')
-        return buffer
-
-    @property
-    def data(self):
-        buffer = b''.join(self.buffer)
-        return self._maybe_decode(buffer)
-
-
-class _StdinWebsocket(WebSocketBaseClient):  # pragma: no cover
-    """A websocket client for handling stdin.
-
-    Allow comunicate with container commands via stdin
-    """
-
-    def __init__(self, url, payload=None, **kwargs):
-        self.encoding = kwargs.pop('encoding', None)
-        self.payload = payload
-        super(_StdinWebsocket, self).__init__(url, **kwargs)
-
-    def _smart_encode(self, msg):
-        if type(msg) == six.text_type and self.encoding:
-            return msg.encode(self.encoding)
-        return msg
-
-    def handshake_ok(self):
-        if self.payload:
-            if hasattr(self.payload, "read"):
-                self.send(
-                    (self._smart_encode(line) for line in self.payload),
-                    binary=True
-                )
-            else:
-                self.send(self._smart_encode(self.payload), binary=True)
-        self.send(b"", binary=False)
-
-
-class Snapshot(model.Model):
-    """A container snapshot."""
-
-    name = model.Attribute()
-    created_at = model.Attribute()
-    stateful = model.Attribute()
-
-    container = model.Parent()
-
-    @property
-    def api(self):
-        return self.client.api.containers[
-            self.container.name].snapshots[self.name]
-
-    @classmethod
-    def get(cls, client, container, name):
-        response = client.api.containers[
-            container.name].snapshots[name].get()
-
-        snapshot = cls(
-            client, container=container,
-            **response.json()['metadata'])
-        # Snapshot names are namespaced in LXD, as
-        # container-name/snapshot-name. We hide that implementation
-        # detail.
-        snapshot.name = snapshot.name.split('/')[-1]
-        return snapshot
-
-    @classmethod
-    def all(cls, client, container):
-        response = client.api.containers[container.name].snapshots.get()
-
-        return [cls(
-                client, name=snapshot.split('/')[-1],
-                container=container)
-                for snapshot in response.json()['metadata']]
-
-    @classmethod
-    def create(cls, client, container, name, stateful=False, wait=False):
-        response = client.api.containers[container.name].snapshots.post(json={
-            'name': name, 'stateful': stateful})
-
-        snapshot = cls(client, container=container, name=name)
-        if wait:
-            client.operations.wait_for_operation(response.json()['operation'])
-        return snapshot
-
-    def rename(self, new_name, wait=False):
-        """Rename a snapshot."""
-        response = self.api.post(json={'name': new_name})
-        if wait:
-            self.client.operations.wait_for_operation(
-                response.json()['operation'])
-        self.name = new_name
-
-    def publish(self, public=False, wait=False):
-        """Publish a snapshot as an image.
-
-        If wait=True, an Image is returned.
-
-        This functionality is currently broken in LXD. Please see
-        https://github.com/lxc/lxd/issues/2201 - The implementation
-        here is mostly a guess. Once that bug is fixed, we can verify
-        that this works, or file a bug to fix it appropriately.
-        """
-        data = {
-            'public': public,
-            'source': {
-                'type': 'snapshot',
-                'name': '{}/{}'.format(self.container.name, self.name),
-            }
-        }
-
-        response = self.client.api.images.post(json=data)
-        if wait:
-            operation = self.client.operations.wait_for_operation(
-                response.json()['operation'])
-            return self.client.images.get(operation.metadata['fingerprint'])
-
-    def restore(self, wait=False):
-        """Restore this snapshot.
-
-        Attempts to restore a container using this snapshot.  The container
-        should be stopped, but the method does not enforce this constraint, so
-        an LXDAPIException may be raised if this method fails.
-
-        :param wait: wait until the operation is completed.
-        :type wait: boolean
-        :raises: LXDAPIException if the the operation fails.
-        :returns: the original response from the restore operation (not the
-            operation result)
-        :rtype: :class:`requests.Response`
-        """
-        return self.container.restore_snapshot(self.name, wait)
+    _endpoint = 'containers'
diff --git a/pylxd/models/instance.py b/pylxd/models/instance.py
new file mode 100644
index 00000000..35621617
--- /dev/null
+++ b/pylxd/models/instance.py
@@ -0,0 +1,803 @@
+# Copyright (c) 2016-2020 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 collections
+import os
+import stat
+import time
+
+import six
+from six.moves.urllib import parse
+try:
+    from ws4py.client import WebSocketBaseClient
+    from ws4py.manager import WebSocketManager
+    from ws4py.messaging import BinaryMessage
+    _ws4py_installed = True
+except ImportError:  # pragma: no cover
+    WebSocketBaseClient = object
+    _ws4py_installed = False
+
+from pylxd import managers
+from pylxd.exceptions import LXDAPIException
+from pylxd.models import _model as model
+from pylxd.models.operation import Operation
+
+if six.PY2:
+    # Python2.7 doesn't have this natively
+    from pylxd.exceptions import NotADirectoryError
+
+
+class InstanceState(object):
+    """A simple object for representing instance state."""
+
+    def __init__(self, **kwargs):
+        for key, value in six.iteritems(kwargs):
+            setattr(self, key, value)
+
+
+_InstanceExecuteResult = collections.namedtuple(
+    'InstanceExecuteResult',
+    ['exit_code', 'stdout', 'stderr'])
+
+
+class Instance(model.Model):
+    """An LXD Instance.
+
+    This class is not intended to be used directly, but rather to be used
+    via `Client.instance.create`.
+    """
+
+    architecture = model.Attribute()
+    config = model.Attribute()
+    created_at = model.Attribute()
+    devices = model.Attribute()
+    ephemeral = model.Attribute()
+    expanded_config = model.Attribute()
+    expanded_devices = model.Attribute()
+    name = model.Attribute(readonly=True)
+    description = model.Attribute()
+    profiles = model.Attribute()
+    status = model.Attribute(readonly=True)
+    last_used_at = model.Attribute(readonly=True)
+    location = model.Attribute(readonly=True)
+    type = model.Attribute(readonly=True)
+
+    status_code = model.Attribute(readonly=True)
+    stateful = model.Attribute(readonly=True)
+
+    snapshots = model.Manager()
+    files = model.Manager()
+
+    _endpoint = 'instances'
+
+    @property
+    def api(self):
+        return getattr(self.client.api, self._endpoint)[self.name]
+
+    class FilesManager(object):
+        """A pseudo-manager for namespacing file operations."""
+
+        def __init__(self, instance):
+            self._instance = instance
+            self._endpoint = getattr(
+                instance.client.api, instance._endpoint)[instance.name].files
+
+        def put(self, filepath, data, mode=None, uid=None, gid=None):
+            """Push a file to the instance.
+
+            This pushes a single file to the instances file system named by
+            the `filepath`.
+
+            :param filepath: The path in the instance to to store the data in.
+            :type filepath: str
+            :param data: The data to store in the file.
+            :type data: bytes or str
+            :param mode: The unit mode to store the file with.  The default of
+                None stores the file with the current mask of 0700, which is
+                the lxd default.
+            :type mode: Union[oct, int, str]
+            :param uid: The uid to use inside the instance. Default of None
+                results in 0 (root).
+            :type uid: int
+            :param gid: The gid to use inside the instance.  Default of None
+                results in 0 (root).
+            :type gid: int
+            :raises: LXDAPIException if something goes wrong
+            """
+            headers = self._resolve_headers(mode=mode, uid=uid, gid=gid)
+            response = self._endpoint.post(
+                params={'path': filepath},
+                data=data,
+                headers=headers or None)
+            if response.status_code == 200:
+                return
+            raise LXDAPIException(response)
+
+        @staticmethod
+        def _resolve_headers(headers=None, mode=None, uid=None, gid=None):
+            if headers is None:
+                headers = {}
+            if mode is not None:
+                if isinstance(mode, int):
+                    mode = format(mode, 'o')
+                if not isinstance(mode, six.string_types):
+                    raise ValueError("'mode' parameter must be int or string")
+                if not mode.startswith('0'):
+                    mode = '0{}'.format(mode)
+                headers['X-LXD-mode'] = mode
+            if uid is not None:
+                headers['X-LXD-uid'] = str(uid)
+            if gid is not None:
+                headers['X-LXD-gid'] = str(gid)
+            return headers
+
+        def delete_available(self):
+            """File deletion is an extension API and may not be available.
+            https://github.com/lxc/lxd/blob/master/doc/api-extensions.md#file_delete
+            """
+            return self._instance.client.has_api_extension('file_delete')
+
+        def delete(self, filepath):
+            self._instance.client.assert_has_api_extension('file_delete')
+            response = self._endpoint.delete(params={'path': filepath})
+            if response.status_code != 200:
+                raise LXDAPIException(response)
+
+        def get(self, filepath):
+            response = self._endpoint.get(
+                params={'path': filepath}, is_api=False)
+            return response.content
+
+        def recursive_put(self, src, dst, mode=None, uid=None, gid=None):
+            """Recursively push directory to the instance.
+
+            Recursively pushes directory to the instances
+            named by the `dst`
+
+            :param src: The source path of directory to copy.
+            :type src: str
+            :param dst: The destination path in the instance
+                    of directory to copy
+            :type dst: str
+            :param mode: The unit mode to store the file with.  The default of
+                None stores the file with the current mask of 0700, which is
+                the lxd default.
+            :type mode: Union[oct, int, str]
+            :param uid: The uid to use inside the instance. Default of None
+                results in 0 (root).
+            :type uid: int
+            :param gid: The gid to use inside the instance.  Default of None
+                results in 0 (root).
+            :type gid: int
+            :raises: NotADirectoryError if src is not a directory
+            :raises: LXDAPIException if an error occurs
+            """
+            norm_src = os.path.normpath(src)
+            if not os.path.isdir(norm_src):
+                raise NotADirectoryError(
+                    "'src' parameter must be a directory "
+                )
+
+            idx = len(norm_src)
+            dst_items = set()
+
+            for path, dirname, files in os.walk(norm_src):
+                dst_path = os.path.normpath(
+                    os.path.join(dst, path[idx:].lstrip(os.path.sep)))
+                # create directory or symlink (depending on what's there)
+                if path not in dst_items:
+                    dst_items.add(path)
+                    headers = self._resolve_headers(mode=mode,
+                                                    uid=uid, gid=gid)
+                    # determine what the file is: a directory or a symlink
+                    fmode = os.stat(path).st_mode
+                    if stat.S_ISLNK(fmode):
+                        headers['X-LXD-type'] = 'symlink'
+                    else:
+                        headers['X-LXD-type'] = 'directory'
+                    self._endpoint.post(
+                        params={'path': dst_path},
+                        headers=headers)
+
+                # copy files
+                for f in files:
+                    src_file = os.path.join(path, f)
+                    with open(src_file, 'rb') as fp:
+                        filepath = os.path.join(dst_path, f)
+                        headers = self._resolve_headers(mode=mode,
+                                                        uid=uid,
+                                                        gid=gid)
+                        response = self._endpoint.post(
+                            params={'path': filepath},
+                            data=fp.read(),
+                            headers=headers or None)
+                        if response.status_code != 200:
+                            raise LXDAPIException(response)
+
+    @classmethod
+    def exists(cls, client, name):
+        """Determine whether a instance exists."""
+        try:
+            getattr(client, cls._endpoint).get(name)
+            return True
+        except cls.NotFound:
+            return False
+        
+    @classmethod
+    def get(cls, client, name):
+        """Get a instance by name."""
+        response = getattr(client.api, cls._endpoint)[name].get()
+
+        return cls(client, **response.json()['metadata'])
+
+    @classmethod
+    def all(cls, client):
+        """Get all instances.
+
+        Instances returned from this method will only have the name
+        set, as that is the only property returned from LXD. If more
+        information is needed, `Instance.sync` is the method call
+        that should be used.
+        """
+        response = getattr(client.api, cls._endpoint).get()
+
+        instances = []
+        for url in response.json()['metadata']:
+            name = url.split('/')[-1]
+            instances.append(cls(client, name=name))
+        return instances
+
+    @classmethod
+    def create(cls, client, config, wait=False, target=None):
+        """Create a new instance config.
+
+        :param client: client instance
+        :type client: Client
+        :param config: The configuration for the new instance.
+        :type config: dict
+        :param wait: Whether to wait for async operations to complete.
+        :type wait: bool
+        :param target: If in cluster mode, the target member.
+        :type target: str
+        :raises LXDAPIException: if something goes wrong.
+        :returns: an instance if successful
+        :rtype: :class:`Instance`
+        """
+        response = getattr(client.api, cls._endpoint).post(
+            json=config, target=target)
+
+        if wait:
+            client.operations.wait_for_operation(response.json()['operation'])
+        return cls(client, name=config['name'])
+
+    def __init__(self, *args, **kwargs):
+        super(Instance, self).__init__(*args, **kwargs)
+
+        self.snapshots = managers.SnapshotManager(self.client, self)
+        self.files = self.FilesManager(self)
+
+    def rename(self, name, wait=False):
+        """Rename an instance."""
+        response = self.api.post(json={'name': name})
+
+        if wait:
+            self.client.operations.wait_for_operation(
+                response.json()['operation'])
+        self.name = name
+
+    def _set_state(self, state, timeout=30, force=True, wait=False):
+        response = self.api.state.put(json={
+            'action': state,
+            'timeout': timeout,
+            'force': force
+        })
+        if wait:
+            self.client.operations.wait_for_operation(
+                response.json()['operation'])
+            if 'status' in self.__dirty__:
+                del self.__dirty__[self.__dirty__.index('status')]
+            if self.ephemeral and state == 'stop':
+                self.client = None
+            else:
+                self.sync()
+
+    def state(self):
+        response = self.api.state.get()
+        state = InstanceState(**response.json()['metadata'])
+        return state
+
+    def start(self, timeout=30, force=True, wait=False):
+        """Start the instance."""
+        return self._set_state('start',
+                               timeout=timeout,
+                               force=force,
+                               wait=wait)
+
+    def stop(self, timeout=30, force=True, wait=False):
+        """Stop the instance."""
+        return self._set_state('stop',
+                               timeout=timeout,
+                               force=force,
+                               wait=wait)
+
+    def restart(self, timeout=30, force=True, wait=False):
+        """Restart the instance."""
+        return self._set_state('restart',
+                               timeout=timeout,
+                               force=force,
+                               wait=wait)
+
+    def freeze(self, timeout=30, force=True, wait=False):
+        """Freeze the instance."""
+        return self._set_state('freeze',
+                               timeout=timeout,
+                               force=force,
+                               wait=wait)
+
+    def unfreeze(self, timeout=30, force=True, wait=False):
+        """Unfreeze the instance."""
+        return self._set_state('unfreeze',
+                               timeout=timeout,
+                               force=force,
+                               wait=wait)
+
+    def execute(
+            self, commands, environment=None, encoding=None, decode=True,
+            stdin_payload=None, stdin_encoding="utf-8",
+            stdout_handler=None, stderr_handler=None):
+        """Execute a command on the instance. stdout and stderr are buffered if
+        no handler is given.
+
+        :param commands: The command and arguments as a list of strings
+        :type commands: [str]
+        :param environment: The environment variables to pass with the command
+        :type environment: {str: str}
+        :param encoding: The encoding to use for stdout/stderr if the param
+            decode is True.  If encoding is None, then no override is
+            performed and whatever the existing encoding from LXD is used.
+        :type encoding: str
+        :param decode: Whether to decode the stdout/stderr or just return the
+            raw buffers.
+        :type decode: bool
+        :param stdin_payload: Payload to pass via stdin
+        :type stdin_payload: Can be a file, string, bytearray, generator or
+            ws4py Message object
+        :param stdin_encoding: Encoding to pass text to stdin (default utf-8)
+        :param stdout_handler: Callable than receive as first parameter each
+            message received via stdout
+        :type stdout_handler: Callable[[str], None]
+        :param stderr_handler: Callable than receive as first parameter each
+            message received via stderr
+        :type stderr_handler: Callable[[str], None]
+        :raises ValueError: if the ws4py library is not installed.
+        :returns: A tuple of `(exit_code, stdout, stderr)`
+        :rtype: _InstanceExecuteResult() namedtuple
+        """
+        if not _ws4py_installed:
+            raise ValueError(
+                'This feature requires the optional ws4py library.')
+        if isinstance(commands, six.string_types):
+            raise TypeError("First argument must be a list.")
+        if environment is None:
+            environment = {}
+
+        response = self.api['exec'].post(json={
+            'command': commands,
+            'environment': environment,
+            'wait-for-websocket': True,
+            'interactive': False,
+        })
+
+        fds = response.json()['metadata']['metadata']['fds']
+        operation_id = \
+            Operation.extract_operation_id(response.json()['operation'])
+        parsed = parse.urlparse(
+            self.client.api.operations[operation_id].websocket._api_endpoint)
+
+        with managers.web_socket_manager(WebSocketManager()) as manager:
+            stdin = _StdinWebsocket(
+                self.client.websocket_url, payload=stdin_payload,
+                encoding=stdin_encoding
+            )
+            stdin.resource = '{}?secret={}'.format(parsed.path, fds['0'])
+            stdin.connect()
+            stdout = _CommandWebsocketClient(
+                manager, self.client.websocket_url,
+                encoding=encoding, decode=decode, handler=stdout_handler)
+            stdout.resource = '{}?secret={}'.format(parsed.path, fds['1'])
+            stdout.connect()
+            stderr = _CommandWebsocketClient(
+                manager, self.client.websocket_url,
+                encoding=encoding, decode=decode, handler=stderr_handler)
+            stderr.resource = '{}?secret={}'.format(parsed.path, fds['2'])
+            stderr.connect()
+
+            manager.start()
+
+            # watch for the end of the command:
+            while True:
+                operation = self.client.operations.get(operation_id)
+                if 'return' in operation.metadata:
+                    break
+                time.sleep(.5)  # pragma: no cover
+
+            try:
+                stdin.close()
+            except BrokenPipeError:
+                pass
+
+            stdout.finish_soon()
+            stderr.finish_soon()
+            manager.close_all()
+
+            while not stdout.finished or not stderr.finished:
+                time.sleep(.1)  # progma: no cover
+
+            manager.stop()
+            manager.join()
+
+            return _InstanceExecuteResult(
+                operation.metadata['return'], stdout.data, stderr.data)
+
+    def raw_interactive_execute(self, commands, environment=None):
+        """Execute a command on the instance interactively and returns
+        urls to websockets. The urls contain a secret uuid, and can be accesses
+        without further authentication. The caller has to open and manage
+        the websockets themselves.
+
+        :param commands: The command and arguments as a list of strings
+           (most likely a shell)
+        :type commands: [str]
+        :param environment: The environment variables to pass with the command
+        :type environment: {str: str}
+        :returns: Two urls to an interactive websocket and a control socket
+        :rtype: {'ws':str,'control':str}
+        """
+        if isinstance(commands, six.string_types):
+            raise TypeError("First argument must be a list.")
+
+        if environment is None:
+            environment = {}
+
+        response = self.api['exec'].post(json={
+            'command': commands,
+            'environment': environment,
+            'wait-for-websocket': True,
+            'interactive': True,
+        })
+
+        fds = response.json()['metadata']['metadata']['fds']
+        operation_id = response.json()['operation']\
+            .split('/')[-1].split('?')[0]
+        parsed = parse.urlparse(
+            self.client.api.operations[operation_id].websocket._api_endpoint)
+
+        return {'ws': '{}?secret={}'.format(parsed.path, fds['0']),
+                'control': '{}?secret={}'.format(parsed.path, fds['control'])}
+
+    def migrate(self, new_client, live=False, wait=False):
+        """Migrate a instance.
+
+        Destination host information is contained in the client
+        connection passed in.
+
+        If the `live` param is True, then a live migration is attempted,
+        otherwise a non live one is running.
+
+        If the instance is running for live migration, it either must be shut
+        down first or criu must be installed on the source and destination
+        machines and the `live` param should be True.
+
+        :param new_client: the pylxd client connection to migrate the instance
+            to.
+        :type new_client: :class:`pylxd.client.Client`
+        :param live: whether to perform a live migration
+        :type live: bool
+        :param wait: if True, wait for the migration to complete
+        :type wait: bool
+        :raises: LXDAPIException if any of the API calls fail.
+        :raises: ValueError if source of target are local connections
+        :returns: the response from LXD of the new instance (the target of the
+            migration and not the operation if waited on.)
+        :rtype: :class:`requests.Response`
+        """
+        if self.api.scheme in ('http+unix',):
+            raise ValueError('Cannot migrate from a local client connection')
+
+        if self.status_code == 103:
+            try:
+                res = getattr(new_client, self._endpoint).create(
+                    self.generate_migration_data(live), wait=wait)
+            except LXDAPIException as e:
+                if e.response.status_code == 103:
+                    self.delete()
+                    return getattr(new_client, self._endpoint).get(self.name)
+                else:
+                    raise e
+        else:
+            res = getattr(new_client, self._endpoint).create(
+                self.generate_migration_data(live), wait=wait)
+        self.delete()
+        return res
+
+    def generate_migration_data(self, live=False):
+        """Generate the migration data.
+
+        This method can be used to handle migrations where the client
+        connection uses the local unix socket. For more information on
+        migration, see `Instance.migrate`.
+
+        :param live: Whether to include "live": "true" in the migration
+        :type live: bool
+        :raises: LXDAPIException if the request to migrate fails
+        :returns: dictionary of migration data suitable to send to an new
+            client to complete a migration.
+        :rtype: Dict[str, ANY]
+        """
+        self.sync()  # Make sure the object isn't stale
+        _json = {'migration': True}
+        if live:
+            _json['live'] = True
+        response = self.api.post(json=_json)
+        operation = self.client.operations.get(response.json()['operation'])
+        operation_url = self.client.api.operations[operation.id]._api_endpoint
+        secrets = response.json()['metadata']['metadata']
+        cert = self.client.host_info['environment']['certificate']
+
+        return {
+            'name': self.name,
+            'architecture': self.architecture,
+            'config': self.config,
+            'devices': self.devices,
+            'epehemeral': self.ephemeral,
+            'default': self.profiles,
+            'source': {
+                'type': 'migration',
+                'operation': operation_url,
+                'mode': 'pull',
+                'certificate': cert,
+                'secrets': secrets,
+            }
+        }
+
+    def publish(self, public=False, wait=False):
+        """Publish a instance as an image.
+
+        The instance must be stopped in order publish it as an image. This
+        method does not enforce that constraint, so a LXDAPIException may be
+        raised if this method is called on a running instance.
+
+        If wait=True, an Image is returned.
+        """
+        data = {
+            'public': public,
+            'source': {
+                'type': 'instance',
+                'name': self.name,
+            }
+        }
+
+        response = self.client.api.images.post(json=data)
+        if wait:
+            operation = self.client.operations.wait_for_operation(
+                response.json()['operation'])
+
+            return self.client.images.get(operation.metadata['fingerprint'])
+
+    def restore_snapshot(self, snapshot_name, wait=False):
+        """Restore a snapshot using its name.
+
+        Attempts to restore a instance using a snapshot previously made.  The
+        instance should be stopped, but the method does not enforce this
+        constraint, so an LXDAPIException may be raised if this method fails.
+
+        :param snapshot_name: the name of the snapshot to restore from
+        :type snapshot_name: str
+        :param wait: wait until the operation is completed.
+        :type wait: boolean
+        :raises: LXDAPIException if the the operation fails.
+        :returns: the original response from the restore operation (not the
+            operation result)
+        :rtype: :class:`requests.Response`
+        """
+        response = self.api.put(json={"restore": snapshot_name})
+        if wait:
+            self.client.operations.wait_for_operation(
+                response.json()['operation'])
+        return response
+
+
+class _CommandWebsocketClient(WebSocketBaseClient):  # pragma: no cover
+    """Handle a websocket for instance.execute(...) and manage decoding of the
+    returned values depending on 'decode' and 'encoding' parameters.
+    """
+
+    def __init__(self, manager, *args, **kwargs):
+        self.manager = manager
+        self.decode = kwargs.pop('decode', True)
+        self.encoding = kwargs.pop('encoding', None)
+        self.handler = kwargs.pop('handler', None)
+        self.message_encoding = None
+        self.finish_off = False
+        self.finished = False
+        self.last_message_empty = False
+        self.buffer = []
+        super(_CommandWebsocketClient, self).__init__(*args, **kwargs)
+
+    def handshake_ok(self):
+        self.manager.add(self)
+        self.buffer = []
+
+    def received_message(self, message):
+        if message.data is None or len(message.data) == 0:
+            self.last_message_empty = True
+            if self.finish_off:
+                self.finished = True
+            return
+        else:
+            self.last_message_empty = False
+        if message.encoding and self.message_encoding is None:
+            self.message_encoding = message.encoding
+        if self.handler:
+            self.handler(self._maybe_decode(message.data))
+        else:
+            self.buffer.append(message.data)
+        if self.finish_off and isinstance(message, BinaryMessage):
+            self.finished = True
+
+    def closed(self, code, reason=None):
+        self.finished = True
+
+    def finish_soon(self):
+        self.finish_off = True
+        if self.last_message_empty:
+            self.finished = True
+
+    def _maybe_decode(self, buffer):
+        if self.decode and buffer is not None:
+            if self.encoding:
+                return buffer.decode(self.encoding)
+            if self.message_encoding:
+                return buffer.decode(self.message_encoding)
+            # This is the backwards compatible "always decode to utf-8"
+            return buffer.decode('utf-8')
+        return buffer
+
+    @property
+    def data(self):
+        buffer = b''.join(self.buffer)
+        return self._maybe_decode(buffer)
+
+
+class _StdinWebsocket(WebSocketBaseClient):  # pragma: no cover
+    """A websocket client for handling stdin.
+
+    Allow comunicate with instance commands via stdin
+    """
+
+    def __init__(self, url, payload=None, **kwargs):
+        self.encoding = kwargs.pop('encoding', None)
+        self.payload = payload
+        super(_StdinWebsocket, self).__init__(url, **kwargs)
+
+    def _smart_encode(self, msg):
+        if type(msg) == six.text_type and self.encoding:
+            return msg.encode(self.encoding)
+        return msg
+
+    def handshake_ok(self):
+        if self.payload:
+            if hasattr(self.payload, "read"):
+                self.send(
+                    (self._smart_encode(line) for line in self.payload),
+                    binary=True
+                )
+            else:
+                self.send(self._smart_encode(self.payload), binary=True)
+        self.send(b"", binary=False)
+
+
+class Snapshot(model.Model):
+    """A instance snapshot."""
+
+    name = model.Attribute()
+    created_at = model.Attribute()
+    stateful = model.Attribute()
+
+    instance = model.Parent()
+
+    @property
+    def api(self):
+        return self.client.api.instances[
+            self.instance.name].snapshots[self.name]
+
+    @classmethod
+    def get(cls, client, instance, name):
+        response = client.api.instances[
+            instance.name].snapshots[name].get()
+
+        snapshot = cls(
+            client, instance=instance,
+            **response.json()['metadata'])
+        # Snapshot names are namespaced in LXD, as
+        # instance-name/snapshot-name. We hide that implementation
+        # detail.
+        snapshot.name = snapshot.name.split('/')[-1]
+        return snapshot
+
+    @classmethod
+    def all(cls, client, instance):
+        response = client.api.instances[instance.name].snapshots.get()
+
+        return [cls(
+                client, name=snapshot.split('/')[-1],
+                instance=instance)
+                for snapshot in response.json()['metadata']]
+
+    @classmethod
+    def create(cls, client, instance, name, stateful=False, wait=False):
+        response = client.api.instances[instance.name].snapshots.post(json={
+            'name': name, 'stateful': stateful})
+
+        snapshot = cls(client, instance=instance, name=name)
+        if wait:
+            client.operations.wait_for_operation(response.json()['operation'])
+        return snapshot
+
+    def rename(self, new_name, wait=False):
+        """Rename a snapshot."""
+        response = self.api.post(json={'name': new_name})
+        if wait:
+            self.client.operations.wait_for_operation(
+                response.json()['operation'])
+        self.name = new_name
+
+    def publish(self, public=False, wait=False):
+        """Publish a snapshot as an image.
+
+        If wait=True, an Image is returned.
+
+        This functionality is currently broken in LXD. Please see
+        https://github.com/lxc/lxd/issues/2201 - The implementation
+        here is mostly a guess. Once that bug is fixed, we can verify
+        that this works, or file a bug to fix it appropriately.
+        """
+        data = {
+            'public': public,
+            'source': {
+                'type': 'snapshot',
+                'name': '{}/{}'.format(self.instance.name, self.name),
+            }
+        }
+
+        response = self.client.api.images.post(json=data)
+        if wait:
+            operation = self.client.operations.wait_for_operation(
+                response.json()['operation'])
+            return self.client.images.get(operation.metadata['fingerprint'])
+
+    def restore(self, wait=False):
+        """Restore this snapshot.
+
+        Attempts to restore a instance using this snapshot.  The instance
+        should be stopped, but the method does not enforce this constraint, so
+        an LXDAPIException may be raised if this method fails.
+
+        :param wait: wait until the operation is completed.
+        :type wait: boolean
+        :raises: LXDAPIException if the the operation fails.
+        :returns: the original response from the restore operation (not the
+            operation result)
+        :rtype: :class:`requests.Response`
+        """
+        return self.instance.restore_snapshot(self.name, wait)
diff --git a/pylxd/models/storage_pool.py b/pylxd/models/storage_pool.py
index 8639b1d7..b50d298e 100644
--- a/pylxd/models/storage_pool.py
+++ b/pylxd/models/storage_pool.py
@@ -351,6 +351,10 @@ def all(cls, storage_pool):
             # for each type, convert to the string that will work with GET
             if _type == 'containers':
                 _type = 'container'
+            elif _type == 'virtual-machines':
+                _type = 'virtual-machine'
+            elif _type == 'instances':
+                _type = 'instance'
             elif _type == 'images':
                 _type = 'image'
             else:
diff --git a/pylxd/models/virtual_machine.py b/pylxd/models/virtual_machine.py
new file mode 100644
index 00000000..84f392be
--- /dev/null
+++ b/pylxd/models/virtual_machine.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2020 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.
+from pylxd.models import Instance
+
+
+class VirtualMachine(Instance):
+
+    _endpoint = 'virtual-machines'
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 5d44550e..c46e25e5 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -1,14 +1,14 @@
 import json
 
 
-def containers_POST(request, context):
+def instances_POST(request, context):
     context.status_code = 202
     return json.dumps({
         'type': 'async',
         'operation': '/1.0/operations/operation-abc?project=default'})
 
 
-def container_POST(request, context):
+def instance_POST(request, context):
     context.status_code = 202
     if not request.json().get('migration', False):
         return {
@@ -28,7 +28,7 @@ def container_POST(request, context):
         }
 
 
-def container_PUT(request, context):
+def instance_PUT(request, context):
     context.status_code = 202
     return {
         'type': 'async',
@@ -36,7 +36,7 @@ def container_PUT(request, context):
     }
 
 
-def container_DELETE(request, context):
+def instance_DELETE(request, context):
     context.status_code = 202
     return json.dumps({
         'type': 'async',
@@ -258,45 +258,45 @@ def snapshot_DELETE(request, context):
     },
 
 
-    # Containers
+    # Instances
     {
         'text': json.dumps({
             'type': 'sync',
             'metadata': [
-                'http://pylxd.test/1.0/containers/an-container',
+                'http://pylxd.test/1.0/instances/an-instance',
             ]}),
         'method': 'GET',
-        'url': r'^http://pylxd.test/1.0/containers$',
+        'url': r'^http://pylxd.test/1.0/instances$',
     },
     {
         'text': json.dumps({
             'type': 'sync',
             'metadata': [
-                'http://pylxd2.test/1.0/containers/an-container',
+                'http://pylxd2.test/1.0/instances/an-instance',
             ]}),
         'method': 'GET',
-        'url': r'^http://pylxd2.test/1.0/containers$',
+        'url': r'^http://pylxd2.test/1.0/instances$',
     },
     {
-        'text': containers_POST,
+        'text': instances_POST,
         'method': 'POST',
-        'url': r'^http://pylxd2.test/1.0/containers$',
+        'url': r'^http://pylxd2.test/1.0/instances$',
     },
     {
-        'text': containers_POST,
+        'text': instances_POST,
         'method': 'POST',
-        'url': r'^http://pylxd.test/1.0/containers$',
+        'url': r'^http://pylxd.test/1.0/instances$',
     },
     {
-        'text': containers_POST,
+        'text': instances_POST,
         'method': 'POST',
-        'url': r'^http://pylxd.test/1.0/containers\?target=an-remote',
+        'url': r'^http://pylxd.test/1.0/instances\?target=an-remote',
     },
     {
         'json': {
             'type': 'sync',
             'metadata': {
-                'name': 'an-container',
+                'name': 'an-instance',
 
                 'architecture': "x86_64",
                 'config': {
@@ -338,13 +338,13 @@ def snapshot_DELETE(request, context):
                     "makes it crash."
             }},
         'method': 'GET',
-        'url': r'^http://pylxd2.test/1.0/containers/an-container$',
+        'url': r'^http://pylxd2.test/1.0/instances/an-instance$',
     },
     {
         'json': {
             'type': 'sync',
             'metadata': {
-                'name': 'an-container',
+                'name': 'an-instance',
 
                 'architecture': "x86_64",
                 'config': {
@@ -386,7 +386,7 @@ def snapshot_DELETE(request, context):
                     "makes it crash."
             }},
         'method': 'GET',
-        'url': r'^http://pylxd.test/1.0/containers/an-container$',
+        'url': r'^http://pylxd.test/1.0/instances/an-instance$',
     },
     {
         'json': {
@@ -419,13 +419,13 @@ def snapshot_DELETE(request, context):
                 'processes': 100,
             }},
         'method': 'GET',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/state$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/state$',  # NOQA
     },
     {
         'json': {
             'type': 'sync',
             'metadata': {
-                'name': 'an-new-remote-container',
+                'name': 'an-new-remote-instance',
 
                 'architecture': "x86_64",
                 'config': {
@@ -442,7 +442,7 @@ def snapshot_DELETE(request, context):
                     "makes it crash."
             }},
         'method': 'GET',
-        'url': r'^http://pylxd.test/1.0/containers/an-new-remote-container$',
+        'url': r'^http://pylxd.test/1.0/instances/an-new-remote-instance$',
     },
     {
         'status_code': 202,
@@ -450,12 +450,12 @@ def snapshot_DELETE(request, context):
             'type': 'async',
             'operation': '/1.0/operations/operation-abc?project=default'},
         'method': 'PUT',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/state$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/state$',  # NOQA
     },
     {
-        'json': container_POST,
+        'json': instance_POST,
         'method': 'POST',
-        'url': r'^http://pylxd.test/1.0/containers/an-container$',
+        'url': r'^http://pylxd.test/1.0/instances/an-instance$',
     },
     {
         'text': json.dumps({
@@ -463,12 +463,12 @@ def snapshot_DELETE(request, context):
             'operation': '/1.0/operations/operation-abc?project=default'}),
         'status_code': 202,
         'method': 'PUT',
-        'url': r'^http://pylxd.test/1.0/containers/an-container$',
+        'url': r'^http://pylxd.test/1.0/instances/an-instance$',
     },
     {
-        'text': container_DELETE,
+        'text': instance_DELETE,
         'method': 'DELETE',
-        'url': r'^http://pylxd.test/1.0/containers/an-container$',
+        'url': r'^http://pylxd.test/1.0/instances/an-instance$',
     },
     {
         'json': {
@@ -486,23 +486,23 @@ def snapshot_DELETE(request, context):
             'operation': '/1.0/operations/operation-abc?project=default'},
         'status_code': 202,
         'method': 'POST',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/exec$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/exec$',  # NOQA
     },
     {
-        'json': container_PUT,
+        'json': instance_PUT,
         'method': 'PUT',
-        'url': r'^http://pylxd.test/1.0/containers/an-container$',
+        'url': r'^http://pylxd.test/1.0/instances/an-instance$',
     },
 
-    # Container Snapshots
+    # Instance Snapshots
     {
         'text': json.dumps({
             'type': 'sync',
             'metadata': [
-                '/1.0/containers/an_container/snapshots/an-snapshot',
+                '/1.0/instances/an_instance/snapshots/an-snapshot',
             ]}),
         'method': 'GET',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/snapshots$',  # NOQA
     },
     {
         'text': json.dumps({
@@ -510,17 +510,17 @@ def snapshot_DELETE(request, context):
             'operation': '/1.0/operations/operation-abc?project=default'}),
         'status_code': 202,
         'method': 'POST',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/snapshots$',  # NOQA
     },
     {
         'text': json.dumps({
             'type': 'sync',
             'metadata': {
-                'name': 'an_container/an-snapshot',
+                'name': 'an_instance/an-snapshot',
                 'stateful': False,
             }}),
         'method': 'GET',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/snapshots/an-snapshot$',  # NOQA
     },
     {
         'text': json.dumps({
@@ -528,33 +528,33 @@ def snapshot_DELETE(request, context):
             'operation': '/1.0/operations/operation-abc?project=default'}),
         'status_code': 202,
         'method': 'POST',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/snapshots/an-snapshot$',  # NOQA
     },
     {
         'text': snapshot_DELETE,
         'method': 'DELETE',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/snapshots/an-snapshot$',  # NOQA
     },
 
 
-    # Container files
+    # Instance files
     {
         'text': 'This is a getted file',
         'method': 'GET',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fgetted$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/files\?path=%2Ftmp%2Fgetted$',  # NOQA
     },
     {
         'text': '{"some": "value"}',
         'method': 'GET',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fjson-get$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/files\?path=%2Ftmp%2Fjson-get$',  # NOQA
     },
     {
         'method': 'POST',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fputted$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/files\?path=%2Ftmp%2Fputted$',  # NOQA
     },
     {
         'method': 'DELETE',
-        'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fputted$',  # NOQA
+        'url': r'^http://pylxd.test/1.0/instances/an-instance/files\?path=%2Ftmp%2Fputted$',  # NOQA
     },
 
 
@@ -842,9 +842,9 @@ def snapshot_DELETE(request, context):
         'json': {
             'type': 'sync',
             'metadata': [
-                '/1.0/storage-pools/default/volumes/containers/c1',
-                '/1.0/storage-pools/default/volumes/containers/c2',
-                '/1.0/storage-pools/default/volumes/containers/c3',
+                '/1.0/storage-pools/default/volumes/instances/c1',
+                '/1.0/storage-pools/default/volumes/instances/c2',
+                '/1.0/storage-pools/default/volumes/instances/c3',
                 '/1.0/storage-pools/default/volumes/images/i1',
                 '/1.0/storage-pools/default/volumes/images/i2',
                 '/1.0/storage-pools/default/volumes/custom/cu1',
diff --git a/pylxd/tests/models/test_container.py b/pylxd/tests/models/test_instance.py
similarity index 59%
rename from pylxd/tests/models/test_container.py
rename to pylxd/tests/models/test_instance.py
index 34f6829f..3bc652ba 100644
--- a/pylxd/tests/models/test_container.py
+++ b/pylxd/tests/models/test_instance.py
@@ -12,25 +12,25 @@
 from pylxd.tests import testing
 
 
-class TestContainer(testing.PyLXDTestCase):
-    """Tests for pylxd.models.Container."""
+class TestInstance(testing.PyLXDTestCase):
+    """Tests for pylxd.models.Instance."""
 
     def test_all(self):
-        """A list of all containers are returned."""
-        containers = models.Container.all(self.client)
+        """A list of all instances are returned."""
+        instances = models.Instance.all(self.client)
 
-        self.assertEqual(1, len(containers))
+        self.assertEqual(1, len(instances))
 
     def test_get(self):
-        """Return a container."""
-        name = 'an-container'
+        """Return a instance."""
+        name = 'an-instance'
 
-        an_container = models.Container.get(self.client, name)
+        an_instance = models.Instance.get(self.client, name)
 
-        self.assertEqual(name, an_container.name)
+        self.assertEqual(name, an_instance.name)
 
     def test_get_not_found(self):
-        """LXDAPIException is raised when the container doesn't exist."""
+        """LXDAPIException is raised when the instance doesn't exist."""
         def not_found(request, context):
             context.status_code = 404
             return json.dumps({
@@ -40,14 +40,14 @@ def not_found(request, context):
         self.add_rule({
             'text': not_found,
             'method': 'GET',
-            'url': r'^http://pylxd.test/1.0/containers/an-missing-container$',
+            'url': r'^http://pylxd.test/1.0/instances/an-missing-instance$',
         })
 
-        name = 'an-missing-container'
+        name = 'an-missing-instance'
 
         self.assertRaises(
             exceptions.LXDAPIException,
-            models.Container.get, self.client, name)
+            models.Instance.get, self.client, name)
 
     def test_get_error(self):
         """LXDAPIException is raised when the LXD API errors."""
@@ -60,42 +60,42 @@ def not_found(request, context):
         self.add_rule({
             'text': not_found,
             'method': 'GET',
-            'url': r'^http://pylxd.test/1.0/containers/an-missing-container$',
+            'url': r'^http://pylxd.test/1.0/instances/an-missing-instance$',
         })
 
-        name = 'an-missing-container'
+        name = 'an-missing-instance'
 
         self.assertRaises(
             exceptions.LXDAPIException,
-            models.Container.get, self.client, name)
+            models.Instance.get, self.client, name)
 
     def test_create(self):
-        """A new container is created."""
-        config = {'name': 'an-new-container'}
+        """A new instance is created."""
+        config = {'name': 'an-new-instance'}
 
-        an_new_container = models.Container.create(
+        an_new_instance = models.Instance.create(
             self.client, config, wait=True)
 
-        self.assertEqual(config['name'], an_new_container.name)
+        self.assertEqual(config['name'], an_new_instance.name)
 
     def test_create_remote(self):
-        """A new container is created at target."""
-        config = {'name': 'an-new-remote-container'}
+        """A new instance is created at target."""
+        config = {'name': 'an-new-remote-instance'}
 
-        an_new_remote_container = models.Container.create(
+        an_new_remote_instance = models.Instance.create(
             self.client, config, wait=True, target="an-remote")
 
-        self.assertEqual(config['name'], an_new_remote_container.name)
-        self.assertEqual("an-remote", an_new_remote_container.location)
+        self.assertEqual(config['name'], an_new_remote_instance.name)
+        self.assertEqual("an-remote", an_new_remote_instance.location)
 
     def test_exists(self):
-        """A container exists."""
-        name = 'an-container'
+        """A instance exists."""
+        name = 'an-instance'
 
-        self.assertTrue(models.Container.exists(self.client, name))
+        self.assertTrue(models.Instance.exists(self.client, name))
 
     def test_not_exists(self):
-        """A container exists."""
+        """A instance exists."""
         def not_found(request, context):
             context.status_code = 404
             return json.dumps({
@@ -105,24 +105,24 @@ def not_found(request, context):
         self.add_rule({
             'text': not_found,
             'method': 'GET',
-            'url': r'^http://pylxd.test/1.0/containers/an-missing-container$',
+            'url': r'^http://pylxd.test/1.0/instances/an-missing-instance$',
         })
 
-        name = 'an-missing-container'
+        name = 'an-missing-instance'
 
-        self.assertFalse(models.Container.exists(self.client, name))
+        self.assertFalse(models.Instance.exists(self.client, name))
 
     def test_fetch(self):
-        """A sync updates the properties of a container."""
-        an_container = models.Container(
-            self.client, name='an-container')
+        """A sync updates the properties of a instance."""
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
-        an_container.sync()
+        an_instance.sync()
 
-        self.assertTrue(an_container.ephemeral)
+        self.assertTrue(an_instance.ephemeral)
 
     def test_fetch_not_found(self):
-        """LXDAPIException is raised on a 404 for updating container."""
+        """LXDAPIException is raised on a 404 for updating instance."""
         def not_found(request, context):
             context.status_code = 404
             return json.dumps({
@@ -132,13 +132,13 @@ def not_found(request, context):
         self.add_rule({
             'text': not_found,
             'method': 'GET',
-            'url': r'^http://pylxd.test/1.0/containers/an-missing-container$',
+            'url': r'^http://pylxd.test/1.0/instances/an-missing-instance$',
         })
 
-        an_container = models.Container(
-            self.client, name='an-missing-container')
+        an_instance = models.Instance(
+            self.client, name='an-missing-instance')
 
-        self.assertRaises(exceptions.LXDAPIException, an_container.sync)
+        self.assertRaises(exceptions.LXDAPIException, an_instance.sync)
 
     def test_fetch_error(self):
         """LXDAPIException is raised on error."""
@@ -151,82 +151,82 @@ def not_found(request, context):
         self.add_rule({
             'text': not_found,
             'method': 'GET',
-            'url': r'^http://pylxd.test/1.0/containers/an-missing-container$',
+            'url': r'^http://pylxd.test/1.0/instances/an-missing-instance$',
         })
 
-        an_container = models.Container(
-            self.client, name='an-missing-container')
+        an_instance = models.Instance(
+            self.client, name='an-missing-instance')
 
-        self.assertRaises(exceptions.LXDAPIException, an_container.sync)
+        self.assertRaises(exceptions.LXDAPIException, an_instance.sync)
 
     def test_update(self):
-        """A container is updated."""
-        an_container = models.Container(
-            self.client, name='an-container')
-        an_container.architecture = 1
-        an_container.config = {}
-        an_container.created_at = 1
-        an_container.devices = {}
-        an_container.ephemeral = 1
-        an_container.expanded_config = {}
-        an_container.expanded_devices = {}
-        an_container.profiles = 1
-        an_container.status = 1
-
-        an_container.save(wait=True)
-
-        self.assertTrue(an_container.ephemeral)
+        """A instance is updated."""
+        an_instance = models.Instance(
+            self.client, name='an-instance')
+        an_instance.architecture = 1
+        an_instance.config = {}
+        an_instance.created_at = 1
+        an_instance.devices = {}
+        an_instance.ephemeral = 1
+        an_instance.expanded_config = {}
+        an_instance.expanded_devices = {}
+        an_instance.profiles = 1
+        an_instance.status = 1
+
+        an_instance.save(wait=True)
+
+        self.assertTrue(an_instance.ephemeral)
 
     def test_rename(self):
-        an_container = models.Container(
-            self.client, name='an-container')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
-        an_container.rename('an-renamed-container', wait=True)
+        an_instance.rename('an-renamed-instance', wait=True)
 
-        self.assertEqual('an-renamed-container', an_container.name)
+        self.assertEqual('an-renamed-instance', an_instance.name)
 
     def test_delete(self):
-        """A container is deleted."""
+        """A instance is deleted."""
         # XXX: rockstar (21 May 2016) - This just executes
         # a code path. There should be an assertion here, but
         # it's not clear how to assert that, just yet.
-        an_container = models.Container(
-            self.client, name='an-container')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
-        an_container.delete(wait=True)
+        an_instance.delete(wait=True)
 
     @testing.requires_ws4py
-    @mock.patch('pylxd.models.container._StdinWebsocket')
-    @mock.patch('pylxd.models.container._CommandWebsocketClient')
+    @mock.patch('pylxd.models.instance._StdinWebsocket')
+    @mock.patch('pylxd.models.instance._CommandWebsocketClient')
     def test_execute(self, _CommandWebsocketClient, _StdinWebsocket):
-        """A command is executed on a container."""
+        """A command is executed on a instance."""
         fake_websocket = mock.Mock()
         fake_websocket.data = 'test\n'
         _StdinWebsocket.return_value = fake_websocket
         _CommandWebsocketClient.return_value = fake_websocket
 
-        an_container = models.Container(
-            self.client, name='an-container')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
-        result = an_container.execute(['echo', 'test'])
+        result = an_instance.execute(['echo', 'test'])
 
         self.assertEqual(0, result.exit_code)
         self.assertEqual('test\n', result.stdout)
 
     @testing.requires_ws4py
-    @mock.patch('pylxd.models.container._StdinWebsocket')
-    @mock.patch('pylxd.models.container._CommandWebsocketClient')
+    @mock.patch('pylxd.models.instance._StdinWebsocket')
+    @mock.patch('pylxd.models.instance._CommandWebsocketClient')
     def test_execute_with_env(self, _CommandWebsocketClient, _StdinWebsocket):
-        """A command is executed on a container with custom env variables."""
+        """A command is executed on a instance with custom env variables."""
         fake_websocket = mock.Mock()
         fake_websocket.data = 'test\n'
         _StdinWebsocket.return_value = fake_websocket
         _CommandWebsocketClient.return_value = fake_websocket
 
-        an_container = models.Container(
-            self.client, name='an-container')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
-        result = an_container.execute(['echo', 'test'], environment={
+        result = an_instance.execute(['echo', 'test'], environment={
             "DISPLAY": ":1"
         })
 
@@ -235,31 +235,31 @@ def test_execute_with_env(self, _CommandWebsocketClient, _StdinWebsocket):
 
     def test_execute_no_ws4py(self):
         """If ws4py is not installed, ValueError is raised."""
-        from pylxd.models import container
-        old_installed = container._ws4py_installed
-        container._ws4py_installed = False
+        from pylxd.models import instance
+        old_installed = instance._ws4py_installed
+        instance._ws4py_installed = False
 
         def cleanup():
-            container._ws4py_installed = old_installed
+            instance._ws4py_installed = old_installed
         self.addCleanup(cleanup)
 
-        an_container = models.Container(
-            self.client, name='an-container')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
-        self.assertRaises(ValueError, an_container.execute, ['echo', 'test'])
+        self.assertRaises(ValueError, an_instance.execute, ['echo', 'test'])
 
     @testing.requires_ws4py
     def test_execute_string(self):
         """A command passed as string raises a TypeError."""
-        an_container = models.Container(
-            self.client, name='an-container')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
-        self.assertRaises(TypeError, an_container.execute, 'apt-get update')
+        self.assertRaises(TypeError, an_instance.execute, 'apt-get update')
 
     def test_raw_interactive_execute(self):
-        an_container = models.Container(self.client, name='an-container')
+        an_instance = models.Instance(self.client, name='an-instance')
 
-        result = an_container.raw_interactive_execute(['/bin/bash'])
+        result = an_instance.raw_interactive_execute(['/bin/bash'])
 
         self.assertEqual(result['ws'],
                          '/1.0/operations/operation-abc/websocket?secret=abc')
@@ -267,10 +267,10 @@ def test_raw_interactive_execute(self):
                          '/1.0/operations/operation-abc/websocket?secret=jkl')
 
     def test_raw_interactive_execute_env(self):
-        an_container = models.Container(self.client, name='an-container')
+        an_instance = models.Instance(self.client, name='an-instance')
 
-        result = an_container.raw_interactive_execute(['/bin/bash'],
-                                                      {"PATH": "/"})
+        result = an_instance.raw_interactive_execute(
+            ['/bin/bash'], {"PATH": "/"})
 
         self.assertEqual(result['ws'],
                          '/1.0/operations/operation-abc/websocket?secret=abc')
@@ -279,27 +279,27 @@ def test_raw_interactive_execute_env(self):
 
     def test_raw_interactive_execute_string(self):
         """A command passed as string raises a TypeError."""
-        an_container = models.Container(
-            self.client, name='an-container')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
         self.assertRaises(TypeError,
-                          an_container.raw_interactive_execute,
+                          an_instance.raw_interactive_execute,
                           'apt-get update')
 
     def test_migrate(self):
-        """A container is migrated."""
+        """A instance is migrated."""
         from pylxd.client import Client
 
         client2 = Client(endpoint='http://pylxd2.test')
-        an_container = models.Container(
-            self.client, name='an-container')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
-        an_migrated_container = an_container.migrate(client2)
+        an_migrated_instance = an_instance.migrate(client2)
 
-        self.assertEqual('an-container', an_migrated_container.name)
-        self.assertEqual(client2, an_migrated_container.client)
+        self.assertEqual('an-instance', an_migrated_instance.name)
+        self.assertEqual(client2, an_migrated_instance.client)
 
-    @mock.patch('pylxd.models.container.Container.generate_migration_data')
+    @mock.patch('pylxd.models.instance.Instance.generate_migration_data')
     def test_migrate_exception_error(self, generate_migration_data):
         """LXDAPIException is raised in case of migration failure"""
         from pylxd.client import Client
@@ -312,22 +312,22 @@ def generate_exception(*args, **kwargs):
 
         generate_migration_data.side_effect = generate_exception
 
-        an_container = models.Container(
-            self.client, name='an-container')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
         client2 = Client(endpoint='http://pylxd2.test')
-        self.assertRaises(LXDAPIException, an_container.migrate, client2)
+        self.assertRaises(LXDAPIException, an_instance.migrate, client2)
 
-    @mock.patch('pylxd.models.container.Container.generate_migration_data')
+    @mock.patch('pylxd.models.instance.Instance.generate_migration_data')
     def test_migrate_exception_running(self, generate_migration_data):
-        """Migrated container already running on destination"""
+        """Migrated instance already running on destination"""
         from pylxd.client import Client
         from pylxd.exceptions import LXDAPIException
 
         client2 = Client(endpoint='http://pylxd2.test')
-        an_container = models.Container(
-            self.client, name='an-container')
-        an_container.status_code = 103
+        an_instance = models.Instance(
+            self.client, name='an-instance')
+        an_instance.status_code = 103
 
         def generate_exception(*args, **kwargs):
             response = mock.Mock()
@@ -336,37 +336,37 @@ def generate_exception(*args, **kwargs):
 
         generate_migration_data.side_effect = generate_exception
 
-        an_migrated_container = an_container.migrate(client2, live=True)
+        an_migrated_instance = an_instance.migrate(client2, live=True)
 
-        self.assertEqual('an-container', an_migrated_container.name)
-        self.assertEqual(client2, an_migrated_container.client)
+        self.assertEqual('an-instance', an_migrated_instance.name)
+        self.assertEqual(client2, an_migrated_instance.client)
         generate_migration_data.assert_called_once_with(True)
 
     def test_migrate_started(self):
-        """A container is migrated."""
+        """A instance is migrated."""
         from pylxd.client import Client
 
         client2 = Client(endpoint='http://pylxd2.test')
-        an_container = models.Container.get(self.client, name='an-container')
-        an_container.status_code = 103
+        an_instance = models.Instance.get(self.client, name='an-instance')
+        an_instance.status_code = 103
 
-        an_migrated_container = an_container.migrate(client2)
+        an_migrated_instance = an_instance.migrate(client2)
 
-        self.assertEqual('an-container', an_migrated_container.name)
-        self.assertEqual(client2, an_migrated_container.client)
+        self.assertEqual('an-instance', an_migrated_instance.name)
+        self.assertEqual(client2, an_migrated_instance.client)
 
     def test_migrate_stopped(self):
-        """A container is migrated."""
+        """A instance is migrated."""
         from pylxd.client import Client
 
         client2 = Client(endpoint='http://pylxd2.test')
-        an_container = models.Container.get(self.client, name='an-container')
-        an_container.status_code = 102
+        an_instance = models.Instance.get(self.client, name='an-instance')
+        an_instance.status_code = 102
 
-        an_migrated_container = an_container.migrate(client2)
+        an_migrated_instance = an_instance.migrate(client2)
 
-        self.assertEqual('an-container', an_migrated_container.name)
-        self.assertEqual(client2, an_migrated_container.client)
+        self.assertEqual('an-instance', an_migrated_instance.name)
+        self.assertEqual(client2, an_migrated_instance.client)
 
     @mock.patch('pylxd.client._APINode.get')
     def test_migrate_local_client(self, get):
@@ -380,13 +380,13 @@ def test_migrate_local_client(self, get):
         from pylxd.client import Client
 
         client2 = Client(endpoint='http+unix://pylxd2.test')
-        an_container = models.Container(
-            client2, name='an-container')
+        an_instance = models.Instance(
+            client2, name='an-instance')
 
-        self.assertRaises(ValueError, an_container.migrate, self.client)
+        self.assertRaises(ValueError, an_instance.migrate, self.client)
 
     def test_publish(self):
-        """Containers can be published."""
+        """Instances can be published."""
         self.add_rule({
             'text': json.dumps({
                 'type': 'sync',
@@ -402,10 +402,10 @@ def test_publish(self):
             'url': r'^http://pylxd.test/1.0/operations/operation-abc$',
         })
 
-        an_container = models.Container(
-            self.client, name='an-container')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
 
-        image = an_container.publish(wait=True)
+        image = an_instance.publish(wait=True)
 
         self.assertEqual(
             'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
@@ -413,80 +413,80 @@ def test_publish(self):
 
     def test_restore_snapshot(self):
         """Snapshots can be restored"""
-        an_container = models.Container(
-            self.client, name='an-container')
-        an_container.restore_snapshot('thing')
+        an_instance = models.Instance(
+            self.client, name='an-instance')
+        an_instance.restore_snapshot('thing')
 
 
-class TestContainerState(testing.PyLXDTestCase):
-    """Tests for pylxd.models.ContainerState."""
+class TestInstanceState(testing.PyLXDTestCase):
+    """Tests for pylxd.models.InstanceState."""
 
     def test_get(self):
-        """Return a container."""
-        name = 'an-container'
+        """Return a instance."""
+        name = 'an-instance'
 
-        an_container = models.Container.get(self.client, name)
-        state = an_container.state()
+        an_instance = models.Instance.get(self.client, name)
+        state = an_instance.state()
 
         self.assertEqual('Running', state.status)
         self.assertEqual(103, state.status_code)
 
     def test_start(self):
-        """A container is started."""
-        an_container = models.Container.get(self.client, 'an-container')
+        """A instance is started."""
+        an_instance = models.Instance.get(self.client, 'an-instance')
 
-        an_container.start(wait=True)
+        an_instance.start(wait=True)
 
     def test_stop(self):
-        """A container is stopped."""
-        an_container = models.Container.get(self.client, 'an-container')
+        """A instance is stopped."""
+        an_instance = models.Instance.get(self.client, 'an-instance')
 
-        an_container.stop()
+        an_instance.stop()
 
     def test_restart(self):
-        """A container is restarted."""
-        an_container = models.Container.get(self.client, 'an-container')
+        """A instance is restarted."""
+        an_instance = models.Instance.get(self.client, 'an-instance')
 
-        an_container.restart()
+        an_instance.restart()
 
     def test_freeze(self):
-        """A container is suspended."""
-        an_container = models.Container.get(self.client, 'an-container')
+        """A instance is suspended."""
+        an_instance = models.Instance.get(self.client, 'an-instance')
 
-        an_container.freeze()
+        an_instance.freeze()
 
     def test_unfreeze(self):
-        """A container is resumed."""
-        an_container = models.Container.get(self.client, 'an-container')
+        """A instance is resumed."""
+        an_instance = models.Instance.get(self.client, 'an-instance')
 
-        an_container.unfreeze()
+        an_instance.unfreeze()
 
 
-class TestContainerSnapshots(testing.PyLXDTestCase):
-    """Tests for pylxd.models.Container.snapshots."""
+class TestInstanceSnapshots(testing.PyLXDTestCase):
+    """Tests for pylxd.models.Instance.snapshots."""
 
     def setUp(self):
-        super(TestContainerSnapshots, self).setUp()
-        self.container = models.Container.get(self.client, 'an-container')
+        super(TestInstanceSnapshots, self).setUp()
+        self.instance = models.Instance.get(self.client, 'an-instance')
 
     def test_get(self):
         """Return a specific snapshot."""
-        snapshot = self.container.snapshots.get('an-snapshot')
+        snapshot = self.instance.snapshots.get('an-snapshot')
 
         self.assertEqual('an-snapshot', snapshot.name)
 
     def test_all(self):
         """Return all snapshots."""
-        snapshots = self.container.snapshots.all()
+        snapshots = self.instance.snapshots.all()
 
         self.assertEqual(1, len(snapshots))
         self.assertEqual('an-snapshot', snapshots[0].name)
         self.assertEqual(self.client, snapshots[0].client)
-        self.assertEqual(self.container, snapshots[0].container)
+        self.assertEqual(self.instance, snapshots[0].instance)
 
     def test_create(self):
         """Create a snapshot."""
-        snapshot = self.container.snapshots.create(
+        snapshot = self.instance.snapshots.create(
             'an-snapshot', stateful=True, wait=True)
 
         self.assertEqual('an-snapshot', snapshot.name)
@@ -497,12 +497,12 @@ class TestSnapshot(testing.PyLXDTestCase):
 
     def setUp(self):
         super(TestSnapshot, self).setUp()
-        self.container = models.Container.get(self.client, 'an-container')
+        self.instance = models.Instance.get(self.client, 'an-instance')
 
     def test_rename(self):
         """A snapshot is renamed."""
         snapshot = models.Snapshot(
-            self.client, container=self.container,
+            self.client, instance=self.instance,
             name='an-snapshot')
 
         snapshot.rename('an-renamed-snapshot', wait=True)
@@ -512,7 +512,7 @@ def test_rename(self):
     def test_delete(self):
         """A snapshot is deleted."""
         snapshot = models.Snapshot(
-            self.client, container=self.container,
+            self.client, instance=self.instance,
             name='an-snapshot')
 
         snapshot.delete(wait=True)
@@ -530,12 +530,12 @@ def not_found(request, context):
         self.add_rule({
             'text': not_found,
             'method': 'DELETE',
-            'url': (r'^http://pylxd.test/1.0/containers/'
-                    'an-container/snapshots/an-snapshot$')
+            'url': (r'^http://pylxd.test/1.0/instances/'
+                    'an-instance/snapshots/an-snapshot$')
         })
 
         snapshot = models.Snapshot(
-            self.client, container=self.container,
+            self.client, instance=self.instance,
             name='an-snapshot')
 
         self.assertRaises(exceptions.LXDAPIException, snapshot.delete)
@@ -558,7 +558,7 @@ def test_publish(self):
         })
 
         snapshot = models.Snapshot(
-            self.client, container=self.container,
+            self.client, instance=self.instance,
             name='an-snapshot')
 
         image = snapshot.publish(wait=True)
@@ -570,24 +570,24 @@ def test_publish(self):
     def test_restore_snapshot(self):
         """Snapshots can be restored from the snapshot object"""
         snapshot = models.Snapshot(
-            self.client, container=self.container,
+            self.client, instance=self.instance,
             name='an-snapshot')
         snapshot.restore(wait=True)
 
 
 class TestFiles(testing.PyLXDTestCase):
-    """Tests for pylxd.models.Container.files."""
+    """Tests for pylxd.models.Instance.files."""
 
     def setUp(self):
         super(TestFiles, self).setUp()
-        self.container = models.Container.get(self.client, 'an-container')
+        self.instance = models.Instance.get(self.client, 'an-instance')
 
     def test_put_delete(self):
-        """A file is put on the container and then deleted"""
+        """A file is put on the instance and then deleted"""
         # we are mocked, so delete should initially not be available
-        self.assertEqual(False, self.container.files.delete_available())
+        self.assertEqual(False, self.instance.files.delete_available())
         self.assertRaises(exceptions.LXDAPIExtensionNotAvailable,
-                          self.container.files.delete, '/some/file')
+                          self.instance.files.delete, '/some/file')
         # Now insert delete
         self.add_rule({
             'text': json.dumps({
@@ -605,15 +605,15 @@ def test_put_delete(self):
         # Update hostinfo
         self.client.host_info = self.client.api.get().json()['metadata']
 
-        self.assertEqual(True, self.container.files.delete_available())
+        self.assertEqual(True, self.instance.files.delete_available())
 
         # mock out the delete rule:
         self.add_rule({
             'method': 'DELETE',
-            'url': (r'^http://pylxd.test/1.0/containers/an-container/files'
+            'url': (r'^http://pylxd.test/1.0/instances/an-instance/files'
                     r'\?path=%2Fsome%2Ffile$')
         })
-        self.container.files.delete('/some/file')
+        self.instance.files.delete('/some/file')
 
         # now check that an error (non 200) causes an exception
         def responder(request, context):
@@ -622,11 +622,11 @@ def responder(request, context):
         self.add_rule({
             'text': responder,
             'method': 'DELETE',
-            'url': (r'^http://pylxd.test/1.0/containers/an-container/files'
+            'url': (r'^http://pylxd.test/1.0/instances/an-instance/files'
                     r'\?path=%2Fsome%2Ffile%2Fnot%2Ffound$')
         })
         with self.assertRaises(exceptions.LXDAPIException):
-            self.container.files.delete('/some/file/not/found')
+            self.instance.files.delete('/some/file/not/found')
 
     def test_put_mode_uid_gid(self):
         """Should be able to set the mode, uid and gid of a file"""
@@ -640,28 +640,28 @@ def capture(request, context):
         self.add_rule({
             'text': capture,
             'method': 'POST',
-            'url': (r'^http://pylxd.test/1.0/containers/an-container/files'
+            'url': (r'^http://pylxd.test/1.0/instances/an-instance/files'
                     r'\?path=%2Ftmp%2Fputted$'),
         })
 
         data = 'The quick brown fox'
         # start with an octal mode
-        self.container.files.put('/tmp/putted', data, mode=0o123, uid=1, gid=2)
+        self.instance.files.put('/tmp/putted', data, mode=0o123, uid=1, gid=2)
         headers = _capture['headers']
         self.assertEqual(headers['X-LXD-mode'], '0123')
         self.assertEqual(headers['X-LXD-uid'], '1')
         self.assertEqual(headers['X-LXD-gid'], '2')
         # use a str mode this type
-        self.container.files.put('/tmp/putted', data, mode='456')
+        self.instance.files.put('/tmp/putted', data, mode='456')
         headers = _capture['headers']
         self.assertEqual(headers['X-LXD-mode'], '0456')
         # check that mode='0644' also works (i.e. already has 0 prefix)
-        self.container.files.put('/tmp/putted', data, mode='0644')
+        self.instance.files.put('/tmp/putted', data, mode='0644')
         headers = _capture['headers']
         self.assertEqual(headers['X-LXD-mode'], '0644')
         # check that assertion is raised
         with self.assertRaises(ValueError):
-            self.container.files.put('/tmp/putted', data, mode=object)
+            self.instance.files.put('/tmp/putted', data, mode=object)
 
     def test_recursive_put(self):
 
@@ -691,8 +691,8 @@ def capture(request, context):
             context.status_code = 200
 
         with tempdir() as _dir:
-            base = (r'^http://pylxd.test/1.0/containers/'
-                    r'an-container/files\?path=')
+            base = (r'^http://pylxd.test/1.0/instances/'
+                    r'an-instance/files\?path=')
             rules = [
                 {
                     'text': capture,
@@ -721,7 +721,7 @@ def capture(request, context):
             create_file(_dir, 'file1', "This is file1")
             create_file(_dir, 'dir/file2', "This is file2")
 
-            self.container.files.recursive_put(_dir, './target/')
+            self.instance.files.recursive_put(_dir, './target/')
 
             self.assertEqual(_captures[0]['headers']['X-LXD-type'],
                              'directory')
@@ -731,8 +731,8 @@ def capture(request, context):
             self.assertEqual(_captures[3]['body'], b"This is file2")
 
     def test_get(self):
-        """A file is retrieved from the container."""
-        data = self.container.files.get('/tmp/getted')
+        """A file is retrieved from the instance."""
+        data = self.instance.files.get('/tmp/getted')
 
         self.assertEqual(b'This is a getted file', data)
 
@@ -743,14 +743,14 @@ def not_found(request, context):
         rule = {
             'text': not_found,
             'method': 'GET',
-            'url': (r'^http://pylxd.test/1.0/containers/an-container/files'
+            'url': (r'^http://pylxd.test/1.0/instances/an-instance/files'
                     r'\?path=%2Ftmp%2Fgetted$'),
         }
         self.add_rule(rule)
 
         self.assertRaises(
             exceptions.LXDAPIException,
-            self.container.files.get, '/tmp/getted')
+            self.instance.files.get, '/tmp/getted')
 
     def test_get_error(self):
         """LXDAPIException is raised on error."""
@@ -759,17 +759,17 @@ def not_found(request, context):
         rule = {
             'text': not_found,
             'method': 'GET',
-            'url': (r'^http://pylxd.test/1.0/containers/an-container/files'
+            'url': (r'^http://pylxd.test/1.0/instances/an-instance/files'
                     r'\?path=%2Ftmp%2Fgetted$'),
         }
         self.add_rule(rule)
 
         self.assertRaises(
             exceptions.LXDAPIException,
-            self.container.files.get, '/tmp/getted')
+            self.instance.files.get, '/tmp/getted')
 
     # for bug/281 -- getting an empty json file is interpreted as an API
     # get rather than a raw get.
     def test_get_json_file(self):
-        data = self.container.files.get('/tmp/json-get')
+        data = self.instance.files.get('/tmp/json-get')
         self.assertEqual(b'{"some": "value"}', data)
diff --git a/pylxd/tests/models/test_storage.py b/pylxd/tests/models/test_storage.py
index 054af364..ac2a4131 100644
--- a/pylxd/tests/models/test_storage.py
+++ b/pylxd/tests/models/test_storage.py
@@ -189,7 +189,7 @@ def test_all(self):
 
         # assert that we decoded stuff reasonably well.
         self.assertEqual(len(volumes), 6)
-        self.assertEqual(volumes[0].type, 'container')
+        self.assertEqual(volumes[0].type, 'instance')
         self.assertEqual(volumes[0].name, 'c1')
         self.assertEqual(volumes[3].type, 'image')
         self.assertEqual(volumes[3].name, 'i1')


More information about the lxc-devel mailing list