Exemple #1
0
class ClusterMember(model.Model):
    """A LXD cluster member."""

    url = model.Attribute(readonly=True)
    database = model.Attribute(readonly=True)
    server_name = model.Attribute(readonly=True)
    status = model.Attribute(readonly=True)
    message = model.Attribute(readonly=True)

    cluster = model.Parent()

    @classmethod
    def get(cls, client, server_name):
        """Get a cluster member by name."""
        response = client.api.cluster.members[server_name].get()

        return cls(client, **response.json()["metadata"])

    @classmethod
    def all(cls, client, *args):
        """Get all cluster members."""
        response = client.api.cluster.members.get()

        nodes = []
        for node in response.json()["metadata"]:
            server_name = node.split("/")[-1]
            nodes.append(cls(client, server_name=server_name))
        return nodes

    @property
    def api(self):
        return self.client.api.cluster.members[self.server_name]
Exemple #2
0
class StorageResources(model.Model):
    """An LXD Storage Resources model.

    This corresponds to the LXD endpoing at
    /1.0/storage-pools/<pool>/resources

    At present, this is read-only model.

    api_extension: 'resources'
    """
    space = model.Attribute(readonly=True)
    inodes = model.Attribute(readonly=True)

    @classmethod
    def get(cls, storage_pool):
        """Get a storage_pool resource for a named pool

        Implements GET /1.0/storage-pools/<pool>/resources

        Needs the 'resources' api extension in the LXD server.

        :param storage_pool: a storage pool object on which to fetch resources
        :type storage_pool: :class:`pylxd.models.storage_pool.StoragePool`
        :returns: A storage resources object
        :rtype: :class:`pylxd.models.storage_pool.StorageResources`
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'resources' api extension is missing.
        """
        storage_pool.client.assert_has_api_extension('resources')
        response = storage_pool.api.resources.get()
        resources = cls(storage_pool.client, **response.json()['metadata'])
        return resources
Exemple #3
0
class Project(model.Model):
    """A LXD project."""

    name = model.Attribute(readonly=True)
    config = model.Attribute()
    description = model.Attribute()
    used_by = model.Attribute(readonly=True)

    @classmethod
    def exists(cls, client, name):
        """Determine whether a project exists."""
        try:
            client.projects.get(name)
            return True
        except cls.NotFound:
            return False

    @classmethod
    def get(cls, client, name):
        """Get a project."""
        response = client.api.projects[name].get()
        return cls(client, **response.json()["metadata"])

    @classmethod
    def all(cls, client):
        """Get all projects."""
        response = client.api.projects.get()

        projects = []
        for url in response.json()["metadata"]:
            name = url.split("/")[-1]
            projects.append(cls(client, name=name))
        return projects

    @classmethod
    def create(
        cls,
        client,
        name,
        description=None,
        config=None,
    ):
        """Create a project."""
        project = {"name": name}
        if config is not None:
            project["config"] = config
        if description is not None:
            project["description"] = description
        client.api.projects.post(json=project)
        return cls.get(client, name)

    @property
    def api(self):
        return self.client.api.projects[self.name]

    def rename(self, new_name):
        """Rename the project."""
        self.api.post(json={"name": new_name})

        return Project.get(self.client, new_name)
Exemple #4
0
class Cluster(model.Model):
    """An LXD Cluster.
    """

    server_name = model.Attribute()
    enabled = model.Attribute()
    member_config = model.Attribute()

    members = model.Manager()

    def __init__(self, *args, **kwargs):
        super(Cluster, self).__init__(*args, **kwargs)
        self.members = managers.ClusterMemberManager(self.client, self)

    @property
    def api(self):
        return self.client.api.cluster

    @classmethod
    def get(cls, client, *args):
        """Get cluster details"""
        print(args)
        response = client.api.cluster.get()
        print(response.json())
        container = cls(client, **response.json()['metadata'])
        return container
Exemple #5
0
class Item(model.Model):
    """A fake model."""
    name = model.Attribute(readonly=True)
    age = model.Attribute(int)
    data = model.Attribute()

    @property
    def api(self):
        return self.client.api.items[self.name]
Exemple #6
0
class Profile(model.Model):
    """A LXD profile."""

    config = model.Attribute()
    description = model.Attribute()
    devices = model.Attribute()
    name = model.Attribute(readonly=True)
    used_by = model.Attribute(readonly=True)

    @classmethod
    def exists(cls, client, name):
        """Determine whether a profile exists."""
        try:
            client.profiles.get(name)
            return True
        except cls.NotFound:
            return False

    @classmethod
    def get(cls, client, name):
        """Get a profile."""
        response = client.api.profiles[name].get()
        return cls(client, **response.json()['metadata'])

    @classmethod
    def all(cls, client):
        """Get all profiles."""
        response = client.api.profiles.get()

        profiles = []
        for url in response.json()['metadata']:
            name = url.split('/')[-1]
            profiles.append(cls(client, name=name))
        return profiles

    @classmethod
    def create(cls, client, name, config=None, devices=None):
        """Create a profile."""
        profile = {'name': name}
        if config is not None:
            profile['config'] = config
        if devices is not None:
            profile['devices'] = devices
        client.api.profiles.post(json=profile)
        return cls.get(client, name)

    @property
    def api(self):
        return self.client.api.profiles[self.name]

    def rename(self, new_name):
        """Rename the profile."""
        self.api.post(json={'name': new_name})

        return Profile.get(self.client, new_name)
Exemple #7
0
class Certificate(model.Model):
    """A LXD certificate."""

    certificate = model.Attribute()
    fingerprint = model.Attribute()
    type = model.Attribute()
    name = model.Attribute()

    @classmethod
    def get(cls, client, fingerprint):
        """Get a certificate by fingerprint."""
        response = client.api.certificates[fingerprint].get()

        return cls(client, **response.json()['metadata'])

    @classmethod
    def all(cls, client):
        """Get all certificates."""
        response = client.api.certificates.get()

        certs = []
        for cert in response.json()['metadata']:
            fingerprint = cert.split('/')[-1]
            certs.append(cls(client, fingerprint=fingerprint))
        return certs

    @classmethod
    def create(cls, client, password, cert_data):
        """Create a new certificate."""
        cert = x509.load_pem_x509_certificate(cert_data, default_backend())
        base64_cert = cert.public_bytes(Encoding.PEM).decode('utf-8')
        # STRIP OUT CERT META "-----BEGIN CERTIFICATE-----"
        base64_cert = '\n'.join(base64_cert.split('\n')[1:-2])
        data = {
            'type': 'client',
            'certificate': base64_cert,
            'password': password,
        }
        client.api.certificates.post(json=data)

        # XXX: rockstar (08 Jun 2016) - Please see the open lxd bug here:
        # https://github.com/lxc/lxd/issues/2092
        fingerprint = binascii.hexlify(cert.fingerprint(
            hashes.SHA256())).decode('utf-8')
        return cls.get(client, fingerprint)

    @property
    def api(self):
        return self.client.api.certificates[self.fingerprint]
