pylxd-2.2.6/0000775000175000017500000000000013250466213012571 5ustar alexalex00000000000000pylxd-2.2.6/openstack-common.conf0000644000175000017500000000020313115536516016712 0ustar alexalex00000000000000[DEFAULT] # The list of modules to copy from oslo-incubator.git # The base module to hold the copy of openstack.common base=pylxdpylxd-2.2.6/AUTHORS0000644000175000017500000000322113250466213013635 0ustar alexalex00000000000000Aleksei Arsenev Alex Kahan Alex Kavanagh Alex Kavanagh Alex Willmer Alexander Anthony ARNAUD Anthony ARNAUD Chris MacNaughton Chris MacNaughton Chuck Short Chuck Short David Negreira Igor Malinovskiy Igor Malinovskiy Igor Vuk Itxaka James Page Jimmy McCrory Kees Bos Lukas Erlacher Matthew Williams Michał Sawicz Omer Akram Paul Hummer Paul Hummer Paul Hummer Paul Oyston Rene Jochum Rufus <31796184+deponian@users.noreply.github.com> Sergio Schvezov Stan Rudenko Stéphane Graber Stéphane Graber Thomas Goirand Tosh Lyons Tycho Andersen Virgil Dupras atx datashaman e1ee1e11 halja jpic reversecipher zulcss pylxd-2.2.6/pylxd/0000775000175000017500000000000013250466213013731 5ustar alexalex00000000000000pylxd-2.2.6/pylxd/client.py0000664000175000017500000002732213250267630015571 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import os import os.path from collections import namedtuple import requests import requests_unixsocket from six.moves.urllib import parse try: from ws4py.client import WebSocketBaseClient _ws4py_installed = True except ImportError: # pragma: no cover WebSocketBaseClient = object _ws4py_installed = False from pylxd import exceptions, managers requests_unixsocket.monkeypatch() LXD_PATH = '.config/lxc/' SNAP_ROOT = '~/snap/lxd/current/' APT_ROOT = '~/' if os.path.exists(os.path.expanduser(SNAP_ROOT)): # pragma: no cover CERTS_PATH = os.path.join(SNAP_ROOT, LXD_PATH) # pragma: no cover else: # pragma: no cover CERTS_PATH = os.path.join(APT_ROOT, LXD_PATH) # pragma: no cover Cert = namedtuple('Cert', ['cert', 'key']) # pragma: no cover DEFAULT_CERTS = Cert( cert=os.path.expanduser(os.path.join(CERTS_PATH, 'client.crt')), key=os.path.expanduser(os.path.join(CERTS_PATH, 'client.key')) ) # pragma: no cover class _APINode(object): """An api node object.""" def __init__(self, api_endpoint, cert=None, verify=True, timeout=None): self._api_endpoint = api_endpoint self._timeout = timeout if self._api_endpoint.startswith('http+unix://'): self.session = requests_unixsocket.Session() else: self.session = requests.Session() self.session.cert = cert self.session.verify = verify def __getattr__(self, name): # name here correspoinds to the model name in the LXD API # and, as such, must have underscores replaced with hyphens return self.__class__( '{}/{}'.format(self._api_endpoint, name.replace('_', '-')), cert=self.session.cert, verify=self.session.verify) def __getitem__(self, item): # item here correspoinds to the model name in the LXD API # and, as such, must have underscores replaced with hyphens return self.__class__( '{}/{}'.format(self._api_endpoint, item.replace('_', '-')), cert=self.session.cert, verify=self.session.verify, timeout=self._timeout) def _assert_response( self, response, allowed_status_codes=(200,), stream=False): """Assert properties of the response. LXD's API clearly defines specific responses. If the API response is something unexpected (i.e. an error), then we need to raise an exception and have the call points handle the errors or just let the issue be raised to the user. """ if response.status_code not in allowed_status_codes: if response.status_code == 404: raise exceptions.NotFound(response) raise exceptions.LXDAPIException(response) # In the case of streaming, we can't validate the json the way we # would with normal HTTP responses, so just ignore that entirely. if stream: return try: data = response.json() except ValueError: # Not a JSON response return if response.status_code == 200: # Synchronous request try: if data['type'] != 'sync': raise exceptions.LXDAPIException(response) except KeyError: # Missing 'type' in response raise exceptions.LXDAPIException(response) @property def scheme(self): return parse.urlparse(self.api._api_endpoint).scheme @property def netloc(self): return parse.urlparse(self.api._api_endpoint).netloc def get(self, *args, **kwargs): """Perform an HTTP GET.""" kwargs['timeout'] = kwargs.get('timeout', self._timeout) response = self.session.get(self._api_endpoint, *args, **kwargs) self._assert_response(response, stream=kwargs.get('stream', False)) return response def post(self, *args, **kwargs): """Perform an HTTP POST.""" kwargs['timeout'] = kwargs.get('timeout', self._timeout) response = self.session.post(self._api_endpoint, *args, **kwargs) # Prior to LXD 2.0.3, successful synchronous requests returned 200, # rather than 201. self._assert_response(response, allowed_status_codes=(200, 201, 202)) return response def put(self, *args, **kwargs): """Perform an HTTP PUT.""" kwargs['timeout'] = kwargs.get('timeout', self._timeout) response = self.session.put(self._api_endpoint, *args, **kwargs) self._assert_response(response, allowed_status_codes=(200, 202)) return response def delete(self, *args, **kwargs): """Perform an HTTP delete.""" kwargs['timeout'] = kwargs.get('timeout', self._timeout) response = self.session.delete(self._api_endpoint, *args, **kwargs) self._assert_response(response, allowed_status_codes=(200, 202)) return response class _WebsocketClient(WebSocketBaseClient): """A basic websocket client for the LXD API. This client is intentionally barebones, and serves as a simple default. It simply connects and saves all json messages to a messages attribute, which can then be read are parsed. """ def handshake_ok(self): self.messages = [] def received_message(self, message): json_message = json.loads(message.data.decode('utf-8')) self.messages.append(json_message) class Client(object): """Client class for LXD REST API. This client wraps all the functionality required to interact with LXD, and is meant to be the sole entry point. .. attribute:: containers Instance of :class:`Client.Containers `: .. attribute:: images Instance of :class:`Client.Images `. .. attribute:: operations Instance of :class:`Client.Operations `. .. attribute:: profiles Instance of :class:`Client.Profiles `. .. attribute:: api This attribute provides tree traversal syntax to LXD's REST API for lower-level interaction. Use the name of the url part as attribute or item of an api object to create another api object appended with the new url part name, ie: >>> api = Client().api # / >>> response = api.get() # Check status code and response >>> print response.status_code, response.json() # /containers/test/ >>> print api.containers['test'].get().json() """ def __init__( self, endpoint=None, version='1.0', cert=None, verify=True, timeout=None): """Constructs a LXD client :param endpoint: (optional): endpoint can be an http endpoint or a path to a unix socket. :param version: (optional): API version string to use with LXD :param cert: (optional): A tuple of (cert, key) to use with the http socket for client authentication :param verify: (optional): Either a boolean, in which case it controls whether we verify the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to ``True``. :param timeout: (optional) How long to wait for the server to send data before giving up, as a float, or a :ref:`(connect timeout, read timeout) ` tuple. """ self.cert = cert if endpoint is not None: if endpoint.startswith('/') and os.path.isfile(endpoint): self.api = _APINode('http+unix://{}'.format( parse.quote(endpoint, safe='')), timeout=timeout) else: # Extra trailing slashes cause LXD to 301 endpoint = endpoint.rstrip('/') if cert is None and ( os.path.exists(DEFAULT_CERTS.cert) and os.path.exists(DEFAULT_CERTS.key)): cert = DEFAULT_CERTS self.api = _APINode( endpoint, cert=cert, verify=verify, timeout=timeout) else: if 'LXD_DIR' in os.environ: path = os.path.join( os.environ.get('LXD_DIR'), 'unix.socket') else: if os.path.exists('/var/snap/lxd/common/lxd/unix.socket'): path = '/var/snap/lxd/common/lxd/unix.socket' else: path = '/var/lib/lxd/unix.socket' self.api = _APINode('http+unix://{}'.format( parse.quote(path, safe='')), timeout=timeout) self.api = self.api[version] # Verify the connection is valid. try: response = self.api.get() if response.status_code != 200: raise exceptions.ClientConnectionFailed() self.host_info = response.json()['metadata'] except (requests.exceptions.ConnectionError, requests.exceptions.InvalidURL): raise exceptions.ClientConnectionFailed() self.certificates = managers.CertificateManager(self) self.containers = managers.ContainerManager(self) self.images = managers.ImageManager(self) self.networks = managers.NetworkManager(self) self.operations = managers.OperationManager(self) self.profiles = managers.ProfileManager(self) self.storage_pools = managers.StoragePoolManager(self) @property def trusted(self): return self.host_info['auth'] == 'trusted' def authenticate(self, password): if self.trusted: return cert = open(self.api.session.cert[0]).read().encode('utf-8') self.certificates.create(password, cert) # Refresh the host info response = self.api.get() self.host_info = response.json()['metadata'] @property def websocket_url(self): if self.api.scheme in ('http', 'https'): host = self.api.netloc if self.api.scheme == 'http': scheme = 'ws' else: scheme = 'wss' else: scheme = 'ws+unix' host = parse.unquote(self.api.netloc) url = parse.urlunparse((scheme, host, '', '', '', '')) return url def events(self, websocket_client=None): """Get a websocket client for getting events. /events is a websocket url, and so must be handled differently than most other LXD API endpoints. This method returns a client that can be interacted with like any regular python socket. An optional `websocket_client` parameter can be specified for implementation-specific handling of events as they occur. """ if not _ws4py_installed: raise ValueError( 'This feature requires the optional ws4py library.') if websocket_client is None: websocket_client = _WebsocketClient client = websocket_client(self.websocket_url) parsed = parse.urlparse(self.api.events._api_endpoint) client.resource = parsed.path return client pylxd-2.2.6/pylxd/models/0000775000175000017500000000000013250466213015214 5ustar alexalex00000000000000pylxd-2.2.6/pylxd/models/container.py0000664000175000017500000004152213250267630017556 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import time import six from six.moves.urllib import parse try: from ws4py.client import WebSocketBaseClient from ws4py.manager import WebSocketManager _ws4py_installed = True except ImportError: # pragma: no cover WebSocketBaseClient = object _ws4py_installed = False from pylxd import managers from pylxd.models import _model as model class ContainerState(object): """A simple object for representing container state.""" def __init__(self, **kwargs): for key, value in six.iteritems(kwargs): setattr(self, key, value) _ContainerExecuteResult = collections.namedtuple( 'ContainerExecuteResult', ['exit_code', 'stdout', 'stderr']) class Container(model.Model): """An LXD Container. This class is not intended to be used directly, but rather to be used via `Client.containers.create`. """ architecture = model.Attribute() config = model.Attribute() created_at = model.Attribute() devices = model.Attribute() ephemeral = model.Attribute() expanded_config = model.Attribute() expanded_devices = model.Attribute() name = model.Attribute(readonly=True) description = model.Attribute() profiles = model.Attribute() status = model.Attribute(readonly=True) last_used_at = model.Attribute(readonly=True) 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 _CommandWebsocketClient(WebSocketBaseClient): # pragma: no cover def __init__(self, manager, *args, **kwargs): self.manager = manager super(_CommandWebsocketClient, self).__init__(*args, **kwargs) def handshake_ok(self): self.manager.add(self) self.buffer = [] def received_message(self, message): if len(message.data) == 0: self.close() self.manager.remove(self) if message.encoding: self.buffer.append(message.data.decode(message.encoding)) else: self.buffer.append(message.data.decode('utf-8')) @property def data(self): return ''.join(self.buffer) class _StdinWebsocket(WebSocketBaseClient): # pragma: no cover """A websocket client for handling stdin. The nature of stdin in Container.execute means that we don't ever use this connection. It is closed as soon as it completes the handshake. """ def handshake_ok(self): self.close() 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']) pylxd-2.2.6/pylxd/models/_model.py0000664000175000017500000001612013250267630017027 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import warnings import six from pylxd import exceptions MISSING = object() class Attribute(object): """A metadata class for model attributes.""" def __init__(self, validator=None, readonly=False, optional=False): self.validator = validator self.readonly = readonly self.optional = optional class Manager(object): """A manager declaration. This class signals to the model that it will have a Manager attribute. """ class Parent(object): """A parent declaration. Child managers must keep a reference to their parent. """ class ModelType(type): """A Model metaclass. This metaclass converts the declarative Attribute style to attributes on the model instance itself. """ def __new__(cls, name, bases, attrs): if '__slots__' in attrs and name != 'Model': # pragma: no cover raise TypeError('__slots__ should not be specified.') attributes = {} for_removal = [] managers = [] for key, val in attrs.items(): if type(val) == Attribute: attributes[key] = val for_removal.append(key) if type(val) in (Manager, Parent): managers.append(key) for_removal.append(key) for key in for_removal: del attrs[key] slots = list(attributes.keys()) if '__slots__' in attrs: slots = slots + attrs['__slots__'] for base in bases: if '__slots__' in dir(base): slots = slots + base.__slots__ if len(managers) > 0: slots = slots + managers attrs['__slots__'] = slots attrs['__attributes__'] = attributes return super(ModelType, cls).__new__(cls, name, bases, attrs) @six.add_metaclass(ModelType) class Model(object): """A Base LXD object model. Objects fetched from the LXD API have state, which allows the objects to be used transactionally, with E-tag support, and be smart about I/O. The model lifecycle is this: A model's get/create methods will return an instance. That instance may or may not be a partial instance. If it is a partial instance, `sync` will be called and the rest of the object retrieved from the server when un-initialized attributes are read. When attributes are modified, the instance is marked as dirty. `save` will save the changes to the server. """ NotFound = exceptions.NotFound __slots__ = ['client', '__dirty__'] def __init__(self, client, **kwargs): self.__dirty__ = set() self.client = client for key, val in kwargs.items(): try: setattr(self, key, val) except AttributeError: warnings.warn( 'Attempted to set unknown attribute "{}" ' 'on instance of "{}"'.format( key, self.__class__.__name__ )) self.__dirty__.clear() def __getattribute__(self, name): try: return super(Model, self).__getattribute__(name) except AttributeError: if name in self.__attributes__: self.sync() return super(Model, self).__getattribute__(name) else: raise def __setattr__(self, name, value): if name in self.__attributes__: attribute = self.__attributes__[name] if attribute.validator is not None: if attribute.validator is not type(value): value = attribute.validator(value) self.__dirty__.add(name) return super(Model, self).__setattr__(name, value) @property def dirty(self): return len(self.__dirty__) > 0 def sync(self, rollback=False): """Sync from the server. When collections of objects are retrieved from the server, they are often partial objects. The full object must be retrieved before it can modified. This method is called when getattr is called on a non-initaliazed object. """ # XXX: rockstar (25 Jun 2016) - This has the potential to step # on existing attributes. response = self.api.get() payload = response.json()['metadata'] for key, val in payload.items(): if key not in self.__dirty__ or rollback: try: setattr(self, key, val) self.__dirty__.remove(key) except AttributeError: # We have received an attribute from the server that we # don't support in our model. Ignore this error, it # doesn't hurt us. pass # Make sure that *all* supported attributes are set, even those that # aren't supported by the server. missing_attrs = set(self.__attributes__.keys()) - set(payload.keys()) for missing_attr in missing_attrs: setattr(self, missing_attr, MISSING) if rollback: self.__dirty__.clear() def rollback(self): """Reset the object from the server.""" return self.sync(rollback=True) def save(self, wait=False): """Save data to the server. This method should write the new data to the server via marshalling. It should be a no-op when the object is not dirty, to prevent needless I/O. """ marshalled = self.marshall() response = self.api.put(json=marshalled) if response.json()['type'] == 'async' and wait: self.client.operations.wait_for_operation( response.json()['operation']) self.__dirty__.clear() def delete(self, wait=False): """Delete an object from the server.""" response = self.api.delete() if response.json()['type'] == 'async' and wait: self.client.operations.wait_for_operation( response.json()['operation']) self.client = None def marshall(self): """Marshall the object in preparation for updating to the server.""" marshalled = {} for key, attr in self.__attributes__.items(): if ((not attr.readonly and not attr.optional) or (attr.optional and hasattr(self, key))): val = getattr(self, key) # Don't send back to the server an attribute it doesn't # support. if val is not MISSING: marshalled[key] = val return marshalled pylxd-2.2.6/pylxd/models/certificate.py0000664000175000017500000000472113250267630020056 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import binascii from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.serialization import Encoding from pylxd.models import _model as model class Certificate(model.Model): """A LXD certificate.""" certificate = model.Attribute() fingerprint = model.Attribute() type = 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] pylxd-2.2.6/pylxd/models/network.py0000664000175000017500000000333213250267630017262 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from pylxd.models import _model as model 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') pylxd-2.2.6/pylxd/models/__init__.py0000664000175000017500000000056613250267630017336 0ustar alexalex00000000000000from pylxd.models.certificate import Certificate # NOQA from pylxd.models.container import Container, Snapshot # NOQA from pylxd.models.image import Image # NOQA from pylxd.models.network import Network # NOQA from pylxd.models.operation import Operation # NOQA from pylxd.models.profile import Profile # NOQA from pylxd.models.storage_pool import StoragePool # NOQA pylxd-2.2.6/pylxd/models/image.py0000664000175000017500000002061513250267630016656 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import contextlib import tempfile import warnings from requests_toolbelt import MultipartEncoder from pylxd.models import _model as model def _image_create_from_config(client, config, wait=False): """ Create an image from the given configuration. See: https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-6 """ response = client.api.images.post(json=config) if wait: return client.operations.wait_for_operation( response.json()['operation']) return response.json()['operation'] 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) pylxd-2.2.6/pylxd/models/storage_pool.py0000664000175000017500000000455713250267630020300 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from pylxd.models import _model as model 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') pylxd-2.2.6/pylxd/models/profile.py0000664000175000017500000000430013250267630017225 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from pylxd.models import _model as model 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) pylxd-2.2.6/pylxd/models/operation.py0000664000175000017500000000453113250460012017560 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import warnings from pylxd import exceptions class Operation(object): """A LXD operation.""" __slots__ = [ '_client', 'class', 'created_at', 'description', 'err', 'id', 'may_cancel', 'metadata', 'resources', 'status', 'status_code', 'updated_at'] @classmethod def wait_for_operation(cls, client, operation_id): """Get an operation and wait for it to complete.""" operation = cls.get(client, operation_id) operation.wait() return cls.get(client, operation.id) @classmethod def get(cls, client, operation_id): """Get an operation.""" if operation_id.startswith('/'): operation_id = operation_id.split('/')[-1] response = client.api.operations[operation_id].get() return cls(_client=client, **response.json()['metadata']) def __init__(self, **kwargs): super(Operation, self).__init__() for key, value in kwargs.items(): try: setattr(self, key, value) except AttributeError: # ignore attributes we don't know about -- prevent breakage # in the future if new attributes are added. warnings.warn( 'Attempted to set unknown attribute "{}" ' 'on instance of "{}"' .format(key, self.__class__.__name__)) pass def wait(self): """Wait for the operation to complete and return.""" response = self._client.api.operations[self.id].wait.get() try: if response.json()['metadata']['status'] == 'Failure': raise exceptions.LXDAPIException(response) except KeyError: # Support for legacy LXD pass pylxd-2.2.6/pylxd/__init__.py0000664000175000017500000000127013250267630016044 0ustar alexalex00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version __version__ = pbr.version.VersionInfo('pylxd').version_string() from pylxd.client import Client # NOQA pylxd-2.2.6/pylxd/exceptions.py0000664000175000017500000000216513250267630016472 0ustar alexalex00000000000000class LXDAPIException(Exception): """A generic exception for representing unexpected LXD API responses. LXD API responses are clearly documented, and are either a standard return value, and background operation, or an error. This exception is raised on an error case, or when the response status code is not expected for the response. This exception should *only* be raised in cases where the LXD REST API has returned something unexpected. """ def __init__(self, response): super(LXDAPIException, self).__init__() self.response = response def __str__(self): if self.response.status_code == 200: # Operation failure return self.response.json()['metadata']['err'] try: data = self.response.json() return data['error'] except (ValueError, KeyError): pass return self.response.content.decode('utf-8') class NotFound(LXDAPIException): """An exception raised when an object is not found.""" class ClientConnectionFailed(Exception): """An exception raised when the Client connection fails.""" pylxd-2.2.6/pylxd/deprecation.py0000644000175000017500000000142213115536516016601 0ustar alexalex00000000000000import warnings warnings.simplefilter('once', DeprecationWarning) class deprecated(): # pragma: no cover """A decorator for warning about deprecation warnings. The decorator takes an optional message argument. This message can be used to direct the user to a new API or specify when it will be removed. """ DEFAULT_MESSAGE = '{} is deprecated and will be removed soon.' def __init__(self, message=None): self.message = message def __call__(self, f): def wrapped(*args, **kwargs): if self.message is None: self.message = self.DEFAULT_MESSAGE.format( f.__name__) warnings.warn(self.message, DeprecationWarning) return f(*args, **kwargs) return wrapped pylxd-2.2.6/pylxd/managers.py0000664000175000017500000000315213250267630016103 0ustar alexalex00000000000000from contextlib import contextmanager import functools import importlib import inspect class BaseManager(object): """A BaseManager class for handling collection operations.""" @property def manager_for(self): # pragma: no cover raise AttributeError( "Manager class requires 'manager_for' attribute") def __init__(self, *args, **kwargs): manager_for = self.manager_for module = '.'.join(manager_for.split('.')[0:-1]) obj = manager_for.split('.')[-1] target_module = importlib.import_module(module) target = getattr(target_module, obj) methods = inspect.getmembers(target, predicate=inspect.ismethod) for name, method in methods: func = functools.partial(method, *args, **kwargs) setattr(self, name, func) return super(BaseManager, self).__init__() class CertificateManager(BaseManager): manager_for = 'pylxd.models.Certificate' class ContainerManager(BaseManager): manager_for = 'pylxd.models.Container' class ImageManager(BaseManager): manager_for = 'pylxd.models.Image' class NetworkManager(BaseManager): manager_for = 'pylxd.models.Network' class OperationManager(BaseManager): manager_for = 'pylxd.models.Operation' class ProfileManager(BaseManager): manager_for = 'pylxd.models.Profile' class SnapshotManager(BaseManager): manager_for = 'pylxd.models.Snapshot' class StoragePoolManager(BaseManager): manager_for = 'pylxd.models.StoragePool' @contextmanager def web_socket_manager(manager): try: yield manager finally: manager.stop() pylxd-2.2.6/pylxd/tests/0000775000175000017500000000000013250466213015073 5ustar alexalex00000000000000pylxd-2.2.6/pylxd/tests/mock_lxd.py0000664000175000017500000004750313250267630017260 0ustar alexalex00000000000000import json def containers_POST(request, context): context.status_code = 202 return json.dumps({ 'type': 'async', 'operation': 'operation-abc'}) def container_POST(request, context): context.status_code = 202 if not request.json().get('migration', False): return { 'type': 'async', 'operation': 'operation-abc'} else: return { 'type': 'async', 'operation': 'operation-abc', 'metadata': { 'metadata': { '0': 'abc', '1': 'def', 'control': 'ghi', } } } def container_DELETE(request, context): context.status_code = 202 return json.dumps({ 'type': 'async', 'operation': 'operation-abc'}) def images_POST(request, context): context.status_code = 202 return json.dumps({ 'type': 'async', 'operation': 'images-create-operation'}) def image_DELETE(request, context): context.status_code = 202 return json.dumps({ 'type': 'async', 'operation': 'operation-abc'}) def profiles_POST(request, context): context.status_code = 200 return json.dumps({ 'type': 'sync', 'metadata': {}}) def profile_DELETE(request, context): context.status_code = 200 return json.dumps({ 'type': 'sync', 'operation': 'operation-abc'}) def snapshot_DELETE(request, context): context.status_code = 202 return json.dumps({ 'type': 'async', 'operation': 'operation-abc'}) def profile_GET(request, context): name = request.path.split('/')[-1] return json.dumps({ 'type': 'sync', 'metadata': { 'name': name, 'description': 'An description', 'config': {}, 'devices': {}, 'used_by': [], }, }) RULES = [ # General service endpoints { 'text': json.dumps({ 'type': 'sync', 'metadata': {'auth': 'trusted', 'environment': { 'certificate': 'an-pem-cert', }, 'api_extensions': [] }}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0$', }, { 'text': json.dumps({ 'type': 'sync', 'metadata': {'auth': 'trusted', 'environment': {}, 'api_extensions': [] }}), 'method': 'GET', 'url': r'^http://pylxd2.test/1.0$', }, # Certificates { 'text': json.dumps({ 'type': 'sync', 'metadata': [ 'http://pylxd.test/1.0/certificates/an-certificate', ]}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/certificates$', }, { 'method': 'POST', 'url': r'^http://pylxd.test/1.0/certificates$', }, { 'text': json.dumps({ 'type': 'sync', 'metadata': { 'certificate': 'certificate-content', 'fingerprint': 'eaf55b72fc23aa516d709271df9b0116064bf8cfa009cf34c67c33ad32c2320c', # NOQA 'type': 'client', }}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/certificates/eaf55b72fc23aa516d709271df9b0116064bf8cfa009cf34c67c33ad32c2320c$', # NOQA }, { 'text': json.dumps({ 'type': 'sync', 'metadata': { 'certificate': 'certificate-content', 'fingerprint': 'an-certificate', 'type': 'client', }}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/certificates/an-certificate$', }, { 'json': { 'type': 'sync', 'metadata': {}, }, 'status_code': 202, 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/certificates/an-certificate$', }, # Containers { 'text': json.dumps({ 'type': 'sync', 'metadata': [ 'http://pylxd.test/1.0/containers/an-container', ]}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers$', }, { 'text': containers_POST, 'method': 'POST', 'url': r'^http://pylxd2.test/1.0/containers$', }, { 'text': containers_POST, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/containers$', }, { 'json': { 'type': 'sync', 'metadata': { 'name': 'an-container', 'architecture': "x86_64", 'config': { 'security.privileged': "true", }, 'created_at': "1983-06-16T00:00:00-00:00", 'last_used_at': "1983-06-16T00:00:00-00:00", 'description': "Some description", 'devices': { 'root': { 'path': "/", 'type': "disk" } }, 'ephemeral': True, 'expanded_config': { 'security.privileged': "true", }, 'expanded_devices': { 'eth0': { 'name': "eth0", 'nictype': "bridged", 'parent': "lxdbr0", 'type': "nic" }, 'root': { 'path': "/", 'type': "disk" } }, 'profiles': [ "default" ], 'stateful': False, 'status': "Running", 'status_code': 103, 'unsupportedbypylxd': "This attribute is not supported by "\ "pylxd. We want to test whether the mere presence of it "\ "makes it crash." }}, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, { 'json': { 'type': 'sync', 'metadata': { 'status': 'Running', 'status_code': 103, 'disk': { 'root': { 'usage': 10, } }, 'memory': { 'usage': 15, 'usage_peak': 20, 'swap_usage': 0, 'swap_usage_peak': 5, }, 'network': { 'l0': { 'addresses': [ {'family': 'inet', 'address': '127.0.0.1', 'netmask': '8', 'scope': 'local'} ], } }, 'pid': 69, 'processes': 100, }}, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-container/state$', # NOQA }, { 'status_code': 202, 'json': { 'type': 'async', 'operation': 'operation-abc'}, 'method': 'PUT', 'url': r'^http://pylxd.test/1.0/containers/an-container/state$', # NOQA }, { 'json': container_POST, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, { 'text': json.dumps({ 'type': 'async', 'operation': 'operation-abc'}), 'status_code': 202, 'method': 'PUT', 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, { 'text': container_DELETE, 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/containers/an-container$', }, { 'json': { 'type': 'async', 'metadata': { 'metadata': { 'fds': { '0': 'abc', '1': 'def', '2': 'ghi', 'control': 'jkl', } }, }, 'operation': 'operation-abc'}, 'status_code': 202, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/containers/an-container/exec$', # NOQA }, # Container Snapshots { 'text': json.dumps({ 'type': 'sync', 'metadata': [ '/1.0/containers/an_container/snapshots/an-snapshot', ]}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots$', # NOQA }, { 'text': json.dumps({ 'type': 'async', 'operation': 'operation-abc'}), 'status_code': 202, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots$', # NOQA }, { 'text': json.dumps({ 'type': 'sync', 'metadata': { 'name': 'an_container/an-snapshot', 'stateful': False, }}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }, { 'text': json.dumps({ 'type': 'async', 'operation': 'operation-abc'}), 'status_code': 202, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }, { 'text': snapshot_DELETE, 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }, # Container files { 'text': 'This is a getted file', 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fgetted$', # NOQA }, { 'method': 'POST', 'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fputted$', # NOQA }, { 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/containers/an-container/files\?path=%2Ftmp%2Fputted$', # NOQA }, # Images { 'text': json.dumps({ 'type': 'sync', 'metadata': [ 'http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA ]}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images$', }, { 'text': images_POST, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/images$', }, { 'text': images_POST, 'method': 'POST', 'url': r'^http://pylxd2.test/1.0/images$', }, { 'json': { 'type': 'sync', 'status': 'Success', 'status_code': 200, 'metadata': { 'name': 'an-alias', 'description': 'an-alias', 'target': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA } }, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/aliases/an-alias$', }, { 'text': json.dumps({ 'type': 'sync', 'metadata': { 'aliases': [ { 'name': 'an-alias', # NOQA 'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA } ], 'architecture': 'x86_64', 'cached': False, 'filename': 'a_image.tar.bz2', 'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA 'public': False, 'properties': {}, 'size': 1, 'auto_update': False, 'created_at': '1983-06-16T02:42:00Z', 'expires_at': '1983-06-16T02:42:00Z', 'last_used_at': '1983-06-16T02:42:00Z', 'uploaded_at': '1983-06-16T02:42:00Z', }, }), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA }, { 'text': json.dumps({ 'type': 'sync', 'metadata': { 'aliases': [ { 'name': 'an-alias', # NOQA 'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA } ], 'architecture': 'x86_64', 'cached': False, 'filename': 'a_image.tar.bz2', 'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA 'public': False, 'properties': {}, 'size': 1, 'auto_update': False, 'created_at': '1983-06-16T02:42:00Z', 'expires_at': '1983-06-16T02:42:00Z', 'last_used_at': '1983-06-16T02:42:00Z', 'uploaded_at': '1983-06-16T02:42:00Z', }, }), 'method': 'GET', 'url': r'^http://pylxd2.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA }, { 'text': json.dumps({'type': 'async', 'operation': 'operation-abc'}), 'status_code': 202, 'method': 'PUT', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA }, { 'text': '0' * 2048, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/export$', # NOQA }, { 'text': image_DELETE, 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA }, # Image Aliases { 'json': { 'type': 'sync', 'status': 'Success', 'status_code': 200, 'metadata': { 'name': 'an-alias', 'description': 'an-alias', 'target': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA } }, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/aliases/an-alias$', }, { 'json': { 'type': 'sync', 'status': 'Success', 'metadata': None }, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/images/aliases$' }, { 'json': { 'type': 'sync', 'status': 'Success', 'status_code': 200, 'metadata': None }, 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/images/aliases/an-alias$' }, { 'json': { 'type': 'sync', 'status': 'Success', 'status_code': 200, 'metadata': None }, 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/images/aliases/b-alias$' }, # Images secret { 'json': { 'type': 'sync', 'status': 'Success', 'status_code': 200, 'metadata': { 'metadata': { 'secret': 'abcdefg' } } }, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/secret$', # NOQA }, # Networks { 'json': { 'type': 'sync', 'metadata': [ 'http://pylxd.test/1.0/networks/lo', ]}, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/networks$', }, { 'json': { 'type': 'sync', 'metadata': { 'name': 'lo', 'type': 'loopback', 'used_by': [], }}, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/networks/lo$', }, # Storage Pools { 'json': { 'type': 'sync', 'metadata': [ 'http://pylxd.test/1.0/storage-pools/lxd', ]}, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/storage-pools$', }, { 'json': { 'type': 'sync', 'metadata': { 'config': { 'size': '0', 'source': '/var/lib/lxd/disks/lxd.img' }, 'description': '', 'name': 'lxd', 'driver': 'zfs', 'used_by': [], }}, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/storage-pools/lxd$', }, { 'json': {'type': 'sync'}, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/storage-pools$', }, # Profiles { 'text': json.dumps({ 'type': 'sync', 'metadata': [ 'http://pylxd.test/1.0/profiles/an-profile', ]}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/profiles$', }, { 'text': profiles_POST, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/profiles$', }, { 'text': profile_GET, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/profiles/(an-profile|an-new-profile|an-renamed-profile)$', # NOQA }, { 'text': json.dumps({'type': 'sync'}), 'method': 'PUT', 'url': r'^http://pylxd.test/1.0/profiles/(an-profile|an-new-profile)$', }, { 'text': json.dumps({'type': 'sync'}), 'method': 'POST', 'url': r'^http://pylxd.test/1.0/profiles/(an-profile|an-new-profile)$', }, { 'text': profile_DELETE, 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/profiles/(an-profile|an-new-profile)$', }, # Operations { 'text': json.dumps({ 'type': 'sync', 'metadata': {'id': 'operation-abc', 'metadata': {'return': 0}}, }), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/operations/operation-abc$', }, { 'text': json.dumps({ 'type': 'sync', }), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/operations/operation-abc/wait$', }, { 'text': json.dumps({ 'type': 'sync', 'metadata': {'id': 'operation-abc'}, }), 'method': 'GET', 'url': r'^http://pylxd2.test/1.0/operations/operation-abc$', }, { 'text': json.dumps({ 'type': 'sync', }), 'method': 'GET', 'url': r'^http://pylxd2.test/1.0/operations/operation-abc/wait$', }, { 'text': json.dumps({ 'type': 'sync', 'metadata': { 'id': 'images-create-operation', 'metadata': { 'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' # NOQA } } }), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/operations/images-create-operation$', }, { 'text': json.dumps({ 'type': 'sync', }), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/operations/images-create-operation/wait$', # NOQA }, { 'text': json.dumps({ 'type': 'sync', 'metadata': {'id': 'operation-abc'}, }), 'method': 'GET', 'url': r'^http://pylxd2.test/1.0/operations/images-create-operation$', }, { 'text': json.dumps({ 'type': 'sync', }), 'method': 'GET', 'url': r'^http://pylxd2.test/1.0/operations/images-create-operation/wait$', # NOQA } ] pylxd-2.2.6/pylxd/tests/models/0000775000175000017500000000000013250466213016356 5ustar alexalex00000000000000pylxd-2.2.6/pylxd/tests/models/test_storage.py0000664000175000017500000000547713250267630021452 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from pylxd import models from pylxd.tests import testing class TestStoragePool(testing.PyLXDTestCase): """Tests for pylxd.models.StoragePool.""" def test_all(self): """A list of all storage_pools are returned.""" storage_pools = models.StoragePool.all(self.client) self.assertEqual(1, len(storage_pools)) def test_get(self): """Return a container.""" name = 'lxd' an_storage_pool = models.StoragePool.get(self.client, name) self.assertEqual(name, an_storage_pool.name) def test_partial(self): """A partial storage_pool is synced.""" an_storage_pool = models.StoragePool(self.client, name='lxd') self.assertEqual('zfs', an_storage_pool.driver) def test_create(self): """A new storage pool is created.""" config = {"config": {}, "driver": "zfs", "name": "lxd"} an_storage_pool = models.StoragePool.create(self.client, config) self.assertEqual(config['name'], an_storage_pool.name) def test_exists(self): """A storage pool exists.""" name = 'lxd' self.assertTrue(models.StoragePool.exists(self.client, name)) def test_not_exists(self): """A storage pool exists.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/storage-pools/an-missing-storage-pool$', # NOQA }) name = 'an-missing-storage-pool' self.assertFalse(models.StoragePool.exists(self.client, name)) def test_delete(self): """delete is not implemented in storage_pools.""" an_storage_pool = models.StoragePool(self.client, name='lxd') with self.assertRaises(NotImplementedError): an_storage_pool.delete() def test_save(self): """save is not implemented in storage_pools.""" an_storage_pool = models.StoragePool(self.client, name='lxd') with self.assertRaises(NotImplementedError): an_storage_pool.save() pylxd-2.2.6/pylxd/tests/models/__init__.py0000664000175000017500000000000013250267630020457 0ustar alexalex00000000000000pylxd-2.2.6/pylxd/tests/models/test_operation.py0000664000175000017500000000475213250460012021766 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from pylxd import exceptions, models from pylxd.tests import testing class TestOperation(testing.PyLXDTestCase): """Tests for pylxd.models.Operation.""" def test_get(self): """Return an operation.""" name = 'operation-abc' an_operation = models.Operation.get(self.client, name) self.assertEqual(name, an_operation.id) def test_get_full_path(self): """Return an operation even if the full path is specified.""" name = '/1.0/operations/operation-abc' an_operation = models.Operation.get(self.client, name) self.assertEqual('operation-abc', an_operation.id) def test_wait_with_error(self): """If the operation errors, wait raises an exception.""" def error(request, context): context.status_code = 200 return { 'type': 'sync', 'metadata': { 'status': 'Failure', 'err': 'Keep your foot off the blasted samoflange.', }} self.add_rule({ 'json': error, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/operations/operation-abc/wait$', # NOQA }) name = '/1.0/operations/operation-abc' an_operation = models.Operation.get(self.client, name) self.assertRaises(exceptions.LXDAPIException, an_operation.wait) def test_unknown_attribute(self): self.add_rule({ 'text': json.dumps({ 'type': 'sync', 'metadata': {'id': 'operation-unknown', 'metadata': {'return': 0}, 'unknown': False}, }), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/operations/operation-unknown$', }) url = '/1.0/operations/operation-unknown' models.Operation.get(self.client, url) pylxd-2.2.6/pylxd/tests/models/test_image.py0000664000175000017500000003174413250267630021064 0ustar alexalex00000000000000import hashlib import json from io import StringIO from pylxd import exceptions, models from pylxd.tests import testing class TestImage(testing.PyLXDTestCase): """Tests for pylxd.models.Image.""" def test_get(self): """An image is fetched.""" fingerprint = hashlib.sha256(b'').hexdigest() a_image = models.Image.get(self.client, fingerprint) self.assertEqual(fingerprint, a_image.fingerprint) def test_get_not_found(self): """LXDAPIException is raised when the image isn't found.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA }) fingerprint = hashlib.sha256(b'').hexdigest() self.assertRaises( exceptions.LXDAPIException, models.Image.get, self.client, fingerprint) def test_get_error(self): """LXDAPIException is raised on error.""" def error(request, context): context.status_code = 500 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 500}) self.add_rule({ 'text': error, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA }) fingerprint = hashlib.sha256(b'').hexdigest() self.assertRaises( exceptions.LXDAPIException, models.Image.get, self.client, fingerprint) def test_get_by_alias(self): fingerprint = hashlib.sha256(b'').hexdigest() a_image = models.Image.get_by_alias(self.client, 'an-alias') self.assertEqual(fingerprint, a_image.fingerprint) def test_exists(self): """An image is fetched.""" fingerprint = hashlib.sha256(b'').hexdigest() self.assertTrue(models.Image.exists(self.client, fingerprint)) def test_exists_by_alias(self): """An image is fetched.""" self.assertTrue(models.Image.exists( self.client, 'an-alias', alias=True)) def test_not_exists(self): """LXDAPIException is raised when the image isn't found.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA }) fingerprint = hashlib.sha256(b'').hexdigest() self.assertFalse(models.Image.exists(self.client, fingerprint)) def test_all(self): """A list of all images is returned.""" images = models.Image.all(self.client) self.assertEqual(1, len(images)) def test_create(self): """An image is created.""" fingerprint = hashlib.sha256(b'').hexdigest() a_image = models.Image.create( self.client, b'', public=True, wait=True) self.assertIsInstance(a_image, models.Image) self.assertEqual(fingerprint, a_image.fingerprint) def test_create_with_metadata(self): """An image with metadata is created.""" fingerprint = hashlib.sha256(b'').hexdigest() a_image = models.Image.create( self.client, b'', metadata=b'', public=True, wait=True) self.assertIsInstance(a_image, models.Image) self.assertEqual(fingerprint, a_image.fingerprint) def test_create_with_metadata_streamed(self): """An image with metadata is created.""" fingerprint = hashlib.sha256(b'').hexdigest() a_image = models.Image.create( self.client, StringIO(u''), metadata=StringIO(u''), public=True, wait=True) self.assertIsInstance(a_image, models.Image) self.assertEqual(fingerprint, a_image.fingerprint) def test_update(self): """An image is updated.""" a_image = self.client.images.all()[0] a_image.sync() a_image.save() def test_fetch(self): """A partial object is fetched and populated.""" a_image = self.client.images.all()[0] a_image.sync() self.assertEqual(1, a_image.size) def test_fetch_notfound(self): """A bogus image fetch raises LXDAPIException.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA }) fingerprint = hashlib.sha256(b'').hexdigest() a_image = models.Image(self.client, fingerprint=fingerprint) self.assertRaises(exceptions.LXDAPIException, a_image.sync) def test_fetch_error(self): """A 500 error raises LXDAPIException.""" def not_found(request, context): context.status_code = 500 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 500}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA }) fingerprint = hashlib.sha256(b'').hexdigest() a_image = models.Image(self.client, fingerprint=fingerprint) self.assertRaises(exceptions.LXDAPIException, a_image.sync) def test_delete(self): """An image is deleted.""" # XXX: rockstar (03 Jun 2016) - This just executes # a code path. There should be an assertion here, but # it's not clear how to assert that, just yet. a_image = self.client.images.all()[0] a_image.delete(wait=True) def test_export(self): """An image is exported.""" expected = 'e2943f8d0b0e7d5835f9533722a6e25f669acb8980daee378b4edb44da212f51' # NOQA a_image = self.client.images.all()[0] data = a_image.export() data_sha = hashlib.sha256(data.read()).hexdigest() self.assertEqual(expected, data_sha) def test_export_not_found(self): """LXDAPIException is raised on export of bogus image.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/export$', # NOQA }) a_image = self.client.images.all()[0] self.assertRaises(exceptions.LXDAPIException, a_image.export) def test_export_error(self): """LXDAPIException is raised on API error.""" def error(request, context): context.status_code = 500 return json.dumps({ 'type': 'error', 'error': 'LOLOLOLOL', 'error_code': 500}) self.add_rule({ 'text': error, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/export$', # NOQA }) a_image = self.client.images.all()[0] self.assertRaises(exceptions.LXDAPIException, a_image.export) def test_add_alias(self): """Try to add an alias.""" a_image = self.client.images.all()[0] a_image.add_alias('lol', 'Just LOL') aliases = [a['name'] for a in a_image.aliases] self.assertTrue('lol' in aliases, "Image didn't get updated.") def test_add_alias_duplicate(self): """Adding a alias twice should raise an LXDAPIException.""" def error(request, context): context.status_code = 409 return json.dumps({ 'type': 'error', 'error': 'already exists', 'error_code': 409}) self.add_rule({ 'text': error, 'method': 'POST', 'url': r'^http://pylxd.test/1.0/images/aliases$', # NOQA }) a_image = self.client.images.all()[0] self.assertRaises( exceptions.LXDAPIException, a_image.add_alias, 'lol', 'Just LOL' ) def test_remove_alias(self): """Try to remove an-alias.""" a_image = self.client.images.all()[0] a_image.delete_alias('an-alias') self.assertEqual(0, len(a_image.aliases), "Alias didn't get deleted.") def test_remove_alias_error(self): """Try to remove an non existant alias.""" def error(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'not found', 'error_code': 404}) self.add_rule({ 'text': error, 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/images/aliases/lol$', # NOQA }) a_image = self.client.images.all()[0] self.assertRaises( exceptions.LXDAPIException, a_image.delete_alias, 'lol' ) def test_remove_alias_not_in_image(self): """Try to remove an alias which is not in the current image.""" a_image = self.client.images.all()[0] a_image.delete_alias('b-alias') def test_copy(self): """Try to copy an image to another LXD instance.""" from pylxd.client import Client a_image = self.client.images.all()[0] client2 = Client(endpoint='http://pylxd2.test') copied_image = a_image.copy(client2, wait=True) self.assertEqual(a_image.fingerprint, copied_image.fingerprint) def test_copy_public(self): """Try to copy a public image.""" from pylxd.client import Client def image_get(request, context): context.status_code = 200 return json.dumps({ 'type': 'sync', 'metadata': { 'aliases': [ { 'name': 'an-alias', # NOQA 'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA } ], 'architecture': 'x86_64', 'cached': False, 'filename': 'a_image.tar.bz2', 'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', # NOQA 'public': True, 'properties': {}, 'size': 1, 'auto_update': False, 'created_at': '1983-06-16T02:42:00Z', 'expires_at': '1983-06-16T02:42:00Z', 'last_used_at': '1983-06-16T02:42:00Z', 'uploaded_at': '1983-06-16T02:42:00Z', }, }) self.add_rule({ 'text': image_get, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$', # NOQA }) a_image = self.client.images.all()[0] self.assertTrue(a_image.public) client2 = Client(endpoint='http://pylxd2.test') copied_image = a_image.copy(client2, wait=True) self.assertEqual(a_image.fingerprint, copied_image.fingerprint) def test_copy_no_wait(self): """Try to copy and don't wait.""" from pylxd.client import Client a_image = self.client.images.all()[0] client2 = Client(endpoint='http://pylxd2.test') a_image.copy(client2, public=False, auto_update=False) def test_create_from_simplestreams(self): """Try to create an image from simplestreams.""" image = self.client.images.create_from_simplestreams( 'https://cloud-images.ubuntu.com/releases', 'trusty/amd64' ) self.assertEqual( 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', image.fingerprint ) def test_create_from_url(self): """Try to create an image from an URL.""" image = self.client.images.create_from_url( 'https://dl.stgraber.org/lxd' ) self.assertEqual( 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', image.fingerprint ) pylxd-2.2.6/pylxd/tests/models/test_profile.py0000664000175000017500000001174313250267630021437 0ustar alexalex00000000000000import json from pylxd import exceptions, models from pylxd.tests import testing class TestProfile(testing.PyLXDTestCase): """Tests for pylxd.models.Profile.""" def test_get(self): """A profile is fetched.""" name = 'an-profile' an_profile = models.Profile.get(self.client, name) self.assertEqual(name, an_profile.name) def test_get_not_found(self): """LXDAPIException is raised on unknown profiles.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/profiles/an-profile$', }) self.assertRaises( exceptions.LXDAPIException, models.Profile.get, self.client, 'an-profile') def test_get_error(self): """LXDAPIException is raised on get error.""" def error(request, context): context.status_code = 500 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 500}) self.add_rule({ 'text': error, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/profiles/an-profile$', }) self.assertRaises( exceptions.LXDAPIException, models.Profile.get, self.client, 'an-profile') def test_exists(self): name = 'an-profile' self.assertTrue(models.Profile.exists(self.client, name)) def test_not_exists(self): def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/profiles/an-profile$', }) name = 'an-profile' self.assertFalse(models.Profile.exists(self.client, name)) def test_all(self): """A list of all profiles is returned.""" profiles = models.Profile.all(self.client) self.assertEqual(1, len(profiles)) def test_create(self): """A new profile is created.""" an_profile = models.Profile.create( self.client, name='an-new-profile', config={}, devices={}) self.assertIsInstance(an_profile, models.Profile) self.assertEqual('an-new-profile', an_profile.name) def test_rename(self): """A profile is renamed.""" an_profile = models.Profile.get(self.client, 'an-profile') an_renamed_profile = an_profile.rename('an-renamed-profile') self.assertEqual('an-renamed-profile', an_renamed_profile.name) def test_update(self): """A profile is updated.""" # XXX: rockstar (03 Jun 2016) - This just executes # a code path. There should be an assertion here, but # it's not clear how to assert that, just yet. an_profile = models.Profile.get(self.client, 'an-profile') an_profile.save() self.assertEqual({}, an_profile.config) def test_fetch(self): """A partially fetched profile is made complete.""" an_profile = self.client.profiles.all()[0] an_profile.sync() self.assertEqual('An description', an_profile.description) def test_fetch_notfound(self): """LXDAPIException is raised on bogus profile fetches.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/profiles/an-profile$', }) an_profile = models.Profile(self.client, name='an-profile') self.assertRaises(exceptions.LXDAPIException, an_profile.sync) def test_fetch_error(self): """LXDAPIException is raised on fetch error.""" def error(request, context): context.status_code = 500 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 500}) self.add_rule({ 'text': error, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/profiles/an-profile$', }) an_profile = models.Profile(self.client, name='an-profile') self.assertRaises(exceptions.LXDAPIException, an_profile.sync) def test_delete(self): """A profile is deleted.""" # XXX: rockstar (03 Jun 2016) - This just executes # a code path. There should be an assertion here, but # it's not clear how to assert that, just yet. an_profile = self.client.profiles.all()[0] an_profile.delete() pylxd-2.2.6/pylxd/tests/models/test_certificate.py0000664000175000017500000000421013250267630022250 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os from pylxd import models from pylxd.tests import testing class TestCertificate(testing.PyLXDTestCase): """Tests for pylxd.models.Certificate.""" def test_get(self): """A certificate is retrieved.""" cert = self.client.certificates.get('an-certificate') self.assertEqual('certificate-content', cert.certificate) def test_all(self): """A certificates are returned.""" certs = self.client.certificates.all() self.assertIn('an-certificate', [c.fingerprint for c in certs]) def test_create(self): """A certificate is created.""" cert_data = open(os.path.join( os.path.dirname(__file__), '..', 'lxd.crt')).read().encode('utf-8') an_certificate = self.client.certificates.create( 'test-password', cert_data) self.assertEqual( 'eaf55b72fc23aa516d709271df9b0116064bf8cfa009cf34c67c33ad32c2320c', an_certificate.fingerprint) def test_fetch(self): """A partial object is fully fetched.""" an_certificate = models.Certificate( self.client, fingerprint='an-certificate') an_certificate.sync() self.assertEqual('certificate-content', an_certificate.certificate) def test_delete(self): """A certificate is deleted.""" # XXX: rockstar (08 Jun 2016) - This just executes a code path. An # assertion should be added. an_certificate = models.Certificate( self.client, fingerprint='an-certificate') an_certificate.delete() pylxd-2.2.6/pylxd/tests/models/test_model.py0000664000175000017500000001265213250267630021077 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from pylxd.models import _model as model from pylxd.tests import testing 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] class TestModel(testing.PyLXDTestCase): """Tests for pylxd.model.Model.""" def setUp(self): super(TestModel, self).setUp() self.add_rule({ 'json': { 'type': 'sync', 'metadata': { 'name': 'an-item', 'age': 1000, 'data': {'key': 'val'}, } }, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/items/an-item', }) self.add_rule({ 'json': { 'type': 'sync', 'metadata': {} }, 'method': 'PUT', 'url': r'^http://pylxd.test/1.0/items/an-item', }) self.add_rule({ 'json': { 'type': 'sync', 'metadata': {} }, 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/items/an-item', }) def test_init(self): """Initial attributes are set.""" item = Item(self.client, name='an-item', age=15, data={'key': 'val'}) self.assertEqual(self.client, item.client) self.assertEqual('an-item', item.name) def test_init_unknown_attribute(self): """Unknown attributes aren't set.""" item = Item(self.client, name='an-item', nonexistent='SRSLY') try: item.nonexistent self.fail('item.nonexistent did not raise AttributeError') except AttributeError: pass def test_unknown_attribute(self): """Setting unknown attributes raise an exception.""" def set_unknown_attribute(): item = Item(self.client, name='an-item') item.nonexistent = 'SRSLY' self.assertRaises(AttributeError, set_unknown_attribute) def test_get_unknown_attribute(self): """Setting unknown attributes raise an exception.""" def get_unknown_attribute(): item = Item(self.client, name='an-item') return item.nonexistent self.assertRaises(AttributeError, get_unknown_attribute) def test_unset_attribute_sync(self): """Reading unavailable attributes calls sync.""" item = Item(self.client, name='an-item') self.assertEqual(1000, item.age) def test_sync(self): """A sync will update attributes from the server.""" item = Item(self.client, name='an-item') item.sync() self.assertEqual(1000, item.age) def test_sync_dirty(self): """Sync will not overwrite local attribute changes.""" item = Item(self.client, name='an-item') item.age = 250 item.sync() self.assertEqual(250, item.age) def test_rollback(self): """Rollback resets the object from the server.""" item = Item(self.client, name='an-item', age=15, data={'key': 'val'}) item.age = 50 item.rollback() self.assertEqual(1000, item.age) self.assertFalse(item.dirty) def test_int_attribute_validator(self): """Age is set properly to be an int.""" item = Item(self.client) item.age = '100' self.assertEqual(100, item.age) def test_int_attribute_invalid(self): """TypeError is raised when data can't be converted to type.""" def set_string(): item = Item(self.client) item.age = 'abc' self.assertRaises(ValueError, set_string) def test_dirty(self): """Changes mark the object as dirty.""" item = Item(self.client, name='an-item', age=15, data={'key': 'val'}) item.age = 100 self.assertTrue(item.dirty) def test_not_dirty(self): """Changes mark the object as dirty.""" item = Item(self.client, name='an-item', age=15, data={'key': 'val'}) self.assertFalse(item.dirty) def test_marshall(self): """The object is marshalled into a dict.""" item = Item(self.client, name='an-item', age=15, data={'key': 'val'}) result = item.marshall() self.assertEqual({'age': 15, 'data': {'key': 'val'}}, result) def test_delete(self): """The object is deleted, and client is unset.""" item = Item(self.client, name='an-item', age=15, data={'key': 'val'}) item.delete() self.assertIsNone(item.client) def test_save(self): """Attributes are written to the server; object is marked clean.""" item = Item(self.client, name='an-item', age=15, data={'key': 'val'}) item.age = 69 item.save() self.assertFalse(item.dirty) pylxd-2.2.6/pylxd/tests/models/test_container.py0000664000175000017500000004455513250267630021770 0ustar alexalex00000000000000import json import mock from pylxd import exceptions, models from pylxd.tests import testing class TestContainer(testing.PyLXDTestCase): """Tests for pylxd.models.Container.""" def test_all(self): """A list of all containers are returned.""" containers = models.Container.all(self.client) self.assertEqual(1, len(containers)) def test_get(self): """Return a container.""" name = 'an-container' an_container = models.Container.get(self.client, name) self.assertEqual(name, an_container.name) def test_get_not_found(self): """LXDAPIException is raised when the container doesn't exist.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-missing-container$', # NOQA }) name = 'an-missing-container' self.assertRaises( exceptions.LXDAPIException, models.Container.get, self.client, name) def test_get_error(self): """LXDAPIException is raised when the LXD API errors.""" def not_found(request, context): context.status_code = 500 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 500}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-missing-container$', # NOQA }) name = 'an-missing-container' self.assertRaises( exceptions.LXDAPIException, models.Container.get, self.client, name) def test_create(self): """A new container is created.""" config = {'name': 'an-new-container'} an_new_container = models.Container.create( self.client, config, wait=True) self.assertEqual(config['name'], an_new_container.name) def test_exists(self): """A container exists.""" name = 'an-container' self.assertTrue(models.Container.exists(self.client, name)) def test_not_exists(self): """A container exists.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-missing-container$', # NOQA }) name = 'an-missing-container' self.assertFalse(models.Container.exists(self.client, name)) def test_fetch(self): """A sync updates the properties of a container.""" an_container = models.Container( self.client, name='an-container') an_container.sync() self.assertTrue(an_container.ephemeral) def test_fetch_not_found(self): """LXDAPIException is raised on a 404 for updating container.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-missing-container$', # NOQA }) an_container = models.Container( self.client, name='an-missing-container') self.assertRaises(exceptions.LXDAPIException, an_container.sync) def test_fetch_error(self): """LXDAPIException is raised on error.""" def not_found(request, context): context.status_code = 500 return json.dumps({ 'type': 'error', 'error': 'An bad error', 'error_code': 500}) self.add_rule({ 'text': not_found, 'method': 'GET', 'url': r'^http://pylxd.test/1.0/containers/an-missing-container$', # NOQA }) an_container = models.Container( self.client, name='an-missing-container') self.assertRaises(exceptions.LXDAPIException, an_container.sync) def test_update(self): """A container is updated.""" an_container = models.Container( self.client, name='an-container') an_container.architecture = 1 an_container.config = {} an_container.created_at = 1 an_container.devices = {} an_container.ephemeral = 1 an_container.expanded_config = {} an_container.expanded_devices = {} an_container.profiles = 1 an_container.status = 1 an_container.save(wait=True) self.assertTrue(an_container.ephemeral) def test_rename(self): an_container = models.Container( self.client, name='an-container') an_container.rename('an-renamed-container', wait=True) self.assertEqual('an-renamed-container', an_container.name) def test_delete(self): """A container is deleted.""" # XXX: rockstar (21 May 2016) - This just executes # a code path. There should be an assertion here, but # it's not clear how to assert that, just yet. an_container = models.Container( self.client, name='an-container') an_container.delete(wait=True) @testing.requires_ws4py @mock.patch('pylxd.models.container._StdinWebsocket') @mock.patch('pylxd.models.container._CommandWebsocketClient') def test_execute(self, _CommandWebsocketClient, _StdinWebsocket): """A command is executed on a container.""" fake_websocket = mock.Mock() fake_websocket.data = 'test\n' _StdinWebsocket.return_value = fake_websocket _CommandWebsocketClient.return_value = fake_websocket an_container = models.Container( self.client, name='an-container') result = an_container.execute(['echo', 'test']) self.assertEqual(0, result.exit_code) self.assertEqual('test\n', result.stdout) def test_execute_no_ws4py(self): """If ws4py is not installed, ValueError is raised.""" from pylxd.models import container old_installed = container._ws4py_installed container._ws4py_installed = False def cleanup(): container._ws4py_installed = old_installed self.addCleanup(cleanup) an_container = models.Container( self.client, name='an-container') self.assertRaises(ValueError, an_container.execute, ['echo', 'test']) @testing.requires_ws4py def test_execute_string(self): """A command passed as string raises a TypeError.""" an_container = models.Container( self.client, name='an-container') self.assertRaises(TypeError, an_container.execute, 'apt-get update') def test_migrate(self): """A container is migrated.""" from pylxd.client import Client client2 = Client(endpoint='http://pylxd2.test') an_container = models.Container( self.client, name='an-container') an_migrated_container = an_container.migrate(client2) self.assertEqual('an-container', an_migrated_container.name) self.assertEqual(client2, an_migrated_container.client) @mock.patch('pylxd.client._APINode.get') def test_migrate_local_client(self, get): """Migration from local clients is not supported.""" # Mock out the _APINode for the local instance. response = mock.Mock() response.json.return_value = {'metadata': {'fake': 'response'}} response.status_code = 200 get.return_value = response from pylxd.client import Client client2 = Client(endpoint='http+unix://pylxd2.test') an_container = models.Container( client2, name='an-container') self.assertRaises(ValueError, an_container.migrate, self.client) def test_publish(self): """Containers can be published.""" self.add_rule({ 'text': json.dumps({ 'type': 'sync', 'metadata': { 'id': 'operation-abc', 'metadata': { 'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' # NOQA } } }), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/operations/operation-abc$', }) an_container = models.Container( self.client, name='an-container') image = an_container.publish(wait=True) self.assertEqual( 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', image.fingerprint) class TestContainerState(testing.PyLXDTestCase): """Tests for pylxd.models.ContainerState.""" def test_get(self): """Return a container.""" name = 'an-container' an_container = models.Container.get(self.client, name) state = an_container.state() self.assertEqual('Running', state.status) self.assertEqual(103, state.status_code) def test_start(self): """A container is started.""" an_container = models.Container.get(self.client, 'an-container') an_container.start(wait=True) def test_stop(self): """A container is stopped.""" an_container = models.Container.get(self.client, 'an-container') an_container.stop() def test_restart(self): """A container is restarted.""" an_container = models.Container.get(self.client, 'an-container') an_container.restart() def test_freeze(self): """A container is suspended.""" an_container = models.Container.get(self.client, 'an-container') an_container.freeze() def test_unfreeze(self): """A container is resumed.""" an_container = models.Container.get(self.client, 'an-container') an_container.unfreeze() class TestContainerSnapshots(testing.PyLXDTestCase): """Tests for pylxd.models.Container.snapshots.""" def setUp(self): super(TestContainerSnapshots, self).setUp() self.container = models.Container.get(self.client, 'an-container') def test_get(self): """Return a specific snapshot.""" snapshot = self.container.snapshots.get('an-snapshot') self.assertEqual('an-snapshot', snapshot.name) def test_all(self): """Return all snapshots.""" snapshots = self.container.snapshots.all() self.assertEqual(1, len(snapshots)) self.assertEqual('an-snapshot', snapshots[0].name) self.assertEqual(self.client, snapshots[0].client) self.assertEqual(self.container, snapshots[0].container) def test_create(self): """Create a snapshot.""" snapshot = self.container.snapshots.create( 'an-snapshot', stateful=True, wait=True) self.assertEqual('an-snapshot', snapshot.name) class TestSnapshot(testing.PyLXDTestCase): """Tests for pylxd.models.Snapshot.""" def setUp(self): super(TestSnapshot, self).setUp() self.container = models.Container.get(self.client, 'an-container') def test_rename(self): """A snapshot is renamed.""" snapshot = models.Snapshot( self.client, container=self.container, name='an-snapshot') snapshot.rename('an-renamed-snapshot', wait=True) self.assertEqual('an-renamed-snapshot', snapshot.name) def test_delete(self): """A snapshot is deleted.""" snapshot = models.Snapshot( self.client, container=self.container, name='an-snapshot') snapshot.delete(wait=True) # TODO: add an assertion here def test_delete_failure(self): """If the response indicates delete failure, raise an exception.""" def not_found(request, context): context.status_code = 404 return json.dumps({ 'type': 'error', 'error': 'Not found', 'error_code': 404}) self.add_rule({ 'text': not_found, 'method': 'DELETE', 'url': r'^http://pylxd.test/1.0/containers/an-container/snapshots/an-snapshot$', # NOQA }) snapshot = models.Snapshot( self.client, container=self.container, name='an-snapshot') self.assertRaises(exceptions.LXDAPIException, snapshot.delete) def test_publish(self): """Snapshots can be published.""" self.add_rule({ 'text': json.dumps({ 'type': 'sync', 'metadata': { 'id': 'operation-abc', 'metadata': { 'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' # NOQA } } }), 'method': 'GET', 'url': r'^http://pylxd.test/1.0/operations/operation-abc$', }) snapshot = models.Snapshot( self.client, container=self.container, name='an-snapshot') image = snapshot.publish(wait=True) self.assertEqual( 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', image.fingerprint) class TestFiles(testing.PyLXDTestCase): """Tests for pylxd.models.Container.files.""" def setUp(self): super(TestFiles, self).setUp() self.container = models.Container.get(self.client, 'an-container') def test_put_delete(self): """A file is put on the container and then deleted""" data = 'The quick brown fox' res = self.container.files.put('/tmp/putted', data) self.assertEqual(True, res, msg=('Failed to put file, result: {}' .format(res))) # NOQA # we are mocked, so delete should initially not be available self.assertEqual(False, self.container.files.delete_available()) self.assertRaises(ValueError, self.container.files.delete, '/tmp/putted') # Now insert delete rule = { 'text': json.dumps({ 'type': 'sync', 'metadata': {'auth': 'trusted', 'environment': { 'certificate': 'an-pem-cert', }, 'api_extensions': ['file_delete'] }}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0$', } self.add_rule(rule) # Update hostinfo self.client.host_info = self.client.api.get().json()['metadata'] self.assertEqual(True, self.container.files.delete_available()) res = self.container.files.delete('/tmp/putted') self.assertEqual(True, res, msg=('Failed to delete file, result: {}' .format(res))) # NOQA def test_put_mode_uid_gid(self): """Should be able to set the mode, uid and gid of a file""" # fix up the default POST rule to allow us to see the posted vars _capture = {} def capture(request, context): _capture['headers'] = getattr(request._request, 'headers') context.status_code = 200 rule = { 'text': capture, 'method': 'POST', 'url': (r'^http://pylxd.test/1.0/containers/an-container/files' '\?path=%2Ftmp%2Fputted$'), # NOQA } self.add_rule(rule) data = 'The quick brown fox' # start with an octal mode res = self.container.files.put('/tmp/putted', data, mode=0o123, uid=1, gid=2) self.assertEqual(True, res, msg=('Failed to put file, result: {}' .format(res))) # NOQA headers = _capture['headers'] self.assertEqual(headers['X-LXD-mode'], '0123') self.assertEqual(headers['X-LXD-uid'], '1') self.assertEqual(headers['X-LXD-gid'], '2') # use a str mode this type res = self.container.files.put('/tmp/putted', data, mode='456') self.assertEqual(True, res, msg=('Failed to put file, result: {}' .format(res))) # NOQA headers = _capture['headers'] self.assertEqual(headers['X-LXD-mode'], '0456') # check that mode='0644' also works (i.e. already has 0 prefix) res = self.container.files.put('/tmp/putted', data, mode='0644') self.assertEqual(True, res, msg=('Failed to put file, result: {}' .format(res))) # NOQA headers = _capture['headers'] self.assertEqual(headers['X-LXD-mode'], '0644') # check that assertion is raised with self.assertRaises(ValueError): res = self.container.files.put('/tmp/putted', data, mode=object) def test_get(self): """A file is retrieved from the container.""" data = self.container.files.get('/tmp/getted') self.assertEqual(b'This is a getted file', data) def test_get_not_found(self): """LXDAPIException is raised on bogus filenames.""" def not_found(request, context): context.status_code = 500 rule = { 'text': not_found, 'method': 'GET', 'url': (r'^http://pylxd.test/1.0/containers/an-container/files' '\?path=%2Ftmp%2Fgetted$'), # NOQA } self.add_rule(rule) self.assertRaises( exceptions.LXDAPIException, self.container.files.get, '/tmp/getted') def test_get_error(self): """LXDAPIException is raised on error.""" def not_found(request, context): context.status_code = 503 rule = { 'text': not_found, 'method': 'GET', 'url': (r'^http://pylxd.test/1.0/containers/an-container/files' '\?path=%2Ftmp%2Fgetted$'), # NOQA } self.add_rule(rule) self.assertRaises( exceptions.LXDAPIException, self.container.files.get, '/tmp/getted') pylxd-2.2.6/pylxd/tests/models/test_network.py0000664000175000017500000000332213250267630021462 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from pylxd import models from pylxd.tests import testing class TestNetwork(testing.PyLXDTestCase): """Tests for pylxd.models.Network.""" def test_all(self): """A list of all networks are returned.""" networks = models.Network.all(self.client) self.assertEqual(1, len(networks)) def test_get(self): """Return a container.""" name = 'lo' an_network = models.Network.get(self.client, name) self.assertEqual(name, an_network.name) def test_partial(self): """A partial network is synced.""" an_network = models.Network(self.client, name='lo') self.assertEqual('loopback', an_network.type) def test_delete(self): """delete is not implemented in networks.""" an_network = models.Network(self.client, name='lo') with self.assertRaises(NotImplementedError): an_network.delete() def test_save(self): """save is not implemented in networks.""" an_network = models.Network(self.client, name='lo') with self.assertRaises(NotImplementedError): an_network.save() pylxd-2.2.6/pylxd/tests/test_client.py0000664000175000017500000003150013250267630017763 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import os import os.path import unittest import mock import requests import requests_unixsocket from pylxd import client, exceptions from pylxd.tests.testing import requires_ws4py class TestClient(unittest.TestCase): """Tests for pylxd.client.Client.""" def setUp(self): self.get_patcher = mock.patch('pylxd.client._APINode.get') self.get = self.get_patcher.start() self.post_patcher = mock.patch('pylxd.client._APINode.post') self.post = self.post_patcher.start() response = mock.MagicMock(status_code=200) response.json.return_value = {'metadata': { 'auth': 'trusted', 'environment': {'storage': 'zfs'}, }} self.get.return_value = response post_response = mock.MagicMock(status_code=200) self.post.return_value = post_response def tearDown(self): self.get_patcher.stop() self.post_patcher.stop() @mock.patch('os.path.exists') def test_create(self, _path_exists): """Client creation sets default API endpoint.""" _path_exists.return_value = False expected = 'http+unix://%2Fvar%2Flib%2Flxd%2Funix.socket/1.0' an_client = client.Client() self.assertEqual(expected, an_client.api._api_endpoint) @mock.patch('os.path.exists') @mock.patch('os.environ') def test_create_with_snap_lxd(self, _environ, _path_exists): # """Client creation sets default API endpoint.""" _path_exists.return_value = True expected = ('http+unix://%2Fvar%2Fsnap%2Flxd%2F' 'common%2Flxd%2Funix.socket/1.0') an_client = client.Client() self.assertEqual(expected, an_client.api._api_endpoint) def test_create_LXD_DIR(self): """When LXD_DIR is set, use it in the client.""" os.environ['LXD_DIR'] = '/lxd' expected = 'http+unix://%2Flxd%2Funix.socket/1.0' an_client = client.Client() self.assertEqual(expected, an_client.api._api_endpoint) def test_create_endpoint(self): """Explicitly set the client endpoint.""" endpoint = 'http://lxd' expected = 'http://lxd/1.0' an_client = client.Client(endpoint=endpoint) self.assertEqual(expected, an_client.api._api_endpoint) def test_create_endpoint_unixsocket(self): """Test with unix socket endpoint.""" endpoint = '/tmp/unix.socket' expected = 'http+unix://%2Ftmp%2Funix.socket/1.0' real_isfile = os.path.isfile os.path.isfile = lambda x: True an_client = client.Client(endpoint) os.path.isfile = real_isfile self.assertEqual(expected, an_client.api._api_endpoint) def test_connection_404(self): """If the endpoint 404s, an exception is raised.""" response = mock.MagicMock(status_code=404) self.get.return_value = response self.assertRaises(exceptions.ClientConnectionFailed, client.Client) def test_connection_failed(self): """If the connection fails, an exception is raised.""" def raise_exception(): raise requests.exceptions.ConnectionError() self.get.side_effect = raise_exception self.get.return_value = None self.assertRaises(exceptions.ClientConnectionFailed, client.Client) def test_connection_untrusted(self): """Client.trusted is False when certs are untrusted.""" response = mock.MagicMock(status_code=200) response.json.return_value = {'metadata': {'auth': 'untrusted'}} self.get.return_value = response an_client = client.Client() self.assertFalse(an_client.trusted) def test_connection_trusted(self): """Client.trusted is True when certs are untrusted.""" response = mock.MagicMock(status_code=200) response.json.return_value = {'metadata': {'auth': 'trusted'}} self.get.return_value = response an_client = client.Client() self.assertTrue(an_client.trusted) def test_authenticate(self): """A client is authenticated.""" response = mock.MagicMock(status_code=200) response.json.return_value = {'metadata': {'auth': 'untrusted'}} self.get.return_value = response certs = ( os.path.join(os.path.dirname(__file__), 'lxd.crt'), os.path.join(os.path.dirname(__file__), 'lxd.key')) an_client = client.Client('https://lxd', cert=certs) get_count = [] def _get(*args, **kwargs): if len(get_count) == 0: get_count.append(None) return {'metadata': { 'type': 'client', 'fingerprint': 'eaf55b72fc23aa516d709271df9b0116064bf8cfa009cf34c67c33ad32c2320c', # NOQA }} else: return {'metadata': {'auth': 'trusted'}} response = mock.MagicMock(status_code=200) response.json.side_effect = _get self.get.return_value = response an_client.authenticate('test-password') self.assertTrue(an_client.trusted) def test_authenticate_already_authenticated(self): """If the client is already authenticated, nothing happens.""" an_client = client.Client() an_client.authenticate('test-password') self.assertTrue(an_client.trusted) def test_host_info(self): """Perform a host query.""" an_client = client.Client() self.assertEqual('zfs', an_client.host_info['environment']['storage']) @requires_ws4py def test_events(self): """The default websocket client is returned.""" an_client = client.Client() ws_client = an_client.events() self.assertEqual('/1.0/events', ws_client.resource) def test_events_no_ws4py(self): """No ws4py will result in a ValueError.""" from pylxd import client old_installed = client._ws4py_installed client._ws4py_installed = False def cleanup(): client._ws4py_installed = old_installed self.addCleanup(cleanup) an_client = client.Client() self.assertRaises(ValueError, an_client.events) client._ws4py_installed @requires_ws4py def test_events_unix_socket(self): """A unix socket compatible websocket client is returned.""" websocket_client = mock.Mock(resource=None) WebsocketClient = mock.Mock() WebsocketClient.return_value = websocket_client os.environ['LXD_DIR'] = '/lxd' an_client = client.Client() an_client.events(websocket_client=WebsocketClient) WebsocketClient.assert_called_once_with('ws+unix:///lxd/unix.socket') @requires_ws4py def test_events_htt(self): """An http compatible websocket client is returned.""" websocket_client = mock.Mock(resource=None) WebsocketClient = mock.Mock() WebsocketClient.return_value = websocket_client an_client = client.Client('http://lxd.local') an_client.events(websocket_client=WebsocketClient) WebsocketClient.assert_called_once_with('ws://lxd.local') @requires_ws4py def test_events_https(self): """An https compatible websocket client is returned.""" websocket_client = mock.Mock(resource=None) WebsocketClient = mock.Mock() WebsocketClient.return_value = websocket_client an_client = client.Client('https://lxd.local') an_client.events(websocket_client=WebsocketClient) WebsocketClient.assert_called_once_with('wss://lxd.local') class TestAPINode(unittest.TestCase): """Tests for pylxd.client._APINode.""" def test_getattr(self): """API Nodes can use object notation for nesting.""" node = client._APINode('http://test.com') new_node = node.test self.assertEqual( 'http://test.com/test', new_node._api_endpoint) def test_getitem(self): """API Nodes can use dict notation for nesting.""" node = client._APINode('http://test.com') new_node = node['test'] self.assertEqual( 'http://test.com/test', new_node._api_endpoint) def test_session_http(self): """HTTP nodes return the default requests session.""" node = client._APINode('http://test.com') self.assertIsInstance(node.session, requests.Session) def test_session_unix_socket(self): """HTTP nodes return a requests_unixsocket session.""" node = client._APINode('http+unix://test.com') self.assertIsInstance(node.session, requests_unixsocket.Session) @mock.patch('pylxd.client.requests.Session') def test_get(self, Session): """Perform a session get.""" response = mock.Mock(**{ 'status_code': 200, 'json.return_value': {'type': 'sync'}, }) session = mock.Mock(**{'get.return_value': response}) Session.return_value = session node = client._APINode('http://test.com') node.get() session.get.assert_called_once_with('http://test.com', timeout=None) @mock.patch('pylxd.client.requests.Session') def test_post(self, Session): """Perform a session post.""" response = mock.Mock(**{ 'status_code': 200, 'json.return_value': {'type': 'sync'}, }) session = mock.Mock(**{'post.return_value': response}) Session.return_value = session node = client._APINode('http://test.com') node.post() session.post.assert_called_once_with('http://test.com', timeout=None) @mock.patch('pylxd.client.requests.Session') def test_post_200_not_sync(self, Session): """A status code of 200 with async request raises an exception.""" response = mock.Mock(**{ 'status_code': 200, 'json.return_value': {'type': 'async'}, }) session = mock.Mock(**{'post.return_value': response}) Session.return_value = session node = client._APINode('http://test.com') self.assertRaises( exceptions.LXDAPIException, node.post) @mock.patch('pylxd.client.requests.Session') def test_post_missing_type_200(self, Session): """A missing response type raises an exception.""" response = mock.Mock(**{ 'status_code': 200, 'json.return_value': {}, }) session = mock.Mock(**{'post.return_value': response}) Session.return_value = session node = client._APINode('http://test.com') self.assertRaises( exceptions.LXDAPIException, node.post) @mock.patch('pylxd.client.requests.Session') def test_put(self, Session): """Perform a session put.""" response = mock.Mock(**{ 'status_code': 200, 'json.return_value': {'type': 'sync'}, }) session = mock.Mock(**{'put.return_value': response}) Session.return_value = session node = client._APINode('http://test.com') node.put() session.put.assert_called_once_with('http://test.com', timeout=None) @mock.patch('pylxd.client.requests.Session') def test_delete(self, Session): """Perform a session delete.""" response = mock.Mock(**{ 'status_code': 200, 'json.return_value': {'type': 'sync'}, }) session = mock.Mock(**{'delete.return_value': response}) Session.return_value = session node = client._APINode('http://test.com') node.delete() session.delete.assert_called_once_with('http://test.com', timeout=None) class TestWebsocketClient(unittest.TestCase): """Tests for pylxd.client.WebsocketClient.""" @requires_ws4py def test_handshake_ok(self): """A `message` attribute of an empty list is created.""" ws_client = client._WebsocketClient('ws://an/fake/path') ws_client.handshake_ok() self.assertEqual([], ws_client.messages) @requires_ws4py def test_received_message(self): """A json dict is added to the messages attribute.""" message = mock.Mock(data=json.dumps({'test': 'data'}).encode('utf-8')) ws_client = client._WebsocketClient('ws://an/fake/path') ws_client.handshake_ok() ws_client.received_message(message) self.assertEqual({'test': 'data'}, ws_client.messages[0]) pylxd-2.2.6/pylxd/tests/__init__.py0000644000175000017500000000000013115536516017174 0ustar alexalex00000000000000pylxd-2.2.6/pylxd/tests/lxd.key0000644000175000017500000000325013115536516016376 0ustar alexalex00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3AJG8ghJ/gAAs bQA7rXweFHjkz7XQhW/BGYMrXNijwtgofEjmyDaI9yeOJQtHkDRm/6qcgQQZAE0T 8SajusUx8a1l2Zk6viBIBbFUNSu/PFLPmBGx0FXCkmHHVqLgZAvG6N+Midx8njt9 lUc07p4zCt/4AjdaRAedziogN1LH7NUE8XvAjE+XVXwBp0nTHEGbqShL7t8RHRkQ TjzyeOuidUkp6w/qd2Qo7Fan13u1hdopTKFOGqwVzwGTlZB/lekYOKr4CsWV1N/B LmAXFC1noErH5q+3AY7KZnTyf0hymxzmJF2Y0RR31EBu0nb1Q2kMedWoEwj0u5zk OngHVvGTAgMBAAECggEBAKBUxVpM83v1XzGNBilC431PHmQJfxeD8NdTTNKO89b1 /H/r88sOGomBUIx+9BTsyJx83rNjbX2h/+W638mO9vm87dhP/qmyrYGsSyKluwA/ D6aFautIxfpEWZpV0zmZLaBFoqX0mtIrp59tTAeaD8xUeMlG18wj0jB10f6LueEh povKfcO0kFTDqDO5rTNdP9H4P9wUfHDaBzHR90aq6nP2vWvQ/XkKSF9IDYSFzYvD rhEDkQhV2JllEnSOfIGPP+/J25Sn9Dq9S6fq/cs4/oi98/0seZZ8A8vKOHpk8aqS IFurWs0x4XhYmlLlW/3TpJrN2HT02g9JX2W4tDDt2MECgYEA70He73UIMgSVoIUn 7K0p9E62qO8DODvDPPa+hXNbcyiF8f83vANW/UqNLvisAo8GjnhvKP4Ohr+YjZAo Nl9m6HD0TRoK8jsyuQgvu1v4lZH651SHaesCKm1CLo2tm0Ho/EEtf4ccqSOeN4fe Zd0Snz95E4NV9Y4ID7VvBRSw1EMCgYEAw87tcjPpQLEPADx9ylL5xOictQrPhmMc xxU/0KPMj6OQOZPW+zj1paof7fGWTuvMtuE7oAMLb+5v+AqFXGiFGVuiKL72CiV0 QyUTV12Qc8r/NPFyznktffTo+a11xKBdeUNZupXxGM82dbXdybnXO3bxPvhMi/xr 4cq8Y3OdwHECgYAXN/VCl8Dr2bYLleCB/2wK4XiofEl7s5EG4YsruD4vtscI7ROj k09l1U5OOKO4u9iPCvD+sWkHeqB7XHoKjMeX1x5ePSDC0Svi+QBo1kwRd9E5keJy TPQw2dmKWwV2A7dwg4K+1YXahDJegTj7+bBM9APz+NLmuZnerGTRwWhHsQKBgE/V 8ghrVAJVbtlY0K0Ksd3wPdyvILgZdyVQ66kE8CXsuaRQPApIShgWylf49aEOEXTL VsVCGIq1vB91IrTvxLz3GKHmYmj2pnWuCznG41vi+7U5cObwj3TYw5jxeaAHBrWn mVEzS48jBYBu+5QBWtlbALf9AzDcZZw1TiR6gmpxAoGBAOYsPhNd0KpfQkuZWTtF P59lKCIBRYo3uJ2F3vIKyic+LfDzDD1bD63+jCcL0KcqzaIlLctXorsNikXJB97C XJjSbr0HMmACRo1Rf+fqLly/y+lCF1azOYM0g4x6O+0F0Xd8/NwYeY5p5WuD41of /jgzLEJI/GauStHGpqCQo6FI -----END PRIVATE KEY----- pylxd-2.2.6/pylxd/tests/testing.py0000644000175000017500000000163013115536516017124 0ustar alexalex00000000000000import unittest import mock_services from pylxd.client import Client from pylxd.tests import mock_lxd def requires_ws4py(f): """Marks a test as requiring ws4py. As ws4py is an optional dependency, some tests should be skipped, as they won't run properly if ws4py is not installed. """ try: import ws4py # NOQA return f except ImportError: return unittest.skip('ws4py is not installed')(f) class PyLXDTestCase(unittest.TestCase): """A test case for handling mocking of LXD services.""" def setUp(self): mock_services.update_http_rules(mock_lxd.RULES) mock_services.start_http_mock() self.client = Client(endpoint='http://pylxd.test') def tearDown(self): mock_services.stop_http_mock() def add_rule(self, rule): """Add a rule to the mock LXD service.""" mock_services.update_http_rules([rule]) pylxd-2.2.6/pylxd/tests/lxd.crt0000644000175000017500000000212713115536516016400 0ustar alexalex00000000000000-----BEGIN CERTIFICATE----- MIIDBjCCAe4CCQCo5Wv+umHW/TANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 cyBQdHkgTHRkMB4XDTE2MDYwNzIzMjQxNVoXDTE5MDMwNDIzMjQxNVowRTELMAkG A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB ALcAkbyCEn+AACxtADutfB4UeOTPtdCFb8EZgytc2KPC2Ch8SObINoj3J44lC0eQ NGb/qpyBBBkATRPxJqO6xTHxrWXZmTq+IEgFsVQ1K788Us+YEbHQVcKSYcdWouBk C8bo34yJ3HyeO32VRzTunjMK3/gCN1pEB53OKiA3Usfs1QTxe8CMT5dVfAGnSdMc QZupKEvu3xEdGRBOPPJ466J1SSnrD+p3ZCjsVqfXe7WF2ilMoU4arBXPAZOVkH+V 6Rg4qvgKxZXU38EuYBcULWegSsfmr7cBjspmdPJ/SHKbHOYkXZjRFHfUQG7SdvVD aQx51agTCPS7nOQ6eAdW8ZMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYf0ByG0g JmpsphNk2MEB0gtxy1DesLwOqKAAzUgzmeU0FtEPFNiRqpvjR7W2ZyPN9dvovrY1 kLrXaRTeLUC5tl3uoAXaMkb3q8ZTrEZ+f70L+5v5LpJ2l6i/v06BIT8tH511IihP vX/q0YdD1Pyab/dJcHpLMWwTY3kOHEoA6xUzRSN9CT4LdNg/EIjpt7LxrbWqOxLD WsfSbG6NKRoSzHtTHLNcD9+E0xc0/OHR8JAw3I7J39VrAphvqc8dwOTuU4nlq2RF xduGTfVb09Zo/yEaocQYsI/yIEyTAfO8mMaexq/ZNjYIzs8JZQds4Zx9HRu3UrcQ DVvz7DMNIzu3yQ== -----END CERTIFICATE----- pylxd-2.2.6/pylxd/deprecated/0000775000175000017500000000000013250466213016031 5ustar alexalex00000000000000pylxd-2.2.6/pylxd/deprecated/container.py0000644000175000017500000002031713115536516020372 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from pylxd.deprecated import base from pylxd.deprecated import exceptions class LXDContainer(base.LXDBase): # containers: def container_list(self): (state, data) = self.connection.get_object('GET', '/1.0/containers') return [container.split('/1.0/containers/')[-1] for container in data['metadata']] def container_running(self, container): (state, data) = self.connection.get_object( 'GET', '/1.0/containers/%s/state' % container) data = data.get('metadata') container_running = False if data['status'].upper() in ['RUNNING', 'STARTING', 'FREEZING', 'FROZEN', 'THAWED']: container_running = True return container_running def container_init(self, container): return self.connection.get_object('POST', '/1.0/containers', json.dumps(container)) def container_update(self, container, config): return self.connection.get_object('PUT', '/1.0/containers/%s' % container, json.dumps(config)) def container_defined(self, container): _, data = self.connection.get_object('GET', '/1.0/containers') try: containers = data["metadata"] except KeyError: raise exceptions.PyLXDException("no metadata in GET containers?") container_url = "/1.0/containers/%s" % container for ct in containers: if ct == container_url: return True return False def container_state(self, container): return self.connection.get_object( 'GET', '/1.0/containers/%s/state' % container) def container_start(self, container, timeout): action = {'action': 'start', 'force': True, 'timeout': timeout} return self.connection.get_object('PUT', '/1.0/containers/%s/state' % container, json.dumps(action)) def container_stop(self, container, timeout): action = {'action': 'stop', 'force': True, 'timeout': timeout} return self.connection.get_object('PUT', '/1.0/containers/%s/state' % container, json.dumps(action)) def container_suspend(self, container, timeout): action = {'action': 'freeze', 'force': True, 'timeout': timeout} return self.connection.get_object('PUT', '/1.0/containers/%s/state' % container, json.dumps(action)) def container_resume(self, container, timeout): action = {'action': 'unfreeze', 'force': True, 'timeout': timeout} return self.connection.get_object('PUT', '/1.0/containers/%s/state' % container, json.dumps(action)) def container_reboot(self, container, timeout): action = {'action': 'restart', 'force': True, 'timeout': timeout} return self.connection.get_object('PUT', '/1.0/containers/%s/state' % container, json.dumps(action)) def container_destroy(self, container): return self.connection.get_object('DELETE', '/1.0/containers/%s' % container) def get_container_log(self, container): (state, data) = self.connection.get_object( 'GET', '/1.0/containers/%s?log=true' % container) return data['metadata']['log'] def get_container_config(self, container): (state, data) = self.connection.get_object( 'GET', '/1.0/containers/%s?log=false' % container) return data['metadata'] def get_container_websocket(self, container): return self.connection.get_status( 'GET', '/1.0/operations/%s/websocket?secret=%s' % (container['operation'], container['fs'])) def container_info(self, container): (state, data) = self.connection.get_object( 'GET', '/1.0/containers/%s/state' % container) return data['metadata'] def container_migrate(self, container): action = {'migration': True} return self.connection.get_object( 'POST', '/1.0/containers/%s' % container, json.dumps(action)) def container_migrate_sync(self, operation_id, container_secret): return self.connection.get_ws( '/1.0/operations/%s/websocket?secret=%s' % (operation_id, container_secret)) def container_local_copy(self, container): return self.connection.get_object( 'POST', '/1.0/containers', json.dumps(container)) def container_local_move(self, instance, config): return self.connection.get_object( 'POST', '/1.0/containers/%s' % instance, json.dumps(config)) # file operations def get_container_file(self, container, filename): return self.connection.get_raw( 'GET', '/1.0/containers/%s/files?path=%s' % (container, filename)) def put_container_file(self, container, src_file, dst_file, uid, gid, mode): with open(src_file, 'rb') as f: data = f.read() return self.connection.get_object( 'POST', '/1.0/containers/%s/files?path=%s' % (container, dst_file), body=data, headers={'X-LXD-uid': uid, 'X-LXD-gid': gid, 'X-LXD-mode': mode}) def container_publish(self, container): return self.connection.get_object('POST', '/1.0/images', json.dumps(container)) # misc operations def run_command(self, container, args, interactive, web_sockets, env): env = env or {} data = {'command': args, 'interactive': interactive, 'wait-for-websocket': web_sockets, 'environment': env} return self.connection.get_object('POST', '/1.0/containers/%s/exec' % container, json.dumps(data)) # snapshots def snapshot_list(self, container): (state, data) = self.connection.get_object( 'GET', '/1.0/containers/%s/snapshots' % container) return [snapshot.split('/1.0/containers/%s/snapshots/%s/' % (container, container))[-1] for snapshot in data['metadata']] def snapshot_create(self, container, config): return self.connection.get_object('POST', '/1.0/containers/%s/snapshots' % container, json.dumps(config)) def snapshot_info(self, container, snapshot): return self.connection.get_object('GET', '/1.0/containers/%s/snapshots/%s' % (container, snapshot)) def snapshot_rename(self, container, snapshot, config): return self.connection.get_object('POST', '/1.0/containers/%s/snapshots/%s' % (container, snapshot), json.dumps(config)) def snapshot_delete(self, container, snapshot): return self.connection.get_object('DELETE', '/1.0/containers/%s/snapshots/%s' % (container, snapshot)) pylxd-2.2.6/pylxd/deprecated/profiles.py0000644000175000017500000000441413115536516020233 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from pylxd.deprecated import base class LXDProfile(base.LXDBase): def profile_list(self): '''List profiles on the LXD daemon as an array.''' (state, data) = self.connection.get_object('GET', '/1.0/profiles') return [profiles.split('/1.0/profiles/')[-1] for profiles in data['metadata']] def profile_create(self, profile): '''Create an LXD Profile''' return self.connection.get_status('POST', '/1.0/profiles', json.dumps(profile)) def profile_show(self, profile): '''Display the LXD profile''' return self.connection.get_object('GET', '/1.0/profiles/%s' % profile) def profile_defined(self, profile): '''Check for an LXD profile''' return self.connection.get_status('GET', '/1.0/profiles/%s' % profile) def profile_update(self, profile, config): '''Update the LXD profile (not implemented)''' return self.connection.get_status('PUT', '/1.0/profiles/%s' % profile, json.dumps(config)) def profile_rename(self, profile, config): '''Rename the LXD profile''' return self.connection.get_status('POST', '/1.0/profiles/%s' % profile, json.dumps(config)) def profile_delete(self, profile): '''Delete the LXD profile''' return self.connection.get_status('DELETE', '/1.0/profiles/%s' % profile) pylxd-2.2.6/pylxd/deprecated/certificate.py0000644000175000017500000000266513115536516020700 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from pylxd.deprecated import base class LXDCertificate(base.LXDBase): def certificate_list(self): (state, data) = self.connection.get_object('GET', '/1.0/certificates') return [certificate.split('/1.0/certificates/')[-1] for certificate in data['metadata']] def certificate_show(self, fingerprint): return self.connection.get_object('GET', '/1.0/certificates/%s' % fingerprint) def certificate_create(self, certificate): return self.connection.get_status('POST', '/1.0/certificates', json.dumps(certificate)) def certificate_delete(self, fingerprint): return self.connection.get_status('DELETE', '/1.0/certificates/%s' % fingerprint) pylxd-2.2.6/pylxd/deprecated/utils.py0000644000175000017500000000157113115536516017551 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from pylxd.deprecated import exceptions def wait_for_container(name, timeout): pass def block_container(): pass def get_lxd_error(state, data): status_code = data.get('error_code') error = data.get('error') raise exceptions.APIError(error, status_code) pylxd-2.2.6/pylxd/deprecated/network.py0000644000175000017500000000441713115536516020104 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from pylxd.deprecated import base class LXDNetwork(base.LXDBase): def network_list(self): (state, data) = self.connection.get_object('GET', '/1.0/networks') return [network.split('/1.0/networks/')[-1] for network in data['metadata']] def network_show(self, network): '''Show details of the LXD network''' (state, data) = self.connection.get_object('GET', '/1.0/networks/%s' % network) return { 'network_name': self.show_network_name(network, data.get('metadata')), 'network_type': self.show_network_type(network, data.get('metadata')), 'network_members': self.show_network_members(network, data.get('metadata')) } def show_network_name(self, network, data): '''Show the LXD network name''' if data is None: (state, data) = self.connection.get_object( 'GET', '/1.0/networks/%s' % network) data = data.get('metadata') return data['name'] def show_network_type(self, network, data): if data is None: (state, data) = self.connection.get_object( 'GET', '/1.0/networks/%s' % network) data = data.get('metadata') return data['type'] def show_network_members(self, network, data): if data is None: (state, data) = self.connection.get_object( 'GET', '/1.0/networks/%s' % network) data = data.get('metadata') return [network_members.split('/1.0/networks/')[-1] for network_members in data['members']] pylxd-2.2.6/pylxd/deprecated/base.py0000644000175000017500000000145213115536516017321 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import print_function from pylxd.deprecated import connection class LXDBase(object): def __init__(self, conn=None): self.connection = conn or connection.LXDConnection() pylxd-2.2.6/pylxd/deprecated/__init__.py0000644000175000017500000000000013115536516020132 0ustar alexalex00000000000000pylxd-2.2.6/pylxd/deprecated/api.py0000644000175000017500000002515413115536516017165 0ustar alexalex00000000000000 # Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import warnings from pylxd.deprecated import certificate from pylxd.deprecated import connection from pylxd.deprecated import container from pylxd.deprecated import hosts from pylxd.deprecated import image from pylxd.deprecated import network from pylxd.deprecated import operation from pylxd.deprecated import profiles class API(object): def __init__(self, host=None, port=8443): warnings.warn( "pylxd.api.API is deprecated. Please use pylxd.Client.", DeprecationWarning) conn = self.connection = connection.LXDConnection(host=host, port=port) self.hosts = hosts.LXDHost(conn) self.image = image.LXDImage(conn) self.alias = image.LXDAlias(conn) self.network = network.LXDNetwork(conn) self.operation = operation.LXDOperation(conn) self.profiles = profiles.LXDProfile(conn) self.certificate = certificate.LXDCertificate(conn) self.container = container.LXDContainer(conn) # host def host_ping(self): return self.hosts.host_ping() def host_info(self): return self.hosts.host_info() def get_lxd_api_compat(self, data=None): return self.hosts.get_lxd_api_compat(data) def get_lxd_host_trust(self, data=None): return self.hosts.get_lxd_host_trust(data) def get_lxd_backing_fs(self, data=None): return self.hosts.get_lxd_backing_fs(data) def get_lxd_driver(self, data=None): return self.hosts.get_lxd_driver(data) def get_lxc_version(self, data=None): return self.hosts.get_lxc_version(data) def get_lxd_version(self, data=None): return self.hosts.get_lxd_version(data) def get_kernel_version(self, data=None): return self.hosts.get_kernel_version(data) def get_host_certificate(self): return self.hosts.get_certificate() def host_config(self): return self.hosts.host_config() # images def image_list(self): return self.image.image_list() def image_defined(self, image): return self.image.image_defined(image) def image_search(self, params): return self.image.image_list_by_key(params) def image_info(self, image): return self.image.image_info(image) def image_upload_date(self, image, data=None): return self.image.get_image_date(image, data, 'uploaded_at') def image_create_date(self, image, data=None): return self.image.get_image_date(image, data, 'created_at') def image_expire_date(self, image, data=None): return self.image.get_image_date(image, data, 'expires_at') def image_upload(self, path=None, data=None, headers={}): return self.image.image_upload(path=path, data=data, headers=headers) def image_delete(self, image): return self.image.image_delete(image) def image_export(self, image): return self.image.image_export(image) def image_update(self, image, data): return self.image.image_update(image, data) def image_rename(self, image, data): return self.image.image_rename(image, data) # alias def alias_list(self): return self.alias.alias_list() def alias_defined(self, alias): return self.alias.alias_defined(alias) def alias_create(self, data): return self.alias.alias_create(data) def alias_update(self, alias, data): return self.alias.alias_update(alias, data) def alias_show(self, alias): return self.alias.alias_show(alias) def alias_rename(self, alias, data): return self.alias.alias_rename(alias, data) def alias_delete(self, alias): return self.alias.alias_delete(alias) # containers: def container_list(self): return self.container.container_list() def container_defined(self, container): return self.container.container_defined(container) def container_running(self, container): return self.container.container_running(container) def container_init(self, container): return self.container.container_init(container) def container_update(self, container, config): return self.container.container_update(container, config) def container_state(self, container): return self.container.container_state(container) def container_start(self, container, timeout): return self.container.container_start(container, timeout) def container_stop(self, container, timeout): return self.container.container_stop(container, timeout) def container_suspend(self, container, timeout): return self.container.container_suspend(container, timeout) def container_resume(self, container, timeout): return self.container.container_resume(container, timeout) def container_reboot(self, container, timeout): return self.container.container_reboot(container, timeout) def container_destroy(self, container): return self.container.container_destroy(container) def get_container_log(self, container): return self.container.get_container_log(container) def get_container_config(self, container): return self.container.get_container_config(container) def get_container_websocket(self, container): return self.container.get_container_websocket(container) def container_info(self, container): return self.container.container_info(container) def container_local_copy(self, container): return self.container.container_local_copy(container) def container_local_move(self, instance, container): return self.container.container_local_move(instance, container) # file operations def get_container_file(self, container, filename): return self.container.get_container_file(container, filename) def container_publish(self, container): return self.container.container_publish(container) def put_container_file(self, container, src_file, dst_file, uid=0, gid=0, mode=0o644): return self.container.put_container_file( container, src_file, dst_file, uid, gid, mode) # snapshots def container_snapshot_list(self, container): return self.container.snapshot_list(container) def container_snapshot_create(self, container, config): return self.container.snapshot_create(container, config) def container_snapshot_info(self, container, snapshot): return self.container.snapshot_info(container, snapshot) def container_snapshot_rename(self, container, snapshot, config): return self.container.snapshot_rename(container, snapshot, config) def container_snapshot_delete(self, container, snapshot): return self.container.snapshot_delete(container, snapshot) def container_migrate(self, container): return self.container.container_migrate(container) def container_migrate_sync(self, operation_id, container_secret): return self.container.container_migrate_sync( operation_id, container_secret) # misc container def container_run_command(self, container, args, interactive=False, web_sockets=False, env=None): return self.container.run_command(container, args, interactive, web_sockets, env) # certificates def certificate_list(self): return self.certificate.certificate_list() def certificate_show(self, fingerprint): return self.certificate.certificate_show(fingerprint) def certificate_delete(self, fingerprint): return self.certificate.certificate_delete(fingerprint) def certificate_create(self, fingerprint): return self.certificate.certificate_create(fingerprint) # profiles def profile_create(self, profile): '''Create LXD profile''' return self.profiles.profile_create(profile) def profile_show(self, profile): '''Show LXD profile''' return self.profiles.profile_show(profile) def profile_defined(self, profile): '''Check to see if profile is defined''' return self.profiles.profile_defined(profile) def profile_list(self): '''List LXD profiles''' return self.profiles.profile_list() def profile_update(self, profile, config): '''Update LXD profile''' return self.profiles.profile_update(profile, config) def profile_rename(self, profile, config): '''Rename LXD profile''' return self.profiles.profile_rename(profile, config) def profile_delete(self, profile): '''Delete LXD profile''' return self.profiles.profile_delete(profile) # lxd operations def list_operations(self): return self.operation.operation_list() def wait_container_operation(self, operation, status_code, timeout): return self.operation.operation_wait(operation, status_code, timeout) def operation_delete(self, operation): return self.operation.operation_delete(operation) def operation_info(self, operation): return self.operation.operation_info(operation) def operation_show_create_time(self, operation, data=None): return self.operation.operation_create_time(operation, data) def operation_show_update_time(self, operation, data=None): return self.operation.operation_update_time(operation, data) def operation_show_status(self, operation, data=None): return self.operation.operation_status_code(operation, data) def operation_stream(self, operation, operation_secret): return self.operation.operation_stream(operation, operation_secret) # networks def network_list(self): return self.network.network_list() def network_show(self, network): return self.network.network_show(network) def network_show_name(self, network, data=None): return self.network.show_network_name(network, data) def network_show_type(self, network, data=None): return self.network.show_network_type(network, data) def network_show_members(self, network, data=None): return self.network.show_network_members(network, data) pylxd-2.2.6/pylxd/deprecated/image.py0000644000175000017500000002153013115536516017470 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import print_function import datetime import json from six.moves import urllib from pylxd.deprecated import base from pylxd.deprecated import connection from pylxd.deprecated import exceptions image_architecture = { 0: 'Unknown', 1: 'i686', 2: 'x86_64', 3: 'armv7l', 4: 'aarch64', 5: 'ppc', 6: 'ppc64', 7: 'ppc64le' } class LXDImage(base.LXDBase): def __init__(self, conn=None): self.connection = conn or connection.LXDConnection() # list images def image_list(self): try: (state, data) = self.connection.get_object('GET', '/1.0/images') return [image.split('/1.0/images/')[-1] for image in data['metadata']] except Exception as e: print("Unable to fetch image info - {}".format(e)) raise def image_defined(self, image): try: (state, data) = self.connection.get_object('GET', '/1.0/images/%s' % image) except exceptions.APIError as ex: if ex.status_code == 404: return False else: raise else: return True def image_list_by_key(self, params): try: (state, data) = self.connection.get_object( 'GET', '/1.0/images', urllib.parse.urlencode(params)) return [image.split('/1.0/images/')[-1] for image in data['metadata']] except Exception as e: print("Unable to fetch image info - {}".format(e)) raise # image info def image_info(self, image): try: (state, data) = self.connection.get_object('GET', '/1.0/images/%s' % image) image = { 'image_upload_date': self.get_image_date(image, data.get('metadata'), 'uploaded_at'), 'image_created_date': self.get_image_date(image, data.get('metadata'), 'created_at'), 'image_expires_date': self.get_image_date(image, data.get('metadata'), 'expires_at'), 'image_public': self.get_image_permission( image, data.get('metadata')), 'image_size': '%sMB' % self.get_image_size( image, data.get('metadata')), 'image_fingerprint': self.get_image_fingerprint( image, data.get('metadata')), 'image_architecture': self.get_image_architecture( image, data.get('metadata')), } return image except Exception as e: print("Unable to fetch image info - {}".format(e)) raise def get_image_date(self, image, data, key): try: if data is None: (state, data) = self.connection.get_object( 'GET', '/1.0/images/%s' % image) data = data.get('metadata') if data[key] != 0: return datetime.datetime.fromtimestamp( data[key]).strftime('%Y-%m-%d %H:%M:%S') else: return 'Unknown' except Exception as e: print("Unable to fetch image info - {}".format(e)) raise def get_image_permission(self, image, data): try: if data is None: (state, data) = self.connection.get_object( 'GET', '/1.0/images/%s' % image) data = data.get('metadata') return True if data['public'] == 1 else False except Exception as e: print("Unable to fetch image info - {}".format(e)) raise def get_image_size(self, image, data): try: if data is None: (state, data) = self.connection.get_object( 'GET', '/1.0/images/%s' % image) data = data.get('metadata') image_size = data['size'] if image_size <= 0: raise exceptions.ImageInvalidSize() return image_size // 1024 ** 2 except Exception as e: print("Unable to fetch image info - {}".format(e)) raise def get_image_fingerprint(self, image, data): try: if data is None: (state, data) = self.connection.get_object( 'GET', '/1.0/images/%s' % image) data = data.get('metadata') return data['fingerprint'] except Exception as e: print("Unable to fetch image info - {}".format(e)) raise def get_image_architecture(self, image, data): try: if data is None: (state, data) = self.connection.get_object( 'GET', '/1.0/images/%s' % image) data = data.get('metadata') return image_architecture[data['architecture']] except Exception as e: print("Unable to fetch image info - {}".format(e)) raise # image operations def image_upload(self, path=None, data=None, headers={}): data = data or open(path, 'rb').read() try: return self.connection.get_object('POST', '/1.0/images', data, headers) except Exception as e: print("Unable to upload image - {}".format(e)) raise def image_delete(self, image): try: return self.connection.get_status('DELETE', '/1.0/images/%s' % image) except Exception as e: print("Unable to delete image - {}".format(e)) raise def image_export(self, image): try: return self.connection.get_raw('GET', '/1.0/images/%s/export' % image) except Exception as e: print("Unable to export image - {}".format(e)) raise def image_update(self, image, data): try: return self.connection.get_status('PUT', '/1.0/images/%s' % image, json.dumps(data)) except Exception as e: print("Unable to update image - {}".format(e)) raise def image_rename(self, image, data): try: return self.connection.get_status('POST', '/1.0/images/%s' % image, json.dumps(data)) except Exception as e: print("Unable to rename image - {}".format(e)) raise class LXDAlias(base.LXDBase): def alias_list(self): (state, data) = self.connection.get_object( 'GET', '/1.0/images/aliases') return [alias.split('/1.0/images/aliases/')[-1] for alias in data['metadata']] def alias_defined(self, alias): return self.connection.get_status('GET', '/1.0/images/aliases/%s' % alias) def alias_show(self, alias): return self.connection.get_object('GET', '/1.0/images/aliases/%s' % alias) def alias_update(self, alias, data): return self.connection.get_status('PUT', '/1.0/images/aliases/%s' % alias, json.dumps(data)) def alias_rename(self, alias, data): return self.connection.get_status('POST', '/1.0/images/aliases/%s' % alias, json.dumps(data)) def alias_create(self, data): return self.connection.get_status('POST', '/1.0/images/aliases', json.dumps(data)) def alias_delete(self, alias): return self.connection.get_status('DELETE', '/1.0/images/aliases/%s' % alias) pylxd-2.2.6/pylxd/deprecated/exceptions.py0000644000175000017500000000222213115536516020564 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. class PyLXDException(Exception): pass class ContainerUnDefined(PyLXDException): pass class UntrustedHost(PyLXDException): pass class ContainerProfileCreateFail(PyLXDException): pass class ContainerProfileDeleteFail(PyLXDException): pass class ImageInvalidSize(PyLXDException): pass class APIError(PyLXDException): def __init__(self, error, status_code): msg = 'Error %s - %s.' % (status_code, error) super(APIError, self).__init__(msg) self.status_code = status_code self.error = error pylxd-2.2.6/pylxd/deprecated/tests/0000775000175000017500000000000013250466213017173 5ustar alexalex00000000000000pylxd-2.2.6/pylxd/deprecated/tests/test_image_alias.py0000644000175000017500000000455113115536516023046 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ddt import data from ddt import ddt import mock from pylxd.deprecated import connection from pylxd.deprecated.tests import annotated_data from pylxd.deprecated.tests import fake_api from pylxd.deprecated.tests import LXDAPITestBase @ddt @mock.patch.object(connection.LXDConnection, 'get_object') class LXDAPIImageAliasTestObject(LXDAPITestBase): def test_alias_list(self, ms): ms.return_value = ('200', fake_api.fake_alias_list()) self.assertEqual(['ubuntu'], self.lxd.alias_list()) ms.assert_called_once_with('GET', '/1.0/images/aliases') def test_alias_show(self, ms): ms.return_value = ('200', fake_api.fake_alias()) self.assertEqual( fake_api.fake_alias(), self.lxd.alias_show('fake')[1]) ms.assert_called_once_with('GET', '/1.0/images/aliases/fake') @ddt @mock.patch.object(connection.LXDConnection, 'get_status') class LXDAPIImageAliasTestStatus(LXDAPITestBase): @data(True, False) def test_alias_defined(self, expected, ms): ms.return_value = expected self.assertEqual(expected, self.lxd.alias_defined('fake')) ms.assert_called_once_with('GET', '/1.0/images/aliases/fake') @annotated_data( ('create', 'POST', '', ('fake',), ('"fake"',)), ('update', 'PUT', '/test-alias', ('test-alias', 'fake',), ('"fake"',)), ('rename', 'POST', '/test-alias', ('test-alias', 'fake',), ('"fake"',)), ('delete', 'DELETE', '/test-alias', ('test-alias',), ()), ) def test_alias_operations(self, method, http, path, args, call_args, ms): self.assertTrue(getattr(self.lxd, 'alias_' + method)(*args)) ms.assert_called_once_with( http, '/1.0/images/aliases' + path, *call_args ) pylxd-2.2.6/pylxd/deprecated/tests/utils.py0000644000175000017500000000243013115536516020706 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from pylxd import api from pylxd import exceptions as lxd_exceptions def upload_image(image): alias = '{}/{}/{}/{}'.format(image['os'], image['release'], image['arch'], image['variant']) lxd = api.API() imgs = api.API(host='images.linuxcontainers.org') d = imgs.alias_show(alias) meta = d[1]['metadata'] tgt = meta['target'] try: lxd.alias_update(meta) except lxd_exceptions.APIError as ex: if ex.status_code == 404: lxd.alias_create(meta) return tgt def delete_image(image): lxd = api.API() lxd.image_delete(image) pylxd-2.2.6/pylxd/deprecated/tests/test_profiles.py0000644000175000017500000000507713115536516022442 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ddt import data from ddt import ddt import mock from pylxd.deprecated import connection from pylxd.deprecated.tests import annotated_data from pylxd.deprecated.tests import fake_api from pylxd.deprecated.tests import LXDAPITestBase @mock.patch.object(connection.LXDConnection, 'get_object', return_value=(200, fake_api.fake_profile())) class LXDAPIProfilesTestObject(LXDAPITestBase): def test_list_profiles(self, ms): ms.return_value = ('200', fake_api.fake_profile_list()) self.assertEqual( ['fake-profile'], self.lxd.profile_list()) ms.assert_called_with('GET', '/1.0/profiles') def test_profile_show(self, ms): self.assertEqual( ms.return_value, self.lxd.profile_show('fake-profile')) ms.assert_called_with('GET', '/1.0/profiles/fake-profile') @ddt @mock.patch.object(connection.LXDConnection, 'get_status', return_value=True) class LXDAPIProfilesTestStatus(LXDAPITestBase): @data(True, False) def test_profile_defined(self, defined, ms): ms.return_value = defined self.assertEqual(defined, self.lxd.profile_defined('fake-profile')) ms.assert_called_with('GET', '/1.0/profiles/fake-profile') @annotated_data( ('create', 'POST', '', ('fake config',), ('"fake config"',)), ('update', 'PUT', '/fake-profile', ('fake-profile', 'fake config',), ('"fake config"',)), ('rename', 'POST', '/fake-profile', ('fake-profile', 'fake config',), ('"fake config"',)), ('delete', 'DELETE', '/fake-profile', ('fake-profile',), ()), ) def test_profile_operations(self, method, http, path, args, call_args, ms): self.assertTrue( getattr(self.lxd, 'profile_' + method)(*args)) ms.assert_called_with(http, '/1.0/profiles' + path, *call_args) pylxd-2.2.6/pylxd/deprecated/tests/__init__.py0000664000175000017500000000211113250267630021301 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ddt import data from ddt import unpack import unittest from pylxd.deprecated import api class LXDAPITestBase(unittest.TestCase): def setUp(self): super(LXDAPITestBase, self).setUp() self.lxd = api.API() def annotated_data(*args): class List(list): pass new_args = [] for arg in args: new_arg = List(arg) new_arg.__name__ = arg[0] new_args.append(new_arg) return lambda func: data(*new_args)(unpack(func)) pylxd-2.2.6/pylxd/deprecated/tests/test_operation.py0000644000175000017500000000532213115536516022610 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime from ddt import ddt import mock from pylxd.deprecated import connection from pylxd.deprecated.tests import annotated_data from pylxd.deprecated.tests import fake_api from pylxd.deprecated.tests import LXDAPITestBase @ddt @mock.patch.object(connection.LXDConnection, 'get_object', return_value=(200, fake_api.fake_operation())) class LXDAPIOperationTestObject(LXDAPITestBase): def test_list_operations(self, ms): ms.return_value = ('200', fake_api.fake_operation_list()) self.assertEqual( ['/1.0/operations/1234'], self.lxd.list_operations()) ms.assert_called_with('GET', '/1.0/operations') def test_operation_info(self, ms): ms.return_value = ('200', fake_api.fake_operation()) self.assertEqual( ms.return_value, self.lxd.operation_info('/1.0/operations/1234')) ms.assert_called_with('GET', '/1.0/operations/1234') @annotated_data( ('create_time', datetime.datetime.utcfromtimestamp(1433876844) .strftime('%Y-%m-%d %H:%M:%S')), ('update_time', datetime.datetime.utcfromtimestamp(1433876843) .strftime('%Y-%m-%d %H:%M:%S')), ('status', 'Running'), ) def test_operation_show(self, method, expected, ms): call = getattr(self.lxd, 'operation_show_' + method) self.assertEqual(expected, call('/1.0/operations/1234')) ms.assert_called_with('GET', '/1.0/operations/1234') @ddt @mock.patch.object(connection.LXDConnection, 'get_status', return_value=True) class LXDAPIOperationTestStatus(LXDAPITestBase): @annotated_data( ('operation_delete', 'DELETE', '', ()), ('wait_container_operation', 'GET', '/wait?status_code=200&timeout=30', ('200', '30')), ) def test_operation_actions(self, method, http, path, args, ms): self.assertTrue( getattr(self.lxd, method)('/1.0/operations/1234', *args)) ms.assert_called_with(http, '/1.0/operations/1234' + path) pylxd-2.2.6/pylxd/deprecated/tests/test_image.py0000644000175000017500000002247613115536516021703 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime from ddt import ddt import mock from six.moves import builtins from six.moves import cStringIO import unittest from pylxd.deprecated import connection from pylxd.deprecated import exceptions from pylxd.deprecated import image from pylxd.deprecated.tests import annotated_data from pylxd.deprecated.tests import fake_api from pylxd.deprecated.tests import LXDAPITestBase @ddt @mock.patch.object(connection.LXDConnection, 'get_object', return_value=('200', fake_api.fake_image_info())) class LXDAPIImageTestObject(LXDAPITestBase): list_data = ( ('list', (), ()), ('search', ({'foo': 'bar'},), ('foo=bar',)), ) @annotated_data(*list_data) def test_list_images(self, method, args, call_args, ms): ms.return_value = ('200', fake_api.fake_image_list()) self.assertEqual( ['trusty'], getattr(self.lxd, 'image_' + method)(*args)) ms.assert_called_once_with('GET', '/1.0/images', *call_args) @annotated_data(*list_data) def test_list_images_fail(self, method, args, call_args, ms): ms.side_effect = exceptions.PyLXDException self.assertRaises(exceptions.PyLXDException, getattr(self.lxd, 'image_' + method), *args) ms.assert_called_once_with('GET', '/1.0/images', *call_args) @annotated_data( (True, (('200', fake_api.fake_image_info()),)), (False, exceptions.APIError("404", 404)), ) def test_image_defined(self, expected, side_effect, ms): ms.side_effect = side_effect self.assertEqual(expected, self.lxd.image_defined('test-image')) ms.assert_called_once_with('GET', '/1.0/images/test-image') @annotated_data( ('APIError', exceptions.APIError("500", 500), exceptions.APIError), ('PyLXDException', exceptions.PyLXDException, exceptions.PyLXDException) ) def test_image_defined_fail(self, tag, side_effect, expected, ms): ms.side_effect = side_effect self.assertRaises(expected, self.lxd.image_defined, ('test-image',)) ms.assert_called_once_with('GET', '/1.0/images/test-image') def test_image_info(self, ms): self.assertEqual({ 'image_upload_date': (datetime.datetime .fromtimestamp(1435669853) .strftime('%Y-%m-%d %H:%M:%S')), 'image_created_date': 'Unknown', 'image_expires_date': 'Unknown', 'image_public': False, 'image_size': '63MB', 'image_fingerprint': '04aac4257341478b49c25d22cea8a6ce' '0489dc6c42d835367945e7596368a37f', 'image_architecture': 'x86_64', }, self.lxd.image_info('test-image')) ms.assert_called_once_with('GET', '/1.0/images/test-image') def test_image_info_fail(self, ms): ms.side_effect = exceptions.PyLXDException self.assertRaises(exceptions.PyLXDException, self.lxd.image_info, ('test-image',)) ms.assert_called_once_with('GET', '/1.0/images/test-image') dates_data = ( ('upload', (datetime.datetime.fromtimestamp(1435669853) .strftime('%Y-%m-%d %H:%M:%S'))), ('create', 'Unknown'), ('expire', 'Unknown'), ) @annotated_data(*dates_data) def test_image_date(self, method, expected, ms): self.assertEqual(expected, getattr( self.lxd, 'image_{}_date'.format(method))('test-image', data=None)) ms.assert_called_once_with('GET', '/1.0/images/test-image') @annotated_data(*dates_data) def test_image_date_fail(self, method, expected, ms): ms.side_effect = exceptions.PyLXDException self.assertRaises(exceptions.PyLXDException, getattr( self.lxd, 'image_{}_date'.format(method)), 'test-image', data=None) ms.assert_called_once_with('GET', '/1.0/images/test-image') @ddt @mock.patch.object(connection.LXDConnection, 'get_status', return_value=True) class LXDAPIImageTestStatus(LXDAPITestBase): operations_data = ( ('delete', 'DELETE', '/test-image', ('test-image',), ()), ('update', 'PUT', '/test-image', ('test-image', 'fake',), ('"fake"',)), ('rename', 'POST', '/test-image', ('test-image', 'fake',), ('"fake"',)), ) @annotated_data(*operations_data) def test_image_operations(self, method, http, path, args, call_args, ms): self.assertTrue( getattr(self.lxd, 'image_' + method)(*args)) ms.assert_called_once_with( http, '/1.0/images' + path, *call_args ) @annotated_data(*operations_data) def test_image_operations_fail(self, method, http, path, args, call_args, ms): ms.side_effect = exceptions.PyLXDException self.assertRaises(exceptions.PyLXDException, getattr(self.lxd, 'image_' + method), *args) ms.assert_called_once_with( http, '/1.0/images' + path, *call_args ) @mock.patch.object(connection.LXDConnection, 'get_object', return_value=('200', fake_api.fake_image_info())) class LXDAPAPIImageTestUpload(LXDAPITestBase): @mock.patch.object(builtins, 'open', return_value=cStringIO('fake')) def test_image_upload_file(self, mo, ms): self.assertTrue(self.lxd.image_upload(path='/fake/path')) mo.assert_called_once_with('/fake/path', 'rb') ms.assert_called_once_with('POST', '/1.0/images', 'fake', {}) @mock.patch.object(connection.LXDConnection, 'get_raw') class LXDAPIImageTestRaw(LXDAPITestBase): def test_image_export(self, ms): ms.return_value = 'fake contents' self.assertEqual('fake contents', self.lxd.image_export('fake')) ms.assert_called_once_with('GET', '/1.0/images/fake/export') def test_image_export_fail(self, ms): ms.side_effect = exceptions.PyLXDException self.assertRaises(exceptions.PyLXDException, self.lxd.image_export, 'fake') ms.assert_called_once_with('GET', '/1.0/images/fake/export') @ddt @mock.patch.object(connection.LXDConnection, 'get_object', return_value=(200, fake_api.fake_image_info())) class LXDAPIImageInfoTest(unittest.TestCase): def setUp(self): super(LXDAPIImageInfoTest, self).setUp() self.image = image.LXDImage() info_list = ( ('permission', False), ('size', 63), ('fingerprint', '04aac4257341478b49c25d22cea8a6ce' '0489dc6c42d835367945e7596368a37f'), ('architecture', 'x86_64'), ) @annotated_data(*info_list) def test_info_no_data(self, method, expected, mc): self.assertEqual(expected, (getattr(self.image, 'get_image_' + method) ('test-image', data=None))) mc.assert_called_once_with('GET', '/1.0/images/test-image') @annotated_data(*info_list) def test_info_no_data_fail(self, method, expected, mc): mc.side_effect = exceptions.PyLXDException self.assertRaises(exceptions.PyLXDException, getattr(self.image, 'get_image_' + method), 'test-image', data=None) @annotated_data( ('permission_true', 'permission', {'public': 0}, False), ('permission_false', 'permission', {'public': 1}, True), ('size', 'size', {'size': 52428800}, 50), ('fingerprint', 'fingerprint', {'fingerprint': 'AAAA'}, 'AAAA'), *[('architecture_' + v, 'architecture', {'architecture': k}, v) for k, v in image.image_architecture.items()] ) def test_info_data(self, tag, method, metadata, expected, mc): self.assertEqual( expected, getattr(self.image, 'get_image_' + method) ('test-image', data=metadata)) self.assertFalse(mc.called) @annotated_data( ('permission', 'permission', {}, KeyError), ('size', 'size', {'size': 0}, exceptions.ImageInvalidSize), ('size', 'size', {'size': -1}, exceptions.ImageInvalidSize), ('fingerprint', 'fingerprint', {}, KeyError), ('architecture', 'architecture', {}, KeyError), ('architecture_invalid', 'architecture', {'architecture': -1}, KeyError) ) def test_info_data_fail(self, tag, method, metadata, expected, mc): self.assertRaises(expected, getattr(self.image, 'get_image_' + method), 'test-image', data=metadata) self.assertFalse(mc.called) pylxd-2.2.6/pylxd/deprecated/tests/test_connection.py0000644000175000017500000001466513115536516022761 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ddt import ddt import inspect import mock import six from six.moves import cStringIO from six.moves import http_client import socket import unittest from pylxd.deprecated import connection from pylxd.deprecated import exceptions from pylxd.deprecated.tests import annotated_data if six.PY3: from io import BytesIO @ddt class LXDInitConnectionTest(unittest.TestCase): @mock.patch('socket.socket') @mock.patch.object(http_client.HTTPConnection, '__init__') def test_http_connection(self, mc, ms): conn = connection.UnixHTTPConnection('/', 'host', 1234) if six.PY3: mc.assert_called_once_with( conn, 'host', port=1234, timeout=None) else: mc.assert_called_once_with( conn, 'host', port=1234, strict=None, timeout=None) conn.connect() ms.assert_called_once_with(socket.AF_UNIX, socket.SOCK_STREAM) ms.return_value.connect.assert_called_once_with('/') @mock.patch('os.environ', {'HOME': '/home/foo'}) @mock.patch('ssl.wrap_socket') @mock.patch('socket.create_connection') def test_https_connection(self, ms, ml): conn = connection.HTTPSConnection('host', 1234) with mock.patch.object(conn, '_tunnel') as mc: conn.connect() self.assertFalse(mc.called) ms.assert_called_once_with( ('host', 1234), socket._GLOBAL_DEFAULT_TIMEOUT, None) ml.assert_called_once_with( ms.return_value, certfile='/home/foo/.config/lxc/client.crt', keyfile='/home/foo/.config/lxc/client.key', ssl_version=connection.DEFAULT_TLS_VERSION, ) @mock.patch('os.environ', {'HOME': '/home/foo'}) @mock.patch('ssl.wrap_socket') @mock.patch('socket.create_connection') def test_https_proxy_connection(self, ms, ml): conn = connection.HTTPSConnection('host', 1234) conn._tunnel_host = 'host' with mock.patch.object(conn, '_tunnel') as mc: conn.connect() self.assertTrue(mc.called) ms.assert_called_once_with( ('host', 1234), socket._GLOBAL_DEFAULT_TIMEOUT, None) ml.assert_called_once_with( ms.return_value, certfile='/home/foo/.config/lxc/client.crt', keyfile='/home/foo/.config/lxc/client.key', ssl_version=connection.DEFAULT_TLS_VERSION) @mock.patch('pylxd.deprecated.connection.HTTPSConnection') @mock.patch('pylxd.deprecated.connection.UnixHTTPConnection') @annotated_data( ('unix', (None,), {}, '/var/lib/lxd/unix.socket'), ('unix_path', (None,), {'LXD_DIR': '/fake/'}, '/fake/unix.socket'), ('https', ('host',), {}, ''), ('https_port', ('host', 1234), {}, ''), ) def test_get_connection(self, mode, args, env, path, mc, ms): with mock.patch('os.environ', env): conn = connection.LXDConnection(*args).get_connection() if mode.startswith('unix'): self.assertEqual(mc.return_value, conn) mc.assert_called_once_with(path) elif mode.startswith('https'): self.assertEqual(ms.return_value, conn) ms.assert_called_once_with( args[0], len(args) == 2 and args[1] or 8443) class FakeResponse(object): def __init__(self, status, data): self.status = status if six.PY3: self.read = BytesIO(six.b(data)).read else: self.read = cStringIO(data).read @ddt @mock.patch('pylxd.deprecated.connection.LXDConnection.get_connection') class LXDConnectionTest(unittest.TestCase): def setUp(self): super(LXDConnectionTest, self).setUp() self.conn = connection.LXDConnection() @annotated_data( ('null', (200, '{}'), exceptions.PyLXDException), ('200', (200, '{"foo": "bar"}'), (200, {'foo': 'bar'})), ('202', (202, '{"status_code": 100}'), (202, {'status_code': 100})), ('500', (500, '{"foo": "bar"}'), exceptions.APIError), ) def test_get_object(self, tag, effect, result, mg): mg.return_value.getresponse.return_value = FakeResponse(*effect) if inspect.isclass(result): self.assertRaises(result, self.conn.get_object) else: self.assertEqual(result, self.conn.get_object()) @annotated_data( ('null', (200, '{}'), exceptions.PyLXDException), ('200', (200, '{"foo": "bar"}'), True), ('202', (202, '{"status_code": 100}'), True), ('200', (200, '{"error": "bar"}'), exceptions.APIError), ('500', (500, '{"foo": "bar"}'), False), ) def test_get_status(self, tag, effect, result, mg): mg.return_value.getresponse.return_value = FakeResponse(*effect) if inspect.isclass(result): self.assertRaises(result, self.conn.get_status) else: self.assertEqual(result, self.conn.get_status()) @annotated_data( ('null', (200, ''), exceptions.PyLXDException), ('200', (200, '{"foo": "bar"}'), six.b('{"foo": "bar"}')), ('500', (500, '{"foo": "bar"}'), exceptions.PyLXDException), ) def test_get_raw(self, tag, effect, result, mg): mg.return_value.getresponse.return_value = FakeResponse(*effect) if inspect.isclass(result): self.assertRaises(result, self.conn.get_raw) else: self.assertEqual(result, self.conn.get_raw()) @mock.patch('pylxd.deprecated.connection.WebSocketClient') @annotated_data( ('fake_host', 'wss://fake_host:8443'), (None, 'ws+unix:///var/lib/lxd/unix.socket') ) def test_get_ws(self, host, result, mock_ws, _): conn = connection.LXDConnection(host) conn.get_ws('/fake/path') mock_ws.assert_has_calls([mock.call(result), mock.call().connect()]) pylxd-2.2.6/pylxd/deprecated/tests/test_certificate.py0000644000175000017500000000430113115536516023066 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ddt import ddt import json import mock from pylxd.deprecated import connection from pylxd.deprecated.tests import annotated_data from pylxd.deprecated.tests import fake_api from pylxd.deprecated.tests import LXDAPITestBase @ddt class LXDAPICertificateTest(LXDAPITestBase): def test_list_certificates(self): with mock.patch.object(connection.LXDConnection, 'get_object') as ms: ms.return_value = ('200', fake_api.fake_certificate_list()) self.assertEqual( ['ABCDEF01'], self.lxd.certificate_list()) ms.assert_called_with('GET', '/1.0/certificates') def test_certificate_show(self): with mock.patch.object(connection.LXDConnection, 'get_object') as ms: ms.return_value = ('200', fake_api.fake_certificate()) self.assertEqual( ms.return_value, self.lxd.certificate_show('ABCDEF01')) ms.assert_called_with('GET', '/1.0/certificates/ABCDEF01') @annotated_data( ('delete', 'DELETE', '/ABCDEF01'), ('create', 'POST', '', (json.dumps('ABCDEF01'),)), ) def test_certificate_operations(self, method, http, path, call_args=()): with mock.patch.object(connection.LXDConnection, 'get_status') as ms: ms.return_value = True self.assertTrue( getattr(self.lxd, 'certificate_' + method)('ABCDEF01')) ms.assert_called_with(http, '/1.0/certificates' + path, *call_args) pylxd-2.2.6/pylxd/deprecated/tests/test_host.py0000644000175000017500000000542113115536516021565 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ddt import data from ddt import ddt import mock from pylxd.deprecated import connection from pylxd.deprecated import exceptions from pylxd.deprecated.tests import annotated_data from pylxd.deprecated.tests import fake_api from pylxd.deprecated.tests import LXDAPITestBase @ddt @mock.patch.object(connection.LXDConnection, 'get_object', return_value=('200', fake_api.fake_host())) class LXDAPIHostTestObject(LXDAPITestBase): def test_get_host_info(self, ms): result = self.lxd.host_info() self.assertEqual(result, { 'lxd_api_compat_level': 1, 'lxd_trusted_host': True, 'lxd_backing_fs': 'ext4', 'lxd_driver': 'lxc', 'lxd_version': 0.12, 'lxc_version': '1.1.2', 'kernel_version': '3.19.0-22-generic', }) ms.assert_called_once_with('GET', '/1.0') host_data = ( ('lxd_api_compat', 1), ('lxd_host_trust', True), ('lxd_backing_fs', 'ext4'), ('lxd_driver', 'lxc'), ('lxc_version', '1.1.2'), ('lxd_version', 0.12), ('kernel_version', '3.19.0-22-generic'), ) @annotated_data(*host_data) def test_get_host_data(self, method, expected, ms): result = getattr(self.lxd, 'get_' + method)(data=None) self.assertEqual(expected, result) ms.assert_called_once_with('GET', '/1.0') @annotated_data(*host_data) def test_get_host_data_fail(self, method, expected, ms): ms.side_effect = exceptions.PyLXDException result = getattr(self.lxd, 'get_' + method)(data=None) self.assertEqual(None, result) ms.assert_called_once_with('GET', '/1.0') @ddt @mock.patch.object(connection.LXDConnection, 'get_status') class LXDAPIHostTestStatus(LXDAPITestBase): @data(True, False) def test_get_host_ping(self, value, ms): ms.return_value = value self.assertEqual(value, self.lxd.host_ping()) ms.assert_called_once_with('GET', '/1.0') def test_get_host_ping_fail(self, ms): ms.side_effect = Exception self.assertRaises(exceptions.PyLXDException, self.lxd.host_ping) ms.assert_called_once_with('GET', '/1.0') pylxd-2.2.6/pylxd/deprecated/tests/fake_api.py0000644000175000017500000001357713115536516021323 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. def fake_standard_return(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": {} } def fake_host(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": { "api_compat": 1, "auth": "trusted", "config": {}, "environment": { "backing_fs": "ext4", "driver": "lxc", "kernel_version": "3.19.0-22-generic", "lxc_version": "1.1.2", "lxd_version": "0.12" } } } def fake_image_list_empty(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": [] } def fake_image_list(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": ['/1.0/images/trusty'] } def fake_image_info(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": { "aliases": [ { "target": "ubuntu", "description": "ubuntu" } ], "architecture": 2, "fingerprint": "04aac4257341478b49c25d22cea8a6ce" "0489dc6c42d835367945e7596368a37f", "filename": "", "properties": {}, "public": 0, "size": 67043148, "created_at": 0, "expires_at": 0, "uploaded_at": 1435669853 } } def fake_alias(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": { "target": "ubuntu", "description": "ubuntu" } } def fake_alias_list(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": [ "/1.0/images/aliases/ubuntu" ] } def fake_container_list(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": [ "/1.0/containers/trusty-1" ] } def fake_container_state(status): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": { "status": status } } def fake_container_log(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": { "log": "fake log" } } def fake_container_migrate(): return { "type": "sync", "status": "Success", "status_code": 200, "operation": "/1.0/operations/1234", "metadata": { "control": "fake_control", "fs": "fake_fs", "criu": "fake_criu", } } def fake_snapshots_list(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": [ "/1.0/containers/trusty-1/snapshots/first" ] } def fake_certificate_list(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": [ "/1.0/certificates/ABCDEF01" ] } def fake_certificate(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": { "type": "client", "certificate": "ABCDEF01" } } def fake_profile_list(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": [ "/1.0/profiles/fake-profile" ] } def fake_profile(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": { "name": "fake-profile", "config": { "resources.memory": "2GB", "network.0.bridge": "lxcbr0" } } } def fake_operation_list(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": [ "/1.0/operations/1234" ] } def fake_operation(): return { "type": "async", "status": "OK", "status_code": 100, "operation": "/1.0/operation/1234", "metadata": { "created_at": "2015-06-09T19:07:24.379615253-06:00", "updated_at": "2015-06-09T19:07:23.379615253-06:00", "status": "Running", "status_code": 103, "resources": { "containers": ["/1.0/containers/1"] }, "metadata": {}, "may_cancel": True } } def fake_network_list(): return { "type": "sync", "status": "Success", "status_code": 200, "metadata": [ "/1.0/networks/lxcbr0" ] } def fake_network(): return { "type": "async", "status": "OK", "status_code": 100, "operation": "/1.0/operation/1234", "metadata": { "name": "lxcbr0", "type": "bridge", "members": ["/1.0/containers/trusty-1"] } } pylxd-2.2.6/pylxd/deprecated/tests/test_container.py0000644000175000017500000002150613115536516022574 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from collections import OrderedDict from ddt import ddt import json import mock import tempfile from pylxd.deprecated import connection from pylxd.deprecated.tests import annotated_data from pylxd.deprecated.tests import fake_api from pylxd.deprecated.tests import LXDAPITestBase @ddt @mock.patch.object(connection.LXDConnection, 'get_object', return_value=('200', fake_api.fake_operation())) class LXDAPIContainerTestObject(LXDAPITestBase): def test_list_containers(self, ms): ms.return_value = ('200', fake_api.fake_container_list()) self.assertEqual( ['trusty-1'], self.lxd.container_list()) ms.assert_called_once_with('GET', '/1.0/containers') @annotated_data( ('STOPPED', False), ('STOPPING', False), ('ABORTING', False), ('RUNNING', True), ('STARTING', True), ('FREEZING', True), ('FROZEN', True), ('THAWED', True), ) def test_container_running(self, status, running, ms): with mock.patch.object(connection.LXDConnection, 'get_object') as ms: ms.return_value = ('200', fake_api.fake_container_state(status)) self.assertEqual(running, self.lxd.container_running('trusty-1')) ms.assert_called_once_with('GET', '/1.0/containers/trusty-1/state') def test_container_init(self, ms): self.assertEqual(ms.return_value, self.lxd.container_init('fake')) ms.assert_called_once_with('POST', '/1.0/containers', '"fake"') def test_container_update(self, ms): self.assertEqual(ms.return_value, self.lxd.container_update('trusty-1', 'fake')) ms.assert_called_once_with('PUT', '/1.0/containers/trusty-1', '"fake"') def test_container_state(self, ms): ms.return_value = ('200', fake_api.fake_container_state('RUNNING')) self.assertEqual(ms.return_value, self.lxd.container_state('trusty-1')) ms.assert_called_with('GET', '/1.0/containers/trusty-1/state') @annotated_data( ('start', 'start'), ('stop', 'stop'), ('suspend', 'freeze'), ('resume', 'unfreeze'), ('reboot', 'restart'), ) def test_container_actions(self, method, action, ms): self.assertEqual( ms.return_value, getattr(self.lxd, 'container_' + method)('trusty-1', 30)) ms.assert_called_once_with('PUT', '/1.0/containers/trusty-1/state', json.dumps({'action': action, 'force': True, 'timeout': 30, })) def test_container_destroy(self, ms): self.assertEqual( ms.return_value, self.lxd.container_destroy('trusty-1')) ms.assert_called_once_with('DELETE', '/1.0/containers/trusty-1') def test_container_log(self, ms): ms.return_value = ('200', fake_api.fake_container_log()) self.assertEqual( 'fake log', self.lxd.get_container_log('trusty-1')) ms.assert_called_once_with('GET', '/1.0/containers/trusty-1?log=true') def test_container_config(self, ms): ms.return_value = ('200', fake_api.fake_container_state('fake')) self.assertEqual( {'status': 'fake'}, self.lxd.get_container_config('trusty-1')) ms.assert_called_once_with('GET', '/1.0/containers/trusty-1?log=false') def test_container_info(self, ms): ms.return_value = ('200', fake_api.fake_container_state('fake')) self.assertEqual( {'status': 'fake'}, self.lxd.container_info('trusty-1')) ms.assert_called_once_with('GET', '/1.0/containers/trusty-1/state') def test_container_migrate(self, ms): ms.return_value = ('200', fake_api.fake_container_migrate()) self.assertEqual( ('200', {'type': 'sync', 'status': 'Success', 'metadata': {'criu': 'fake_criu', 'fs': 'fake_fs', 'control': 'fake_control'}, 'operation': '/1.0/operations/1234', 'status_code': 200}), self.lxd.container_migrate('trusty-1')) ms.assert_called_once_with('POST', '/1.0/containers/trusty-1', '{"migration": true}') def test_container_publish(self, ms): ms.return_value = ('200', fake_api.fake_operation()) self.assertEqual( ms.return_value, self.lxd.container_publish('trusty-1')) ms.assert_called_once_with('POST', '/1.0/images', '"trusty-1"') def test_container_put_file(self, ms): temp_file = tempfile.NamedTemporaryFile() ms.return_value = ('200', fake_api.fake_standard_return()) self.assertEqual( ms.return_value, self.lxd.put_container_file('trusty-1', temp_file.name, 'dst_file')) ms.assert_called_once_with( 'POST', '/1.0/containers/trusty-1/files?path=dst_file', body=b'', headers={'X-LXD-gid': 0, 'X-LXD-mode': 0o644, 'X-LXD-uid': 0}) def test_list_snapshots(self, ms): ms.return_value = ('200', fake_api.fake_snapshots_list()) self.assertEqual( ['/1.0/containers/trusty-1/snapshots/first'], self.lxd.container_snapshot_list('trusty-1')) ms.assert_called_once_with('GET', '/1.0/containers/trusty-1/snapshots') @annotated_data( ('create', 'POST', '', ('fake config',), ('"fake config"',)), ('info', 'GET', '/first', ('first',), ()), ('rename', 'POST', '/first', ('first', 'fake config'), ('"fake config"',)), ('delete', 'DELETE', '/first', ('first',), ()), ) def test_snapshot_operations(self, method, http, path, args, call_args, ms): self.assertEqual( ms.return_value, getattr(self.lxd, 'container_snapshot_' + method)('trusty-1', *args)) ms.assert_called_once_with(http, '/1.0/containers/trusty-1/snapshots' + path, *call_args) def test_container_run_command(self, ms): data = OrderedDict(( ('command', ['/fake/command']), ('interactive', False), ('wait-for-websocket', False), ('environment', {'FAKE_ENV': 'fake'}) )) self.assertEqual( ms.return_value, self.lxd.container_run_command('trusty-1', *data.values())) self.assertEqual(1, ms.call_count) self.assertEqual( ms.call_args[0][:2], ('POST', '/1.0/containers/trusty-1/exec')) self.assertEqual( json.loads(ms.call_args[0][2]), dict(data) ) @ddt @mock.patch.object(connection.LXDConnection, 'get_object', return_value=('200', fake_api.fake_container_list())) class LXDAPIContainerTestStatus(LXDAPITestBase): def test_container_defined(self, ms): self.assertTrue(self.lxd.container_defined('trusty-1')) ms.assert_called_once_with('GET', '/1.0/containers') @ddt @mock.patch.object(connection.LXDConnection, 'get_raw', return_value='fake contents') class LXDAPIContainerTestRaw(LXDAPITestBase): def test_container_file(self, ms): self.assertEqual( 'fake contents', self.lxd.get_container_file('trusty-1', '/file/name')) ms.assert_called_once_with( 'GET', '/1.0/containers/trusty-1/files?path=/file/name') pylxd-2.2.6/pylxd/deprecated/tests/test_network.py0000644000175000017500000000375013115536516022304 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ddt import ddt import mock from pylxd.deprecated import connection from pylxd.deprecated.tests import annotated_data from pylxd.deprecated.tests import fake_api from pylxd.deprecated.tests import LXDAPITestBase @ddt @mock.patch.object(connection.LXDConnection, 'get_object', return_value=(200, fake_api.fake_network())) class LXDAPINetworkTest(LXDAPITestBase): def test_list_networks(self, ms): ms.return_value = ('200', fake_api.fake_network_list()) self.assertEqual( ['lxcbr0'], self.lxd.network_list()) ms.assert_called_with('GET', '/1.0/networks') def test_network_show(self, ms): self.assertEqual({ 'network_name': 'lxcbr0', 'network_type': 'bridge', 'network_members': ['/1.0/containers/trusty-1'], }, self.lxd.network_show('lxcbr0')) ms.assert_called_with('GET', '/1.0/networks/lxcbr0') @annotated_data( ('name', 'lxcbr0'), ('type', 'bridge'), ('members', ['/1.0/containers/trusty-1']), ) def test_network_data(self, method, expected, ms): self.assertEqual( expected, getattr(self.lxd, 'network_show_' + method)('lxcbr0')) ms.assert_called_with('GET', '/1.0/networks/lxcbr0') pylxd-2.2.6/pylxd/deprecated/hosts.py0000644000175000017500000001134113115536516017545 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import print_function from pylxd.deprecated import base from pylxd.deprecated import exceptions class LXDHost(base.LXDBase): def host_ping(self): try: return self.connection.get_status('GET', '/1.0') except Exception as e: msg = 'LXD service is unavailable. %s' % e raise exceptions.PyLXDException(msg) def host_info(self): (state, data) = self.connection.get_object('GET', '/1.0') return { 'lxd_api_compat_level': self.get_lxd_api_compat(data.get('metadata')), 'lxd_trusted_host': self.get_lxd_host_trust(data.get('metadata')), 'lxd_backing_fs': self.get_lxd_backing_fs(data.get('metadata')), 'lxd_driver': self.get_lxd_driver(data.get('metadata')), 'lxd_version': self.get_lxd_version(data.get('metadata')), 'lxc_version': self.get_lxc_version(data.get('metadata')), 'kernel_version': self.get_kernel_version(data.get('metadata')) } def get_lxd_api_compat(self, data): try: if data is None: (state, data) = self.connection.get_object('GET', '/1.0') data = data.get('metadata') return data['api_compat'] except exceptions.PyLXDException as e: print('Handling run-time error: {}'.format(e)) def get_lxd_host_trust(self, data): try: if data is None: (state, data) = self.connection.get_object('GET', '/1.0') data = data.get('metadata') return True if data['auth'] == 'trusted' else False except exceptions.PyLXDException as e: print('Handling run-time error: {}'.format(e)) def get_lxd_backing_fs(self, data): try: if data is None: (state, data) = self.connection.get_object('GET', '/1.0') data = data.get('metadata') return data['environment']['backing_fs'] except exceptions.PyLXDException as e: print('Handling run-time error: {}'.format(e)) def get_lxd_driver(self, data): try: if data is None: (state, data) = self.connection.get_object('GET', '/1.0') data = data.get('metadata') return data['environment']['driver'] except exceptions.PyLXDException as e: print('Handling run-time error: {}'.format(e)) def get_lxc_version(self, data): try: if data is None: (state, data) = self.connection.get_object('GET', '/1.0') data = data.get('metadata') return data['environment']['lxc_version'] except exceptions.PyLXDException as e: print('Handling run-time error: {}'.format(e)) def get_lxd_version(self, data): try: if data is None: (state, data) = self.connection.get_object('GET', '/1.0') data = data.get('metadata') return float(data['environment']['lxd_version']) except exceptions.PyLXDException as e: print('Handling run-time error: {}'.format(e)) def get_kernel_version(self, data): try: if data is None: (state, data) = self.connection.get_object('GET', '/1.0') data = data.get('metadata') return data['environment']['kernel_version'] except exceptions.PyLXDException as e: print('Handling run-time error: {}'.format(e)) def get_certificate(self): try: (state, data) = self.connection.get_object('GET', '/1.0') data = data.get('metadata') return data['environment']['certificate'] except exceptions.PyLXDException as e: print('Handling run-time error: {}'.format(e)) def host_config(self): try: (state, data) = self.connection.get_object('GET', '/1.0') return data.get('metadata') except exceptions.PyLXDException as e: print('Handling run-time error: {}'.format(e)) pylxd-2.2.6/pylxd/deprecated/connection.py0000644000175000017500000001710613115536516020551 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # Copyright (c) 2015 Mirantis inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from collections import namedtuple import copy import json import os import six import socket import ssl import threading from six.moves import http_client from six.moves import queue try: from ws4py import client as websocket except ImportError: websocket = None from pylxd.deprecated import exceptions from pylxd.deprecated import utils if hasattr(ssl, 'SSLContext'): # For Python >= 2.7.9 and Python 3.x if hasattr(ssl, 'PROTOCOL_TLSv1_2'): DEFAULT_TLS_VERSION = ssl.PROTOCOL_TLSv1_2 else: DEFAULT_TLS_VERSION = ssl.PROTOCOL_TLSv1 else: # For Python 2.6 and <= 2.7.8 from OpenSSL import SSL DEFAULT_TLS_VERSION = SSL.TLSv1_2_METHOD class UnixHTTPConnection(http_client.HTTPConnection): def __init__(self, path, host='localhost', port=None, strict=None, timeout=None): if six.PY3: http_client.HTTPConnection.__init__(self, host, port=port, timeout=timeout) else: http_client.HTTPConnection.__init__(self, host, port=port, strict=strict, timeout=timeout) self.path = path def connect(self): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.connect(self.path) self.sock = sock class HTTPSConnection(http_client.HTTPConnection): default_port = 8443 def __init__(self, *args, **kwargs): http_client.HTTPConnection.__init__(self, *args, **kwargs) def connect(self): sock = socket.create_connection((self.host, self.port), self.timeout, self.source_address) if self._tunnel_host: self.sock = sock self._tunnel() (cert_file, key_file) = self._get_ssl_certs() self.sock = ssl.wrap_socket(sock, certfile=cert_file, keyfile=key_file, ssl_version=DEFAULT_TLS_VERSION) @staticmethod def _get_ssl_certs(): return (os.path.join(os.environ['HOME'], '.config/lxc/client.crt'), os.path.join(os.environ['HOME'], '.config/lxc/client.key')) _LXDResponse = namedtuple('LXDResponse', ['status', 'body', 'json']) if websocket is not None: class WebSocketClient(websocket.WebSocketBaseClient): def __init__(self, url, protocols=None, extensions=None, ssl_options=None, headers=None): """WebSocket client that executes into a eventlet green thread.""" websocket.WebSocketBaseClient.__init__(self, url, protocols, extensions, ssl_options=ssl_options, headers=headers) self._th = threading.Thread( target=self.run, name='WebSocketClient') self._th.daemon = True self.messages = queue.Queue() def handshake_ok(self): """Starts the client's thread.""" self._th.start() def received_message(self, message): """Override the base class to store the incoming message.""" self.messages.put(copy.deepcopy(message)) def closed(self, code, reason=None): # When the connection is closed, put a StopIteration # on the message queue to signal there's nothing left # to wait for self.messages.put(StopIteration) def receive(self): # If the websocket was terminated and there are no messages # left in the queue, return None immediately otherwise the client # will block forever if self.terminated and self.messages.empty(): return None message = self.messages.get() if message is StopIteration: return None return message class LXDConnection(object): def __init__(self, host=None, port=8443): if host: self.host = host self.port = port self.unix_socket = None else: if 'LXD_DIR' in os.environ: self.unix_socket = os.path.join(os.environ['LXD_DIR'], 'unix.socket') else: self.unix_socket = '/var/lib/lxd/unix.socket' self.host, self.port = None, None self.connection = None def _request(self, *args, **kwargs): if self.connection is None: self.connection = self.get_connection() self.connection.request(*args, **kwargs) response = self.connection.getresponse() status = response.status raw_body = response.read() try: if six.PY3: body = json.loads(raw_body.decode()) else: body = json.loads(raw_body) except ValueError: body = None return _LXDResponse(status, raw_body, body) def get_connection(self): if self.host: return HTTPSConnection(self.host, self.port) return UnixHTTPConnection(self.unix_socket) def get_object(self, *args, **kwargs): response = self._request(*args, **kwargs) if not response.json: raise exceptions.PyLXDException('Null Data') elif response.status == 200 or ( response.status == 202 and response.json.get('status_code') == 100): return response.status, response.json else: utils.get_lxd_error(response.status, response.json) def get_status(self, *args, **kwargs): response = self._request(*args, **kwargs) if not response.json: raise exceptions.PyLXDException('Null Data') elif response.json.get('error'): utils.get_lxd_error(response.status, response.json) elif response.status == 200 or ( response.status == 202 and response.json.get('status_code') == 100): return True return False def get_raw(self, *args, **kwargs): response = self._request(*args, **kwargs) if not response.body: raise exceptions.PyLXDException('Null Body') elif response.status == 200: return response.body else: raise exceptions.PyLXDException('Failed to get raw response') def get_ws(self, path): if websocket is None: raise ValueError( 'This feature requires the optional ws4py library.') if self.unix_socket: connection_string = 'ws+unix://%s' % self.unix_socket else: connection_string = ( 'wss://%(host)s:%(port)s' % {'host': self.host, 'port': self.port} ) ws = WebSocketClient(connection_string) ws.resource = path ws.connect() return ws pylxd-2.2.6/pylxd/deprecated/operation.py0000644000175000017500000000551313115536516020411 0ustar alexalex00000000000000# Copyright (c) 2015 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from dateutil.parser import parse as parse_date from pylxd.deprecated import base class LXDOperation(base.LXDBase): def operation_list(self): (state, data) = self.connection.get_object('GET', '/1.0/operations') return data['metadata'] def operation_show(self, operation): (state, data) = self.connection.get_object('GET', operation) return { 'operation_create_time': self.operation_create_time(operation, data.get('metadata')), 'operation_update_time': self.operation_update_time(operation, data.get('metadata')), 'operation_status_code': self.operation_status_code(operation, data.get('metadata')) } def operation_info(self, operation): return self.connection.get_object('GET', operation) def operation_create_time(self, operation, data): if data is None: (state, data) = self.connection.get_object('GET', operation) data = data.get('metadata') return parse_date(data['created_at']).strftime('%Y-%m-%d %H:%M:%S') def operation_update_time(self, operation, data): if data is None: (state, data) = self.connection.get_object('GET', operation) data = data.get('metadata') return parse_date(data['updated_at']).strftime('%Y-%m-%d %H:%M:%S') def operation_status_code(self, operation, data): if data is None: (state, data) = self.connection.get_object('GET', operation) data = data.get('metadata') return data['status'] def operation_wait(self, operation, status_code, timeout): if timeout == -1: return self.connection.get_status( 'GET', '%s/wait?status_code=%s' % (operation, status_code)) else: return self.connection.get_status( 'GET', '%s/wait?status_code=%s&timeout=%s' % (operation, status_code, timeout)) def operation_stream(self, operation, operation_secret): return self.connection.get_ws( '%s/websocket?secret=%s' % (operation, operation_secret)) def operation_delete(self, operation): return self.connection.get_status('DELETE', operation) pylxd-2.2.6/ChangeLog0000644000175000017500000004626613250466213014357 0ustar alexalex00000000000000CHANGES ======= 2.2.6 ----- * Fix Operation class to allow unknown attributes * Bump version to 2.2.6 for development purposes 2.2.5 ----- * Add an additional test to improve code coverage * Fix up file mode to work on Py3 and add tests/docs * Fix FilesManager.put mode checking * Handle mode/uid/gid in FilesManager.put() * And fix the typo to the typo fix * Fix up typo on storage-pools.rst page * Fix warnings on sphinx document generation * Update docs copyright date * Update index.rst with new pages * Add storage-pools.rst to the documentation * Update profiles.rst to match the current implemenation * Add operations.rst to explain operations * Update networks.rst to match models/network.py functionality * Update images.rst to match models/image.py * Update the containers documentation * Add used\_by to profile attributes (#267) * Add some doc links to the README.rst * Add FilesManager.delete and corresponding test (#262) * Add storage pool methods (#254) * cleanup: remove pylxd/mixin.py * Image import - memory hog fix (#252) * Update cert check to look in snap path as well (#253) * remove prints * add testt for snap socket * Revert "Revert "Update pylxd to look in snap path as well"" * Revert "Revert "update to pmock os.path.exists"" * Revert "Update pylxd to look in snap path as well" * Revert "update to pmock os.path.exists" * update to pmock os.path.exists * Update pylxd to look in snap path as well * pep8 update * Add some comments documenting weird name requirements * update formatting * update version of pbr * update CONTRIBUTORS.rst * Add support for LXD storage pools 2.2.4 ----- * Add zulcss to contributors list * Change the email address and add CONTRIBUTORS.rst * fixed not needed multi line * more clean kwargs timeout processing * fix tests n2, no timeout kwarg for unix socket * trying to fix test * fix line too long * fix kwargs check on timeout present * timeout kwarg for client object * Support older versions of LXD * pep8 fixes * Make models resilient to new attributes in LXD * Removing tox envs from travis cache * Fix broken CI testing * Added description to container * Update containers.rst * Update containers.rst 2.2.3 ----- * Fix ephermal container stop * include created\_at for snapshot * pep 8 clean up * add websocket context manager to clean up after itself 2.2.2 ----- * Bump release version * Allow requests 2.12.2+, as its not broken anymore 2.2.1 ----- * Bump version * Change Operation calls to use the manager methods on client * Add \`exists\` to \`Container\`, \`Image\`, and \`Profile\` * Add NotFound exception in the case of 404 2.2 --- * Support more cryptography versions * Fix coverage issues * Allow multipart form uploads for images * Update tests * Rename Container.execute\_with\_result to Container.execute * Bump to 2.2 2.1.3 ----- * Delete the integration test container on completion * Fix unit test * Add integration test for Client.authenticate * Strip trailing slashes * Default cert parameter to the certs that were generated by \`lxc $command\` * Add Container.execute\_with\_result * Prevent request 2.12+ for now * Require pyOpenSSL for older Python releases * Bump to 2.1.3 2.1.2 ----- * Remove the need for a mock-services branch test * Fully deprecate the old api * Remove unneeded debugging code * Fix the ws4py connection handling for container execution * Bump version * Extract the migration data step of migration * Add missing attributes to Network model * Improve documentation with a new container example 2.1.1 ----- * Fix missing coverage * Fix tests * Add integration tests for container.publish * Add documentation for the new Image.export API * Return a file-like object from Image.export * Update the apt cache in the integration test run before installing packages * Make ws4py an optional library * Update \_APINode to have a cleaner interface * Don't allow local connection clients to attempt a migration * Bump to 2.1.1 * Add script to easily run the integration tests 2.1 --- * Remove the deprecated apis in preparation for pylxd 2.1 * Made Model.\_\_dirty\_\_ a set, not a list * Fix other integration tests * Fix start/stop integration test * Bump version number * Move the model tests to pylxd.tests.models * Move pylxd.profile to pylxd.models.profile * Move pylxd.operation to pylxd.models.operation * Move pylxd.network to pylxd.models.network * Move pylxd.image to pylxd.models.image * Move pylxd.container to pylxd.models.container * Move pylxd.certificate to pylxd.models.certificate * Move pylxd.model to pylxd.models.\_model * Add pylxd.models package 2.0.4 ----- * Warn but don't raise exception on unknown attributes * Fix traceback when using when LXD 2.1 * Revert "Fix traceback when using when LXD 2.1" * Fix traceback when using when LXD 2.1 * Doc update: howto change attributes of a container * Add images.create\_from\_\* tests * Add image.create\_from\_simplestreams and image.create\_from\_url * Make auto\_update optional, fixes #165 * Add images.copy() * Add a testcase for deleting aliases which are not in the current image * Don't return self on image alias operations * Add image aliases support * Quote the path of unix endpoints * Remove silly bit * Third time's the charm * Ugh. Missed the proper exception * Add test for operation wait errors * Also ignore sublime files * Add the update\_source attribute images, fixes #158 * Bump version to 2.0.4 * Add support for container and snapshot publish 2.0.3 ----- * Add the ability to get an image by its alias (fixes #152) * Add new docs * Fix lint * Fix for 2.0.3 * Remove unneeded/useless exceptions * Add more tests for model (and fix a bug) * Keep a list of all dirty attributes * Remove \_\_dirty\_\_; opt for attributes keeping track of state * Make migration work * Fix broken merge * Add rename support to Profile (fixes #148) * Convert tests to use async * Only \`wait\` if the response is async * Update deprecated API calls in tests * Fix unit and integration tests * Fix integration test issues * Remove calls to \`update\`, opting for \`save\` * Convert Snapshot to the model API * Update Container to the model api * Update Image to the model api * Convert the Profile to the model api * Update Certificate to use the model code * Explore sorting the certificate issue * Don't if. It's a standard PEM format * Fix the cert handling * Fix lint * Fix tests * Fix busybox.obfuscate (closes #143) * \`delete\` and \`save\` are not allowed in networks * sync should be covered by test coverage * Add support for /1.0/networks * Add the model * Add coverage (why wasn't it already there?) * Fix lint * Remove deprecated codepaths from the integration tests * Pin ws4py to not use 0.3.5 * Add explanatory note for the req-unixsockets version dependency * Revert "Add integration test for HTTP\_PROXY environment variable" * Require latest version of requests-unixsocket * Add integration test for HTTP\_PROXY environment variable * Remove stale comment * Add stub for migration * Add Authentication doc to index * Add docs for how authentication works with LXD * Move Client.authenticated to Client.trusted * Fix lint * Whitespace.. * Add support for certificate authentication * Bump version 2.0.2 ----- * Add license * Add test coverage for Operation * Fix lint * Oops. Forgot to remove references to Waitable * Remove Waitable entirely * Remove Waitable.get\_operation * Fix lint * Use Operation * Remove XXX comment * Fix execute * Unify the websocket url logic * Add integration test * Fix lint * Add tests for websocket events * Add documentation for websocket events support * Add websocket events support * Add more tests for container state * Increase test coverage for pylxd.image * Add image export support with accompanying tests * Update client back to 100% coverage * Fix lint * More test coverage, specific to the patch * Add test coverage (to satisfy codecov) * Update client tests for more coverage * Fix lint * Fix the unit tests * Add response assertions for lxd * Fix container test to be able to run in parallel * Fix lint * Update mock\_lxd to look more like lxd * Fix profile integration tests * Fix test\_images integration tests * Fix the container integration tests * Update host cache based on feedback * Cache LXD host info * Add coverage exclusions * Add a manager-esque FilesManager to namespace files * Update the mock service to be more strict * Don't run coverage on a method that raises an exception * Refactor the manager protocol to be a little cleaner * Use the new deprecated decorator * Add documentation for the snapshots manager * Don't worry about coverage in \`deprecated\` * Change all internal instances of \`Container.reload\` to \`Container.fetch\` * Fix pep8 * Add a deprecated decorator * Fix pep8 * Update for future development * Add Container.snapshots manager API * Remove HACKING (it's unneeded) * Add more changes based on a local build of the docs * Add documentation for the other objects * Update usage docs for containers * Update installation doc * Update skeleton copyright (not OpenStack Foundation) * Update contributing document * Update README.rst 2.0.1 ----- * Release of pylxd 2.0.1 * Remove \`Profile.rename\` code; add a test * Add Image.fetch and impartial image state knowledge to Image * Implement ObjectIncomplete exception on \`Container.update\` * Implement fetch for Profile * Add test for issue #110 * Add certificate handling for LXD over https * Delete redundant tests * Fix https connection * Removed some duplicated logic * Don't regress on test coverage * Consolidate ContainerState into pylxd.container * Use python 3.5 * Fix travis run * Add coverage report to tox * Small typo fixed * Add some stinkin' badges * Add codecov.io integration * Fix pep8 * Add data to the exceptions, so they can be explored better * Allow specification of devices in a profile * Add new exceptions to Profile and Image * Add CreateFailed exception to Container * Raise NotFound when the container isn't found, rather than NameError * Add \`pylxd.client.Client\` to \`pylxd.Client\` * Add validation to the \`pylxd.Client\` creation * Make architecture a string type * status is a string now * Documentation improvements * Travis.yml update * Remove module that breaks autodoc * Update tests and code for issues found in updating example * Update api example for PyLXD 2.0 API * Add new pylxd unittests * Add more coverage settings * Fix the DEFAULT\_TLS\_VERSION holes * Add tests for pylxd.client * Fix a bug with LXD\_DIR in the lxd client * Update README to display instructions of installing * Add basic API documentation * Fix documentation * No py34, just py3 * python3: Fix use of iteritems * python3: Fix use of urllib quote * Tag 2.0.0 * Container methods fixed for freeze, unfreeze and restart * Fix six.PY34 into six.PY3 * Fix pep8 * Add host config API call * Grab the certificate of the host * Fix container migration 2.0.0b2 ------- * Bump to 2.0.0b2 * Fix formatting file for travis * Add state function on container to return state with network. ex: container.state().network['eth0'] * Fix change "creation\_date" to "created\_at" 2.0.0b1 ------- * Bump version 2.0.0b * Move pylxd.tests to pylxd.deprecated.tests * Move pylxd.profiles pylxd.profile * Move pylxd.profiles.LXDProfile to pylxd.deprecated.profiles * Move pylxd.operation.LXDOperation to pylxd.deprecated.operation * Move pylxd.image.LXDImage to pylxd.deprecated.image * Move pylxd.network import pylxd.deprecated.network * Move pylxd.hosts to pylxd.deprecated.hosts * Move pylxd.exceptions to pylxd.deprecated.exceptions * Move pylxd.utils to pylxd.deprecated.utils * Move pylxd.connection import pylxd.deprecated.connection * Move pylxd.certificate to pylxd.deprecated.certificate * Move pylxd.container.LXDContainer to pylxd.deprecated.container * Move base to deprecated * API now raises a DeprecationWarning * Move api to the deprecated module * Add docstrings * We support python 3 lol * Head and shoulders * Fixed the APINode tests * Cleanup the convenience/wrapper classes in Client * Move Operation to its final place * Move Image to its final spot * Move Profile to its final spot * Move Container to its final spot * Fix the integration tests * Add tests and functionality for profiles * Update the create\_image use for multiple return values * Add support for updating/deleting images * Get the basic Images skeleton working * Change the way that test objects are named * Fully functional Container class * Remove redundant tests * Get the first of the integration tests working * Fix integration tests * Move \_APINode * Initial skeleton for 2.0 api * Add more integration tests * Add integration tests for containers * Support python 3.4+ * Update requests to not conflict * Ugh. So many things, but just remove openstack junk * No pypy * Upgrade pypy * Update travis config * Upgrade ostestr * Blacklist with travis as well.. * Blacklist the integration tests * Fix lint * Add integration tests * Add support for unix sockets and post * Add better docs for APINode * Add APINode class, with accompanying tests * Add requests library to dependencies * Update timeout usage * Remove override of close() in WebSocketClient * Start next release 0.19 ---- * Bump to v0.19 * Remove get\_ws() method stub with real websocket implementation * Update requirements * Detect SSL TLS version * Add pyopenssl to requirements * Refactor LXDConnection http requests * Fix issue in LXDContainer.container\_running() method * faster behavior for determining if a container is defined * update version to keep tox happy * refer to the operation by its URL always 0.18 ---- * Bump to 0.18 * Bump the version * Automatically populate container\_migrate metadata * Use os-testr for test suite execution * Update the docs, add travis status indicator * Switch to using get\_object in image\_upload * Container state * Bump to 0.17 * fix pep8 * Fix tests for python34 * fix syntax typo * fix operation\_test * Fixup failing container migration test * Fixup test failures due to passing {} for default headers * Revert "Use object rather than status." * Ensure that an empty dict is passed by default * Tidy all pep8 warnings and errors * Revert from using eventlet directly, avoiding hard link to async framework * Use correct octal literal (not hex) * weak ordering of parameters in expected test results * Revert "Return metadata for container\_state" * Add force attribue to test\_container\_actions * Add missing dependency to requirements.txt on eventlet, fixup py3 syntax * Ensure TLS 1.2 protocol is enforced when communicating with LXD over HTTPS * Add container sync * wire up local move * Fix requirements * Add container move local * Add container\_local\_copy * Fix bad merge * Use eventlet green for httplib and socket * switch to eventlet patcher * fix typo * add websocket stream * Return metadata for container\_state * Add container\_info * Add websocket support * Removed unnecessary file close * Add .travis.yml * Implement container file upload * Use object rather than status * Tag 0.16 * Fix more typos * Fix typo * Add image headers * Container copy doesnt send criu * Force the container action * Update container migration * fix up CONTRIBUTING.rst * Add tests for info, config and migrate calls * Fix typo * Fix coveragerc * Fix typo * Add container\_migrate * Add container\_info * Fix spelling typo * Add support for 'lxc info' * tag 0.13 * Sync requirements * Fix test\_https\_proxy\_connection assertion * Fix certificates list * Refactor profiles tests to reduce copypasta * Refactor operation tests to reduce copypasta * Refactor network tests to reduce copypasta * Refactor image alias tests to reduce copypasta * Refactor host tests to reduce copypasta * Refactor container tests to reduce copypasta * Refactor certificate test to use LXDAPITestBase * Fix image test rename * Refactor image tests to reduce copypasta * Use \_once\_ and add asserts to container tests * Fix image size calculation in py3 * API.profile\_rename is implemented, in fact * Refactor image alias tests * Fix LXDAlias.alias\_{update,rename} * Fix LXDAlias.alias\_defined * Refactor alias tests * Add image export fail test and add a call assert * Add file upload test * Add failed image operations tests * Fix image\_{update,rename} calls * Refactor image operations tests * Drop image\_defined function * Fix get\_image\_size * Add image info tests * Refactor image date tests * Refactor and improve image info test * Fix LXDImage.image\_defined * Refactor base image tests * Refactor and complete host test * Add LXDConnection.get\_ tests * Add base connection tests * Fix host\_ping tests * Introduce base exceptions.PyLXDException * Fix test\_container\_run\_command * Use six.moves.urllib for urlencode * Make operation tests timezone-aware * Fix network list * Add network tests * Add operation tests * Fix LXDProfile.profile\_rename raise * Add profile tests * Add certificate API tests * Add container\_run\_command test * Fix typos in LXDContainer.container\_snapshot\_\* * Add container snapshots tests * Fix LXDContainer.put\_container\_file() raise * Add container file operations tests * Fix LXDContainer.container\_running * Add container api tests * Add annotated\_data decorator * Add missing alias tests * Add missing image tests * Improve api coverage * Use absolute imports for py3 compatibility * Use six.moves.http\_client for py3 compatibility * Fix PEP8 errors * Fix test\_image mocks to use get\_object * Fix test\_image\_alias fake data * Fix test\_image\_upload\_date for timezone difference * tag 0.12 * Fi alias listing * Fix contianer -> container * Fix enviorn -> environ * fix alias list * Add more tests * Remove functional tests * Update tests * Fix image\_defined * Add more hosts tests * Add more host tests * Add missing file * Re-do unittests * fix container status * Add container\_update * Fix get\_status * Improve error checking * Add alias existance * raise more useful exception * Add certificate unit tests * Drop unused imports * Fix typo * Check for existance * Fix image\_defined * Various fixes * Update profile usage * Fix up hosts * Fix some bugs add lxd\_version * fix image export; allow image upload from blob * add support for connecting to remote LXD hosts * fix typo in run\_command * fix date parsing and typo * Add exec support * Add snapshot support * Add support for contiainer\_running * container\_defined * Add container timeout * Add container defined * Fix typo in operations * update requiremets.txt to fix a bug. Fixes #3 * remove oslo\_utils and calculate the size directly. Fixes #4 * Fix get\_status * Changes the old print to the new print compatible with py2 and py3 and changes the error messages to reflect the actual error 0.1 --- * Add missing example * Update Readme * Add example and various fix ups for example * wire up more containers * Add version and container support * fix typos * Wire up certificates * Fix up pep8 warnings * Wire up profiles * rename client to API * wire up operation * wire up network * Wire up image alias * Wire up images * Wire up hosts * Fix warnings and pep8 issues * Refactor pylxd connection * image updates * fix typos * add kernel and lxc version * more refactoring * more refactoring * refactor hosts again * fix typo * add trust info * break out host info * start wiring up host\_info * fix exception handling * more smarts for host\_up * Fix host ping * fix typo * wire up a bit harder * Wire up ping * Fix typo * Fix hosts * Add host\_ping example * Add skeleton * Update connections info * Initial commit * Initial Cookiecutter Commit pylxd-2.2.6/run_integration_tests0000775000175000017500000000207013250267630017151 0ustar alexalex00000000000000#!/bin/sh CONTAINER_IMAGE=ubuntu:16.04 CONTAINER_NAME=pylxd-`uuidgen | cut -d"-" -f1` # This creates a privileged container, because I was bumping into situations where it # seemed that we had maxed out user namespaces (I haven't checked it out, but it's likely # a bug in LXD). lxc launch $CONTAINER_IMAGE $CONTAINER_NAME -c security.nesting=true -c security.privileged=true sleep 5 # Wait for the network to come up lxc exec $CONTAINER_NAME -- apt-get update lxc exec $CONTAINER_NAME -- apt-get install -y tox python3-dev libssl-dev libffi-dev build-essential lxc exec $CONTAINER_NAME -- lxc config set core.trust_password password lxc exec $CONTAINER_NAME -- lxc config set core.https_address [::] lxc exec $CONTAINER_NAME -- mkdir -p /opt/pylxd # NOTE: rockstar (13 Sep 2016) - --recursive is not supported in lxd <2.1, so # until we have pervasive support for that, we'll do this tar hack. tar cf - * .git | lxc exec $CONTAINER_NAME -- tar xf - -C /opt/pylxd lxc exec $CONTAINER_NAME -- /bin/sh -c "cd /opt/pylxd && tox -eintegration" lxc delete --force $CONTAINER_NAME pylxd-2.2.6/MANIFEST.in0000664000175000017500000000013513250460012014315 0ustar alexalex00000000000000include AUTHORS include ChangeLog exclude .gitignore exclude .gitreview global-exclude *.pycpylxd-2.2.6/PKG-INFO0000664000175000017500000000455413250466213013676 0ustar alexalex00000000000000Metadata-Version: 1.1 Name: pylxd Version: 2.2.6 Summary: python library for lxd Home-page: http://www.linuxcontainers.org Author: Paul Hummer and others (see CONTRIBUTORS.rst) Author-email: lxc-devel@lists.linuxcontainers.org License: UNKNOWN Description-Content-Type: UNKNOWN Description: pylxd ~~~~~ .. image:: http://img.shields.io/pypi/v/pylxd.svg :target: https://pypi.python.org/pypi/pylxd .. image:: https://travis-ci.org/lxc/pylxd.svg?branch=master :target: https://travis-ci.org/lxc/pylxd .. image:: https://codecov.io/github/lxc/pylxd/coverage.svg?branch=master :target: https://codecov.io/github/lxc/pylxd .. image:: https://readthedocs.org/projects/docs/badge/?version=latest :target: https://pylxd.readthedocs.io/en/latest/?badge=latest A Python library for interacting with the LXD REST API. Installation ============= ``pip install pylxd`` Bug reports =========== Bug reports can be filed on the `GitHub repository `_. Support and discussions ======================= We use the `LXC mailing-lists for developer and user discussions `_. If you prefer live discussions, some of us also hang out in `#lxcontainers `_ on irc.freenode.net. What is LXD? `LXD: Introduction `_ PyLXD API Documentation: `http://pylxd.readthedocs.io/en/latest/ `_ Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 pylxd-2.2.6/.travis.yml0000644000175000017500000000056613236353114014706 0ustar alexalex00000000000000language: python python: - "3.5" env: matrix: - TOXENV=py27 - TOXENV=py3 - TOXENV=pep8 # - TOXENV=integration # requires a remote lxd setup install: - travis_retry pip install tox - pip install codecov script: - tox - test -d .tox/$TOXENV/log && cat .tox/$TOXENV/log/*.log || true cache: directories: - $HOME/.cache/pip after_success: - codecov pylxd-2.2.6/README.rst0000664000175000017500000000225013250267630014261 0ustar alexalex00000000000000pylxd ~~~~~ .. image:: http://img.shields.io/pypi/v/pylxd.svg :target: https://pypi.python.org/pypi/pylxd .. image:: https://travis-ci.org/lxc/pylxd.svg?branch=master :target: https://travis-ci.org/lxc/pylxd .. image:: https://codecov.io/github/lxc/pylxd/coverage.svg?branch=master :target: https://codecov.io/github/lxc/pylxd .. image:: https://readthedocs.org/projects/docs/badge/?version=latest :target: https://pylxd.readthedocs.io/en/latest/?badge=latest A Python library for interacting with the LXD REST API. Installation ============= ``pip install pylxd`` Bug reports =========== Bug reports can be filed on the `GitHub repository `_. Support and discussions ======================= We use the `LXC mailing-lists for developer and user discussions `_. If you prefer live discussions, some of us also hang out in `#lxcontainers `_ on irc.freenode.net. What is LXD? `LXD: Introduction `_ PyLXD API Documentation: `http://pylxd.readthedocs.io/en/latest/ `_ pylxd-2.2.6/doc/0000775000175000017500000000000013250466213013336 5ustar alexalex00000000000000pylxd-2.2.6/doc/source/0000775000175000017500000000000013250466213014636 5ustar alexalex00000000000000pylxd-2.2.6/doc/source/profiles.rst0000664000175000017500000000335613250267630017224 0ustar alexalex00000000000000Profiles ======== `Profile` describe configuration options for containers in a re-usable way. Manager methods --------------- Profiles can be queried through the following client manager methods: - `all()` - Retrieve all profiles - `exists()` - See if a profile with a name exists. Returns `boolean`. - `get()` - Get a specific profile, by its name. - `create(name, config, devices)` - Create a new profile. The name of the profile is required. `config` and `devices` dictionaries are optional, and the scope of their contents is documented in the LXD documentation. Profile attributes ------------------ - `config` - config options for containers - `description` - The description of the profile - `devices` - device options for containers - `name` - The name of the profile - `used_by` - A list of containers using this profile Profile methods --------------- - `rename` - Rename the profile. - `save` - save a profile. This uses the PUT HTTP method and not the PATCH. - `delete` - deletes a profile. Examples -------- :class:`~profile.Profile` operations follow the same manager-style as Containers and Images. Profiles are keyed on a unique name. .. code-block:: python >>> profile = client.profiles.get('my-profile') >>> profile The profile can then be modified and saved. >>> profile.config = profile.config.update({'security.nesting': 'true'}) >>> profile.update() To create a new profile, use `create` with a name, and optional `config` and `devices` config dictionaries. >>> profile = client.profiles.create( ... 'an-profile', config={'security.nesting': 'true'}, ... devices={'root': {'path': '/', 'size': '10GB', 'type': 'disk'}}) pylxd-2.2.6/doc/source/conf.py0000775000175000017500000000457313250267630016153 0ustar alexalex00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', #'sphinx.ext.intersphinx', ] # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'pylxd' copyright = u'2016-2018, Canonical Ltd' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = ['static'] # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', '%s.tex' % project, u'%s Documentation' % project, u'Canonical Ltd', 'manual'), ] # Example configuration for intersphinx: refer to the Python standard library. #intersphinx_mapping = {'http://docs.python.org/': None} pylxd-2.2.6/doc/source/installation.rst0000644000175000017500000000055313115536516020076 0ustar alexalex00000000000000============ Installation ============ If you're running on Ubuntu Xenial or greater:: sudo apt-get install python-pylxd lxd Otherwise you can track LXD development on other Ubuntu releases:: sudo add-apt-repository ppa:ubuntu-lxc/lxd-git-master && sudo apt-get update sudo apt-get install lxd Or install pylxd using pip:: pip install pylxd pylxd-2.2.6/doc/source/containers.rst0000664000175000017500000001565313250267630017551 0ustar alexalex00000000000000Containers ========== `Container` objects are the core of LXD. Containers can be created, updated, and deleted. Most of the methods for operating on the container itself are asynchronous, but many of the methods for getting information about the container are synchronous. Manager methods --------------- Containers can be queried through the following client manager methods: - `exists(name)` - Returns `boolean` indicating if the container exists. - `all()` - Retrieve all containers. - `get()` - Get a specific container, by its name. - `create(config, wait=False)` - Create a new container. This method requires the container config as the first parameter. The config itself is beyond the scope of this documentation. Please refer to the LXD documentation for more information. This method will also return immediately, unless `wait` is `True`. Container attributes -------------------- For more information about the specifics of these attributes, please see the LXD documentation. - `architecture` - The container architecture. - `config` - The container config - `created_at` - The time the container was created - `devices` - The devices for the container - `ephemeral` - Whether the container is ephemeral - `expanded_config` - An expanded version of the config - `expanded_devices` - An expanded version of devices - `name` - (Read only) The name of the container. This attribute serves as the primary identifier of a container - `description` - A description given to the container - `profiles` - A list of profiles applied to the container - `status` - (Read only) A string representing the status of the container - `last_used_at` - (Read only) when the container was last used - `status_code` - (Read only) A LXD status code of the container - `stateful` - (Read only) Whether the container is stateful Container methods ----------------- - `rename` - Rename a container. Because `name` is the key, it cannot be renamed by simply changing the name of the container as an attribute and calling `save`. The new name is the first argument and, as the method is asynchronous, you may pass `wait=True` as well. - `save` - Update container's configuration - `state` - Get the expanded state of the container. - `start` - Start the container - `stop` - Stop the container - `restart` - Restart the container - `freeze` - Suspend the container - `unfreeze` - Resume the container - `execute` - Execute a command on the container. The first argument is a list, in the form of `subprocess.Popen` with each item of the command as a separate item in the list. Returns a two part tuple of `(stdout, stderr)`. This method will block while the command is executed. - `migrate` - Migrate the container. The first argument is a client connection to the destination server. This call is asynchronous, so `wait=True` is optional. The container on the new client is returned. - `publish` - Publish the container as an image. Note the container must be stopped in order to use this method. If `wait=True` is passed, then the image is returned. Examples -------- If you'd only like to fetch a single container by its name... .. code-block:: python >>> client.containers.get('my-container') If you're looking to operate on all containers of a LXD instance, you can get a list of all LXD containers with `all`. .. code-block:: python >>> client.containers.all() [,] In order to create a new :class:`~container.Container`, a container config dictionary is needed, containing a name and the source. A create operation is asynchronous, so the operation will take some time. If you'd like to wait for the container to be created before the command returns, you'll pass `wait=True` as well. .. code-block:: python >>> config = {'name': 'my-container', 'source': {'type': 'none'}} >>> container = client.containers.create(config, wait=False) >>> container If you were to use an actual image source, you would be able to operate on the container, starting, stopping, snapshotting, and deleting the container. .. code-block:: python >>> config = {'name': 'my-container', 'source': {'type': 'image', 'alias': 'ubuntu/trusty'}} >>> container = client.containers.create(config, wait=True) >>> container.start() >>> container.freeze() >>> container.delete() To modify container's configuration method `save` should be called after :class:`~container.Container` attributes changes. >>> container = client.containers.get('my-container') >>> container.ephemeral = False >>> container.devices = { 'root': { 'path': '/', 'type': 'disk', 'size': '7GB'} } >>> container.save Container Snapshots ------------------- Each container carries its own manager for managing :class:`~container.Snapshot` functionality. It has `get`, `all`, and `create` functionality. Snapshots are keyed by their name (and only their name, in pylxd; LXD keys them by /, but the manager allows us to use our own namespacing). A container object (returned by `get` or `all`) has the following methods: - `rename` - rename a snapshot - `publish` - create an image from a snapshot. However, this may fail if the image from the snapshot is bigger than the logical volume that is allocated by lxc. See https://github.com/lxc/lxd/issues/2201 for more details. The solution is to increase the `storage.lvm_volume_size` parameter in lxc. .. code-block:: python >>> snapshot = container.snapshots.get('an-snapshot') >>> snapshot.created_at '1983-06-16T2:38:00' >>> snapshot.rename('backup-snapshot', wait=True) >>> snapshot.delete(wait=True) To create a new snapshot, use `create` with a `name` argument. If you want to capture the contents of RAM in the snapshot, you can use `stateful=True`. .. note:: Your LXD requires a relatively recent version of CRIU for this. .. code-block:: python >>> snapshot = container.snapshots.create( ... 'my-backup', stateful=True, wait=True) >>> snapshot.name 'my-backup' Container files --------------- Containers also have a `files` manager for getting and putting files on the container. The following methods are available on the `files` manager: - `put` - push a file into the container. - `get` - get a file from the container. - `delete_available` - If the `file_delete` extension is available on the lxc host, then this method returns `True` and the `delete` method is available. - `delete` - delete a file on the container. .. note:: All file operations use `uid` and `gid` of 0 in the container. i.e. root. .. code-block:: python >>> filedata = open('my-script').read() >>> container.files.put('/tmp/my-script', filedata) >>> newfiledata = container.files.get('/tmp/my-script2') >>> open('my-script2', 'wb').write(newfiledata) pylxd-2.2.6/doc/source/contributing.rst0000644000175000017500000000232313115536516020101 0ustar alexalex00000000000000============ Contributing ============ pyLXD development is done `on Github `_. Pull Requests and Issues should be filed there. We try and respond to PRs and Issues within a few days. If you would like to contribute large features or have big ideas, it's best to post on to `the lxc-users list `_ to discuss your ideas before submitting PRs. Code standards -------------- pyLXD follows `PEP 8 `_ as closely as practical. To check your compliance, use the `pep8` tox target:: tox -epep8 Testing ------- pyLXD tries to follow best practices when it comes to testing. PRs are gated by `Travis CI `_ and `CodeCov `_. It's best to submit tests with new changes, as your patch is unlikely to be accepted without them. To run the tests, you can use nose:: nosetests pylxd ...or, alternatively, you can use `tox` (with the added bonus that it will test python 2.7, python 3, and pypy, as well as run pep8). This is the way that Travis will test, so it's recommended that you run this at least once before submitting a Pull Request. pylxd-2.2.6/doc/source/authentication.rst0000644000175000017500000000235313115536516020414 0ustar alexalex00000000000000===================== Client Authentication ===================== When using LXD over https, LXD uses an asymmetric keypair for authentication. The keypairs are added to the authentication database after entering the LXD instance's "trust password". Generate a certificate ====================== To generate a keypair, you should use the `openssl` command. As an example:: openssl req -newkey rsa:2048 -nodes -keyout lxd.key -out lxd.csr openssl x509 -signkey lxd.key -in lxd.csr -req -days 365 -out lxd.crt For more detail on the commands, or to customize the keys, please see the documentation for the `openssl` command. Authenticate a new keypair ========================== If a client is created using this keypair, it would originally be "untrusted", essentially meaning that the authentication has not yet occurred. .. code-block:: python >>> from pylxd import Client >>> client = Client( ... endpoint='http://10.0.0.1:8443', ... cert=('lxd.crt', 'lxd.key')) >>> client.trusted False In order to authenticate the client, pass the lxd instance's trust password to `Client.authenticate` .. code-block:: python >>> client.authenticate('a-secret-trust-password') >>> client.trusted >>> True pylxd-2.2.6/doc/source/networks.rst0000664000175000017500000000113113250267630017242 0ustar alexalex00000000000000Networks ======== `Network` objects show the current networks available to lxd. They are read-only via the REST API. Manager methods --------------- Networks can be queried through the following client manager methods: - `all()` - Retrieve all networks - `get()` - Get a specific network, by its name. Network attributes ------------------ - `name` - The name of the network - `type` - The type of the network - `used_by` - A list of containers using this network - `config` - The configuration associated with the network. - `managed` - Boolean; whether LXD manages the network pylxd-2.2.6/doc/source/storage-pools.rst0000664000175000017500000000326613250267630020177 0ustar alexalex00000000000000Storage Pools ============= LXD supports creating and managing storage pools and storage volumes. General keys are top-level. Driver specific keys are namespaced by driver name. Volume keys apply to any volume created in the pool unless the value is overridden on a per-volume basis. `Storage Pool` objects represent the json object that is returned from `GET /1.0/storage-pools/` and then the associated methods that are then available at the same endpoint. Manager methods --------------- Storage-pools can be queried through the following client manager methods: - `all()` - Return a list of storage pools. - `get()` - Get a specific storage-pool, by its name. - `exists()` - Return a boolean for whether a storage-pool exists by name. - `create()` - Create a storage-pool. **Note the config in the create class method is the WHOLE json object described as `input` in the API docs.** e.g. the 'config' key in the API docs would actually be `config.config` as passed to this method. Storage-pool attributes ----------------------- For more information about the specifics of these attributes, please see the `LXD documentation`_. - `name` - the name of the storage pool - `driver` - the driver (or type of storage pool). e.g. 'zfs' or 'btrfs', etc. - `used_by` - which containers (by API endpoint `/1.0/containers/`) are using this storage-pool. - `config` - a string (json encoded) with some information about the storage-pool. e.g. size, source (path), volume.size, etc. .. _LXD documentation: https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10storage-pools Storage-pool methods -------------------- The are no storage pool methods defined yet. pylxd-2.2.6/doc/source/operations.rst0000664000175000017500000000145113250267630017556 0ustar alexalex00000000000000Operations ========== `Operation` objects detail the status of an asynchronous operation that is taking place in the background. Some operations (e.g. image related actions) can take a long time and so the operation is performed in the background. They return an operation `id` that may be used to discover the state of the operation. Manager methods --------------- Operations can be queried through the following client manager methods: - `get()` - Get a specific network, by its name. - `wait_for_operation()` - get an operation, but wait until it is complete before returning the operation object. Operation object methods ------------------------ - `wait()` - Wait for the operation to complete and return. Note that this can raise a `LXDAPIExceptiion` if the operations fails. pylxd-2.2.6/doc/source/certificates.rst0000644000175000017500000000160113115536516020035 0ustar alexalex00000000000000Certificates ============ Certificates are used to manage authentications in LXD. Certificates are not editable. They may only be created or deleted. None of the certificate operations in LXD are asynchronous. Manager methods --------------- Certificates can be queried through the following client manager methods: - `all()` - Retrieve all certificates. - `get()` - Get a specifit certificate, by its fingerprint. - `create()` - Create a new certificate. This method requires a first argument that is the LXD trust password, and the cert data, in binary format. Certificate attributes ---------------------- Certificates have the following attributes: - `fingerprint` - The fingerprint of the certificate. Certificates are keyed off this attribute. - `certificate` - The certificate itself, in PEM format. - `type` - The certificate type (currently only "client") pylxd-2.2.6/doc/source/images.rst0000664000175000017500000000647113250267630016647 0ustar alexalex00000000000000Images ====== `Image` objects are the base for which containers are built. Many of the methods of images are asynchronous, as they required reading and writing large files. Manager methods --------------- Images can be queried through the following client manager methods: - `all()` - Retrieve all images. - `get()` - Get a specific image, by its fingerprint. - `get_by_alias()` - Ger a specific image using its alias. And create through the following methods, there's also a copy method on an image: - `create(data, public=False, wait=False)` - Create a new image. The first argument is the binary data of the image itself. If the image is public, set `public` to `True`. - `create_from_simplestreams(server, alias, public=False, auto_update=False, wait=False)` - Create an image from simplestreams. - `create_from_url(url, public=False, auto_update=False, wait=False)` - Create an image from a url. Image attributes ---------------- For more information about the specifics of these attributes, please see the `LXD documentation`_. - `aliases` - A list of aliases for this image - `auto_update` - Whether the image should auto-update - `architecture` - The target architecture for the image - `cached` - Whether the image is cached - `created_at` - The date and time the image was created - `expires_at` - The date and time the image expires - `filename` - The name of the image file - `fingerprint` - The image fingerprint, a sha2 hash of the image data itself. This unique key identifies the image. - `last_used_at` - The last time the image was used - `properties` - The configuration of image itself - `public` - Whether the image is public or not - `size` - The size of the image - `uploaded_at` - The date and time the image was uploaded - `update_source` - A dict of update informations .. _LXD documentation: https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10imagesfingerprint Image methods ------------- - `export` - Export the image. Returns a file object with the contents of the image. *Note: Prior to pylxd 2.1.1, this method returned a bytestring with data; as it was not unbuffered, the API was severely limited.* - `add_alias` - Add an alias to the image. - `delete_alias` - Remove an alias. - `copy` - Copy the image to another LXD client. Examples -------- :class:`~image.Image` operations follow the same protocol from the client`s `images` manager (i.e. `get`, `all`, and `create`). Images are keyed on a sha-1 fingerprint of the image itself. To get an image... .. code-block:: python >>> image = client.images.get( ... 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') >>> image Once you have an image, you can operate on it as before: .. code-block:: python >>> image.public False >>> image.public = True >>> image.update() To create a new Image, you'll open an image file, and pass that to `create`. If the image is to be public, `public=True`. As this is an asynchonous operation, you may also want to `wait=True`. .. code-block:: python >>> image_data = open('an_image.tar.gz').read() >>> image = client.images.create(image_data, public=True, wait=True) >>> image.fingerprint 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' pylxd-2.2.6/doc/source/events.rst0000644000175000017500000000127413115536516016702 0ustar alexalex00000000000000Events ====== LXD provides an `/events` endpoint that is upgraded to a streaming websocket for getting LXD events in real-time. The :class:`~pylxd.Client`'s `events` method will return a websocket client that can interact with the web socket messages. .. code-block:: python >>> ws_client = client.events() >>> ws_client.connect() >>> ws_client.run() A default client class is provided, which will block indefinitely, and collect all json messages in a `messages` attribute. An optional `websocket_client` parameter can be provided when more functionality is needed. The `ws4py` library is used to establish the connection; please see the `ws4py` documentation for more information. pylxd-2.2.6/doc/source/index.rst0000664000175000017500000000111713250267630016501 0ustar alexalex00000000000000.. pylxd documentation master file, created by sphinx-quickstart on Tue Jul 9 22:26:36 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to pylxd's documentation! ================================= Contents: .. toctree:: :maxdepth: 2 installation usage authentication events certificates containers images networks profiles operations storage-pools contributing api Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pylxd-2.2.6/doc/source/usage.rst0000664000175000017500000000614713250267630016506 0ustar alexalex00000000000000=============== Getting started =============== .. currentmodule:: pylxd Client ====== Once you have :doc:`installed `, you're ready to instantiate an API client to start interacting with the LXD daemon on localhost: .. code-block:: python >>> from pylxd import Client >>> client = Client() If your LXD instance is listening on HTTPS, you can pass a two part tuple of (cert, key) as the `cert` argument. .. code-block:: python >>> from pylxd import Client >>> client = Client( ... endpoint='http://10.0.0.1:8443', ... cert=('/path/to/client.crt', '/path/to/client.key')) Note: in the case where the certificate is self signed (LXD default), you may need to pass `verify=False`. Querying LXD ------------ LXD exposes a number of objects via its REST API that are used to orchestrate containers. Those objects are all accessed via manager attributes on the client itself. This includes `certificates`, `containers`, `images`, `networks`, `operations`, and `profiles`. Each manager has methods for querying the LXD instance. For example, to get all containers in a LXD instance .. code-block:: python >>> client.containers.all() [,] For specific manager methods, please see the documentation for each object. pylxd Objects ------------- Each LXD object has an analagous pylxd object. Returning to the previous `client.containers.all` example, a `Container` object is manipulated as such: .. code-block:: python >>> container = client.containers.all()[0] >>> container.name 'lxd-container' Each pylxd object has a lifecycle which includes support for transactional changes. This lifecycle includes the following methods and attributes: - `sync()` - Synchronize the object with the server. This method is called implicitly when accessing attributes that have not yet been populated, but may also be called explicitly. Why would attributes not yet be populated? When retrieving objects via `all`, LXD's API does not return a full representation. - `dirty` - After setting attributes on the object, the object is considered "dirty". - `rollback()` - Discard all local changes to the object, opting for a representation taken from the server. - `save()` - Save the object, writing changes to the server. Returning again to the `Container` example .. code-block:: python >>> container.config { 'security.privileged': True } >>> container.config.update({'security.nesting': True}) >>> container.dirty True >>> container.rollback() >>> container.dirty False >>> container.config { 'security.privileged': True } >>> container.config = {'security.privileged': False} >>> container.save(wait=True) # The object is now saved back to LXD A note about asynchronous operations ------------------------------------ Some changes to LXD will return immediately, but actually occur in the background after the http response returns. All operations that happen this way will also take an optional `wait` parameter that, when `True`, will not return until the operation is completed. pylxd-2.2.6/doc/source/api.rst0000664000175000017500000000151113250267630016141 0ustar alexalex00000000000000================= API documentation ================= Client ------ .. autoclass:: pylxd.client.Client :members: Exceptions ---------- .. autoclass:: pylxd.exceptions.LXDAPIException .. autoclass:: pylxd.exceptions.NotFound .. autoclass:: pylxd.exceptions.ClientConnectionFailed Certificate ----------- .. autoclass:: pylxd.models.Certificate :members: Container --------- .. autoclass:: pylxd.models.Container :members: .. autoclass:: pylxd.models.Snapshot :members: Image ----- .. autoclass:: pylxd.models.Image :members: Network ------- .. autoclass:: pylxd.models.Network :members: Operation --------- .. autoclass:: pylxd.models.Operation :members: Profile ------- .. autoclass:: pylxd.models.Profile :members: Storage Pool ------------ .. autoclass:: pylxd.models.StoragePool :members: pylxd-2.2.6/test-requirements.txt0000644000175000017500000000034013236353114017024 0ustar alexalex00000000000000ddt>=0.7.0 nose>=1.3.7 mock>=1.3.0 flake8>=2.5.0 coverage>=4.1 mock-services>=0.3 # mock-services is old and unmaintained. Doesn't work with newer versions of # requests-mock. Thus, we have to pin it down. requests-mock<1.2 pylxd-2.2.6/.testr.conf0000644000175000017500000000047613115536516014670 0ustar alexalex00000000000000[DEFAULT] test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--listpylxd-2.2.6/integration/0000775000175000017500000000000013250466213015114 5ustar alexalex00000000000000pylxd-2.2.6/integration/test_client.py0000664000175000017500000000236513250267630020013 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pylxd import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning from integration.testing import IntegrationTestCase requests.packages.urllib3.disable_warnings(InsecureRequestWarning) class TestClient(IntegrationTestCase): """Tests for `Client`.""" def test_authenticate(self): # This is another test with multiple assertions, as it is a test of # flow, rather than a single source of functionality. client = pylxd.Client('https://127.0.0.1:8443/', verify=False) self.assertFalse(client.trusted) client.authenticate('password') self.assertTrue(client.trusted) pylxd-2.2.6/integration/test_profiles.py0000664000175000017500000000537213250267630020361 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import unittest from pylxd import exceptions from integration.testing import IntegrationTestCase class TestProfiles(IntegrationTestCase): """Tests for `Client.profiles.`""" def test_get(self): """A profile is fetched by name.""" name = self.create_profile() self.addCleanup(self.delete_profile, name) profile = self.client.profiles.get(name) self.assertEqual(name, profile.name) def test_all(self): """All profiles are fetched.""" name = self.create_profile() self.addCleanup(self.delete_profile, name) profiles = self.client.profiles.all() self.assertIn(name, [profile.name for profile in profiles]) def test_create(self): """A profile is created.""" name = 'an-profile' config = {'limits.memory': '1GB'} profile = self.client.profiles.create(name, config) self.addCleanup(self.delete_profile, name) self.assertEqual(name, profile.name) self.assertEqual(config, profile.config) class TestProfile(IntegrationTestCase): """Tests for `Profile`.""" def setUp(self): super(TestProfile, self).setUp() name = self.create_profile() self.profile = self.client.profiles.get(name) def tearDown(self): super(TestProfile, self).tearDown() self.delete_profile(self.profile.name) def test_save(self): """A profile is updated.""" self.profile.config['limits.memory'] = '16GB' self.profile.save() profile = self.client.profiles.get(self.profile.name) self.assertEqual('16GB', profile.config['limits.memory']) @unittest.skip('Not implemented in LXD') def test_rename(self): """A profile is renamed.""" name = 'a-other-profile' self.addCleanup(self.delete_profile, name) self.profile.rename(name) profile = self.client.profiles.get(name) self.assertEqual(name, profile.name) def test_delete(self): """A profile is deleted.""" self.profile.delete() self.assertRaises( exceptions.LXDAPIException, self.client.profiles.get, self.profile.name) pylxd-2.2.6/integration/__init__.py0000644000175000017500000000000013115536516017215 0ustar alexalex00000000000000pylxd-2.2.6/integration/test_images.py0000664000175000017500000000572413250267630020004 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import hashlib import time from pylxd import exceptions from integration.testing import create_busybox_image, IntegrationTestCase class TestImages(IntegrationTestCase): """Tests for `Client.images.`""" def test_get(self): """An image is fetched by fingerprint.""" fingerprint, _ = self.create_image() self.addCleanup(self.delete_image, fingerprint) image = self.client.images.get(fingerprint) self.assertEqual(fingerprint, image.fingerprint) def test_all(self): """A list of all images is returned.""" fingerprint, _ = self.create_image() self.addCleanup(self.delete_image, fingerprint) # XXX: rockstar (02 Jun 2016) - This seems to have a failure # of some sort. This is a hack. time.sleep(5) images = self.client.images.all() self.assertIn(fingerprint, [image.fingerprint for image in images]) def test_create(self): """An image is created.""" path, fingerprint = create_busybox_image() self.addCleanup(self.delete_image, fingerprint) with open(path, 'rb') as f: data = f.read() image = self.client.images.create(data, wait=True) self.assertEqual(fingerprint, image.fingerprint) class TestImage(IntegrationTestCase): """Tests for Image.""" def setUp(self): super(TestImage, self).setUp() fingerprint, _ = self.create_image() self.image = self.client.images.get(fingerprint) def tearDown(self): super(TestImage, self).tearDown() self.delete_image(self.image.fingerprint) def test_save(self): """The image properties are updated.""" description = 'an description' self.image.properties['description'] = description self.image.save() image = self.client.images.get(self.image.fingerprint) self.assertEqual(description, image.properties['description']) def test_delete(self): """The image is deleted.""" self.image.delete(wait=True) self.assertRaises( exceptions.LXDAPIException, self.client.images.get, self.image.fingerprint) def test_export(self): """The image is successfully exported.""" data = self.image.export().read() data_sha = hashlib.sha256(data).hexdigest() self.assertEqual(self.image.fingerprint, data_sha) pylxd-2.2.6/integration/busybox.py0000644000175000017500000001242413115536516017166 0ustar alexalex00000000000000# This code is stolen directly from lxd-images, for expediency's sake. import atexit import hashlib import io import json import os import shutil import subprocess import tarfile import tempfile import uuid def find_on_path(command): """Is command on the executable search path?""" if 'PATH' not in os.environ: return False path = os.environ['PATH'] for element in path.split(os.pathsep): if not element: continue filename = os.path.join(element, command) if os.path.isfile(filename) and os.access(filename, os.X_OK): return True return False class Busybox(object): workdir = None def __init__(self): # Create our workdir self.workdir = tempfile.mkdtemp() def cleanup(self): if self.workdir: shutil.rmtree(self.workdir) def create_tarball(self, split=False): xz = "pxz" if find_on_path("pxz") else "xz" destination_tar = os.path.join(self.workdir, "busybox.tar") target_tarball = tarfile.open(destination_tar, "w:") if split: destination_tar_rootfs = os.path.join(self.workdir, "busybox.rootfs.tar") target_tarball_rootfs = tarfile.open(destination_tar_rootfs, "w:") metadata = {'architecture': os.uname()[4], 'creation_date': int(os.stat("/bin/busybox").st_ctime), 'properties': { 'os': "Busybox", 'architecture': os.uname()[4], 'description': "Busybox %s" % os.uname()[4], 'name': "busybox-%s" % os.uname()[4], # Don't overwrite actual busybox images. 'obfuscate': str(uuid.uuid4()), }, } # Add busybox with open("/bin/busybox", "rb") as fd: busybox_file = tarfile.TarInfo() busybox_file.size = os.stat("/bin/busybox").st_size busybox_file.mode = 0o755 if split: busybox_file.name = "bin/busybox" target_tarball_rootfs.addfile(busybox_file, fd) else: busybox_file.name = "rootfs/bin/busybox" target_tarball.addfile(busybox_file, fd) # Add symlinks busybox = subprocess.Popen(["/bin/busybox", "--list-full"], stdout=subprocess.PIPE, universal_newlines=True) busybox.wait() for path in busybox.stdout.read().split("\n"): if not path.strip(): continue symlink_file = tarfile.TarInfo() symlink_file.type = tarfile.SYMTYPE symlink_file.linkname = "/bin/busybox" if split: symlink_file.name = "%s" % path.strip() target_tarball_rootfs.addfile(symlink_file) else: symlink_file.name = "rootfs/%s" % path.strip() target_tarball.addfile(symlink_file) # Add directories for path in ("dev", "mnt", "proc", "root", "sys", "tmp"): directory_file = tarfile.TarInfo() directory_file.type = tarfile.DIRTYPE if split: directory_file.name = "%s" % path target_tarball_rootfs.addfile(directory_file) else: directory_file.name = "rootfs/%s" % path target_tarball.addfile(directory_file) # Add the metadata file metadata_yaml = json.dumps(metadata, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False).encode('utf-8') + b"\n" metadata_file = tarfile.TarInfo() metadata_file.size = len(metadata_yaml) metadata_file.name = "metadata.yaml" target_tarball.addfile(metadata_file, io.BytesIO(metadata_yaml)) # Add an /etc/inittab; this is to work around: # http://lists.busybox.net/pipermail/busybox/2015-November/083618.html # Basically, since there are some hardcoded defaults that misbehave, we # just pass an empty inittab so those aren't applied, and then busybox # doesn't spin forever. inittab = tarfile.TarInfo() inittab.size = 1 inittab.name = "/rootfs/etc/inittab" target_tarball.addfile(inittab, io.BytesIO(b"\n")) target_tarball.close() if split: target_tarball_rootfs.close() # Compress the tarball r = subprocess.call([xz, "-9", destination_tar]) if r: raise Exception("Failed to compress: %s" % destination_tar) if split: r = subprocess.call([xz, "-9", destination_tar_rootfs]) if r: raise Exception("Failed to compress: %s" % destination_tar_rootfs) return destination_tar + ".xz", destination_tar_rootfs + ".xz" else: return destination_tar + ".xz" def create_busybox_image(): busybox = Busybox() atexit.register(busybox.cleanup) path = busybox.create_tarball() with open(path, "rb") as fd: fingerprint = hashlib.sha256(fd.read()).hexdigest() return path, fingerprint pylxd-2.2.6/integration/testing.py0000644000175000017500000001155213115536516017151 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import unittest import uuid from pylxd import exceptions from pylxd.client import Client from integration.busybox import create_busybox_image class IntegrationTestCase(unittest.TestCase): """A base test case for pylxd integration tests.""" def setUp(self): super(IntegrationTestCase, self).setUp() self.client = Client() self.lxd = self.client.api def generate_object_name(self): """Generate a random object name.""" # Underscores are not allowed in container names. test = self.id().split('.')[-1].replace('_', '') rando = str(uuid.uuid1()).split('-')[-1] return '{}-{}'.format(test, rando) def create_container(self): """Create a container in lxd.""" fingerprint, alias = self.create_image() name = self.generate_object_name() machine = { 'name': name, 'architecture': '2', 'profiles': ['default'], 'ephemeral': False, 'config': {'limits.cpu': '2'}, 'source': {'type': 'image', 'alias': alias}, } result = self.lxd['containers'].post(json=machine) operation_uuid = result.json()['operation'].split('/')[-1] result = self.lxd.operations[operation_uuid].wait.get() self.addCleanup(self.delete_container, name) return name def delete_container(self, name, enforce=False): """Delete a container in lxd.""" # enforce is a hack. There's a race somewhere in the delete. # To ensure we don't get an infinite loop, let's count. count = 0 try: result = self.lxd['containers'][name].delete() except exceptions.LXDAPIException as e: if e.response.status_code in (400, 404): return raise while enforce and result.status_code == 404 and count < 10: try: result = self.lxd['containers'][name].delete() except exceptions.LXDAPIException as e: if e.response.status_code in (400, 404): return raise count += 1 try: operation_uuid = result.json()['operation'].split('/')[-1] result = self.lxd.operations[operation_uuid].wait.get() except KeyError: pass # 404 cases are okay. def create_image(self): """Create an image in lxd.""" path, fingerprint = create_busybox_image() with open(path, 'rb') as f: headers = { 'X-LXD-Public': '1', } response = self.lxd.images.post(data=f.read(), headers=headers) operation_uuid = response.json()['operation'].split('/')[-1] self.lxd.operations[operation_uuid].wait.get() alias = self.generate_object_name() response = self.lxd.images.aliases.post(json={ 'description': '', 'target': fingerprint, 'name': alias }) self.addCleanup(self.delete_image, fingerprint) return fingerprint, alias def delete_image(self, fingerprint): """Delete an image in lxd.""" try: self.lxd.images[fingerprint].delete() except exceptions.LXDAPIException as e: if e.response.status_code == 404: return raise def create_profile(self): """Create a profile.""" name = self.generate_object_name() config = {'limits.memory': '1GB'} self.lxd.profiles.post(json={ 'name': name, 'config': config }) return name def delete_profile(self, name): """Delete a profile.""" try: self.lxd.profiles[name].delete() except exceptions.LXDAPIException as e: if e.response.status_code == 404: return raise def assertCommon(self, response): """Assert common LXD responses. LXD responses are relatively standard. This function makes assertions to all those standards. """ self.assertEqual(response.status_code, response.json()['status_code']) self.assertEqual( ['metadata', 'operation', 'status', 'status_code', 'type'], sorted(response.json().keys())) pylxd-2.2.6/integration/test_containers.py0000664000175000017500000001262713250267630020704 0ustar alexalex00000000000000# Copyright (c) 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from pylxd import exceptions from integration.testing import IntegrationTestCase class TestContainers(IntegrationTestCase): """Tests for `Client.containers`.""" def test_get(self): """A container is fetched by name.""" name = self.create_container() self.addCleanup(self.delete_container, name) container = self.client.containers.get(name) self.assertEqual(name, container.name) def test_all(self): """A list of all containers is returned.""" name = self.create_container() self.addCleanup(self.delete_container, name) containers = self.client.containers.all() self.assertIn(name, [c.name for c in containers]) def test_create(self): """Creates and returns a new container.""" _, alias = self.create_image() config = { 'name': 'an-container', 'architecture': '2', 'profiles': ['default'], 'ephemeral': True, 'config': {'limits.cpu': '2'}, 'source': {'type': 'image', 'alias': alias}, } self.addCleanup(self.delete_container, config['name']) container = self.client.containers.create(config, wait=True) self.assertEqual(config['name'], container.name) class TestContainer(IntegrationTestCase): """Tests for Client.Container.""" def setUp(self): super(TestContainer, self).setUp() name = self.create_container() self.container = self.client.containers.get(name) def tearDown(self): super(TestContainer, self).tearDown() self.delete_container(self.container.name) def test_save(self): """The container is updated to a new config.""" self.container.config['limits.cpu'] = '1' self.container.save(wait=True) self.assertEqual('1', self.container.config['limits.cpu']) container = self.client.containers.get(self.container.name) self.assertEqual('1', container.config['limits.cpu']) def test_rename(self): """The container is renamed.""" name = 'an-renamed-container' self.container.rename(name, wait=True) self.assertEqual(name, self.container.name) container = self.client.containers.get(name) self.assertEqual(name, container.name) def test_delete(self): """The container is deleted.""" self.container.delete(wait=True) self.assertRaises( exceptions.LXDAPIException, self.client.containers.get, self.container.name) def test_start_stop(self): """The container is started and then stopped.""" # NOTE: rockstar (15 Feb 2016) - I don't care for the # multiple assertions here, but it's a okay-ish way # to test what we need. self.container.start(wait=True) self.assertEqual('Running', self.container.status) container = self.client.containers.get(self.container.name) self.assertEqual('Running', container.status) self.container.stop(wait=True) self.assertEqual('Stopped', self.container.status) container = self.client.containers.get(self.container.name) self.assertEqual('Stopped', container.status) def test_snapshot(self): """A container snapshot is made, renamed, and deleted.""" # NOTE: rockstar (15 Feb 2016) - Once again, multiple things # asserted in the same test. name = 'an-snapshot' snapshot = self.container.snapshots.create(name, wait=True) self.assertEqual( [name], [s.name for s in self.container.snapshots.all()]) new_name = 'an-other-snapshot' snapshot.rename(new_name, wait=True) self.assertEqual( [new_name], [s.name for s in self.container.snapshots.all()]) snapshot.delete(wait=True) self.assertEqual([], self.container.snapshots.all()) def test_put_get_file(self): """A file is written to the container and then read.""" filepath = '/tmp/an_file' data = b'abcdef' retval = self.container.files.put(filepath, data) self.assertTrue(retval) contents = self.container.files.get(filepath) self.assertEqual(data, contents) def test_execute(self): """A command is executed on the container.""" self.container.start(wait=True) self.addCleanup(self.container.stop, wait=True) result = self.container.execute(['echo', 'test']) self.assertEqual(0, result.exit_code) self.assertEqual('test\n', result.stdout) self.assertEqual('', result.stderr) def test_publish(self): """A container is published.""" image = self.container.publish(wait=True) self.assertIn( image.fingerprint, [i.fingerprint for i in self.client.images.all()]) pylxd-2.2.6/.mailmap0000644000175000017500000000013013115536516014206 0ustar alexalex00000000000000# Format is: # # pylxd-2.2.6/tox.ini0000644000175000017500000000115713115536516014112 0ustar alexalex00000000000000[tox] minversion = 1.6 envlist = py3,py27,pypy,pep8 skipsdist = True [testenv] usedevelop = True install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = nosetests --with-coverage --cover-package=pylxd pylxd [testenv:pep8] commands = flake8 --ignore=E123,E125 [testenv:integration] commands = nosetests integration [flake8] # E123, E125 skipped as they are invalid PEP-8. show-source = True ignore = E123,E125 builtins = _ exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build pylxd-2.2.6/blacklist0000644000175000017500000000001613115536516014463 0ustar alexalex00000000000000integration.* pylxd-2.2.6/.coveragerc0000644000175000017500000000022213115536516014710 0ustar alexalex00000000000000[run] branch = True source = pylxd [report] omit = pylxd/tests/* pylxd/deprecated/* exclude_lines = def __str__ pragma: no cover pylxd-2.2.6/setup.py0000644000175000017500000000221513115536516014305 0ustar alexalex00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools # In python < 2.7.4, a lazy loading of package `pbr` will break # setuptools if some other modules registered functions in `atexit`. # solution from: http://bugs.python.org/issue15881#msg170215 try: import multiprocessing # noqa except ImportError: pass setuptools.setup( setup_requires=[ 'pbr>=1.8', 'requests!=2.8.0,>=2.5.2', # >= 0.1.5 needed for HTTP_PROXY support 'requests-unixsocket>=0.1.5', ], pbr=True) pylxd-2.2.6/LICENSE0000644000175000017500000002363613115536516013612 0ustar alexalex00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. pylxd-2.2.6/requirements.txt0000664000175000017500000000041113250267630016053 0ustar alexalex00000000000000pbr>=1.6 python-dateutil>=2.4.2 six>=1.9.0 ws4py!=0.3.5,>=0.3.4 # 0.3.5 is broken for websocket support requests!=2.8.0,!=2.12.0,!=2.12.1,>=2.5.2 requests-unixsocket>=0.1.5 requests-toolbelt>=0.8.0 cryptography!=1.3.0,>=1.0 pyOpenSSL>=0.14;python_version<='2.7.8' pylxd-2.2.6/CONTRIBUTORS.rst0000664000175000017500000000206113250267630015261 0ustar alexalex00000000000000Contributors to pylxd ~~~~~~~~~~~~~~~~~~~~~ These are the contributors to pylxd according to the Github repository. =============== ================================== GHsername Name =============== ================================== rockstar Paul Hummer zulcss Chuck Short saviq Michał Sawicz javacruft James Page (Canonical) pcdummy Rene Jochum jpic ??? hsoft Virgil Dupras mgwilliams Matthew Williams tych0 Tycho Andersen (Canonical) aarnaud Anthony Arnaud moreati Alex Willmer uglide Igor Malinovskiy Itxaka ??? ivuk Igor Vuk reversecipher ??? stgraber Stéphane Graber (Canonical) toshism Tosh Lyons om26er Omer Akram sergiusens Sergio Schvezov datashaman ??? rooty0 ??? jimmymccrory Jimmy McCrory Synforge Paul Oyston overquota ??? chrismacnaughton Chris MacNaughton =============== ================================== pylxd-2.2.6/setup.cfg0000664000175000017500000000232113250466213014410 0ustar alexalex00000000000000[metadata] name = pylxd summary = python library for lxd version = 2.2.6 description-file = README.rst author = Paul Hummer and others (see CONTRIBUTORS.rst) author-email = lxc-devel@lists.linuxcontainers.org home-page = http://www.linuxcontainers.org classifier = Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 [files] packages = pylxd [build_sphinx] source-dir = doc/source build-dir = doc/build all_files = 1 [upload_sphinx] upload-dir = doc/build/html [compile_catalog] directory = pylxd/locale domain = pylxd [update_catalog] domain = pylxd output_dir = pylxd/locale input_file = pylxd/locale/pylxd.pot [extract_messages] keywords = _ gettext ngettext l_ lazy_gettext mapping_file = babel.cfg output_file = pylxd/locale/pylxd.pot [nosetests] nologcapture = 1 [egg_info] tag_build = tag_date = 0 pylxd-2.2.6/pylxd.egg-info/0000775000175000017500000000000013250466213015423 5ustar alexalex00000000000000pylxd-2.2.6/pylxd.egg-info/dependency_links.txt0000664000175000017500000000000113250466213021471 0ustar alexalex00000000000000 pylxd-2.2.6/pylxd.egg-info/PKG-INFO0000664000175000017500000000455413250466213016530 0ustar alexalex00000000000000Metadata-Version: 1.1 Name: pylxd Version: 2.2.6 Summary: python library for lxd Home-page: http://www.linuxcontainers.org Author: Paul Hummer and others (see CONTRIBUTORS.rst) Author-email: lxc-devel@lists.linuxcontainers.org License: UNKNOWN Description-Content-Type: UNKNOWN Description: pylxd ~~~~~ .. image:: http://img.shields.io/pypi/v/pylxd.svg :target: https://pypi.python.org/pypi/pylxd .. image:: https://travis-ci.org/lxc/pylxd.svg?branch=master :target: https://travis-ci.org/lxc/pylxd .. image:: https://codecov.io/github/lxc/pylxd/coverage.svg?branch=master :target: https://codecov.io/github/lxc/pylxd .. image:: https://readthedocs.org/projects/docs/badge/?version=latest :target: https://pylxd.readthedocs.io/en/latest/?badge=latest A Python library for interacting with the LXD REST API. Installation ============= ``pip install pylxd`` Bug reports =========== Bug reports can be filed on the `GitHub repository `_. Support and discussions ======================= We use the `LXC mailing-lists for developer and user discussions `_. If you prefer live discussions, some of us also hang out in `#lxcontainers `_ on irc.freenode.net. What is LXD? `LXD: Introduction `_ PyLXD API Documentation: `http://pylxd.readthedocs.io/en/latest/ `_ Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 pylxd-2.2.6/pylxd.egg-info/top_level.txt0000664000175000017500000000000613250466213020151 0ustar alexalex00000000000000pylxd pylxd-2.2.6/pylxd.egg-info/pbr.json0000664000175000017500000000005613250466213017102 0ustar alexalex00000000000000{"git_version": "7569594", "is_release": true}pylxd-2.2.6/pylxd.egg-info/requires.txt0000664000175000017500000000031013250466213020015 0ustar alexalex00000000000000pbr>=1.6 python-dateutil>=2.4.2 six>=1.9.0 ws4py!=0.3.5,>=0.3.4 requests!=2.12.0,!=2.12.1,!=2.8.0,>=2.5.2 requests-unixsocket>=0.1.5 requests-toolbelt>=0.8.0 cryptography!=1.3.0,>=1.0 pyOpenSSL>=0.14 pylxd-2.2.6/pylxd.egg-info/not-zip-safe0000664000175000017500000000000113250463762017657 0ustar alexalex00000000000000 pylxd-2.2.6/pylxd.egg-info/SOURCES.txt0000664000175000017500000000503713250466213017314 0ustar alexalex00000000000000.coveragerc .mailmap .testr.conf .travis.yml AUTHORS CONTRIBUTORS.rst ChangeLog LICENSE MANIFEST.in README.rst babel.cfg blacklist openstack-common.conf requirements.txt run_integration_tests setup.cfg setup.py test-requirements.txt tox.ini doc/source/api.rst doc/source/authentication.rst doc/source/certificates.rst doc/source/conf.py doc/source/containers.rst doc/source/contributing.rst doc/source/events.rst doc/source/images.rst doc/source/index.rst doc/source/installation.rst doc/source/networks.rst doc/source/operations.rst doc/source/profiles.rst doc/source/storage-pools.rst doc/source/usage.rst integration/__init__.py integration/busybox.py integration/test_client.py integration/test_containers.py integration/test_images.py integration/test_profiles.py integration/testing.py pylxd/__init__.py pylxd/client.py pylxd/deprecation.py pylxd/exceptions.py pylxd/managers.py pylxd.egg-info/PKG-INFO pylxd.egg-info/SOURCES.txt pylxd.egg-info/dependency_links.txt pylxd.egg-info/not-zip-safe pylxd.egg-info/pbr.json pylxd.egg-info/requires.txt pylxd.egg-info/top_level.txt pylxd/deprecated/__init__.py pylxd/deprecated/api.py pylxd/deprecated/base.py pylxd/deprecated/certificate.py pylxd/deprecated/connection.py pylxd/deprecated/container.py pylxd/deprecated/exceptions.py pylxd/deprecated/hosts.py pylxd/deprecated/image.py pylxd/deprecated/network.py pylxd/deprecated/operation.py pylxd/deprecated/profiles.py pylxd/deprecated/utils.py pylxd/deprecated/tests/__init__.py pylxd/deprecated/tests/fake_api.py pylxd/deprecated/tests/test_certificate.py pylxd/deprecated/tests/test_connection.py pylxd/deprecated/tests/test_container.py pylxd/deprecated/tests/test_host.py pylxd/deprecated/tests/test_image.py pylxd/deprecated/tests/test_image_alias.py pylxd/deprecated/tests/test_network.py pylxd/deprecated/tests/test_operation.py pylxd/deprecated/tests/test_profiles.py pylxd/deprecated/tests/utils.py pylxd/models/__init__.py pylxd/models/_model.py pylxd/models/certificate.py pylxd/models/container.py pylxd/models/image.py pylxd/models/network.py pylxd/models/operation.py pylxd/models/profile.py pylxd/models/storage_pool.py pylxd/tests/__init__.py pylxd/tests/lxd.crt pylxd/tests/lxd.key pylxd/tests/mock_lxd.py pylxd/tests/test_client.py pylxd/tests/testing.py pylxd/tests/models/__init__.py pylxd/tests/models/test_certificate.py pylxd/tests/models/test_container.py pylxd/tests/models/test_image.py pylxd/tests/models/test_model.py pylxd/tests/models/test_network.py pylxd/tests/models/test_operation.py pylxd/tests/models/test_profile.py pylxd/tests/models/test_storage.pypylxd-2.2.6/babel.cfg0000644000175000017500000000002013115536516014311 0ustar alexalex00000000000000[python: **.py]