pylxd-2.0.7/0000775000175000017500000000000013250466546012601 5ustar alexalex00000000000000pylxd-2.0.7/openstack-common.conf0000644000175000017500000000020313115536516016711 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.0.7/AUTHORS0000644000175000017500000000217313250466546013652 0ustar alexalex00000000000000Alex Kahan Alex Kavanagh Alex Willmer Anthony ARNAUD Anthony ARNAUD Chris MacNaughton Chuck Short Chuck Short Igor Malinovskiy Igor Malinovskiy Igor Vuk Itxaka James Page Jimmy McCrory Matthew Williams Michał Sawicz Paul Hummer Paul Hummer Paul Hummer Rene Jochum Sergio Schvezov Stéphane Graber Stéphane Graber Thomas Goirand Tycho Andersen Virgil Dupras datashaman halja jpic reversecipher zulcss pylxd-2.0.7/pylxd/0000775000175000017500000000000013250466546013741 5ustar alexalex00000000000000pylxd-2.0.7/pylxd/container.py0000664000175000017500000003574513250466311016301 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 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, model from pylxd.deprecation import deprecated from pylxd.operation import Operation 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) class Container(model.Model): """An LXD Container. This class is not intended to be used directly, but rather to be used via `Client.containers.create`. """ architecture = model.Attribute() config = model.Attribute() created_at = model.Attribute() devices = model.Attribute() ephemeral = model.Attribute() expanded_config = model.Attribute() expanded_devices = model.Attribute() name = model.Attribute(readonly=True) profiles = model.Attribute() status = model.Attribute(readonly=True) last_used_at = model.Attribute(readonly=True) status_code = model.Attribute(readonly=True) stateful = model.Attribute(readonly=True) snapshots = model.Manager() files = model.Manager() @property def api(self): return self.client.api.containers[self.name] class FilesManager(object): """A pseudo-manager for namespacing file operations.""" def __init__(self, client, container): self._client = client self._container = container def put(self, filepath, data): response = self._client.api.containers[ self._container.name].files.post( params={'path': filepath}, data=data) return response.status_code == 200 def get(self, filepath): response = self._client.api.containers[ self._container.name].files.get( params={'path': filepath}) return response.content @classmethod def get(cls, client, name): """Get a container by name.""" response = client.api.containers[name].get() container = cls(client, **response.json()['metadata']) return container @classmethod def all(cls, client): """Get all containers. Containers returned from this method will only have the name set, as that is the only property returned from LXD. If more information is needed, `Container.sync` is the method call that should be used. """ response = client.api.containers.get() containers = [] for url in response.json()['metadata']: name = url.split('/')[-1] containers.append(cls(client, name=name)) return containers @classmethod def create(cls, client, config, wait=False): """Create a new container config.""" response = client.api.containers.post(json=config) if wait: Operation.wait_for_operation(client, response.json()['operation']) return cls(client, name=config['name']) def __init__(self, *args, **kwargs): super(Container, self).__init__(*args, **kwargs) self.snapshots = managers.SnapshotManager(self.client, self) self.files = self.FilesManager(self.client, self) # XXX: rockstar (28 Mar 2016) - This method was named improperly # originally. It's being kept here for backwards compatibility. reload = deprecated( "Container.reload is deprecated. Please use Container.sync")( model.Model.sync) def rename(self, name, wait=False): """Rename a container.""" response = self.api.post(json={'name': name}) if wait: Operation.wait_for_operation( self.client, response.json()['operation']) self.name = name def _set_state(self, state, timeout=30, force=True, wait=False): response = self.api.state.put(json={ 'action': state, 'timeout': timeout, 'force': force }) if wait: Operation.wait_for_operation( self.client, response.json()['operation']) 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) @deprecated('Container.snapshot is deprecated. Please use Container.snapshots.create') # NOQA def snapshot(self, name, stateful=False, wait=False): # pragma: no cover """Take a snapshot of the container.""" self.snapshots.create(name, stateful, wait) @deprecated('Container.list_snapshots is deprecated. Please use Container.snapshots.all') # NOQA def list_snapshots(self): # pragma: no cover """List all container snapshots.""" return [s.name for s in self.snapshots.all()] @deprecated('Container.rename_snapshot is deprecated. Please use Snapshot.rename') # NOQA def rename_snapshot(self, old, new, wait=False): # pragma: no cover """Rename a snapshot.""" snapshot = self.snapshots.get(old) snapshot.rename(new, wait=wait) @deprecated('Container.delete_snapshot is deprecated. Please use Snapshot.delete') # NOQA def delete_snapshot(self, name, wait=False): # pragma: no cover """Delete a snapshot.""" snapshot = self.snapshots.get(name) snapshot.delete(wait=wait) @deprecated('Container.get_file is deprecated. Please use Container.files.get') # NOQA def get_file(self, filepath): # pragma: no cover """Get a file from the container.""" return self.files.get(filepath) @deprecated('Container.put_file is deprecated. Please use Container.files.put') # NOQA def put_file(self, filepath, data): # pragma: no cover """Put a file on the container.""" return self.files.put(filepath, data) def execute(self, commands, environment={}): """Execute a command on the container.""" if not _ws4py_installed: raise ValueError( 'This feature requires the optional ws4py library.') if isinstance(commands, six.string_types): raise TypeError("First argument must be a list.") response = self.api['exec'].post(json={ 'command': commands, 'environment': environment, 'wait-for-websocket': True, 'interactive': False, }) fds = response.json()['metadata']['metadata']['fds'] operation_id = response.json()['operation'].split('/')[-1] parsed = parse.urlparse( self.client.api.operations[operation_id].websocket._api_endpoint) manager = WebSocketManager() stdin = _StdinWebsocket(manager, 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 True: # pragma: no cover for websocket in manager.websockets.values(): if not websocket.terminated: break else: break time.sleep(1) return stdout.data, stderr.data def migrate(self, new_client, wait=False): """Migrate a container. Destination host information is contained in the client connection passed in. If the container is running, it either must be shut down first or criu must be installed on the source and destination machines. """ 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'] config = { '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, } } return new_client.containers.create(config, wait=wait) def publish(self, public=False, wait=False): """Publish a container as an image. The container must be stopped in order publish it as an image. This method does not enforce that constraint, so a LXDAPIException may be raised if this method is called on a running container. If wait=True, an Image is returned. """ data = { 'public': public, 'source': { 'type': 'container', 'name': self.name, } } response = self.client.api.images.post(json=data) if wait: operation = Operation.wait_for_operation( self.client, response.json()['operation']) return self.client.images.get(operation.metadata['fingerprint']) class _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 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.""" def __init__(self, manager, *args, **kwargs): self.manager = manager super(_StdinWebsocket, self).__init__(*args, **kwargs) def handshake_ok(self): self.manager.add(self) self.close() class Snapshot(model.Model): """A container snapshot.""" name = model.Attribute() stateful = model.Attribute() container = model.Parent() @property def api(self): return self.client.api.containers[ self.container.name].snapshots[self.name] @classmethod def get(cls, client, container, name): response = client.api.containers[ container.name].snapshots[name].get() snapshot = cls( client, container=container, **response.json()['metadata']) # Snapshot names are namespaced in LXD, as # container-name/snapshot-name. We hide that implementation # detail. snapshot.name = snapshot.name.split('/')[-1] return snapshot @classmethod def all(cls, client, container): response = client.api.containers[container.name].snapshots.get() return [cls( client, name=snapshot.split('/')[-1], container=container) for snapshot in response.json()['metadata']] @classmethod def create(cls, client, container, name, stateful=False, wait=False): response = client.api.containers[container.name].snapshots.post(json={ 'name': name, 'stateful': stateful}) snapshot = cls(client, container=container, name=name) if wait: Operation.wait_for_operation(client, response.json()['operation']) return snapshot def rename(self, new_name, wait=False): """Rename a snapshot.""" response = self.api.post(json={'name': new_name}) if wait: Operation.wait_for_operation( self.client, response.json()['operation']) self.name = new_name def publish(self, public=False, wait=False): """Publish a snapshot as an image. If wait=True, an Image is returned. This functionality is currently broken in LXD. Please see https://github.com/lxc/lxd/issues/2201 - The implementation here is mostly a guess. Once that bug is fixed, we can verify that this works, or file a bug to fix it appropriately. """ data = { 'public': public, 'source': { 'type': 'snapshot', 'name': '{}/{}'.format(self.container.name, self.name), } } response = self.client.api.images.post(json=data) if wait: operation = Operation.wait_for_operation( self.client, response.json()['operation']) return self.client.images.get(operation.metadata['fingerprint']) pylxd-2.0.7/pylxd/client.py0000664000175000017500000002111313250466311015555 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 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() class _APINode(object): """An api node object.""" def __init__(self, api_endpoint, cert=None, verify=True): self._api_endpoint = api_endpoint 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): return self.__class__( '{}/{}'.format(self._api_endpoint, name), cert=self.session.cert, verify=self.session.verify) def __getitem__(self, item): return self.__class__( '{}/{}'.format(self._api_endpoint, item), cert=self.session.cert, verify=self.session.verify) def _assert_response(self, response, allowed_status_codes=(200,)): """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: raise exceptions.LXDAPIException(response) 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) def get(self, *args, **kwargs): """Perform an HTTP GET.""" response = self.session.get(self._api_endpoint, *args, **kwargs) self._assert_response(response) return response def post(self, *args, **kwargs): """Perform an HTTP POST.""" 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.""" 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.""" 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): 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=''))) else: self.api = _APINode(endpoint, cert=cert, verify=verify) else: if 'LXD_DIR' in os.environ: path = os.path.join( os.environ.get('LXD_DIR'), 'unix.socket') else: path = '/var/lib/lxd/unix.socket' self.api = _APINode('http+unix://{}'.format( parse.quote(path, safe=''))) 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) @property def trusted(self): return self.host_info['auth'] == 'trusted' def authenticate(self, password): if self.trusted: return cert = open(self.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): parsed = parse.urlparse(self.api._api_endpoint) if parsed.scheme in ('http', 'https'): host = parsed.netloc if parsed.scheme == 'http': scheme = 'ws' else: scheme = 'wss' else: scheme = 'ws+unix' host = parse.unquote(parsed.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.0.7/pylxd/certificate.py0000664000175000017500000000470013250466311016564 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 import 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.0.7/pylxd/mixin.py0000664000175000017500000000153713250466311015433 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. class Marshallable(object): def marshall(self): marshalled = {} for name in self.__slots__: if name.startswith('_'): continue marshalled[name] = getattr(self, name) return marshalled pylxd-2.0.7/pylxd/model.py0000664000175000017500000001635013250466311015406 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.deprecation import deprecated from pylxd.operation import Operation 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. """ __slots__ = ['client', '__dirty__'] def __init__(self, client, **kwargs): self.__dirty__ = [] 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__ )) del self.__dirty__[:] 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__.append(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: del self.__dirty__[:] fetch = deprecated("fetch is deprecated; please use sync")(sync) 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: Operation.wait_for_operation( self.client, response.json()['operation']) del self.__dirty__[:] update = deprecated('update is deprecated; please use save')(save) def delete(self, wait=False): """Delete an object from the server.""" response = self.api.delete() if response.json()['type'] == 'async' and wait: Operation.wait_for_operation( self.client, 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.0.7/pylxd/network.py0000664000175000017500000000321213250466311015770 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 model class Network(model.Model): """A LXD network.""" name = model.Attribute() type = model.Attribute() used_by = 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.0.7/pylxd/__init__.py0000664000175000017500000000134113250466311016037 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.deprecated import api # NOQA from pylxd.client import Client # NOQA pylxd-2.0.7/pylxd/image.py0000664000175000017500000001475613250466311015400 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 from pylxd import model from pylxd.operation import Operation 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: Operation.wait_for_operation(client, response.json()['operation']) return Operation.get(client, 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 get(cls, client, fingerprint): """Get an image.""" response = client.api.images[fingerprint].get() image = cls(client, **response.json()['metadata']) return image @classmethod def get_by_alias(cls, client, alias): """Get an image by its alias.""" response = client.api.images.aliases[alias].get() fingerprint = response.json()['metadata']['target'] return cls.get(client, fingerprint) @classmethod def all(cls, client): """Get all images.""" response = client.api.images.get() images = [] for url in response.json()['metadata']: fingerprint = url.split('/')[-1] images.append(cls(client, fingerprint=fingerprint)) return images @classmethod def create(cls, client, image_data, public=False, wait=False): """Create an image.""" fingerprint = hashlib.sha256(image_data).hexdigest() headers = {} if public: headers['X-LXD-Public'] = '1' response = client.api.images.post( data=image_data, headers=headers) if wait: Operation.wait_for_operation(client, response.json()['operation']) return cls(client, fingerprint=fingerprint) @classmethod def create_from_simplestreams(cls, client, server, alias, public=False, auto_update=False): """ Copy an image from simplestreams. """ config = { 'public': public, 'auto_update': auto_update, 'source': { 'type': 'image', 'mode': 'pull', 'server': server, 'protocol': 'simplestreams', 'fingerprint': alias } } op = _image_create_from_config(client, config, wait=True) return client.images.get(op.metadata['fingerprint']) @classmethod def create_from_url(cls, client, url, public=False, auto_update=False): """ Copy an image from an url. """ config = { 'public': public, 'auto_update': auto_update, 'source': { 'type': 'url', 'mode': 'pull', 'url': url } } op = _image_create_from_config(client, config, wait=True) return client.images.get(op.metadata['fingerprint']) def export(self): """Export the image.""" response = self.api.export.get() return response.content 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.0.7/pylxd/exceptions.py0000664000175000017500000000202713250466311016463 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 ClientConnectionFailed(Exception): """An exception raised when the Client connection fails.""" pylxd-2.0.7/pylxd/deprecation.py0000644000175000017500000000142213115536516016600 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.0.7/pylxd/managers.py0000664000175000017500000000260613250466311016102 0ustar alexalex00000000000000import 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.certificate.Certificate' class ContainerManager(BaseManager): manager_for = 'pylxd.container.Container' class ImageManager(BaseManager): manager_for = 'pylxd.image.Image' class NetworkManager(BaseManager): manager_for = 'pylxd.network.Network' class OperationManager(BaseManager): manager_for = 'pylxd.operation.Operation' class ProfileManager(BaseManager): manager_for = 'pylxd.profile.Profile' class SnapshotManager(BaseManager): manager_for = 'pylxd.container.Snapshot' pylxd-2.0.7/pylxd/tests/0000775000175000017500000000000013250466546015103 5ustar alexalex00000000000000pylxd-2.0.7/pylxd/tests/mock_lxd.py0000664000175000017500000004527113250466311017254 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': {}, }, }) RULES = [ # General service endpoints { 'text': json.dumps({ 'type': 'sync', 'metadata': {'auth': 'trusted', 'environment': { 'certificate': 'an-pem-cert', }}}), 'method': 'GET', 'url': r'^http://pylxd.test/1.0$', }, { 'text': json.dumps({ 'type': 'sync', 'metadata': {'auth': 'trusted', 'environment': {}}}), '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 }, # 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': '', '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$', }, # 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'}, }), '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.0.7/pylxd/tests/test_client.py0000664000175000017500000003027613250466311017770 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() def test_create(self): """Client creation sets default API endpoint.""" expected = 'http+unix://%2Fvar%2Flib%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(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 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') @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') @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') @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') 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.0.7/pylxd/tests/__init__.py0000644000175000017500000000000013115536516017173 0ustar alexalex00000000000000pylxd-2.0.7/pylxd/tests/test_operation.py0000664000175000017500000000477413250466311020516 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, operation from pylxd.tests import testing class TestOperation(testing.PyLXDTestCase): """Tests for pylxd.operation.Operation.""" def test_get(self): """Return an operation.""" name = 'operation-abc' an_operation = operation.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 = operation.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 = operation.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' operation.Operation.get(self.client, url) pylxd-2.0.7/pylxd/tests/test_image.py0000664000175000017500000002612713250466311017574 0ustar alexalex00000000000000import hashlib import json from pylxd import exceptions, image from pylxd.tests import testing class TestImage(testing.PyLXDTestCase): """Tests for pylxd.image.Image.""" def test_get(self): """An image is fetched.""" fingerprint = hashlib.sha256(b'').hexdigest() a_image = image.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, image.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, image.Image.get, self.client, fingerprint) def test_get_by_alias(self): fingerprint = hashlib.sha256(b'').hexdigest() a_image = image.Image.get_by_alias(self.client, 'an-alias') self.assertEqual(fingerprint, a_image.fingerprint) def test_all(self): """A list of all images is returned.""" images = image.Image.all(self.client) self.assertEqual(1, len(images)) def test_create(self): """An image is created.""" fingerprint = hashlib.sha256(b'').hexdigest() a_image = image.Image.create(self.client, b'', public=True, wait=True) self.assertIsInstance(a_image, image.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 = image.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 = image.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.""" a_image = self.client.images.all()[0] data = a_image.export() data_sha = hashlib.sha256(data).hexdigest() self.assertEqual(a_image.fingerprint, 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.0.7/pylxd/tests/test_profile.py0000664000175000017500000001056713250466311020153 0ustar alexalex00000000000000import json from pylxd import exceptions, profile from pylxd.tests import testing class TestProfile(testing.PyLXDTestCase): """Tests for pylxd.profile.Profile.""" def test_get(self): """A profile is fetched.""" name = 'an-profile' an_profile = profile.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, profile.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, profile.Profile.get, self.client, 'an-profile') def test_all(self): """A list of all profiles is returned.""" profiles = profile.Profile.all(self.client) self.assertEqual(1, len(profiles)) def test_create(self): """A new profile is created.""" an_profile = profile.Profile.create( self.client, name='an-new-profile', config={}, devices={}) self.assertIsInstance(an_profile, profile.Profile) self.assertEqual('an-new-profile', an_profile.name) def test_rename(self): """A profile is renamed.""" an_profile = profile.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 = profile.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 = profile.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 = profile.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.0.7/pylxd/tests/test_certificate.py0000664000175000017500000000422613250466311020770 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 certificate from pylxd.tests import testing class TestCertificate(testing.PyLXDTestCase): """Tests for pylxd.certificate.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 = certificate.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 = certificate.Certificate( self.client, fingerprint='an-certificate') an_certificate.delete() pylxd-2.0.7/pylxd/tests/lxd.key0000644000175000017500000000325013115536516016375 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.0.7/pylxd/tests/testing.py0000644000175000017500000000163013115536516017123 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.0.7/pylxd/tests/test_model.py0000664000175000017500000001263113250466311017605 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 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.0.7/pylxd/tests/test_container.py0000664000175000017500000003335213250466311020472 0ustar alexalex00000000000000import json import mock from pylxd import container, exceptions from pylxd.tests import testing class TestContainer(testing.PyLXDTestCase): """Tests for pylxd.container.Container.""" def test_all(self): """A list of all containers are returned.""" containers = container.Container.all(self.client) self.assertEqual(1, len(containers)) def test_get(self): """Return a container.""" name = 'an-container' an_container = container.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, container.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, container.Container.get, self.client, name) def test_create(self): """A new container is created.""" config = {'name': 'an-new-container'} an_new_container = container.Container.create( self.client, config, wait=True) self.assertEqual(config['name'], an_new_container.name) def test_fetch(self): """A sync updates the properties of a container.""" an_container = container.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 = container.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 = container.Container( self.client, name='an-missing-container') self.assertRaises(exceptions.LXDAPIException, an_container.sync) def test_update(self): """A container is updated.""" an_container = container.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 = container.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 = container.Container( self.client, name='an-container') an_container.delete(wait=True) @testing.requires_ws4py @mock.patch('pylxd.container._StdinWebsocket') @mock.patch('pylxd.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 = container.Container( self.client, name='an-container') stdout, _ = an_container.execute(['echo', 'test']) self.assertEqual('test\n', stdout) def test_execute_no_ws4py(self): """If ws4py is not installed, ValueError is raised.""" from pylxd import container old_installed = container._ws4py_installed container._ws4py_installed = False def cleanup(): container._ws4py_installed = old_installed self.addCleanup(cleanup) an_container = container.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 = container.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 = container.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) 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 = container.Container( self.client, name='an-container') image = an_container.publish(wait=True) self.assertEqual( 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', image.fingerprint) class TestContainerState(testing.PyLXDTestCase): """Tests for pylxd.container.ContainerState.""" def test_get(self): """Return a container.""" name = 'an-container' an_container = container.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 = container.Container.get(self.client, 'an-container') an_container.start(wait=True) def test_stop(self): """A container is stopped.""" an_container = container.Container.get(self.client, 'an-container') an_container.stop() def test_restart(self): """A container is restarted.""" an_container = container.Container.get(self.client, 'an-container') an_container.restart() def test_freeze(self): """A container is suspended.""" an_container = container.Container.get(self.client, 'an-container') an_container.freeze() def test_unfreeze(self): """A container is resumed.""" an_container = container.Container.get(self.client, 'an-container') an_container.unfreeze() class TestContainerSnapshots(testing.PyLXDTestCase): """Tests for pylxd.container.Container.snapshots.""" def setUp(self): super(TestContainerSnapshots, self).setUp() self.container = container.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.container.Snapshot.""" def setUp(self): super(TestSnapshot, self).setUp() self.container = container.Container.get(self.client, 'an-container') def test_rename(self): """A snapshot is renamed.""" snapshot = container.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 = container.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 = container.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 = container.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.container.Container.files.""" def setUp(self): super(TestFiles, self).setUp() self.container = container.Container.get(self.client, 'an-container') def test_put(self): """A file is put on the container.""" data = 'The quick brown fox' self.container.files.put('/tmp/putted', data) # TODO: Add an assertion here 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.0.7/pylxd/tests/test_network.py0000664000175000017500000000333113250466311020173 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 network from pylxd.tests import testing class TestNetwork(testing.PyLXDTestCase): """Tests for pylxd.network.Network.""" def test_all(self): """A list of all networks are returned.""" networks = network.Network.all(self.client) self.assertEqual(1, len(networks)) def test_get(self): """Return a container.""" name = 'lo' an_network = network.Network.get(self.client, name) self.assertEqual(name, an_network.name) def test_partial(self): """A partial network is synced.""" an_network = network.Network(self.client, name='lo') self.assertEqual('loopback', an_network.type) def test_delete(self): """delete is not implemented in networks.""" an_network = network.Network(self.client, name='lo') with self.assertRaises(NotImplementedError): an_network.delete() def test_save(self): """save is not implemented in networks.""" an_network = network.Network(self.client, name='lo') with self.assertRaises(NotImplementedError): an_network.save() pylxd-2.0.7/pylxd/tests/lxd.crt0000644000175000017500000000212713115536516016377 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.0.7/pylxd/profile.py0000664000175000017500000000363213250466311015745 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 model class Profile(model.Model): """A LXD profile.""" name = model.Attribute(readonly=True) description = model.Attribute() config = model.Attribute() devices = model.Attribute() @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.0.7/pylxd/operation.py0000664000175000017500000000453113250466311016304 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.0.7/pylxd/deprecated/0000775000175000017500000000000013250466546016041 5ustar alexalex00000000000000pylxd-2.0.7/pylxd/deprecated/container.py0000644000175000017500000002031713115536516020371 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.0.7/pylxd/deprecated/profiles.py0000644000175000017500000000441413115536516020232 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.0.7/pylxd/deprecated/certificate.py0000644000175000017500000000266513115536516020677 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.0.7/pylxd/deprecated/utils.py0000644000175000017500000000157113115536516017550 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.0.7/pylxd/deprecated/network.py0000644000175000017500000000441713115536516020103 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.0.7/pylxd/deprecated/base.py0000644000175000017500000000145213115536516017320 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.0.7/pylxd/deprecated/__init__.py0000644000175000017500000000000013115536516020131 0ustar alexalex00000000000000pylxd-2.0.7/pylxd/deprecated/api.py0000644000175000017500000002515413115536516017164 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.0.7/pylxd/deprecated/image.py0000644000175000017500000002153013115536516017467 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.0.7/pylxd/deprecated/exceptions.py0000644000175000017500000000222213115536516020563 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.0.7/pylxd/deprecated/tests/0000775000175000017500000000000013250466546017203 5ustar alexalex00000000000000pylxd-2.0.7/pylxd/deprecated/tests/test_image_alias.py0000644000175000017500000000455113115536516023045 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.0.7/pylxd/deprecated/tests/utils.py0000644000175000017500000000243013115536516020705 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.0.7/pylxd/deprecated/tests/test_profiles.py0000644000175000017500000000507713115536516022441 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.0.7/pylxd/deprecated/tests/__init__.py0000664000175000017500000000207613250466311021307 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 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.0.7/pylxd/deprecated/tests/test_operation.py0000644000175000017500000000532213115536516022607 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.0.7/pylxd/deprecated/tests/test_image.py0000644000175000017500000002247613115536516021702 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.0.7/pylxd/deprecated/tests/test_connection.py0000644000175000017500000001466513115536516022760 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.0.7/pylxd/deprecated/tests/test_certificate.py0000644000175000017500000000430113115536516023065 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.0.7/pylxd/deprecated/tests/test_host.py0000644000175000017500000000542113115536516021564 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.0.7/pylxd/deprecated/tests/fake_api.py0000644000175000017500000001357713115536516021322 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.0.7/pylxd/deprecated/tests/test_container.py0000644000175000017500000002150613115536516022573 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.0.7/pylxd/deprecated/tests/test_network.py0000644000175000017500000000375013115536516022303 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.0.7/pylxd/deprecated/hosts.py0000644000175000017500000001134113115536516017544 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.0.7/pylxd/deprecated/connection.py0000644000175000017500000001710613115536516020550 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.0.7/pylxd/deprecated/operation.py0000644000175000017500000000551313115536516020410 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.0.7/ChangeLog0000644000175000017500000003615213250466546014360 0ustar alexalex00000000000000CHANGES ======= 2.0.7 ----- * Bump version to 2.0.7 for next point release * Fix Operation class to allow unknown attributes 2.0.6 ----- * Require pyOpenSSL for older Python releases * bump version to 2.0.6 prior to merge * Fix up pep8 issues on cherry-pick 5904292b0 * Make models resilient to new attributes in LXD * Support older versions of LXD * Removing tox envs from travis cache * Fix broken CI testing 2.0.5 ----- * Bump to 2.0.5 * Make ws4py an optional library 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 * 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.0.7/MANIFEST.in0000664000175000017500000000013513250460012014314 0ustar alexalex00000000000000include AUTHORS include ChangeLog exclude .gitignore exclude .gitreview global-exclude *.pycpylxd-2.0.7/PKG-INFO0000664000175000017500000000406313250466546013701 0ustar alexalex00000000000000Metadata-Version: 1.1 Name: pylxd Version: 2.0.7 Summary: python library for lxd Home-page: http://www.linuxcontainers.org Author: Paul Hummer Author-email: paul.hummer@canonical.com 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. 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 pylxd-2.0.7/.travis.yml0000644000175000017500000000056613236353114014705 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.0.7/README.rst0000664000175000017500000000176513250466311014267 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. pylxd-2.0.7/doc/0000775000175000017500000000000013250466546013346 5ustar alexalex00000000000000pylxd-2.0.7/doc/source/0000775000175000017500000000000013250466546014646 5ustar alexalex00000000000000pylxd-2.0.7/doc/source/profiles.rst0000664000175000017500000000271213250466311017213 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 networks - `get()` - Get a specific network, 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 ------------------ - `name` - The name of the network - `type` - The type of the network - `used_by` - A list of containers using this network Profile methods --------------- - `rename` - Rename the 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.0.7/doc/source/conf.py0000775000175000017500000000456613250466311016151 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, 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.0.7/doc/source/installation.rst0000644000175000017500000000055313115536516020075 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.0.7/doc/source/containers.rst0000664000175000017500000001220013250466311017526 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: - `all()` - Retrieve all containers. - `get()` - Get a specific container, by its name. - `create(wait=False)` - Create a new container. This method requires a first argument that is the container name, followed by a config. 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` - The name of the container. This attribute serves as the primary identifier of a container. - `profiles` - A list of profiles applied to the container - `status` - A string representing the status of the container - `status_code` - A LXD status code of the container - `stateful` - 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. - `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. 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. >>> container.start() >>> container.freeze() >>> container.delete() 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). .. 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. .. 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.0.7/doc/source/contributing.rst0000644000175000017500000000232313115536516020100 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.0.7/doc/source/authentication.rst0000644000175000017500000000235313115536516020413 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.0.7/doc/source/networks.rst0000664000175000017500000000074013250466311017243 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 pylxd-2.0.7/doc/source/certificates.rst0000644000175000017500000000160113115536516020034 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.0.7/doc/source/images.rst0000664000175000017500000000621513250466311016637 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. And create through the following methods, theres 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/3207c2c67d02b3c7504c118f9af6262747103d65/doc/rest-api.md#10imagesfingerprint Image methods ------------- - `export` - Export the image. Returns binary data that is the image itself. - `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.0.7/doc/source/events.rst0000644000175000017500000000127413115536516016701 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.0.7/doc/source/index.rst0000664000175000017500000000106113250466311016473 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 contributing api Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pylxd-2.0.7/doc/source/usage.rst0000664000175000017500000000614413250466311016477 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.0.7/doc/source/api.rst0000664000175000017500000000113413250466311016136 0ustar alexalex00000000000000================= API documentation ================= Client ------ .. autoclass:: pylxd.client.Client :members: Certificate ----------- .. autoclass:: pylxd.certificate.Certificate :members: Container --------- .. autoclass:: pylxd.container.Container :members: .. autoclass:: pylxd.container.Snapshot :members: Image ----- .. autoclass:: pylxd.image.Image :members: Network ------- .. autoclass:: pylxd.network.Network :members: Operation --------- .. autoclass:: pylxd.operation.Operation :members: Profile ------- .. autoclass:: pylxd.profile.Profile :members: pylxd-2.0.7/test-requirements.txt0000644000175000017500000000034013236353114017023 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.0.7/.testr.conf0000644000175000017500000000047613115536516014667 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.0.7/integration/0000775000175000017500000000000013250466546015124 5ustar alexalex00000000000000pylxd-2.0.7/integration/test_profiles.py0000664000175000017500000000535313250466311020354 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_update(self): """A profile is updated.""" self.profile.config['limits.memory'] = '16GB' self.profile.update() 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.NotFound, self.client.profiles.get, self.profile.name) pylxd-2.0.7/integration/__init__.py0000644000175000017500000000000013115536516017214 0ustar alexalex00000000000000pylxd-2.0.7/integration/test_images.py0000664000175000017500000000571413250466311017777 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_update(self): """The image properties are updated.""" description = 'an description' self.image.properties['description'] = description self.image.update() 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.NotFound, self.client.images.get, self.image.fingerprint) def test_export(self): """The imerage is successfully exported.""" data = self.image.export() data_sha = hashlib.sha256(data).hexdigest() self.assertEqual(self.image.fingerprint, data_sha) pylxd-2.0.7/integration/busybox.py0000644000175000017500000001242413115536516017165 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.0.7/integration/testing.py0000644000175000017500000001155213115536516017150 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.0.7/integration/test_containers.py0000664000175000017500000001211613250466311020671 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_update(self): """The container is updated to a new config.""" self.container.config['limits.cpu'] = '1' self.container.update(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.NotFound, 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) stdout, stderr = self.container.execute(['echo', 'test']) self.assertEqual('test\n', stdout) pylxd-2.0.7/.mailmap0000644000175000017500000000013013115536516014205 0ustar alexalex00000000000000# Format is: # # pylxd-2.0.7/tox.ini0000644000175000017500000000115713115536516014111 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.0.7/blacklist0000644000175000017500000000001613115536516014462 0ustar alexalex00000000000000integration.* pylxd-2.0.7/.coveragerc0000644000175000017500000000022213115536516014707 0ustar alexalex00000000000000[run] branch = True source = pylxd [report] omit = pylxd/tests/* pylxd/deprecated/* exclude_lines = def __str__ pragma: no cover pylxd-2.0.7/setup.py0000644000175000017500000000221513115536516014304 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.0.7/LICENSE0000644000175000017500000002363613115536516013611 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.0.7/requirements.txt0000664000175000017500000000032613250466311016054 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.5.2 requests-unixsocket>=0.1.5 cryptography>=1.4 pyOpenSSL>=0.14;python_version<='2.7.8' pylxd-2.0.7/setup.cfg0000664000175000017500000000217613250466546014430 0ustar alexalex00000000000000[metadata] name = pylxd summary = python library for lxd version = 2.0.7 description-file = README.rst author = Paul Hummer author-email = paul.hummer@canonical.com 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 [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.0.7/pylxd.egg-info/0000775000175000017500000000000013250466546015433 5ustar alexalex00000000000000pylxd-2.0.7/pylxd.egg-info/dependency_links.txt0000664000175000017500000000000113250466546021501 0ustar alexalex00000000000000 pylxd-2.0.7/pylxd.egg-info/PKG-INFO0000664000175000017500000000406313250466546016533 0ustar alexalex00000000000000Metadata-Version: 1.1 Name: pylxd Version: 2.0.7 Summary: python library for lxd Home-page: http://www.linuxcontainers.org Author: Paul Hummer Author-email: paul.hummer@canonical.com 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. 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 pylxd-2.0.7/pylxd.egg-info/top_level.txt0000664000175000017500000000000613250466546020161 0ustar alexalex00000000000000pylxd pylxd-2.0.7/pylxd.egg-info/pbr.json0000664000175000017500000000005613250466546017112 0ustar alexalex00000000000000{"git_version": "9c06297", "is_release": true}pylxd-2.0.7/pylxd.egg-info/requires.txt0000664000175000017500000000022513250466546020032 0ustar alexalex00000000000000pbr>=1.6 python-dateutil>=2.4.2 six>=1.9.0 ws4py!=0.3.5,>=0.3.4 requests!=2.8.0,>=2.5.2 requests-unixsocket>=0.1.5 cryptography>=1.4 pyOpenSSL>=0.14 pylxd-2.0.7/pylxd.egg-info/not-zip-safe0000664000175000017500000000000113250463762017656 0ustar alexalex00000000000000 pylxd-2.0.7/pylxd.egg-info/SOURCES.txt0000664000175000017500000000433213250466546017321 0ustar alexalex00000000000000.coveragerc .mailmap .testr.conf .travis.yml AUTHORS ChangeLog LICENSE MANIFEST.in README.rst babel.cfg blacklist openstack-common.conf requirements.txt 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/profiles.rst doc/source/usage.rst integration/__init__.py integration/busybox.py integration/test_containers.py integration/test_images.py integration/test_profiles.py integration/testing.py pylxd/__init__.py pylxd/certificate.py pylxd/client.py pylxd/container.py pylxd/deprecation.py pylxd/exceptions.py pylxd/image.py pylxd/managers.py pylxd/mixin.py pylxd/model.py pylxd/network.py pylxd/operation.py pylxd/profile.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/tests/__init__.py pylxd/tests/lxd.crt pylxd/tests/lxd.key pylxd/tests/mock_lxd.py pylxd/tests/test_certificate.py pylxd/tests/test_client.py pylxd/tests/test_container.py pylxd/tests/test_image.py pylxd/tests/test_model.py pylxd/tests/test_network.py pylxd/tests/test_operation.py pylxd/tests/test_profile.py pylxd/tests/testing.pypylxd-2.0.7/babel.cfg0000644000175000017500000000002013115536516014310 0ustar alexalex00000000000000[python: **.py]