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
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'])
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'])
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'])
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
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)