Exemple #8
0
class ClusterCertificate(model.Model):
    """A LXD cluster certificate"""

    cluster_certificate = model.Attribute()
    cluster_certificate_key = model.Attribute()

    cluster = model.Parent()

    @classmethod
    def put(cls, client, cert, key):
        response = client.api.cluster.certificate.put(
            json={"cluster_certificate": cert, "cluster_certificate_key": key}
        )

        if response.status_code == 200:
            return
        raise LXDAPIException(response)

    @property
    def api(self):
        return self.client.api.cluster.certificate
Exemple #9
0
class StoragePool(model.Model):
    """A LXD storage_pool.

    This corresponds to the LXD endpoint at
    /1.0/storage-pools
    """
    name = model.Attribute()
    driver = model.Attribute()
    description = model.Attribute()
    used_by = model.Attribute()
    config = model.Attribute()
    managed = model.Attribute()

    @classmethod
    def get(cls, client, name):
        """Get a storage_pool by name."""
        response = client.api.storage_pools[name].get()

        storage_pool = cls(client, **response.json()['metadata'])
        return storage_pool

    @classmethod
    def all(cls, client):
        """Get all storage_pools."""
        response = client.api.storage_pools.get()

        storage_pools = []
        for url in response.json()['metadata']:
            name = url.split('/')[-1]
            storage_pools.append(cls(client, name=name))
        return storage_pools

    @classmethod
    def create(cls, client, config):
        """Create a storage_pool from config."""
        client.api.storage_pools.post(json=config)

        storage_pool = cls.get(client, config['name'])
        return storage_pool

    @classmethod
    def exists(cls, client, name):
        """Determine whether a storage pool exists."""
        try:
            client.storage_pools.get(name)
            return True
        except cls.NotFound:
            return False

    @property
    def api(self):
        return self.client.api.storage_pools[self.name]

    def save(self, wait=False):
        """Save is not available for storage_pools."""
        raise NotImplementedError('save is not implemented')

    def delete(self):
        """Delete is not available for storage_pools."""
        raise NotImplementedError('delete is not implemented')
Exemple #10
0
class Network(model.Model):
    """A LXD network."""
    name = model.Attribute()
    type = model.Attribute()
    used_by = model.Attribute()
    config = model.Attribute()
    managed = model.Attribute()

    @classmethod
    def get(cls, client, name):
        """Get a network by name."""
        response = client.api.networks[name].get()

        network = cls(client, **response.json()['metadata'])
        return network

    @classmethod
    def all(cls, client):
        """Get all networks."""
        response = client.api.networks.get()

        networks = []
        for url in response.json()['metadata']:
            name = url.split('/')[-1]
            networks.append(cls(client, name=name))
        return networks

    @property
    def api(self):
        return self.client.api.networks[self.name]

    def save(self, wait=False):
        """Save is not available for networks."""
        raise NotImplementedError('save is not implemented')

    def delete(self):
        """Delete is not available for networks."""
        raise NotImplementedError('delete is not implemented')
Exemple #11
0
class StoragePool(model.Model):
    """An LXD storage_pool.

    This corresponds to the LXD endpoint at
    /1.0/storage-pools

    api_extension: 'storage'
    """
    name = model.Attribute(readonly=True)
    driver = model.Attribute(readonly=True)
    used_by = model.Attribute(readonly=True)
    config = model.Attribute()
    managed = model.Attribute(readonly=True)
    description = model.Attribute()
    status = model.Attribute(readonly=True)
    locations = model.Attribute(readonly=True)

    resources = model.Manager()
    volumes = model.Manager()

    def __init__(self, *args, **kwargs):
        super(StoragePool, self).__init__(*args, **kwargs)

        self.resources = StorageResourcesManager(self)
        self.volumes = StorageVolumeManager(self)

    @classmethod
    def get(cls, client, name):
        """Get a storage_pool by name.

        Implements GET /1.0/storage-pools/<name>

        :param client: The pylxd client object
        :type client: :class:`pylxd.client.Client`
        :param name: the name of the storage pool to get
        :type name: str
        :returns: a storage pool if successful, raises NotFound if not found
        :rtype: :class:`pylxd.models.storage_pool.StoragePool`
        :raises: :class:`pylxd.exceptions.NotFound`
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        """
        client.assert_has_api_extension('storage')
        response = client.api.storage_pools[name].get()

        storage_pool = cls(client, **response.json()['metadata'])
        return storage_pool

    @classmethod
    def all(cls, client):
        """Get all storage_pools.

        Implements GET /1.0/storage-pools

        Note that the returned list is 'sparse' in that only the name of the
        pool is populated.  If any of the attributes are used, then the `sync`
        function is called to populate the object fully.

        :param client: The pylxd client object
        :type client: :class:`pylxd.client.Client`
        :returns: a storage pool if successful, raises NotFound if not found
        :rtype: [:class:`pylxd.models.storage_pool.StoragePool`]
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        """
        client.assert_has_api_extension('storage')
        response = client.api.storage_pools.get()

        storage_pools = []
        for url in response.json()['metadata']:
            name = url.split('/')[-1]
            storage_pools.append(cls(client, name=name))
        return storage_pools

    @classmethod
    def create(cls, client, definition):
        """Create a storage_pool from config.

        Implements POST /1.0/storage-pools

        The `definition` parameter defines what the storage pool will be.  An
        example config for the zfs driver is:

            {
                "config": {
                    "size": "10GB"
                },
                "driver": "zfs",
                "name": "pool1"
            }

        Note that **all** fields in the `definition` parameter are strings.

        For further details on the storage pool types see:
        https://lxd.readthedocs.io/en/latest/storage/

        The function returns the a `StoragePool` instance, if it is
        successfully created, otherwise an Exception is raised.

        :param client: The pylxd client object
        :type client: :class:`pylxd.client.Client`
        :param definition: the fields to pass to the LXD API endpoint
        :type definition: dict
        :returns: a storage pool if successful, raises NotFound if not found
        :rtype: :class:`pylxd.models.storage_pool.StoragePool`
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage pool
            couldn't be created.
        """
        client.assert_has_api_extension('storage')
        client.api.storage_pools.post(json=definition)

        storage_pool = cls.get(client, definition['name'])
        return storage_pool

    @classmethod
    def exists(cls, client, name):
        """Determine whether a storage pool exists.

        A convenience method to determine a pool exists.  However, it is better
        to try to fetch it and catch the :class:`pylxd.exceptions.NotFound`
        exception, as otherwise the calling code is like to fetch the pool
        twice.  Only use this if the calling code doesn't *need* the actual
        storage pool information.

        :param client: The pylxd client object
        :type client: :class:`pylxd.client.Client`
        :param name: the name of the storage pool to get
        :type name: str
        :returns: True if the storage pool exists, False if it doesn't.
        :rtype: bool
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        """
        try:
            cls.get(client, name)
            return True
        except cls.NotFound:
            return False

    @property
    def api(self):
        """Provides an object with the endpoint:

        /1.0/storage-pools/<self.name>

        Used internally to construct endpoints.

        :returns: an API node with the named endpoint
        :rtype: :class:`pylxd.client._APINode`
        """
        return self.client.api.storage_pools[self.name]

    def save(self, wait=False):
        """Save the model using PUT back to the LXD server.

        Implements PUT /1.0/storage-pools/<self.name> *automagically*

        The fields affected are: `description` and `config`.  Note that they
        are replaced in their *entirety*.  If finer grained control is
        required, please use the
        :meth:`~pylxd.models.storage_pool.StoragePool.patch` method directly.

        Updating a storage pool may fail if the config is not acceptable to
        LXD. An :class:`~pylxd.exceptions.LXDAPIException` will be generated in
        that case.

        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage pool
            can't be deleted.
        """
        # Note this method exists so that it is documented via sphinx.
        super(StoragePool, self).save(wait=wait)

    def delete(self):
        """Delete the storage pool.

        Implements DELETE /1.0/storage-pools/<self.name>

        Deleting a storage pool may fail if it is being used.  See the LXD
        documentation for further details.

        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage pool
            can't be deleted.
        """
        # Note this method exists so that it is documented via sphinx.
        super(StoragePool, self).delete()

    def put(self, put_object, wait=False):
        """Put the storage pool.

        Implements PUT /1.0/storage-pools/<self.name>

        Putting to a storage pool may fail if the new configuration is
        incompatible with the pool.  See the LXD documentation for further
        details.

        Note that the object is refreshed with a `sync` if the PUT is
        successful.  If this is *not* desired, then the raw API on the client
        should be used.

        :param put_object: A dictionary.  The most useful key will be the
            `config` key.
        :type put_object: dict
        :param wait: Whether to wait for async operations to complete.
        :type wait: bool
        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage pool
            can't be modified.
        """
        # Note this method exists so that it is documented via sphinx.
        super(StoragePool, self).put(put_object, wait)

    def patch(self, patch_object, wait=False):
        """Patch the storage pool.

        Implements PATCH /1.0/storage-pools/<self.name>

        Patching the object allows for more fine grained changes to the config.
        The object is refreshed if the PATCH is successful.  If this is *not*
        required, then use the client api directly.

        :param patch_object: A dictionary.  The most useful key will be the
            `config` key.
        :type patch_object: dict
        :param wait: Whether to wait for async operations to complete.
        :type wait: bool
        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage pool
            can't be modified.
        """
        # Note this method exists so that it is documented via sphinx.
        super(StoragePool, self).patch(patch_object, wait)
