diff --git a/novaclient/accounts.py b/novaclient/accounts.py index 01ff2f52e..28a9dedcd 100644 --- a/novaclient/accounts.py +++ b/novaclient/accounts.py @@ -1,8 +1,10 @@ from novaclient import base + class Account(base.Resource): pass + class AccountManager(base.BootingManagerWithFind): resource_class = Account diff --git a/novaclient/base.py b/novaclient/base.py index e402039fe..ee24ea76c 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -68,7 +68,7 @@ class Manager(object): obj_class = self.resource_class return [obj_class(self, res) for res in body[response_key] if res] - + def _get(self, url, response_key): resp, body = self.api.client.get(url) return self.resource_class(self, body[response_key]) diff --git a/novaclient/client.py b/novaclient/client.py index 103679a45..2d2f4846b 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -24,6 +24,7 @@ from novaclient import exceptions _logger = logging.getLogger(__name__) + class OpenStackClient(httplib2.Http): USER_AGENT = 'python-novaclient/%s' % novaclient.__version__ @@ -45,19 +46,20 @@ class OpenStackClient(httplib2.Http): def http_log(self, args, kwargs, resp, body): if not _logger.isEnabledFor(logging.DEBUG): return - + string_parts = ['curl -i'] for element in args: - if element in ('GET','POST'): + if element in ('GET', 'POST'): string_parts.append(' -X %s' % element) else: string_parts.append(' %s' % element) for element in kwargs['headers']: - string_parts.append(' -H "%s: %s"' % (element,kwargs['headers'][element])) + header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) + string_parts.append(header) _logger.debug("REQ: %s\n" % "".join(string_parts)) - _logger.debug("RESP:%s %s\n", resp,body) + _logger.debug("RESP:%s %s\n", resp, body) def request(self, *args, **kwargs): kwargs.setdefault('headers', {}) @@ -69,7 +71,7 @@ class OpenStackClient(httplib2.Http): resp, body = super(OpenStackClient, self).request(*args, **kwargs) self.http_log(args, kwargs, resp, body) - + if body: try: body = json.loads(body) diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 1709d806f..64efb4b75 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -3,6 +3,7 @@ Exception definitions. """ + class OpenStackException(Exception): """ The base exception class for all exceptions this library raises. diff --git a/novaclient/images.py b/novaclient/images.py index d911cc747..706915828 100644 --- a/novaclient/images.py +++ b/novaclient/images.py @@ -46,8 +46,7 @@ class ImageManager(base.ManagerWithFind): detail = "/detail" return self._list("/images%s" % detail, "images") - - def create(self, server, name, image_type=None, backup_type=None, rotation=None): + def create(self, server, name): """ Create a new image by snapshotting a running :class:`Server` @@ -55,23 +54,7 @@ class ImageManager(base.ManagerWithFind): :param server: The :class:`Server` (or its ID) to make a snapshot of. :rtype: :class:`Image` """ - if image_type is None: - image_type = "snapshot" - - if image_type not in ("backup", "snapshot"): - raise Exception("Invalid image_type: must be backup or snapshot") - - if image_type == "backup": - if not rotation: - raise Exception("rotation is required for backups") - elif not backup_type: - raise Exception("backup_type required for backups") - elif backup_type not in ("daily", "weekly"): - raise Exception("Invalid backup_type: must be daily or weekly") - - data = {"image": {"serverId": base.getid(server), "name": name, - "image_type": image_type, "backup_type": backup_type, - "rotation": rotation}} + data = {"image": {"serverId": base.getid(server), "name": name}} return self._create("/images", data, "image") def delete(self, image): diff --git a/novaclient/servers.py b/novaclient/servers.py index 18392e4de..c9166228d 100644 --- a/novaclient/servers.py +++ b/novaclient/servers.py @@ -154,6 +154,18 @@ class Server(base.Resource): """ self.manager.resize(self, flavor) + def backup(self, image_name, backup_type, rotation): + """ + Create a server backup. + + :param server: The :class:`Server` (or its ID). + :param image_name: The name to assign the newly create image. + :param backup_type: 'daily' or 'weekly' + :param rotation: number of backups of type 'backup_type' to keep + :returns Newly created :class:`Image` object + """ + return self.manager.backup(self, image_name, backup_type, rotation) + def confirm_resize(self): """ Confirm that the resize worked, thus removing the original server. @@ -228,7 +240,7 @@ class ServerManager(base.BootingManagerWithFind): qparams[opt] = val query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" - + detail = "" if detailed: detail = "/detail" @@ -370,6 +382,31 @@ class ServerManager(base.BootingManagerWithFind): """ self._action('resize', server, {'flavorId': base.getid(flavor)}) + def backup(self, server, image_name, backup_type, rotation): + """ + Create a server backup. + + :param server: The :class:`Server` (or its ID). + :param image_name: The name to assign the newly create image. + :param backup_type: 'daily' or 'weekly' + :param rotation: number of backups of type 'backup_type' to keep + :returns Newly created :class:`Image` object + """ + if not rotation: + raise Exception("rotation is required for backups") + elif not backup_type: + raise Exception("backup_type required for backups") + elif backup_type not in ("daily", "weekly"): + raise Exception("Invalid backup_type: must be daily or weekly") + + data = { + "name": image_name, + "rotation": rotation, + "backup_type": backup_type, + } + + self._action('createBackup', server, data) + def pause(self, server): """ Pause the server. diff --git a/novaclient/shell.py b/novaclient/shell.py index b420e1c3b..f9f5252e9 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -98,7 +98,7 @@ class OpenStackShell(object): help='Defaults to env[NOVA_API_KEY].') self.parser.add_argument('--projectid', - default=env('NOVA_PROJECT_ID'), + default=env('NOVA_PROJECT_ID'), help='Defaults to env[NOVA_PROJECT_ID].') auth_url = env('NOVA_URL') @@ -281,7 +281,7 @@ class OpenStackShell(object): except IOError, e: raise CommandError("Can't open '%s': %s" % (keyfile, e)) - return (args.name, image, flavor, ipgroup, metadata, files, + return (args.name, image, flavor, ipgroup, metadata, files, reservation_id, min_count, max_count) @arg('--flavor', @@ -461,7 +461,7 @@ class OpenStackShell(object): for from_key, to_key in convert: if from_key in keys and to_key not in keys: setattr(item, to_key, item._info[from_key]) - + def do_flavor_list(self, args): """Print a list of available 'flavors' (sizes of servers).""" flavors = self.cs.flavors.list() @@ -481,27 +481,11 @@ class OpenStackShell(object): print_list(self.cs.images.list(), ['ID', 'Name', 'Status']) @arg('server', metavar='', help='Name or ID of server.') - @arg('name', metavar='', help='Name of backup or snapshot.') - @arg('--image-type', - metavar='', - default='snapshot', - help='type of image (default: snapshot)') - @arg('--backup-type', - metavar='', - default=None, - help='type of backup') - @arg('--rotation', - default=None, - type=int, - metavar='', - help="Number of backups to retain. Used for backup image_type.") + @arg('name', metavar='', help='Name of snapshot.') 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(server, args.name, - image_type=args.image_type, - backup_type=args.backup_type, - rotation=args.rotation) + image = self.cs.images.create(server, args.name) print_dict(image._info) @arg('image', metavar='', help='Name or ID of image.') @@ -660,6 +644,16 @@ class OpenStackShell(object): flavor = self._find_flavor(args.flavor) server.resize(flavor) + @arg('server', metavar='', help='Name or ID of server.') + @arg('name', metavar='', help='Name of snapshot.') + @arg('backup_type', metavar='', help='type of backup') + @arg('rotation', type=int, metavar='', + help="Number of backups to retain. Used for backup image_type.") + def do_backup(self, args): + """Resize a server.""" + server = self._find_server(args.server) + server.backup(args.name, args.backup_type, args.rotation) + @arg('server', metavar='', help='Name or ID of server.') def do_migrate(self, args): """Migrate a server.""" @@ -766,7 +760,7 @@ class OpenStackShell(object): def do_zone(self, args): """Show or edit a child zone. No zone arg for this zone.""" zone = self.cs.zones.get(args.zone) - + # If we have some flags, update the zone zone_delta = {} if args.api_url: @@ -790,7 +784,7 @@ class OpenStackShell(object): print_dict(zone._info) @arg('api_url', metavar='', help="URL for the Zone's API") - @arg('zone_username', metavar='', + @arg('zone_username', metavar='', help='Authentication username.') @arg('password', metavar='', help='Authentication password.') @arg('weight_offset', metavar='', @@ -799,7 +793,7 @@ class OpenStackShell(object): help='Child Zone weight scale (typically 1.0).') def do_zone_add(self, args): """Add a new child zone.""" - zone = self.cs.zones.create(args.api_url, args.zone_username, + zone = self.cs.zones.create(args.api_url, args.zone_username, args.password, args.weight_offset, args.weight_scale) print_dict(zone._info) @@ -820,7 +814,7 @@ class OpenStackShell(object): """Add new IP address to network.""" server = self._find_server(args.server) server.add_fixed_ip(args.network_id) - + @arg('server', metavar='', help='Name or ID of server.') @arg('address', metavar='
', help='IP Address.') def do_remove_fixed_ip(self, args): diff --git a/novaclient/zones.py b/novaclient/zones.py index 5bcee5501..28d0d6676 100644 --- a/novaclient/zones.py +++ b/novaclient/zones.py @@ -24,7 +24,7 @@ class Weighting(base.Resource): def __init__(self, manager, info): self.name = "n/a" super(Weighting, self).__init__(manager, info) - + def __repr__(self): return "" % self.name diff --git a/setup.py b/setup.py index 6adef8c37..4e66ec2b5 100644 --- a/setup.py +++ b/setup.py @@ -2,24 +2,25 @@ import os import sys from setuptools import setup, find_packages + def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() requirements = ['httplib2', 'argparse', 'prettytable'] -if sys.version_info < (2,6): +if sys.version_info < (2, 6): requirements.append('simplejson') setup( - name = "python-novaclient", - version = "2.5.8", - description = "Client library for OpenStack Nova API", - long_description = read('README.rst'), - url = 'https://github.com/rackspace/python-novaclient', - license = 'Apache', - author = 'Rackspace, based on work by Jacob Kaplan-Moss', - author_email = 'github@racklabs.com', - packages = find_packages(exclude=['tests']), - classifiers = [ + name="python-novaclient", + version="2.5.8", + description="Client library for OpenStack Nova API", + long_description=read('README.rst'), + url='https://github.com/rackspace/python-novaclient', + license='Apache', + author='Rackspace, based on work by Jacob Kaplan-Moss', + author_email='github@racklabs.com', + packages=find_packages(exclude=['tests']), + classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Developers', @@ -28,12 +29,12 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python', ], - install_requires = requirements, - - tests_require = ["nose", "mock"], - test_suite = "nose.collector", - - entry_points = { + install_requires=requirements, + + tests_require=["nose", "mock"], + test_suite="nose.collector", + + entry_points={ 'console_scripts': ['nova = novaclient.shell:main'] } ) diff --git a/tests/fakeserver.py b/tests/fakeserver.py index 232c7b350..a1aa22608 100644 --- a/tests/fakeserver.py +++ b/tests/fakeserver.py @@ -267,6 +267,9 @@ class FakeClient(OpenStackClient): assert_equal(body[action].keys(), ['imageId']) elif action == 'resize': assert_equal(body[action].keys(), ['flavorId']) + elif action == 'createBackup': + assert_equal(set(body[action].keys()), + set(['name', 'rotation', 'backup_type'])) elif action == 'confirmResize': assert_equal(body[action], None) # This one method returns a different response code @@ -342,7 +345,7 @@ class FakeClient(OpenStackClient): def post_images(self, body, **kw): assert_equal(body.keys(), ['image']) - assert_has_keys(body['image'], required=['serverId', 'name', 'image_type', 'backup_type', 'rotation']) + assert_has_keys(body['image'], required=['serverId', 'name']) return (202, self.get_images_1()[1]) def delete_images_1(self, **kw): @@ -409,10 +412,9 @@ class FakeClient(OpenStackClient): {'id': 2, 'api_url': 'http://foo.com', 'username': 'alice'}, ]}) - def get_zones_detail(self, **kw): return (200, {'zones': [ - {'id': 1, 'api_url': 'http://foo.com', 'username': 'bob', + {'id': 1, 'api_url': 'http://foo.com', 'username': 'bob', 'password': 'qwerty'}, {'id': 2, 'api_url': 'http://foo.com', 'username': 'alice', 'password': 'password'} diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 37e37c553..8c488efe2 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -7,6 +7,7 @@ from novaclient import Account cs = FakeServer() + def test_instance_creation_for_account(): s = cs.accounts.create_instance_for( account_id='test_account', diff --git a/tests/test_servers.py b/tests/test_servers.py index b88ed9212..29c3069aa 100644 --- a/tests/test_servers.py +++ b/tests/test_servers.py @@ -150,6 +150,14 @@ def test_revert_resized_server(): cs.assert_called('POST', '/servers/1234/action') +def test_backup_server(): + s = cs.servers.get(1234) + s.backup("ImageName", "daily", 10) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.backup(s, "ImageName", "daily", 10) + cs.assert_called('POST', '/servers/1234/action') + + def test_migrate_server(): s = cs.servers.get(1234) s.migrate() diff --git a/tests/test_shell.py b/tests/test_shell.py index 17ba5a442..0d8b10751 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -195,15 +195,7 @@ def test_snapshot_create(): shell('image-create sample-server mysnapshot') assert_called( 'POST', '/images', - {'image': {'name': 'mysnapshot', 'serverId': 1234, 'image_type': 'snapshot', 'backup_type': None, 'rotation': None}} - ) - - -def test_backup_create(): - shell('image-create sample-server mybackup --image-type backup --backup-type daily --rotation 1') - assert_called( - 'POST', '/images', - {'image': {'name': 'mybackup', 'serverId': 1234, 'image_type': 'backup', 'backup_type': 'daily', 'rotation': 1}} + {'image': {'name': 'mysnapshot', 'serverId': 1234}} ) @@ -295,6 +287,15 @@ def test_resize_revert(): assert_called('POST', '/servers/1234/action', {'revertResize': None}) +def test_backup(): + shell('backup sample-server mybackup daily 1') + assert_called( + 'POST', '/servers/1234/action', + {'createBackup': {'name': 'mybackup', 'backup_type': 'daily', + 'rotation': 1}} + ) + + @mock.patch('getpass.getpass', mock.Mock(return_value='p')) def test_root_password(): shell('root-password sample-server') @@ -326,15 +327,17 @@ def test_zone(): 'password': 'xxx'}} ) + def test_zone_add(): shell('zone-add http://zzz frank xxx 0.0 1.0') assert_called( 'POST', '/zones', - {'zone': {'api_url': 'http://zzz', 'username': 'frank', + {'zone': {'api_url': 'http://zzz', 'username': 'frank', 'password': 'xxx', 'weight_offset': '0.0', 'weight_scale': '1.0'}} ) + def test_zone_delete(): shell('zone-delete 1') assert_called('DELETE', '/zones/1') diff --git a/tests/test_zones.py b/tests/test_zones.py index cd773ad3e..11194189a 100644 --- a/tests/test_zones.py +++ b/tests/test_zones.py @@ -49,7 +49,7 @@ def test_update_zone(): # Update via manager os.zones.update(s, api_url='http://blah.com') os.assert_called('PUT', '/zones/1') - os.zones.update(1, api_url= 'http://blah.com') + os.zones.update(1, api_url='http://blah.com') os.assert_called('PUT', '/zones/1') os.zones.update(s, api_url='http://blah.com', username='fred', password='zip')