pylxd-2.0.0/0000775000175000017500000000000012703041406013064 5ustar chuckchuck00000000000000pylxd-2.0.0/HACKING.rst0000664000175000017500000000023412703040623014661 0ustar chuckchuck00000000000000pylxd Style Commandments =============================================== Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/pylxd-2.0.0/PKG-INFO0000664000175000017500000000553112703041406014165 0ustar chuckchuck00000000000000Metadata-Version: 1.1 Name: pylxd Version: 2.0.0 Summary: python library for lxd Home-page: http://www.linuxcontainers.org Author: Chuck Short Author-email: chuck.short@canonical.com License: UNKNOWN Description: # pylxd [![Build Status](https://travis-ci.org/lxc/pylxd.svg?branch=master)](https://travis-ci.org/lxc/pylxd) A Python library for interacting with the LXD REST API. ## Getting started with pylxd If you're running on Ubuntu Wily 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 and install pylxd using pip: pip install pylxd ## First steps Once you have pylxd installed, you're ready to start interacting with LXD: ```python import uuid from pylxd import api # Let's pick a random name, avoiding clashes CONTAINER_NAME = str(uuid.uuid1()) lxd = api.API() try: lxd.container_defined(CONTAINER_NAME) except Exception as e: print("Container does not exist: %s" % e) config = {'name': CONTAINER_NAME, 'source': {'type': 'none'}} lxd.container_init(config) if lxd.container_defined(CONTAINER_NAME): print("Container is running") else: print("Whoops - please report a bug!") containers = lxd.container_list() for x in containers: lxd.container_destroy(x) ``` ## Bug reports Bug reports can be filed at https://github.com/lxc/pylxd/issues/new ## Support and discussions We use the LXC mailing-lists for developer and user discussions, you can find and subscribe to those at: https://lists.linuxcontainers.org If you prefer live discussions, some of us also hang out in [#lxcontainers](http://webchat.freenode.net/?channels=#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 :: 2.6 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 pylxd-2.0.0/AUTHORS0000664000175000017500000000143412703041406014136 0ustar chuckchuck00000000000000Anthony ARNAUD Anthony ARNAUD Chuck Short Chuck Short Igor Malinovskiy Igor Malinovskiy Igor Vuk Itxaka James Page Matthew Williams Michał Sawicz Paul Hummer Paul Hummer Paul Hummer Sergio Schvezov Stéphane Graber Tycho Andersen datashaman halja reversecipher zulcss pylxd-2.0.0/ChangeLog0000664000175000017500000002117412703041406014643 0ustar chuckchuck00000000000000CHANGES ======= 2.0.0 ----- * Tag 2.0.0 * Container methods fixed for freeze, unfreeze and restart * Fix pep8 * Add host config API call * Grab the certificate of the host * Fix container migration * 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.0/requirements.txt0000664000175000017500000000015312703040623016347 0ustar chuckchuck00000000000000pbr>=1.6 python-dateutil>=2.4.2 six>=1.9.0 ws4py>=0.3.4 requests!=2.8.0,>=2.5.2 requests-unixsocket==0.1.4 pylxd-2.0.0/MANIFEST.in0000664000175000017500000000013512703040623014621 0ustar chuckchuck00000000000000include AUTHORS include ChangeLog exclude .gitignore exclude .gitreview global-exclude *.pycpylxd-2.0.0/integration/0000775000175000017500000000000012703041406015407 5ustar chuckchuck00000000000000pylxd-2.0.0/integration/test_images.py0000664000175000017500000000475712703040623020302 0ustar chuckchuck00000000000000# 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 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) 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) as f: image = self.client.images.create(f.read(), 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() self.assertRaises( NameError, self.client.images.get, self.image.fingerprint) pylxd-2.0.0/integration/test_containers.py0000664000175000017500000001173012703040623021167 0ustar chuckchuck00000000000000# 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 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.assertEqual(1, len(containers)) self.assertEqual(name, containers[0].name) def test_create(self): """Creates and returns a new container.""" config = { 'name': 'an-container', 'architecture': 2, 'profiles': ['default'], 'ephemeral': True, 'config': {'limits.cpu': '2'}, 'source': {'type': 'image', 'alias': 'busybox'}, } 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( NameError, 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['status']) container = self.client.containers.get(self.container.name) self.assertEqual('Running', container.status['status']) self.container.stop(wait=True) self.assertEqual('Stopped', self.container.status['status']) container = self.client.containers.get(self.container.name) self.assertEqual('Stopped', container.status['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' self.container.snapshot(name, wait=True) self.assertEqual([name], self.container.list_snapshots()) new_name = 'an-other-snapshot' self.container.rename_snapshot(name, new_name, wait=True) self.assertEqual([new_name], self.container.list_snapshots()) self.container.delete_snapshot(new_name, wait=True) self.assertEqual([], self.container.list_snapshots()) def test_put_get_file(self): """A file is written to the container and then read.""" filepath = '/tmp/an_file' data = 'abcdef' retval = self.container.put_file(filepath, data) self.assertTrue(retval) contents = self.container.get_file(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) self.container.execute('ls /') pylxd-2.0.0/integration/testing.py0000664000175000017500000001005612703040623017440 0ustar chuckchuck00000000000000# 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 uuid import unittest 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): # 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.""" name = self.generate_object_name() machine = { 'name': name, 'architecture': 2, 'profiles': ['default'], 'ephemeral': False, 'config': {'limits.cpu': '2'}, 'source': {'type': 'image', 'alias': 'busybox'}, } 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 result = self.lxd['containers'][name].delete() while enforce and result.status_code == 404 and count < 10: result = self.lxd['containers'][name].delete() 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.""" self.lxd.images[fingerprint].delete() def create_profile(self): 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): self.lxd.profiles[name].delete() 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.0/integration/test_profiles.py0000664000175000017500000000530312703040623020644 0ustar chuckchuck00000000000000# 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 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': '4GB'} 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( NameError, self.client.profiles.get, self.profile.name) pylxd-2.0.0/integration/busybox.py0000664000175000017500000001245212703040623017460 0ustar chuckchuck00000000000000# 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.0/integration/__init__.py0000664000175000017500000000000012703040623017506 0ustar chuckchuck00000000000000pylxd-2.0.0/doc/0000775000175000017500000000000012703041406013631 5ustar chuckchuck00000000000000pylxd-2.0.0/doc/source/0000775000175000017500000000000012703041406015131 5ustar chuckchuck00000000000000pylxd-2.0.0/doc/source/conf.py0000775000175000017500000000462412703040623016441 0ustar chuckchuck00000000000000# -*- 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', 'oslosphinx' ] # 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'2013, OpenStack Foundation' # 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'OpenStack Foundation', 'manual'), ] # Example configuration for intersphinx: refer to the Python standard library. #intersphinx_mapping = {'http://docs.python.org/': None}pylxd-2.0.0/doc/source/index.rst0000664000175000017500000000075412703040623017000 0ustar chuckchuck00000000000000.. 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 readme installation usage contributing Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pylxd-2.0.0/doc/source/installation.rst0000664000175000017500000000026712703040623020371 0ustar chuckchuck00000000000000============ Installation ============ At the command line:: $ pip install pylxd Or, if you have virtualenvwrapper installed:: $ mkvirtualenv pylxd $ pip install pylxdpylxd-2.0.0/doc/source/contributing.rst0000664000175000017500000000011212703040623020364 0ustar chuckchuck00000000000000============ Contributing ============ .. include:: ../../CONTRIBUTING.rstpylxd-2.0.0/doc/source/usage.rst0000664000175000017500000000010612703040623016764 0ustar chuckchuck00000000000000======== Usage ======== To use pylxd in a project:: import pylxdpylxd-2.0.0/doc/source/readme.rst0000664000175000017500000000003512703040623017116 0ustar chuckchuck00000000000000.. include:: ../../README.rstpylxd-2.0.0/.mailmap0000664000175000017500000000013012703040623014477 0ustar chuckchuck00000000000000# Format is: # # pylxd-2.0.0/pylxd/0000775000175000017500000000000012703041406014224 5ustar chuckchuck00000000000000pylxd-2.0.0/pylxd/operation.py0000664000175000017500000000316312703040623016601 0ustar chuckchuck00000000000000# 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 Operation(object): """A LXD operation.""" __slots__ = [ '_client', 'class', 'created_at', '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.""" if operation_id.startswith('/'): operation_id = operation_id.split('/')[-1] operation = cls.get(client, operation_id) operation.wait() @classmethod def get(cls, client, operation_id): """Get an operation.""" 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.iteritems(): setattr(self, key, value) def wait(self): """Wait for the operation to complete and return.""" self._client.api.operations[self.id].wait.get() pylxd-2.0.0/pylxd/image.py0000664000175000017500000000520012703040623015655 0ustar chuckchuck00000000000000# 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 mixin from pylxd.operation import Operation class Image(mixin.Waitable, mixin.Marshallable): """A LXD Image.""" __slots__ = [ '_client', 'aliases', 'architecture', 'created_at', 'expires_at', 'filename', 'fingerprint', 'properties', 'public', 'size', 'uploaded_at' ] @classmethod def get(cls, client, fingerprint): """Get an image.""" response = client.api.images[fingerprint].get() if response.status_code == 404: raise NameError( 'No image with fingerprint "{}"'.format(fingerprint)) image = Image(_client=client, **response.json()['metadata']) return image @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(Image(_client=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.get(client, fingerprint) def __init__(self, **kwargs): super(Image, self).__init__() for key, value in kwargs.iteritems(): setattr(self, key, value) def update(self): """Update LXD based on changes to this image.""" self._client.api.images[self.fingerprint].put( json=self.marshall()) def delete(self, wait=False): """Delete the image.""" response = self._client.api.images[self.fingerprint].delete() if wait: self.wait_for_operation(response.json()['operation']) pylxd-2.0.0/pylxd/container.py0000664000175000017500000002021712703040623016562 0ustar chuckchuck00000000000000# 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 six from pylxd import mixin from pylxd.containerState import ContainerState from pylxd.operation import Operation class Container(mixin.Waitable, mixin.Marshallable): """An LXD Container. This class is not intended to be used directly, but rather to be used via `Client.containers.create`. """ __slots__ = [ '_client', 'architecture', 'config', 'created_at', 'devices', 'ephemeral', 'expanded_config', 'expanded_devices', 'name', 'profiles', 'status' ] @classmethod def get(cls, client, name): """Get a container by name.""" response = client.api.containers[name].get() if response.status_code == 404: raise NameError('No container named "{}"'.format(name)) container = cls(_client=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.reload` 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=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(name=config['name']) def __init__(self, **kwargs): super(Container, self).__init__() for key, value in kwargs.iteritems(): setattr(self, key, value) def reload(self): """Reload the container information.""" response = self._client.api.containers[self.name].get() if response.status_code == 404: raise NameError( 'Container named "{}" has gone away'.format(self.name)) for key, value in response.json()['metadata'].iteritems(): setattr(self, key, value) def update(self, wait=False): """Update the container in lxd from local changes.""" marshalled = self.marshall() # These two properties are explicitly not allowed. del marshalled['name'] del marshalled['status'] response = self._client.api.containers[self.name].put( json=marshalled) if wait: self.wait_for_operation(response.json()['operation']) def rename(self, name, wait=False): """Rename a container.""" response = self._client.api.containers[ self.name].post(json={'name': name}) if wait: self.wait_for_operation(response.json()['operation']) self.name = name def delete(self, wait=False): """Delete the container.""" response = self._client.api.containers[self.name].delete() if wait: self.wait_for_operation(response.json()['operation']) def _set_state(self, state, timeout=30, force=True, wait=False): response = self._client.api.containers[self.name].state.put(json={ 'action': state, 'timeout': timeout, 'force': force }) if wait: self.wait_for_operation(response.json()['operation']) self.reload() def state(self): state = ContainerState() response = self._client.api.containers[self.name].state.get() for key, value in response.json()['metadata'].iteritems(): setattr(state, key, value) return state def start(self, timeout=30, force=True, wait=False): """Start the container.""" return self._set_state('start', timeout=timeout, force=force, wait=wait) def stop(self, timeout=30, force=True, wait=False): """Stop the container.""" return self._set_state('stop', timeout=timeout, force=force, wait=wait) def restart(self, timeout=30, force=True, wait=False): """Restart the container.""" return self._set_state('restart', timeout=timeout, force=force, wait=wait) def freeze(self, timeout=30, force=True, wait=False): """Freeze the container.""" return self._set_state('freeze', timeout=timeout, force=force, wait=wait) def unfreeze(self, timeout=30, force=True, wait=False): """Unfreeze the container.""" return self._set_state('unfreeze', timeout=timeout, force=force, wait=wait) def snapshot(self, name, stateful=False, wait=False): """Take a snapshot of the container.""" response = self._client.api.containers[self.name].snapshots.post(json={ 'name': name, 'stateful': stateful}) if wait: self.wait_for_operation(response.json()['operation']) def list_snapshots(self): """List all container snapshots.""" response = self._client.api.containers[self.name].snapshots.get() return [snapshot.split('/')[-1] for snapshot in response.json()['metadata']] def rename_snapshot(self, old, new, wait=False): """Rename a snapshot.""" response = self._client.api.containers[ self.name].snapshots[old].post(json={ 'name': new }) if wait: self.wait_for_operation(response.json()['operation']) def delete_snapshot(self, name, wait=False): """Delete a snapshot.""" response = self._client.api.containers[ self.name].snapshots[name].delete() if wait: self.wait_for_operation(response.json()['operation']) def get_file(self, filepath): """Get a file from the container.""" response = self._client.api.containers[self.name].files.get( params={'path': filepath}) if response.status_code == 500: # XXX: rockstar (15 Feb 2016) - This should really return a 404. # I blame LXD. :) raise IOError('Error reading "{}"'.format(filepath)) return response.content def put_file(self, filepath, data): """Put a file on the container.""" response = self._client.api.containers[self.name].files.post( params={'path': filepath}, data=data) return response.status_code == 200 def execute(self, commands, environment={}): """Execute a command on the container.""" # XXX: rockstar (15 Feb 2016) - This functionality is limited by # design, for now. It needs to grow the ability to return web sockets # and perform interactive functions. if isinstance(commands, six.string_types): commands = [commands] response = self._client.api.containers[self.name]['exec'].post(json={ 'command': commands, 'environment': environment, 'wait-for-websocket': False, 'interactive': False, }) operation_id = response.json()['operation'] self.wait_for_operation(operation_id) pylxd-2.0.0/pylxd/mixin.py0000664000175000017500000000232612703040623015725 0ustar chuckchuck00000000000000# 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 Waitable(object): def get_operation(self, operation_id): if operation_id.startswith('/'): operation_id = operation_id.split('/')[-1] return self._client.operations.get(operation_id) def wait_for_operation(self, operation_id): operation = self.get_operation(operation_id) operation.wait() return operation 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.0/pylxd/profile.py0000664000175000017500000000460112703040623016237 0ustar chuckchuck00000000000000# 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 mixin class Profile(mixin.Marshallable): """A LXD profile.""" __slots__ = [ '_client', 'config', 'devices', 'name' ] @classmethod def get(cls, client, name): """Get a profile.""" response = client.api.profiles[name].get() if response.status_code == 404: raise NameError('No profile with name "{}"'.format(name)) return cls(_client=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=client, name=name)) return profiles @classmethod def create(cls, client, name, config): """Create a profile.""" client.api.profiles.post(json={ 'name': name, 'config': config }) return cls.get(client, name) def __init__(self, **kwargs): super(Profile, self).__init__() for key, value in kwargs.iteritems(): setattr(self, key, value) def update(self): """Update the profile in LXD based on local changes.""" marshalled = self.marshall() # The name property cannot be updated. del marshalled['name'] self._client.api.profiles[self.name].put(json=marshalled) def rename(self, new): """Rename the profile.""" raise NotImplementedError( 'LXD does not currently support renaming profiles') self._client.api.profiles[self.name].post(json={'name': new}) self.name = new def delete(self): """Delete a profile.""" self._client.api.profiles[self.name].delete() pylxd-2.0.0/pylxd/containerState.py0000664000175000017500000000136212703040623017563 0ustar chuckchuck00000000000000# 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 ContainerState(): def __init__(self, **kwargs): for key, value in kwargs.iteritems(): setattr(self, key, value) pylxd-2.0.0/pylxd/client.py0000664000175000017500000001017712703040623016062 0ustar chuckchuck00000000000000# 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 functools import os import urllib import requests import requests_unixsocket from pylxd.container import Container from pylxd.image import Image from pylxd.operation import Operation from pylxd.profile import Profile requests_unixsocket.monkeypatch() class _APINode(object): """An api node object. This class allows us to dynamically create request urls by expressing them in python. For example: >>> node = _APINode('http://example.com/api') >>> node.users[1].comments.get() ...would make an HTTP GET request on http://example.com/api/users/1/comments """ def __init__(self, api_endpoint): self._api_endpoint = api_endpoint def __getattr__(self, name): return self.__class__('{}/{}'.format(self._api_endpoint, name)) def __getitem__(self, item): return self.__class__('{}/{}'.format(self._api_endpoint, item)) @property def session(self): if self._api_endpoint.startswith('http+unix://'): return requests_unixsocket.Session() else: return requests def get(self, *args, **kwargs): """Perform an HTTP GET.""" return self.session.get(self._api_endpoint, *args, **kwargs) def post(self, *args, **kwargs): """Perform an HTTP POST.""" return self.session.post(self._api_endpoint, *args, **kwargs) def put(self, *args, **kwargs): """Perform an HTTP PUT.""" return self.session.put(self._api_endpoint, *args, **kwargs) def delete(self, *args, **kwargs): """Perform an HTTP delete.""" return self.session.delete(self._api_endpoint, *args, **kwargs) class Client(object): """A LXD client. This client wraps all the functionality required to interact with LXD, and is meant to be the sole entry point. """ class Containers(object): """A convenience wrapper for Container.""" def __init__(self, client): self.get = functools.partial(Container.get, client) self.all = functools.partial(Container.all, client) self.create = functools.partial(Container.create, client) class Images(object): """A convenience wrapper for Image.""" def __init__(self, client): self.get = functools.partial(Image.get, client) self.all = functools.partial(Image.all, client) self.create = functools.partial(Image.create, client) class Operations(object): """A convenience wrapper for Operation.""" def __init__(self, client): self.get = functools.partial(Operation.get, client) class Profiles(object): """A convenience wrapper for Profile.""" def __init__(self, client): self.get = functools.partial(Profile.get, client) self.all = functools.partial(Profile.all, client) self.create = functools.partial(Profile.create, client) def __init__(self, endpoint=None, version='1.0'): if endpoint is not None: self.api = _APINode(endpoint) 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( urllib.quote(path, safe=''))) self.api = self.api[version] self.containers = self.Containers(self) self.images = self.Images(self) self.operations = self.Operations(self) self.profiles = self.Profiles(self) pylxd-2.0.0/pylxd/__init__.py0000664000175000017500000000127112703040623016336 0ustar chuckchuck00000000000000# -*- 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 pylxd-2.0.0/pylxd/deprecated/0000775000175000017500000000000012703041406016324 5ustar chuckchuck00000000000000pylxd-2.0.0/pylxd/deprecated/exceptions.py0000664000175000017500000000222212703040623021055 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/0000775000175000017500000000000012703041406017466 5ustar chuckchuck00000000000000pylxd-2.0.0/pylxd/deprecated/tests/test_network.py0000664000175000017500000000375012703040623022575 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/test_container.py0000664000175000017500000002150612703040623023065 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/test_host.py0000664000175000017500000000542112703040623022056 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/fake_api.py0000664000175000017500000001357712703040623021614 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/test_profiles.py0000664000175000017500000000507712703040623022733 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/test_connection.py0000664000175000017500000001467012703040623023246 0ustar chuckchuck00000000000000# 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.PY34: 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.PY34: 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.PY34: 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.0/pylxd/deprecated/tests/test_operation.py0000664000175000017500000000532212703040623023101 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/test_image.py0000664000175000017500000002247612703040623022174 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/test_client.py0000664000175000017500000000546512703040623022367 0ustar chuckchuck00000000000000# 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. """Tests for pylxd.client.""" import unittest import mock from pylxd import client class Test_APINode(unittest.TestCase): """Tests for pylxd.client._APINode.""" ROOT = 'http://lxd/api' def test_getattr(self): """`__getattr__` returns a nested node.""" lxd = client._APINode(self.ROOT) self.assertEqual('{}/fake'.format(self.ROOT), lxd.fake._api_endpoint) def test_getattr_nested(self): """Nested objects return a more detailed path.""" lxd = client._APINode(self.ROOT) # NOQA self.assertEqual( '{}/fake/path'.format(self.ROOT), lxd.fake.path._api_endpoint) def test_getitem(self): """`__getitem__` enables dynamic url parts.""" lxd = client._APINode(self.ROOT) # NOQA self.assertEqual( '{}/fake/path'.format(self.ROOT), lxd.fake['path']._api_endpoint) def test_getitem_integer(self): """`__getitem__` with an integer allows dynamic integer url parts.""" lxd = client._APINode(self.ROOT) # NOQA self.assertEqual( '{}/fake/0'.format(self.ROOT), lxd.fake[0]._api_endpoint) @mock.patch('pylxd.client.requests.get') def test_get(self, _get): """`get` will make a request to the smart url.""" lxd = client._APINode(self.ROOT) lxd.fake.get() _get.assert_called_once_with('{}/{}'.format(self.ROOT, 'fake')) @mock.patch('pylxd.client.requests.post') def test_post(self, _post): """`post` will POST to the smart url.""" lxd = client._APINode(self.ROOT) lxd.fake.post() _post.assert_called_once_with('{}/{}'.format(self.ROOT, 'fake')) @mock.patch('pylxd.client.requests.put') def test_put(self, _put): """`put` will PUT to the smart url.""" lxd = client._APINode(self.ROOT) lxd.fake.put() _put.assert_called_once_with('{}/{}'.format(self.ROOT, 'fake')) @mock.patch('pylxd.client.requests.delete') def test_delete(self, _delete): """`delete` will DELETE to the smart url.""" lxd = client._APINode(self.ROOT) lxd.fake.delete() _delete.assert_called_once_with('{}/{}'.format(self.ROOT, 'fake')) pylxd-2.0.0/pylxd/deprecated/tests/__init__.py0000664000175000017500000000207612703040623021604 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/test_image_alias.py0000664000175000017500000000455112703040623023337 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/utils.py0000664000175000017500000000243012703040623021177 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/tests/test_certificate.py0000664000175000017500000000430112703040623023357 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/operation.py0000664000175000017500000000551312703040623020702 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/image.py0000664000175000017500000002153012703040623017761 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/api.py0000664000175000017500000002515412703040623017456 0ustar chuckchuck00000000000000 # 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.0/pylxd/deprecated/base.py0000664000175000017500000000145212703040623017612 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/container.py0000664000175000017500000002031712703040623020663 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/certificate.py0000664000175000017500000000266512703040623021171 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/network.py0000664000175000017500000000441712703040623020375 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/__init__.py0000664000175000017500000000000012703040623020423 0ustar chuckchuck00000000000000pylxd-2.0.0/pylxd/deprecated/connection.py0000664000175000017500000001642712703040623021047 0ustar chuckchuck00000000000000# 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 from ws4py import client as websocket from pylxd.deprecated import exceptions from pylxd.deprecated import utils if hasattr(ssl, 'SSLContext'): # For Python >= 2.7.9 and Python 3.x USE_STDLIB_SSL = True else: # For Python 2.6 and <= 2.7.8 USE_STDLIB_SSL = False if not USE_STDLIB_SSL: import OpenSSL.SSL # Detect SSL tls version if hasattr(ssl, 'PROTOCOL_TLSv1_2'): DEFAULT_TLS_VERSION = ssl.PROTOCOL_TLSv1_2 else: DEFAULT_TLS_VERSION = OpenSSL.SSL.TLSv1_2_METHOD class UnixHTTPConnection(http_client.HTTPConnection): def __init__(self, path, host='localhost', port=None, strict=None, timeout=None): if six.PY34: 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']) 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.PY34: 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 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.0/pylxd/deprecated/profiles.py0000664000175000017500000000441412703040623020524 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/utils.py0000664000175000017500000000157112703040623020042 0ustar chuckchuck00000000000000# 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.0/pylxd/deprecated/hosts.py0000664000175000017500000001134112703040623020036 0ustar chuckchuck00000000000000# 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.0/babel.cfg0000664000175000017500000000002012703040623014602 0ustar chuckchuck00000000000000[python: **.py] pylxd-2.0.0/openstack-common.conf0000664000175000017500000000020312703040623017203 0ustar chuckchuck00000000000000[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.0/setup.cfg0000664000175000017500000000227112703041406014707 0ustar chuckchuck00000000000000[metadata] name = pylxd summary = python library for lxd version = 2.0.0 description-file = README.md author = Chuck Short author-email = chuck.short@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 :: 2.6 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 tag_svn_revision = 0 pylxd-2.0.0/examples/0000775000175000017500000000000012703041406014702 5ustar chuckchuck00000000000000pylxd-2.0.0/examples/api_test.py0000775000175000017500000000221312703040623017065 0ustar chuckchuck00000000000000#!/usr/bin/python # 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 uuid from pylxd import api # Let's pick a random name, avoiding clashes CONTAINER_NAME = str(uuid.uuid1()) lxd = api.API() try: lxd.container_defined(CONTAINER_NAME) except Exception as e: print("Container doesnt exist: %s" % e) config = {'name': CONTAINER_NAME, 'source': {'type': 'none'}} lxd.container_init(config) if lxd.container_defined(CONTAINER_NAME): print("Container is running") else: print("Whoops!") containers = lxd.container_list() for x in containers: lxd.container_destroy(x) pylxd-2.0.0/setup.py0000664000175000017500000000213412703040623014576 0ustar chuckchuck00000000000000# 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', 'requests-unixsocket==0.1.4', ], pbr=True) pylxd-2.0.0/test-requirements.txt0000664000175000017500000000006112703040623017322 0ustar chuckchuck00000000000000ddt>=0.7.0 nose>=1.3.7 mock>=1.3.0 flake8>=2.5.0 pylxd-2.0.0/README.md0000664000175000017500000000306312703040623014345 0ustar chuckchuck00000000000000# pylxd [![Build Status](https://travis-ci.org/lxc/pylxd.svg?branch=master)](https://travis-ci.org/lxc/pylxd) A Python library for interacting with the LXD REST API. ## Getting started with pylxd If you're running on Ubuntu Wily 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 and install pylxd using pip: pip install pylxd ## First steps Once you have pylxd installed, you're ready to start interacting with LXD: ```python import uuid from pylxd import api # Let's pick a random name, avoiding clashes CONTAINER_NAME = str(uuid.uuid1()) lxd = api.API() try: lxd.container_defined(CONTAINER_NAME) except Exception as e: print("Container does not exist: %s" % e) config = {'name': CONTAINER_NAME, 'source': {'type': 'none'}} lxd.container_init(config) if lxd.container_defined(CONTAINER_NAME): print("Container is running") else: print("Whoops - please report a bug!") containers = lxd.container_list() for x in containers: lxd.container_destroy(x) ``` ## Bug reports Bug reports can be filed at https://github.com/lxc/pylxd/issues/new ## Support and discussions We use the LXC mailing-lists for developer and user discussions, you can find and subscribe to those at: https://lists.linuxcontainers.org If you prefer live discussions, some of us also hang out in [#lxcontainers](http://webchat.freenode.net/?channels=#lxcontainers) on irc.freenode.net. pylxd-2.0.0/blacklist0000664000175000017500000000001612703040623014754 0ustar chuckchuck00000000000000integration.* pylxd-2.0.0/.testr.conf0000664000175000017500000000047612703040623015161 0ustar chuckchuck00000000000000[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.0/tox.ini0000664000175000017500000000106712703040623014403 0ustar chuckchuck00000000000000[tox] minversion = 1.6 envlist = py34,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 pylxd [testenv:pep8] commands = flake8 [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.0/LICENSE0000664000175000017500000002363612703040623014103 0ustar chuckchuck00000000000000 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.0/pylxd.egg-info/0000775000175000017500000000000012703041406015716 5ustar chuckchuck00000000000000pylxd-2.0.0/pylxd.egg-info/top_level.txt0000664000175000017500000000000612703041406020444 0ustar chuckchuck00000000000000pylxd pylxd-2.0.0/pylxd.egg-info/SOURCES.txt0000664000175000017500000000333212703041406017603 0ustar chuckchuck00000000000000.coveragerc .mailmap .testr.conf .travis.yml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE MANIFEST.in README.md babel.cfg blacklist openstack-common.conf requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/source/conf.py doc/source/contributing.rst doc/source/index.rst doc/source/installation.rst doc/source/readme.rst doc/source/usage.rst examples/api_test.py 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/client.py pylxd/container.py pylxd/containerState.py pylxd/image.py pylxd/mixin.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_client.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.pypylxd-2.0.0/pylxd.egg-info/PKG-INFO0000664000175000017500000000553112703041406017017 0ustar chuckchuck00000000000000Metadata-Version: 1.1 Name: pylxd Version: 2.0.0 Summary: python library for lxd Home-page: http://www.linuxcontainers.org Author: Chuck Short Author-email: chuck.short@canonical.com License: UNKNOWN Description: # pylxd [![Build Status](https://travis-ci.org/lxc/pylxd.svg?branch=master)](https://travis-ci.org/lxc/pylxd) A Python library for interacting with the LXD REST API. ## Getting started with pylxd If you're running on Ubuntu Wily 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 and install pylxd using pip: pip install pylxd ## First steps Once you have pylxd installed, you're ready to start interacting with LXD: ```python import uuid from pylxd import api # Let's pick a random name, avoiding clashes CONTAINER_NAME = str(uuid.uuid1()) lxd = api.API() try: lxd.container_defined(CONTAINER_NAME) except Exception as e: print("Container does not exist: %s" % e) config = {'name': CONTAINER_NAME, 'source': {'type': 'none'}} lxd.container_init(config) if lxd.container_defined(CONTAINER_NAME): print("Container is running") else: print("Whoops - please report a bug!") containers = lxd.container_list() for x in containers: lxd.container_destroy(x) ``` ## Bug reports Bug reports can be filed at https://github.com/lxc/pylxd/issues/new ## Support and discussions We use the LXC mailing-lists for developer and user discussions, you can find and subscribe to those at: https://lists.linuxcontainers.org If you prefer live discussions, some of us also hang out in [#lxcontainers](http://webchat.freenode.net/?channels=#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 :: 2.6 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 pylxd-2.0.0/pylxd.egg-info/requires.txt0000664000175000017500000000015312703041406020315 0ustar chuckchuck00000000000000pbr>=1.6 python-dateutil>=2.4.2 six>=1.9.0 ws4py>=0.3.4 requests!=2.8.0,>=2.5.2 requests-unixsocket==0.1.4 pylxd-2.0.0/pylxd.egg-info/not-zip-safe0000664000175000017500000000000112703041400020136 0ustar chuckchuck00000000000000 pylxd-2.0.0/pylxd.egg-info/dependency_links.txt0000664000175000017500000000000112703041406021764 0ustar chuckchuck00000000000000 pylxd-2.0.0/pylxd.egg-info/pbr.json0000664000175000017500000000005612703041406017375 0ustar chuckchuck00000000000000{"is_release": true, "git_version": "02374c3"}pylxd-2.0.0/CONTRIBUTING.rst0000664000175000017500000000012712703040623015525 0ustar chuckchuck00000000000000Bugs and pull requests should be submitted on Github: https://github.com/lxc/pylxd pylxd-2.0.0/.coveragerc0000664000175000017500000000012712703040623015205 0ustar chuckchuck00000000000000[run] branch = True source = pylxd [report] ignore-errors = True omit = pylxd/tests/* pylxd-2.0.0/.travis.yml0000664000175000017500000000025412703040623015176 0ustar chuckchuck00000000000000language: python python: - "2.7" - "3.4" install: pip install -U -r requirements.txt -r test-requirements.txt script: - nosetests pylxd - flake8 --ignore=E123,E125