Exemple #12
0
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={},
                encoding=None,
                decode=True,
                stdin_payload=None,
                stdin_encoding="utf-8",
                stdout_handler=None,
                stderr_handler=None):
        """Execute a command on the container.

        In pylxd 2.2, this method will be renamed `execute` and the existing
        `execute` method removed.

        :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 recived via stdout
        :type stdout_handler: Callable[[str], None]
        :param stderr_handler: Callable than receive as first parameter each
            message recived via stderr
        :type stderr_handler: Callable[[str], None]
        :raises ValueError: if the ws4py library is not installed.
        :returns: The return value, stdout and stdin
        :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.")
        response = self.api['exec'].post(
            json={
                'command': commands,
                'environment': environment,
                'wait-for-websocket': True,
                'interactive': False,
            })

        fds = response.json()['metadata']['metadata']['fds']
        operation_id = response.json()['operation'].split('/')[-1]
        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

            while len(manager.websockets.values()) > 0:
                time.sleep(.1)  # pragma: no cover

            manager.stop()
            manager.join()

            return _ContainerExecuteResult(operation.metadata['return'],
                                           stdout.data, stderr.data)

    def migrate(self, new_client, wait=False):
        """Migrate a container.

        Destination host information is contained in the client
        connection passed in.

        If the container is running, it either must be shut down
        first or criu must be installed on the source and destination
        machines.
        """
        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(), 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(),
                                               wait=wait)
        self.delete()
        return res

    def generate_migration_data(self):
        """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`.
        """
        self.sync()  # Make sure the object isn't stale
        response = self.api.post(json={'migration': True})
        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'])
Exemple #13
0
class StorageVolume(model.Model):
    """An LXD Storage volume.

    This corresponds to the LXD endpoing at
    /1.0/storage-pools/<pool>/volumes

    api_extension: 'storage'
    """
    name = model.Attribute(readonly=True)
    type = model.Attribute(readonly=True)
    description = model.Attribute(readonly=True)
    config = model.Attribute()
    used_by = model.Attribute(readonly=True)
    location = model.Attribute(readonly=True)

    storage_pool = model.Parent()

    @property
    def api(self):
        """Provides an object with the endpoint:

        /1.0/storage-pools/<storage_pool.name>/volumes/<self.type>/<self.name>

        Used internally to construct endpoints.

        :returns: an API node with the named endpoint
        :rtype: :class:`pylxd.client._APINode`
        """
        return self.storage_pool.api.volumes[self.type][self.name]

    @classmethod
    def all(cls, storage_pool):
        """Get all the volumnes for this storage pool.

        Implements GET /1.0/storage-pools/<name>/volumes

        Volumes returned from this method will only have the name
        set, as that is the only property returned from LXD. If more
        information is needed, `StorageVolume.sync` is the method call
        that should be used.

        Note that the storage volume types are 'container', 'image' and
        'custom', and these maps to the names 'containers', 'images' and
        everything else is mapped to 'custom'.

        :param storage_pool: a storage pool object on which to fetch resources
        :type storage_pool: :class:`pylxd.models.storage_pool.StoragePool`
        :returns: a list storage volume if successful
        :rtype: [:class:`pylxd.models.storage_pool.StorageVolume`]
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        """
        storage_pool.client.assert_has_api_extension('storage')
        response = storage_pool.api.volumes.get()

        volumes = []
        for volume in response.json()['metadata']:
            (_type, name) = volume.split('/')[-2:]
            # for each type, convert to the string that will work with GET
            if _type == 'containers':
                _type = 'container'
            elif _type == 'images':
                _type = 'image'
            else:
                _type = 'custom'
            volumes.append(
                cls(storage_pool.client,
                    name=name,
                    type=_type,
                    storage_pool=storage_pool))
        return volumes

    @classmethod
    def get(cls, storage_pool, _type, name):
        """Get a StorageVolume by type and name.

        Implements GET /1.0/storage-pools/<pool>/volumes/<type>/<name>

        The `_type` param can only (currently) be one of 'container', 'image'
        or 'custom'.  This was determined by read the LXD source.

        :param storage_pool: a storage pool object on which to fetch resources
        :type storage_pool: :class:`pylxd.models.storage_pool.StoragePool`
        :param _type: the type; one of 'container', 'image', 'custom'
        :type _type: str
        :param name: the name of the storage volume to get
        :type name: str
        :returns: a storage pool if successful, raises NotFound if not found
        :rtype: :class:`pylxd.models.storage_pool.StorageVolume`
        :raises: :class:`pylxd.exceptions.NotFound`
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        """
        storage_pool.client.assert_has_api_extension('storage')
        response = storage_pool.api.volumes[_type][name].get()

        volume = cls(storage_pool.client,
                     storage_pool=storage_pool,
                     **response.json()['metadata'])
        return volume

    @classmethod
    # def create(cls, storage_pool, definition, wait=True, *args):
    def create(cls, storage_pool, *args, **kwargs):
        """Create a 'custom' Storage Volume in the associated storage pool.

        Implements POST /1.0/storage-pools/<pool>/volumes/custom

        See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-19 for
        more details on what the `definition` parameter dictionary should
        contain for various volume creation.

        At the moment the only type of volume that can be created is 'custom',
        and this is currently hardwired into the function.

        The function signature 'hides' that the first parameter to the function
        is the definition.  The function should be called as:

        >>> a_storage_pool.volumes.create(definition_dict, wait=<bool>)

        where `definition_dict` is mandatory, and `wait` defaults to True,
        which makes the default a synchronous function call.

        Note that **all** fields in the `definition` parameter are strings.

        If the caller doesn't wan't to wait for an async operation, then it
        MUST be passed as a keyword argument, and not as a positional
        substitute.

        The function returns the a
        :class:`~pylxd.models.storage_pool.StoragePool` instance, if it is
        successfully created, otherwise an Exception is raised.

        :param storage_pool: a storage pool object on which to fetch resources
        :type storage_pool: :class:`pylxd.models.storage_pool.StoragePool`
        :param definition: the fields to pass to the LXD API endpoint
        :type definition: dict
        :param wait: wait until an async action has completed (default True)
        :type wait: bool
        :returns: a storage pool volume if successful, raises NotFound if not
            found
        :rtype: :class:`pylxd.models.storage_pool.StorageVolume`
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage pool
            volume couldn't be created.
        """
        # This is really awkward, but the implementation details mean,
        # depending on how this function is called, we can't know whether the
        # 2nd positional argument will be definition or a client object. This
        # is an difficulty with how BaseManager is implemented, and to get the
        # convenience of being able to call this 'naturally' off of a
        # storage_pool.  So we have to jump through some hurdles to get the
        # right positional parameters.
        storage_pool.client.assert_has_api_extension('storage')
        wait = kwargs.get('wait', True)
        definition = args[-1]
        assert isinstance(definition, dict)
        assert 'name' in definition
        response = storage_pool.api.volumes.custom.post(json=definition)

        if response.json()['type'] == 'async' and wait:
            storage_pool.client.operations.wait_for_operation(
                response.json()['operation'])

        volume = cls.get(storage_pool, 'custom', definition['name'])
        return volume

    def rename(self, _input, wait=False):
        """Rename a storage volume

        This requires api_extension: 'storage_api_volume_rename'.

        Implements: POST /1.0/storage-pools/<pool>/volumes/<type>/<name>

        This operation is either sync or async (when moving to a different
        pool).

        This command also allows the migration across instances, and therefore
        it returns the metadata section (as a dictionary) from the call.  The
        caller should assume that the object is stale and refetch it after any
        migrations are done.  However, the 'name' attribute of the object is
        updated for consistency.

        Unlike :meth:`~pylxd.models.storage_pool.StorageVolume.create`, this
        method does not override any items in the input definition, although it
        does check that the 'name' and 'pool' parameters are set.

        Please see: https://github.com/lxc/lxd/blob/master/doc/rest-api.md
        #10storage-poolspoolvolumestypename
        for more details.

        :param _input: The `input` specification for the rename.
        :type _input: dict
        :param wait: Wait for an async operation, if it is async.
        :type wait: bool
        :returns:  The dictionary from the metadata section of the response if
            successful.
        :rtype: dict[str, str]
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage_api_volume_rename' api extension is missing.
        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage pool
            volume couldn't be renamed.
        """
        assert isinstance(_input, dict)
        assert 'name' in _input
        assert 'pool' in _input
        response = self.api.post(json=_input)

        response_json = response.json()
        if wait:
            self.client.operations.wait_for_operation(
                response_json['operation'])
        self.name = _input['name']
        return response_json['metadata']

    def put(self, put_object, wait=False):
        """Put the storage volume.

        Implements: PUT /1.0/storage-pools/<pool>/volumes/<type>/<name>

        Note that this is functionality equivalent to
        :meth:`~pyxld.models.storage_pool.StorageVolume.save` but by using a
        new object (`put_object`) rather than modifying the object and then
        calling :meth:`~pyxld.models.storage_pool.StorageVolume.save`.

        Putting to a storage volume may fail if the new configuration is
        incompatible with the pool.  See the LXD documentation for further
        details.

        Note that the object is refreshed with a `sync` if the PUT is
        successful.  If this is *not* desired, then the raw API on the client
        should be used.

        :param put_object: A dictionary.  The most useful key will be the
            `config` key.
        :type put_object: dict
        :param wait: Whether to wait for async operations to complete.
        :type wait: bool
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage pool
            can't be modified.
        """
        # Note this method exists so that it is documented via sphinx.
        super(StorageVolume, self).put(put_object, wait)

    def patch(self, patch_object, wait=False):
        """Patch the storage volume.

        Implements: PATCH /1.0/storage-pools/<pool>/volumes/<type>/<name>

        Patching the object allows for more fine grained changes to the config.
        The object is refreshed if the PATCH is successful.  If this is *not*
        required, then use the client api directly.

        :param patch_object: A dictionary.  The most useful key will be the
            `config` key.
        :type patch_object: dict
        :param wait: Whether to wait for async operations to complete.
        :type wait: bool
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage
            volume can't be modified.
        """
        # Note this method exists so that it is documented via sphinx.
        super(StorageVolume, self).patch(patch_object, wait)

    def save(self, wait=False):
        """Save the model using PUT back to the LXD server.

        Implements: PUT /1.0/storage-pools/<pool>/volumes/<type>/<name>
        *automagically*.

        The field affected is `config`.  Note that it is replaced *entirety*.
        If finer grained control is required, please use the
        :meth:`~pylxd.models.storage_pool.StorageVolume.patch` method directly.

        Updating a storage volume may fail if the config is not acceptable to
        LXD. An :class:`~pylxd.exceptions.LXDAPIException` will be generated in
        that case.

        :param wait: Whether to wait for async operations to complete.
        :type wait: bool
        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage
            volume can't be deleted.
        """
        # Note this method exists so that it is documented via sphinx.
        super(StorageVolume, self).save(wait=wait)

    def delete(self):
        """Delete the storage pool.

        Implements: DELETE /1.0/storage-pools/<pool>/volumes/<type>/<name>

        Deleting a storage volume may fail if it is being used.  See the LXD
        documentation for further details.

        :raises: :class:`pylxd.exceptions.LXDAPIExtensionNotAvailable` if the
            'storage' api extension is missing.
        :raises: :class:`pylxd.exceptions.LXDAPIException` if the storage pool
            can't be deleted.
        """
        # Note this method exists so that it is documented via sphinx.
        super(StorageVolume, self).delete()
Exemple #14
0
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)

    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: 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
            :returns: True if the file store succeeded otherwise False.
            :rtype: Bool
            """
            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)
            response = (
                self._client.api.containers[self._container.name].files.post(
                    params={'path': filepath},
                    data=data,
                    headers=headers or None))
            return response.status_code == 200

        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 u'file_delete' in self._client.host_info['api_extensions']

        def delete(self, filepath):
            if self.delete_available():
                response = self._client.api.containers[
                    self._container.name].files.delete(
                        params={'path': filepath})
                return response.status_code == 200
            else:
                raise ValueError(
                    'File Deletion is not available for this host')

        def get(self, filepath):
            response = (
                self._client.api.containers[self._container.name].files.get(
                    params={'path': filepath}))
            return response.content

    @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):
        """Create a new container config."""
        response = client.api.containers.post(json=config)

        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={}):
        """Execute a command on the container.

        In pylxd 2.2, this method will be renamed `execute` and the existing
        `execute` method removed.
        """
        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.")
        response = self.api['exec'].post(
            json={
                'command': commands,
                'environment': environment,
                'wait-for-websocket': True,
                'interactive': False,
            })

        fds = response.json()['metadata']['metadata']['fds']
        operation_id = response.json()['operation'].split('/')[-1]
        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)
            stdin.resource = '{}?secret={}'.format(parsed.path, fds['0'])
            stdin.connect()
            stdout = _CommandWebsocketClient(manager,
                                             self.client.websocket_url)
            stdout.resource = '{}?secret={}'.format(parsed.path, fds['1'])
            stdout.connect()
            stderr = _CommandWebsocketClient(manager,
                                             self.client.websocket_url)
            stderr.resource = '{}?secret={}'.format(parsed.path, fds['2'])
            stderr.connect()

            manager.start()

            while len(manager.websockets.values()) > 0:
                time.sleep(.1)

            operation = self.client.operations.get(operation_id)

            return _ContainerExecuteResult(operation.metadata['return'],
                                           stdout.data, stderr.data)

    def migrate(self, new_client, wait=False):
        """Migrate a container.

        Destination host information is contained in the client
        connection passed in.

        If the container is running, it either must be shut down
        first or criu must be installed on the source and destination
        machines.
        """
        if self.api.scheme in ('http+unix', ):
            raise ValueError('Cannot migrate from a local client connection')

        return new_client.containers.create(self.generate_migration_data(),
                                            wait=wait)

    def generate_migration_data(self):
        """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`.
        """
        self.sync()  # Make sure the object isn't stale
        response = self.api.post(json={'migration': True})
        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'])
Exemple #15
0
class Image(model.Model):
    """A LXD Image."""
    aliases = model.Attribute(readonly=True)
    auto_update = model.Attribute(optional=True)
    architecture = model.Attribute(readonly=True)
    cached = model.Attribute(readonly=True)
    created_at = model.Attribute(readonly=True)
    expires_at = model.Attribute(readonly=True)
    filename = model.Attribute(readonly=True)
    fingerprint = model.Attribute(readonly=True)
    last_used_at = model.Attribute(readonly=True)
    properties = model.Attribute()
    public = model.Attribute()
    size = model.Attribute(readonly=True)
    uploaded_at = model.Attribute(readonly=True)
    update_source = model.Attribute(readonly=True)

    @property
    def api(self):
        return self.client.api.images[self.fingerprint]

    @classmethod
    def get(cls, client, fingerprint):
        """Get an image."""
        response = client.api.images[fingerprint].get()

        image = cls(client, **response.json()['metadata'])
        return image

    @classmethod
    def get_by_alias(cls, client, alias):
        """Get an image by its alias."""
        response = client.api.images.aliases[alias].get()

        fingerprint = response.json()['metadata']['target']
        return cls.get(client, fingerprint)

    @classmethod
    def all(cls, client):
        """Get all images."""
        response = client.api.images.get()

        images = []
        for url in response.json()['metadata']:
            fingerprint = url.split('/')[-1]
            images.append(cls(client, fingerprint=fingerprint))
        return images

    @classmethod
    def create(cls, client, image_data, public=False, wait=False):
        """Create an image."""
        fingerprint = hashlib.sha256(image_data).hexdigest()

        headers = {}
        if public:
            headers['X-LXD-Public'] = '1'
        response = client.api.images.post(data=image_data, headers=headers)

        if wait:
            Operation.wait_for_operation(client, response.json()['operation'])
        return cls(client, fingerprint=fingerprint)

    @classmethod
    def create_from_simplestreams(cls,
                                  client,
                                  server,
                                  alias,
                                  public=False,
                                  auto_update=False):
        """Copy an image from simplestreams."""
        config = {
            'public': public,
            'auto_update': auto_update,
            'source': {
                'type': 'image',
                'mode': 'pull',
                'server': server,
                'protocol': 'simplestreams',
                'fingerprint': alias
            }
        }

        op = _image_create_from_config(client, config, wait=True)

        return client.images.get(op.metadata['fingerprint'])

    @classmethod
    def create_from_url(cls, client, url, public=False, auto_update=False):
        """Copy an image from an url."""
        config = {
            'public': public,
            'auto_update': auto_update,
            'source': {
                'type': 'url',
                'mode': 'pull',
                'url': url
            }
        }

        op = _image_create_from_config(client, config, wait=True)

        return client.images.get(op.metadata['fingerprint'])

    def export(self):
        """Export the image.

        Because the image itself may be quite large, we stream the download
        in 1kb chunks, and write it to a temporary file on disk. Once that
        file is closed, it is deleted from the disk.
        """
        on_disk = tempfile.TemporaryFile()
        with contextlib.closing(self.api.export.get(stream=True)) as response:
            for chunk in response.iter_content(chunk_size=1024):
                on_disk.write(chunk)
        on_disk.seek(0)
        return on_disk

    def add_alias(self, name, description):
        """Add an alias to the image."""
        self.client.api.images.aliases.post(json={
            'description': description,
            'target': self.fingerprint,
            'name': name
        })

        # Update current aliases list
        self.aliases.append({
            'description': description,
            'target': self.fingerprint,
            'name': name
        })

    def delete_alias(self, name):
        """Delete an alias from the image."""
        self.client.api.images.aliases[name].delete()

        # Update current aliases list
        la = [a['name'] for a in self.aliases]
        try:
            del self.aliases[la.index(name)]
        except ValueError:
            pass

    def copy(self, new_client, public=None, auto_update=None, wait=False):
        """Copy an image to a another LXD.

        Destination host information is contained in the client
        connection passed in.
        """
        self.sync()  # Make sure the object isn't stale

        url = '/'.join(self.client.api._api_endpoint.split('/')[:-1])

        if public is None:
            public = self.public

        if auto_update is None:
            auto_update = self.auto_update

        config = {
            'filename': self.filename,
            'public': public,
            'auto_update': auto_update,
            'properties': self.properties,
            'source': {
                'type': 'image',
                'mode': 'pull',
                'server': url,
                'protocol': 'lxd',
                'fingerprint': self.fingerprint
            }
        }

        if self.public is not True:
            response = self.api.secret.post(json={})
            secret = response.json()['metadata']['metadata']['secret']
            config['source']['secret'] = secret
            cert = self.client.host_info['environment']['certificate']
            config['source']['certificate'] = cert

        _image_create_from_config(new_client, config, wait)

        if wait:
            return new_client.images.get(self.fingerprint)
Exemple #16
0
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)
Exemple #17
0
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[self.instance._endpoint][
            self.instance.name].snapshots[self.name]

    @classmethod
    def get(cls, client, instance, name):
        response = client.api[instance._endpoint][
            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[instance._endpoint][
            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[instance._endpoint][
            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)
Exemple #18
0
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 self.client.api[self._endpoint][self.name]

    class FilesManager:
        """A pseudo-manager for namespacing file operations."""
        def __init__(self, instance):
            self._instance = instance
            self._endpoint = 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)

        def mk_dir(self, path, mode=None, uid=None, gid=None):
            """Creates an empty directory on the container.
            This pushes an empty directory to the containers file system
            named by the `filepath`.
            :param path: The path in the container to to store the data in.
            :type path: 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)
            headers["X-LXD-type"] = "directory"
            response = self._endpoint.post(params={"path": path},
                                           headers=headers)
            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, str):
                    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)

        def recursive_get(self, remote_path, local_path):
            """Recursively pulls a directory from the container.
            Pulls the directory named `remote_path` from the container and
            creates a local folder named `local_path` with the
            content of `remote_path`.
            If `remote_path` is a file, it will be copied to `local_path`.
            :param remote_path: The directory path on the container.
            :type remote_path: str
            :param local_path: The path at which the directory will be stored.
            :type local_path: str
            :return:
            :raises: LXDAPIException if an error occurs
            """
            response = self._endpoint.get(params={"path": remote_path},
                                          is_api=False)

            if "X-LXD-type" in response.headers:
                if response.headers["X-LXD-type"] == "directory":
                    # TODO: We considered using the X-LXD-uid, X-LXD-gid,
                    #       and X-LXD-mode header information, but it was
                    #       beyond the scope of this change.
                    os.mkdir(local_path)
                    content = json.loads(response.content)
                    if "metadata" in content and content["metadata"]:
                        for file in content["metadata"]:
                            self.recursive_get(
                                os.path.join(remote_path, file),
                                os.path.join(local_path, file),
                            )
                elif response.headers["X-LXD-type"] == "file":
                    with open(local_path, "wb") as f:
                        # TODO: Same thoughts on file permissions as above.
                        f.write(response.content)

    @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 = 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 = 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 = 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().__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__:
                self.__dirty__.remove("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, str):
            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(0.5)  # pragma: no cover

            try:
                stdin.close()
            except BrokenPipeError:
                pass

            stdout.finish_soon()
            stderr.finish_soon()
            try:
                manager.close_all()
            except BrokenPipeError:
                pass

            while not stdout.finished or not stderr.finished:
                time.sleep(0.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, str):
            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": self.type,
                "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
Exemple #19
0
class Image(model.Model):
    """A LXD Image."""

    aliases = model.Attribute(readonly=True)
    auto_update = model.Attribute(optional=True)
    architecture = model.Attribute(readonly=True)
    cached = model.Attribute(readonly=True)
    created_at = model.Attribute(readonly=True)
    expires_at = model.Attribute(readonly=True)
    filename = model.Attribute(readonly=True)
    fingerprint = model.Attribute(readonly=True)
    last_used_at = model.Attribute(readonly=True)
    properties = model.Attribute()
    public = model.Attribute()
    size = model.Attribute(readonly=True)
    uploaded_at = model.Attribute(readonly=True)
    update_source = model.Attribute(readonly=True)

    @property
    def api(self):
        return self.client.api.images[self.fingerprint]

    @classmethod
    def exists(cls, client, fingerprint, alias=False):
        """Determine whether an image exists.

        If `alias` is True, look up the image by its alias,
        rather than its fingerprint.
        """
        try:
            if alias:
                client.images.get_by_alias(fingerprint)
            else:
                client.images.get(fingerprint)
            return True
        except cls.NotFound:
            return False

    @classmethod
    def get(cls, client, fingerprint):
        """Get an image."""
        response = client.api.images[fingerprint].get()

        image = cls(client, **response.json()["metadata"])
        return image

    @classmethod
    def get_by_alias(cls, client, alias):
        """Get an image by its alias."""
        response = client.api.images.aliases[alias].get()

        fingerprint = response.json()["metadata"]["target"]
        return cls.get(client, fingerprint)

    @classmethod
    def all(cls, client):
        """Get all images."""
        response = client.api.images.get()

        images = []
        for url in response.json()["metadata"]:
            fingerprint = url.split("/")[-1]
            images.append(cls(client, fingerprint=fingerprint))
        return images

    @classmethod
    def create(cls,
               client,
               image_data,
               metadata=None,
               public=False,
               wait=True):
        """Create an image.

        If metadata is provided, a multipart form data request is formed to
        push metadata and image together in a single request. The metadata must
        be a tar achive.

        `wait` parameter is now ignored, as the image fingerprint cannot be
        reliably determined consistently until after the image is indexed.
        """

        if wait is False:  # pragma: no cover
            warnings.warn(
                "Image.create wait parameter ignored and will be removed in "
                "2.3",
                DeprecationWarning,
            )

        headers = {}
        if public:
            headers["X-LXD-Public"] = "1"

        if metadata is not None:
            # Image uploaded as chunked/stream (metadata, rootfs)
            # multipart message.
            # Order of parts is important metadata should be passed first
            files = collections.OrderedDict(
                metadata=("metadata", metadata, "application/octet-stream"),
                rootfs=("rootfs", image_data, "application/octet-stream"),
            )
            data = MultipartEncoder(files)
            headers.update({"Content-Type": data.content_type})
        else:
            data = image_data

        response = client.api.images.post(data=data, headers=headers)
        operation = client.operations.wait_for_operation(
            response.json()["operation"])
        return cls(client, fingerprint=operation.metadata["fingerprint"])

    @classmethod
    def create_from_simplestreams(cls,
                                  client,
                                  server,
                                  alias,
                                  public=False,
                                  auto_update=False):
        """Copy an image from simplestreams."""
        config = {
            "public": public,
            "auto_update": auto_update,
            "source": {
                "type": "image",
                "mode": "pull",
                "server": server,
                "protocol": "simplestreams",
                "fingerprint": alias,
            },
        }

        op = _image_create_from_config(client, config, wait=True)

        return client.images.get(op.metadata["fingerprint"])

    @classmethod
    def create_from_url(cls, client, url, public=False, auto_update=False):
        """Copy an image from an url."""
        config = {
            "public": public,
            "auto_update": auto_update,
            "source": {
                "type": "url",
                "mode": "pull",
                "url": url
            },
        }

        op = _image_create_from_config(client, config, wait=True)

        return client.images.get(op.metadata["fingerprint"])

    def export(self):
        """Export the image.

        Because the image itself may be quite large, we stream the download
        in 1kb chunks, and write it to a temporary file on disk. Once that
        file is closed, it is deleted from the disk.
        """
        on_disk = tempfile.TemporaryFile()
        with contextlib.closing(self.api.export.get(stream=True)) as response:
            for chunk in response.iter_content(chunk_size=1024):
                on_disk.write(chunk)
        on_disk.seek(0)
        return on_disk

    def add_alias(self, name, description):
        """Add an alias to the image."""
        self.client.api.images.aliases.post(json={
            "description": description,
            "target": self.fingerprint,
            "name": name
        })

        # Update current aliases list
        self.aliases.append({
            "description": description,
            "target": self.fingerprint,
            "name": name
        })

    def delete_alias(self, name):
        """Delete an alias from the image."""
        self.client.api.images.aliases[name].delete()

        # Update current aliases list
        la = [a["name"] for a in self.aliases]
        try:
            del self.aliases[la.index(name)]
        except ValueError:
            pass

    def copy(self, new_client, public=None, auto_update=None, wait=False):
        """Copy an image to a another LXD.

        Destination host information is contained in the client
        connection passed in.
        """
        self.sync()  # Make sure the object isn't stale

        url = "/".join(self.client.api._api_endpoint.split("/")[:-1])

        if public is None:
            public = self.public

        if auto_update is None:
            auto_update = self.auto_update

        config = {
            "filename": self.filename,
            "public": public,
            "auto_update": auto_update,
            "properties": self.properties,
            "source": {
                "type": "image",
                "mode": "pull",
                "server": url,
                "protocol": "lxd",
                "fingerprint": self.fingerprint,
            },
        }

        if self.public is not True:
            response = self.api.secret.post(json={})
            secret = response.json()["metadata"]["metadata"]["secret"]
            config["source"]["secret"] = secret
            cert = self.client.host_info["environment"]["certificate"]
            config["source"]["certificate"] = cert

        _image_create_from_config(new_client, config, wait)

        if wait:
            return new_client.images.get(self.fingerprint)
Exemple #20
0
class Network(model.Model):
    """Model representing a LXD network."""

    name = model.Attribute()
    description = model.Attribute()
    type = model.Attribute()
    config = model.Attribute()
    status = model.Attribute(readonly=True)
    locations = model.Attribute(readonly=True)
    managed = model.Attribute(readonly=True)
    used_by = model.Attribute(readonly=True)

    @classmethod
    def exists(cls, client, name):
        """
        Determine whether network with provided name exists.

        :param client: client instance
        :type client: :class:`~pylxd.client.Client`
        :param name: name of the network
        :type name: str
        :returns: `True` if network exists, `False` otherwise
        :rtype: bool
        """
        try:
            client.networks.get(name)
            return True
        except cls.NotFound:
            return False

    @classmethod
    def get(cls, client, name):
        """
        Get a network by name.

        :param client: client instance
        :type client: :class:`~pylxd.client.Client`
        :param name: name of the network
        :type name: str
        :returns: network instance (if exists)
        :rtype: :class:`Network`
        :raises: :class:`~pylxd.exceptions.NotFound` if network does not exist
        """
        response = client.api.networks[name].get()

        return cls(client, **response.json()["metadata"])

    @classmethod
    def all(cls, client):
        """
        Get all networks.

        :param client: client instance
        :type client: :class:`~pylxd.client.Client`
        :rtype: list[:class:`Network`]
        """
        response = client.api.networks.get()

        networks = []
        for url in response.json()["metadata"]:
            name = url.split("/")[-1]
            networks.append(cls(client, name=name))
        return networks

    @classmethod
    def create(cls, client, name, description=None, type=None, config=None):
        """
        Create a network.

        :param client: client instance
        :type client: :class:`~pylxd.client.Client`
        :param name: name of the network
        :type name: str
        :param description: description of the network
        :type description: str
        :param type: type of the network
        :type type: str
        :param config: additional configuration
        :type config: dict
        """
        client.assert_has_api_extension("network")

        network = {"name": name}
        if description is not None:
            network["description"] = description
        if type is not None:
            network["type"] = type
        if config is not None:
            network["config"] = config
        client.api.networks.post(json=network)
        return cls.get(client, name)

    def rename(self, new_name):
        """
        Rename a network.

        :param new_name: new name of the network
        :type new_name: str
        :return: Renamed network instance
        :rtype: :class:`Network`
        """
        self.client.assert_has_api_extension("network")
        self.client.api.networks.post(json={"name": new_name})
        return Network.get(self.client, new_name)

    def save(self, *args, **kwargs):
        self.client.assert_has_api_extension("network")
        super().save(*args, **kwargs)

    @property
    def api(self):
        return self.client.api.networks[self.name]

    def __str__(self):
        return json.dumps(self.marshall(skip_readonly=False), indent=2)

    def __repr__(self):
        attrs = []
        for attribute, value in self.marshall().items():
            attrs.append("{}={}".format(attribute,
                                        json.dumps(value, sort_keys=True)))

        return "{}({})".format(self.__class__.__name__,
                               ", ".join(sorted(attrs)))
Exemple #21
0
class Snapshot(model.Model):
    """A container snapshot."""

    name = 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:
            Operation.wait_for_operation(client, 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:
            Operation.wait_for_operation(self.client,
                                         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 = Operation.wait_for_operation(
                self.client,
                response.json()['operation'])
            return self.client.images.get(operation.metadata['fingerprint'])
Exemple #22
0
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)
    profiles = model.Attribute()
    status = model.Attribute(readonly=True)
    last_used_at = 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):
            response = self._client.api.containers[
                self._container.name].files.post(params={'path': filepath},
                                                 data=data)
            return response.status_code == 200

        def get(self, filepath):
            response = self._client.api.containers[
                self._container.name].files.get(params={'path': filepath})
            return response.content

    @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):
        """Create a new container config."""
        response = client.api.containers.post(json=config)

        if wait:
            Operation.wait_for_operation(client, 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:
            Operation.wait_for_operation(self.client,
                                         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:
            Operation.wait_for_operation(self.client,
                                         response.json()['operation'])
            if 'status' in self.__dirty__:
                del self.__dirty__[self.__dirty__.index('status')]
            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={}):
        """Execute a command on the container."""
        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.")
        response = self.api['exec'].post(
            json={
                'command': commands,
                'environment': environment,
                'wait-for-websocket': True,
                'interactive': False,
            })

        fds = response.json()['metadata']['metadata']['fds']
        operation_id = response.json()['operation'].split('/')[-1]
        parsed = parse.urlparse(
            self.client.api.operations[operation_id].websocket._api_endpoint)

        manager = WebSocketManager()

        stdin = _StdinWebsocket(self.client.websocket_url)
        stdin.resource = '{}?secret={}'.format(parsed.path, fds['0'])
        stdin.connect()
        stdout = _CommandWebsocketClient(manager, self.client.websocket_url)
        stdout.resource = '{}?secret={}'.format(parsed.path, fds['1'])
        stdout.connect()
        stderr = _CommandWebsocketClient(manager, self.client.websocket_url)
        stderr.resource = '{}?secret={}'.format(parsed.path, fds['2'])
        stderr.connect()

        manager.start()

        while len(manager.websockets.values()) > 0:
            time.sleep(.1)

        return stdout.data, stderr.data

    def migrate(self, new_client, wait=False):
        """Migrate a container.

        Destination host information is contained in the client
        connection passed in.

        If the container is running, it either must be shut down
        first or criu must be installed on the source and destination
        machines.
        """
        if self.api.scheme in ('http+unix', ):
            raise ValueError('Cannot migrate from a local client connection')

        return new_client.containers.create(self.generate_migration_data(),
                                            wait=wait)

    def generate_migration_data(self):
        """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`.
        """
        self.sync()  # Make sure the object isn't stale
        response = self.api.post(json={'migration': True})
        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 = Operation.wait_for_operation(
                self.client,
                response.json()['operation'])

            return self.client.images.get(operation.metadata['fingerprint'])
Exemple #23
0
class Image(model.Model):
    """A LXD Image."""
    aliases = model.Attribute(readonly=True)
    auto_update = model.Attribute(optional=True)
    architecture = model.Attribute(readonly=True)
    cached = model.Attribute(readonly=True)
    created_at = model.Attribute(readonly=True)
    expires_at = model.Attribute(readonly=True)
    filename = model.Attribute(readonly=True)
    fingerprint = model.Attribute(readonly=True)
    last_used_at = model.Attribute(readonly=True)
    properties = model.Attribute()
    public = model.Attribute()
    size = model.Attribute(readonly=True)
    uploaded_at = model.Attribute(readonly=True)
    update_source = model.Attribute(readonly=True)

    @property
    def api(self):
        return self.client.api.images[self.fingerprint]

    @classmethod
    def exists(cls, client, fingerprint, alias=False):
        """Determine whether an image exists.

        If `alias` is True, look up the image by its alias,
        rather than its fingerprint.
        """
        try:
            if alias:
                client.images.get_by_alias(fingerprint)
            else:
                client.images.get(fingerprint)
            return True
        except cls.NotFound:
            return False

    @classmethod
    def get(cls, client, fingerprint):
        """Get an image."""
        response = client.api.images[fingerprint].get()

        image = cls(client, **response.json()['metadata'])
        return image

    @classmethod
    def get_by_alias(cls, client, alias):
        """Get an image by its alias."""
        response = client.api.images.aliases[alias].get()

        fingerprint = response.json()['metadata']['target']
        return cls.get(client, fingerprint)

    @classmethod
    def all(cls, client):
        """Get all images."""
        response = client.api.images.get()

        images = []
        for url in response.json()['metadata']:
            fingerprint = url.split('/')[-1]
            images.append(cls(client, fingerprint=fingerprint))
        return images

    @classmethod
    def create(
            cls, client, image_data, metadata=None, public=False, wait=True):
        """Create an image.

        If metadata is provided, a multipart form data request is formed to
        push metadata and image together in a single request. The metadata must
        be a tar achive.

        `wait` parameter is now ignored, as the image fingerprint cannot be
        reliably determined consistently until after the image is indexed.
        """

        if wait is False:  # pragma: no cover
            warnings.warn(
                'Image.create wait parameter ignored and will be removed in '
                '2.3', DeprecationWarning)

        headers = {}
        if public:
            headers['X-LXD-Public'] = '1'

        if metadata is not None:
            boundary = str(uuid.uuid1())

            data = b''
            for name, contents in (
                    ('metadata', metadata), ('rootfs', image_data)):
                data += b'\r\n'.join([
                    six.b('--{}'.format(boundary)),
                    six.b(
                        'Content-Disposition:form-data;'
                        'name={};filename={}'.format(name, name)),
                    b'Content-Type: application/octet-stream',
                    b'',
                    contents,
                    b'',
                ])
            data += six.b('--{}--\r\n\r\n'.format(boundary))

            headers['Content-Type'] = "multipart/form-data;boundary={}".format(
                boundary)
        else:
            data = image_data

        response = client.api.images.post(data=data, headers=headers)
        operation = client.operations.wait_for_operation(
            response.json()['operation'])
        return cls(client, fingerprint=operation.metadata['fingerprint'])

    @classmethod
    def create_from_simplestreams(cls, client, server, alias,
                                  public=False, auto_update=False):
        """Copy an image from simplestreams."""
        config = {
            'public': public,
            'auto_update': auto_update,

            'source': {
                'type': 'image',
                'mode': 'pull',
                'server': server,
                'protocol': 'simplestreams',
                'fingerprint': alias
            }
        }

        op = _image_create_from_config(client, config, wait=True)

        return client.images.get(op.metadata['fingerprint'])

    @classmethod
    def create_from_url(cls, client, url,
                        public=False, auto_update=False):
        """Copy an image from an url."""
        config = {
            'public': public,
            'auto_update': auto_update,

            'source': {
                'type': 'url',
                'mode': 'pull',
                'url': url
            }
        }

        op = _image_create_from_config(client, config, wait=True)

        return client.images.get(op.metadata['fingerprint'])

    def export(self):
        """Export the image.

        Because the image itself may be quite large, we stream the download
        in 1kb chunks, and write it to a temporary file on disk. Once that
        file is closed, it is deleted from the disk.
        """
        on_disk = tempfile.TemporaryFile()
        with contextlib.closing(self.api.export.get(stream=True)) as response:
            for chunk in response.iter_content(chunk_size=1024):
                on_disk.write(chunk)
        on_disk.seek(0)
        return on_disk

    def add_alias(self, name, description):
        """Add an alias to the image."""
        self.client.api.images.aliases.post(json={
            'description': description,
            'target': self.fingerprint,
            'name': name
        })

        # Update current aliases list
        self.aliases.append({
            'description': description,
            'target': self.fingerprint,
            'name': name
        })

    def delete_alias(self, name):
        """Delete an alias from the image."""
        self.client.api.images.aliases[name].delete()

        # Update current aliases list
        la = [a['name'] for a in self.aliases]
        try:
            del self.aliases[la.index(name)]
        except ValueError:
            pass

    def copy(self, new_client, public=None, auto_update=None, wait=False):
        """Copy an image to a another LXD.

        Destination host information is contained in the client
        connection passed in.
        """
        self.sync()  # Make sure the object isn't stale

        url = '/'.join(self.client.api._api_endpoint.split('/')[:-1])

        if public is None:
            public = self.public

        if auto_update is None:
            auto_update = self.auto_update

        config = {
            'filename': self.filename,
            'public': public,
            'auto_update': auto_update,
            'properties': self.properties,

            'source': {
                'type': 'image',
                'mode': 'pull',
                'server': url,
                'protocol': 'lxd',
                'fingerprint': self.fingerprint
            }
        }

        if self.public is not True:
            response = self.api.secret.post(json={})
            secret = response.json()['metadata']['metadata']['secret']
            config['source']['secret'] = secret
            cert = self.client.host_info['environment']['certificate']
            config['source']['certificate'] = cert

        _image_create_from_config(new_client, config, wait)

        if wait:
            return new_client.images.get(self.fingerprint)