python-cloudservers-1.0a5/000755 000765 000765 00000000000 11337017727 015060 5ustar00jacob000000 000000 python-cloudservers-1.0a5/cloudservers/000755 000765 000765 00000000000 11337017727 017600 5ustar00jacob000000 000000 python-cloudservers-1.0a5/._distribute_setup.py000644 000765 000024 00000000270 11273361413 022245 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRe  com.macromates.caretxR<[k0?'3/«python-cloudservers-1.0a5/distribute_setup.py000644 000765 000765 00000034426 11273361413 021032 0ustar00jacob000000 000000 #!python """Bootstrap distribute installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from distribute_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import os import sys import time import fnmatch import tempfile import tarfile from distutils import log try: from site import USER_SITE except ImportError: USER_SITE = None try: import subprocess def _python_cmd(*args): args = (sys.executable,) + args return subprocess.call(args) == 0 except ImportError: # will be used for python 2.3 def _python_cmd(*args): args = (sys.executable,) + args # quoting arguments if windows if sys.platform == 'win32': def quote(arg): if ' ' in arg: return '"%s"' % arg return arg args = [quote(arg) for arg in args] return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 DEFAULT_VERSION = "0.6.8" DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" SETUPTOOLS_PKG_INFO = """\ Metadata-Version: 1.0 Name: setuptools Version: 0.6c9 Summary: xxxx Home-page: xxx Author: xxx Author-email: xxx License: xxx Description: xxx """ def _install(tarball): # extracting the tarball tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) tar = tarfile.open(tarball) _extractall(tar) tar.close() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) # installing log.warn('Installing Distribute') assert _python_cmd('setup.py', 'install') finally: os.chdir(old_wd) def _build_egg(egg, tarball, to_dir): # extracting the tarball tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) tar = tarfile.open(tarball) _extractall(tar) tar.close() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) # building an egg log.warn('Building a Distribute egg in %s', to_dir) _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) finally: os.chdir(old_wd) # returning the result log.warn(egg) if not os.path.exists(egg): raise IOError('Could not build the egg.') def _do_download(version, download_base, to_dir, download_delay): egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' % (version, sys.version_info[0], sys.version_info[1])) if not os.path.exists(egg): tarball = download_setuptools(version, download_base, to_dir, download_delay) _build_egg(egg, tarball, to_dir) sys.path.insert(0, egg) import setuptools setuptools.bootstrap_install_from = egg def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15, no_fake=True): # making sure we use the absolute path to_dir = os.path.abspath(to_dir) was_imported = 'pkg_resources' in sys.modules or \ 'setuptools' in sys.modules try: try: import pkg_resources if not hasattr(pkg_resources, '_distribute'): if not no_fake: _fake_setuptools() raise ImportError except ImportError: return _do_download(version, download_base, to_dir, download_delay) try: pkg_resources.require("distribute>="+version) return except pkg_resources.VersionConflict: e = sys.exc_info()[1] if was_imported: sys.stderr.write( "The required version of distribute (>=%s) is not available,\n" "and can't be installed while this script is running. Please\n" "install a more recent version first, using\n" "'easy_install -U distribute'." "\n\n(Currently using %r)\n" % (version, e.args[0])) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return _do_download(version, download_base, to_dir, download_delay) except pkg_resources.DistributionNotFound: return _do_download(version, download_base, to_dir, download_delay) finally: if not no_fake: _create_fake_setuptools_pkg_info(to_dir) def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay=15): """Download distribute from a specified location and return its filename `version` should be a valid distribute version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ # making sure we use the absolute path to_dir = os.path.abspath(to_dir) try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen tgz_name = "distribute-%s.tar.gz" % version url = download_base + tgz_name saveto = os.path.join(to_dir, tgz_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: log.warn("Downloading %s", url) src = urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = src.read() dst = open(saveto, "wb") dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def _patch_file(path, content): """Will backup the file then patch it""" existing_content = open(path).read() if existing_content == content: # already patched log.warn('Already patched.') return False log.warn('Patching...') _rename_path(path) f = open(path, 'w') try: f.write(content) finally: f.close() return True def _same_content(path, content): return open(path).read() == content def _rename_path(path): new_name = path + '.OLD.%s' % time.time() log.warn('Renaming %s into %s', path, new_name) try: from setuptools.sandbox import DirectorySandbox def _violation(*args): pass DirectorySandbox._violation = _violation except ImportError: pass os.rename(path, new_name) return new_name def _remove_flat_installation(placeholder): if not os.path.isdir(placeholder): log.warn('Unkown installation at %s', placeholder) return False found = False for file in os.listdir(placeholder): if fnmatch.fnmatch(file, 'setuptools*.egg-info'): found = True break if not found: log.warn('Could not locate setuptools*.egg-info') return log.warn('Removing elements out of the way...') pkg_info = os.path.join(placeholder, file) if os.path.isdir(pkg_info): patched = _patch_egg_dir(pkg_info) else: patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) if not patched: log.warn('%s already patched.', pkg_info) return False # now let's move the files out of the way for element in ('setuptools', 'pkg_resources.py', 'site.py'): element = os.path.join(placeholder, element) if os.path.exists(element): _rename_path(element) else: log.warn('Could not find the %s element of the ' 'Setuptools distribution', element) return True def _after_install(dist): log.warn('After install bootstrap.') placeholder = dist.get_command_obj('install').install_purelib _create_fake_setuptools_pkg_info(placeholder) def _create_fake_setuptools_pkg_info(placeholder): if not placeholder or not os.path.exists(placeholder): log.warn('Could not find the install location') return pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) setuptools_file = 'setuptools-0.6c9-py%s.egg-info' % pyver pkg_info = os.path.join(placeholder, setuptools_file) if os.path.exists(pkg_info): log.warn('%s already exists', pkg_info) return log.warn('Creating %s', pkg_info) f = open(pkg_info, 'w') try: f.write(SETUPTOOLS_PKG_INFO) finally: f.close() pth_file = os.path.join(placeholder, 'setuptools.pth') log.warn('Creating %s', pth_file) f = open(pth_file, 'w') try: f.write(os.path.join(os.curdir, setuptools_file)) finally: f.close() def _patch_egg_dir(path): # let's check if it's already patched pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') if os.path.exists(pkg_info): if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): log.warn('%s already patched.', pkg_info) return False _rename_path(path) os.mkdir(path) os.mkdir(os.path.join(path, 'EGG-INFO')) pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') f = open(pkg_info, 'w') try: f.write(SETUPTOOLS_PKG_INFO) finally: f.close() return True def _before_install(): log.warn('Before install bootstrap.') _fake_setuptools() def _under_prefix(location): if 'install' not in sys.argv: return True args = sys.argv[sys.argv.index('install')+1:] for index, arg in enumerate(args): for option in ('--root', '--prefix'): if arg.startswith('%s=' % option): top_dir = arg.split('root=')[-1] return location.startswith(top_dir) elif arg == option: if len(args) > index: top_dir = args[index+1] return location.startswith(top_dir) elif option == '--user' and USER_SITE is not None: return location.startswith(USER_SITE) return True def _fake_setuptools(): log.warn('Scanning installed packages') try: import pkg_resources except ImportError: # we're cool log.warn('Setuptools or Distribute does not seem to be installed.') return ws = pkg_resources.working_set try: setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', replacement=False)) except TypeError: # old distribute API setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) if setuptools_dist is None: log.warn('No setuptools distribution found') return # detecting if it was already faked setuptools_location = setuptools_dist.location log.warn('Setuptools installation detected at %s', setuptools_location) # if --root or --preix was provided, and if # setuptools is not located in them, we don't patch it if not _under_prefix(setuptools_location): log.warn('Not patching, --root or --prefix is installing Distribute' ' in another location') return # let's see if its an egg if not setuptools_location.endswith('.egg'): log.warn('Non-egg installation') res = _remove_flat_installation(setuptools_location) if not res: return else: log.warn('Egg installation') pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') if (os.path.exists(pkg_info) and _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): log.warn('Already patched.') return log.warn('Patching...') # let's create a fake egg replacing setuptools one res = _patch_egg_dir(setuptools_location) if not res: return log.warn('Patched done.') _relaunch() def _relaunch(): log.warn('Relaunching...') # we have to relaunch the process args = [sys.executable] + sys.argv sys.exit(subprocess.call(args)) def _extractall(self, path=".", members=None): """Extract all members from the archive to the current working directory and set owner, modification time and permissions on directories afterwards. `path' specifies a different directory to extract to. `members' is optional and must be a subset of the list returned by getmembers(). """ import copy import operator from tarfile import ExtractError directories = [] if members is None: members = self for tarinfo in members: if tarinfo.isdir(): # Extract directories with a safe mode. directories.append(tarinfo) tarinfo = copy.copy(tarinfo) tarinfo.mode = 448 # decimal for oct 0700 self.extract(tarinfo, path) # Reverse sort directories. if sys.version_info < (2, 4): def sorter(dir1, dir2): return cmp(dir1.name, dir2.name) directories.sort(sorter) directories.reverse() else: directories.sort(key=operator.attrgetter('name'), reverse=True) # Set correct owner, mtime and filemode on directories. for tarinfo in directories: dirpath = os.path.join(path, tarinfo.name) try: self.chown(tarinfo, dirpath) self.utime(tarinfo, dirpath) self.chmod(tarinfo, dirpath) except ExtractError: e = sys.exc_info()[1] if self.errorlevel > 1: raise else: self._dbg(1, "tarfile: %s" % e) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" tarball = download_setuptools() _install(tarball) if __name__ == '__main__': main(sys.argv[1:]) python-cloudservers-1.0a5/docs/000755 000765 000765 00000000000 11337017727 016010 5ustar00jacob000000 000000 python-cloudservers-1.0a5/._MANIFEST.in000644 000765 000024 00000000272 11311457317 020037 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRr""com.macromates.caret{ column = 25; line = 2; }python-cloudservers-1.0a5/MANIFEST.in000644 000765 000765 00000000105 11311457317 016605 0ustar00jacob000000 000000 include README.rst recursive-include docs * recursive-include tests *python-cloudservers-1.0a5/PKG-INFO000644 000765 000765 00000011654 11337017727 016164 0ustar00jacob000000 000000 Metadata-Version: 1.0 Name: python-cloudservers Version: 1.0a5 Summary: Client library for Rackspace's Cloud Servers API Home-page: http://packages.python.org/python-cloudservers Author: Jacob Kaplan-Moss Author-email: jacob@jacobian.org License: BSD Description: Python bindings to the Rackspace Cloud Servers API ================================================== This is a client for Rackspace's Cloud Servers API. There's a Python API (the ``cloudservers`` module), and a command-line script (``cloudservers``). Each implements 100% of the Rackspace API. You'll also probably want to read `Rackspace's API guide`__ (PDF) -- the first bit, at least -- to get an idea of the concepts. Rackspace is doing the cloud hosting thing a bit differently from Amazon, and if you get the concepts this library should make more sense. __ http://docs.rackspacecloud.com/servers/api/cs-devguide-latest.pdf Command-line API ---------------- Installing this package gets you a shell command, ``cloudservers``, that you can use to interact with Rackspace. You'll need to provide your Rackspace username and API key. You can do this with the ``--username`` and ``--apikey`` params, but it's easier to just set them as environment variables:: export CLOUD_SERVERS_USERNAME=jacobian export CLOUD_SERVERS_API_KEY=yadayada You'll find complete documentation on the shell by running ``cloudservers help``:: usage: cloudservers [--username USERNAME] [--apikey APIKEY] ... Command-line interface to the Cloud Servers API. Positional arguments: backup-schedule Show or edit the backup schedule for a server. backup-schedule-delete Delete the backup schedule for a server. boot Boot a new server. delete Immediately shut down and delete a server. flavor-list Print a list of available 'flavors' (sizes of servers). help Display help about this program or one of its subcommands. image-create Create a new image by taking a snapshot of a running server. image-delete Delete an image. image-list Print a list of available images to boot from. ip-share Share an IP address from the given IP group onto a server. ip-unshare Stop sharing an given address with a server. ipgroup-create Create a new IP group. ipgroup-delete Delete an IP group. ipgroup-list Show IP groups. ipgroup-show Show details about a particular IP group. list List active servers. reboot Reboot a server. rebuild Shutdown, re-image, and re-boot a server. rename Rename a server. resize Resize a server. resize-confirm Confirm a previous resize. resize-revert Revert a previous resize (and return to the previous VM). root-password Change the root password for a server. show Show details about the given server. Optional arguments: --username USERNAME Defaults to env[CLOUD_SERVERS_USERNAME]. --apikey APIKEY Defaults to env[CLOUD_SERVERS_API_KEY]. See "cloudservers help COMMAND" for help on a specific command. Python API ---------- `Documentation is available`__, but somewhat rudimentary -- it's only a reference. __ http://packages.python.org/python-cloudservers/ By way of a quick-start:: >>> import cloudservers >>> cs = cloudservers.CloudServers(USERNAME, API_KEY) >>> cs.flavors.list() [...] >>> cs.servers.list() [...] >>> s = cs.servers.create(image=2, flavor=1, name='myserver') ... time passes ... >>> s.reboot() ... time passes ... >>> s.delete() FAQ === What's wrong with libcloud? Nothing! However, as a cross-service binding it's by definition lowest common denominator; I needed access to the Rackspace-specific APIs (shared IP groups, image snapshots, resizing, etc.). I also wanted a command-line utility. Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python python-cloudservers-1.0a5/python_cloudservers.egg-info/000755 000765 000765 00000000000 11337017727 022673 5ustar00jacob000000 000000 python-cloudservers-1.0a5/._README.rst000644 000765 000024 00000000272 11310520770 017761 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRp\""com.macromates.caret{ column = 4; line = 70; }python-cloudservers-1.0a5/README.rst000644 000765 000765 00000007763 11310520770 016550 0ustar00jacob000000 000000 Python bindings to the Rackspace Cloud Servers API ================================================== This is a client for Rackspace's Cloud Servers API. There's a Python API (the ``cloudservers`` module), and a command-line script (``cloudservers``). Each implements 100% of the Rackspace API. You'll also probably want to read `Rackspace's API guide`__ (PDF) -- the first bit, at least -- to get an idea of the concepts. Rackspace is doing the cloud hosting thing a bit differently from Amazon, and if you get the concepts this library should make more sense. __ http://docs.rackspacecloud.com/servers/api/cs-devguide-latest.pdf Command-line API ---------------- Installing this package gets you a shell command, ``cloudservers``, that you can use to interact with Rackspace. You'll need to provide your Rackspace username and API key. You can do this with the ``--username`` and ``--apikey`` params, but it's easier to just set them as environment variables:: export CLOUD_SERVERS_USERNAME=jacobian export CLOUD_SERVERS_API_KEY=yadayada You'll find complete documentation on the shell by running ``cloudservers help``:: usage: cloudservers [--username USERNAME] [--apikey APIKEY] ... Command-line interface to the Cloud Servers API. Positional arguments: backup-schedule Show or edit the backup schedule for a server. backup-schedule-delete Delete the backup schedule for a server. boot Boot a new server. delete Immediately shut down and delete a server. flavor-list Print a list of available 'flavors' (sizes of servers). help Display help about this program or one of its subcommands. image-create Create a new image by taking a snapshot of a running server. image-delete Delete an image. image-list Print a list of available images to boot from. ip-share Share an IP address from the given IP group onto a server. ip-unshare Stop sharing an given address with a server. ipgroup-create Create a new IP group. ipgroup-delete Delete an IP group. ipgroup-list Show IP groups. ipgroup-show Show details about a particular IP group. list List active servers. reboot Reboot a server. rebuild Shutdown, re-image, and re-boot a server. rename Rename a server. resize Resize a server. resize-confirm Confirm a previous resize. resize-revert Revert a previous resize (and return to the previous VM). root-password Change the root password for a server. show Show details about the given server. Optional arguments: --username USERNAME Defaults to env[CLOUD_SERVERS_USERNAME]. --apikey APIKEY Defaults to env[CLOUD_SERVERS_API_KEY]. See "cloudservers help COMMAND" for help on a specific command. Python API ---------- `Documentation is available`__, but somewhat rudimentary -- it's only a reference. __ http://packages.python.org/python-cloudservers/ By way of a quick-start:: >>> import cloudservers >>> cs = cloudservers.CloudServers(USERNAME, API_KEY) >>> cs.flavors.list() [...] >>> cs.servers.list() [...] >>> s = cs.servers.create(image=2, flavor=1, name='myserver') ... time passes ... >>> s.reboot() ... time passes ... >>> s.delete() FAQ === What's wrong with libcloud? Nothing! However, as a cross-service binding it's by definition lowest common denominator; I needed access to the Rackspace-specific APIs (shared IP groups, image snapshots, resizing, etc.). I also wanted a command-line utility. python-cloudservers-1.0a5/setup.cfg000644 000765 000765 00000000456 11337017727 016706 0ustar00jacob000000 000000 [build_sphinx] all_files = 1 build-dir = docs/_build source-dir = docs/ [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 [nosetests] cover-erase = true with-coverage = true cover-package = cloudservers cover-html = true cover-inclusive = true [upload_sphinx] upload-dir = docs/_build/html python-cloudservers-1.0a5/._setup.py000644 000765 000024 00000000272 11337017641 020013 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRbK""com.macromates.caret{ column = 20; line = 9; }python-cloudservers-1.0a5/setup.py000644 000765 000765 00000002251 11337017641 016565 0ustar00jacob000000 000000 import os from distribute_setup import use_setuptools; use_setuptools() from setuptools import setup, find_packages def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() setup( name = "python-cloudservers", version = "1.0a5", description = "Client library for Rackspace's Cloud Servers API", long_description = read('README.rst'), url = 'http://packages.python.org/python-cloudservers', license = 'BSD', author = 'Jacob Kaplan-Moss', author_email = 'jacob@jacobian.org', packages = find_packages(exclude=['distribute_setup', 'tests']), classifiers = [ 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', ], install_requires = ['httplib2', 'argparse', 'prettytable'], tests_require = ["nose", "mock"], test_suite = "nose.collector", entry_points = { 'console_scripts': ['cloudservers = cloudservers.shell:main'] } )python-cloudservers-1.0a5/tests/000755 000765 000765 00000000000 11337017727 016222 5ustar00jacob000000 000000 python-cloudservers-1.0a5/tests/.___init__.py000644 000765 000024 00000000270 11302543113 021540 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRh;l  com.macromates.caretxR<[k0?'3/«python-cloudservers-1.0a5/tests/__init__.py000644 000765 000765 00000000000 11302543113 020302 0ustar00jacob000000 000000 python-cloudservers-1.0a5/tests/__init__.pyc000644 000765 000765 00000000213 11302543232 020455 0ustar00jacob000000 000000 K Kc@sdS(N((((s4/Users/jacob/Projects/cloudservers/tests/__init__.pytspython-cloudservers-1.0a5/tests/._fakeserver.py000644 000765 000024 00000000274 11334071773 022157 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRm5$$com.macromates.caret{ column = 70; line = 289; }python-cloudservers-1.0a5/tests/fakeserver.py000644 000765 000765 00000026631 11334071773 020737 0ustar00jacob000000 000000 """ A fake server that "responds" to API methods with pre-canned responses. All of these responses come from the spec, so if for some reason the spec's wrong the tests might fail. I've indicated in comments the places where actual behavior differs from the spec. """ from __future__ import absolute_import import httplib2 from nose.tools import assert_equal from cloudservers import CloudServers from cloudservers.client import CloudServersClient from .utils import fail, assert_in, assert_not_in, assert_has_keys class FakeServer(CloudServers): def __init__(self, username=None, password=None): super(FakeServer, self).__init__('username', 'apikey') self.client = FakeClient() def assert_called(self, method, url, body=None): """ Assert than an API method was just called. """ expected = (method, url) called = self.client.callstack[-1][0:2] assert self.client.callstack, "Expected %s %s but no calls were made." % expected assert expected == called, 'Expected %s %s; got %s %s' % (expected + called) if body is not None: assert_equal(self.client.callstack[-1][2], body) self.client.callstack = [] def authenticate(self): pass class FakeClient(CloudServersClient): def __init__(self): self.username = 'username' self.apikey = 'apikey' self.callstack = [] def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly if method in ['GET', 'DELETE']: assert_not_in('body', kwargs) elif method in ['PUT', 'POST']: assert_in('body', kwargs) # Call the method munged_url = url.strip('/').replace('/', '_').replace('.', '_') callback = "%s_%s" % (method.lower(), munged_url) if not hasattr(self, callback): fail('Called unknown API method: %s %s' % (method, url)) # Note the call self.callstack.append((method, url, kwargs.get('body', None))) status, body = getattr(self, callback)(**kwargs) return httplib2.Response({"status": status}), body # # Limits # def get_limits(self, **kw): return (200, {"limits" : { "rate" : [ { "verb" : "POST", "URI" : "*", "regex" : ".*", "value" : 10, "remaining" : 2, "unit" : "MINUTE", "resetTime" : 1244425439 }, { "verb" : "POST", "URI" : "*/servers", "regex" : "^/servers", "value" : 50, "remaining" : 49, "unit" : "DAY", "resetTime" : 1244511839 }, { "verb" : "PUT", "URI" : "*", "regex" : ".*", "value" : 10, "remaining" : 2, "unit" : "MINUTE", "resetTime" : 1244425439 }, { "verb" : "GET", "URI" : "*changes-since*", "regex" : "changes-since", "value" : 3, "remaining" : 3, "unit" : "MINUTE", "resetTime" : 1244425439 }, { "verb" : "DELETE", "URI" : "*", "regex" : ".*", "value" : 100, "remaining" : 100, "unit" : "MINUTE", "resetTime" : 1244425439 } ], "absolute" : { "maxTotalRAMSize" : 51200, "maxIPGroups" : 50, "maxIPGroupMembers" : 25 } }}) # # Servers # def get_servers(self, **kw): return (200, {"servers": [ {'id': 1234, 'name': 'sample-server'}, {'id': 5678, 'name': 'sample-server2'} ]}) def get_servers_detail(self, **kw): return (200, {"servers" : [ { "id" : 1234, "name" : "sample-server", "imageId" : 2, "flavorId" : 1, "hostId" : "e4d909c290d0fb1ca068ffaddf22cbd0", "status" : "BUILD", "progress" : 60, "addresses" : { "public" : ["1.2.3.4", "5.6.7.8"], "private" : ["10.11.12.13"] }, "metadata" : { "Server Label" : "Web Head 1", "Image Version" : "2.1" } }, { "id" : 5678, "name" : "sample-server2", "imageId" : 2, "flavorId" : 1, "hostId" : "9e107d9d372bb6826bd81d3542a419d6", "status" : "ACTIVE", "addresses" : { "public" : ["9.10.11.12"], "private" : ["10.11.12.14"] }, "metadata" : { "Server Label" : "DB 1" } } ]}) def post_servers(self, body, **kw): assert_equal(body.keys(), ['server']) assert_has_keys(body['server'], required = ['name', 'imageId', 'flavorId'], optional = ['sharedIpGroupId', 'metadata', 'personality']) if 'personality' in body['server']: for pfile in body['server']['personality']: assert_has_keys(pfile, required=['path', 'contents']) return (202, self.get_servers_1234()[1]) def get_servers_1234(self, **kw): r = {'server': self.get_servers_detail()[1]['servers'][0]} return (200, r) def put_servers_1234(self, body, **kw): assert_equal(body.keys(), ['server']) assert_has_keys(body['server'], optional=['name', 'adminPass']) return (204, None) def delete_servers_1234(self, **kw): return (202, None) # # Server Addresses # def get_servers_1234_ips(self, **kw): return (200, {'addresses': self.get_servers_1234()[1]['server']['addresses']}) def get_servers_1234_ips_public(self, **kw): return (200, {'public': self.get_servers_1234_ips()[1]['addresses']['public']}) def get_servers_1234_ips_private(self, **kw): return (200, {'private': self.get_servers_1234_ips()[1]['addresses']['private']}) def put_servers_1234_ips_public_1_2_3_4(self, body, **kw): assert_equal(body.keys(), ['shareIp']) assert_has_keys(body['shareIp'], required=['sharedIpGroupId', 'configureServer']) return (202, None) def delete_servers_1234_ips_public_1_2_3_4(self, **kw): return (202, None) # # Server actions # def post_servers_1234_action(self, body, **kw): assert_equal(len(body.keys()), 1) action = body.keys()[0] if action == 'reboot': assert_equal(body[action].keys(), ['type']) assert_in(body[action]['type'], ['HARD', 'SOFT']) elif action == 'rebuild': assert_equal(body[action].keys(), ['imageId']) elif action == 'resize': assert_equal(body[action].keys(), ['flavorId']) elif action == 'confirmResize': assert_equal(body[action], None) # This one method returns a different response code return (204, None) elif action == 'revertResize': assert_equal(body[action], None) else: fail("Unexpected server action: %s" % action) return (202, None) # # Flavors # def get_flavors(self, **kw): return (200, {'flavors': [ {'id': 1, 'name': '256 MB Server'}, {'id': 2, 'name': '512 MB Server'} ]}) def get_flavors_detail(self, **kw): return (200, {'flavors': [ {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10}, {'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20} ]}) def get_flavors_1(self, **kw): return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][0]}) # # Images # def get_images(self, **kw): return (200, {'images': [ {'id': 1, 'name': 'CentOS 5.2'}, {'id': 2, 'name': 'My Server Backup'} ]}) def get_images_detail(self, **kw): return (200, {'images': [ { 'id': 1, 'name': 'CentOS 5.2', "updated" : "2010-10-10T12:00:00Z", "created" : "2010-08-10T12:00:00Z", "status" : "ACTIVE" }, { "id" : 743, "name" : "My Server Backup", "serverId" : 12, "updated" : "2010-10-10T12:00:00Z", "created" : "2010-08-10T12:00:00Z", "status" : "SAVING", "progress" : 80 } ]}) def get_images_1(self, **kw): return (200, {'image': self.get_images_detail()[1]['images'][0]}) def get_images_2(self, **kw): return (200, {'image': self.get_images_detail()[1]['images'][1]}) def post_images(self, body, **kw): assert_equal(body.keys(), ['image']) assert_has_keys(body['image'], required=['serverId', 'name']) return (202, self.get_images_1()[1]) def delete_images_1(self, **kw): return (204, None) # # Backup schedules # def get_servers_1234_backup_schedule(self, **kw): return (200, {"backupSchedule" : { "enabled" : True, "weekly" : "THURSDAY", "daily" : "H_0400_0600" }}) def post_servers_1234_backup_schedule(self, body, **kw): assert_equal(body.keys(), ['backupSchedule']) assert_has_keys(body['backupSchedule'], required=['enabled'], optional=['weekly', 'daily']) return (204, None) def delete_servers_1234_backup_schedule(self, **kw): return (204, None) # # Shared IP groups # def get_shared_ip_groups(self, **kw): return (200, {'sharedIpGroups': [ {'id': 1, 'name': 'group1'}, {'id': 2, 'name': 'group2'}, ]}) def get_shared_ip_groups_detail(self, **kw): return (200, {'sharedIpGroups': [ {'id': 1, 'name': 'group1', 'servers': [1234]}, {'id': 2, 'name': 'group2', 'servers': [5678]}, ]}) def get_shared_ip_groups_1(self, **kw): return (200, {'sharedIpGroup': self.get_shared_ip_groups_detail()[1]['sharedIpGroups'][0]}) def post_shared_ip_groups(self, body, **kw): assert_equal(body.keys(), ['sharedIpGroup']) assert_has_keys(body['sharedIpGroup'], required=['name'], optional=['server']) return (201, {'sharedIpGroup': { 'id': 10101, 'name': body['sharedIpGroup']['name'], 'servers': 'server' in body['sharedIpGroup'] and [body['sharedIpGroup']['server']] or None }}) def delete_shared_ip_groups_1(self, **kw): return (204, None)python-cloudservers-1.0a5/tests/fakeserver.pyc000644 000765 000765 00000031770 11334071774 021103 0ustar00jacob000000 000000 spKc @@sdZddklZddkZddklZddklZddkl Z ddk l Z l Z l Z lZd efd YZd e fd YZdS( s A fake server that "responds" to API methods with pre-canned responses. All of these responses come from the spec, so if for some reason the spec's wrong the tests might fail. I've indicated in comments the places where actual behavior differs from the spec. i(tabsolute_importN(t assert_equal(t CloudServers(tCloudServersClienti(tfailt assert_int assert_not_intassert_has_keyst FakeServercB@s,eZdddZddZdZRS(cC@s)tt|iddt|_dS(Ntusernametapikey(tsuperRt__init__t FakeClienttclient(tselfR tpassword((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyR scC@s||f}|iiddd!}|iiptd|||jptd|||dj ot|iidd|ng|i_dS(s< Assert than an API method was just called. iiis&Expected %s %s but no calls were made.sExpected %s %s; got %s %sN(Rt callstacktAssertionErrortNoneR(Rtmethodturltbodytexpectedtcalled((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt assert_calleds  cC@sdS(N((R((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt authenticate&sN(t__name__t __module__RR RR(((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyRs R cB@s(eZdZdZdZdZdZdZdZdZ dZ d Z d Z d Z d Zd ZdZdZdZdZdZdZdZdZdZdZdZdZdZdZdZdZdZ dZ!RS( cC@sd|_d|_g|_dS(NR R (R R R(R((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyR *s  cK@s|d jotd|n|d jotd|n|ididdidd}d |i|f}t||ptd ||fn|ii|||i ddft |||\}}t i h|d 6|fS(NtGETtDELETERtPUTtPOSTt/t_t.s%s_%ss Called unknown API method: %s %ststatus(sGETsDELETE(sPUTsPOST(RRtstriptreplacetlowerthasattrRRtappendtgetRtgetattrthttplib2tResponse(RRRtkwargst munged_urltcallbackR$R((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt _cs_request/s  '%c K@s8dhhhdd6dd6dd6dd 6d d 6d d 6dd6hdd6dd6dd6dd 6dd 6dd 6dd6hdd6dd6dd6dd 6d d 6d d 6dd6hdd6dd6dd6dd 6dd 6d d 6dd6hdd6dd6dd6dd 6dd 6d d 6dd6gd6hdd6dd 6d!d"6d#6d$6fS(%NiR tverbt*tURIs.*tregexi tvalueit remainingtMINUTEtunitil,Jt resetTimes */serverss ^/serversi2i1tDAYi_-JRRs*changes-since*s changes-sinceiRidtrateitmaxTotalRAMSizet maxIPGroupsitmaxIPGroupMemberstabsolutetlimits((Rtkw((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt get_limitsFsX cK@s3dhhdd6dd6hdd6dd6gd6fS( Niitids sample-servertnamei.ssample-server2tservers((RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt get_serversscK@sdhh dd6dd6dd6dd 6d d 6d d 6dd6hddgd6dgd6d6hdd6dd6d6hdd6dd6dd6dd 6dd 6dd 6hdgd6d gd6d6hd!d6d6gd"6fS(#NiiRDs sample-serverREitimageIditflavorIdt e4d909c290d0fb1ca068ffaddf22cbd0thostIdtBUILDR$i<tprogresss1.2.3.4s5.6.7.8tpublics 10.11.12.13tprivatet addressess Web Head 1s Server Labels2.1s Image Versiontmetadatai.ssample-server2t 9e107d9d372bb6826bd81d3542a419d6tACTIVEs 9.10.11.12s 10.11.12.14sDB 1RF((RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytget_servers_details6   cK@st|idgt|dddddgdddd gd |djo3x0|dd D]}t|dd d gqbWnd |id fS(NtservertrequiredRERHRItoptionaltsharedIpGroupIdRQt personalitytpathtcontentsii(RtkeysRtget_servers_1234(RRRBtpfile((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt post_serverss cK@s)h|idddd6}d|fS(NiRFiRUi(RT(RRBtr((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyR]scK@s4t|idgt|ddddgdS(NRURWREt adminPassi(iN(RR\RR(RRRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytput_servers_1234scK@sdS(Ni(iN(R(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytdelete_servers_1234scK@s#dh|idddd6fS(NiiRURP(R](RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytget_servers_1234_ipsscK@s#dh|idddd6fS(NiiRPRN(Rd(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytget_servers_1234_ips_publicscK@s#dh|idddd6fS(NiiRPRO(Rd(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytget_servers_1234_ips_privatescK@s4t|idgt|ddddgdS(NtshareIpRVRXtconfigureServeri(iN(RR\RR(RRRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt#put_servers_1234_ips_public_1_2_3_4scK@sdS(Ni(iN(R(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt&delete_servers_1234_ips_public_1_2_3_4scK@stt|id|id}|djo9t||idgt||dddgn|djot||idgn|d jot||id gnT|d jot||ddS|d jot||dntd|dS(NiitrebootttypetHARDtSOFTtrebuildRHtresizeRIt confirmResizeit revertResizesUnexpected server action: %si(iN(iN(RtlenR\RRR(RRRBtaction((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytpost_servers_1234_actions      cK@s3dhhdd6dd6hdd6dd6gd6fS( NiiRDs 256 MB ServerREis 512 MB Servertflavors((RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt get_flavorsscK@sOdhhdd6dd6dd6dd 6hd d6d d6d d6d d 6gd6fS(NiiRDs 256 MB ServerREitrami tdiskis 512 MB ServeriiRv((RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytget_flavors_detailscK@s#dh|idddd6fS(NiiRvitflavor(Rz(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt get_flavors_1scK@s3dhhdd6dd6hdd6dd6gd6fS( NiiRDs CentOS 5.2REisMy Server Backuptimages((RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt get_imagesscK@skdhhdd6dd6dd6dd 6d d 6hd d6d d6dd6dd6dd 6dd 6dd6gd6fS(NiiRDs CentOS 5.2REs2010-10-10T12:00:00Ztupdateds2010-08-10T12:00:00ZtcreatedRSR$isMy Server Backupi tserverIdtSAVINGiPRMR}((RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytget_images_detail scK@s#dh|idddd6fS(NiiR}itimage(R(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt get_images_1scK@s#dh|idddd6fS(NiiR}R(R(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt get_images_2!scK@sDt|idgt|ddddgd|idfS(NRRVRREii(RR\RR(RRRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt post_images$scK@sdS(Ni(iN(R(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytdelete_images_1)scK@s&dhhtd6dd6dd6d6fS(NitenabledtTHURSDAYtweeklyt H_0400_0600tdailytbackupSchedule(tTrue(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt get_servers_1234_backup_schedule/s cK@s=t|idgt|dddgdddgdS( NRRVRRWRRi(iN(RR\RR(RRRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt!post_servers_1234_backup_schedule6s#cK@sdS(Ni(iN(R(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyt#delete_servers_1234_backup_schedule;scK@s3dhhdd6dd6hdd6dd6gd6fS( NiiRDtgroup1REitgroup2tsharedIpGroups((RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytget_shared_ip_groupsAscK@sGdhhdd6dd6dgd6hdd6d d6d gd6gd 6fS( NiiRDRREiRFiRi.R((RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytget_shared_ip_groups_detailGscK@s#dh|idddd6fS(NiiRit sharedIpGroup(R(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytget_shared_ip_groups_1MscK@st|idgt|dddgddgdhhdd6|ddd6d|djo|ddgpdd 6d6fS( NRRVRERWRUiiu'RDRF(RR\RR(RRRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytpost_shared_ip_groupsPs   cK@sdS(Ni(iN(R(RRB((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pytdelete_shared_ip_groups_1Ys("RRR R1RCRGRTR_R]RbRcRdReRfRiRjRuRwRzR|R~RRRRRRRRRRRRR(((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyR )s@   ;  $                         (t__doc__t __future__RR,t nose.toolsRt cloudserversRtcloudservers.clientRtutilsRRRRRR (((s6/Users/jacob/Projects/cloudservers/tests/fakeserver.pyts "python-cloudservers-1.0a5/tests/livetests.pyc000644 000765 000765 00000000555 11306315053 020753 0ustar00jacob000000 000000 Kc@sddklZdZdS(i(tfailcCstddS(Ntoops(R(((s5/Users/jacob/Projects/cloudservers/tests/livetests.pyt test_livesN(tutilsRR(((s5/Users/jacob/Projects/cloudservers/tests/livetests.pytspython-cloudservers-1.0a5/tests/._test_auth.py000644 000765 000024 00000000272 11310247042 022004 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRh;o""com.macromates.caret{ column = 50; line = 3; }python-cloudservers-1.0a5/tests/test_auth.py000644 000765 000765 00000003400 11310247042 020553 0ustar00jacob000000 000000 import mock import cloudservers import httplib2 from nose.tools import assert_raises, assert_equal def test_authenticate_success(): cs = cloudservers.CloudServers("username", "apikey") auth_response = httplib2.Response({ 'status': 204, 'x-server-management-url': 'https://servers.api.rackspacecloud.com/v1.0/443470', 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1', }) mock_request = mock.Mock(return_value=(auth_response, None)) with mock.patch_object(httplib2.Http, "request", mock_request): cs.client.authenticate() mock_request.assert_called_with(cs.client.AUTH_URL, 'GET', headers = { 'X-Auth-User': 'username', 'X-Auth-Key': 'apikey', 'User-Agent': cs.client.USER_AGENT }) assert_equal(cs.client.management_url, auth_response['x-server-management-url']) assert_equal(cs.client.auth_token, auth_response['x-auth-token']) def test_authenticate_failure(): cs = cloudservers.CloudServers("username", "apikey") auth_response = httplib2.Response({'status': 401}) mock_request = mock.Mock(return_value=(auth_response, None)) with mock.patch_object(httplib2.Http, "request", mock_request): assert_raises(cloudservers.Unauthorized, cs.client.authenticate) def test_auth_automatic(): client = cloudservers.CloudServers("username", "apikey").client client.management_url = '' mock_request = mock.Mock(return_value=(None, None)) with mock.patch_object(client, 'request', mock_request): with mock.patch_object(client, 'authenticate') as mock_authenticate: client.get('/') mock_authenticate.assert_called() mock_request.assert_called() python-cloudservers-1.0a5/tests/test_auth.pyc000644 000765 000765 00000004267 11310247044 020734 0ustar00jacob000000 000000 "N!Kc@sYddkZddkZddkZddklZlZdZdZdZdS(iN(t assert_raisest assert_equalc Cstidd}tihdd6dd6dd6}tid |df}titid |i i zv|i i |i |i id d hdd 6dd6|i id6t|i i|dt|i i|dWdQXdS(Ntusernametapikeyitstatuss2https://servers.api.rackspacecloud.com/v1.0/443470sx-server-management-urls$1b751d74-de0c-46ae-84f0-915744b582d1s x-auth-tokent return_valuetrequesttGETtheaderss X-Auth-Users X-Auth-Keys User-Agent(t cloudserverst CloudServersthttplib2tResponsetmocktMocktNonet patch_objecttHttpt__exit__t __enter__tclientt authenticatetassert_called_withtAUTH_URLt USER_AGENTRtmanagement_urlt auth_token(tcst auth_responset mock_request((s5/Users/jacob/Projects/cloudservers/tests/test_auth.pyttest_authenticate_successs  $ cCstidd}tihdd6}tid|df}titid|i i zt ti |i iWdQXdS(NRRiRRR(R R R R R RRRRRRRt UnauthorizedRR(RRR((s5/Users/jacob/Projects/cloudservers/tests/test_auth.pyttest_authenticate_failures $c Cstiddi}d|_tidd}ti|d|ii zTti|dii }z.|~}|i d|i |i WdQXWdQXdS( NRRtRRRt/(NN( R R RRR RRRRRtgett assert_called(RRt_[1]tmock_authenticate((s5/Users/jacob/Projects/cloudservers/tests/test_auth.pyttest_auth_automatic s !)  ( R R R t nose.toolsRRRR R'(((s5/Users/jacob/Projects/cloudservers/tests/test_auth.pyts     python-cloudservers-1.0a5/tests/._test_backup_schedules.py000644 000765 000024 00000000271 11310247134 024350 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRn;!!com.macromates.caret{ column = 0; line = 5; }python-cloudservers-1.0a5/tests/test_backup_schedules.py000644 000765 000765 00000003307 11310247134 023126 0ustar00jacob000000 000000 from __future__ import absolute_import from cloudservers.backup_schedules import * from .fakeserver import FakeServer from .utils import assert_isinstance cs = FakeServer() def test_get_backup_schedule(): s = cs.servers.get(1234) # access via manager b = cs.backup_schedules.get(server=s) assert_isinstance(b, BackupSchedule) cs.assert_called('GET', '/servers/1234/backup_schedule') b = cs.backup_schedules.get(server=1234) assert_isinstance(b, BackupSchedule) cs.assert_called('GET', '/servers/1234/backup_schedule') # access via instance assert_isinstance(s.backup_schedule, BackupSchedule) cs.assert_called('GET', '/servers/1234/backup_schedule') # Just for coverage's sake b = s.backup_schedule.get() cs.assert_called('GET', '/servers/1234/backup_schedule') def test_create_update_backup_schedule(): s = cs.servers.get(1234) # create/update via manager cs.backup_schedules.update( server = s, enabled = True, weekly = BACKUP_WEEKLY_THURSDAY, daily = BACKUP_DAILY_H_1000_1200 ) cs.assert_called('POST', '/servers/1234/backup_schedule') # and via instance s.backup_schedule.update(enabled=False) cs.assert_called('POST', '/servers/1234/backup_schedule') def test_delete_backup_schedule(): s = cs.servers.get(1234) # delete via manager cs.backup_schedules.delete(s) cs.assert_called('DELETE', '/servers/1234/backup_schedule') cs.backup_schedules.delete(1234) cs.assert_called('DELETE', '/servers/1234/backup_schedule') # and via instance s.backup_schedule.delete() cs.assert_called('DELETE', '/servers/1234/backup_schedule') python-cloudservers-1.0a5/tests/test_backup_schedules.pyc000644 000765 000765 00000003542 11310247141 023270 0ustar00jacob000000 000000 \N!Kc@@sbddklZddkTddklZddklZeZdZdZ dZ d S( i(tabsolute_import(t*i(t FakeServer(tassert_isinstancecC@stiid}tiid|}t|ttiddtiidd}t|ttiddt|ittidd|ii}tidddS(NitservertGETs/servers/1234/backup_schedule(tcstserverstgettbackup_schedulesRtBackupSchedulet assert_calledtbackup_schedule(tstb((sA/Users/jacob/Projects/cloudservers/tests/test_backup_schedules.pyttest_get_backup_schedule s  c C@sntiid}tiid|dtdtdttidd|i idt tidddS(NiRtenabledtweeklytdailytPOSTs/servers/1234/backup_schedule( RRRR tupdatetTruetBACKUP_WEEKLY_THURSDAYtBACKUP_DAILY_H_1000_1200R R tFalse(R ((sA/Users/jacob/Projects/cloudservers/tests/test_backup_schedules.pyt"test_create_update_backup_schedules cC@sstiid}tii|tiddtiidtidd|iitidddS(NitDELETEs/servers/1234/backup_schedule(RRRR tdeleteR R (R ((sA/Users/jacob/Projects/cloudservers/tests/test_backup_schedules.pyttest_delete_backup_schedule-s N( t __future__Rtcloudservers.backup_schedulest fakeserverRtutilsRRRRR(((sA/Users/jacob/Projects/cloudservers/tests/test_backup_schedules.pyts    python-cloudservers-1.0a5/tests/._test_base.py000644 000765 000024 00000000272 11310247207 021760 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRn>""com.macromates.caret{ column = 4; line = 12; }python-cloudservers-1.0a5/tests/test_base.py000644 000765 000765 00000002577 11310247207 020545 0ustar00jacob000000 000000 from __future__ import absolute_import import cloudservers.base from .fakeserver import FakeServer from cloudservers import Flavor from cloudservers.base import Resource from nose.tools import assert_equal, assert_not_equal, assert_raises cs = FakeServer() def test_resource_repr(): r = Resource(None, dict(foo="bar", baz="spam")) assert_equal(repr(r), "") def test_getid(): assert_equal(cloudservers.base.getid(4), 4) class O(object): id = 4 assert_equal(cloudservers.base.getid(O), 4) def test_resource_lazy_getattr(): f = Flavor(cs.flavors, {'id': 1}) assert_equal(f.name, '256 MB Server') cs.assert_called('GET', '/flavors/1') # Missing stuff still fails after a second get assert_raises(AttributeError, getattr, f, 'blahblah') cs.assert_called('GET', '/flavors/1') def test_eq(): # Two resources of the same type with the same id: equal r1 = Resource(None, {'id':1, 'name':'hi'}) r2 = Resource(None, {'id':1, 'name':'hello'}) assert_equal(r1, r2) # Two resoruces of different types: never equal r1 = Resource(None, {'id': 1}) r2 = Flavor(None, {'id': 1}) assert_not_equal(r1, r2) # Two resources with no ID: equal if their info is equal r1 = Resource(None, {'name': 'joe', 'age': 12}) r2 = Resource(None, {'name': 'joe', 'age': 12}) assert_equal(r1, r2)python-cloudservers-1.0a5/tests/test_base.pyc000644 000765 000765 00000004234 11310247211 020673 0ustar00jacob000000 000000 N!Kc@@sddklZddkZddklZddklZddklZddkl Z l Z l Z eZ dZ d Zd Zd ZdS( i(tabsolute_importNi(t FakeServer(tFlavor(tResource(t assert_equaltassert_not_equalt assert_raisescC@s5tdtdddd}tt|ddS(Ntfootbartbaztspams(RtNonetdictRtrepr(tr((s5/Users/jacob/Projects/cloudservers/tests/test_base.pyttest_resource_repr scC@sLttiidddtfdY}ttii|ddS(NitOcB@seZdZRS(i(t__name__t __module__tid(((s5/Users/jacob/Projects/cloudservers/tests/test_base.pyRs(Rt cloudserverstbasetgetidtobject(R((s5/Users/jacob/Projects/cloudservers/tests/test_base.pyt test_getidscC@s`ttihdd6}t|idtiddttt|dtidddS(NiRs 256 MB ServertGETs /flavors/1tblahblah( RtcstflavorsRtnamet assert_calledRtAttributeErrortgetattr(tf((s5/Users/jacob/Projects/cloudservers/tests/test_base.pyttest_resource_lazy_getattrs cC@stdhdd6dd6}tdhdd6dd6}t||tdhdd6}tdhdd6}t||tdhdd6dd6}tdhdd6dd6}t||dS( NiRthiRthellotjoei tage(RR RRR(tr1tr2((s5/Users/jacob/Projects/cloudservers/tests/test_base.pyttest_eqs  (t __future__Rtcloudservers.baseRt fakeserverRRRt nose.toolsRRRRRRR"R)(((s5/Users/jacob/Projects/cloudservers/tests/test_base.pyts     python-cloudservers-1.0a5/tests/._test_client.py000644 000765 000024 00000000271 11310247235 022324 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRn:!!com.macromates.caret{ column = 0; line = 3; }python-cloudservers-1.0a5/tests/test_client.py000644 000765 000765 00000002256 11310247235 021104 0ustar00jacob000000 000000 import mock import httplib2 from cloudservers.client import CloudServersClient from nose.tools import assert_equal fake_response = httplib2.Response({"status": 200}) fake_body = '{"hi": "there"}' mock_request = mock.Mock(return_value=(fake_response, fake_body)) def client(): cl = CloudServersClient("username", "apikey") cl.management_url = "http://example.com" cl.auth_token = "token" return cl def test_get(): cl = client() with mock.patch_object(httplib2.Http, "request", mock_request): resp, body = cl.get("/hi") mock_request.assert_called_with("http://example.com/hi", "GET", headers={"X-Auth-Token": "token", "User-Agent": cl.USER_AGENT}) # Automatic JSON parsing assert_equal(body, {"hi":"there"}) def test_post(): cl = client() with mock.patch_object(httplib2.Http, "request", mock_request): cl.post("/hi", body=[1, 2, 3]) mock_request.assert_called_with("http://example.com/hi", "POST", headers = { "X-Auth-Token": "token", "Content-Type": "application/json", "User-Agent": cl.USER_AGENT}, body = '[1, 2, 3]' )python-cloudservers-1.0a5/tests/test_client.pyc000644 000765 000765 00000003542 11310247240 021242 0ustar00jacob000000 000000 N!Kc@sddkZddkZddklZddklZeihdd6ZdZei deefZ dZ d Z d Z dS( iN(tCloudServersClient(t assert_equalitstatuss{"hi": "there"}t return_valuecCs%tdd}d|_d|_|S(Ntusernametapikeyshttp://example.comttoken(Rtmanagement_urlt auth_token(tcl((s7/Users/jacob/Projects/cloudservers/tests/test_client.pytclient s  c Cst}titidtiizT|id\}}ti dddhdd6|i d6t |hd d 6WdQXdS( Ntrequests/hishttp://example.com/hitGETtheadersRs X-Auth-Tokens User-Agentttherethi( R tmockt patch_objectthttplib2tHttpt mock_requestt__exit__t __enter__tgettassert_called_witht USER_AGENTR(R tresptbody((s7/Users/jacob/Projects/cloudservers/tests/test_client.pyttest_gets  $c Cst}titidtiizT|idddddgti ddd hd d 6d d 6|i d6ddWdQXdS(NR s/hiRiiishttp://example.com/hitPOSTR Rs X-Auth-Tokensapplication/jsons Content-Types User-Agents [1, 2, 3]( R RRRRRRRtpostRR(R ((s7/Users/jacob/Projects/cloudservers/tests/test_client.pyt test_posts $ (RRtcloudservers.clientRt nose.toolsRtResponset fake_responset fake_bodytMockRR RR(((s7/Users/jacob/Projects/cloudservers/tests/test_client.pyts    python-cloudservers-1.0a5/tests/._test_flavors.py000644 000765 000024 00000000272 11310247273 022525 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRm|""com.macromates.caret{ column = 4; line = 29; }python-cloudservers-1.0a5/tests/test_flavors.py000644 000765 000765 00000001523 11310247273 021300 0ustar00jacob000000 000000 from __future__ import absolute_import from cloudservers import Flavor, NotFound from .fakeserver import FakeServer from .utils import assert_isinstance from nose.tools import assert_raises, assert_equal cs = FakeServer() def test_list_flavors(): fl = cs.flavors.list() cs.assert_called('GET', '/flavors/detail') [assert_isinstance(f, Flavor) for f in fl] def test_get_flavor_details(): f = cs.flavors.get(1) cs.assert_called('GET', '/flavors/1') assert_isinstance(f, Flavor) assert_equal(f.ram, 256) assert_equal(f.disk, 10) def test_find(): f = cs.flavors.find(ram=256) cs.assert_called('GET', '/flavors/detail') assert_equal(f.name, '256 MB Server') f = cs.flavors.find(disk=20) assert_equal(f.name, '512 MB Server') assert_raises(NotFound, cs.flavors.find, disk=12345)python-cloudservers-1.0a5/tests/test_flavors.pyc000644 000765 000765 00000003134 11310247301 021433 0ustar00jacob000000 000000 N!Kc@@sddklZddklZlZddklZddklZddk l Z l Z eZ dZ dZd Zd S( i(tabsolute_import(tFlavortNotFoundi(t FakeServer(tassert_isinstance(t assert_raisest assert_equalcC@sKtii}tiddg}|D]}|t|tq*~dS(NtGETs/flavors/detail(tcstflavorstlistt assert_calledRR(tflt_[1]tf((s8/Users/jacob/Projects/cloudservers/tests/test_flavors.pyttest_list_flavors scC@sStiid}tiddt|tt|idt|iddS(NiRs /flavors/1ii ( RR tgetR RRRtramtdisk(R((s8/Users/jacob/Projects/cloudservers/tests/test_flavors.pyttest_get_flavor_detailss  cC@swtiidd}tiddt|idtiidd}t|idtttiidd dS( NRiRs/flavors/details 256 MB ServerRis 512 MB Serveri90(RR tfindR RtnameRR(R((s8/Users/jacob/Projects/cloudservers/tests/test_flavors.pyt test_finds N(t __future__Rt cloudserversRRt fakeserverRtutilsRt nose.toolsRRRRRR(((s8/Users/jacob/Projects/cloudservers/tests/test_flavors.pyts   python-cloudservers-1.0a5/tests/._test_images.py000644 000765 000024 00000000272 11310247324 022313 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRm""com.macromates.caret{ column = 35; line = 5; }python-cloudservers-1.0a5/tests/test_images.py000644 000765 000765 00000002047 11310247324 021070 0ustar00jacob000000 000000 from __future__ import absolute_import from cloudservers import Image from .fakeserver import FakeServer from .utils import assert_isinstance from nose.tools import assert_equal cs = FakeServer() def test_list_images(): il = cs.images.list() cs.assert_called('GET', '/images/detail') [assert_isinstance(i, Image) for i in il] def test_get_image_details(): i = cs.images.get(1) cs.assert_called('GET', '/images/1') assert_isinstance(i, Image) assert_equal(i.id, 1) assert_equal(i.name, 'CentOS 5.2') def test_create_image(): i = cs.images.create(server=1234, name="Just in case") cs.assert_called('POST', '/images') assert_isinstance(i, Image) def test_delete_image(): cs.images.delete(1) cs.assert_called('DELETE', '/images/1') def test_find(): i = cs.images.find(name="CentOS 5.2") assert_equal(i.id, 1) cs.assert_called('GET', '/images/detail') iml = cs.images.findall(status='SAVING') assert_equal(len(iml), 1) assert_equal(iml[0].name, 'My Server Backup')python-cloudservers-1.0a5/tests/test_images.pyc000644 000765 000765 00000004171 11310247403 021231 0ustar00jacob000000 000000 N!Kc@@sddklZddklZddklZddklZddkl Z eZ dZ dZ d Z d Zd Zd S( i(tabsolute_import(tImagei(t FakeServer(tassert_isinstance(t assert_equalcC@sKtii}tiddg}|D]}|t|tq*~dS(NtGETs/images/detail(tcstimagestlistt assert_calledRR(tilt_[1]ti((s7/Users/jacob/Projects/cloudservers/tests/test_images.pyttest_list_images scC@sStiid}tiddt|tt|idt|iddS(NiRs /images/1s CentOS 5.2( RRtgetR RRRtidtname(R ((s7/Users/jacob/Projects/cloudservers/tests/test_images.pyttest_get_image_detailss  cC@s<tiidddd}tiddt|tdS(NtserveriRs Just in casetPOSTs/images(RRtcreateR RR(R ((s7/Users/jacob/Projects/cloudservers/tests/test_images.pyttest_create_imagescC@s$tiidtidddS(NitDELETEs /images/1(RRtdeleteR (((s7/Users/jacob/Projects/cloudservers/tests/test_images.pyttest_delete_imagescC@sutiidd}t|idtiddtiidd}tt|dt|did dS( NRs CentOS 5.2iRs/images/detailtstatustSAVINGisMy Server Backup( RRtfindRRR tfindalltlenR(R timl((s7/Users/jacob/Projects/cloudservers/tests/test_images.pyt test_finds N(t __future__Rt cloudserversRt fakeserverRtutilsRt nose.toolsRRR RRRR(((s7/Users/jacob/Projects/cloudservers/tests/test_images.pyts     python-cloudservers-1.0a5/tests/._test_ipgroups.py000644 000765 000024 00000000272 11310247344 022720 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRm""com.macromates.caret{ column = 35; line = 5; }python-cloudservers-1.0a5/tests/test_ipgroups.py000644 000765 000765 00000002265 11310247344 021477 0ustar00jacob000000 000000 from __future__ import absolute_import from cloudservers import IPGroup from .fakeserver import FakeServer from .utils import assert_isinstance from nose.tools import assert_equal cs = FakeServer() def test_list_ipgroups(): ipl = cs.ipgroups.list() cs.assert_called('GET', '/shared_ip_groups/detail') [assert_isinstance(ipg, IPGroup) for ipg in ipl] def test_get_ipgroup(): ipg = cs.ipgroups.get(1) cs.assert_called('GET', '/shared_ip_groups/1') assert_isinstance(ipg, IPGroup) def test_create_ipgroup(): ipg = cs.ipgroups.create("My group", 1234) cs.assert_called('POST', '/shared_ip_groups') assert_isinstance(ipg, IPGroup) def test_delete_ipgroup(): ipg = cs.ipgroups.get(1) ipg.delete() cs.assert_called('DELETE', '/shared_ip_groups/1') cs.ipgroups.delete(ipg) cs.assert_called('DELETE', '/shared_ip_groups/1') cs.ipgroups.delete(1) cs.assert_called('DELETE', '/shared_ip_groups/1') def test_find(): ipg = cs.ipgroups.find(name='group1') cs.assert_called('GET', '/shared_ip_groups/detail') assert_equal(ipg.name, 'group1') ipgl = cs.ipgroups.findall(id=1) assert_equal(ipgl, [IPGroup(None, {'id': 1})])python-cloudservers-1.0a5/tests/test_ipgroups.pyc000644 000765 000765 00000004214 11310247403 021632 0ustar00jacob000000 000000 N!Kc@@sddklZddklZddklZddklZddkl Z eZ dZ dZ d Z d Zd Zd S( i(tabsolute_import(tIPGroupi(t FakeServer(tassert_isinstance(t assert_equalcC@sKtii}tiddg}|D]}|t|tq*~dS(NtGETs/shared_ip_groups/detail(tcstipgroupstlistt assert_calledRR(tiplt_[1]tipg((s9/Users/jacob/Projects/cloudservers/tests/test_ipgroups.pyttest_list_ipgroups scC@s3tiid}tiddt|tdS(NiRs/shared_ip_groups/1(RRtgetR RR(R ((s9/Users/jacob/Projects/cloudservers/tests/test_ipgroups.pyttest_get_ipgroupscC@s6tiidd}tiddt|tdS(NsMy groupitPOSTs/shared_ip_groups(RRtcreateR RR(R ((s9/Users/jacob/Projects/cloudservers/tests/test_ipgroups.pyttest_create_ipgroupscC@sptiid}|itiddtii|tiddtiidtidddS(NitDELETEs/shared_ip_groups/1(RRRtdeleteR (R ((s9/Users/jacob/Projects/cloudservers/tests/test_ipgroups.pyttest_delete_ipgroups cC@sntiidd}tiddt|idtiidd}t|tdhdd6gdS(Ntnametgroup1Rs/shared_ip_groups/detailtidi( RRtfindR RRtfindallRtNone(R tipgl((s9/Users/jacob/Projects/cloudservers/tests/test_ipgroups.pyt test_find"s N(t __future__Rt cloudserversRt fakeserverRtutilsRt nose.toolsRRR RRRR(((s9/Users/jacob/Projects/cloudservers/tests/test_ipgroups.pyts     python-cloudservers-1.0a5/tests/test_live.pyc000644 000765 000765 00000002714 11306323166 020732 0ustar00jacob000000 000000 ^Kc @sddkZddkZddkZddkZddklZdeijo deijZeo$eieideidZ ndZ e dZ dS(iN(tSkipTesttCLOUD_SERVERS_USERNAMEtCLOUD_SERVERS_API_KEYcs"tifd}|S(Ncs"tptdndS(Nsno creds in environ(tTESTABLER((ttest(s5/Users/jacob/Projects/cloudservers/tests/test_live.pyt_tests(t functoolstwraps(RR((Rs5/Users/jacob/Projects/cloudservers/tests/test_live.pytlivescCsx,tiiD]}|idjoPqqWx,tiiD]}|idjoPq?q?Wtiid||}x,|idjot i d|i qyW|i dS(NsUbuntu 9.10 (karmic)it testservertACTIVEi ( tcstimagestlisttnametflavorstramtserverstcreatetstatusttimetsleeptgettdelete(timtflavts((s5/Users/jacob/Projects/cloudservers/tests/test_live.pyttest_create_destroys   ( tosRt cloudserversRtnose.plugins.skipRtenvironRt CloudServersR RR(((s5/Users/jacob/Projects/cloudservers/tests/test_live.pyts      python-cloudservers-1.0a5/tests/test_server_addresses.pyc000644 000765 000765 00000000230 11306274442 023327 0ustar00jacob000000 000000 /vKc@sdS(N((((sA/Users/jacob/Projects/cloudservers/tests/test_server_addresses.pytspython-cloudservers-1.0a5/tests/._test_servers.py000644 000765 000024 00000000273 11310247371 022542 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRm##com.macromates.caret{ column = 19; line = 26; }python-cloudservers-1.0a5/tests/test_servers.py000644 000765 000765 00000007713 11310247371 021323 0ustar00jacob000000 000000 from __future__ import absolute_import import StringIO from nose.tools import assert_equal from .fakeserver import FakeServer from .utils import assert_isinstance from cloudservers import Server cs = FakeServer() def test_list_servers(): sl = cs.servers.list() cs.assert_called('GET', '/servers/detail') [assert_isinstance(s, Server) for s in sl] def test_get_server_details(): s = cs.servers.get(1234) cs.assert_called('GET', '/servers/1234') assert_isinstance(s, Server) assert_equal(s.id, 1234) assert_equal(s.status, 'BUILD') def test_create_server(): s = cs.servers.create( name = "My server", image = 1, flavor = 1, meta = {'foo': 'bar'}, ipgroup = 1, files = { '/etc/passwd': 'some data', # a file '/tmp/foo.txt': StringIO.StringIO('data') # a stream } ) cs.assert_called('POST', '/servers') assert_isinstance(s, Server) def test_update_server(): s = cs.servers.get(1234) # Update via instance s.update(name='hi') cs.assert_called('PUT', '/servers/1234') s.update(name='hi', password='there') cs.assert_called('PUT', '/servers/1234') # Silly, but not an error s.update() # Update via manager cs.servers.update(s, name='hi') cs.assert_called('PUT', '/servers/1234') cs.servers.update(1234, password='there') cs.assert_called('PUT', '/servers/1234') cs.servers.update(s, name='hi', password='there') cs.assert_called('PUT', '/servers/1234') def test_delete_server(): s = cs.servers.get(1234) s.delete() cs.assert_called('DELETE', '/servers/1234') cs.servers.delete(1234) cs.assert_called('DELETE', '/servers/1234') cs.servers.delete(s) cs.assert_called('DELETE', '/servers/1234') def test_find(): s = cs.servers.find(name='sample-server') cs.assert_called('GET', '/servers/detail') assert_equal(s.name, 'sample-server') # Find with multiple results arbitraility returns the first item s = cs.servers.find(flavorId=1) sl = cs.servers.findall(flavorId=1) assert_equal(sl[0], s) assert_equal([s.id for s in sl], [1234, 5678]) def test_share_ip(): s = cs.servers.get(1234) # Share via instance s.share_ip(ipgroup=1, address='1.2.3.4') cs.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4') # Share via manager cs.servers.share_ip(s, ipgroup=1, address='1.2.3.4', configure=False) cs.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4') def test_unshare_ip(): s = cs.servers.get(1234) # Unshare via instance s.unshare_ip('1.2.3.4') cs.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4') # Unshare via manager cs.servers.unshare_ip(s, '1.2.3.4') cs.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4') def test_reboot_server(): s = cs.servers.get(1234) s.reboot() cs.assert_called('POST', '/servers/1234/action') cs.servers.reboot(s, type='HARD') cs.assert_called('POST', '/servers/1234/action') def test_rebuild_server(): s = cs.servers.get(1234) s.rebuild(image=1) cs.assert_called('POST', '/servers/1234/action') cs.servers.rebuild(s, image=1) cs.assert_called('POST', '/servers/1234/action') def test_resize_server(): s = cs.servers.get(1234) s.resize(flavor=1) cs.assert_called('POST', '/servers/1234/action') cs.servers.resize(s, flavor=1) cs.assert_called('POST', '/servers/1234/action') def test_confirm_resized_server(): s = cs.servers.get(1234) s.confirm_resize() cs.assert_called('POST', '/servers/1234/action') cs.servers.confirm_resize(s) cs.assert_called('POST', '/servers/1234/action') def test_revert_resized_server(): s = cs.servers.get(1234) s.revert_resize() cs.assert_called('POST', '/servers/1234/action') cs.servers.revert_resize(s) cs.assert_called('POST', '/servers/1234/action')python-cloudservers-1.0a5/tests/test_servers.pyc000644 000765 000765 00000012561 11310247403 021457 0ustar00jacob000000 000000 N!Kc@@sddklZddkZddklZddklZddklZddk l Z eZ dZ d Z d Zd Zd Zd ZdZdZdZdZdZdZdZdS(i(tabsolute_importN(t assert_equali(t FakeServer(tassert_isinstance(tServercC@sKtii}tiddg}|D]}|t|tq*~dS(NtGETs/servers/detail(tcstserverstlistt assert_calledRR(tslt_[1]ts((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_list_servers scC@sStiid}tiddt|tt|idt|iddS(NiRs /servers/1234tBUILD( RRtgetR RRRtidtstatus(R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_get_server_detailss  cC@srtiidddddddhdd6d dd hd d 6tid d6}tiddt|tdS(Ntnames My servertimageitflavortmetatbartfootipgrouptfiless some datas /etc/passwdtdatas /tmp/foo.txttPOSTs/servers(RRtcreatetStringIOR RR(R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_create_servers  cC@stiid}|iddtidd|iddddtidd|itii|ddtiddtiidddtiddtii|ddddtidddS(NiRthitPUTs /servers/1234tpasswordtthere(RRRtupdateR (R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_update_server&s cC@sptiid}|itiddtiidtiddtii|tidddS(NitDELETEs /servers/1234(RRRtdeleteR (R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_delete_server:s cC@stiidd}tiddt|idtiidd}tiidd}t|d|tg}|D]}||iq~~dd gdS( NRs sample-serverRs/servers/detailtflavorIdiiii.(RRtfindR RRtfindallR(R R R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyt test_findCscC@sntiid}|iddddtiddtii|dddddttidddS( NiRitaddresss1.2.3.4R!s /servers/1234/ips/public/1.2.3.4t configure(RRRtshare_ipR tFalse(R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyt test_share_ipNs "cC@sVtiid}|idtiddtii|dtidddS(Nis1.2.3.4R&s /servers/1234/ips/public/1.2.3.4(RRRt unshare_ipR (R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_unshare_ipYs  cC@sVtiid}|itiddtii|ddtidddS(NiRs/servers/1234/actionttypetHARD(RRRtrebootR (R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_reboot_serverds  cC@s\tiid}|iddtiddtii|ddtidddS(NiRiRs/servers/1234/action(RRRtrebuildR (R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_rebuild_serverks cC@s\tiid}|iddtiddtii|ddtidddS(NiRiRs/servers/1234/action(RRRtresizeR (R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_resize_serverrs cC@sPtiid}|itiddtii|tidddS(NiRs/servers/1234/action(RRRtconfirm_resizeR (R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_confirm_resized_serverys  cC@sPtiid}|itiddtii|tidddS(NiRs/servers/1234/action(RRRt revert_resizeR (R ((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyttest_revert_resized_servers  (t __future__RRt nose.toolsRt fakeserverRtutilsRt cloudserversRRR RRR%R(R,R1R3R7R9R;R=R?(((s8/Users/jacob/Projects/cloudservers/tests/test_servers.pyts&          python-cloudservers-1.0a5/tests/._test_shell.py000644 000765 000024 00000000274 11334072311 022155 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRpj$$com.macromates.caret{ column = 35; line = 168; }python-cloudservers-1.0a5/tests/test_shell.py000644 000765 000765 00000013416 11334072311 020732 0ustar00jacob000000 000000 from __future__ import absolute_import import os import mock from nose.tools import assert_raises from cloudservers.shell import CloudserversShell, CommandError from .fakeserver import FakeServer # Patch os.environ to avoid required auth info. def setup(): global _old_env fake_env = { 'CLOUD_SERVERS_USERNAME': 'username', 'CLOUD_SERVERS_API_KEY': 'password' } _old_env, os.environ = os.environ, fake_env # Make a fake shell object, a helping wrapper to call it, and a quick way # of asserting that certain API calls were made. global shell, _shell, assert_called _shell = CloudserversShell() _shell._api_class = FakeServer assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b) shell = lambda cmd: _shell.main(cmd.split()) def teardown(): global _old_env os.environ = _old_env def test_backup_schedule(): shell('backup-schedule 1234') assert_called('GET', '/servers/1234/backup_schedule') shell('backup-schedule sample-server --weekly monday') assert_called( 'POST', '/servers/1234/backup_schedule', {'backupSchedule': {'enabled': True, 'daily': 'DISABLED', 'weekly': 'MONDAY'}} ) shell('backup-schedule sample-server --weekly disabled --daily h_0000_0200') assert_called( 'POST', '/servers/1234/backup_schedule', {'backupSchedule': {'enabled': True, 'daily': 'H_0000_0200', 'weekly': 'DISABLED'}} ) shell('backup-schedule sample-server --disable') assert_called( 'POST', '/servers/1234/backup_schedule', {'backupSchedule': {'enabled': False, 'daily': 'DISABLED', 'weekly': 'DISABLED'}} ) def test_backup_schedule_delete(): shell('backup-schedule-delete 1234') assert_called('DELETE', '/servers/1234/backup_schedule') def test_boot(): shell('boot --image 1 some-server') assert_called( 'POST', '/servers', {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': 1}} ) shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ') assert_called( 'POST', '/servers', {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': 1, 'metadata': {'foo': 'bar', 'spam': 'eggs'}}} ) def test_flavor_list(): shell('flavor-list') assert_called('GET', '/flavors/detail') def test_image_list(): shell('image-list') assert_called('GET', '/images/detail') def test_image_create(): shell('image-create sample-server new-image') assert_called( 'POST', '/images', {'image': {'name': 'new-image', 'serverId': 1234}} ) def test_image_delete(): shell('image-delete 1') assert_called('DELETE', '/images/1') def test_ip_share(): shell('ip-share sample-server 1 1.2.3.4') assert_called( 'PUT', '/servers/1234/ips/public/1.2.3.4', {'shareIp': {'sharedIpGroupId': 1, 'configureServer': True}} ) def test_ip_unshare(): shell('ip-unshare sample-server 1.2.3.4') assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4') def test_ipgroup_list(): shell('ipgroup-list') assert_called('GET', '/shared_ip_groups/detail') def test_ipgroup_show(): shell('ipgroup-show 1') assert_called('GET', '/shared_ip_groups/1') shell('ipgroup-show group2') # does a search, not a direct GET assert_called('GET', '/shared_ip_groups/detail') def test_ipgroup_create(): shell('ipgroup-create a-group') assert_called( 'POST', '/shared_ip_groups', {'sharedIpGroup': {'name': 'a-group'}} ) shell('ipgroup-create a-group sample-server') assert_called( 'POST', '/shared_ip_groups', {'sharedIpGroup': {'name': 'a-group', 'server': 1234}} ) def test_ipgroup_delete(): shell('ipgroup-delete group1') assert_called('DELETE', '/shared_ip_groups/1') def test_list(): shell('list') assert_called('GET', '/servers/detail') def test_reboot(): shell('reboot sample-server') assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}}) shell('reboot sample-server --hard') assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}}) def test_rebuild(): shell('rebuild sample-server 1') assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}}) def test_rename(): shell('rename sample-server newname') assert_called('PUT', '/servers/1234', {'server': {'name':'newname'}}) def test_resize(): shell('resize sample-server 1') assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}}) def test_resize_confirm(): shell('resize-confirm sample-server') assert_called('POST', '/servers/1234/action', {'confirmResize': None}) def test_resize_revert(): shell('resize-revert sample-server') assert_called('POST', '/servers/1234/action', {'revertResize': None}) @mock.patch('getpass.getpass', mock.Mock(return_value='p')) def test_root_password(): shell('root-password sample-server') assert_called('PUT', '/servers/1234', {'server': {'adminPass':'p'}}) def test_show(): shell('show 1234') # XXX need a way to test multiple calls # assert_called('GET', '/servers/1234') assert_called('GET', '/images/2') def test_delete(): shell('delete 1234') assert_called('DELETE', '/servers/1234') shell('delete sample-server') assert_called('DELETE', '/servers/1234') def test_help(): with mock.patch_object(_shell.parser, 'print_help') as m: shell('help') m.assert_called() with mock.patch_object(_shell.subcommands['delete'], 'print_help') as m: shell('help delete') m.assert_called() assert_raises(CommandError, shell, 'help foofoo')python-cloudservers-1.0a5/tests/test_shell.pyc000644 000765 000765 00000022162 11334072315 021077 0ustar00jacob000000 000000 tpKc @@sjddklZddkZddkZddklZddklZlZddk l Z dZ dZ d Z d Zd Zd Zd ZdZdZdZdZdZdZdZdZdZdZdZdZdZdZdZ ei!dei"ddd Z#d!Z$d"Z%d#Z&dS($i(tabsolute_importN(t assert_raises(tCloudserversShellt CommandErrori(t FakeServercC@sRhdd6dd6}ti|at_tatt_ddada dS(NtusernametCLOUD_SERVERS_USERNAMEtpasswordtCLOUD_SERVERS_API_KEYcS@stii|||S((t_shelltcst assert_called(tmtutb((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pytscS@sti|iS((R tmaintsplit(tcmd((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyRs( tostenviront_old_envRR Rt _api_classtNoneR tshell(tfake_env((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pytsetup s    cC@s tt_dS(N(RRR(((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pytteardownscC@stdtddtdtddhhtd6dd6d d 6d 6td tddhhtd6d d6dd 6d 6tdtddhhtd6dd6dd 6d 6dS(Nsbackup-schedule 1234tGETs/servers/1234/backup_schedules-backup-schedule sample-server --weekly mondaytPOSTtenabledtDISABLEDtdailytMONDAYtweeklytbackupSchedulesCbackup-schedule sample-server --weekly disabled --daily h_0000_0200t H_0000_0200s'backup-schedule sample-server --disable(RR tTruetFalse(((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_backup_schedules"     cC@stdtdddS(Nsbackup-schedule-delete 1234tDELETEs/servers/1234/backup_schedule(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_backup_schedule_delete7s cC@stdtddhhdd6dd6dd6d 6td tddhhdd6dd6dd6hd d 6d d6d6d 6dS(Nsboot --image 1 some-serverRs/serversitflavorIds some-servertnametimageIdtservers;boot --image 1 --meta foo=bar --meta spam=eggs some-server tbartfooteggstspamtmetadata(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyt test_boot;s # cC@stdtdddS(Ns flavor-listRs/flavors/detail(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_flavor_listIs cC@stdtdddS(Ns image-listRs/images/detail(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_image_listMs cC@s3tdtddhhdd6dd6d6dS( Ns$image-create sample-server new-imageRs/imagess new-imageR+itserverIdtimage(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_image_createQs cC@stdtdddS(Nsimage-delete 1R(s /images/1(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_image_deleteXs cC@s3tdtddhhdd6td6d6dS(Ns ip-share sample-server 1 1.2.3.4tPUTs /servers/1234/ips/public/1.2.3.4itsharedIpGroupIdtconfigureServertshareIp(RR R%(((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyt test_ip_share\s cC@stdtdddS(Ns ip-unshare sample-server 1.2.3.4R(s /servers/1234/ips/public/1.2.3.4(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_ip_unsharecs cC@stdtdddS(Ns ipgroup-listRs/shared_ip_groups/detail(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_ipgroup_listgs cC@s2tdtddtdtdddS(Nsipgroup-show 1Rs/shared_ip_groups/1sipgroup-show group2s/shared_ip_groups/detail(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_ipgroup_showks   cC@s[tdtddhhdd6d6tdtddhhdd6dd 6d6dS( Nsipgroup-create a-groupRs/shared_ip_groupssa-groupR+t sharedIpGroups$ipgroup-create a-group sample-serveriR-(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_ipgroup_creaters  cC@stdtdddS(Nsipgroup-delete group1R(s/shared_ip_groups/1(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_ipgroup_delete~s cC@stdtdddS(NtlistRs/servers/detail(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyt test_lists cC@sTtdtddhhdd6d6tdtddhhdd6d6dS( Nsreboot sample-serverRs/servers/1234/actiontSOFTttypetrebootsreboot sample-server --hardtHARD(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyt test_reboots  cC@s,tdtddhhdd6d6dS(Nsrebuild sample-server 1Rs/servers/1234/actioniR,trebuild(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyt test_rebuilds cC@s,tdtddhhdd6d6dS(Nsrename sample-server newnameR:s /servers/1234tnewnameR+R-(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyt test_renames cC@s,tdtddhhdd6d6dS(Nsresize sample-server 1Rs/servers/1234/actioniR*tresize(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyt test_resizes cC@s%tdtddhdd6dS(Nsresize-confirm sample-serverRs/servers/1234/actiont confirmResize(RR R(((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_resize_confirms cC@s%tdtddhdd6dS(Nsresize-revert sample-serverRs/servers/1234/actiont revertResize(RR R(((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_resize_reverts sgetpass.getpasst return_valuetpcC@s,tdtddhhdd6d6dS(Nsroot-password sample-serverR:s /servers/1234RWt adminPassR-(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyttest_root_passwords cC@stdtdddS(Ns show 1234Rs /images/2(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyt test_shows cC@s2tdtddtdtdddS(Ns delete 1234R(s /servers/1234sdelete sample-server(RR (((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyt test_deletes   c C@stitidii}z!|~}td|iWdQXtitiddii}z!|~}td|iWdQXt t tddS(Nt print_helpthelptdeletes help deletes help foofoo( tmockt patch_objectR tparsert__exit__t __enter__RR t subcommandsRR(t_[1]R t_[2]((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyt test_helps, 0 ('t __future__RRR_t nose.toolsRtcloudservers.shellRRt fakeserverRRRR'R)R3R4R5R8R9R>R?R@RARCRDRFRKRMRORQRSRUtpatchtMockRYRZR[Rg(((s6/Users/jacob/Projects/cloudservers/tests/test_shell.pyts>                       '  python-cloudservers-1.0a5/tests/._utils.py000644 000765 000024 00000000273 11310247377 021160 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRme##com.macromates.caret{ column = 71; line = 23; }python-cloudservers-1.0a5/tests/utils.py000644 000765 000765 00000001464 11310247377 017736 0ustar00jacob000000 000000 from nose.tools import ok_ def fail(msg): raise AssertionError(msg) def assert_in(thing, seq, msg=None): msg = msg or "'%s' not found in %s" % (thing, seq) ok_(thing in seq, msg) def assert_not_in(thing, seq, msg=None): msg = msg or "unexpected '%s' found in %s" % (thing, seq) ok_(thing not in seq, msg) def assert_has_keys(dict, required=[], optional=[]): keys = dict.keys() for k in required: assert_in(k, keys, "required key %s missing from %s" % (k, dict)) allowed_keys = set(required) | set(optional) extra_keys = set(keys).difference(set(required + optional)) if extra_keys: fail("found unexpected keys: %s" % list(extra_keys)) def assert_isinstance(thing, kls): ok_(isinstance(thing, kls), "%s is not an instance of %s" % (thing, kls))python-cloudservers-1.0a5/tests/utils.pyc000644 000765 000765 00000003144 11310247403 020064 0ustar00jacob000000 000000 N!Kc@sMddklZdZddZddZggdZdZdS(i(tok_cCst|dS(N(tAssertionError(tmsg((s1/Users/jacob/Projects/cloudservers/tests/utils.pytfailscCs.|pd||f}t||j|dS(Ns'%s' not found in %s(R(tthingtseqR((s1/Users/jacob/Projects/cloudservers/tests/utils.pyt assert_inscCs.|pd||f}t||j|dS(Nsunexpected '%s' found in %s(R(RRR((s1/Users/jacob/Projects/cloudservers/tests/utils.pyt assert_not_in scCs|i}x(|D] }t||d||fqWt|t|B}t|it||}|otdt|ndS(Nsrequired key %s missing from %ssfound unexpected keys: %s(tkeysRtsett differenceRtlist(tdicttrequiredtoptionalRtkt allowed_keyst extra_keys((s1/Users/jacob/Projects/cloudservers/tests/utils.pytassert_has_keyss cCs$tt||d||fdS(Ns%s is not an instance of %s(Rt isinstance(Rtkls((s1/Users/jacob/Projects/cloudservers/tests/utils.pytassert_isinstancesN(t nose.toolsRRtNoneRRRR(((s1/Users/jacob/Projects/cloudservers/tests/utils.pyts     python-cloudservers-1.0a5/python_cloudservers.egg-info/dependency_links.txt000644 000765 000765 00000000001 11337017726 026740 0ustar00jacob000000 000000 python-cloudservers-1.0a5/python_cloudservers.egg-info/entry_points.txt000644 000765 000765 00000000072 11337017726 026167 0ustar00jacob000000 000000 [console_scripts] cloudservers = cloudservers.shell:main python-cloudservers-1.0a5/python_cloudservers.egg-info/PKG-INFO000644 000765 000765 00000011654 11337017726 023776 0ustar00jacob000000 000000 Metadata-Version: 1.0 Name: python-cloudservers Version: 1.0a5 Summary: Client library for Rackspace's Cloud Servers API Home-page: http://packages.python.org/python-cloudservers Author: Jacob Kaplan-Moss Author-email: jacob@jacobian.org License: BSD Description: Python bindings to the Rackspace Cloud Servers API ================================================== This is a client for Rackspace's Cloud Servers API. There's a Python API (the ``cloudservers`` module), and a command-line script (``cloudservers``). Each implements 100% of the Rackspace API. You'll also probably want to read `Rackspace's API guide`__ (PDF) -- the first bit, at least -- to get an idea of the concepts. Rackspace is doing the cloud hosting thing a bit differently from Amazon, and if you get the concepts this library should make more sense. __ http://docs.rackspacecloud.com/servers/api/cs-devguide-latest.pdf Command-line API ---------------- Installing this package gets you a shell command, ``cloudservers``, that you can use to interact with Rackspace. You'll need to provide your Rackspace username and API key. You can do this with the ``--username`` and ``--apikey`` params, but it's easier to just set them as environment variables:: export CLOUD_SERVERS_USERNAME=jacobian export CLOUD_SERVERS_API_KEY=yadayada You'll find complete documentation on the shell by running ``cloudservers help``:: usage: cloudservers [--username USERNAME] [--apikey APIKEY] ... Command-line interface to the Cloud Servers API. Positional arguments: backup-schedule Show or edit the backup schedule for a server. backup-schedule-delete Delete the backup schedule for a server. boot Boot a new server. delete Immediately shut down and delete a server. flavor-list Print a list of available 'flavors' (sizes of servers). help Display help about this program or one of its subcommands. image-create Create a new image by taking a snapshot of a running server. image-delete Delete an image. image-list Print a list of available images to boot from. ip-share Share an IP address from the given IP group onto a server. ip-unshare Stop sharing an given address with a server. ipgroup-create Create a new IP group. ipgroup-delete Delete an IP group. ipgroup-list Show IP groups. ipgroup-show Show details about a particular IP group. list List active servers. reboot Reboot a server. rebuild Shutdown, re-image, and re-boot a server. rename Rename a server. resize Resize a server. resize-confirm Confirm a previous resize. resize-revert Revert a previous resize (and return to the previous VM). root-password Change the root password for a server. show Show details about the given server. Optional arguments: --username USERNAME Defaults to env[CLOUD_SERVERS_USERNAME]. --apikey APIKEY Defaults to env[CLOUD_SERVERS_API_KEY]. See "cloudservers help COMMAND" for help on a specific command. Python API ---------- `Documentation is available`__, but somewhat rudimentary -- it's only a reference. __ http://packages.python.org/python-cloudservers/ By way of a quick-start:: >>> import cloudservers >>> cs = cloudservers.CloudServers(USERNAME, API_KEY) >>> cs.flavors.list() [...] >>> cs.servers.list() [...] >>> s = cs.servers.create(image=2, flavor=1, name='myserver') ... time passes ... >>> s.reboot() ... time passes ... >>> s.delete() FAQ === What's wrong with libcloud? Nothing! However, as a cross-service binding it's by definition lowest common denominator; I needed access to the Rackspace-specific APIs (shared IP groups, image snapshots, resizing, etc.). I also wanted a command-line utility. Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python python-cloudservers-1.0a5/python_cloudservers.egg-info/requires.txt000644 000765 000765 00000000035 11337017726 025270 0ustar00jacob000000 000000 httplib2 argparse prettytablepython-cloudservers-1.0a5/python_cloudservers.egg-info/SOURCES.txt000644 000765 000765 00000002531 11337017727 024560 0ustar00jacob000000 000000 MANIFEST.in README.rst distribute_setup.py setup.cfg setup.py cloudservers/__init__.py cloudservers/backup_schedules.py cloudservers/base.py cloudservers/client.py cloudservers/exceptions.py cloudservers/flavors.py cloudservers/images.py cloudservers/ipgroups.py cloudservers/servers.py cloudservers/shell.py docs/.gitignore docs/Makefile docs/conf.py docs/index.rst docs/ref/backup_schedules.rst docs/ref/exceptions.rst docs/ref/flavors.rst docs/ref/images.rst docs/ref/index.rst docs/ref/ipgroups.rst docs/ref/servers.rst python_cloudservers.egg-info/PKG-INFO python_cloudservers.egg-info/SOURCES.txt python_cloudservers.egg-info/dependency_links.txt python_cloudservers.egg-info/entry_points.txt python_cloudservers.egg-info/requires.txt python_cloudservers.egg-info/top_level.txt tests/__init__.py tests/__init__.pyc tests/fakeserver.py tests/fakeserver.pyc tests/livetests.pyc tests/test_auth.py tests/test_auth.pyc tests/test_backup_schedules.py tests/test_backup_schedules.pyc tests/test_base.py tests/test_base.pyc tests/test_client.py tests/test_client.pyc tests/test_flavors.py tests/test_flavors.pyc tests/test_images.py tests/test_images.pyc tests/test_ipgroups.py tests/test_ipgroups.pyc tests/test_live.pyc tests/test_server_addresses.pyc tests/test_servers.py tests/test_servers.pyc tests/test_shell.py tests/test_shell.pyc tests/utils.py tests/utils.pycpython-cloudservers-1.0a5/python_cloudservers.egg-info/top_level.txt000644 000765 000765 00000000015 11337017726 025420 0ustar00jacob000000 000000 cloudservers python-cloudservers-1.0a5/docs/._.gitignore000644 000765 000024 00000000271 11307012040 021201 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRnV3!!com.macromates.caret{ column = 7; line = 0; }python-cloudservers-1.0a5/docs/.gitignore000644 000765 000765 00000000007 11307012040 017751 0ustar00jacob000000 000000 _build/python-cloudservers-1.0a5/docs/._conf.py000644 000765 000024 00000000273 11307014067 020525 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRnU##com.macromates.caret{ column = 21; line = 93; }python-cloudservers-1.0a5/docs/conf.py000644 000765 000765 00000014616 11307014067 017306 0ustar00jacob000000 000000 # -*- coding: utf-8 -*- # # python-cloudservers documentation build configuration file, created by # sphinx-quickstart on Sun Dec 6 14:19:25 2009. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(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'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'python-cloudservers' copyright = u'2009, Jacob Kaplan-Moss' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '1.0' # The full version, including alpha/beta/rc tags. release = '1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # 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 # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- 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 = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'python-cloudserversdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'python-cloudservers.tex', u'python-cloudservers Documentation', u'Jacob Kaplan-Moss', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} python-cloudservers-1.0a5/docs/._index.rst000644 000765 000024 00000000273 11307011401 021054 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRnU##com.macromates.caret{ column = 12; line = 13; }python-cloudservers-1.0a5/docs/index.rst000644 000765 000765 00000000735 11307011401 017632 0ustar00jacob000000 000000 .. python-cloudservers documentation master file, created by sphinx-quickstart on Sun Dec 6 14:19:25 2009. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to python-cloudservers's documentation! =============================================== Contents: .. toctree:: :maxdepth: 2 ref/index Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-cloudservers-1.0a5/docs/Makefile000644 000765 000765 00000006120 11307010715 017432 0ustar00jacob000000 000000 # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-cloudservers.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-cloudservers.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." python-cloudservers-1.0a5/docs/ref/000755 000765 000765 00000000000 11337017727 016564 5ustar00jacob000000 000000 python-cloudservers-1.0a5/docs/ref/._backup_schedules.rst000644 000765 000024 00000000272 11307046123 024035 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRnU""com.macromates.caret{ column = 16; line = 1; }python-cloudservers-1.0a5/docs/ref/backup_schedules.rst000644 000765 000765 00000003255 11307046123 022614 0ustar00jacob000000 000000 Backup schedules ================ .. currentmodule:: cloudservers Rackspace allows scheduling of weekly and/or daily backups for virtual servers. You can access these backup schedules either off the API object as :attr:`CloudServers.backup_schedules`, or directly off a particular :class:`Server` instance as :attr:`Server.backup_schedule`. Classes ------- .. autoclass:: BackupScheduleManager :members: create, delete, update, get .. autoclass:: BackupSchedule :members: update, delete .. attribute:: enabled Is this backup enabled? (boolean) .. attribute:: weekly The day of week upon which to perform a weekly backup. .. attribute:: daily The daily time period during which to perform a daily backup. Constants --------- Constants for selecting weekly backup days: .. data:: BACKUP_WEEKLY_DISABLED .. data:: BACKUP_WEEKLY_SUNDAY .. data:: BACKUP_WEEKLY_MONDAY .. data:: BACKUP_WEEKLY_TUESDAY .. data:: BACKUP_WEEKLY_WEDNESDA .. data:: BACKUP_WEEKLY_THURSDAY .. data:: BACKUP_WEEKLY_FRIDAY .. data:: BACKUP_WEEKLY_SATURDAY Constants for selecting hourly backup windows: .. data:: BACKUP_DAILY_DISABLED .. data:: BACKUP_DAILY_H_0000_0200 .. data:: BACKUP_DAILY_H_0200_0400 .. data:: BACKUP_DAILY_H_0400_0600 .. data:: BACKUP_DAILY_H_0600_0800 .. data:: BACKUP_DAILY_H_0800_1000 .. data:: BACKUP_DAILY_H_1000_1200 .. data:: BACKUP_DAILY_H_1200_1400 .. data:: BACKUP_DAILY_H_1400_1600 .. data:: BACKUP_DAILY_H_1600_1800 .. data:: BACKUP_DAILY_H_1800_2000 .. data:: BACKUP_DAILY_H_2000_2200 .. data:: BACKUP_DAILY_H_2200_0000 python-cloudservers-1.0a5/docs/ref/._exceptions.rst000644 000765 000024 00000000272 11307046136 022716 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRnWl""com.macromates.caret{ column = 10; line = 1; }python-cloudservers-1.0a5/docs/ref/exceptions.rst000644 000765 000765 00000000376 11307046136 021476 0ustar00jacob000000 000000 Exceptions ========== .. currentmodule:: cloudservers Exceptions ---------- Exceptions that the API might throw: .. automodule:: cloudservers :members: CloudServersException, BadRequest, Unauthorized, Forbidden, NotFound, OverLimit python-cloudservers-1.0a5/docs/ref/._flavors.rst000644 000765 000024 00000000271 11307046143 022206 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRnW!!com.macromates.caret{ column = 7; line = 1; }python-cloudservers-1.0a5/docs/ref/flavors.rst000644 000765 000765 00000001254 11307046143 020763 0ustar00jacob000000 000000 Flavors ======= From Rackspace's API documentation: A flavor is an available hardware configuration for a server. Each flavor has a unique combination of disk space, memory capacity and priority for CPU time. Classes ------- .. currentmodule:: cloudservers .. autoclass:: FlavorManager :members: get, list, find, findall .. autoclass:: Flavor :members: .. attribute:: id This flavor's ID. .. attribute:: name A human-readable name for this flavor. .. attribute:: ram The amount of RAM this flavor has, in MB. .. attribute:: disk The amount of disk space this flavor has, in MBpython-cloudservers-1.0a5/docs/ref/._images.rst000644 000765 000024 00000000271 11307046147 022003 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRnW!!com.macromates.caret{ column = 6; line = 1; }python-cloudservers-1.0a5/docs/ref/images.rst000644 000765 000765 00000002625 11307046147 020563 0ustar00jacob000000 000000 Images ====== .. currentmodule:: cloudservers An "image" is a snapshot from which you can create new server instances. From Rackspace's own API documentation: An image is a collection of files used to create or rebuild a server. Rackspace provides a number of pre-built OS images by default. You may also create custom images from cloud servers you have launched. These custom images are useful for backup purposes or for producing "gold" server images if you plan to deploy a particular server configuration frequently. Classes ------- .. autoclass:: ImageManager :members: get, list, find, findall, create, delete .. autoclass:: Image :members: delete .. attribute:: id This image's ID. .. attribute:: name This image's name. .. attribute:: created The date/time this image was created. .. attribute:: updated The date/time this instance was updated. .. attribute:: status The status of this image (usually ``"SAVING"`` or ``ACTIVE``). .. attribute:: progress During saving of an image this'll be set to something between 0 and 100, representing a rough percentage done. .. attribute:: serverId If this image was created from a :class:`Server` then this attribute will be set to the ID of the server whence this image came.python-cloudservers-1.0a5/docs/ref/._index.rst000644 000765 000024 00000000273 11307046102 021636 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRnU##com.macromates.caret{ column = 10; line = 11; }python-cloudservers-1.0a5/docs/ref/index.rst000644 000765 000765 00000000203 11307046102 020402 0ustar00jacob000000 000000 Reference ========= .. toctree:: :maxdepth: 1 backup_schedules exceptions flavors images ipgroups serverspython-cloudservers-1.0a5/docs/ref/._ipgroups.rst000644 000765 000024 00000000273 11307043646 022411 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRn\x##com.macromates.caret{ column = 49; line = 21; }python-cloudservers-1.0a5/docs/ref/ipgroups.rst000644 000765 000765 00000002650 11307043646 021165 0ustar00jacob000000 000000 Shared IP addresses =================== From the Rackspace API guide: Public IP addresses can be shared across multiple servers for use in various high availability scenarios. When an IP address is shared to another server, the cloud network restrictions are modified to allow each server to listen to and respond on that IP address (you may optionally specify that the target server network configuration be modified). Shared IP addresses can be used with many standard heartbeat facilities (e.g. ``keepalived``) that monitor for failure and manage IP failover. A shared IP group is a collection of servers that can share IPs with other members of the group. Any server in a group can share one or more public IPs with any other server in the group. With the exception of the first server in a shared IP group, servers must be launched into shared IP groups. A server may only be a member of one shared IP group. .. seealso:: Use :meth:`Server.share_ip` and `Server.unshare_ip` to share and unshare IPs in a group. Classes ------- .. currentmodule:: cloudservers .. autoclass:: IPGroupManager :members: get, list, find, findall, create, delete .. autoclass:: IPGroup :members: delete .. attribute:: id Shared group ID. .. attribute:: name Name of the group. .. attribute:: servers A list of server IDs in this group.python-cloudservers-1.0a5/docs/ref/._servers.rst000644 000765 000024 00000000273 11307047130 022222 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRn] ##com.macromates.caret{ column = 21; line = 72; }python-cloudservers-1.0a5/docs/ref/servers.rst000644 000765 000765 00000003364 11307047130 021001 0ustar00jacob000000 000000 Servers ======= A virtual machine instance. Classes ------- .. currentmodule:: cloudservers .. autoclass:: ServerManager :members: get, list, find, findall, create, update, delete, share_ip, unshare_ip, reboot, rebuild, resize, confirm_resize, revert_resize .. autoclass:: Server :members: update, delete, share_ip, unshare_ip, reboot, rebuild, resize, confirm_resize, revert_resize .. attribute:: id This server's ID. .. attribute:: name The name you gave the server when you booted it. .. attribute:: imageId The :class:`Image` this server was booted with. .. attribute:: flavorId This server's current :class:`Flavor`. .. attribute:: hostId Rackspace doesn't document this value. It appears to be SHA1 hash. .. attribute:: status The server's status (``BOOTING``, ``ACTIVE``, etc). .. attribute:: progress When booting, resizing, updating, etc., this will be set to a value between 0 and 100 giving a rough estimate of the progress of the current operation. .. attribute:: addresses The public and private IP addresses of this server. This'll be a dict of the form:: { "public" : ["67.23.10.138"], "private" : ["10.176.42.19"] } You *can* get more than one public/private IP provisioned, but not directly from the API; you'll need to open a support ticket. .. attribute:: metadata The metadata dict you gave when creating the server. Constants --------- Reboot types: .. data:: REBOOT_SOFT .. data:: REBOOT_HARDpython-cloudservers-1.0a5/cloudservers/.___init__.py000644 000765 000024 00000000273 11307050327 023126 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRbQ##com.macromates.caret{ column = 37; line = 31; }python-cloudservers-1.0a5/cloudservers/__init__.py000644 000765 000765 00000004225 11307050327 021702 0ustar00jacob000000 000000 from __future__ import absolute_import __version__ = ('1.0a1') from .backup_schedules import (BackupSchedule, BackupScheduleManager, BACKUP_WEEKLY_DISABLED, BACKUP_WEEKLY_SUNDAY, BACKUP_WEEKLY_MONDAY, BACKUP_WEEKLY_TUESDAY, BACKUP_WEEKLY_WEDNESDAY, BACKUP_WEEKLY_THURSDAY, BACKUP_WEEKLY_FRIDAY, BACKUP_WEEKLY_SATURDAY, BACKUP_DAILY_DISABLED, BACKUP_DAILY_H_0000_0200, BACKUP_DAILY_H_0200_0400, BACKUP_DAILY_H_0400_0600, BACKUP_DAILY_H_0600_0800, BACKUP_DAILY_H_0800_1000, BACKUP_DAILY_H_1000_1200, BACKUP_DAILY_H_1200_1400, BACKUP_DAILY_H_1400_1600, BACKUP_DAILY_H_1600_1800, BACKUP_DAILY_H_1800_2000, BACKUP_DAILY_H_2000_2200, BACKUP_DAILY_H_2200_0000) from .client import CloudServersClient from .exceptions import (CloudServersException, BadRequest, Unauthorized, Forbidden, NotFound, OverLimit) from .flavors import FlavorManager, Flavor from .images import ImageManager, Image from .ipgroups import IPGroupManager, IPGroup from .servers import ServerManager, Server, REBOOT_HARD, REBOOT_SOFT class CloudServers(object): """ Top-level object to access the Rackspace Cloud Servers API. Create an instance with your creds:: >>> cs = CloudServers(USERNAME, API_KEY) Then call methods on its managers:: >>> cs.servers.list() ... >>> cs.flavors.list() ... &c. """ def __init__(self, username, apikey): self.backup_schedules = BackupScheduleManager(self) self.client = CloudServersClient(username, apikey) self.flavors = FlavorManager(self) self.images = ImageManager(self) self.ipgroups = IPGroupManager(self) self.servers = ServerManager(self) def authenticate(self): """ Authenticate against the server. Normally this is called automatically when you first access the API, but you can call this method to force authentication right now. Returns on success; raises :class:`cloudservers.Unauthorized` if the credentials are wrong. """ self.client.authenticate()python-cloudservers-1.0a5/cloudservers/._backup_schedules.py000644 000765 000024 00000000273 11310222471 024666 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRn;##com.macromates.caret{ column = 52; line = 41; }python-cloudservers-1.0a5/cloudservers/backup_schedules.py000644 000765 000765 00000006324 11310222471 023444 0ustar00jacob000000 000000 from . import base BACKUP_WEEKLY_DISABLED = 'DISABLED' BACKUP_WEEKLY_SUNDAY = 'SUNDAY' BACKUP_WEEKLY_MONDAY = 'MONDAY' BACKUP_WEEKLY_TUESDAY = 'TUESDAY' BACKUP_WEEKLY_WEDNESDAY = 'WEDNESDAY' BACKUP_WEEKLY_THURSDAY = 'THURSDAY' BACKUP_WEEKLY_FRIDAY = 'FRIDAY' BACKUP_WEEKLY_SATURDAY = 'SATURDAY' BACKUP_DAILY_DISABLED = 'DISABLED' BACKUP_DAILY_H_0000_0200 = 'H_0000_0200' BACKUP_DAILY_H_0200_0400 = 'H_0200_0400' BACKUP_DAILY_H_0400_0600 = 'H_0400_0600' BACKUP_DAILY_H_0600_0800 = 'H_0600_0800' BACKUP_DAILY_H_0800_1000 = 'H_0800_1000' BACKUP_DAILY_H_1000_1200 = 'H_1000_1200' BACKUP_DAILY_H_1200_1400 = 'H_1200_1400' BACKUP_DAILY_H_1400_1600 = 'H_1400_1600' BACKUP_DAILY_H_1600_1800 = 'H_1600_1800' BACKUP_DAILY_H_1800_2000 = 'H_1800_2000' BACKUP_DAILY_H_2000_2200 = 'H_2000_2200' BACKUP_DAILY_H_2200_0000 = 'H_2200_0000' class BackupSchedule(base.Resource): """ Represents the daily or weekly backup schedule for some server. """ def get(self): """ Get this `BackupSchedule` again from the API. """ return self.manager.get(server=self.server) def delete(self): """ Delete (i.e. disable and remove) this scheduled backup. """ self.manager.delete(server=self.server) def update(self, enabled=True, weekly=BACKUP_WEEKLY_DISABLED, daily=BACKUP_DAILY_DISABLED): """ Update this backup schedule. See :meth:`BackupScheduleManager.create` for details. """ self.manager.create(self.server, enabled, weekly, daily) class BackupScheduleManager(base.Manager): """ Manage server backup schedules. """ resource_class = BackupSchedule def get(self, server): """ Get the current backup schedule for a server. :arg server: The server (or its ID). :rtype: :class:`BackupSchedule` """ s = base.getid(server) schedule = self._get('/servers/%s/backup_schedule' % s, 'backupSchedule') schedule.server = server return schedule # Backup schedules use POST for both create and update, so allow both here. # Unlike the rest of the API, POST here returns no body, so we can't use the # nice little helper methods. def create(self, server, enabled=True, weekly=BACKUP_WEEKLY_DISABLED, daily=BACKUP_DAILY_DISABLED): """ Create or update the backup schedule for the given server. :arg server: The server (or its ID). :arg enabled: boolean; should this schedule be enabled? :arg weekly: Run a weekly backup on this day (one of the `BACKUP_WEEKLY_*` constants) :arg daily: Run a daily backup at this time (one of the `BACKUP_DAILY_*` constants) """ s = base.getid(server) body = {'backupSchedule': { 'enabled': enabled, 'weekly': weekly, 'daily': daily }} self.api.client.post('/servers/%s/backup_schedule' % s, body=body) update = create def delete(self, server): """ Remove the scheduled backup for `server`. :arg server: The server (or its ID). """ s = base.getid(server) self._delete('/servers/%s/backup_schedule' % s)python-cloudservers-1.0a5/cloudservers/._base.py000644 000765 000024 00000000272 11310240764 022300 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRn>""com.macromates.caret{ column = 32; line = 4; }python-cloudservers-1.0a5/cloudservers/base.py000644 000765 000765 00000007053 11310240764 021057 0ustar00jacob000000 000000 """ Base utilities to build API operation managers and objects on top of. """ from .exceptions import NotFound class Manager(object): """ Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. """ resource_class = None def __init__(self, api): self.api = api def _list(self, url, response_key): resp, body = self.api.client.get(url) return [self.resource_class(self, res) for res in body[response_key]] def _get(self, url, response_key): resp, body = self.api.client.get(url) return self.resource_class(self, body[response_key]) def _create(self, url, body, response_key): resp, body = self.api.client.post(url, body=body) return self.resource_class(self, body[response_key]) def _delete(self, url): resp, body = self.api.client.delete(url) def _update(self, url, body): resp, body = self.api.client.put(url, body=body) class ManagerWithFind(Manager): """ Like a `Manager`, but with additional `find()`/`findall()` methods. """ def find(self, **kwargs): """ Find a single item with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ rl = self.findall(**kwargs) try: return rl[0] except IndexError: raise NotFound(404, "No %s matching %s." % (self.resource_class.__name__, kwargs)) def findall(self, **kwargs): """ Find all items with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ found = [] searches = kwargs.items() for obj in self.list(): try: if all(getattr(obj, attr) == value for (attr, value) in searches): found.append(obj) except AttributeError: continue return found class Resource(object): """ A resource represents a particular instance of an object (server, flavor, etc). This is pretty much just a bag for attributes. """ def __init__(self, manager, info): self.manager = manager self._info = info self._add_details(info) def _add_details(self, info): for (k, v) in info.iteritems(): setattr(self, k, v) def __getattr__(self, k): self.get() if k not in self.__dict__: raise AttributeError(k) else: return self.__dict__[k] def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) def get(self): new = self.manager.get(self.id) self._add_details(new._info) def __eq__(self, other): if not isinstance(other, self.__class__): return False if hasattr(self, 'id') and hasattr(other, 'id'): return self.id == other.id return self._info == other._info def getid(obj): """ Abstracts the common pattern of allowing both an object or an object's ID (integer) as a parameter when dealing with relationships. """ try: return obj.id except AttributeError: return int(obj)python-cloudservers-1.0a5/cloudservers/._client.py000644 000765 000024 00000000273 11306763127 022655 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRei&##com.macromates.caret{ column = 47; line = 27; }python-cloudservers-1.0a5/cloudservers/client.py000644 000765 000765 00000005137 11306763127 021434 0ustar00jacob000000 000000 import httplib2 try: import json except ImportError: import simplejson as json import cloudservers from . import exceptions class CloudServersClient(httplib2.Http): AUTH_URL = 'https://auth.api.rackspacecloud.com/v1.0' USER_AGENT = 'python-cloudservers/%s' % cloudservers.__version__ def __init__(self, user, apikey): super(CloudServersClient, self).__init__() self.user = user self.apikey = apikey self.management_url = None self.auth_token = None # httplib2 overrides self.force_exception_to_status_code = True def request(self, *args, **kwargs): kwargs.setdefault('headers', {}) kwargs['headers']['User-Agent'] = self.USER_AGENT if 'body' in kwargs: kwargs['headers']['Content-Type'] = 'application/json' kwargs['body'] = json.dumps(kwargs['body']) resp, body = super(CloudServersClient, self).request(*args, **kwargs) body = json.loads(body) if body else None if resp.status in (400, 401, 403, 404, 413, 500): raise exceptions.from_response(resp, body) return resp, body def _cs_request(self, url, method, **kwargs): if not self.management_url: self.authenticate() # Perform the request once. If we get a 401 back then it # might be because the auth token expired, so try to # re-authenticate and try again. If it still fails, bail. try: kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token resp, body = self.request(self.management_url + url, method, **kwargs) return resp, body except exceptions.Unauthorized, ex: try: self.authenticate() resp, body = self.request(self.management_url + url, method, **kwargs) return resp, body except exceptions.Unauthorized: raise ex def get(self, url, **kwargs): return self._cs_request(url, 'GET', **kwargs) def post(self, url, **kwargs): return self._cs_request(url, 'POST', **kwargs) def put(self, url, **kwargs): return self._cs_request(url, 'PUT', **kwargs) def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) def authenticate(self): headers = {'X-Auth-User': self.user, 'X-Auth-Key': self.apikey} resp, body = self.request(self.AUTH_URL, 'GET', headers=headers) self.management_url = resp['x-server-management-url'] self.auth_token = resp['x-auth-token']python-cloudservers-1.0a5/cloudservers/._exceptions.py000644 000765 000024 00000000272 11307014602 023543 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRej""com.macromates.caret{ column = 68; line = 2; }python-cloudservers-1.0a5/cloudservers/exceptions.py000644 000765 000765 00000003566 11307014602 022327 0ustar00jacob000000 000000 class CloudServersException(Exception): """ The base exception class for all exceptions this library raises. """ def __init__(self, code, message=None, details=None): self.code = code self.message = message or self.__class__.message self.details = details def __str__(self): return "%s (HTTP %s)" % (self.message, self.code) class BadRequest(CloudServersException): """ HTTP 400 - Bad request: you sent some malformed data. """ http_status = 400 message = "Bad request" class Unauthorized(CloudServersException): """ HTTP 401 - Unauthorized: bad credentials. """ http_status = 401 message = "Unauthorized" class Forbidden(CloudServersException): """ HTTP 403 - Forbidden: your credentials don't give you access to this resource. """ http_status = 403 message = "Forbidden" class NotFound(CloudServersException): """ HTTP 404 - Not found """ http_status = 404 message = "Not found" class OverLimit(CloudServersException): """ HTTP 413 - Over limit: you're over the API limits for this time period. """ http_status = 413 message = "Over limit" _code_map = dict((c.http_status, c) for c in CloudServersException.__subclasses__()) def from_response(response, body): """ Return an instance of a CloudServersException or subclass based on an httplib2 response. Usage:: resp, body = http.request(...) if resp.status != 200: raise exception_from_response(resp, body) """ cls = _code_map.get(response.status, CloudServersException) if body: error = body[body.keys()[0]] return cls(code=response.status, message=error.get('message', None), details=error.get('details', None)) else: return cls(code=response.status)python-cloudservers-1.0a5/cloudservers/._flavors.py000644 000765 000024 00000000273 11307015436 023045 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRe##com.macromates.caret{ column = 31; line = 28; }python-cloudservers-1.0a5/cloudservers/flavors.py000644 000765 000765 00000001410 11307015436 021612 0ustar00jacob000000 000000 from . import base class Flavor(base.Resource): """ A flavor is an available hardware configuration for a server. """ def __repr__(self): return "" % self.name class FlavorManager(base.ManagerWithFind): """ Manage :class:`Flavor` resources. """ resource_class = Flavor def list(self): """ Get a list of all flavors. :rtype: list of :class:`Flavor`. """ return self._list("/flavors/detail", "flavors") def get(self, flavor): """ Get a specific flavor. :param flavor: The ID of the :class:`Flavor` to get. :rtype: :class:`Flavor` """ return self._get("/flavors/%s" % base.getid(flavor), "flavor")python-cloudservers-1.0a5/cloudservers/._images.py000644 000765 000024 00000000273 11310515302 022625 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRi9##com.macromates.caret{ column = 54; line = 58; }python-cloudservers-1.0a5/cloudservers/images.py000644 000765 000765 00000003127 11310515302 021401 0ustar00jacob000000 000000 from . import base class Image(base.Resource): """ An image is a collection of files used to create or rebuild a server. """ def __repr__(self): return "" % self.name def delete(self): """ Delete this image. """ return self.manager.delete(self) class ImageManager(base.ManagerWithFind): """ Manage :class:`Image` resources. """ resource_class = Image def get(self, image): """ Get an image. :param image: The ID of the image to get. :rtype: :class:`Image` """ return self._get("/images/%s" % base.getid(image), "image") def list(self): """ Get a list of all images. :rtype: list of :class:`Image` """ return self._list("/images/detail", "images") def create(self, name, server): """ Create a new image by snapshotting a running :class:`Server` :param name: An (arbitrary) name for the new image. :param server: The :class:`Server` (or its ID) to make a snapshot of. :rtype: :class:`Image` """ data = {"image": {"serverId": base.getid(server), "name": name}} return self._create("/images", data, "image") def delete(self, image): """ Delete an image. It should go without saying that you can't delete an image that you didn't create. :param image: The :class:`Image` (or its ID) to delete. """ self._delete("/images/%s" % base.getid(image))python-cloudservers-1.0a5/cloudservers/._ipgroups.py000644 000765 000024 00000000273 11310242114 023226 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRm&##com.macromates.caret{ column = 46; line = 42; }python-cloudservers-1.0a5/cloudservers/ipgroups.py000644 000765 000765 00000002725 11310242114 022005 0ustar00jacob000000 000000 from . import base class IPGroup(base.Resource): def __repr__(self): return "" % self.name def delete(self): """ Delete this group. """ self.manager.delete(self) class IPGroupManager(base.ManagerWithFind): resource_class = IPGroup def list(self): """ Get a list of all groups. :rtype: list of :class:`IPGroup` """ return self._list("/shared_ip_groups/detail", "sharedIpGroups") def get(self, group): """ Get an IP group. :param group: ID of the image to get. :rtype: :class:`IPGroup` """ return self._get("/shared_ip_groups/%s" % base.getid(group), "sharedIpGroup") def create(self, name, server=None): """ Create a new :class:`IPGroup` :param name: An (arbitrary) name for the new image. :param server: A :class:`Server` (or its ID) to make a member of this group. :rtype: :class:`IPGroup` """ data = {"sharedIpGroup": {"name": name}} if server: data['sharedIpGroup']['server'] = base.getid(server) return self._create('/shared_ip_groups', data, "sharedIpGroup") def delete(self, group): """ Delete a group. :param group: The :class:`IPGroup` (or its ID) to delete. """ self._delete("/shared_ip_groups/%s" % base.getid(group)) python-cloudservers-1.0a5/cloudservers/._servers.py000644 000765 000024 00000000274 11334072426 023065 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRhK$$com.macromates.caret{ column = 38; line = 104; }python-cloudservers-1.0a5/cloudservers/servers.py000644 000765 000765 00000023554 11334072426 021646 0ustar00jacob000000 000000 from . import base REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' class Server(base.Resource): def __repr__(self): return "" % self.name def delete(self): """ Delete (i.e. shut down and delete the image) this server. """ self.manager.delete(self) def update(self, name=None, password=None): """ Update the name or the password for this server. :param name: Update the server's name. :param password: Update the root password. """ self.manager.update(self, name, password) def share_ip(self, ipgroup, address, configure=True): """ Share an IP address from the given IP group onto this server. :param ipgroup: The :class:`IPGroup` that the given address belongs to. :param address: The IP address to share. :param configure: If ``True``, the server will be automatically configured to use this IP. I don't know why you'd want this to be ``False``. """ self.manager.share_ip(self, ipgroup, address, configure) def unshare_ip(self, address): """ Stop sharing the given address. :param address: The IP address to stop sharing. """ self.manager.unshare_ip(self, address) def reboot(self, type=REBOOT_SOFT): """ Reboot the server. :param type: either :data:`REBOOT_SOFT` for a software-level reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. """ self.manager.reboot(self, type) def rebuild(self, image): """ Rebuild -- shut down and then re-image -- this server. :param image: the :class:`Image` (or its ID) to re-image with. """ self.manager.rebuild(self, image) def resize(self, flavor): """ Resize the server's resources. :param flavor: the :class:`Flavor` (or its ID) to resize to. Until a resize event is confirmed with :meth:`confirm_resize`, the old server will be kept around and you'll be able to roll back to the old flavor quickly with :meth:`revert_resize`. All resizes are automatically confirmed after 24 hours. """ self.manager.resize(self, flavor) def confirm_resize(self): """ Confirm that the resize worked, thus removing the original server. """ self.manager.confirm_resize(self) def revert_resize(self): """ Revert a previous resize, switching back to the old server. """ self.manager.revert_resize(self) @property def backup_schedule(self): """ This server's :class:`BackupSchedule`. """ return self.manager.api.backup_schedules.get(self) @property def public_ip(self): """ Shortcut to get this server's primary public IP address. """ return self.addresses['public'][0] @property def private_ip(self): """ Shortcut to get this server's primary private IP address. """ return self.addresses['private'][0] class ServerManager(base.ManagerWithFind): resource_class = Server def get(self, server): """ Get a server. :param server: ID of the :class:`Server` to get. :rtype: :class:`Server` """ return self._get("/servers/%s" % base.getid(server), "server") def list(self): """ Get a list of servers. :rtype: list of :class:`Server` """ return self._list("/servers/detail", "servers") def create(self, name, image, flavor, ipgroup=None, meta=None, files=None): """ Create (boot) a new server. :param name: Something to name the server. :param image: The :class:`Image` to boot with. :param flavor: The :class:`Flavor` to boot onto. :param ipgroup: An initial :class:`IPGroup` for this server. :param meta: A dict of arbitrary key/value metadata to store for this server. A maximum of five entries is allowed, and both keys and values must be 255 characters or less. :param files: A dict of files to overrwrite on the server upon boot. Keys are file names (i.e. ``/etc/passwd``) and values are the file contents (either as a string or as a file-like object). A maximum of five entries is allowed, and each file must be 10k or less. There's a bunch more info about how a server boots in Rackspace's official API docs, page 23. """ body = {"server": { "name": name, "imageId": base.getid(image), "flavorId": base.getid(flavor), }} if ipgroup: body["server"]["sharedIpGroupId"] = base.getid(ipgroup) if meta: body["server"]["metadata"] = meta # Files are a slight bit tricky. They're passed in a "personality" # list to the POST. Each item is a dict giving a file name and the # base64-encoded contents of the file. We want to allow passing # either an open file *or* some contents as files here. if files: personality = body['server']['personality'] = [] for filepath, file_or_string in files.items(): if hasattr(file_or_string, 'read'): data = file_or_string.read() else: data = file_or_string personality.append({ 'path': filepath, 'contents': data.encode('base64'), }) return self._create("/servers", body, "server") def update(self, server, name=None, password=None): """ Update the name or the password for a server. :param server: The :class:`Server` (or its ID) to update. :param name: Update the server's name. :param password: Update the root password. """ if name is None and password is None: return body = {"server": {}} if name: body["server"]["name"] = name if password: body["server"]["adminPass"] = password self._update("/servers/%s" % base.getid(server), body) def delete(self, server): """ Delete (i.e. shut down and delete the image) this server. """ self._delete("/servers/%s" % base.getid(server)) def share_ip(self, server, ipgroup, address, configure=True): """ Share an IP address from the given IP group onto a server. :param server: The :class:`Server` (or its ID) to share onto. :param ipgroup: The :class:`IPGroup` that the given address belongs to. :param address: The IP address to share. :param configure: If ``True``, the server will be automatically configured to use this IP. I don't know why you'd want this to be ``False``. """ server = base.getid(server) ipgroup = base.getid(ipgroup) body = {'shareIp': {'sharedIpGroupId': ipgroup, 'configureServer': configure}} self._update("/servers/%s/ips/public/%s" % (server, address), body) def unshare_ip(self, server, address): """ Stop sharing the given address. :param server: The :class:`Server` (or its ID) to share onto. :param address: The IP address to stop sharing. """ server = base.getid(server) self._delete("/servers/%s/ips/public/%s" % (server, address)) def reboot(self, server, type=REBOOT_SOFT): """ Reboot a server. :param server: The :class:`Server` (or its ID) to share onto. :param type: either :data:`REBOOT_SOFT` for a software-level reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. """ self._action('reboot', server, {'type':type}) def rebuild(self, server, image): """ Rebuild -- shut down and then re-image -- a server. :param server: The :class:`Server` (or its ID) to share onto. :param image: the :class:`Image` (or its ID) to re-image with. """ self._action('rebuild', server, {'imageId': base.getid(image)}) def resize(self, server, flavor): """ Resize a server's resources. :param server: The :class:`Server` (or its ID) to share onto. :param flavor: the :class:`Flavor` (or its ID) to resize to. Until a resize event is confirmed with :meth:`confirm_resize`, the old server will be kept around and you'll be able to roll back to the old flavor quickly with :meth:`revert_resize`. All resizes are automatically confirmed after 24 hours. """ self._action('resize', server, {'flavorId': base.getid(flavor)}) def confirm_resize(self, server): """ Confirm that the resize worked, thus removing the original server. :param server: The :class:`Server` (or its ID) to share onto. """ self._action('confirmResize', server) def revert_resize(self, server): """ Revert a previous resize, switching back to the old server. :param server: The :class:`Server` (or its ID) to share onto. """ self._action('revertResize', server) def _action(self, action, server, info=None): """ Perform a server "action" -- reboot/rebuild/resize/etc. """ self.api.client.post('/servers/%s/action' % base.getid(server), body={action: info}) python-cloudservers-1.0a5/cloudservers/._shell.py000644 000765 000024 00000000274 11334072450 022500 0ustar00jacobstaff000000 000000 Mac OS X  2ATTRp]$$com.macromates.caret{ column = 52; line = 323; }python-cloudservers-1.0a5/cloudservers/shell.py000644 000765 000765 00000037226 11334072450 021262 0ustar00jacob000000 000000 """ Command-line interface to the Cloud Servers API. """ import argparse import cloudservers import getpass import httplib2 import os import prettytable import sys import textwrap # Choices for flags. DAY_CHOICES = [getattr(cloudservers, i).lower() for i in dir(cloudservers) if i.startswith('BACKUP_WEEKLY_')] HOUR_CHOICES = [getattr(cloudservers, i).lower() for i in dir(cloudservers) if i.startswith('BACKUP_DAILY_')] def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) # Decorator for args def arg(*args, **kwargs): def _decorator(func): # Because of the sematics of decorator composition if we just append # to the options list positional options will appear to be backwards. func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) return func return _decorator class CommandError(Exception): pass def env(e): return os.environ.get(e, '') class CloudserversShell(object): # Hook for the test suite to inject a fake server. _api_class = cloudservers.CloudServers def __init__(self): self.parser = argparse.ArgumentParser( prog = 'cloudservers', description = __doc__.strip(), epilog = 'See "cloudservers help COMMAND" for help on a specific command.', add_help = False, formatter_class = CloudserversHelpFormatter, ) # Global arguments self.parser.add_argument('-h', '--help', action = 'help', help = argparse.SUPPRESS, ) self.parser.add_argument('--debug', default = False, action = 'store_true', help = argparse.SUPPRESS) self.parser.add_argument('--username', default = env('CLOUD_SERVERS_USERNAME'), help = 'Defaults to env[CLOUD_SERVERS_USERNAME].') self.parser.add_argument('--apikey', default = env('CLOUD_SERVERS_API_KEY'), help='Defaults to env[CLOUD_SERVERS_API_KEY].') # Subcommands subparsers = self.parser.add_subparsers(metavar='') self.subcommands = {} # Everything that's do_* is a subcommand. for attr in (a for a in dir(self) if a.startswith('do_')): # I prefer to be hypen-separated instead of underscores. command = attr[3:].replace('_', '-') callback = getattr(self, attr) desc = callback.__doc__ or '' help = desc.strip().split('\n')[0] arguments = getattr(callback, 'arguments', []) subparser = subparsers.add_parser(command, help = help, description = desc, add_help=False, formatter_class = CloudserversHelpFormatter ) subparser.add_argument('-h', '--help', action = 'help', help = argparse.SUPPRESS, ) self.subcommands[command] = subparser for (args, kwargs) in arguments: subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=callback) def main(self, argv): # Parse args and call whatever callback was selected args = self.parser.parse_args(argv) # Short-circuit and deal with help right away. if args.func == self.do_help: self.do_help(args) return 0 # Deal with global arguments if args.debug: httplib2.debuglevel = 1 user, apikey = args.username, args.apikey if not user: raise CommandError("You must provide a username, either via " "--username or via env[CLOUD_SERVERS_USERNAME]") if not apikey: raise CommandError("You must provide an API key, either via " "--apikey or via env[CLOUD_SERVERS_API_KEY]") self.cs = self._api_class(user, apikey) try: self.cs.authenticate() except cloudservers.Unauthorized: raise CommandError("Invalid Cloud Servers credentials.") args.func(args) @arg('command', metavar='', nargs='?', help='Display help for ') def do_help(self, args): """ Display help about this program or one of its subcommands. """ if args.command: if args.command in self.subcommands: self.subcommands[args.command].print_help() else: raise CommandError("'%s' is not a valid subcommand." % args.command) else: self.parser.print_help() @arg('server', metavar='', help='Name or ID of server.') @arg('--enable', dest='enabled', default=None, action='store_true', help='Enable backups.') @arg('--disable', dest='enabled', action='store_false', help='Disable backups.') @arg('--weekly', metavar='', choices=DAY_CHOICES, help='Schedule a weekly backup for (one of: %s).' % pretty_choice_list(DAY_CHOICES)) @arg('--daily', metavar='', choices=HOUR_CHOICES, help='Schedule a daily backup during (one of: %s).' % pretty_choice_list(HOUR_CHOICES)) def do_backup_schedule(self, args): """ Show or edit the backup schedule for a server. With no flags, the backup schedule will be shown. If flags are given, the backup schedule will be modified accordingly. """ server = self._find_server(args.server) # If we have some flags, update the backup backup = {} if args.daily: backup['daily'] = getattr(cloudservers, 'BACKUP_DAILY_%s' % args.daily.upper()) if args.weekly: backup['weekly'] = getattr(cloudservers, 'BACKUP_WEEKLY_%s' % args.weekly.upper()) if args.enabled is not None: backup['enabled'] = args.enabled if backup: server.backup_schedule.update(**backup) else: print_dict(server.backup_schedule._info) @arg('server', metavar='', help='Name or ID of server.') def do_backup_schedule_delete(self, args): """ Delete the backup schedule for a server. """ server = self._find_server(args.server) server.backup_schedule.delete() @arg('--flavor', default = None, metavar = '', help = "Flavor ID (see 'cloudservers flavors'). Defaults to 256MB RAM instance.") @arg('--image', default = None, metavar = '', help = "Image ID (see 'cloudservers images'). Defaults to Ubuntu 9.10.") @arg('--ipgroup', default = None, metavar = '', help = "IP group ID (see 'cloudservers ipgroups').") @arg('--meta', metavar = "", action = 'append', help = "Record arbitrary key/value metadata.") @arg('name', metavar='', help='Name for the new server') def do_boot(self, args): """Boot a new server.""" flavor = args.flavor or self.cs.flavors.find(ram=256) image = args.image or self.cs.images.find(name="Ubuntu 9.10 (karmic)") if args.meta: metadata = dict(v.split('=') for v in args.meta) else: metadata = None server = self.cs.servers.create(args.name, image, flavor, args.ipgroup, metadata) print_dict(server._info) def do_flavor_list(self, args): """Print a list of available 'flavors' (sizes of servers).""" print_list(self.cs.flavors.list(), ['ID', 'Name', 'RAM', 'Disk']) def do_image_list(self, args): """Print a list of available images to boot from.""" print_list(self.cs.images.list(), ['ID', 'Name', 'Status']) @arg('server', metavar='', help='Name or ID of server.') @arg('name', metavar='', help='Name for the new image.') def do_image_create(self, args): """Create a new image by taking a snapshot of a running server.""" server = self._find_server(args.server) image = self.cs.images.create(args.name, server) print_dict(image._info) @arg('image', metavar='', help='Name or ID of image.') def do_image_delete(self, args): """ Delete an image. It should go without saying, but you cn only delete images you created. """ image = self._find_image(args.image) image.delete() @arg('server', metavar='', help='Name or ID of server.') @arg('group', metavar='', help='Name or ID of group.') @arg('address', metavar='
', help='IP address to share.') def do_ip_share(self, args): """Share an IP address from the given IP group onto a server.""" server = self._find_server(args.server) group = self._find_ipgroup(args.group) server.share_ip(group, args.address) @arg('server', metavar='', help='Name or ID of server.') @arg('address', metavar='
', help='Shared IP address to remove from the server.') def do_ip_unshare(self, args): """Stop sharing an given address with a server.""" server = self._find_server(args.server) server.unshare_ip(args.address) def do_ipgroup_list(self, args): """Show IP groups.""" print_list(self.cs.ipgroups.list(), ['ID', 'Name', 'Servers']) @arg('group', metavar='', help='Name or ID of group.') def do_ipgroup_show(self, args): """Show details about a particular IP group.""" group = self._find_ipgroup(args.group) print_dict(group._info) @arg('name', metavar='', help='What to name this new group.') @arg('server', metavar='', nargs='?', help='Server (name or ID) to make a member of this new group.') def do_ipgroup_create(self, args): """Create a new IP group.""" if args.server: server = self._find_server(args.server) else: server = None group = self.cs.ipgroups.create(args.name, server) print_dict(group._info) @arg('group', metavar='', help='Name or ID of group.') def do_ipgroup_delete(self, args): """Delete an IP group.""" self._find_ipgroup(args.group).delete() def do_list(self, args): """List active servers.""" print_list(self.cs.servers.list(), ['ID', 'Name', 'Status', 'Public IP', 'Private IP']) @arg('--hard', dest = 'reboot_type', action = 'store_const', const = cloudservers.REBOOT_HARD, default = cloudservers.REBOOT_SOFT, help = 'Perform a hard reboot (instead of a soft one).') @arg('server', metavar='', help='Name or ID of server.') def do_reboot(self, args): """Reboot a server.""" self._find_server(args.server).reboot(args.reboot_type) @arg('server', metavar='', help='Name or ID of server.') @arg('image', metavar='', help="Name or ID of new image.") def do_rebuild(self, args): """Shutdown, re-image, and re-boot a server.""" server = self._find_server(args.server) image = self._find_image(args.image) server.rebuild(image) @arg('server', metavar='', help='Name (old name) or ID of server.') @arg('name', metavar='', help='New name for the server.') def do_rename(self, args): """Rename a server.""" self._find_server(args.server).update(name=args.name) @arg('server', metavar='', help='Name or ID of server.') @arg('flavor', metavar='', help = "Name or ID of new flavor.") def do_resize(self, args): """Resize a server.""" server = self._find_server(args.server) flavor = self._find_flavor(args.flavor) server.resize(flavor) @arg('server', metavar='', help='Name or ID of server.') def do_resize_confirm(self, args): """Confirm a previous resize.""" self._find_server(args.server).confirm_resize() @arg('server', metavar='', help='Name or ID of server.') def do_resize_revert(self, args): """Revert a previous resize (and return to the previous VM).""" self._find_server(args.server).revert_resize() @arg('server', metavar='', help='Name or ID of server.') def do_root_password(self, args): """ Change the root password for a server. """ server = self._find_server(args.server) p1 = getpass.getpass('New password: ') p2 = getpass.getpass('Again: ') if p1 != p2: raise CommandError("Passwords do not match.") server.update(password=p1) @arg('server', metavar='', help='Name or ID of server.') def do_show(self, args): """Show details about the given server.""" s = self.cs.servers.get(self._find_server(args.server)) info = s._info.copy() addresses = info.pop('addresses') for addrtype in addresses: info['%s ip' % addrtype] = ', '.join(addresses[addrtype]) info['flavor'] = self._find_flavor(info.pop('flavorId')).name info['image'] = self._find_image(info.pop('imageId')).name print_dict(info) @arg('server', metavar='', help='Name or ID of server.') def do_delete(self, args): """Immediately shut down and delete a server.""" self._find_server(args.server).delete() def _find_server(self, server): """Get a server by name or ID.""" return self._find_resource(self.cs.servers, server) def _find_ipgroup(self, group): """Get an IP group by name or ID.""" return self._find_resource(self.cs.ipgroups, group) def _find_image(self, image): """Get an image by name or ID.""" return self._find_resource(self.cs.images, image) def _find_flavor(self, flavor): """Get a flavor by name, ID, or RAM size.""" try: return self._find_resource(self.cs.flavors, flavor) except cloudservers.NotFound: return self.cs.flavors.find(ram=flavor) def _find_resource(self, manager, name_or_id): """Helper for the _find_* methods.""" try: if isinstance(name_or_id, int) or name_or_id.isdigit(): return manager.get(int(name_or_id)) else: return manager.find(name=name_or_id) except cloudservers.NotFound: raise CommandError("No %s with a name or ID of '%s' exists." % (manager.resource_class.__name__.lower(), name_or_id)) # I'm picky about my shell help. class CloudserversHelpFormatter(argparse.HelpFormatter): def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) super(CloudserversHelpFormatter, self).start_section(heading) # Helpers def print_list(objs, fields): pt = prettytable.PrettyTable([f for f in fields], caching=False) pt.aligns = ['l' for f in fields] for o in objs: pt.add_row([getattr(o, f.lower().replace(' ', '_'), '') for f in fields]) pt.printt(sortby=fields[0]) def print_dict(d): pt = prettytable.PrettyTable(['Property', 'Value'], caching=False) pt.aligns = ['l', 'l'] [pt.add_row(list(r)) for r in d.iteritems()] pt.printt(sortby='Property') def main(): try: CloudserversShell().main(sys.argv[1:]) except CommandError, e: print >> sys.stderr, e sys.exit(1)