From f8496672cc61ffba052a8c9626e24fde18519010 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 3 Aug 2011 16:36:03 -0400 Subject: [PATCH 01/17] Split everything down the middle into v1_0 and v1_1, including tests. --- .gitignore | 5 +- novaclient/__init__.py | 70 --- novaclient/v1_0/__init__.py | 74 +++ novaclient/{ => v1_0}/accounts.py | 3 +- novaclient/{ => v1_0}/backup_schedules.py | 2 +- novaclient/{ => v1_0}/base.py | 6 +- novaclient/{ => v1_0}/client.py | 12 +- novaclient/{ => v1_0}/exceptions.py | 0 novaclient/{ => v1_0}/flavors.py | 3 +- novaclient/{ => v1_0}/images.py | 4 +- novaclient/{ => v1_0}/ipgroups.py | 2 +- novaclient/{ => v1_0}/servers.py | 4 +- novaclient/{ => v1_0}/shell.py | 44 +- novaclient/{ => v1_0}/zones.py | 4 +- novaclient/v1_1/__init__.py | 66 +++ novaclient/v1_1/base.py | 214 ++++++++ novaclient/v1_1/client.py | 154 ++++++ novaclient/v1_1/exceptions.py | 100 ++++ novaclient/v1_1/flavors.py | 41 ++ novaclient/v1_1/images.py | 86 ++++ novaclient/v1_1/servers.py | 300 +++++++++++ novaclient/v1_1/shell.py | 584 ++++++++++++++++++++++ novaclient/v1_1/zones.py | 196 ++++++++ tests/test_backup_schedules.py | 58 --- tests/test_flavors.py | 37 -- tests/test_images.py | 47 -- tests/test_ipgroups.py | 48 -- tests/test_servers.py | 174 ------- tests/v1_0/__init__.py | 0 tests/{fakeserver.py => v1_0/fakes.py} | 30 +- tests/{ => v1_0}/test_accounts.py | 13 +- tests/{ => v1_0}/test_auth.py | 14 +- tests/v1_0/test_backup_schedules.py | 60 +++ tests/{ => v1_0}/test_base.py | 42 +- tests/{ => v1_0}/test_client.py | 11 +- tests/v1_0/test_flavors.py | 42 ++ tests/v1_0/test_images.py | 51 ++ tests/v1_0/test_ipgroups.py | 52 ++ tests/v1_0/test_servers.py | 180 +++++++ tests/{ => v1_0}/test_shell.py | 15 +- tests/{ => v1_0}/test_zones.py | 25 +- tests/{ => v1_0}/testfile.txt | 0 tests/{ => v1_0}/utils.py | 0 tests/v1_1/__init__.py | 0 tests/v1_1/fakes.py | 496 ++++++++++++++++++ tests/v1_1/test_base.py | 61 +++ tests/v1_1/test_client.py | 52 ++ tests/v1_1/test_flavors.py | 42 ++ tests/v1_1/test_images.py | 51 ++ tests/v1_1/test_servers.py | 125 +++++ tests/v1_1/test_shell.py | 234 +++++++++ tests/v1_1/testfile.txt | 1 + tests/v1_1/utils.py | 29 ++ 53 files changed, 3425 insertions(+), 539 deletions(-) create mode 100644 novaclient/v1_0/__init__.py rename novaclient/{ => v1_0}/accounts.py (94%) rename novaclient/{ => v1_0}/backup_schedules.py (99%) rename novaclient/{ => v1_0}/base.py (98%) rename novaclient/{ => v1_0}/client.py (95%) rename novaclient/{ => v1_0}/exceptions.py (100%) rename novaclient/{ => v1_0}/flavors.py (96%) rename novaclient/{ => v1_0}/images.py (98%) rename novaclient/{ => v1_0}/ipgroups.py (98%) rename novaclient/{ => v1_0}/servers.py (99%) rename novaclient/{ => v1_0}/shell.py (97%) rename novaclient/{ => v1_0}/zones.py (99%) create mode 100644 novaclient/v1_1/__init__.py create mode 100644 novaclient/v1_1/base.py create mode 100644 novaclient/v1_1/client.py create mode 100644 novaclient/v1_1/exceptions.py create mode 100644 novaclient/v1_1/flavors.py create mode 100644 novaclient/v1_1/images.py create mode 100644 novaclient/v1_1/servers.py create mode 100644 novaclient/v1_1/shell.py create mode 100644 novaclient/v1_1/zones.py delete mode 100644 tests/test_backup_schedules.py delete mode 100644 tests/test_flavors.py delete mode 100644 tests/test_images.py delete mode 100644 tests/test_ipgroups.py delete mode 100644 tests/test_servers.py create mode 100644 tests/v1_0/__init__.py rename tests/{fakeserver.py => v1_0/fakes.py} (96%) rename tests/{ => v1_0}/test_accounts.py (62%) rename tests/{ => v1_0}/test_auth.py (81%) create mode 100644 tests/v1_0/test_backup_schedules.py rename tests/{ => v1_0}/test_base.py (50%) rename tests/{ => v1_0}/test_client.py (88%) create mode 100644 tests/v1_0/test_flavors.py create mode 100644 tests/v1_0/test_images.py create mode 100644 tests/v1_0/test_ipgroups.py create mode 100644 tests/v1_0/test_servers.py rename tests/{ => v1_0}/test_shell.py (97%) rename tests/{ => v1_0}/test_zones.py (84%) rename tests/{ => v1_0}/testfile.txt (100%) rename tests/{ => v1_0}/utils.py (100%) create mode 100644 tests/v1_1/__init__.py create mode 100644 tests/v1_1/fakes.py create mode 100644 tests/v1_1/test_base.py create mode 100644 tests/v1_1/test_client.py create mode 100644 tests/v1_1/test_flavors.py create mode 100644 tests/v1_1/test_images.py create mode 100644 tests/v1_1/test_servers.py create mode 100644 tests/v1_1/test_shell.py create mode 100644 tests/v1_1/testfile.txt create mode 100644 tests/v1_1/utils.py diff --git a/.gitignore b/.gitignore index b32d8aa7a..355cfbd22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ +.coverage +*,cover +cover *.pyc -.idea \ No newline at end of file +.idea diff --git a/novaclient/__init__.py b/novaclient/__init__.py index a0807c710..08bd9d9a7 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -14,74 +14,4 @@ # License for the specific language governing permissions and limitations # under the License. -""" -novaclient module. -""" - __version__ = '2.5' - -from novaclient.accounts import Account, AccountManager -from novaclient.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 novaclient.client import OpenStackClient -from novaclient.exceptions import (OpenStackException, BadRequest, - Unauthorized, Forbidden, NotFound, OverLimit) -from novaclient.flavors import FlavorManager, Flavor -from novaclient.images import ImageManager, Image -from novaclient.ipgroups import IPGroupManager, IPGroup -from novaclient.servers import (ServerManager, Server, REBOOT_HARD, - REBOOT_SOFT) -from novaclient.zones import Zone, ZoneManager - - -class OpenStack(object): - """ - Top-level object to access the OpenStack Nova API. - - Create an instance with your creds:: - - >>> os = OpenStack(USERNAME, API_KEY, PROJECT_ID, AUTH_URL) - - Then call methods on its managers:: - - >>> os.servers.list() - ... - >>> os.flavors.list() - ... - - &c. - """ - - def __init__(self, username, apikey, projectid, - auth_url='https://auth.api.rackspacecloud.com/v1.0', timeout=None): - self.backup_schedules = BackupScheduleManager(self) - self.client = OpenStackClient(username, apikey, projectid, auth_url, - timeout=timeout) - self.flavors = FlavorManager(self) - self.images = ImageManager(self) - self.ipgroups = IPGroupManager(self) - self.servers = ServerManager(self) - self.zones = ZoneManager(self) - self.accounts = AccountManager(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 :exc:`novaclient.Unauthorized` if the - credentials are wrong. - """ - self.client.authenticate() diff --git a/novaclient/v1_0/__init__.py b/novaclient/v1_0/__init__.py new file mode 100644 index 000000000..7b2c9737f --- /dev/null +++ b/novaclient/v1_0/__init__.py @@ -0,0 +1,74 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.v1_0 import accounts +from novaclient.v1_0 import backup_schedules +from novaclient.v1_0 import client +from novaclient.v1_0 import exceptions +from novaclient.v1_0 import flavors +from novaclient.v1_0 import images +from novaclient.v1_0 import ipgroups +from novaclient.v1_0 import servers +from novaclient.v1_0 import zones + + +class Client(object): + """ + Top-level object to access the OpenStack Compute v1.0 API. + + Create an instance with your creds:: + + >>> os = novaclient.v1_0.Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL) + + Then call methods on its managers:: + + >>> os.servers.list() + ... + >>> os.flavors.list() + ... + + &c. + """ + + def __init__(self, username, apikey, projectid, auth_url=None, timeout=None): + """Initialize v1.0 Openstack Client.""" + self.backup_schedules = backup_schedules.BackupScheduleManager(self) + self.flavors = flavors.FlavorManager(self) + self.images = images.ImageManager(self) + self.ipgroups = ipgroups.IPGroupManager(self) + self.servers = servers.ServerManager(self) + self.zones = zones.ZoneManager(self) + self.accounts = accounts.AccountManager(self) + + auth_url = auth_url or "https://auth.api.rackspacecloud.com/v1.0" + + self.client = client.HTTPClient(username, + apikey, + projectid, + auth_url, + timeout=timeout) + + 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 :exc:`novaclient.Unauthorized` if the + credentials are wrong. + """ + self.client.authenticate() diff --git a/novaclient/accounts.py b/novaclient/v1_0/accounts.py similarity index 94% rename from novaclient/accounts.py rename to novaclient/v1_0/accounts.py index 01ff2f52e..264ce8431 100644 --- a/novaclient/accounts.py +++ b/novaclient/v1_0/accounts.py @@ -1,9 +1,10 @@ -from novaclient import base +from novaclient.v1_0 import base class Account(base.Resource): pass class AccountManager(base.BootingManagerWithFind): + resource_class = Account def create_instance_for(self, account_id, name, image, flavor, diff --git a/novaclient/backup_schedules.py b/novaclient/v1_0/backup_schedules.py similarity index 99% rename from novaclient/backup_schedules.py rename to novaclient/v1_0/backup_schedules.py index 662e31489..78f4d49f2 100644 --- a/novaclient/backup_schedules.py +++ b/novaclient/v1_0/backup_schedules.py @@ -3,7 +3,7 @@ Backup Schedule interface. """ -from novaclient import base +from novaclient.v1_0 import base BACKUP_WEEKLY_DISABLED = 'DISABLED' BACKUP_WEEKLY_SUNDAY = 'SUNDAY' diff --git a/novaclient/base.py b/novaclient/v1_0/base.py similarity index 98% rename from novaclient/base.py rename to novaclient/v1_0/base.py index e402039fe..3dbec636d 100644 --- a/novaclient/base.py +++ b/novaclient/v1_0/base.py @@ -19,7 +19,7 @@ Base utilities to build API operation managers and objects on top of. """ -from novaclient.exceptions import NotFound +from novaclient.v1_0 import exceptions # Python 2.4 compat try: @@ -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]) @@ -101,7 +101,7 @@ class ManagerWithFind(Manager): try: return rl[0] except IndexError: - raise NotFound(404, "No %s matching %s." % + raise exceptions.NotFound(404, "No %s matching %s." % (self.resource_class.__name__, kwargs)) def findall(self, **kwargs): diff --git a/novaclient/client.py b/novaclient/v1_0/client.py similarity index 95% rename from novaclient/client.py rename to novaclient/v1_0/client.py index 103679a45..202f6da56 100644 --- a/novaclient/client.py +++ b/novaclient/v1_0/client.py @@ -20,16 +20,16 @@ if not hasattr(urlparse, 'parse_qsl'): urlparse.parse_qsl = cgi.parse_qsl import novaclient -from novaclient import exceptions +from novaclient.v1_0 import exceptions _logger = logging.getLogger(__name__) -class OpenStackClient(httplib2.Http): +class HTTPClient(httplib2.Http): USER_AGENT = 'python-novaclient/%s' % novaclient.__version__ def __init__(self, user, apikey, projectid, auth_url, timeout=None): - super(OpenStackClient, self).__init__(timeout=timeout) + super(HTTPClient, self).__init__(timeout=timeout) self.user = user self.apikey = apikey self.projectid = projectid @@ -45,7 +45,7 @@ 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'): @@ -66,10 +66,10 @@ class OpenStackClient(httplib2.Http): kwargs['headers']['Content-Type'] = 'application/json' kwargs['body'] = json.dumps(kwargs['body']) - resp, body = super(OpenStackClient, self).request(*args, **kwargs) + resp, body = super(HTTPClient, 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/v1_0/exceptions.py similarity index 100% rename from novaclient/exceptions.py rename to novaclient/v1_0/exceptions.py diff --git a/novaclient/flavors.py b/novaclient/v1_0/flavors.py similarity index 96% rename from novaclient/flavors.py rename to novaclient/v1_0/flavors.py index bfede1e13..4dc4ac994 100644 --- a/novaclient/flavors.py +++ b/novaclient/v1_0/flavors.py @@ -3,8 +3,7 @@ Flavor interface. """ - -from novaclient import base +from novaclient.v1_0 import base class Flavor(base.Resource): diff --git a/novaclient/images.py b/novaclient/v1_0/images.py similarity index 98% rename from novaclient/images.py rename to novaclient/v1_0/images.py index d911cc747..ec36fe34a 100644 --- a/novaclient/images.py +++ b/novaclient/v1_0/images.py @@ -3,7 +3,7 @@ Image interface. """ -from novaclient import base +from novaclient.v1_0 import base class Image(base.Resource): @@ -60,7 +60,7 @@ class ImageManager(base.ManagerWithFind): 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") diff --git a/novaclient/ipgroups.py b/novaclient/v1_0/ipgroups.py similarity index 98% rename from novaclient/ipgroups.py rename to novaclient/v1_0/ipgroups.py index 86cd3cb43..66821ee70 100644 --- a/novaclient/ipgroups.py +++ b/novaclient/v1_0/ipgroups.py @@ -3,7 +3,7 @@ IP Group interface. """ -from novaclient import base +from novaclient.v1_0 import base class IPGroup(base.Resource): diff --git a/novaclient/servers.py b/novaclient/v1_0/servers.py similarity index 99% rename from novaclient/servers.py rename to novaclient/v1_0/servers.py index 18392e4de..a2b014f05 100644 --- a/novaclient/servers.py +++ b/novaclient/v1_0/servers.py @@ -20,7 +20,7 @@ Server interface. """ import urllib -from novaclient import base +from novaclient.v1_0 import base REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -228,7 +228,7 @@ class ServerManager(base.BootingManagerWithFind): qparams[opt] = val query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" - + detail = "" if detailed: detail = "/detail" diff --git a/novaclient/shell.py b/novaclient/v1_0/shell.py similarity index 97% rename from novaclient/shell.py rename to novaclient/v1_0/shell.py index b420e1c3b..b4dcef2d2 100644 --- a/novaclient/shell.py +++ b/novaclient/v1_0/shell.py @@ -20,7 +20,6 @@ Command-line interface to the OpenStack Nova API. """ import argparse -import novaclient import getpass import httplib2 import os @@ -29,12 +28,17 @@ import sys import textwrap import uuid +import novaclient.v1_0 +from novaclient.v1_0 import backup_schedules +from novaclient.v1_0 import exceptions +from novaclient.v1_0 import servers + # Choices for flags. -DAY_CHOICES = [getattr(novaclient, i).lower() - for i in dir(novaclient) +DAY_CHOICES = [getattr(backup_schedules, i).lower() + for i in dir(backup_schedules) if i.startswith('BACKUP_WEEKLY_')] -HOUR_CHOICES = [getattr(novaclient, i).lower() - for i in dir(novaclient) +HOUR_CHOICES = [getattr(backup_schedules, i).lower() + for i in dir(backup_schedules) if i.startswith('BACKUP_DAILY_')] @@ -66,7 +70,7 @@ def env(e): class OpenStackShell(object): # Hook for the test suite to inject a fake server. - _api_class = novaclient.OpenStack + _api_class = novaclient.v1_0.Client def __init__(self): self.parser = argparse.ArgumentParser( @@ -98,7 +102,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') @@ -165,7 +169,7 @@ class OpenStackShell(object): self.cs = self._api_class(user, apikey, projectid, url) try: self.cs.authenticate() - except novaclient.Unauthorized: + except exceptions.Unauthorized: raise CommandError("Invalid OpenStack Nova credentials.") args.func(args) @@ -208,10 +212,10 @@ class OpenStackShell(object): # If we have some flags, update the backup backup = {} if args.daily: - backup['daily'] = getattr(novaclient, 'BACKUP_DAILY_%s' % + backup['daily'] = getattr(backup_schedules, 'BACKUP_DAILY_%s' % args.daily.upper()) if args.weekly: - backup['weekly'] = getattr(novaclient, 'BACKUP_WEEKLY_%s' % + backup['weekly'] = getattr(backup_schedules, 'BACKUP_WEEKLY_%s' % args.weekly.upper()) if args.enabled is not None: backup['enabled'] = args.enabled @@ -281,7 +285,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 +465,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() @@ -630,8 +634,8 @@ class OpenStackShell(object): @arg('--hard', dest='reboot_type', action='store_const', - const=novaclient.REBOOT_HARD, - default=novaclient.REBOOT_SOFT, + const=servers.REBOOT_HARD, + default=servers.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): @@ -766,7 +770,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 +794,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 +803,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 +824,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): @@ -844,7 +848,7 @@ class OpenStackShell(object): """Get a flavor by name, ID, or RAM size.""" try: return self._find_resource(self.cs.flavors, flavor) - except novaclient.NotFound: + except exceptions.NotFound: return self.cs.flavors.find(ram=flavor) def _find_resource(self, manager, name_or_id): @@ -858,7 +862,7 @@ class OpenStackShell(object): return manager.get(name_or_id) except ValueError: return manager.find(name=name_or_id) - except novaclient.NotFound: + except exceptions.NotFound: raise CommandError("No %s with a name or ID of '%s' exists." % (manager.resource_class.__name__.lower(), name_or_id)) diff --git a/novaclient/zones.py b/novaclient/v1_0/zones.py similarity index 99% rename from novaclient/zones.py rename to novaclient/v1_0/zones.py index 5bcee5501..f711ec219 100644 --- a/novaclient/zones.py +++ b/novaclient/v1_0/zones.py @@ -17,14 +17,14 @@ Zone interface. """ -from novaclient import base +from novaclient.v1_0 import base 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/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py new file mode 100644 index 000000000..955b27e53 --- /dev/null +++ b/novaclient/v1_1/__init__.py @@ -0,0 +1,66 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.v1_1 import client +from novaclient.v1_1 import exceptions +from novaclient.v1_1 import flavors +from novaclient.v1_1 import images +from novaclient.v1_1 import servers + + +class Client(object): + """ + Top-level object to access the OpenStack Compute v1.0 API. + + Create an instance with your creds:: + + >>> os = novaclient.v1_1.Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL) + + Then call methods on its managers:: + + >>> os.servers.list() + ... + >>> os.flavors.list() + ... + + &c. + """ + + def __init__(self, username, apikey, projectid, auth_url=None, timeout=None): + """Initialize v1.0 Openstack Client.""" + self.flavors = flavors.FlavorManager(self) + self.images = images.ImageManager(self) + self.servers = servers.ServerManager(self) + + auth_url = auth_url or "https://auth.api.rackspacecloud.com/v1.0" + + self.client = client.HTTPClient(username, + apikey, + projectid, + auth_url, + timeout=timeout) + + 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 :exc:`novaclient.Unauthorized` if the + credentials are wrong. + """ + self.client.authenticate() diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py new file mode 100644 index 000000000..f283d8596 --- /dev/null +++ b/novaclient/v1_1/base.py @@ -0,0 +1,214 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Base utilities to build API operation managers and objects on top of. +""" + +from novaclient.v1_1 import exceptions + +# Python 2.4 compat +try: + all +except NameError: + def all(iterable): + return True not in (not x for x in iterable) + + +def getid(obj): + """ + Abstracts the common pattern of allowing both an object or an object's ID + (UUID) as a parameter when dealing with relationships. + """ + + # Try to return the object's UUID first, if we have a UUID. + try: + if obj.uuid: + return obj.uuid + except AttributeError: + pass + try: + return obj.id + except AttributeError: + return obj + + +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, obj_class=None, body=None): + resp = None + if body: + resp, body = self.api.client.post(url, body=body) + else: + resp, body = self.api.client.get(url) + + if obj_class is None: + 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]) + + def _create(self, url, body, response_key, return_raw=False): + resp, body = self.api.client.post(url, body=body) + if return_raw: + return body[response_key] + 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 exceptions.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 BootingManagerWithFind(ManagerWithFind): + """Like a `ManagerWithFind`, but has the ability to boot servers.""" + def _boot(self, resource_url, response_key, name, image, flavor, + meta=None, files=None, return_raw=False): + """ + 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 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. + :param return_raw: If True, don't try to coearse the result into + a Resource object. + """ + body = {"server": { + "name": name, + "imageRef": getid(image), + "flavorRef": getid(flavor), + }} + 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(resource_url, body, response_key, + return_raw=return_raw) + + +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) + if new: + 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 diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py new file mode 100644 index 000000000..a805826d8 --- /dev/null +++ b/novaclient/v1_1/client.py @@ -0,0 +1,154 @@ +# Copyright 2010 Jacob Kaplan-Moss +""" +OpenStack Client interface. Handles the REST calls and responses. +""" + +import time +import urlparse +import urllib +import httplib2 +import logging + +try: + import json +except ImportError: + import simplejson as json + +# Python 2.5 compat fix +if not hasattr(urlparse, 'parse_qsl'): + import cgi + urlparse.parse_qsl = cgi.parse_qsl + +import novaclient +from novaclient.v1_1 import exceptions + +_logger = logging.getLogger(__name__) + +class HTTPClient(httplib2.Http): + + USER_AGENT = 'python-novaclient/%s' % novaclient.__version__ + + def __init__(self, user, apikey, projectid, auth_url, timeout=None): + super(HTTPClient, self).__init__(timeout=timeout) + self.user = user + self.apikey = apikey + self.projectid = projectid + self.auth_url = auth_url + self.version = 'v1.0' + + self.management_url = None + self.auth_token = None + + # httplib2 overrides + self.force_exception_to_status_code = True + + 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'): + 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])) + + _logger.debug("REQ: %s\n" % "".join(string_parts)) + _logger.debug("RESP:%s %s\n", resp,body) + + 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(HTTPClient, self).request(*args, **kwargs) + + self.http_log(args, kwargs, resp, body) + + if body: + try: + body = json.loads(body) + except ValueError, e: + pass + else: + body = None + + if resp.status in (400, 401, 403, 404, 408, 413, 500, 501): + 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 + if self.projectid: + kwargs['headers']['X-Auth-Project-Id'] = self.projectid + + 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): + url = self._munge_get_url(url) + 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): + scheme, netloc, path, query, frag = urlparse.urlsplit( + self.auth_url) + path_parts = path.split('/') + for part in path_parts: + if len(part) > 0 and part[0] == 'v': + self.version = part + break + + headers = {'X-Auth-User': self.user, + 'X-Auth-Key': self.apikey} + if self.projectid: + headers['X-Auth-Project-Id'] = self.projectid + 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'] + + def _munge_get_url(self, url): + """ + Munge GET URLs to always return uncached content. + + The OpenStack Nova API caches data *very* agressively and doesn't + respect cache headers. To avoid stale data, then, we append a little + bit of nonsense onto GET parameters; this appears to force the data not + to be cached. + """ + scheme, netloc, path, query, frag = urlparse.urlsplit(url) + query = urlparse.parse_qsl(query) + query.append(('fresh', str(time.time()))) + query = urllib.urlencode(query) + return urlparse.urlunsplit((scheme, netloc, path, query, frag)) diff --git a/novaclient/v1_1/exceptions.py b/novaclient/v1_1/exceptions.py new file mode 100644 index 000000000..1709d806f --- /dev/null +++ b/novaclient/v1_1/exceptions.py @@ -0,0 +1,100 @@ +# Copyright 2010 Jacob Kaplan-Moss +""" +Exception definitions. +""" + +class OpenStackException(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(OpenStackException): + """ + HTTP 400 - Bad request: you sent some malformed data. + """ + http_status = 400 + message = "Bad request" + + +class Unauthorized(OpenStackException): + """ + HTTP 401 - Unauthorized: bad credentials. + """ + http_status = 401 + message = "Unauthorized" + + +class Forbidden(OpenStackException): + """ + HTTP 403 - Forbidden: your credentials don't give you access to this + resource. + """ + http_status = 403 + message = "Forbidden" + + +class NotFound(OpenStackException): + """ + HTTP 404 - Not found + """ + http_status = 404 + message = "Not found" + + +class OverLimit(OpenStackException): + """ + HTTP 413 - Over limit: you're over the API limits for this time period. + """ + http_status = 413 + message = "Over limit" + + +# NotImplemented is a python keyword. +class HTTPNotImplemented(OpenStackException): + """ + HTTP 501 - Not Implemented: the server does not support this operation. + """ + http_status = 501 + message = "Not Implemented" + + +# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() +# so we can do this: +# _code_map = dict((c.http_status, c) +# for c in OpenStackException.__subclasses__()) +# +# Instead, we have to hardcode it: +_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, + Forbidden, NotFound, OverLimit, HTTPNotImplemented]) + + +def from_response(response, body): + """ + Return an instance of an OpenStackException 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, OpenStackException) + if body: + message = "n/a" + details = "n/a" + if hasattr(body, 'keys'): + error = body[body.keys()[0]] + message = error.get('message', None) + details = error.get('details', None) + return cls(code=response.status, message=message, details=details) + else: + return cls(code=response.status) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py new file mode 100644 index 000000000..f4a82a962 --- /dev/null +++ b/novaclient/v1_1/flavors.py @@ -0,0 +1,41 @@ +# Copyright 2010 Jacob Kaplan-Moss +""" +Flavor interface. +""" + +from novaclient.v1_1 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, detailed=True): + """ + Get a list of all flavors. + + :rtype: list of :class:`Flavor`. + """ + detail = "" + if detailed: + detail = "/detail" + return self._list("/flavors%s" % 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") diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py new file mode 100644 index 000000000..48d86ac8d --- /dev/null +++ b/novaclient/v1_1/images.py @@ -0,0 +1,86 @@ +# Copyright 2010 Jacob Kaplan-Moss +""" +Image interface. +""" + +from novaclient.v1_1 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, detailed=True): + """ + Get a list of all images. + + :rtype: list of :class:`Image` + """ + detail = "" + if detailed: + detail = "/detail" + return self._list("/images%s" % detail, "images") + + + def create(self, server, name, image_type=None, backup_type=None, rotation=None): + """ + 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` + """ + 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}} + 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)) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py new file mode 100644 index 000000000..fbba4ae18 --- /dev/null +++ b/novaclient/v1_1/servers.py @@ -0,0 +1,300 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Server interface. +""" + +import urllib +from novaclient.v1_1 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): + """ + 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) + + def create_image(self, name, metadata=None): + """ + Create an image based on this server. + + :param name: The name of the image to create + :param metadata: The metadata to associated with the image. + """ + self.manager.create_image(self, name, metadata) + + def change_password(self, password): + """ + Update the root password on this server. + + :param password: The password to set. + """ + self.manager.change_password(self, password) + + 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 public_ip(self): + """ + Shortcut to get this server's primary public IP address. + """ + if len(self.addresses['public']) == 0: + return "" + return self.addresses['public'] + + @property + def private_ip(self): + """ + Shortcut to get this server's primary private IP address. + """ + if len(self.addresses['private']) == 0: + return "" + return self.addresses['private'] + + @property + def image_id(self): + """ + Shortcut to get the image identifier. + """ + return self.image["id"] + + @property + def flavor_id(self): + """ + Shortcut to get the flavor identifier. + """ + return self.flavor["id"] + + + + +class ServerManager(base.BootingManagerWithFind): + 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, detailed=True, search_opts=None): + """ + Get a list of servers. + Optional detailed returns details server info. + Optional reservation_id only returns instances with that + reservation_id. + + :rtype: list of :class:`Server` + """ + if search_opts is None: + search_opts = {} + qparams = {} + # only use values in query string if they are set + for opt, val in search_opts.iteritems(): + if val: + qparams[opt] = val + + query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" + + detail = "" + if detailed: + detail = "/detail" + return self._list("/servers%s%s" % (detail, query_string), "servers") + + def create(self, name, image, flavor, 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 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. + """ + return self._boot("/servers", "server", name, image, flavor, + meta=meta, files=files) + + def update(self, server, name=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. + """ + if name is None: + return + + body = { + "server": { + "name": name, + }, + } + + 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 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 create_image(self, server, name, metadata=None): + """ + Create an image based on this server. + + :param server: The :class:`Server` (or its ID) to create image from. + :param name: The name of the image to create + :param metadata: The metadata to associated with the image. + """ + body = { + "name": name, + "metadata": metadata or {}, + } + self._action('createImage', server, body) + + def change_password(self, server, password): + """ + Update the root password on a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param password: The password to set. + """ + body = { + "adminPass": password, + } + self._action('changePassword', server, body) + + 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, {'imageRef': 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, {'flavorRef': 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}) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py new file mode 100644 index 000000000..7f9217e09 --- /dev/null +++ b/novaclient/v1_1/shell.py @@ -0,0 +1,584 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Command-line interface to the OpenStack Nova API. +""" + +import argparse +import getpass +import httplib2 +import os +import prettytable +import sys +import textwrap +import uuid + +import novaclient.v1_1 +from novaclient.v1_1 import exceptions +from novaclient.v1_1 import servers + + +def pretty_choice_list(l): + return ', '.join("'%s'" % i for i in l) + +# Sentinal for boot --key +AUTO_KEY = object() + + +# 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 OpenStackShell(object): + + # Hook for the test suite to inject a fake server. + _api_class = novaclient.v1_1.Client + + def __init__(self): + self.parser = argparse.ArgumentParser( + prog='nova', + description=__doc__.strip(), + epilog='See "nova help COMMAND" '\ + 'for help on a specific command.', + add_help=False, + formatter_class=OpenStackHelpFormatter, + ) + + # 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('NOVA_USERNAME'), + help='Defaults to env[NOVA_USERNAME].') + + self.parser.add_argument('--apikey', + default=env('NOVA_API_KEY'), + help='Defaults to env[NOVA_API_KEY].') + + self.parser.add_argument('--projectid', + default=env('NOVA_PROJECT_ID'), + help='Defaults to env[NOVA_PROJECT_ID].') + + auth_url = env('NOVA_URL') + if auth_url == '': + auth_url = 'https://auth.api.rackspacecloud.com/v1.0' + self.parser.add_argument('--url', + default=auth_url, + help='Defaults to env[NOVA_URL].') + + # 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=OpenStackHelpFormatter + ) + 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, projectid, url = args.username, args.apikey, \ + args.projectid, args.url + + #FIXME(usrleon): Here should be restrict for project id same as + # for username or apikey but for compatibility it is not. + + if not user: + raise CommandError("You must provide a username, either via " + "--username or via env[NOVA_USERNAME]") + if not apikey: + raise CommandError("You must provide an API key, either via " + "--apikey or via env[NOVA_API_KEY]") + + self.cs = self._api_class(user, apikey, projectid, url) + try: + self.cs.authenticate() + except exceptions.Unauthorized: + raise CommandError("Invalid OpenStack Nova 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() + + def _boot(self, args, reservation_id=None, min_count=None, max_count=None): + """Boot a new server.""" + flavor = args.flavor or self.cs.flavors.find(ram=256) + image = args.image or self.cs.images.find(name="Ubuntu 10.04 LTS "\ + "(lucid)") + + metadata = dict(v.split('=') for v in args.meta) + + files = {} + for f in args.files: + dst, src = f.split('=', 1) + try: + files[dst] = open(src) + except IOError, e: + raise CommandError("Can't open '%s': %s" % (src, e)) + + if args.key is AUTO_KEY: + possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k) + for k in ('id_dsa.pub', 'id_rsa.pub')] + for k in possible_keys: + if os.path.exists(k): + keyfile = k + break + else: + raise CommandError("Couldn't find a key file: tried " + "~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub") + elif args.key: + keyfile = args.key + else: + keyfile = None + + if keyfile: + try: + files['/root/.ssh/authorized_keys2'] = open(keyfile) + except IOError, e: + raise CommandError("Can't open '%s': %s" % (keyfile, e)) + + return (args.name, image, flavor, metadata, files) + + @arg('--flavor', + default=None, + metavar='', + help="Flavor ID (see 'novaclient flavors'). "\ + "Defaults to 256MB RAM instance.") + @arg('--image', + default=None, + metavar='', + help="Image ID (see 'novaclient images'). "\ + "Defaults to Ubuntu 10.04 LTS.") + @arg('--meta', + metavar="", + action='append', + default=[], + help="Record arbitrary key/value metadata. "\ + "May be give multiple times.") + @arg('--file', + metavar="", + action='append', + dest='files', + default=[], + help="Store arbitrary files from locally to "\ + "on the new server. You may store up to 5 files.") + @arg('--key', + metavar='', + nargs='?', + const=AUTO_KEY, + help="Key the server with an SSH keypair. "\ + "Looks in ~/.ssh for a key, "\ + "or takes an explicit to one.") + @arg('name', metavar='', help='Name for the new server') + def do_boot(self, args): + """Boot a new server.""" + name, image, flavor, metadata, files = self._boot(args) + + server = self.cs.servers.create(args.name, image, flavor, + meta=metadata, + files=files) + print_dict(server._info) + + def _translate_flavor_keys(self, collection): + convert = [('ram', 'memory_mb'), ('disk', 'local_gb')] + for item in collection: + keys = item.__dict__.keys() + 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]) + + @arg('--fixed_ip', + dest='fixed_ip', + metavar='', + default=None, + help='Only match against fixed IP.') + @arg('--reservation_id', + dest='reservation_id', + metavar='', + default=None, + help='Only return instances that match reservation_id.') + @arg('--recurse_zones', + dest='recurse_zones', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0, + help='Recurse through all zones if set.') + @arg('--ip', + dest='ip', + metavar='', + default=None, + help='Search with regular expression match by IP address') + @arg('--ip6', + dest='ip6', + metavar='', + default=None, + help='Search with regular expression match by IPv6 address') + @arg('--server_name', + dest='server_name', + metavar='', + default=None, + help='Search with regular expression match by server name') + @arg('--name', + dest='display_name', + metavar='', + default=None, + help='Search with regular expression match by display name') + @arg('--instance_name', + dest='name', + metavar='', + default=None, + help='Search with regular expression match by instance name') + def do_list(self, args): + """List active servers.""" + recurse_zones = args.recurse_zones + search_opts = { + 'reservation_id': args.reservation_id, + 'fixed_ip': args.fixed_ip, + 'recurse_zones': recurse_zones, + 'ip': args.ip, + 'ip6': args.ip6, + 'name': args.name, + 'server_name': args.server_name, + 'display_name': args.display_name} + if recurse_zones: + to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP'] + else: + to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP'] + print_list(self.cs.servers.list(search_opts=search_opts), + to_print) + + def do_flavor_list(self, args): + """Print a list of available 'flavors' (sizes of servers).""" + flavors = self.cs.flavors.list() + self._translate_flavor_keys(flavors) + print_list(flavors, [ + 'ID', + 'Name', + 'Memory_MB', + 'Swap', + 'Local_GB', + 'VCPUs', + 'RXTX_Quota', + 'RXTX_Cap']) + + 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 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.") + def do_create_image(self, args): + """Create a new image by taking a snapshot of a running server.""" + server = self._find_server(args.server) + server.create_image(args.name) + + @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 can only delete images you + created. + """ + image = self._find_image(args.image) + image.delete() + + @arg('--hard', + dest='reboot_type', + action='store_const', + const=servers.REBOOT_HARD, + default=servers.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_migrate(self, args): + """Migrate a server.""" + self._find_server(args.server).migrate() + + @arg('server', metavar='', help='Name or ID of server.') + def do_pause(self, args): + """Pause a server.""" + self._find_server(args.server).pause() + + @arg('server', metavar='', help='Name or ID of server.') + def do_unpause(self, args): + """Unpause a server.""" + self._find_server(args.server).unpause() + + @arg('server', metavar='', help='Name or ID of server.') + def do_suspend(self, args): + """Suspend a server.""" + self._find_server(args.server).suspend() + + @arg('server', metavar='', help='Name or ID of server.') + def do_resume(self, args): + """Resume a server.""" + self._find_server(args.server).resume() + + @arg('server', metavar='', help='Name or ID of server.') + def do_rescue(self, args): + """Rescue a server.""" + self._find_server(args.server).rescue() + + @arg('server', metavar='', help='Name or ID of server.') + def do_unrescue(self, args): + """Unrescue a server.""" + self._find_server(args.server).unrescue() + + @arg('server', metavar='', help='Name or ID of server.') + def do_diagnostics(self, args): + """Retrieve server diagnostics.""" + print_dict(self.cs.servers.diagnostics(args.server)[1]) + + @arg('server', metavar='', help='Name or ID of server.') + def do_actions(self, args): + """Retrieve server actions.""" + print_list( + self.cs.servers.actions(args.server), + ["Created_At", "Action", "Error"]) + + @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.change_password(p1) + + @arg('server', metavar='', help='Name or ID of server.') + def do_show(self, args): + """Show details about the given server.""" + s = self._find_server(args.server) + + info = s._info.copy() + addresses = info.pop('addresses') + for network_name in addresses.keys(): + ips = map(lambda x: x["addr"], addresses[network_name]) + info['%s ip' % network_name] = ', '.join(ips) + + flavor = info.get('flavor', {}) + flavor_id = flavor.get('id') + if flavor_id: + info['flavor'] = self._find_flavor(flavor_id).name + image = info.get('image', {}) + image_id = image.get('id') + if image_id: + info['image'] = self._find_image(image_id).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_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 exceptions.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)) + + try: + uuid.UUID(name_or_id) + return manager.get(name_or_id) + except ValueError: + return manager.find(name=name_or_id) + except exceptions.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 OpenStackHelpFormatter(argparse.HelpFormatter): + def start_section(self, heading): + # Title-case the headings + heading = '%s%s' % (heading[0].upper(), heading[1:]) + super(OpenStackHelpFormatter, self).start_section(heading) + + +# Helpers +def print_list(objs, fields, formatters={}): + pt = prettytable.PrettyTable([f for f in fields], caching=False) + pt.aligns = ['l' for f in fields] + + for o in objs: + row = [] + for field in fields: + if field in formatters: + row.append(formatters[field](o)) + else: + field_name = field.lower().replace(' ', '_') + data = getattr(o, field_name, '') + row.append(data) + pt.add_row(row) + + 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: + OpenStackShell().main(sys.argv[1:]) + + except Exception, e: + if httplib2.debuglevel == 1: + raise # dump stack. + else: + print >> sys.stderr, e + sys.exit(1) diff --git a/novaclient/v1_1/zones.py b/novaclient/v1_1/zones.py new file mode 100644 index 000000000..4e8baa3ef --- /dev/null +++ b/novaclient/v1_1/zones.py @@ -0,0 +1,196 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Zone interface. +""" + +from novaclient.v1_1 import base + + +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 + + def to_dict(self): + """Return the original info setting, which is a dict.""" + return self._info + + +class Zone(base.Resource): + def __init__(self, manager, info): + self.name = "n/a" + self.is_active = "n/a" + self.capabilities = "n/a" + super(Zone, self).__init__(manager, info) + + def __repr__(self): + return "" % self.api_url + + def delete(self): + """ + Delete a child zone. + """ + self.manager.delete(self) + + def update(self, api_url=None, username=None, password=None, + weight_offset=None, weight_scale=None): + """ + Update the name for this child zone. + + :param api_url: Update the child zone's API URL. + :param username: Update the child zone's username. + :param password: Update the child zone's password. + :param weight_offset: Update the child zone's weight offset. + :param weight_scale: Update the child zone's weight scale. + """ + self.manager.update(self, api_url, username, password, + weight_offset, weight_scale) + + +class ZoneManager(base.BootingManagerWithFind): + resource_class = Zone + + def info(self): + """ + Get info on this zone. + + :rtype: :class:`Zone` + """ + return self._get("/zones/info", "zone") + + def get(self, zone): + """ + Get a child zone. + + :param server: ID of the :class:`Zone` to get. + :rtype: :class:`Zone` + """ + return self._get("/zones/%s" % base.getid(zone), "zone") + + def list(self, detailed=True): + """ + Get a list of child zones. + :rtype: list of :class:`Zone` + """ + detail = "" + if detailed: + detail = "/detail" + return self._list("/zones%s" % detail, "zones") + + def create(self, api_url, username, password, + weight_offset=0.0, weight_scale=1.0): + """ + Create a new child zone. + + :param api_url: The child zone's API URL. + :param username: The child zone's username. + :param password: The child zone's password. + :param weight_offset: The child zone's weight offset. + :param weight_scale: The child zone's weight scale. + """ + body = {"zone": { + "api_url": api_url, + "username": username, + "password": password, + "weight_offset": weight_offset, + "weight_scale": weight_scale + }} + + return self._create("/zones", body, "zone") + + def boot(self, name, image, flavor, ipgroup=None, meta=None, files=None, + zone_blob=None, reservation_id=None, min_count=None, + max_count=None): + """ + Create (boot) a new server while being aware of Zones. + + :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. + :param zone_blob: a single (encrypted) string which is used internally + by Nova for routing between Zones. Users cannot populate + this field. + :param reservation_id: a UUID for the set of servers being requested. + :param min_count: minimum number of servers to create. + :param max_count: maximum number of servers to create. + """ + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + return self._boot("/zones/boot", "reservation_id", name, image, flavor, + ipgroup=ipgroup, meta=meta, files=files, + zone_blob=zone_blob, reservation_id=reservation_id, + return_raw=True, min_count=min_count, + max_count=max_count) + + def select(self, *args, **kwargs): + """ + Given requirements for a new instance, select hosts + in this zone that best match those requirements. + """ + # 'specs' may be passed in as None, so change to an empty string. + specs = kwargs.get("specs") or "" + url = "/zones/select" + weighting_list = self._list(url, "weights", Weighting, body=specs) + return [wt.to_dict() for wt in weighting_list] + + def delete(self, zone): + """ + Delete a child zone. + """ + self._delete("/zones/%s" % base.getid(zone)) + + def update(self, zone, api_url=None, username=None, password=None, + weight_offset=None, weight_scale=None): + """ + Update the name or the api_url for a zone. + + :param zone: The :class:`Zone` (or its ID) to update. + :param api_url: Update the API URL. + :param username: Update the username. + :param password: Update the password. + :param weight_offset: Update the child zone's weight offset. + :param weight_scale: Update the child zone's weight scale. + """ + + body = {"zone": {}} + if api_url: + body["zone"]["api_url"] = api_url + if username: + body["zone"]["username"] = username + if password: + body["zone"]["password"] = password + if weight_offset: + body["zone"]["weight_offset"] = weight_offset + if weight_scale: + body["zone"]["weight_scale"] = weight_scale + if not len(body["zone"]): + return + self._update("/zones/%s" % base.getid(zone), body) diff --git a/tests/test_backup_schedules.py b/tests/test_backup_schedules.py deleted file mode 100644 index 60b2ac5ea..000000000 --- a/tests/test_backup_schedules.py +++ /dev/null @@ -1,58 +0,0 @@ - -from novaclient.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') diff --git a/tests/test_flavors.py b/tests/test_flavors.py deleted file mode 100644 index cf4c6cfb8..000000000 --- a/tests/test_flavors.py +++ /dev/null @@ -1,37 +0,0 @@ -from novaclient 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_list_flavors_undetailed(): - fl = cs.flavors.list(detailed=False) - cs.assert_called('GET', '/flavors') - [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) diff --git a/tests/test_images.py b/tests/test_images.py deleted file mode 100644 index 1cc150a3f..000000000 --- a/tests/test_images.py +++ /dev/null @@ -1,47 +0,0 @@ -from novaclient 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_list_images_undetailed(): - il = cs.images.list(detailed=False) - cs.assert_called('GET', '/images') - [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') diff --git a/tests/test_ipgroups.py b/tests/test_ipgroups.py deleted file mode 100644 index 98a6f151d..000000000 --- a/tests/test_ipgroups.py +++ /dev/null @@ -1,48 +0,0 @@ -from novaclient 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_list_ipgroups_undetailed(): - ipl = cs.ipgroups.list(detailed=False) - cs.assert_called('GET', '/shared_ip_groups') - [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})]) diff --git a/tests/test_servers.py b/tests/test_servers.py deleted file mode 100644 index b88ed9212..000000000 --- a/tests/test_servers.py +++ /dev/null @@ -1,174 +0,0 @@ -import StringIO -from nose.tools import assert_equal -from fakeserver import FakeServer -from utils import assert_isinstance -from novaclient 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_list_servers_undetailed(): - sl = cs.servers.list(detailed=False) - cs.assert_called('GET', '/servers') - [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') - - -def test_migrate_server(): - s = cs.servers.get(1234) - s.migrate() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.migrate(s) - cs.assert_called('POST', '/servers/1234/action') - - -def test_add_fixed_ip(): - s = cs.servers.get(1234) - s.add_fixed_ip(1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.add_fixed_ip(s, 1) - cs.assert_called('POST', '/servers/1234/action') - - -def test_remove_fixed_ip(): - s = cs.servers.get(1234) - s.remove_fixed_ip('10.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.remove_fixed_ip(s, '10.0.0.1') - cs.assert_called('POST', '/servers/1234/action') diff --git a/tests/v1_0/__init__.py b/tests/v1_0/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fakeserver.py b/tests/v1_0/fakes.py similarity index 96% rename from tests/fakeserver.py rename to tests/v1_0/fakes.py index 232c7b350..2ee96e0ae 100644 --- a/tests/fakeserver.py +++ b/tests/v1_0/fakes.py @@ -6,21 +6,29 @@ wrong the tests might fail. I've indicated in comments the places where actual behavior differs from the spec. """ -import httplib2 +from __future__ import absolute_import + import urlparse import urllib -from nose.tools import assert_equal -from novaclient import OpenStack -from novaclient.client import OpenStackClient -from utils import fail, assert_in, assert_not_in, assert_has_keys + +import httplib2 + +from novaclient.v1_0 import Client +from novaclient.v1_0.client import HTTPClient + +from .utils import fail, assert_in, assert_not_in, assert_has_keys -class FakeServer(OpenStack): +def assert_equal(value_one, value_two): + assert value_one == value_two + + +class FakeClient(Client): def __init__(self, username=None, password=None, project_id=None, auth_url=None): - super(FakeServer, self).__init__('username', 'apikey', + super(FakeClient, self).__init__('username', 'apikey', 'project_id', 'auth_url') - self.client = FakeClient() + self.client = FakeHTTPClient() def assert_called(self, method, url, body=None): """ @@ -56,7 +64,7 @@ class FakeServer(OpenStack): found = True break - assert found, 'Expected %s %s; got %s' % \ + assert found, 'Expected %s; got %s' % \ (expected, self.client.callstack) if body is not None: assert_equal(entry[2], body) @@ -67,7 +75,7 @@ class FakeServer(OpenStack): pass -class FakeClient(OpenStackClient): +class FakeHTTPClient(HTTPClient): def __init__(self): self.username = 'username' self.apikey = 'apikey' @@ -412,7 +420,7 @@ class FakeClient(OpenStackClient): 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/v1_0/test_accounts.py similarity index 62% rename from tests/test_accounts.py rename to tests/v1_0/test_accounts.py index 37e37c553..93eeed92f 100644 --- a/tests/test_accounts.py +++ b/tests/v1_0/test_accounts.py @@ -1,14 +1,13 @@ +from __future__ import absolute_import + import StringIO -from nose.tools import assert_equal +from .fakes import FakeClient -from fakeserver import FakeServer -from novaclient import Account - -cs = FakeServer() +os = FakeClient() def test_instance_creation_for_account(): - s = cs.accounts.create_instance_for( + s = os.accounts.create_instance_for( account_id='test_account', name="My server", image=1, @@ -19,4 +18,4 @@ def test_instance_creation_for_account(): '/etc/passwd': 'some data', # a file '/tmp/foo.txt': StringIO.StringIO('data') # a stream }) - cs.assert_called('POST', '/accounts/test_account/create_instance') + os.assert_called('POST', '/accounts/test_account/create_instance') diff --git a/tests/test_auth.py b/tests/v1_0/test_auth.py similarity index 81% rename from tests/test_auth.py rename to tests/v1_0/test_auth.py index 206fb851d..d27907cb4 100644 --- a/tests/test_auth.py +++ b/tests/v1_0/test_auth.py @@ -1,11 +1,13 @@ import mock -import novaclient import httplib2 from nose.tools import assert_raises, assert_equal +import novaclient.v1_0 +from novaclient.v1_0 import exceptions + def test_authenticate_success(): - cs = novaclient.OpenStack("username", "apikey", "project_id") + cs = novaclient.v1_0.Client("username", "apikey", "project_id") auth_response = httplib2.Response({ 'status': 204, 'x-server-management-url': @@ -32,19 +34,19 @@ def test_authenticate_success(): def test_authenticate_failure(): - cs = novaclient.OpenStack("username", "apikey", "project_id") + cs = novaclient.v1_0.Client("username", "apikey", "project_id") auth_response = httplib2.Response({'status': 401}) mock_request = mock.Mock(return_value=(auth_response, None)) @mock.patch.object(httplib2.Http, "request", mock_request) def test_auth_call(): - assert_raises(novaclient.Unauthorized, cs.client.authenticate) + assert_raises(exceptions.Unauthorized, cs.client.authenticate) test_auth_call() def test_auth_automatic(): - client = novaclient.OpenStack("username", "apikey", "project_id").client + client = novaclient.v1_0.Client("username", "apikey", "project_id").client client.management_url = '' mock_request = mock.Mock(return_value=(None, None)) @@ -59,7 +61,7 @@ def test_auth_automatic(): def test_auth_manual(): - cs = novaclient.OpenStack("username", "apikey", "project_id") + cs = novaclient.v1_0.Client("username", "apikey", "project_id") @mock.patch.object(cs.client, 'authenticate') def test_auth_call(m): diff --git a/tests/v1_0/test_backup_schedules.py b/tests/v1_0/test_backup_schedules.py new file mode 100644 index 000000000..831f81067 --- /dev/null +++ b/tests/v1_0/test_backup_schedules.py @@ -0,0 +1,60 @@ +from __future__ import absolute_import + +from novaclient.v1_0 import backup_schedules + +from .fakes import FakeClient +from .utils import assert_isinstance + +os = FakeClient() + + +def test_get_backup_schedule(): + s = os.servers.get(1234) + + # access via manager + b = os.backup_schedules.get(server=s) + assert_isinstance(b, backup_schedules.BackupSchedule) + os.assert_called('GET', '/servers/1234/backup_schedule') + + b = os.backup_schedules.get(server=1234) + assert_isinstance(b, backup_schedules.BackupSchedule) + os.assert_called('GET', '/servers/1234/backup_schedule') + + # access via instance + assert_isinstance(s.backup_schedule, backup_schedules.BackupSchedule) + os.assert_called('GET', '/servers/1234/backup_schedule') + + # Just for coverage's sake + b = s.backup_schedule.get() + os.assert_called('GET', '/servers/1234/backup_schedule') + + +def test_create_update_backup_schedule(): + s = os.servers.get(1234) + + # create/update via manager + os.backup_schedules.update( + server=s, + enabled=True, + weekly=backup_schedules.BACKUP_WEEKLY_THURSDAY, + daily=backup_schedules.BACKUP_DAILY_H_1000_1200 + ) + os.assert_called('POST', '/servers/1234/backup_schedule') + + # and via instance + s.backup_schedule.update(enabled=False) + os.assert_called('POST', '/servers/1234/backup_schedule') + + +def test_delete_backup_schedule(): + s = os.servers.get(1234) + + # delete via manager + os.backup_schedules.delete(s) + os.assert_called('DELETE', '/servers/1234/backup_schedule') + os.backup_schedules.delete(1234) + os.assert_called('DELETE', '/servers/1234/backup_schedule') + + # and via instance + s.backup_schedule.delete() + os.assert_called('DELETE', '/servers/1234/backup_schedule') diff --git a/tests/test_base.py b/tests/v1_0/test_base.py similarity index 50% rename from tests/test_base.py rename to tests/v1_0/test_base.py index 8477987f7..db440bbe6 100644 --- a/tests/test_base.py +++ b/tests/v1_0/test_base.py @@ -1,59 +1,61 @@ +from __future__ import absolute_import import mock -import novaclient.base -from novaclient import Flavor -from novaclient.exceptions import NotFound -from novaclient.base import Resource from nose.tools import assert_equal, assert_not_equal, assert_raises -from fakeserver import FakeServer -cs = FakeServer() +from novaclient.v1_0 import flavors +from novaclient.v1_0 import exceptions +from novaclient.v1_0 import base + +from .fakes import FakeClient + +os = FakeClient() def test_resource_repr(): - r = Resource(None, dict(foo="bar", baz="spam")) + r = base.Resource(None, dict(foo="bar", baz="spam")) assert_equal(repr(r), "") def test_getid(): - assert_equal(novaclient.base.getid(4), 4) + assert_equal(base.getid(4), 4) class O(object): id = 4 - assert_equal(novaclient.base.getid(O), 4) + assert_equal(base.getid(O), 4) def test_resource_lazy_getattr(): - f = Flavor(cs.flavors, {'id': 1}) + f = flavors.Flavor(os.flavors, {'id': 1}) assert_equal(f.name, '256 MB Server') - cs.assert_called('GET', '/flavors/1') + os.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') + os.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'}) + r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) + r2 = base.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}) + r1 = base.Resource(None, {'id': 1}) + r2 = flavors.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}) + r1 = base.Resource(None, {'name': 'joe', 'age': 12}) + r2 = base.Resource(None, {'name': 'joe', 'age': 12}) assert_equal(r1, r2) def test_findall_invalid_attribute(): # Make sure findall with an invalid attribute doesn't cause errors. # The following should not raise an exception. - cs.flavors.findall(vegetable='carrot') + os.flavors.findall(vegetable='carrot') # However, find() should raise an error - assert_raises(NotFound, cs.flavors.find, vegetable='carrot') + assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot') diff --git a/tests/test_client.py b/tests/v1_0/test_client.py similarity index 88% rename from tests/test_client.py rename to tests/v1_0/test_client.py index b22de0347..e7a63b7d7 100644 --- a/tests/test_client.py +++ b/tests/v1_0/test_client.py @@ -1,6 +1,7 @@ import mock import httplib2 -from novaclient.client import OpenStackClient + +from novaclient.v1_0 import client from nose.tools import assert_equal fake_response = httplib2.Response({"status": 200}) @@ -8,15 +9,15 @@ fake_body = '{"hi": "there"}' mock_request = mock.Mock(return_value=(fake_response, fake_body)) -def client(): - cl = OpenStackClient("username", "apikey", "project_id", "auth_test") +def get_client(): + cl = client.HTTPClient("username", "apikey", "project_id", "auth_test") cl.management_url = "http://example.com" cl.auth_token = "token" return cl def test_get(): - cl = client() + cl = get_client() @mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch('time.time', mock.Mock(return_value=1234)) @@ -34,7 +35,7 @@ def test_get(): def test_post(): - cl = client() + cl = get_client() @mock.patch.object(httplib2.Http, "request", mock_request) def test_post_call(): diff --git a/tests/v1_0/test_flavors.py b/tests/v1_0/test_flavors.py new file mode 100644 index 000000000..3c9f644c3 --- /dev/null +++ b/tests/v1_0/test_flavors.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import + +from nose.tools import assert_raises, assert_equal + +from novaclient.v1_0 import flavors +from novaclient.v1_0 import exceptions + +from .fakes import FakeClient +from .utils import assert_isinstance + +os = FakeClient() + + +def test_list_flavors(): + fl = os.flavors.list() + os.assert_called('GET', '/flavors/detail') + [assert_isinstance(f, flavors.Flavor) for f in fl] + + +def test_list_flavors_undetailed(): + fl = os.flavors.list(detailed=False) + os.assert_called('GET', '/flavors') + [assert_isinstance(f, flavors.Flavor) for f in fl] + + +def test_get_flavor_details(): + f = os.flavors.get(1) + os.assert_called('GET', '/flavors/1') + assert_isinstance(f, flavors.Flavor) + assert_equal(f.ram, 256) + assert_equal(f.disk, 10) + + +def test_find(): + f = os.flavors.find(ram=256) + os.assert_called('GET', '/flavors/detail') + assert_equal(f.name, '256 MB Server') + + f = os.flavors.find(disk=20) + assert_equal(f.name, '512 MB Server') + + assert_raises(exceptions.NotFound, os.flavors.find, disk=12345) diff --git a/tests/v1_0/test_images.py b/tests/v1_0/test_images.py new file mode 100644 index 000000000..9e9bb4732 --- /dev/null +++ b/tests/v1_0/test_images.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import + +from nose.tools import assert_equal + +from novaclient.v1_0 import images + +from .fakes import FakeClient +from .utils import assert_isinstance + +os = FakeClient() + + +def test_list_images(): + il = os.images.list() + os.assert_called('GET', '/images/detail') + [assert_isinstance(i, images.Image) for i in il] + + +def test_list_images_undetailed(): + il = os.images.list(detailed=False) + os.assert_called('GET', '/images') + [assert_isinstance(i, images.Image) for i in il] + + +def test_get_image_details(): + i = os.images.get(1) + os.assert_called('GET', '/images/1') + assert_isinstance(i, images.Image) + assert_equal(i.id, 1) + assert_equal(i.name, 'CentOS 5.2') + + +def test_create_image(): + i = os.images.create(server=1234, name="Just in case") + os.assert_called('POST', '/images') + assert_isinstance(i, images.Image) + + +def test_delete_image(): + os.images.delete(1) + os.assert_called('DELETE', '/images/1') + + +def test_find(): + i = os.images.find(name="CentOS 5.2") + assert_equal(i.id, 1) + os.assert_called('GET', '/images/detail') + + iml = os.images.findall(status='SAVING') + assert_equal(len(iml), 1) + assert_equal(iml[0].name, 'My Server Backup') diff --git a/tests/v1_0/test_ipgroups.py b/tests/v1_0/test_ipgroups.py new file mode 100644 index 000000000..3c89d4c68 --- /dev/null +++ b/tests/v1_0/test_ipgroups.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import + +from nose.tools import assert_equal + +from novaclient.v1_0 import ipgroups + +from .fakes import FakeClient +from .utils import assert_isinstance + +os = FakeClient() + + +def test_list_ipgroups(): + ipl = os.ipgroups.list() + os.assert_called('GET', '/shared_ip_groups/detail') + [assert_isinstance(ipg, ipgroups.IPGroup) for ipg in ipl] + + +def test_list_ipgroups_undetailed(): + ipl = os.ipgroups.list(detailed=False) + os.assert_called('GET', '/shared_ip_groups') + [assert_isinstance(ipg, ipgroups.IPGroup) for ipg in ipl] + + +def test_get_ipgroup(): + ipg = os.ipgroups.get(1) + os.assert_called('GET', '/shared_ip_groups/1') + assert_isinstance(ipg, ipgroups.IPGroup) + + +def test_create_ipgroup(): + ipg = os.ipgroups.create("My group", 1234) + os.assert_called('POST', '/shared_ip_groups') + assert_isinstance(ipg, ipgroups.IPGroup) + + +def test_delete_ipgroup(): + ipg = os.ipgroups.get(1) + ipg.delete() + os.assert_called('DELETE', '/shared_ip_groups/1') + os.ipgroups.delete(ipg) + os.assert_called('DELETE', '/shared_ip_groups/1') + os.ipgroups.delete(1) + os.assert_called('DELETE', '/shared_ip_groups/1') + + +def test_find(): + ipg = os.ipgroups.find(name='group1') + os.assert_called('GET', '/shared_ip_groups/detail') + assert_equal(ipg.name, 'group1') + ipgl = os.ipgroups.findall(id=1) + assert_equal(ipgl, [ipgroups.IPGroup(None, {'id': 1})]) diff --git a/tests/v1_0/test_servers.py b/tests/v1_0/test_servers.py new file mode 100644 index 000000000..e32b3d61b --- /dev/null +++ b/tests/v1_0/test_servers.py @@ -0,0 +1,180 @@ +from __future__ import absolute_import + +import StringIO + +from nose.tools import assert_equal + +from novaclient.v1_0 import servers + +from .fakes import FakeClient +from .utils import assert_isinstance + + +os = FakeClient() + + +def test_list_servers(): + sl = os.servers.list() + os.assert_called('GET', '/servers/detail') + [assert_isinstance(s, servers.Server) for s in sl] + + +def test_list_servers_undetailed(): + sl = os.servers.list(detailed=False) + os.assert_called('GET', '/servers') + [assert_isinstance(s, servers.Server) for s in sl] + + +def test_get_server_details(): + s = os.servers.get(1234) + os.assert_called('GET', '/servers/1234') + assert_isinstance(s, servers.Server) + assert_equal(s.id, 1234) + assert_equal(s.status, 'BUILD') + + +def test_create_server(): + s = os.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 + } + ) + os.assert_called('POST', '/servers') + assert_isinstance(s, servers.Server) + + +def test_update_server(): + s = os.servers.get(1234) + + # Update via instance + s.update(name='hi') + os.assert_called('PUT', '/servers/1234') + s.update(name='hi', password='there') + os.assert_called('PUT', '/servers/1234') + + # Silly, but not an error + s.update() + + # Update via manager + os.servers.update(s, name='hi') + os.assert_called('PUT', '/servers/1234') + os.servers.update(1234, password='there') + os.assert_called('PUT', '/servers/1234') + os.servers.update(s, name='hi', password='there') + os.assert_called('PUT', '/servers/1234') + + +def test_delete_server(): + s = os.servers.get(1234) + s.delete() + os.assert_called('DELETE', '/servers/1234') + os.servers.delete(1234) + os.assert_called('DELETE', '/servers/1234') + os.servers.delete(s) + os.assert_called('DELETE', '/servers/1234') + + +def test_find(): + s = os.servers.find(name='sample-server') + os.assert_called('GET', '/servers/detail') + assert_equal(s.name, 'sample-server') + + # Find with multiple results arbitraility returns the first item + s = os.servers.find(flavorId=1) + sl = os.servers.findall(flavorId=1) + assert_equal(sl[0], s) + assert_equal([s.id for s in sl], [1234, 5678]) + + +def test_share_ip(): + s = os.servers.get(1234) + + # Share via instance + s.share_ip(ipgroup=1, address='1.2.3.4') + os.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4') + + # Share via manager + os.servers.share_ip(s, ipgroup=1, address='1.2.3.4', configure=False) + os.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4') + + +def test_unshare_ip(): + s = os.servers.get(1234) + + # Unshare via instance + s.unshare_ip('1.2.3.4') + os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4') + + # Unshare via manager + os.servers.unshare_ip(s, '1.2.3.4') + os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4') + + +def test_reboot_server(): + s = os.servers.get(1234) + s.reboot() + os.assert_called('POST', '/servers/1234/action') + os.servers.reboot(s, type='HARD') + os.assert_called('POST', '/servers/1234/action') + + +def test_rebuild_server(): + s = os.servers.get(1234) + s.rebuild(image=1) + os.assert_called('POST', '/servers/1234/action') + os.servers.rebuild(s, image=1) + os.assert_called('POST', '/servers/1234/action') + + +def test_resize_server(): + s = os.servers.get(1234) + s.resize(flavor=1) + os.assert_called('POST', '/servers/1234/action') + os.servers.resize(s, flavor=1) + os.assert_called('POST', '/servers/1234/action') + + +def test_confirm_resized_server(): + s = os.servers.get(1234) + s.confirm_resize() + os.assert_called('POST', '/servers/1234/action') + os.servers.confirm_resize(s) + os.assert_called('POST', '/servers/1234/action') + + +def test_revert_resized_server(): + s = os.servers.get(1234) + s.revert_resize() + os.assert_called('POST', '/servers/1234/action') + os.servers.revert_resize(s) + os.assert_called('POST', '/servers/1234/action') + + +def test_migrate_server(): + s = os.servers.get(1234) + s.migrate() + os.assert_called('POST', '/servers/1234/action') + os.servers.migrate(s) + os.assert_called('POST', '/servers/1234/action') + + +def test_add_fixed_ip(): + s = os.servers.get(1234) + s.add_fixed_ip(1) + os.assert_called('POST', '/servers/1234/action') + os.servers.add_fixed_ip(s, 1) + os.assert_called('POST', '/servers/1234/action') + + +def test_remove_fixed_ip(): + s = os.servers.get(1234) + s.remove_fixed_ip('10.0.0.1') + os.assert_called('POST', '/servers/1234/action') + os.servers.remove_fixed_ip(s, '10.0.0.1') + os.assert_called('POST', '/servers/1234/action') diff --git a/tests/test_shell.py b/tests/v1_0/test_shell.py similarity index 97% rename from tests/test_shell.py rename to tests/v1_0/test_shell.py index 17ba5a442..bc607292e 100644 --- a/tests/test_shell.py +++ b/tests/v1_0/test_shell.py @@ -1,10 +1,15 @@ +from __future__ import absolute_import + import os import mock import httplib2 + from nose.tools import assert_raises, assert_equal -from novaclient.shell import OpenStackShell, CommandError -from fakeserver import FakeServer -from utils import assert_in + +from novaclient.v1_0.shell import OpenStackShell, CommandError + +from .fakes import FakeClient +from .utils import assert_in # Patch os.environ to avoid required auth info. @@ -21,7 +26,7 @@ def setup(): # of asserting that certain API calls were made. global shell, _shell, assert_called, assert_called_anytime _shell = OpenStackShell() - _shell._api_class = FakeServer + _shell._api_class = FakeClient assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b) assert_called_anytime = lambda m, u, b=None: \ _shell.cs.assert_called_anytime(m, u, b) @@ -330,7 +335,7 @@ 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'}} ) diff --git a/tests/test_zones.py b/tests/v1_0/test_zones.py similarity index 84% rename from tests/test_zones.py rename to tests/v1_0/test_zones.py index cd773ad3e..2e6aa8487 100644 --- a/tests/test_zones.py +++ b/tests/v1_0/test_zones.py @@ -1,28 +1,33 @@ -import StringIO -from nose.tools import assert_equal -from fakeserver import FakeServer -from utils import assert_isinstance -from novaclient import Zone +from __future__ import absolute_import -os = FakeServer() +import StringIO + +from nose.tools import assert_equal + +from novaclient.v1_0 import zones + +from .fakes import FakeClient +from .utils import assert_isinstance + +os = FakeClient() def test_list_zones(): sl = os.zones.list() os.assert_called('GET', '/zones/detail') - [assert_isinstance(s, Zone) for s in sl] + [assert_isinstance(s, zones.Zone) for s in sl] def test_list_zones_undetailed(): sl = os.zones.list(detailed=False) os.assert_called('GET', '/zones') - [assert_isinstance(s, Zone) for s in sl] + [assert_isinstance(s, zones.Zone) for s in sl] def test_get_zone_details(): s = os.zones.get(1) os.assert_called('GET', '/zones/1') - assert_isinstance(s, Zone) + assert_isinstance(s, zones.Zone) assert_equal(s.id, 1) assert_equal(s.api_url, 'http://foo.com') @@ -31,7 +36,7 @@ def test_create_zone(): s = os.zones.create(api_url="http://foo.com", username='bob', password='xxx') os.assert_called('POST', '/zones') - assert_isinstance(s, Zone) + assert_isinstance(s, zones.Zone) def test_update_zone(): diff --git a/tests/testfile.txt b/tests/v1_0/testfile.txt similarity index 100% rename from tests/testfile.txt rename to tests/v1_0/testfile.txt diff --git a/tests/utils.py b/tests/v1_0/utils.py similarity index 100% rename from tests/utils.py rename to tests/v1_0/utils.py diff --git a/tests/v1_1/__init__.py b/tests/v1_1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py new file mode 100644 index 000000000..fc7793bf4 --- /dev/null +++ b/tests/v1_1/fakes.py @@ -0,0 +1,496 @@ +""" +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 urlparse +import urllib + +import httplib2 + +from novaclient.v1_1 import Client +from novaclient.v1_1.client import HTTPClient + +from .utils import fail, assert_in, assert_not_in, assert_has_keys + + +def assert_equal(value_one, value_two): + try: + assert value_one == value_two + except AssertionError: + print "%(value_one)s does not equal %(value_two)s" % locals() + raise + + +class FakeClient(Client): + def __init__(self, username=None, password=None, project_id=None, + auth_url=None): + super(FakeClient, self).__init__('username', 'apikey', + 'project_id', 'auth_url') + self.client = FakeHTTPClient() + + 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 assert_called_anytime(self, method, url, body=None): + """ + Assert than an API method was called anytime in the test. + """ + expected = (method, url) + + assert self.client.callstack, \ + "Expected %s %s but no calls were made." % expected + + found = False + for entry in self.client.callstack: + called = entry[0:2] + if expected == entry[0:2]: + found = True + break + + assert found, 'Expected %s; got %s' % \ + (expected, self.client.callstack) + if body is not None: + assert_equal(entry[2], body) + + self.client.callstack = [] + + def authenticate(self): + pass + + +class FakeHTTPClient(HTTPClient): + def __init__(self): + self.username = 'username' + self.apikey = 'apikey' + self.auth_url = 'auth_url' + 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 + + def _munge_get_url(self, url): + return url + + # + # 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", + "image": { + "id": 2, + }, + "flavor": { + "id": 1, + }, + "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", + "status": "BUILD", + "progress": 60, + "addresses": { + "public": [{ + "addr": "1.2.3.4", + "version": 4, + }, + { + "addr": "5.6.7.8", + "version": 4, + }], + "private": [{ + "addr": "10.11.12.13", + "version": 4, + }], + }, + "metadata": { + "Server Label": "Web Head 1", + "Image Version": "2.1" + } + }, + { + "id": 5678, + "name": "sample-server2", + "image": { + "id": 2, + }, + "flavor": { + "id": 1, + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [{ + "addr": "1.2.3.5", + "version": 4, + }, + { + "addr": "5.6.7.9", + "version": 4, + }], + "private": [{ + "addr": "10.13.12.13", + "version": 4, + }], + }, + "metadata": { + "Server Label": "DB 1" + } + } + ]}) + + def post_servers(self, body, **kw): + assert_equal(body.keys(), ['server']) + assert_has_keys(body['server'], + required=['name', 'imageRef', 'flavorRef'], + optional=['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 get_servers_5678(self, **kw): + r = {'server': self.get_servers_detail()[1]['servers'][1]} + 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(), ['imageRef']) + elif action == 'resize': + assert_equal(body[action].keys(), ['flavorRef']) + 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) + elif action == 'changePassword': + assert_equal(body[action].keys(), ["adminPass"]) + elif action == 'createImage': + assert_equal(body[action].keys(), ["name", "metadata"]) + 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]}) + + def get_flavors_2(self, **kw): + return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][1]}) + + # + # 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', 'image_type', 'backup_type', 'rotation']) + 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) + + # + # Zones + # + def get_zones(self, **kw): + return (200, {'zones': [ + {'id': 1, 'api_url': 'http://foo.com', 'username': 'bob'}, + {'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', + 'password': 'qwerty'}, + {'id': 2, 'api_url': 'http://foo.com', 'username': 'alice', + 'password': 'password'} + ]}) + + def get_zones_1(self, **kw): + r = {'zone': self.get_zones_detail()[1]['zones'][0]} + return (200, r) + + def get_zones_2(self, **kw): + r = {'zone': self.get_zones_detail()[1]['zones'][1]} + return (200, r) + + def post_zones(self, body, **kw): + assert_equal(body.keys(), ['zone']) + assert_has_keys(body['zone'], + required=['api_url', 'username', 'password'], + optional=['weight_offset', 'weight_scale']) + + return (202, self.get_zones_1()[1]) + + def put_zones_1(self, body, **kw): + assert_equal(body.keys(), ['zone']) + assert_has_keys(body['zone'], optional=['api_url', 'username', + 'password', + 'weight_offset', + 'weight_scale']) + return (204, None) + + def delete_zones_1(self, **kw): + return (202, None) + + # + # Accounts + # + def post_accounts_test_account_create_instance(self, body, **kw): + assert_equal(body.keys(), ['server']) + assert_has_keys(body['server'], + required=['name', 'imageRef', 'flavorRef'], + optional=['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]) diff --git a/tests/v1_1/test_base.py b/tests/v1_1/test_base.py new file mode 100644 index 000000000..9db08eb80 --- /dev/null +++ b/tests/v1_1/test_base.py @@ -0,0 +1,61 @@ +from __future__ import absolute_import + +import mock +from nose.tools import assert_equal, assert_not_equal, assert_raises + +from novaclient.v1_1 import flavors +from novaclient.v1_1 import exceptions +from novaclient.v1_1 import base + +from .fakes import FakeClient + +os = FakeClient() + + +def test_resource_repr(): + r = base.Resource(None, dict(foo="bar", baz="spam")) + assert_equal(repr(r), "") + + +def test_getid(): + assert_equal(base.getid(4), 4) + + class O(object): + id = 4 + assert_equal(base.getid(O), 4) + + +def test_resource_lazy_getattr(): + f = flavors.Flavor(os.flavors, {'id': 1}) + assert_equal(f.name, '256 MB Server') + os.assert_called('GET', '/flavors/1') + + # Missing stuff still fails after a second get + assert_raises(AttributeError, getattr, f, 'blahblah') + os.assert_called('GET', '/flavors/1') + + +def test_eq(): + # Two resources of the same type with the same id: equal + r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) + r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) + assert_equal(r1, r2) + + # Two resoruces of different types: never equal + r1 = base.Resource(None, {'id': 1}) + r2 = flavors.Flavor(None, {'id': 1}) + assert_not_equal(r1, r2) + + # Two resources with no ID: equal if their info is equal + r1 = base.Resource(None, {'name': 'joe', 'age': 12}) + r2 = base.Resource(None, {'name': 'joe', 'age': 12}) + assert_equal(r1, r2) + + +def test_findall_invalid_attribute(): + # Make sure findall with an invalid attribute doesn't cause errors. + # The following should not raise an exception. + os.flavors.findall(vegetable='carrot') + + # However, find() should raise an error + assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot') diff --git a/tests/v1_1/test_client.py b/tests/v1_1/test_client.py new file mode 100644 index 000000000..7cf90e368 --- /dev/null +++ b/tests/v1_1/test_client.py @@ -0,0 +1,52 @@ +import mock +import httplib2 + +from novaclient.v1_1 import client +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 get_client(): + cl = client.HTTPClient("username", "apikey", "project_id", "auth_test") + cl.management_url = "http://example.com" + cl.auth_token = "token" + return cl + + +def test_get(): + cl = get_client() + + @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch('time.time', mock.Mock(return_value=1234)) + def test_get_call(): + resp, body = cl.get("/hi") + mock_request.assert_called_with("http://example.com/hi?fresh=1234", + "GET", + headers={"X-Auth-Token": "token", + "X-Auth-Project-Id": "project_id", + "User-Agent": cl.USER_AGENT}) + # Automatic JSON parsing + assert_equal(body, {"hi": "there"}) + + test_get_call() + + +def test_post(): + cl = get_client() + + @mock.patch.object(httplib2.Http, "request", mock_request) + def test_post_call(): + cl.post("/hi", body=[1, 2, 3]) + mock_request.assert_called_with("http://example.com/hi", "POST", + headers={ + "X-Auth-Token": "token", + "X-Auth-Project-Id": "project_id", + "Content-Type": "application/json", + "User-Agent": cl.USER_AGENT}, + body='[1, 2, 3]' + ) + + test_post_call() diff --git a/tests/v1_1/test_flavors.py b/tests/v1_1/test_flavors.py new file mode 100644 index 000000000..51ca7a1bf --- /dev/null +++ b/tests/v1_1/test_flavors.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import + +from nose.tools import assert_raises, assert_equal + +from novaclient.v1_1 import flavors +from novaclient.v1_1 import exceptions + +from .fakes import FakeClient +from .utils import assert_isinstance + +os = FakeClient() + + +def test_list_flavors(): + fl = os.flavors.list() + os.assert_called('GET', '/flavors/detail') + [assert_isinstance(f, flavors.Flavor) for f in fl] + + +def test_list_flavors_undetailed(): + fl = os.flavors.list(detailed=False) + os.assert_called('GET', '/flavors') + [assert_isinstance(f, flavors.Flavor) for f in fl] + + +def test_get_flavor_details(): + f = os.flavors.get(1) + os.assert_called('GET', '/flavors/1') + assert_isinstance(f, flavors.Flavor) + assert_equal(f.ram, 256) + assert_equal(f.disk, 10) + + +def test_find(): + f = os.flavors.find(ram=256) + os.assert_called('GET', '/flavors/detail') + assert_equal(f.name, '256 MB Server') + + f = os.flavors.find(disk=20) + assert_equal(f.name, '512 MB Server') + + assert_raises(exceptions.NotFound, os.flavors.find, disk=12345) diff --git a/tests/v1_1/test_images.py b/tests/v1_1/test_images.py new file mode 100644 index 000000000..03b63b7cb --- /dev/null +++ b/tests/v1_1/test_images.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import + +from nose.tools import assert_equal + +from novaclient.v1_1 import images + +from .fakes import FakeClient +from .utils import assert_isinstance + +os = FakeClient() + + +def test_list_images(): + il = os.images.list() + os.assert_called('GET', '/images/detail') + [assert_isinstance(i, images.Image) for i in il] + + +def test_list_images_undetailed(): + il = os.images.list(detailed=False) + os.assert_called('GET', '/images') + [assert_isinstance(i, images.Image) for i in il] + + +def test_get_image_details(): + i = os.images.get(1) + os.assert_called('GET', '/images/1') + assert_isinstance(i, images.Image) + assert_equal(i.id, 1) + assert_equal(i.name, 'CentOS 5.2') + + +def test_create_image(): + i = os.images.create(server=1234, name="Just in case") + os.assert_called('POST', '/images') + assert_isinstance(i, images.Image) + + +def test_delete_image(): + os.images.delete(1) + os.assert_called('DELETE', '/images/1') + + +def test_find(): + i = os.images.find(name="CentOS 5.2") + assert_equal(i.id, 1) + os.assert_called('GET', '/images/detail') + + iml = os.images.findall(status='SAVING') + assert_equal(len(iml), 1) + assert_equal(iml[0].name, 'My Server Backup') diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py new file mode 100644 index 000000000..1ffb7b959 --- /dev/null +++ b/tests/v1_1/test_servers.py @@ -0,0 +1,125 @@ +from __future__ import absolute_import + +import StringIO + +from nose.tools import assert_equal + +from novaclient.v1_1 import servers + +from .fakes import FakeClient +from .utils import assert_isinstance + + +os = FakeClient() + + +def test_list_servers(): + sl = os.servers.list() + os.assert_called('GET', '/servers/detail') + [assert_isinstance(s, servers.Server) for s in sl] + + +def test_list_servers_undetailed(): + sl = os.servers.list(detailed=False) + os.assert_called('GET', '/servers') + [assert_isinstance(s, servers.Server) for s in sl] + + +def test_get_server_details(): + s = os.servers.get(1234) + os.assert_called('GET', '/servers/1234') + assert_isinstance(s, servers.Server) + assert_equal(s.id, 1234) + assert_equal(s.status, 'BUILD') + + +def test_create_server(): + s = os.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': StringIO.StringIO('data') # a stream + } + ) + os.assert_called('POST', '/servers') + assert_isinstance(s, servers.Server) + + +def test_update_server(): + s = os.servers.get(1234) + + # Update via instance + s.update(name='hi') + os.assert_called('PUT', '/servers/1234') + + # Silly, but not an error + s.update() + + # Update via manager + os.servers.update(s, name='hi') + os.assert_called('PUT', '/servers/1234') + + +def test_delete_server(): + s = os.servers.get(1234) + s.delete() + os.assert_called('DELETE', '/servers/1234') + os.servers.delete(1234) + os.assert_called('DELETE', '/servers/1234') + os.servers.delete(s) + os.assert_called('DELETE', '/servers/1234') + + +def test_find(): + s = os.servers.find(name='sample-server') + os.assert_called('GET', '/servers/detail') + assert_equal(s.name, 'sample-server') + + # Find with multiple results arbitraility returns the first item + s = os.servers.find(flavor_id=1) + sl = os.servers.findall(flavor_id=1) + assert_equal(sl[0], s) + assert_equal([s.id for s in sl], [1234, 5678]) + + +def test_reboot_server(): + s = os.servers.get(1234) + s.reboot() + os.assert_called('POST', '/servers/1234/action') + os.servers.reboot(s, type='HARD') + os.assert_called('POST', '/servers/1234/action') + + +def test_rebuild_server(): + s = os.servers.get(1234) + s.rebuild(image=1) + os.assert_called('POST', '/servers/1234/action') + os.servers.rebuild(s, image=1) + os.assert_called('POST', '/servers/1234/action') + + +def test_resize_server(): + s = os.servers.get(1234) + s.resize(flavor=1) + os.assert_called('POST', '/servers/1234/action') + os.servers.resize(s, flavor=1) + os.assert_called('POST', '/servers/1234/action') + + +def test_confirm_resized_server(): + s = os.servers.get(1234) + s.confirm_resize() + os.assert_called('POST', '/servers/1234/action') + os.servers.confirm_resize(s) + os.assert_called('POST', '/servers/1234/action') + + +def test_revert_resized_server(): + s = os.servers.get(1234) + s.revert_resize() + os.assert_called('POST', '/servers/1234/action') + os.servers.revert_resize(s) + os.assert_called('POST', '/servers/1234/action') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py new file mode 100644 index 000000000..baf117b93 --- /dev/null +++ b/tests/v1_1/test_shell.py @@ -0,0 +1,234 @@ +from __future__ import absolute_import + +import os +import mock +import httplib2 + +from nose.tools import assert_raises, assert_equal + +from novaclient.v1_1.shell import OpenStackShell, CommandError + +from .fakes import FakeClient +from .utils import assert_in + + +# Patch os.environ to avoid required auth info. +def setup(): + global _old_env + fake_env = { + 'NOVA_USERNAME': 'username', + 'NOVA_API_KEY': 'password', + 'NOVA_PROJECT_ID': 'project_id' + } + _old_env, os.environ = os.environ, fake_env.copy() + + # 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, assert_called_anytime + _shell = OpenStackShell() + _shell._api_class = FakeClient + assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b) + assert_called_anytime = lambda m, u, b=None: \ + _shell.cs.assert_called_anytime(m, u, b) + shell = lambda cmd: _shell.main(cmd.split()) + + +def teardown(): + global _old_env + os.environ = _old_env + + +def test_boot(): + shell('boot --image 1 some-server') + assert_called( + 'POST', '/servers', + {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1'}} + ) + + shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ') + assert_called( + 'POST', '/servers', + {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', + 'metadata': {'foo': 'bar', 'spam': 'eggs'}}} + ) + + +def test_boot_files(): + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + expected_file_data = open(testfile).read().encode('base64') + + shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' % + (testfile, testfile)) + + assert_called( + 'POST', '/servers', + {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', + 'personality': [ + {'path': '/tmp/bar', 'contents': expected_file_data}, + {'path': '/tmp/foo', 'contents': expected_file_data} + ]} + } + ) + + +def test_boot_invalid_file(): + invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') + assert_raises(CommandError, shell, 'boot some-server --image 1 ' + '--file /foo=%s' % invalid_file) + + +def test_boot_key_auto(): + mock_exists = mock.Mock(return_value=True) + mock_open = mock.Mock() + mock_open.return_value = mock.Mock() + mock_open.return_value.read = mock.Mock(return_value='SSHKEY') + + @mock.patch('os.path.exists', mock_exists) + @mock.patch('__builtin__.open', mock_open) + def test_shell_call(): + shell('boot some-server --image 1 --key') + assert_called( + 'POST', '/servers', + {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', + 'personality': [{ + 'path': '/root/.ssh/authorized_keys2', + 'contents': ('SSHKEY').encode('base64')}, + ]} + } + ) + + test_shell_call() + + +def test_boot_key_auto_no_keys(): + mock_exists = mock.Mock(return_value=False) + + @mock.patch('os.path.exists', mock_exists) + def test_shell_call(): + assert_raises(CommandError, shell, 'boot some-server --image 1 --key') + + test_shell_call() + + +def test_boot_key_file(): + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + expected_file_data = open(testfile).read().encode('base64') + shell('boot some-server --image 1 --key %s' % testfile) + assert_called( + 'POST', '/servers', + {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', + 'personality': [ + {'path': '/root/.ssh/authorized_keys2', 'contents': + expected_file_data}, + ]} + } + ) + + +def test_boot_invalid_keyfile(): + invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') + assert_raises(CommandError, shell, 'boot some-server ' + '--image 1 --key %s' % invalid_file) + + +def test_flavor_list(): + shell('flavor-list') + assert_called_anytime('GET', '/flavors/detail') + + +def test_image_list(): + shell('image-list') + assert_called('GET', '/images/detail') + + +def test_create_image(): + shell('create-image sample-server mysnapshot') + assert_called( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', "metadata": {}}} + ) + + +def test_image_delete(): + shell('image-delete 1') + assert_called('DELETE', '/images/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': {'imageRef': 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': {'flavorRef': 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('POST', '/servers/1234/action', {'changePassword': {'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(): + @mock.patch.object(_shell.parser, 'print_help') + def test_help(m): + shell('help') + m.assert_called() + + @mock.patch.object(_shell.subcommands['delete'], 'print_help') + def test_help_delete(m): + shell('help delete') + m.assert_called() + + test_help() + test_help_delete() + + assert_raises(CommandError, shell, 'help foofoo') + + +def test_debug(): + httplib2.debuglevel = 0 + shell('--debug list') + assert httplib2.debuglevel == 1 diff --git a/tests/v1_1/testfile.txt b/tests/v1_1/testfile.txt new file mode 100644 index 000000000..90763c69f --- /dev/null +++ b/tests/v1_1/testfile.txt @@ -0,0 +1 @@ +OH HAI! \ No newline at end of file diff --git a/tests/v1_1/utils.py b/tests/v1_1/utils.py new file mode 100644 index 000000000..f878a5e26 --- /dev/null +++ b/tests/v1_1/utils.py @@ -0,0 +1,29 @@ +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)) From 454daa2cba9058d0e744b398ce6af0135b5b27b7 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 3 Aug 2011 17:41:33 -0400 Subject: [PATCH 02/17] Tests working again...merged in some work we did earlier. --- novaclient/__init__.py | 17 - novaclient/base.py | 235 +++++ novaclient/client.py | 157 +++ novaclient/{v1_0 => }/exceptions.py | 25 +- novaclient/shell.py | 204 ++++ novaclient/utils.py | 43 + novaclient/v1_0/__init__.py | 74 -- novaclient/v1_0/accounts.py | 7 +- novaclient/v1_0/backup_schedules.py | 3 +- novaclient/v1_0/base.py | 148 +-- novaclient/v1_0/client.py | 176 +--- novaclient/v1_0/flavors.py | 2 +- novaclient/v1_0/images.py | 23 +- novaclient/v1_0/ipgroups.py | 2 +- novaclient/v1_0/servers.py | 44 +- novaclient/v1_0/shell.py | 1417 ++++++++++++--------------- novaclient/v1_0/zones.py | 5 +- novaclient/v1_1/__init__.py | 66 -- novaclient/v1_1/base.py | 2 +- novaclient/v1_1/client.py | 168 +--- novaclient/v1_1/exceptions.py | 100 -- novaclient/v1_1/flavors.py | 10 +- novaclient/v1_1/images.py | 38 +- novaclient/v1_1/servers.py | 119 ++- novaclient/v1_1/shell.py | 812 ++++++--------- tests/fakes.py | 66 ++ tests/test_base.py | 60 ++ tests/test_http.py | 58 ++ tests/test_shell.py | 39 + tests/utils.py | 5 + tests/v1_0/fakes.py | 166 ++-- tests/v1_0/test_accounts.py | 36 +- tests/v1_0/test_auth.py | 105 +- tests/v1_0/test_backup_schedules.py | 96 +- tests/v1_0/test_base.py | 61 -- tests/v1_0/test_client.py | 52 - tests/v1_0/test_flavors.py | 56 +- tests/v1_0/test_images.py | 70 +- tests/v1_0/test_ipgroups.py | 76 +- tests/v1_0/test_servers.py | 281 +++--- tests/v1_0/test_shell.py | 651 ++++++------ tests/v1_0/test_zones.py | 121 ++- tests/v1_0/testfile.txt | 2 +- tests/v1_1/fakes.py | 263 +---- tests/v1_1/test_auth.py | 74 ++ tests/v1_1/test_base.py | 61 -- tests/v1_1/test_client.py | 52 - tests/v1_1/test_flavors.py | 56 +- tests/v1_1/test_images.py | 67 +- tests/v1_1/test_servers.py | 191 ++-- tests/v1_1/test_shell.py | 366 ++++--- tests/v1_1/testfile.txt | 2 +- 52 files changed, 3172 insertions(+), 3858 deletions(-) create mode 100644 novaclient/base.py create mode 100644 novaclient/client.py rename novaclient/{v1_0 => }/exceptions.py (81%) create mode 100644 novaclient/shell.py create mode 100644 novaclient/utils.py delete mode 100644 novaclient/v1_1/exceptions.py create mode 100644 tests/fakes.py create mode 100644 tests/test_base.py create mode 100644 tests/test_http.py create mode 100644 tests/test_shell.py create mode 100644 tests/utils.py delete mode 100644 tests/v1_0/test_base.py delete mode 100644 tests/v1_0/test_client.py create mode 100644 tests/v1_1/test_auth.py delete mode 100644 tests/v1_1/test_base.py delete mode 100644 tests/v1_1/test_client.py diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 08bd9d9a7..e69de29bb 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -1,17 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -__version__ = '2.5' diff --git a/novaclient/base.py b/novaclient/base.py new file mode 100644 index 000000000..7928f8d5c --- /dev/null +++ b/novaclient/base.py @@ -0,0 +1,235 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Base utilities to build API operation managers and objects on top of. +""" + +from novaclient import exceptions + + +# Python 2.4 compat +try: + all +except NameError: + def all(iterable): + return True not in (not x for x in iterable) + + +def getid(obj): + """ + Abstracts the common pattern of allowing both an object or an object's ID + (UUID) as a parameter when dealing with relationships. + """ + + # Try to return the object's UUID first, if we have a UUID. + try: + if obj.uuid: + return obj.uuid + except AttributeError: + pass + try: + return obj.id + except AttributeError: + return obj + + +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, obj_class=None, body=None): + resp = None + if body: + resp, body = self.api.client.post(url, body=body) + else: + resp, body = self.api.client.get(url) + + if obj_class is None: + 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]) + + def _create(self, url, body, response_key, return_raw=False): + resp, body = self.api.client.post(url, body=body) + if return_raw: + return body[response_key] + 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: + msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + raise exceptions.NotFound(404, msg) + + 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 BootingManagerWithFind(ManagerWithFind): + """Like a `ManagerWithFind`, but has the ability to boot servers.""" + def _boot(self, resource_url, response_key, name, image, flavor, + ipgroup=None, meta=None, files=None, zone_blob=None, + reservation_id=None, return_raw=False, min_count=None, + max_count=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. + :param zone_blob: a single (encrypted) string which is used internally + by Nova for routing between Zones. Users cannot populate + this field. + :param reservation_id: a UUID for the set of servers being requested. + :param return_raw: If True, don't try to coearse the result into + a Resource object. + """ + body = {"server": { + "name": name, + "imageId": getid(image), + "flavorId": getid(flavor), + }} + if ipgroup: + body["server"]["sharedIpGroupId"] = getid(ipgroup) + if meta: + body["server"]["metadata"] = meta + if reservation_id: + body["server"]["reservation_id"] = reservation_id + if zone_blob: + body["server"]["zone_blob"] = zone_blob + + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + body["server"]["min_count"] = min_count + body["server"]["max_count"] = max_count + + # 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(resource_url, body, response_key, + return_raw=return_raw) + + +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) + if new: + 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 diff --git a/novaclient/client.py b/novaclient/client.py new file mode 100644 index 000000000..fc1c98401 --- /dev/null +++ b/novaclient/client.py @@ -0,0 +1,157 @@ +# Copyright 2010 Jacob Kaplan-Moss +""" +OpenStack Client interface. Handles the REST calls and responses. +""" + +import time +import urlparse +import urllib +import httplib2 +import logging + +try: + import json +except ImportError: + import simplejson as json + +# Python 2.5 compat fix +if not hasattr(urlparse, 'parse_qsl'): + import cgi + urlparse.parse_qsl = cgi.parse_qsl + + +from novaclient import exceptions + + +_logger = logging.getLogger(__name__) + + +class HTTPClient(httplib2.Http): + + USER_AGENT = 'python-novaclient' + + def __init__(self, user, apikey, projectid, auth_url, timeout=None): + super(HTTPClient, self).__init__(timeout=timeout) + self.user = user + self.apikey = apikey + self.projectid = projectid + self.auth_url = auth_url + self.version = 'v1.0' + + self.management_url = None + self.auth_token = None + + # httplib2 overrides + self.force_exception_to_status_code = True + + 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'): + string_parts.append(' -X %s' % element) + else: + string_parts.append(' %s' % element) + + for element in kwargs['headers']: + 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) + + 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(HTTPClient, self).request(*args, **kwargs) + + self.http_log(args, kwargs, resp, body) + + if body: + try: + body = json.loads(body) + except ValueError, e: + pass + else: + body = None + + if resp.status in (400, 401, 403, 404, 408, 413, 500, 501): + 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 + if self.projectid: + kwargs['headers']['X-Auth-Project-Id'] = self.projectid + + 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): + url = self._munge_get_url(url) + 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): + scheme, netloc, path, query, frag = urlparse.urlsplit( + self.auth_url) + path_parts = path.split('/') + for part in path_parts: + if len(part) > 0 and part[0] == 'v': + self.version = part + break + + headers = {'X-Auth-User': self.user, + 'X-Auth-Key': self.apikey} + if self.projectid: + headers['X-Auth-Project-Id'] = self.projectid + 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'] + + def _munge_get_url(self, url): + """ + Munge GET URLs to always return uncached content. + + The OpenStack Compute API caches data *very* agressively and doesn't + respect cache headers. To avoid stale data, then, we append a little + bit of nonsense onto GET parameters; this appears to force the data not + to be cached. + """ + scheme, netloc, path, query, frag = urlparse.urlsplit(url) + query = urlparse.parse_qsl(query) + query.append(('fresh', str(time.time()))) + query = urllib.urlencode(query) + return urlparse.urlunsplit((scheme, netloc, path, query, frag)) diff --git a/novaclient/v1_0/exceptions.py b/novaclient/exceptions.py similarity index 81% rename from novaclient/v1_0/exceptions.py rename to novaclient/exceptions.py index 1709d806f..ae456decd 100644 --- a/novaclient/v1_0/exceptions.py +++ b/novaclient/exceptions.py @@ -3,7 +3,12 @@ Exception definitions. """ -class OpenStackException(Exception): + +class CommandError(Exception): + pass + + +class ClientException(Exception): """ The base exception class for all exceptions this library raises. """ @@ -16,7 +21,7 @@ class OpenStackException(Exception): return "%s (HTTP %s)" % (self.message, self.code) -class BadRequest(OpenStackException): +class BadRequest(ClientException): """ HTTP 400 - Bad request: you sent some malformed data. """ @@ -24,7 +29,7 @@ class BadRequest(OpenStackException): message = "Bad request" -class Unauthorized(OpenStackException): +class Unauthorized(ClientException): """ HTTP 401 - Unauthorized: bad credentials. """ @@ -32,7 +37,7 @@ class Unauthorized(OpenStackException): message = "Unauthorized" -class Forbidden(OpenStackException): +class Forbidden(ClientException): """ HTTP 403 - Forbidden: your credentials don't give you access to this resource. @@ -41,7 +46,7 @@ class Forbidden(OpenStackException): message = "Forbidden" -class NotFound(OpenStackException): +class NotFound(ClientException): """ HTTP 404 - Not found """ @@ -49,7 +54,7 @@ class NotFound(OpenStackException): message = "Not found" -class OverLimit(OpenStackException): +class OverLimit(ClientException): """ HTTP 413 - Over limit: you're over the API limits for this time period. """ @@ -58,7 +63,7 @@ class OverLimit(OpenStackException): # NotImplemented is a python keyword. -class HTTPNotImplemented(OpenStackException): +class HTTPNotImplemented(ClientException): """ HTTP 501 - Not Implemented: the server does not support this operation. """ @@ -69,7 +74,7 @@ class HTTPNotImplemented(OpenStackException): # In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() # so we can do this: # _code_map = dict((c.http_status, c) -# for c in OpenStackException.__subclasses__()) +# for c in ClientException.__subclasses__()) # # Instead, we have to hardcode it: _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, @@ -78,7 +83,7 @@ _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, def from_response(response, body): """ - Return an instance of an OpenStackException or subclass + Return an instance of an ClientException or subclass based on an httplib2 response. Usage:: @@ -87,7 +92,7 @@ def from_response(response, body): if resp.status != 200: raise exception_from_response(resp, body) """ - cls = _code_map.get(response.status, OpenStackException) + cls = _code_map.get(response.status, ClientException) if body: message = "n/a" details = "n/a" diff --git a/novaclient/shell.py b/novaclient/shell.py new file mode 100644 index 000000000..0ab0d6945 --- /dev/null +++ b/novaclient/shell.py @@ -0,0 +1,204 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Command-line interface to the OpenStack Nova API. +""" + +import argparse +import httplib2 +import os +import prettytable +import sys + +from novaclient import exceptions +from novaclient import utils +from novaclient.v1_0 import shell as shell_v1_0 +from novaclient.v1_1 import shell as shell_v1_1 + + +def env(e): + return os.environ.get(e, '') + + +class OpenStackComputeShell(object): + + # Hook for the test suite to inject a fake server. + _api_class = None + + def get_base_parser(self): + parser = argparse.ArgumentParser( + prog='nova', + description=__doc__.strip(), + epilog='See "nova help COMMAND" '\ + 'for help on a specific command.', + add_help=False, + formatter_class=OpenStackHelpFormatter, + ) + + # Global arguments + parser.add_argument('-h', '--help', + action='help', + help=argparse.SUPPRESS, + ) + + parser.add_argument('--debug', + default=False, + action='store_true', + help=argparse.SUPPRESS) + + parser.add_argument('--username', + default=env('NOVA_USERNAME'), + help='Defaults to env[NOVA_USERNAME].') + + parser.add_argument('--apikey', + default=env('NOVA_API_KEY'), + help='Defaults to env[NOVA_API_KEY].') + + parser.add_argument('--projectid', + default=env('NOVA_PROJECT_ID'), + help='Defaults to env[NOVA_PROJECT_ID].') + + parser.add_argument('--url', + default=env('NOVA_URL'), + help='Defaults to env[NOVA_URL].') + + parser.add_argument('--version', + default='1.1', + help='Accepts 1.0 or 1.1, defaults to 1.1') + + return parser + + def get_subcommand_parser(self, version): + parser = self.get_base_parser() + + self.subcommands = {} + subparsers = parser.add_subparsers(metavar='') + + actions_module = { + '1.0': shell_v1_0, + '1.1': shell_v1_1, + }[version] + + self._find_actions(subparsers, actions_module) + self._find_actions(subparsers, self) + + return parser + + def _find_actions(self, subparsers, actions_module): + for attr in (a for a in dir(actions_module) if a.startswith('do_')): + # I prefer to be hypen-separated instead of underscores. + command = attr[3:].replace('_', '-') + callback = getattr(actions_module, 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=OpenStackHelpFormatter + ) + 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 once to find version + parser = self.get_base_parser() + (options, args) = parser.parse_known_args(argv) + + # build available subcommands based on version + subcommand_parser = self.get_subcommand_parser(options.version) + self.parser = subcommand_parser + + # Parse args again and call whatever callback was selected + args = subcommand_parser.parse_args(argv) + + # Deal with global arguments + if args.debug: + httplib2.debuglevel = 1 + + # Short-circuit and deal with help right away. + if args.func == self.do_help: + self.do_help(args) + return 0 + + user, apikey, projectid, url = args.username, args.apikey, \ + args.projectid, args.url + + #FIXME(usrleon): Here should be restrict for project id same as + # for username or apikey but for compatibility it is not. + + if not user: + raise exceptions.CommandError("You must provide a username, either via " + "--username or via env[NOVA_USERNAME]") + if not apikey: + raise exceptions.CommandError("You must provide an API key, either via " + "--apikey or via env[NOVA_API_KEY]") + + self.cs = self.get_api_class()(user, apikey, projectid, url) + try: + self.cs.authenticate() + except exceptions.Unauthorized: + raise exceptions.CommandError("Invalid OpenStack Nova credentials.") + + args.func(self.cs, args) + + def get_api_class(self): + return self._api_class or shell_v1_0.CLIENT_CLASS + + @utils.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 exceptions.CommandError("'%s' is not a valid subcommand." % + args.command) + else: + self.parser.print_help() + + +# I'm picky about my shell help. +class OpenStackHelpFormatter(argparse.HelpFormatter): + def start_section(self, heading): + # Title-case the headings + heading = '%s%s' % (heading[0].upper(), heading[1:]) + super(OpenStackHelpFormatter, self).start_section(heading) + + +def main(): + try: + OpenStackComputeShell().main(sys.argv[1:]) + + except Exception, e: + if httplib2.debuglevel == 1: + raise # dump stack. + else: + print >> sys.stderr, e + sys.exit(1) diff --git a/novaclient/utils.py b/novaclient/utils.py new file mode 100644 index 000000000..4397046d0 --- /dev/null +++ b/novaclient/utils.py @@ -0,0 +1,43 @@ + +import prettytable + + +# Decorator for cli-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 + + +def pretty_choice_list(l): + return ', '.join("'%s'" % i for i in l) + + +def print_list(objs, fields, formatters={}): + pt = prettytable.PrettyTable([f for f in fields], caching=False) + pt.aligns = ['l' for f in fields] + + for o in objs: + row = [] + for field in fields: + if field in formatters: + row.append(formatters[field](o)) + else: + field_name = field.lower().replace(' ', '_') + data = getattr(o, field_name, '') + row.append(data) + pt.add_row(row) + + 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') + + diff --git a/novaclient/v1_0/__init__.py b/novaclient/v1_0/__init__.py index 7b2c9737f..e69de29bb 100644 --- a/novaclient/v1_0/__init__.py +++ b/novaclient/v1_0/__init__.py @@ -1,74 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v1_0 import accounts -from novaclient.v1_0 import backup_schedules -from novaclient.v1_0 import client -from novaclient.v1_0 import exceptions -from novaclient.v1_0 import flavors -from novaclient.v1_0 import images -from novaclient.v1_0 import ipgroups -from novaclient.v1_0 import servers -from novaclient.v1_0 import zones - - -class Client(object): - """ - Top-level object to access the OpenStack Compute v1.0 API. - - Create an instance with your creds:: - - >>> os = novaclient.v1_0.Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL) - - Then call methods on its managers:: - - >>> os.servers.list() - ... - >>> os.flavors.list() - ... - - &c. - """ - - def __init__(self, username, apikey, projectid, auth_url=None, timeout=None): - """Initialize v1.0 Openstack Client.""" - self.backup_schedules = backup_schedules.BackupScheduleManager(self) - self.flavors = flavors.FlavorManager(self) - self.images = images.ImageManager(self) - self.ipgroups = ipgroups.IPGroupManager(self) - self.servers = servers.ServerManager(self) - self.zones = zones.ZoneManager(self) - self.accounts = accounts.AccountManager(self) - - auth_url = auth_url or "https://auth.api.rackspacecloud.com/v1.0" - - self.client = client.HTTPClient(username, - apikey, - projectid, - auth_url, - timeout=timeout) - - 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 :exc:`novaclient.Unauthorized` if the - credentials are wrong. - """ - self.client.authenticate() diff --git a/novaclient/v1_0/accounts.py b/novaclient/v1_0/accounts.py index 264ce8431..be162d9bf 100644 --- a/novaclient/v1_0/accounts.py +++ b/novaclient/v1_0/accounts.py @@ -1,10 +1,13 @@ -from novaclient.v1_0 import base + +from novaclient import base +from novaclient.v1_0 import base as local_base + class Account(base.Resource): pass -class AccountManager(base.BootingManagerWithFind): +class AccountManager(local_base.BootingManagerWithFind): resource_class = Account def create_instance_for(self, account_id, name, image, flavor, diff --git a/novaclient/v1_0/backup_schedules.py b/novaclient/v1_0/backup_schedules.py index 78f4d49f2..2d8aea824 100644 --- a/novaclient/v1_0/backup_schedules.py +++ b/novaclient/v1_0/backup_schedules.py @@ -3,7 +3,8 @@ Backup Schedule interface. """ -from novaclient.v1_0 import base +from novaclient import base + BACKUP_WEEKLY_DISABLED = 'DISABLED' BACKUP_WEEKLY_SUNDAY = 'SUNDAY' diff --git a/novaclient/v1_0/base.py b/novaclient/v1_0/base.py index 3dbec636d..3ff7ac2d6 100644 --- a/novaclient/v1_0/base.py +++ b/novaclient/v1_0/base.py @@ -19,7 +19,9 @@ Base utilities to build API operation managers and objects on top of. """ -from novaclient.v1_0 import exceptions +from novaclient import base +from novaclient import exceptions + # Python 2.4 compat try: @@ -29,103 +31,7 @@ except NameError: return True not in (not x for x in iterable) -def getid(obj): - """ - Abstracts the common pattern of allowing both an object or an object's ID - (UUID) as a parameter when dealing with relationships. - """ - - # Try to return the object's UUID first, if we have a UUID. - try: - if obj.uuid: - return obj.uuid - except AttributeError: - pass - try: - return obj.id - except AttributeError: - return obj - - -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, obj_class=None, body=None): - resp = None - if body: - resp, body = self.api.client.post(url, body=body) - else: - resp, body = self.api.client.get(url) - - if obj_class is None: - 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]) - - def _create(self, url, body, response_key, return_raw=False): - resp, body = self.api.client.post(url, body=body) - if return_raw: - return body[response_key] - 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 exceptions.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 BootingManagerWithFind(ManagerWithFind): +class BootingManagerWithFind(base.ManagerWithFind): """Like a `ManagerWithFind`, but has the ability to boot servers.""" def _boot(self, resource_url, response_key, name, image, flavor, ipgroup=None, meta=None, files=None, zone_blob=None, @@ -155,11 +61,11 @@ class BootingManagerWithFind(ManagerWithFind): """ body = {"server": { "name": name, - "imageId": getid(image), - "flavorId": getid(flavor), + "imageId": base.getid(image), + "flavorId": base.getid(flavor), }} if ipgroup: - body["server"]["sharedIpGroupId"] = getid(ipgroup) + body["server"]["sharedIpGroupId"] = base.getid(ipgroup) if meta: body["server"]["metadata"] = meta if reservation_id: @@ -192,43 +98,3 @@ class BootingManagerWithFind(ManagerWithFind): return self._create(resource_url, body, response_key, return_raw=return_raw) - - -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) - if new: - 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 diff --git a/novaclient/v1_0/client.py b/novaclient/v1_0/client.py index 202f6da56..f7e203fae 100644 --- a/novaclient/v1_0/client.py +++ b/novaclient/v1_0/client.py @@ -1,154 +1,60 @@ -# Copyright 2010 Jacob Kaplan-Moss -""" -OpenStack Client interface. Handles the REST calls and responses. -""" -import time -import urlparse -import urllib -import httplib2 -import logging -try: - import json -except ImportError: - import simplejson as json +from novaclient import client +from novaclient.v1_0 import accounts +from novaclient.v1_0 import backup_schedules +from novaclient.v1_0 import flavors +from novaclient.v1_0 import images +from novaclient.v1_0 import ipgroups +from novaclient.v1_0 import servers +from novaclient.v1_0 import zones -# Python 2.5 compat fix -if not hasattr(urlparse, 'parse_qsl'): - import cgi - urlparse.parse_qsl = cgi.parse_qsl -import novaclient -from novaclient.v1_0 import exceptions -_logger = logging.getLogger(__name__) +class Client(object): + """ + Top-level object to access the OpenStack Compute API. -class HTTPClient(httplib2.Http): + Create an instance with your creds:: - USER_AGENT = 'python-novaclient/%s' % novaclient.__version__ + >>> client = Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL) - def __init__(self, user, apikey, projectid, auth_url, timeout=None): - super(HTTPClient, self).__init__(timeout=timeout) - self.user = user - self.apikey = apikey - self.projectid = projectid - self.auth_url = auth_url - self.version = 'v1.0' + Then call methods on its managers:: - self.management_url = None - self.auth_token = None + >>> client.servers.list() + ... + >>> client.flavors.list() + ... - # httplib2 overrides - self.force_exception_to_status_code = True + """ - def http_log(self, args, kwargs, resp, body): - if not _logger.isEnabledFor(logging.DEBUG): - return + def __init__(self, username, api_key, project_id, auth_url=None, + timeout=None): - string_parts = ['curl -i'] - for element in args: - if element in ('GET','POST'): - string_parts.append(' -X %s' % element) - else: - string_parts.append(' %s' % element) + self.accounts = accounts.AccountManager(self) + self.backup_schedules = backup_schedules.BackupScheduleManager(self) + self.flavors = flavors.FlavorManager(self) + self.images = images.ImageManager(self) + self.ipgroups = ipgroups.IPGroupManager(self) + self.servers = servers.ServerManager(self) + self.zones = zones.ZoneManager(self) - for element in kwargs['headers']: - string_parts.append(' -H "%s: %s"' % (element,kwargs['headers'][element])) + _auth_url = auth_url or 'https://auth.api.rackspacecloud.com/v1.0' - _logger.debug("REQ: %s\n" % "".join(string_parts)) - _logger.debug("RESP:%s %s\n", resp,body) - - 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(HTTPClient, self).request(*args, **kwargs) - - self.http_log(args, kwargs, resp, body) - - if body: - try: - body = json.loads(body) - except ValueError, e: - pass - else: - body = None - - if resp.status in (400, 401, 403, 404, 408, 413, 500, 501): - 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 - if self.projectid: - kwargs['headers']['X-Auth-Project-Id'] = self.projectid - - 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): - url = self._munge_get_url(url) - 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) + self.client = client.HTTPClient(username, + api_key, + project_id, + _auth_url, + timeout=timeout) def authenticate(self): - scheme, netloc, path, query, frag = urlparse.urlsplit( - self.auth_url) - path_parts = path.split('/') - for part in path_parts: - if len(part) > 0 and part[0] == 'v': - self.version = part - break - - headers = {'X-Auth-User': self.user, - 'X-Auth-Key': self.apikey} - if self.projectid: - headers['X-Auth-Project-Id'] = self.projectid - 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'] - - def _munge_get_url(self, url): """ - Munge GET URLs to always return uncached content. + Authenticate against the server. - The OpenStack Nova API caches data *very* agressively and doesn't - respect cache headers. To avoid stale data, then, we append a little - bit of nonsense onto GET parameters; this appears to force the data not - to be cached. + 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 :exc:`exceptions.Unauthorized` if the + credentials are wrong. """ - scheme, netloc, path, query, frag = urlparse.urlsplit(url) - query = urlparse.parse_qsl(query) - query.append(('fresh', str(time.time()))) - query = urllib.urlencode(query) - return urlparse.urlunsplit((scheme, netloc, path, query, frag)) + self.client.authenticate() diff --git a/novaclient/v1_0/flavors.py b/novaclient/v1_0/flavors.py index 4dc4ac994..f1b495804 100644 --- a/novaclient/v1_0/flavors.py +++ b/novaclient/v1_0/flavors.py @@ -3,7 +3,7 @@ Flavor interface. """ -from novaclient.v1_0 import base +from novaclient import base class Flavor(base.Resource): diff --git a/novaclient/v1_0/images.py b/novaclient/v1_0/images.py index ec36fe34a..706915828 100644 --- a/novaclient/v1_0/images.py +++ b/novaclient/v1_0/images.py @@ -3,7 +3,7 @@ Image interface. """ -from novaclient.v1_0 import base +from novaclient import base class Image(base.Resource): @@ -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/v1_0/ipgroups.py b/novaclient/v1_0/ipgroups.py index 66821ee70..86cd3cb43 100644 --- a/novaclient/v1_0/ipgroups.py +++ b/novaclient/v1_0/ipgroups.py @@ -3,7 +3,7 @@ IP Group interface. """ -from novaclient.v1_0 import base +from novaclient import base class IPGroup(base.Resource): diff --git a/novaclient/v1_0/servers.py b/novaclient/v1_0/servers.py index a2b014f05..83cbebab1 100644 --- a/novaclient/v1_0/servers.py +++ b/novaclient/v1_0/servers.py @@ -20,7 +20,10 @@ Server interface. """ import urllib -from novaclient.v1_0 import base + +from novaclient import base +from novaclient.v1_0 import base as local_base + REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -154,6 +157,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. @@ -198,7 +213,7 @@ class Server(base.Resource): return self.addresses['private'] -class ServerManager(base.BootingManagerWithFind): +class ServerManager(local_base.BootingManagerWithFind): resource_class = Server def get(self, server): @@ -370,6 +385,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/v1_0/shell.py b/novaclient/v1_0/shell.py index b4dcef2d2..f652e3db4 100644 --- a/novaclient/v1_0/shell.py +++ b/novaclient/v1_0/shell.py @@ -15,24 +15,19 @@ # License for the specific language governing permissions and limitations # under the License. -""" -Command-line interface to the OpenStack Nova API. -""" - -import argparse import getpass -import httplib2 import os -import prettytable -import sys -import textwrap import uuid -import novaclient.v1_0 +from novaclient import exceptions +from novaclient import utils +from novaclient.v1_0 import client from novaclient.v1_0 import backup_schedules -from novaclient.v1_0 import exceptions from novaclient.v1_0 import servers + +CLIENT_CLASS = client.Client + # Choices for flags. DAY_CHOICES = [getattr(backup_schedules, i).lower() for i in dir(backup_schedules) @@ -42,872 +37,678 @@ HOUR_CHOICES = [getattr(backup_schedules, i).lower() if i.startswith('BACKUP_DAILY_')] -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) - # Sentinal for boot --key AUTO_KEY = object() -# 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 +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('--enable', dest='enabled', default=None, action='store_true', + help='Enable backups.') +@utils.arg('--disable', dest='enabled', action='store_false', + help='Disable backups.') +@utils.arg('--weekly', metavar='', choices=DAY_CHOICES, + help='Schedule a weekly backup for (one of: %s).' % + utils.pretty_choice_list(DAY_CHOICES)) +@utils.arg('--daily', metavar='', choices=HOUR_CHOICES, + help='Schedule a daily backup during (one of: %s).' % + utils.pretty_choice_list(HOUR_CHOICES)) +def do_backup_schedule(cs, 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 = _find_server(cs, args.server) -class CommandError(Exception): - pass - - -def env(e): - return os.environ.get(e, '') - - -class OpenStackShell(object): - - # Hook for the test suite to inject a fake server. - _api_class = novaclient.v1_0.Client - - def __init__(self): - self.parser = argparse.ArgumentParser( - prog='nova', - description=__doc__.strip(), - epilog='See "nova help COMMAND" '\ - 'for help on a specific command.', - add_help=False, - formatter_class=OpenStackHelpFormatter, - ) - - # 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('NOVA_USERNAME'), - help='Defaults to env[NOVA_USERNAME].') - - self.parser.add_argument('--apikey', - default=env('NOVA_API_KEY'), - help='Defaults to env[NOVA_API_KEY].') - - self.parser.add_argument('--projectid', - default=env('NOVA_PROJECT_ID'), - help='Defaults to env[NOVA_PROJECT_ID].') - - auth_url = env('NOVA_URL') - if auth_url == '': - auth_url = 'https://auth.api.rackspacecloud.com/v1.0' - self.parser.add_argument('--url', - default=auth_url, - help='Defaults to env[NOVA_URL].') - - # 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=OpenStackHelpFormatter - ) - 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, projectid, url = args.username, args.apikey, \ - args.projectid, args.url - - #FIXME(usrleon): Here should be restrict for project id same as - # for username or apikey but for compatibility it is not. - - if not user: - raise CommandError("You must provide a username, either via " - "--username or via env[NOVA_USERNAME]") - if not apikey: - raise CommandError("You must provide an API key, either via " - "--apikey or via env[NOVA_API_KEY]") - - self.cs = self._api_class(user, apikey, projectid, url) - try: - self.cs.authenticate() - except exceptions.Unauthorized: - raise CommandError("Invalid OpenStack Nova 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(backup_schedules, 'BACKUP_DAILY_%s' % + # If we have some flags, update the backup + backup = {} + if args.daily: + backup['daily'] = getattr(backup_schedules, 'BACKUP_DAILY_%s' % args.daily.upper()) - if args.weekly: - backup['weekly'] = getattr(backup_schedules, 'BACKUP_WEEKLY_%s' % + if args.weekly: + backup['weekly'] = getattr(backup_schedules, 'BACKUP_WEEKLY_%s' % args.weekly.upper()) - if args.enabled is not None: - backup['enabled'] = args.enabled - if backup: - server.backup_schedule.update(**backup) + if args.enabled is not None: + backup['enabled'] = args.enabled + if backup: + server.backup_schedule.update(**backup) + else: + utils.print_dict(server.backup_schedule._info) + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_backup_schedule_delete(cs, args): + """ + Delete the backup schedule for a server. + """ + server = _find_server(cs, args.server) + server.backup_schedule.delete() + +def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): + """Boot a new server.""" + if min_count is None: + min_count = 1 + if max_count is None: + max_count = min_count + if min_count > max_count: + raise exceptions.CommandError("min_instances should be <= max_instances") + if not min_count or not max_count: + raise exceptions.CommandError("min_instances nor max_instances should be 0") + + flavor = args.flavor or cs.flavors.find(ram=256) + image = args.image or cs.images.find(name="Ubuntu 10.04 LTS "\ + "(lucid)") + + # Map --ipgroup to an ID. + # XXX do this for flavor/image? + if args.ipgroup: + ipgroup = _find_ipgroup(cs, args.ipgroup) + else: + ipgroup = None + + metadata = dict(v.split('=') for v in args.meta) + + files = {} + for f in args.files: + dst, src = f.split('=', 1) + try: + files[dst] = open(src) + except IOError, e: + raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) + + if args.key is AUTO_KEY: + possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k) + for k in ('id_dsa.pub', 'id_rsa.pub')] + for k in possible_keys: + if os.path.exists(k): + keyfile = k + break else: - print_dict(server.backup_schedule._info) + raise exceptions.CommandError("Couldn't find a key file: tried " + "~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub") + elif args.key: + keyfile = args.key + else: + keyfile = None - @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() + if keyfile: + try: + files['/root/.ssh/authorized_keys2'] = open(keyfile) + except IOError, e: + raise exceptions.CommandError("Can't open '%s': %s" % (keyfile, e)) - def _boot(self, args, reservation_id=None, min_count=None, max_count=None): - """Boot a new server.""" - if min_count is None: - min_count = 1 - if max_count is None: - max_count = min_count - if min_count > max_count: - raise CommandError("min_instances should be <= max_instances") - if not min_count or not max_count: - raise CommandError("min_instances nor max_instances should be 0") + return (args.name, image, flavor, ipgroup, metadata, files, + reservation_id, min_count, max_count) - flavor = args.flavor or self.cs.flavors.find(ram=256) - image = args.image or self.cs.images.find(name="Ubuntu 10.04 LTS "\ - "(lucid)") +@utils.arg('--flavor', + default=None, + metavar='', + help="Flavor ID (see 'osc flavors'). "\ + "Defaults to 256MB RAM instance.") +@utils.arg('--image', + default=None, + metavar='', + help="Image ID (see 'osc images'). "\ + "Defaults to Ubuntu 10.04 LTS.") +@utils.arg('--ipgroup', + default=None, + metavar='', + help="IP group name or ID (see 'osc ipgroup-list').") +@utils.arg('--meta', + metavar="", + action='append', + default=[], + help="Record arbitrary key/value metadata. "\ + "May be give multiple times.") +@utils.arg('--file', + metavar="", + action='append', + dest='files', + default=[], + help="Store arbitrary files from locally to "\ + "on the new server. You may store up to 5 files.") +@utils.arg('--key', + metavar='', + nargs='?', + const=AUTO_KEY, + help="Key the server with an SSH keypair. "\ + "Looks in ~/.ssh for a key, "\ + "or takes an explicit to one.") +@utils.arg('name', metavar='', help='Name for the new server') +def do_boot(cs, args): + """Boot a new server.""" + name, image, flavor, ipgroup, metadata, files, reservation_id, \ + min_count, max_count = _boot(cs, args) - # Map --ipgroup to an ID. - # XXX do this for flavor/image? - if args.ipgroup: - ipgroup = self._find_ipgroup(args.ipgroup) - else: - ipgroup = None + server = cs.servers.create(args.name, image, flavor, + ipgroup=ipgroup, + meta=metadata, + files=files, + min_count=min_count, + max_count=max_count) + utils.print_dict(server._info) - metadata = dict(v.split('=') for v in args.meta) +@utils.arg('--flavor', + default=None, + metavar='', + help="Flavor ID (see 'osc flavors'). "\ + "Defaults to 256MB RAM instance.") +@utils.arg('--image', + default=None, + metavar='', + help="Image ID (see 'osc images'). "\ + "Defaults to Ubuntu 10.04 LTS.") +@utils.arg('--ipgroup', + default=None, + metavar='', + help="IP group name or ID (see 'osc ipgroup-list').") +@utils.arg('--meta', + metavar="", + action='append', + default=[], + help="Record arbitrary key/value metadata. "\ + "May be give multiple times.") +@utils.arg('--file', + metavar="", + action='append', + dest='files', + default=[], + help="Store arbitrary files from locally to "\ + "on the new server. You may store up to 5 files.") +@utils.arg('--key', + metavar='', + nargs='?', + const=AUTO_KEY, + help="Key the server with an SSH keypair. "\ + "Looks in ~/.ssh for a key, "\ + "or takes an explicit to one.") +@utils.arg('account', metavar='', help='Account to build this'\ + ' server for') +@utils.arg('name', metavar='', help='Name for the new server') +def do_boot_for_account(cs, args): + """Boot a new server in an account.""" + name, image, flavor, ipgroup, metadata, files, reservation_id, \ + min_count, max_count = _boot(cs, args) - files = {} - for f in args.files: - dst, src = f.split('=', 1) - try: - files[dst] = open(src) - except IOError, e: - raise CommandError("Can't open '%s': %s" % (src, e)) + server = cs.accounts.create_instance_for(args.account, args.name, + image, flavor, + ipgroup=ipgroup, + meta=metadata, + files=files) + utils.print_dict(server._info) - if args.key is AUTO_KEY: - possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k) - for k in ('id_dsa.pub', 'id_rsa.pub')] - for k in possible_keys: - if os.path.exists(k): - keyfile = k - break - else: - raise CommandError("Couldn't find a key file: tried " - "~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub") - elif args.key: - keyfile = args.key - else: - keyfile = None +@utils.arg('--flavor', + default=None, + metavar='', + help="Flavor ID (see 'osc flavors'). "\ + "Defaults to 256MB RAM instance.") +@utils.arg('--image', + default=None, + metavar='', + help="Image ID (see 'osc images'). "\ + "Defaults to Ubuntu 10.04 LTS.") +@utils.arg('--ipgroup', + default=None, + metavar='', + help="IP group name or ID (see 'osc ipgroup-list').") +@utils.arg('--meta', + metavar="", + action='append', + default=[], + help="Record arbitrary key/value metadata. "\ + "May be give multiple times.") +@utils.arg('--file', + metavar="", + action='append', + dest='files', + default=[], + help="Store arbitrary files from locally to "\ + "on the new server. You may store up to 5 files.") +@utils.arg('--key', + metavar='', + nargs='?', + const=AUTO_KEY, + help="Key the server with an SSH keypair. "\ + "Looks in ~/.ssh for a key, "\ + "or takes an explicit to one.") +@utils.arg('--reservation_id', + default=None, + metavar='', + help="Reservation ID (a UUID). "\ + "If unspecified will be generated by the server.") +@utils.arg('--min_instances', + default=None, + type=int, + metavar='', + help="The minimum number of instances to build. "\ + "Defaults to 1.") +@utils.arg('--max_instances', + default=None, + type=int, + metavar='', + help="The maximum number of instances to build. "\ + "Defaults to 'min_instances' setting.") +@utils.arg('name', metavar='', help='Name for the new server') +def do_zone_boot(cs, args): + """Boot a new server, potentially across Zones.""" + reservation_id = args.reservation_id + min_count = args.min_instances + max_count = args.max_instances + name, image, flavor, ipgroup, metadata, \ + files, reservation_id, min_count, max_count = \ + _boot(cs, args, + reservation_id=reservation_id, + min_count=min_count, + max_count=max_count) - if keyfile: - try: - files['/root/.ssh/authorized_keys2'] = open(keyfile) - except IOError, e: - raise CommandError("Can't open '%s': %s" % (keyfile, e)) - - return (args.name, image, flavor, ipgroup, metadata, files, - reservation_id, min_count, max_count) - - @arg('--flavor', - default=None, - metavar='', - help="Flavor ID (see 'novaclient flavors'). "\ - "Defaults to 256MB RAM instance.") - @arg('--image', - default=None, - metavar='', - help="Image ID (see 'novaclient images'). "\ - "Defaults to Ubuntu 10.04 LTS.") - @arg('--ipgroup', - default=None, - metavar='', - help="IP group name or ID (see 'novaclient ipgroup-list').") - @arg('--meta', - metavar="", - action='append', - default=[], - help="Record arbitrary key/value metadata. "\ - "May be give multiple times.") - @arg('--file', - metavar="", - action='append', - dest='files', - default=[], - help="Store arbitrary files from locally to "\ - "on the new server. You may store up to 5 files.") - @arg('--key', - metavar='', - nargs='?', - const=AUTO_KEY, - help="Key the server with an SSH keypair. "\ - "Looks in ~/.ssh for a key, "\ - "or takes an explicit to one.") - @arg('name', metavar='', help='Name for the new server') - def do_boot(self, args): - """Boot a new server.""" - name, image, flavor, ipgroup, metadata, files, reservation_id, \ - min_count, max_count = self._boot(args) - - server = self.cs.servers.create(args.name, image, flavor, + reservation_id = cs.zones.boot(args.name, image, flavor, ipgroup=ipgroup, meta=metadata, files=files, + reservation_id=reservation_id, min_count=min_count, max_count=max_count) - print_dict(server._info) + print "Reservation ID=", reservation_id - @arg('--flavor', - default=None, - metavar='', - help="Flavor ID (see 'novaclient flavors'). "\ - "Defaults to 256MB RAM instance.") - @arg('--image', - default=None, - metavar='', - help="Image ID (see 'novaclient images'). "\ - "Defaults to Ubuntu 10.04 LTS.") - @arg('--ipgroup', - default=None, - metavar='', - help="IP group name or ID (see 'novaclient ipgroup-list').") - @arg('--meta', - metavar="", - action='append', - default=[], - help="Record arbitrary key/value metadata. "\ - "May be give multiple times.") - @arg('--file', - metavar="", - action='append', - dest='files', - default=[], - help="Store arbitrary files from locally to "\ - "on the new server. You may store up to 5 files.") - @arg('--key', - metavar='', - nargs='?', - const=AUTO_KEY, - help="Key the server with an SSH keypair. "\ - "Looks in ~/.ssh for a key, "\ - "or takes an explicit to one.") - @arg('account', metavar='', help='Account to build this'\ - ' server for') - @arg('name', metavar='', help='Name for the new server') - def do_boot_for_account(self, args): - """Boot a new server in an account.""" - name, image, flavor, ipgroup, metadata, files, reservation_id, \ - min_count, max_count = self._boot(args) +def _translate_flavor_keys(collection): + convert = [('ram', 'memory_mb'), ('disk', 'local_gb')] + for item in collection: + keys = item.__dict__.keys() + 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]) - server = self.cs.accounts.create_instance_for(args.account, args.name, - image, flavor, - ipgroup=ipgroup, - meta=metadata, - files=files) - print_dict(server._info) +def do_flavor_list(cs, args): + """Print a list of available 'flavors' (sizes of servers).""" + flavors = cs.flavors.list() + _translate_flavor_keys(flavors) + utils.print_list(flavors, [ + 'ID', + 'Name', + 'Memory_MB', + 'Swap', + 'Local_GB', + 'VCPUs', + 'RXTX_Quota', + 'RXTX_Cap']) - @arg('--flavor', - default=None, - metavar='', - help="Flavor ID (see 'novaclient flavors'). "\ - "Defaults to 256MB RAM instance.") - @arg('--image', - default=None, - metavar='', - help="Image ID (see 'novaclient images'). "\ - "Defaults to Ubuntu 10.04 LTS.") - @arg('--ipgroup', - default=None, - metavar='', - help="IP group name or ID (see 'novaclient ipgroup-list').") - @arg('--meta', - metavar="", - action='append', - default=[], - help="Record arbitrary key/value metadata. "\ - "May be give multiple times.") - @arg('--file', - metavar="", - action='append', - dest='files', - default=[], - help="Store arbitrary files from locally to "\ - "on the new server. You may store up to 5 files.") - @arg('--key', - metavar='', - nargs='?', - const=AUTO_KEY, - help="Key the server with an SSH keypair. "\ - "Looks in ~/.ssh for a key, "\ - "or takes an explicit to one.") - @arg('--reservation_id', - default=None, - metavar='', - help="Reservation ID (a UUID). "\ - "If unspecified will be generated by the server.") - @arg('--min_instances', - default=None, - type=int, - metavar='', - help="The minimum number of instances to build. "\ - "Defaults to 1.") - @arg('--max_instances', - default=None, - type=int, - metavar='', - help="The maximum number of instances to build. "\ - "Defaults to 'min_instances' setting.") - @arg('name', metavar='', help='Name for the new server') - def do_zone_boot(self, args): - """Boot a new server, potentially across Zones.""" - reservation_id = args.reservation_id - min_count = args.min_instances - max_count = args.max_instances - name, image, flavor, ipgroup, metadata, \ - files, reservation_id, min_count, max_count = \ - self._boot(args, - reservation_id=reservation_id, - min_count=min_count, - max_count=max_count) +def do_image_list(cs, args): + """Print a list of available images to boot from.""" + utils.print_list(cs.images.list(), ['ID', 'Name', 'Status']) - reservation_id = self.cs.zones.boot(args.name, image, flavor, - ipgroup=ipgroup, - meta=metadata, - files=files, - reservation_id=reservation_id, - min_count=min_count, - max_count=max_count) - print "Reservation ID=", reservation_id +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('name', metavar='', help='Name of snapshot.') +def do_image_create(cs, args): + """Create a new image by taking a snapshot of a running server.""" + server = _find_server(cs, args.server) + image = cs.images.create(server, args.name) + utils.print_dict(image._info) - def _translate_flavor_keys(self, collection): - convert = [('ram', 'memory_mb'), ('disk', 'local_gb')] - for item in collection: - keys = item.__dict__.keys() - 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]) +@utils.arg('image', metavar='', help='Name or ID of image.') +def do_image_delete(cs, args): + """ + Delete an image. - def do_flavor_list(self, args): - """Print a list of available 'flavors' (sizes of servers).""" - flavors = self.cs.flavors.list() - self._translate_flavor_keys(flavors) - print_list(flavors, [ - 'ID', - 'Name', - 'Memory_MB', - 'Swap', - 'Local_GB', - 'VCPUs', - 'RXTX_Quota', - 'RXTX_Cap']) + It should go without saying, but you can only delete images you + created. + """ + image = _find_image(cs, args.image) + image.delete() - def do_image_list(self, args): - """Print a list of available images to boot from.""" - print_list(self.cs.images.list(), ['ID', 'Name', 'Status']) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('group', metavar='', help='Name or ID of group.') +@utils.arg('address', metavar='
', help='IP address to share.') +def do_ip_share(cs, args): + """Share an IP address from the given IP group onto a server.""" + server = _find_server(cs, args.server) + group = _find_ipgroup(cs, args.group) + server.share_ip(group, args.address) - @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.") - 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) - print_dict(image._info) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('address', metavar='
', + help='Shared IP address to remove from the server.') +def do_ip_unshare(cs, args): + """Stop sharing an given address with a server.""" + server = _find_server(cs, args.server) + server.unshare_ip(args.address) - @arg('image', metavar='', help='Name or ID of image.') - def do_image_delete(self, args): - """ - Delete an image. +def do_ipgroup_list(cs, args): + """Show IP groups.""" + def pretty_server_list(ipgroup): + return ", ".join(cs.servers.get(id).name + for id in ipgroup.servers) - It should go without saying, but you can only delete images you - created. - """ - image = self._find_image(args.image) - image.delete() + utils.print_list(cs.ipgroups.list(), + fields=['ID', 'Name', 'Server List'], + formatters={'Server List': pretty_server_list}) - @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) +@utils.arg('group', metavar='', help='Name or ID of group.') +def do_ipgroup_show(cs, args): + """Show details about a particular IP group.""" + group = _find_ipgroup(cs, args.group) + utils.print_dict(group._info) - @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) +@utils.arg('name', metavar='', help='What to name this new group.') +@utils.arg('server', metavar='', nargs='?', + help='Server (name or ID) to make a member of this new group.') +def do_ipgroup_create(cs, args): + """Create a new IP group.""" + if args.server: + server = _find_server(cs, args.server) + else: + server = None + group = cs.ipgroups.create(args.name, server) + utils.print_dict(group._info) - def do_ipgroup_list(self, args): - """Show IP groups.""" - def pretty_server_list(ipgroup): - return ", ".join(self.cs.servers.get(id).name - for id in ipgroup.servers) +@utils.arg('group', metavar='', help='Name or ID of group.') +def do_ipgroup_delete(cs, args): + """Delete an IP group.""" + _find_ipgroup(cs, args.group).delete() - print_list(self.cs.ipgroups.list(), - fields=['ID', 'Name', 'Server List'], - formatters={'Server List': pretty_server_list}) +@utils.arg('--fixed_ip', + dest='fixed_ip', + metavar='', + default=None, + help='Only match against fixed IP.') +@utils.arg('--reservation_id', + dest='reservation_id', + metavar='', + default=None, + help='Only return instances that match reservation_id.') +@utils.arg('--recurse_zones', + dest='recurse_zones', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0, + help='Recurse through all zones if set.') +@utils.arg('--ip', + dest='ip', + metavar='', + default=None, + help='Search with regular expression match by IP address') +@utils.arg('--ip6', + dest='ip6', + metavar='', + default=None, + help='Search with regular expression match by IPv6 address') +@utils.arg('--server_name', + dest='server_name', + metavar='', + default=None, + help='Search with regular expression match by server name') +@utils.arg('--name', + dest='display_name', + metavar='', + default=None, + help='Search with regular expression match by display name') +@utils.arg('--instance_name', + dest='name', + metavar='', + default=None, + help='Search with regular expression match by instance name') +def do_list(cs, args): + """List active servers.""" + recurse_zones = args.recurse_zones + search_opts = { + 'reservation_id': args.reservation_id, + 'fixed_ip': args.fixed_ip, + 'recurse_zones': recurse_zones, + 'ip': args.ip, + 'ip6': args.ip6, + 'name': args.name, + 'server_name': args.server_name, + 'display_name': args.display_name} + if recurse_zones: + to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP'] + else: + to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP'] + utils.print_list(cs.servers.list(search_opts=search_opts), + to_print) - @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) +@utils.arg('--hard', + dest='reboot_type', + action='store_const', + const=servers.REBOOT_HARD, + default=servers.REBOOT_SOFT, + help='Perform a hard reboot (instead of a soft one).') +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_reboot(cs, args): + """Reboot a server.""" + _find_server(cs, args.server).reboot(args.reboot_type) - @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) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('image', metavar='', help="Name or ID of new image.") +def do_rebuild(cs, args): + """Shutdown, re-image, and re-boot a server.""" + server = _find_server(cs, args.server) + image = _find_image(cs, args.image) + server.rebuild(image) - @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() +@utils.arg('server', metavar='', help='Name (old name) or ID of server.') +@utils.arg('name', metavar='', help='New name for the server.') +def do_rename(cs, args): + """Rename a server.""" + _find_server(cs, args.server).update(name=args.name) - @arg('--fixed_ip', - dest='fixed_ip', - metavar='', - default=None, - help='Only match against fixed IP.') - @arg('--reservation_id', - dest='reservation_id', - metavar='', - default=None, - help='Only return instances that match reservation_id.') - @arg('--recurse_zones', - dest='recurse_zones', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=0, - help='Recurse through all zones if set.') - @arg('--ip', - dest='ip', - metavar='', - default=None, - help='Search with regular expression match by IP address') - @arg('--ip6', - dest='ip6', - metavar='', - default=None, - help='Search with regular expression match by IPv6 address') - @arg('--server_name', - dest='server_name', - metavar='', - default=None, - help='Search with regular expression match by server name') - @arg('--name', - dest='display_name', - metavar='', - default=None, - help='Search with regular expression match by display name') - @arg('--instance_name', - dest='name', - metavar='', - default=None, - help='Search with regular expression match by instance name') - def do_list(self, args): - """List active servers.""" - recurse_zones = args.recurse_zones - search_opts = { - 'reservation_id': args.reservation_id, - 'fixed_ip': args.fixed_ip, - 'recurse_zones': recurse_zones, - 'ip': args.ip, - 'ip6': args.ip6, - 'name': args.name, - 'server_name': args.server_name, - 'display_name': args.display_name} - if recurse_zones: - to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP'] - else: - to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP'] - print_list(self.cs.servers.list(search_opts=search_opts), - to_print) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('flavor', metavar='', help="Name or ID of new flavor.") +def do_resize(cs, args): + """Resize a server.""" + server = _find_server(cs, args.server) + flavor = _find_flavor(cs, args.flavor) + server.resize(flavor) - @arg('--hard', - dest='reboot_type', - action='store_const', - const=servers.REBOOT_HARD, - default=servers.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) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('name', metavar='', help='Name of snapshot.') +@utils.arg('backup_type', metavar='', help='type of backup') +@utils.arg('rotation', type=int, metavar='', + help="Number of backups to retain. Used for backup image_type.") +def do_backup(cs, args): + """Resize a server.""" + server = _find_server(cs, args.server) + server.backup(args.name, args.backup_type, args.rotation) - @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) +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_migrate(cs, args): + """Migrate a server.""" + _find_server(cs, args.server).migrate() - @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) +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_pause(cs, args): + """Pause a server.""" + _find_server(cs, args.server).pause() - @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) +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_unpause(cs, args): + """Unpause a server.""" + _find_server(cs, args.server).unpause() - @arg('server', metavar='', help='Name or ID of server.') - def do_migrate(self, args): - """Migrate a server.""" - self._find_server(args.server).migrate() +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_suspend(cs, args): + """Suspend a server.""" + _find_server(cs, args.server).suspend() - @arg('server', metavar='', help='Name or ID of server.') - def do_pause(self, args): - """Pause a server.""" - self._find_server(args.server).pause() +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_resume(cs, args): + """Resume a server.""" + _find_server(cs, args.server).resume() - @arg('server', metavar='', help='Name or ID of server.') - def do_unpause(self, args): - """Unpause a server.""" - self._find_server(args.server).unpause() +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_rescue(cs, args): + """Rescue a server.""" + _find_server(cs, args.server).rescue() - @arg('server', metavar='', help='Name or ID of server.') - def do_suspend(self, args): - """Suspend a server.""" - self._find_server(args.server).suspend() +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_unrescue(cs, args): + """Unrescue a server.""" + _find_server(cs, args.server).unrescue() - @arg('server', metavar='', help='Name or ID of server.') - def do_resume(self, args): - """Resume a server.""" - self._find_server(args.server).resume() +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_diagnostics(cs, args): + """Retrieve server diagnostics.""" + utils.print_dict(cs.servers.diagnostics(args.server)[1]) - @arg('server', metavar='', help='Name or ID of server.') - def do_rescue(self, args): - """Rescue a server.""" - self._find_server(args.server).rescue() +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_actions(cs, args): + """Retrieve server actions.""" + utils.print_list( + cs.servers.actions(args.server), + ["Created_At", "Action", "Error"]) - @arg('server', metavar='', help='Name or ID of server.') - def do_unrescue(self, args): - """Unrescue a server.""" - self._find_server(args.server).unrescue() +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_resize_confirm(cs, args): + """Confirm a previous resize.""" + _find_server(cs, args.server).confirm_resize() - @arg('server', metavar='', help='Name or ID of server.') - def do_diagnostics(self, args): - """Retrieve server diagnostics.""" - print_dict(self.cs.servers.diagnostics(args.server)[1]) +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_resize_revert(cs, args): + """Revert a previous resize (and return to the previous VM).""" + _find_server(cs, args.server).revert_resize() - @arg('server', metavar='', help='Name or ID of server.') - def do_actions(self, args): - """Retrieve server actions.""" - print_list( - self.cs.servers.actions(args.server), - ["Created_At", "Action", "Error"]) +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_root_password(cs, args): + """ + Change the root password for a server. + """ + server = _find_server(cs, args.server) + p1 = getpass.getpass('New password: ') + p2 = getpass.getpass('Again: ') + if p1 != p2: + raise exceptions.CommandError("Passwords do not match.") + server.update(password=p1) - @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() +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_show(cs, args): + """Show details about the given server.""" + s = _find_server(cs, args.server) - @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() + info = s._info.copy() + addresses = info.pop('addresses') + for addrtype in addresses: + info['%s ip' % addrtype] = ', '.join(addresses[addrtype]) - @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) + flavorId = info.get('flavorId', None) + if flavorId: + info['flavor'] = _find_flavor(cs, info.pop('flavorId')).name + imageId = info.get('imageId', None) + if imageId: + info['image'] = _find_image(cs, info.pop('imageId')).name - @arg('server', metavar='', help='Name or ID of server.') - def do_show(self, args): - """Show details about the given server.""" - s = self._find_server(args.server) + utils.print_dict(info) - info = s._info.copy() - addresses = info.pop('addresses') - for addrtype in addresses: - info['%s ip' % addrtype] = ', '.join(addresses[addrtype]) +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_delete(cs, args): + """Immediately shut down and delete a server.""" + _find_server(cs, args.server).delete() - flavorId = info.get('flavorId', None) - if flavorId: - info['flavor'] = self._find_flavor(info.pop('flavorId')).name - imageId = info.get('imageId', None) - if imageId: - info['image'] = self._find_image(info.pop('imageId')).name +# --zone_username is required since --username is already used. +@utils.arg('zone', metavar='', help='ID of the zone', default=None) +@utils.arg('--api_url', dest='api_url', default=None, help='New URL.') +@utils.arg('--zone_username', dest='zone_username', default=None, + help='New zone username.') +@utils.arg('--password', dest='password', default=None, help='New password.') +@utils.arg('--weight_offset', dest='weight_offset', default=None, + help='Child Zone weight offset.') +@utils.arg('--weight_scale', dest='weight_scale', default=None, + help='Child Zone weight scale.') +def do_zone(cs, args): + """Show or edit a child zone. No zone arg for this zone.""" + zone = cs.zones.get(args.zone) - print_dict(info) + # If we have some flags, update the zone + zone_delta = {} + if args.api_url: + zone_delta['api_url'] = args.api_url + if args.zone_username: + zone_delta['username'] = args.zone_username + if args.password: + zone_delta['password'] = args.password + if args.weight_offset: + zone_delta['weight_offset'] = args.weight_offset + if args.weight_scale: + zone_delta['weight_scale'] = args.weight_scale + if zone_delta: + zone.update(**zone_delta) + else: + utils.print_dict(zone._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 do_zone_info(cs, args): + """Get this zones name and capabilities.""" + zone = cs.zones.info() + utils.print_dict(zone._info) - # --zone_username is required since --username is already used. - @arg('zone', metavar='', help='ID of the zone', default=None) - @arg('--api_url', dest='api_url', default=None, help='New URL.') - @arg('--zone_username', dest='zone_username', default=None, - help='New zone username.') - @arg('--password', dest='password', default=None, help='New password.') - @arg('--weight_offset', dest='weight_offset', default=None, - help='Child Zone weight offset.') - @arg('--weight_scale', dest='weight_scale', default=None, - help='Child Zone weight scale.') - def do_zone(self, args): - """Show or edit a child zone. No zone arg for this zone.""" - zone = self.cs.zones.get(args.zone) +@utils.arg('api_url', metavar='', help="URL for the Zone's API") +@utils.arg('zone_username', metavar='', + help='Authentication username.') +@utils.arg('password', metavar='', help='Authentication password.') +@utils.arg('weight_offset', metavar='', + help='Child Zone weight offset (typically 0.0).') +@utils.arg('weight_scale', metavar='', + help='Child Zone weight scale (typically 1.0).') +def do_zone_add(cs, args): + """Add a new child zone.""" + zone = cs.zones.create(args.api_url, args.zone_username, + args.password, args.weight_offset, + args.weight_scale) + utils.print_dict(zone._info) - # If we have some flags, update the zone - zone_delta = {} - if args.api_url: - zone_delta['api_url'] = args.api_url - if args.zone_username: - zone_delta['username'] = args.zone_username - if args.password: - zone_delta['password'] = args.password - if args.weight_offset: - zone_delta['weight_offset'] = args.weight_offset - if args.weight_scale: - zone_delta['weight_scale'] = args.weight_scale - if zone_delta: - zone.update(**zone_delta) - else: - print_dict(zone._info) +@utils.arg('zone', metavar='', help='Name or ID of the zone') +def do_zone_delete(cs, args): + """Delete a zone.""" + cs.zones.delete(args.zone) - def do_zone_info(self, args): - """Get this zones name and capabilities.""" - zone = self.cs.zones.info() - print_dict(zone._info) +def do_zone_list(cs, args): + """List the children of a zone.""" + utils.print_list(cs.zones.list(), ['ID', 'Name', 'Is Active', \ + 'API URL', 'Weight Offset', 'Weight Scale']) - @arg('api_url', metavar='', help="URL for the Zone's API") - @arg('zone_username', metavar='', - help='Authentication username.') - @arg('password', metavar='', help='Authentication password.') - @arg('weight_offset', metavar='', - help='Child Zone weight offset (typically 0.0).') - @arg('weight_scale', metavar='', - 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, - args.password, args.weight_offset, - args.weight_scale) - print_dict(zone._info) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('network_id', metavar='', help='Network ID.') +def do_add_fixed_ip(cs, args): + """Add new IP address to network.""" + server = _find_server(cs, args.server) + server.add_fixed_ip(args.network_id) - @arg('zone', metavar='', help='Name or ID of the zone') - def do_zone_delete(self, args): - """Delete a zone.""" - self.cs.zones.delete(args.zone) +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('address', metavar='
', help='IP Address.') +def do_remove_fixed_ip(cs, args): + """Remove an IP address from a server.""" + server = _find_server(cs, args.server) + server.remove_fixed_ip(args.address) - def do_zone_list(self, args): - """List the children of a zone.""" - print_list(self.cs.zones.list(), ['ID', 'Name', 'Is Active', \ - 'API URL', 'Weight Offset', 'Weight Scale']) +def _find_server(cs, server): + """Get a server by name or ID.""" + return _find_resource(cs.servers, server) - @arg('server', metavar='', help='Name or ID of server.') - @arg('network_id', metavar='', help='Network ID.') - def do_add_fixed_ip(self, args): - """Add new IP address to network.""" - server = self._find_server(args.server) - server.add_fixed_ip(args.network_id) +def _find_ipgroup(cs, group): + """Get an IP group by name or ID.""" + return _find_resource(cs.ipgroups, group) - @arg('server', metavar='', help='Name or ID of server.') - @arg('address', metavar='
', help='IP Address.') - def do_remove_fixed_ip(self, args): - """Remove an IP address from a server.""" - server = self._find_server(args.server) - server.remove_fixed_ip(args.address) +def _find_image(cs, image): + """Get an image by name or ID.""" + return _find_resource(cs.images, image) - 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 exceptions.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)) - - try: - uuid.UUID(name_or_id) - return manager.get(name_or_id) - except ValueError: - return manager.find(name=name_or_id) - except exceptions.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 OpenStackHelpFormatter(argparse.HelpFormatter): - def start_section(self, heading): - # Title-case the headings - heading = '%s%s' % (heading[0].upper(), heading[1:]) - super(OpenStackHelpFormatter, self).start_section(heading) - - -# Helpers -def print_list(objs, fields, formatters={}): - pt = prettytable.PrettyTable([f for f in fields], caching=False) - pt.aligns = ['l' for f in fields] - - for o in objs: - row = [] - for field in fields: - if field in formatters: - row.append(formatters[field](o)) - else: - field_name = field.lower().replace(' ', '_') - data = getattr(o, field_name, '') - row.append(data) - pt.add_row(row) - - 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(): +def _find_flavor(cs, flavor): + """Get a flavor by name, ID, or RAM size.""" try: - OpenStackShell().main(sys.argv[1:]) + return _find_resource(cs.flavors, flavor) + except exceptions.NotFound: + return cs.flavors.find(ram=flavor) + +def _find_resource(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)) + + try: + uuid.UUID(name_or_id) + return manager.get(name_or_id) + except ValueError: + return manager.find(name=name_or_id) + except exceptions.NotFound: + raise exceptions.CommandError("No %s with a name or ID of '%s' exists." % + (manager.resource_class.__name__.lower(), name_or_id)) - except Exception, e: - if httplib2.debuglevel == 1: - raise # dump stack. - else: - print >> sys.stderr, e - sys.exit(1) diff --git a/novaclient/v1_0/zones.py b/novaclient/v1_0/zones.py index f711ec219..01d128f69 100644 --- a/novaclient/v1_0/zones.py +++ b/novaclient/v1_0/zones.py @@ -17,7 +17,8 @@ Zone interface. """ -from novaclient.v1_0 import base +from novaclient import base +from novaclient.v1_0 import base as local_base class Weighting(base.Resource): @@ -64,7 +65,7 @@ class Zone(base.Resource): weight_offset, weight_scale) -class ZoneManager(base.BootingManagerWithFind): +class ZoneManager(local_base.BootingManagerWithFind): resource_class = Zone def info(self): diff --git a/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py index 955b27e53..e69de29bb 100644 --- a/novaclient/v1_1/__init__.py +++ b/novaclient/v1_1/__init__.py @@ -1,66 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v1_1 import client -from novaclient.v1_1 import exceptions -from novaclient.v1_1 import flavors -from novaclient.v1_1 import images -from novaclient.v1_1 import servers - - -class Client(object): - """ - Top-level object to access the OpenStack Compute v1.0 API. - - Create an instance with your creds:: - - >>> os = novaclient.v1_1.Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL) - - Then call methods on its managers:: - - >>> os.servers.list() - ... - >>> os.flavors.list() - ... - - &c. - """ - - def __init__(self, username, apikey, projectid, auth_url=None, timeout=None): - """Initialize v1.0 Openstack Client.""" - self.flavors = flavors.FlavorManager(self) - self.images = images.ImageManager(self) - self.servers = servers.ServerManager(self) - - auth_url = auth_url or "https://auth.api.rackspacecloud.com/v1.0" - - self.client = client.HTTPClient(username, - apikey, - projectid, - auth_url, - timeout=timeout) - - 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 :exc:`novaclient.Unauthorized` if the - credentials are wrong. - """ - self.client.authenticate() diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py index f283d8596..9113d53bb 100644 --- a/novaclient/v1_1/base.py +++ b/novaclient/v1_1/base.py @@ -19,7 +19,7 @@ Base utilities to build API operation managers and objects on top of. """ -from novaclient.v1_1 import exceptions +from novaclient import exceptions # Python 2.4 compat try: diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index a805826d8..ee7195896 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -1,154 +1,48 @@ -# Copyright 2010 Jacob Kaplan-Moss -""" -OpenStack Client interface. Handles the REST calls and responses. -""" -import time -import urlparse -import urllib -import httplib2 -import logging -try: - import json -except ImportError: - import simplejson as json +from novaclient import client +from novaclient.v1_1 import flavors +from novaclient.v1_1 import images +from novaclient.v1_1 import servers -# Python 2.5 compat fix -if not hasattr(urlparse, 'parse_qsl'): - import cgi - urlparse.parse_qsl = cgi.parse_qsl -import novaclient -from novaclient.v1_1 import exceptions -_logger = logging.getLogger(__name__) +class Client(object): + """ + Top-level object to access the OpenStack Compute API. -class HTTPClient(httplib2.Http): + Create an instance with your creds:: - USER_AGENT = 'python-novaclient/%s' % novaclient.__version__ + >>> client = Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL) - def __init__(self, user, apikey, projectid, auth_url, timeout=None): - super(HTTPClient, self).__init__(timeout=timeout) - self.user = user - self.apikey = apikey - self.projectid = projectid - self.auth_url = auth_url - self.version = 'v1.0' + Then call methods on its managers:: - self.management_url = None - self.auth_token = None + >>> client.servers.list() + ... + >>> client.flavors.list() + ... - # httplib2 overrides - self.force_exception_to_status_code = True + """ - def http_log(self, args, kwargs, resp, body): - if not _logger.isEnabledFor(logging.DEBUG): - return + def __init__(self, username, api_key, project_id, auth_url, timeout=None): + self.flavors = flavors.FlavorManager(self) + self.images = images.ImageManager(self) + self.servers = servers.ServerManager(self) - string_parts = ['curl -i'] - for element in args: - 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])) - - _logger.debug("REQ: %s\n" % "".join(string_parts)) - _logger.debug("RESP:%s %s\n", resp,body) - - 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(HTTPClient, self).request(*args, **kwargs) - - self.http_log(args, kwargs, resp, body) - - if body: - try: - body = json.loads(body) - except ValueError, e: - pass - else: - body = None - - if resp.status in (400, 401, 403, 404, 408, 413, 500, 501): - 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 - if self.projectid: - kwargs['headers']['X-Auth-Project-Id'] = self.projectid - - 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): - url = self._munge_get_url(url) - 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) + self.client = client.HTTPClient(username, + api_key, + project_id, + auth_url, + timeout=timeout) def authenticate(self): - scheme, netloc, path, query, frag = urlparse.urlsplit( - self.auth_url) - path_parts = path.split('/') - for part in path_parts: - if len(part) > 0 and part[0] == 'v': - self.version = part - break - - headers = {'X-Auth-User': self.user, - 'X-Auth-Key': self.apikey} - if self.projectid: - headers['X-Auth-Project-Id'] = self.projectid - 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'] - - def _munge_get_url(self, url): """ - Munge GET URLs to always return uncached content. + Authenticate against the server. - The OpenStack Nova API caches data *very* agressively and doesn't - respect cache headers. To avoid stale data, then, we append a little - bit of nonsense onto GET parameters; this appears to force the data not - to be cached. + 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 :exc:`exceptions.Unauthorized` if the + credentials are wrong. """ - scheme, netloc, path, query, frag = urlparse.urlsplit(url) - query = urlparse.parse_qsl(query) - query.append(('fresh', str(time.time()))) - query = urllib.urlencode(query) - return urlparse.urlunsplit((scheme, netloc, path, query, frag)) + self.client.authenticate() diff --git a/novaclient/v1_1/exceptions.py b/novaclient/v1_1/exceptions.py deleted file mode 100644 index 1709d806f..000000000 --- a/novaclient/v1_1/exceptions.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -""" -Exception definitions. -""" - -class OpenStackException(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(OpenStackException): - """ - HTTP 400 - Bad request: you sent some malformed data. - """ - http_status = 400 - message = "Bad request" - - -class Unauthorized(OpenStackException): - """ - HTTP 401 - Unauthorized: bad credentials. - """ - http_status = 401 - message = "Unauthorized" - - -class Forbidden(OpenStackException): - """ - HTTP 403 - Forbidden: your credentials don't give you access to this - resource. - """ - http_status = 403 - message = "Forbidden" - - -class NotFound(OpenStackException): - """ - HTTP 404 - Not found - """ - http_status = 404 - message = "Not found" - - -class OverLimit(OpenStackException): - """ - HTTP 413 - Over limit: you're over the API limits for this time period. - """ - http_status = 413 - message = "Over limit" - - -# NotImplemented is a python keyword. -class HTTPNotImplemented(OpenStackException): - """ - HTTP 501 - Not Implemented: the server does not support this operation. - """ - http_status = 501 - message = "Not Implemented" - - -# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() -# so we can do this: -# _code_map = dict((c.http_status, c) -# for c in OpenStackException.__subclasses__()) -# -# Instead, we have to hardcode it: -_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, - Forbidden, NotFound, OverLimit, HTTPNotImplemented]) - - -def from_response(response, body): - """ - Return an instance of an OpenStackException 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, OpenStackException) - if body: - message = "n/a" - details = "n/a" - if hasattr(body, 'keys'): - error = body[body.keys()[0]] - message = error.get('message', None) - details = error.get('details', None) - return cls(code=response.status, message=message, details=details) - else: - return cls(code=response.status) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py index f4a82a962..6eb1e2c4b 100644 --- a/novaclient/v1_1/flavors.py +++ b/novaclient/v1_1/flavors.py @@ -3,7 +3,7 @@ Flavor interface. """ -from novaclient.v1_1 import base +from novaclient import base class Flavor(base.Resource): @@ -26,10 +26,10 @@ class FlavorManager(base.ManagerWithFind): :rtype: list of :class:`Flavor`. """ - detail = "" - if detailed: - detail = "/detail" - return self._list("/flavors%s" % detail, "flavors") + if detailed is True: + return self._list("/flavors/detail", "flavors") + else: + return self._list("/flavors", "flavors") def get(self, flavor): """ diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py index 48d86ac8d..e25c237e7 100644 --- a/novaclient/v1_1/images.py +++ b/novaclient/v1_1/images.py @@ -3,7 +3,7 @@ Image interface. """ -from novaclient.v1_1 import base +from novaclient import base class Image(base.Resource): @@ -41,38 +41,10 @@ class ImageManager(base.ManagerWithFind): :rtype: list of :class:`Image` """ - detail = "" - if detailed: - detail = "/detail" - return self._list("/images%s" % detail, "images") - - - def create(self, server, name, image_type=None, backup_type=None, rotation=None): - """ - 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` - """ - 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}} - return self._create("/images", data, "image") + if detailed is True: + return self._list("/images/detail", "images") + else: + return self._list("/images", "images") def delete(self, image): """ diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index fbba4ae18..e51ece70d 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -20,7 +20,10 @@ Server interface. """ import urllib -from novaclient.v1_1 import base + +from novaclient import base +from novaclient.v1_0 import base as local_base + REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -42,22 +45,11 @@ class Server(base.Resource): :param name: Update the server's name. :param password: Update the root password. """ - self.manager.update(self, name) - - def create_image(self, name, metadata=None): - """ - Create an image based on this server. - - :param name: The name of the image to create - :param metadata: The metadata to associated with the image. - """ - self.manager.create_image(self, name, metadata) + self.manager.update(self, name=name) def change_password(self, password): """ - Update the root password on this server. - - :param password: The password to set. + Update the password for a server. """ self.manager.change_password(self, password) @@ -91,6 +83,15 @@ class Server(base.Resource): """ self.manager.resize(self, flavor) + def create_image(self, image_name, metadata): + """ + Create an image based on this server. + + :param image_name: The name to assign the newly create image. + :param metadata: Metadata to assign to the image. + """ + self.manager.create_image(self, image_name, metadata) + def confirm_resize(self): """ Confirm that the resize worked, thus removing the original server. @@ -121,24 +122,8 @@ class Server(base.Resource): return "" return self.addresses['private'] - @property - def image_id(self): - """ - Shortcut to get the image identifier. - """ - return self.image["id"] - @property - def flavor_id(self): - """ - Shortcut to get the flavor identifier. - """ - return self.flavor["id"] - - - - -class ServerManager(base.BootingManagerWithFind): +class ServerManager(local_base.BootingManagerWithFind): resource_class = Server def get(self, server): @@ -161,8 +146,9 @@ class ServerManager(base.BootingManagerWithFind): """ if search_opts is None: search_opts = {} + qparams = {} - # only use values in query string if they are set + for opt, val in search_opts.iteritems(): if val: qparams[opt] = val @@ -190,8 +176,30 @@ class ServerManager(base.BootingManagerWithFind): file-like object). A maximum of five entries is allowed, and each file must be 10k or less. """ - return self._boot("/servers", "server", name, image, flavor, - meta=meta, files=files) + personality = [] + + for file_path, filelike in files.items(): + try: + data = filelike.read() + except AttributeError: + data = str(filelike) + + personality.append({ + "path": file_path, + "contents": data.encode("base64"), + }) + + body = { + "server": { + "name": name, + "imageRef": base.getid(image), + "flavorRef": base.getid(flavor), + "metadata": meta or {}, + "personality": personality, + }, + } + + return self._create("/servers", body, "server", return_raw=False) def update(self, server, name=None): """ @@ -211,6 +219,12 @@ class ServerManager(base.BootingManagerWithFind): self._update("/servers/%s" % base.getid(server), body) + def change_password(self, server, password): + """ + Update the password for a server. + """ + self._action("changePassword", server, {"adminPass": password}) + def delete(self, server): """ Delete (i.e. shut down and delete the image) this server. @@ -227,32 +241,6 @@ class ServerManager(base.BootingManagerWithFind): """ self._action('reboot', server, {'type': type}) - def create_image(self, server, name, metadata=None): - """ - Create an image based on this server. - - :param server: The :class:`Server` (or its ID) to create image from. - :param name: The name of the image to create - :param metadata: The metadata to associated with the image. - """ - body = { - "name": name, - "metadata": metadata or {}, - } - self._action('createImage', server, body) - - def change_password(self, server, password): - """ - Update the root password on a server. - - :param server: The :class:`Server` (or its ID) to share onto. - :param password: The password to set. - """ - body = { - "adminPass": password, - } - self._action('changePassword', server, body) - def rebuild(self, server, image): """ Rebuild -- shut down and then re-image -- a server. @@ -292,6 +280,17 @@ class ServerManager(base.BootingManagerWithFind): """ self._action('revertResize', server) + def create_image(self, server, image_name, metadata=None): + """ + Snapshot a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param image_name: Name to give the snapshot image + :param meta: Metadata to give newly-created image entity + """ + self._action('createImage', server, + {'name': image_name, 'metadata': metadata or {}}) + def _action(self, action, server, info=None): """ Perform a server "action" -- reboot/rebuild/resize/etc. diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 7f9217e09..c4e79f120 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -15,570 +15,316 @@ # License for the specific language governing permissions and limitations # under the License. -""" -Command-line interface to the OpenStack Nova API. -""" - -import argparse import getpass -import httplib2 import os -import prettytable -import sys -import textwrap import uuid -import novaclient.v1_1 -from novaclient.v1_1 import exceptions +from novaclient import exceptions +from novaclient import utils +from novaclient.v1_1 import client from novaclient.v1_1 import servers -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) +CLIENT_CLASS = client.Client + -# Sentinal for boot --key AUTO_KEY = object() -# 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 +def _translate_flavor_keys(collection): + convert = [('ram', 'memory_mb'), ('disk', 'local_gb')] + for item in collection: + keys = item.__dict__.keys() + 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]) + +@utils.arg('--flavor', + default=None, + metavar='', + help="Flavor ID (see 'osc flavors'). "\ + "Defaults to 256MB RAM instance.") +@utils.arg('--image', + default=None, + metavar='', + help="Image ID (see 'osc images'). "\ + "Defaults to Ubuntu 10.04 LTS.") +@utils.arg('--meta', + metavar="", + action='append', + default=[], + help="Record arbitrary key/value metadata. "\ + "May be give multiple times.") +@utils.arg('--file', + metavar="", + action='append', + dest='files', + default=[], + help="Store arbitrary files from locally to "\ + "on the new server. You may store up to 5 files.") +@utils.arg('--key', + metavar='', + nargs='?', + const=AUTO_KEY, + help="Key the server with an SSH keypair. "\ + "Looks in ~/.ssh for a key, "\ + "or takes an explicit to one.") +@utils.arg('name', metavar='', help='Name for the new server') +def do_boot(cs, args): + """Boot a new server.""" + name, image, flavor, metadata, files = _boot(cs, args) + server = cs.servers.create(args.name, image, flavor, + meta=metadata, files=files) + utils.print_dict(server._info) -class CommandError(Exception): - pass +def _boot(cs, args): + """Boot a new server.""" + flavor = args.flavor or cs.flavors.find(ram=256) + image = args.image or cs.images.find(name="Ubuntu 10.04 LTS "\ + "(lucid)") -def env(e): - return os.environ.get(e, '') + metadata = dict(v.split('=') for v in args.meta) - -class OpenStackShell(object): - - # Hook for the test suite to inject a fake server. - _api_class = novaclient.v1_1.Client - - def __init__(self): - self.parser = argparse.ArgumentParser( - prog='nova', - description=__doc__.strip(), - epilog='See "nova help COMMAND" '\ - 'for help on a specific command.', - add_help=False, - formatter_class=OpenStackHelpFormatter, - ) - - # 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('NOVA_USERNAME'), - help='Defaults to env[NOVA_USERNAME].') - - self.parser.add_argument('--apikey', - default=env('NOVA_API_KEY'), - help='Defaults to env[NOVA_API_KEY].') - - self.parser.add_argument('--projectid', - default=env('NOVA_PROJECT_ID'), - help='Defaults to env[NOVA_PROJECT_ID].') - - auth_url = env('NOVA_URL') - if auth_url == '': - auth_url = 'https://auth.api.rackspacecloud.com/v1.0' - self.parser.add_argument('--url', - default=auth_url, - help='Defaults to env[NOVA_URL].') - - # 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=OpenStackHelpFormatter - ) - 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, projectid, url = args.username, args.apikey, \ - args.projectid, args.url - - #FIXME(usrleon): Here should be restrict for project id same as - # for username or apikey but for compatibility it is not. - - if not user: - raise CommandError("You must provide a username, either via " - "--username or via env[NOVA_USERNAME]") - if not apikey: - raise CommandError("You must provide an API key, either via " - "--apikey or via env[NOVA_API_KEY]") - - self.cs = self._api_class(user, apikey, projectid, url) + files = {} + for f in args.files: + dst, src = f.split('=', 1) try: - self.cs.authenticate() - except exceptions.Unauthorized: - raise CommandError("Invalid OpenStack Nova credentials.") + files[dst] = open(src) + except IOError, e: + raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) - 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) + if args.key is AUTO_KEY: + possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k) + for k in ('id_dsa.pub', 'id_rsa.pub')] + for k in possible_keys: + if os.path.exists(k): + keyfile = k + break else: - self.parser.print_help() + raise exceptions.CommandError("Couldn't find a key file: tried " + "~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub") + elif args.key: + keyfile = args.key + else: + keyfile = None - def _boot(self, args, reservation_id=None, min_count=None, max_count=None): - """Boot a new server.""" - flavor = args.flavor or self.cs.flavors.find(ram=256) - image = args.image or self.cs.images.find(name="Ubuntu 10.04 LTS "\ - "(lucid)") - - metadata = dict(v.split('=') for v in args.meta) - - files = {} - for f in args.files: - dst, src = f.split('=', 1) - try: - files[dst] = open(src) - except IOError, e: - raise CommandError("Can't open '%s': %s" % (src, e)) - - if args.key is AUTO_KEY: - possible_keys = [os.path.join(os.path.expanduser('~'), '.ssh', k) - for k in ('id_dsa.pub', 'id_rsa.pub')] - for k in possible_keys: - if os.path.exists(k): - keyfile = k - break - else: - raise CommandError("Couldn't find a key file: tried " - "~/.ssh/id_dsa.pub or ~/.ssh/id_rsa.pub") - elif args.key: - keyfile = args.key - else: - keyfile = None - - if keyfile: - try: - files['/root/.ssh/authorized_keys2'] = open(keyfile) - except IOError, e: - raise CommandError("Can't open '%s': %s" % (keyfile, e)) - - return (args.name, image, flavor, metadata, files) - - @arg('--flavor', - default=None, - metavar='', - help="Flavor ID (see 'novaclient flavors'). "\ - "Defaults to 256MB RAM instance.") - @arg('--image', - default=None, - metavar='', - help="Image ID (see 'novaclient images'). "\ - "Defaults to Ubuntu 10.04 LTS.") - @arg('--meta', - metavar="", - action='append', - default=[], - help="Record arbitrary key/value metadata. "\ - "May be give multiple times.") - @arg('--file', - metavar="", - action='append', - dest='files', - default=[], - help="Store arbitrary files from locally to "\ - "on the new server. You may store up to 5 files.") - @arg('--key', - metavar='', - nargs='?', - const=AUTO_KEY, - help="Key the server with an SSH keypair. "\ - "Looks in ~/.ssh for a key, "\ - "or takes an explicit to one.") - @arg('name', metavar='', help='Name for the new server') - def do_boot(self, args): - """Boot a new server.""" - name, image, flavor, metadata, files = self._boot(args) - - server = self.cs.servers.create(args.name, image, flavor, - meta=metadata, - files=files) - print_dict(server._info) - - def _translate_flavor_keys(self, collection): - convert = [('ram', 'memory_mb'), ('disk', 'local_gb')] - for item in collection: - keys = item.__dict__.keys() - 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]) - - @arg('--fixed_ip', - dest='fixed_ip', - metavar='', - default=None, - help='Only match against fixed IP.') - @arg('--reservation_id', - dest='reservation_id', - metavar='', - default=None, - help='Only return instances that match reservation_id.') - @arg('--recurse_zones', - dest='recurse_zones', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=0, - help='Recurse through all zones if set.') - @arg('--ip', - dest='ip', - metavar='', - default=None, - help='Search with regular expression match by IP address') - @arg('--ip6', - dest='ip6', - metavar='', - default=None, - help='Search with regular expression match by IPv6 address') - @arg('--server_name', - dest='server_name', - metavar='', - default=None, - help='Search with regular expression match by server name') - @arg('--name', - dest='display_name', - metavar='', - default=None, - help='Search with regular expression match by display name') - @arg('--instance_name', - dest='name', - metavar='', - default=None, - help='Search with regular expression match by instance name') - def do_list(self, args): - """List active servers.""" - recurse_zones = args.recurse_zones - search_opts = { - 'reservation_id': args.reservation_id, - 'fixed_ip': args.fixed_ip, - 'recurse_zones': recurse_zones, - 'ip': args.ip, - 'ip6': args.ip6, - 'name': args.name, - 'server_name': args.server_name, - 'display_name': args.display_name} - if recurse_zones: - to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP'] - else: - to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP'] - print_list(self.cs.servers.list(search_opts=search_opts), - to_print) - - def do_flavor_list(self, args): - """Print a list of available 'flavors' (sizes of servers).""" - flavors = self.cs.flavors.list() - self._translate_flavor_keys(flavors) - print_list(flavors, [ - 'ID', - 'Name', - 'Memory_MB', - 'Swap', - 'Local_GB', - 'VCPUs', - 'RXTX_Quota', - 'RXTX_Cap']) - - 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 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.") - def do_create_image(self, args): - """Create a new image by taking a snapshot of a running server.""" - server = self._find_server(args.server) - server.create_image(args.name) - - @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 can only delete images you - created. - """ - image = self._find_image(args.image) - image.delete() - - @arg('--hard', - dest='reboot_type', - action='store_const', - const=servers.REBOOT_HARD, - default=servers.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_migrate(self, args): - """Migrate a server.""" - self._find_server(args.server).migrate() - - @arg('server', metavar='', help='Name or ID of server.') - def do_pause(self, args): - """Pause a server.""" - self._find_server(args.server).pause() - - @arg('server', metavar='', help='Name or ID of server.') - def do_unpause(self, args): - """Unpause a server.""" - self._find_server(args.server).unpause() - - @arg('server', metavar='', help='Name or ID of server.') - def do_suspend(self, args): - """Suspend a server.""" - self._find_server(args.server).suspend() - - @arg('server', metavar='', help='Name or ID of server.') - def do_resume(self, args): - """Resume a server.""" - self._find_server(args.server).resume() - - @arg('server', metavar='', help='Name or ID of server.') - def do_rescue(self, args): - """Rescue a server.""" - self._find_server(args.server).rescue() - - @arg('server', metavar='', help='Name or ID of server.') - def do_unrescue(self, args): - """Unrescue a server.""" - self._find_server(args.server).unrescue() - - @arg('server', metavar='', help='Name or ID of server.') - def do_diagnostics(self, args): - """Retrieve server diagnostics.""" - print_dict(self.cs.servers.diagnostics(args.server)[1]) - - @arg('server', metavar='', help='Name or ID of server.') - def do_actions(self, args): - """Retrieve server actions.""" - print_list( - self.cs.servers.actions(args.server), - ["Created_At", "Action", "Error"]) - - @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.change_password(p1) - - @arg('server', metavar='', help='Name or ID of server.') - def do_show(self, args): - """Show details about the given server.""" - s = self._find_server(args.server) - - info = s._info.copy() - addresses = info.pop('addresses') - for network_name in addresses.keys(): - ips = map(lambda x: x["addr"], addresses[network_name]) - info['%s ip' % network_name] = ', '.join(ips) - - flavor = info.get('flavor', {}) - flavor_id = flavor.get('id') - if flavor_id: - info['flavor'] = self._find_flavor(flavor_id).name - image = info.get('image', {}) - image_id = image.get('id') - if image_id: - info['image'] = self._find_image(image_id).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_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.""" + if keyfile: try: - return self._find_resource(self.cs.flavors, flavor) - except exceptions.NotFound: - return self.cs.flavors.find(ram=flavor) + files['/root/.ssh/authorized_keys2'] = open(keyfile) + except IOError, e: + raise exceptions.CommandError("Can't open '%s': %s" % (keyfile, e)) - 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)) - - try: - uuid.UUID(name_or_id) - return manager.get(name_or_id) - except ValueError: - return manager.find(name=name_or_id) - except exceptions.NotFound: - raise CommandError("No %s with a name or ID of '%s' exists." % - (manager.resource_class.__name__.lower(), name_or_id)) + return (args.name, image, flavor, metadata, files) -# I'm picky about my shell help. -class OpenStackHelpFormatter(argparse.HelpFormatter): - def start_section(self, heading): - # Title-case the headings - heading = '%s%s' % (heading[0].upper(), heading[1:]) - super(OpenStackHelpFormatter, self).start_section(heading) +def do_flavor_list(cs, args): + """Print a list of available 'flavors' (sizes of servers).""" + flavors = cs.flavors.list() + _translate_flavor_keys(flavors) + utils.print_list(flavors, [ + 'ID', + 'Name', + 'Memory_MB', + 'Swap', + 'Local_GB', + 'VCPUs', + 'RXTX_Quota', + 'RXTX_Cap']) +def do_image_list(cs, args): + """Print a list of available images to boot from.""" + utils.print_list(cs.images.list(), ['ID', 'Name', 'Status']) -# Helpers -def print_list(objs, fields, formatters={}): - pt = prettytable.PrettyTable([f for f in fields], caching=False) - pt.aligns = ['l' for f in fields] +@utils.arg('image', metavar='', help='Name or ID of image.') +def do_image_delete(cs, args): + """ + Delete an image. - for o in objs: - row = [] - for field in fields: - if field in formatters: - row.append(formatters[field](o)) - else: - field_name = field.lower().replace(' ', '_') - data = getattr(o, field_name, '') - row.append(data) - pt.add_row(row) + It should go without saying, but you can only delete images you + created. + """ + image = _find_image(cs, args.image) + image.delete() - pt.printt(sortby=fields[0]) +@utils.arg('--fixed_ip', + dest='fixed_ip', + metavar='', + default=None, + help='Only match against fixed IP.') +@utils.arg('--reservation_id', + dest='reservation_id', + metavar='', + default=None, + help='Only return instances that match reservation_id.') +@utils.arg('--recurse_zones', + dest='recurse_zones', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0, + help='Recurse through all zones if set.') +@utils.arg('--ip', + dest='ip', + metavar='', + default=None, + help='Search with regular expression match by IP address') +@utils.arg('--ip6', + dest='ip6', + metavar='', + default=None, + help='Search with regular expression match by IPv6 address') +@utils.arg('--server_name', + dest='server_name', + metavar='', + default=None, + help='Search with regular expression match by server name') +@utils.arg('--name', + dest='display_name', + metavar='', + default=None, + help='Search with regular expression match by display name') +@utils.arg('--instance_name', + dest='name', + metavar='', + default=None, + help='Search with regular expression match by instance name') +def do_list(cs, args): + """List active servers.""" + recurse_zones = args.recurse_zones + search_opts = { + 'reservation_id': args.reservation_id, + 'fixed_ip': args.fixed_ip, + 'recurse_zones': recurse_zones, + 'ip': args.ip, + 'ip6': args.ip6, + 'name': args.name, + 'server_name': args.server_name, + 'display_name': args.display_name} + if recurse_zones: + to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP'] + else: + to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP'] + utils.print_list(cs.servers.list(search_opts=search_opts), + to_print) +@utils.arg('--hard', + dest='reboot_type', + action='store_const', + const=servers.REBOOT_HARD, + default=servers.REBOOT_SOFT, + help='Perform a hard reboot (instead of a soft one).') +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_reboot(cs, args): + """Reboot a server.""" + _find_server(cs, args.server).reboot(args.reboot_type) -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') +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('image', metavar='', help="Name or ID of new image.") +def do_rebuild(cs, args): + """Shutdown, re-image, and re-boot a server.""" + server = _find_server(cs, args.server) + image = _find_image(cs, args.image) + server.rebuild(image) +@utils.arg('server', metavar='', help='Name (old name) or ID of server.') +@utils.arg('name', metavar='', help='New name for the server.') +def do_rename(cs, args): + """Rename a server.""" + _find_server(cs, args.server).update(name=args.name) -def main(): +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('flavor', metavar='', help="Name or ID of new flavor.") +def do_resize(cs, args): + """Resize a server.""" + server = _find_server(cs, args.server) + flavor = _find_flavor(cs, args.flavor) + server.resize(flavor) + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_resize_confirm(cs, args): + """Confirm a previous resize.""" + _find_server(cs, args.server).confirm_resize() + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_resize_revert(cs, args): + """Revert a previous resize (and return to the previous VM).""" + _find_server(cs, args.server).revert_resize() + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_root_password(cs, args): + """ + Change the root password for a server. + """ + server = _find_server(cs, args.server) + p1 = getpass.getpass('New password: ') + p2 = getpass.getpass('Again: ') + if p1 != p2: + raise exceptions.CommandError("Passwords do not match.") + server.change_password(p1) + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('name', metavar='', help='Name of snapshot.') +def do_create_image(cs, args): + """Create a new image by taking a snapshot of a running server.""" + server = _find_server(cs, args.server) + cs.servers.create_image(server, args.name) + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_show(cs, args): + """Show details about the given server.""" + s = _find_server(cs, args.server) + + info = s._info.copy() + addresses = info.pop('addresses', []) + for addrtype in addresses: + ips = map(lambda x: x['addr'], addresses[addrtype]) + info['%s ip' % addrtype] = ', '.join(ips) + + flavor = info.get('flavor', {}) + flavor_id = flavor.get('id', '') + info['flavor'] = _find_flavor(cs, flavor_id).name + + image = info.get('image', {}) + image_id = image.get('id', '') + info['image'] = _find_image(cs, image_id).name + + utils.print_dict(info) + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_delete(cs, args): + """Immediately shut down and delete a server.""" + _find_server(cs, args.server).delete() + +def _find_server(cs, server): + """Get a server by name or ID.""" + return _find_resource(cs.servers, server) + +def _find_image(cs, image): + """Get an image by name or ID.""" + return _find_resource(cs.images, image) + +def _find_flavor(cs, flavor): + """Get a flavor by name, ID, or RAM size.""" try: - OpenStackShell().main(sys.argv[1:]) + return _find_resource(cs.flavors, flavor) + except exceptions.NotFound: + return cs.flavors.find(ram=flavor) + +def _find_resource(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)) + + try: + uuid.UUID(name_or_id) + return manager.get(name_or_id) + except ValueError: + return manager.find(name=name_or_id) + except exceptions.NotFound: + raise exceptions.CommandError("No %s with a name or ID of '%s' exists." % + (manager.resource_class.__name__.lower(), name_or_id)) - except Exception, e: - if httplib2.debuglevel == 1: - raise # dump stack. - else: - print >> sys.stderr, e - sys.exit(1) diff --git a/tests/fakes.py b/tests/fakes.py new file mode 100644 index 000000000..b63cc8087 --- /dev/null +++ b/tests/fakes.py @@ -0,0 +1,66 @@ +""" +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 raise AssertionError. I've indicated in comments the places where actual +behavior differs from the spec. +""" + +import novaclient.client + + +def assert_has_keys(dict, required=[], optional=[]): + keys = dict.keys() + for k in required: + assert k in keys + allowed_keys = set(required) | set(optional) + extra_keys = set(keys).difference(set(required + optional)) + if extra_keys: + raise AssertionError("found unexpected keys: %s" % list(extra_keys)) + + +class FakeClient(object): + + 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 self.client.callstack[-1][2] == body + + self.client.callstack = [] + + def assert_called_anytime(self, method, url, body=None): + """ + Assert than an API method was called anytime in the test. + """ + expected = (method, url) + + assert self.client.callstack, \ + "Expected %s %s but no calls were made." % expected + + found = False + for entry in self.client.callstack: + called = entry[0:2] + if expected == entry[0:2]: + found = True + break + + assert found, 'Expected %s %s; got %s' % \ + (expected, self.client.callstack) + if body is not None: + assert entry[2] == body + + self.client.callstack = [] + + def authenticate(self): + pass diff --git a/tests/test_base.py b/tests/test_base.py new file mode 100644 index 000000000..1dda3086f --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,60 @@ + +import mock + +from novaclient import base +from novaclient import exceptions +from novaclient.v1_0 import flavors +from tests.v1_0 import fakes +from tests import utils + + +cs = fakes.FakeClient() + + +class BaseTest(utils.TestCase): + + def test_resource_repr(self): + r = base.Resource(None, dict(foo="bar", baz="spam")) + self.assertEqual(repr(r), "") + + def test_getid(self): + self.assertEqual(base.getid(4), 4) + + class TmpObject(object): + id = 4 + self.assertEqual(base.getid(TmpObject), 4) + + def test_resource_lazy_getattr(self): + f = flavors.Flavor(cs.flavors, {'id': 1}) + self.assertEqual(f.name, '256 MB Server') + cs.assert_called('GET', '/flavors/1') + + # Missing stuff still fails after a second get + self.assertRaises(AttributeError, getattr, f, 'blahblah') + cs.assert_called('GET', '/flavors/1') + + def test_eq(self): + # Two resources of the same type with the same id: equal + r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) + r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) + self.assertEqual(r1, r2) + + # Two resoruces of different types: never equal + r1 = base.Resource(None, {'id': 1}) + r2 = flavors.Flavor(None, {'id': 1}) + self.assertNotEqual(r1, r2) + + # Two resources with no ID: equal if their info is equal + r1 = base.Resource(None, {'name': 'joe', 'age': 12}) + r2 = base.Resource(None, {'name': 'joe', 'age': 12}) + self.assertEqual(r1, r2) + + def test_findall_invalid_attribute(self): + # Make sure findall with an invalid attribute doesn't cause errors. + # The following should not raise an exception. + cs.flavors.findall(vegetable='carrot') + + # However, find() should raise an error + self.assertRaises(exceptions.NotFound, + cs.flavors.find, + vegetable='carrot') diff --git a/tests/test_http.py b/tests/test_http.py new file mode 100644 index 000000000..5e8e6ef77 --- /dev/null +++ b/tests/test_http.py @@ -0,0 +1,58 @@ + +import httplib2 +import mock + +from novaclient import client +from tests import utils + + +fake_response = httplib2.Response({"status": 200}) +fake_body = '{"hi": "there"}' +mock_request = mock.Mock(return_value=(fake_response, fake_body)) + + +def get_client(): + cl = client.HTTPClient("username", "apikey", + "project_id", "auth_test") + cl.management_url = "http://example.com" + cl.auth_token = "token" + return cl + + +class ClientTest(utils.TestCase): + + def test_get(self): + cl = get_client() + + @mock.patch.object(httplib2.Http, "request", mock_request) + @mock.patch('time.time', mock.Mock(return_value=1234)) + def test_get_call(): + resp, body = cl.get("/hi") + headers={"X-Auth-Token": "token", + "X-Auth-Project-Id": "project_id", + "User-Agent": cl.USER_AGENT, + } + mock_request.assert_called_with("http://example.com/hi?fresh=1234", + "GET", headers=headers) + # Automatic JSON parsing + self.assertEqual(body, {"hi": "there"}) + + test_get_call() + + + def test_post(self): + cl = get_client() + + @mock.patch.object(httplib2.Http, "request", mock_request) + def test_post_call(): + cl.post("/hi", body=[1, 2, 3]) + headers={ + "X-Auth-Token": "token", + "X-Auth-Project-Id": "project_id", + "Content-Type": "application/json", + "User-Agent": cl.USER_AGENT + } + mock_request.assert_called_with("http://example.com/hi", "POST", + headers=headers, body='[1, 2, 3]') + + test_post_call() diff --git a/tests/test_shell.py b/tests/test_shell.py new file mode 100644 index 000000000..869f2d822 --- /dev/null +++ b/tests/test_shell.py @@ -0,0 +1,39 @@ + +import os +import mock +import httplib2 + +from novaclient.shell import OpenStackComputeShell +from novaclient import exceptions +from tests import utils + + +class ShellTest(utils.TestCase): + + # Patch os.environ to avoid required auth info. + def setUp(self): + global _old_env + fake_env = { + 'NOVA_USERNAME': 'username', + 'NOVA_API_KEY': 'password', + 'NOVA_PROJECT_ID': 'project_id' + } + _old_env, os.environ = os.environ, fake_env.copy() + + # 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, assert_called_anytime + _shell = OpenStackComputeShell() + shell = lambda cmd: _shell.main(cmd.split()) + + def tearDown(self): + global _old_env + os.environ = _old_env + + def test_help_unknown_command(self): + self.assertRaises(exceptions.CommandError, shell, 'help foofoo') + + def test_debug(self): + httplib2.debuglevel = 0 + shell('--debug help') + assert httplib2.debuglevel == 1 diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 000000000..4f1ca3f1e --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,5 @@ +import unittest + + +class TestCase(unittest.TestCase): + pass diff --git a/tests/v1_0/fakes.py b/tests/v1_0/fakes.py index 2ee96e0ae..c47affc5e 100644 --- a/tests/v1_0/fakes.py +++ b/tests/v1_0/fakes.py @@ -1,82 +1,22 @@ -""" -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 urlparse -import urllib - import httplib2 +import urllib +import urlparse -from novaclient.v1_0 import Client -from novaclient.v1_0.client import HTTPClient - -from .utils import fail, assert_in, assert_not_in, assert_has_keys +from novaclient import client as base_client +from novaclient.v1_0 import client +from tests import fakes -def assert_equal(value_one, value_two): - assert value_one == value_two +class FakeClient(fakes.FakeClient, client.Client): + + def __init__(self, *args, **kwargs): + client.Client.__init__(self, 'username', 'apikey', + 'project_id', 'auth_url') + self.client = FakeHTTPClient(**kwargs) -class FakeClient(Client): - def __init__(self, username=None, password=None, project_id=None, - auth_url=None): - super(FakeClient, self).__init__('username', 'apikey', - 'project_id', 'auth_url') - self.client = FakeHTTPClient() - - 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 assert_called_anytime(self, method, url, body=None): - """ - Assert than an API method was called anytime in the test. - """ - expected = (method, url) - - assert self.client.callstack, \ - "Expected %s %s but no calls were made." % expected - - found = False - for entry in self.client.callstack: - called = entry[0:2] - if expected == entry[0:2]: - found = True - break - - assert found, 'Expected %s; got %s' % \ - (expected, self.client.callstack) - if body is not None: - assert_equal(entry[2], body) - - self.client.callstack = [] - - def authenticate(self): - pass - - -class FakeHTTPClient(HTTPClient): - def __init__(self): +class FakeHTTPClient(base_client.HTTPClient): + def __init__(self, **kwargs): self.username = 'username' self.apikey = 'apikey' self.auth_url = 'auth_url' @@ -85,15 +25,15 @@ class FakeHTTPClient(HTTPClient): def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly if method in ['GET', 'DELETE']: - assert_not_in('body', kwargs) + assert 'body' not in kwargs elif method in ['PUT', 'POST']: - assert_in('body', kwargs) + assert 'body' in 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)) + raise AssertionError('Called unknown API method: %s %s' % (method, url)) # Note the call self.callstack.append((method, url, kwargs.get('body', None))) @@ -210,14 +150,14 @@ class FakeHTTPClient(HTTPClient): ]}) def post_servers(self, body, **kw): - assert_equal(body.keys(), ['server']) - assert_has_keys(body['server'], - required=['name', 'imageId', 'flavorId'], - optional=['sharedIpGroupId', 'metadata', - 'personality', 'min_count', 'max_count']) + assert body.keys() == ['server'] + fakes.assert_has_keys(body['server'], + required=['name', 'imageId', 'flavorId'], + optional=['sharedIpGroupId', 'metadata', + 'personality', 'min_count', 'max_count']) if 'personality' in body['server']: for pfile in body['server']['personality']: - assert_has_keys(pfile, required=['path', 'contents']) + fakes.assert_has_keys(pfile, required=['path', 'contents']) return (202, self.get_servers_1234()[1]) def get_servers_1234(self, **kw): @@ -229,8 +169,8 @@ class FakeHTTPClient(HTTPClient): return (200, r) def put_servers_1234(self, body, **kw): - assert_equal(body.keys(), ['server']) - assert_has_keys(body['server'], optional=['name', 'adminPass']) + assert body.keys() == ['server'] + fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) return (204, None) def delete_servers_1234(self, **kw): @@ -253,8 +193,8 @@ class FakeHTTPClient(HTTPClient): 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', + assert body.keys() == ['shareIp'] + fakes.assert_has_keys(body['shareIp'], required=['sharedIpGroupId', 'configureServer']) return (202, None) @@ -266,29 +206,32 @@ class FakeHTTPClient(HTTPClient): # def post_servers_1234_action(self, body, **kw): - assert_equal(len(body.keys()), 1) + assert len(body.keys()) == 1 action = body.keys()[0] if action == 'reboot': - assert_equal(body[action].keys(), ['type']) - assert_in(body[action]['type'], ['HARD', 'SOFT']) + assert body[action].keys() == ['type'] + assert body[action]['type'] in ['HARD', 'SOFT'] elif action == 'rebuild': - assert_equal(body[action].keys(), ['imageId']) + assert body[action].keys() == ['imageId'] elif action == 'resize': - assert_equal(body[action].keys(), ['flavorId']) + assert body[action].keys() == ['flavorId'] + elif action == 'createBackup': + assert set(body[action].keys()) == \ + set(['name', 'rotation', 'backup_type']) elif action == 'confirmResize': - assert_equal(body[action], None) + assert body[action] is None # This one method returns a different response code return (204, None) elif action == 'revertResize': - assert_equal(body[action], None) + assert body[action] is None elif action == 'migrate': - assert_equal(body[action], None) + assert body[action] is None elif action == 'addFixedIp': - assert_equal(body[action].keys(), ['networkId']) + assert body[action].keys() == ['networkId'] elif action == 'removeFixedIp': - assert_equal(body[action].keys(), ['address']) + assert body[action].keys() == ['address'] else: - fail("Unexpected server action: %s" % action) + raise AssertionError("Unexpected server action: %s" % action) return (202, None) # @@ -349,8 +292,8 @@ class FakeHTTPClient(HTTPClient): 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', 'image_type', 'backup_type', 'rotation']) + assert body.keys() == ['image'] + fakes.assert_has_keys(body['image'], required=['serverId', 'name']) return (202, self.get_images_1()[1]) def delete_images_1(self, **kw): @@ -367,8 +310,8 @@ class FakeHTTPClient(HTTPClient): }}) def post_servers_1234_backup_schedule(self, body, **kw): - assert_equal(body.keys(), ['backupSchedule']) - assert_has_keys(body['backupSchedule'], required=['enabled'], + assert body.keys() == ['backupSchedule'] + fakes.assert_has_keys(body['backupSchedule'], required=['enabled'], optional=['weekly', 'daily']) return (204, None) @@ -395,8 +338,8 @@ class FakeHTTPClient(HTTPClient): 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'], + assert body.keys() == ['sharedIpGroup'] + fakes.assert_has_keys(body['sharedIpGroup'], required=['name'], optional=['server']) return (201, {'sharedIpGroup': { 'id': 10101, @@ -417,7 +360,6 @@ class FakeHTTPClient(HTTPClient): {'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', @@ -435,16 +377,16 @@ class FakeHTTPClient(HTTPClient): return (200, r) def post_zones(self, body, **kw): - assert_equal(body.keys(), ['zone']) - assert_has_keys(body['zone'], + assert body.keys() == ['zone'] + fakes.assert_has_keys(body['zone'], required=['api_url', 'username', 'password'], optional=['weight_offset', 'weight_scale']) return (202, self.get_zones_1()[1]) def put_zones_1(self, body, **kw): - assert_equal(body.keys(), ['zone']) - assert_has_keys(body['zone'], optional=['api_url', 'username', + assert body.keys() == ['zone'] + fakes.assert_has_keys(body['zone'], optional=['api_url', 'username', 'password', 'weight_offset', 'weight_scale']) @@ -457,12 +399,14 @@ class FakeHTTPClient(HTTPClient): # Accounts # def post_accounts_test_account_create_instance(self, body, **kw): - assert_equal(body.keys(), ['server']) - assert_has_keys(body['server'], + assert body.keys() == ['server'] + fakes.assert_has_keys(body['server'], required=['name', 'imageId', 'flavorId'], optional=['sharedIpGroupId', 'metadata', 'personality', 'min_count', 'max_count']) if 'personality' in body['server']: for pfile in body['server']['personality']: - assert_has_keys(pfile, required=['path', 'contents']) + fakes.assert_has_keys(pfile, required=['path', 'contents']) return (202, self.get_servers_1234()[1]) + + diff --git a/tests/v1_0/test_accounts.py b/tests/v1_0/test_accounts.py index 93eeed92f..72d77b17e 100644 --- a/tests/v1_0/test_accounts.py +++ b/tests/v1_0/test_accounts.py @@ -1,21 +1,25 @@ -from __future__ import absolute_import import StringIO -from .fakes import FakeClient +from tests.v1_0 import fakes +from tests import utils -os = FakeClient() -def test_instance_creation_for_account(): - s = os.accounts.create_instance_for( - account_id='test_account', - 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 - }) - os.assert_called('POST', '/accounts/test_account/create_instance') +cs = fakes.FakeClient() + + +class AccountsTest(utils.TestCase): + + def test_instance_creation_for_account(self): + s = cs.accounts.create_instance_for( + account_id='test_account', + 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', '/accounts/test_account/create_instance') diff --git a/tests/v1_0/test_auth.py b/tests/v1_0/test_auth.py index d27907cb4..f66336f2b 100644 --- a/tests/v1_0/test_auth.py +++ b/tests/v1_0/test_auth.py @@ -1,71 +1,74 @@ -import mock + import httplib2 -from nose.tools import assert_raises, assert_equal +import mock -import novaclient.v1_0 -from novaclient.v1_0 import exceptions +from novaclient.v1_0 import client +from novaclient import exceptions +from tests import utils -def test_authenticate_success(): - cs = novaclient.v1_0.Client("username", "apikey", "project_id") - 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)) +class AuthenticationTests(utils.TestCase): - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - mock_request.assert_called_with(cs.client.auth_url, 'GET', + def test_authenticate_success(self): + cs = client.Client("username", "apikey", "project_id") + management_url = 'https://servers.api.rackspacecloud.com/v1.0/443470' + auth_response = httplib2.Response({ + 'status': 204, + 'x-server-management-url': management_url, + 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1', + }) + mock_request = mock.Mock(return_value=(auth_response, None)) + + @mock.patch.object(httplib2.Http, "request", mock_request) + def test_auth_call(): + cs.client.authenticate() headers={ 'X-Auth-User': 'username', 'X-Auth-Key': 'apikey', 'X-Auth-Project-Id': 'project_id', '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']) + } + mock_request.assert_called_with(cs.client.auth_url, 'GET', + headers=headers) + self.assertEqual(cs.client.management_url, + auth_response['x-server-management-url']) + self.assertEqual(cs.client.auth_token, + auth_response['x-auth-token']) - test_auth_call() + test_auth_call() + def test_authenticate_failure(self): + cs = client.Client("username", "apikey", "project_id") + auth_response = httplib2.Response({'status': 401}) + mock_request = mock.Mock(return_value=(auth_response, None)) -def test_authenticate_failure(): - cs = novaclient.v1_0.Client("username", "apikey", "project_id") - auth_response = httplib2.Response({'status': 401}) - mock_request = mock.Mock(return_value=(auth_response, None)) + @mock.patch.object(httplib2.Http, "request", mock_request) + def test_auth_call(): + self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - assert_raises(exceptions.Unauthorized, cs.client.authenticate) + test_auth_call() - test_auth_call() + def test_auth_automatic(self): + cs = client.Client("username", "apikey", "project_id") + http_client = cs.client + http_client.management_url = '' + mock_request = mock.Mock(return_value=(None, None)) + @mock.patch.object(http_client, 'request', mock_request) + @mock.patch.object(http_client, 'authenticate') + def test_auth_call(m): + http_client.get('/') + m.assert_called() + mock_request.assert_called() -def test_auth_automatic(): - client = novaclient.v1_0.Client("username", "apikey", "project_id").client - client.management_url = '' - mock_request = mock.Mock(return_value=(None, None)) + test_auth_call() - @mock.patch.object(client, 'request', mock_request) - @mock.patch.object(client, 'authenticate') - def test_auth_call(m): - client.get('/') - m.assert_called() - mock_request.assert_called() + def test_auth_manual(self): + cs = client.Client("username", "apikey", "project_id") - test_auth_call() + @mock.patch.object(cs.client, 'authenticate') + def test_auth_call(m): + cs.authenticate() + m.assert_called() - -def test_auth_manual(): - cs = novaclient.v1_0.Client("username", "apikey", "project_id") - - @mock.patch.object(cs.client, 'authenticate') - def test_auth_call(m): - cs.authenticate() - m.assert_called() - - test_auth_call() + test_auth_call() diff --git a/tests/v1_0/test_backup_schedules.py b/tests/v1_0/test_backup_schedules.py index 831f81067..ebf5dad40 100644 --- a/tests/v1_0/test_backup_schedules.py +++ b/tests/v1_0/test_backup_schedules.py @@ -1,60 +1,60 @@ -from __future__ import absolute_import from novaclient.v1_0 import backup_schedules - -from .fakes import FakeClient -from .utils import assert_isinstance - -os = FakeClient() +from tests.v1_0 import fakes +from tests import utils -def test_get_backup_schedule(): - s = os.servers.get(1234) - - # access via manager - b = os.backup_schedules.get(server=s) - assert_isinstance(b, backup_schedules.BackupSchedule) - os.assert_called('GET', '/servers/1234/backup_schedule') - - b = os.backup_schedules.get(server=1234) - assert_isinstance(b, backup_schedules.BackupSchedule) - os.assert_called('GET', '/servers/1234/backup_schedule') - - # access via instance - assert_isinstance(s.backup_schedule, backup_schedules.BackupSchedule) - os.assert_called('GET', '/servers/1234/backup_schedule') - - # Just for coverage's sake - b = s.backup_schedule.get() - os.assert_called('GET', '/servers/1234/backup_schedule') +cs = fakes.FakeClient() -def test_create_update_backup_schedule(): - s = os.servers.get(1234) +class BackupSchedulesTest(utils.TestCase): - # create/update via manager - os.backup_schedules.update( - server=s, - enabled=True, - weekly=backup_schedules.BACKUP_WEEKLY_THURSDAY, - daily=backup_schedules.BACKUP_DAILY_H_1000_1200 - ) - os.assert_called('POST', '/servers/1234/backup_schedule') + def test_get_backup_schedule(self): + s = cs.servers.get(1234) - # and via instance - s.backup_schedule.update(enabled=False) - os.assert_called('POST', '/servers/1234/backup_schedule') + # access via manager + b = cs.backup_schedules.get(server=s) + self.assertTrue(isinstance(b, backup_schedules.BackupSchedule)) + cs.assert_called('GET', '/servers/1234/backup_schedule') + b = cs.backup_schedules.get(server=1234) + self.assertTrue(isinstance(b, backup_schedules.BackupSchedule)) + cs.assert_called('GET', '/servers/1234/backup_schedule') -def test_delete_backup_schedule(): - s = os.servers.get(1234) + # access via instance + self.assertTrue(isinstance(s.backup_schedule, + backup_schedules.BackupSchedule)) + cs.assert_called('GET', '/servers/1234/backup_schedule') - # delete via manager - os.backup_schedules.delete(s) - os.assert_called('DELETE', '/servers/1234/backup_schedule') - os.backup_schedules.delete(1234) - os.assert_called('DELETE', '/servers/1234/backup_schedule') + # Just for coverage's sake + b = s.backup_schedule.get() + cs.assert_called('GET', '/servers/1234/backup_schedule') - # and via instance - s.backup_schedule.delete() - os.assert_called('DELETE', '/servers/1234/backup_schedule') + def test_create_update_backup_schedule(self): + s = cs.servers.get(1234) + + # create/update via manager + cs.backup_schedules.update( + server=s, + enabled=True, + weekly=backup_schedules.BACKUP_WEEKLY_THURSDAY, + daily=backup_schedules.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(self): + 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') diff --git a/tests/v1_0/test_base.py b/tests/v1_0/test_base.py deleted file mode 100644 index db440bbe6..000000000 --- a/tests/v1_0/test_base.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import absolute_import - -import mock -from nose.tools import assert_equal, assert_not_equal, assert_raises - -from novaclient.v1_0 import flavors -from novaclient.v1_0 import exceptions -from novaclient.v1_0 import base - -from .fakes import FakeClient - -os = FakeClient() - - -def test_resource_repr(): - r = base.Resource(None, dict(foo="bar", baz="spam")) - assert_equal(repr(r), "") - - -def test_getid(): - assert_equal(base.getid(4), 4) - - class O(object): - id = 4 - assert_equal(base.getid(O), 4) - - -def test_resource_lazy_getattr(): - f = flavors.Flavor(os.flavors, {'id': 1}) - assert_equal(f.name, '256 MB Server') - os.assert_called('GET', '/flavors/1') - - # Missing stuff still fails after a second get - assert_raises(AttributeError, getattr, f, 'blahblah') - os.assert_called('GET', '/flavors/1') - - -def test_eq(): - # Two resources of the same type with the same id: equal - r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) - r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) - assert_equal(r1, r2) - - # Two resoruces of different types: never equal - r1 = base.Resource(None, {'id': 1}) - r2 = flavors.Flavor(None, {'id': 1}) - assert_not_equal(r1, r2) - - # Two resources with no ID: equal if their info is equal - r1 = base.Resource(None, {'name': 'joe', 'age': 12}) - r2 = base.Resource(None, {'name': 'joe', 'age': 12}) - assert_equal(r1, r2) - - -def test_findall_invalid_attribute(): - # Make sure findall with an invalid attribute doesn't cause errors. - # The following should not raise an exception. - os.flavors.findall(vegetable='carrot') - - # However, find() should raise an error - assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot') diff --git a/tests/v1_0/test_client.py b/tests/v1_0/test_client.py deleted file mode 100644 index e7a63b7d7..000000000 --- a/tests/v1_0/test_client.py +++ /dev/null @@ -1,52 +0,0 @@ -import mock -import httplib2 - -from novaclient.v1_0 import client -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 get_client(): - cl = client.HTTPClient("username", "apikey", "project_id", "auth_test") - cl.management_url = "http://example.com" - cl.auth_token = "token" - return cl - - -def test_get(): - cl = get_client() - - @mock.patch.object(httplib2.Http, "request", mock_request) - @mock.patch('time.time', mock.Mock(return_value=1234)) - def test_get_call(): - resp, body = cl.get("/hi") - mock_request.assert_called_with("http://example.com/hi?fresh=1234", - "GET", - headers={"X-Auth-Token": "token", - "X-Auth-Project-Id": "project_id", - "User-Agent": cl.USER_AGENT}) - # Automatic JSON parsing - assert_equal(body, {"hi": "there"}) - - test_get_call() - - -def test_post(): - cl = get_client() - - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_post_call(): - cl.post("/hi", body=[1, 2, 3]) - mock_request.assert_called_with("http://example.com/hi", "POST", - headers={ - "X-Auth-Token": "token", - "X-Auth-Project-Id": "project_id", - "Content-Type": "application/json", - "User-Agent": cl.USER_AGENT}, - body='[1, 2, 3]' - ) - - test_post_call() diff --git a/tests/v1_0/test_flavors.py b/tests/v1_0/test_flavors.py index 3c9f644c3..30cf84c47 100644 --- a/tests/v1_0/test_flavors.py +++ b/tests/v1_0/test_flavors.py @@ -1,42 +1,38 @@ -from __future__ import absolute_import - -from nose.tools import assert_raises, assert_equal +from novaclient import exceptions from novaclient.v1_0 import flavors -from novaclient.v1_0 import exceptions - -from .fakes import FakeClient -from .utils import assert_isinstance - -os = FakeClient() +from tests.v1_0 import fakes +from tests import utils -def test_list_flavors(): - fl = os.flavors.list() - os.assert_called('GET', '/flavors/detail') - [assert_isinstance(f, flavors.Flavor) for f in fl] +cs = fakes.FakeClient() -def test_list_flavors_undetailed(): - fl = os.flavors.list(detailed=False) - os.assert_called('GET', '/flavors') - [assert_isinstance(f, flavors.Flavor) for f in fl] +class FlavorsTest(utils.TestCase): + def test_list_flavors(self): + fl = cs.flavors.list() + cs.assert_called('GET', '/flavors/detail') + [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl] -def test_get_flavor_details(): - f = os.flavors.get(1) - os.assert_called('GET', '/flavors/1') - assert_isinstance(f, flavors.Flavor) - assert_equal(f.ram, 256) - assert_equal(f.disk, 10) + def test_list_flavors_undetailed(self): + fl = cs.flavors.list(detailed=False) + cs.assert_called('GET', '/flavors') + [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl] + def test_get_flavor_details(self): + f = cs.flavors.get(1) + cs.assert_called('GET', '/flavors/1') + self.assertTrue(isinstance(f, flavors.Flavor)) + self.assertEqual(f.ram, 256) + self.assertEqual(f.disk, 10) -def test_find(): - f = os.flavors.find(ram=256) - os.assert_called('GET', '/flavors/detail') - assert_equal(f.name, '256 MB Server') + def test_find(self): + f = cs.flavors.find(ram=256) + cs.assert_called('GET', '/flavors/detail') + self.assertEqual(f.name, '256 MB Server') - f = os.flavors.find(disk=20) - assert_equal(f.name, '512 MB Server') + f = cs.flavors.find(disk=20) + self.assertEqual(f.name, '512 MB Server') - assert_raises(exceptions.NotFound, os.flavors.find, disk=12345) + self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345) diff --git a/tests/v1_0/test_images.py b/tests/v1_0/test_images.py index 9e9bb4732..41c1399f7 100644 --- a/tests/v1_0/test_images.py +++ b/tests/v1_0/test_images.py @@ -1,51 +1,45 @@ -from __future__ import absolute_import - -from nose.tools import assert_equal from novaclient.v1_0 import images - -from .fakes import FakeClient -from .utils import assert_isinstance - -os = FakeClient() +from tests.v1_0 import fakes +from tests import utils -def test_list_images(): - il = os.images.list() - os.assert_called('GET', '/images/detail') - [assert_isinstance(i, images.Image) for i in il] +cs = fakes.FakeClient() -def test_list_images_undetailed(): - il = os.images.list(detailed=False) - os.assert_called('GET', '/images') - [assert_isinstance(i, images.Image) for i in il] +class ImagesTest(utils.TestCase): + def test_list_images(self): + il = cs.images.list() + cs.assert_called('GET', '/images/detail') + [self.assertTrue(isinstance(i, images.Image)) for i in il] -def test_get_image_details(): - i = os.images.get(1) - os.assert_called('GET', '/images/1') - assert_isinstance(i, images.Image) - assert_equal(i.id, 1) - assert_equal(i.name, 'CentOS 5.2') + def test_list_images_undetailed(self): + il = cs.images.list(detailed=False) + cs.assert_called('GET', '/images') + [self.assertTrue(isinstance(i, images.Image)) for i in il] + def test_get_image_details(self): + i = cs.images.get(1) + cs.assert_called('GET', '/images/1') + self.assertTrue(isinstance(i, images.Image)) + self.assertEqual(i.id, 1) + self.assertEqual(i.name, 'CentOS 5.2') -def test_create_image(): - i = os.images.create(server=1234, name="Just in case") - os.assert_called('POST', '/images') - assert_isinstance(i, images.Image) + def test_create_image(self): + i = cs.images.create(server=1234, name="Just in case") + cs.assert_called('POST', '/images') + self.assertTrue(isinstance(i, images.Image)) + def test_delete_image(self): + cs.images.delete(1) + cs.assert_called('DELETE', '/images/1') -def test_delete_image(): - os.images.delete(1) - os.assert_called('DELETE', '/images/1') + def test_find(self): + i = cs.images.find(name="CentOS 5.2") + self.assertEqual(i.id, 1) + cs.assert_called('GET', '/images/detail') - -def test_find(): - i = os.images.find(name="CentOS 5.2") - assert_equal(i.id, 1) - os.assert_called('GET', '/images/detail') - - iml = os.images.findall(status='SAVING') - assert_equal(len(iml), 1) - assert_equal(iml[0].name, 'My Server Backup') + iml = cs.images.findall(status='SAVING') + self.assertEqual(len(iml), 1) + self.assertEqual(iml[0].name, 'My Server Backup') diff --git a/tests/v1_0/test_ipgroups.py b/tests/v1_0/test_ipgroups.py index 3c89d4c68..3ab308b44 100644 --- a/tests/v1_0/test_ipgroups.py +++ b/tests/v1_0/test_ipgroups.py @@ -1,52 +1,48 @@ -from __future__ import absolute_import - -from nose.tools import assert_equal from novaclient.v1_0 import ipgroups - -from .fakes import FakeClient -from .utils import assert_isinstance - -os = FakeClient() +from tests.v1_0 import fakes +from tests import utils -def test_list_ipgroups(): - ipl = os.ipgroups.list() - os.assert_called('GET', '/shared_ip_groups/detail') - [assert_isinstance(ipg, ipgroups.IPGroup) for ipg in ipl] +cs = fakes.FakeClient() -def test_list_ipgroups_undetailed(): - ipl = os.ipgroups.list(detailed=False) - os.assert_called('GET', '/shared_ip_groups') - [assert_isinstance(ipg, ipgroups.IPGroup) for ipg in ipl] +class IPGroupTest(utils.TestCase): + def test_list_ipgroups(self): + ipl = cs.ipgroups.list() + cs.assert_called('GET', '/shared_ip_groups/detail') + [self.assertTrue(isinstance(ipg, ipgroups.IPGroup)) \ + for ipg in ipl] -def test_get_ipgroup(): - ipg = os.ipgroups.get(1) - os.assert_called('GET', '/shared_ip_groups/1') - assert_isinstance(ipg, ipgroups.IPGroup) + def test_list_ipgroups_undetailed(self): + ipl = cs.ipgroups.list(detailed=False) + cs.assert_called('GET', '/shared_ip_groups') + [self.assertTrue(isinstance(ipg, ipgroups.IPGroup)) \ + for ipg in ipl] + def test_get_ipgroup(self): + ipg = cs.ipgroups.get(1) + cs.assert_called('GET', '/shared_ip_groups/1') + self.assertTrue(isinstance(ipg, ipgroups.IPGroup)) -def test_create_ipgroup(): - ipg = os.ipgroups.create("My group", 1234) - os.assert_called('POST', '/shared_ip_groups') - assert_isinstance(ipg, ipgroups.IPGroup) + def test_create_ipgroup(self): + ipg = cs.ipgroups.create("My group", 1234) + cs.assert_called('POST', '/shared_ip_groups') + self.assertTrue(isinstance(ipg, ipgroups.IPGroup)) + def test_delete_ipgroup(self): + 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_delete_ipgroup(): - ipg = os.ipgroups.get(1) - ipg.delete() - os.assert_called('DELETE', '/shared_ip_groups/1') - os.ipgroups.delete(ipg) - os.assert_called('DELETE', '/shared_ip_groups/1') - os.ipgroups.delete(1) - os.assert_called('DELETE', '/shared_ip_groups/1') - - -def test_find(): - ipg = os.ipgroups.find(name='group1') - os.assert_called('GET', '/shared_ip_groups/detail') - assert_equal(ipg.name, 'group1') - ipgl = os.ipgroups.findall(id=1) - assert_equal(ipgl, [ipgroups.IPGroup(None, {'id': 1})]) + def test_find(self): + ipg = cs.ipgroups.find(name='group1') + cs.assert_called('GET', '/shared_ip_groups/detail') + self.assertEqual(ipg.name, 'group1') + ipgl = cs.ipgroups.findall(id=1) + self.assertEqual(ipgl, [ipgroups.IPGroup(None, {'id': 1})]) diff --git a/tests/v1_0/test_servers.py b/tests/v1_0/test_servers.py index e32b3d61b..fcb3e9068 100644 --- a/tests/v1_0/test_servers.py +++ b/tests/v1_0/test_servers.py @@ -1,180 +1,169 @@ -from __future__ import absolute_import import StringIO -from nose.tools import assert_equal - from novaclient.v1_0 import servers - -from .fakes import FakeClient -from .utils import assert_isinstance +from tests.v1_0 import fakes +from tests import utils -os = FakeClient() +cs = fakes.FakeClient() -def test_list_servers(): - sl = os.servers.list() - os.assert_called('GET', '/servers/detail') - [assert_isinstance(s, servers.Server) for s in sl] +class ServersTest(utils.TestCase): + def test_list_servers(self): + sl = cs.servers.list() + cs.assert_called('GET', '/servers/detail') + [self.assertTrue(isinstance(s, servers.Server)) for s in sl] -def test_list_servers_undetailed(): - sl = os.servers.list(detailed=False) - os.assert_called('GET', '/servers') - [assert_isinstance(s, servers.Server) for s in sl] + def test_list_servers_undetailed(self): + sl = cs.servers.list(detailed=False) + cs.assert_called('GET', '/servers') + [self.assertTrue(isinstance(s, servers.Server)) for s in sl] + def test_get_server_details(self): + s = cs.servers.get(1234) + cs.assert_called('GET', '/servers/1234') + self.assertTrue(isinstance(s, servers.Server)) + self.assertEqual(s.id, 1234) + self.assertEqual(s.status, 'BUILD') -def test_get_server_details(): - s = os.servers.get(1234) - os.assert_called('GET', '/servers/1234') - assert_isinstance(s, servers.Server) - assert_equal(s.id, 1234) - assert_equal(s.status, 'BUILD') + def test_create_server(self): + 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') + self.assertTrue(isinstance(s, servers.Server)) + def test_update_server(self): + s = cs.servers.get(1234) -def test_create_server(): - s = os.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 - } - ) - os.assert_called('POST', '/servers') - assert_isinstance(s, servers.Server) + # 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() -def test_update_server(): - s = os.servers.get(1234) + # 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') - # Update via instance - s.update(name='hi') - os.assert_called('PUT', '/servers/1234') - s.update(name='hi', password='there') - os.assert_called('PUT', '/servers/1234') + def test_delete_server(self): + 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') - # Silly, but not an error - s.update() + def test_find(self): + s = cs.servers.find(name='sample-server') + cs.assert_called('GET', '/servers/detail') + self.assertEqual(s.name, 'sample-server') - # Update via manager - os.servers.update(s, name='hi') - os.assert_called('PUT', '/servers/1234') - os.servers.update(1234, password='there') - os.assert_called('PUT', '/servers/1234') - os.servers.update(s, name='hi', password='there') - os.assert_called('PUT', '/servers/1234') + # Find with multiple results arbitraility returns the first item + s = cs.servers.find(flavorId=1) + sl = cs.servers.findall(flavorId=1) + self.assertEqual(sl[0], s) + self.assertEqual([s.id for s in sl], [1234, 5678]) + def test_share_ip(self): + s = cs.servers.get(1234) -def test_delete_server(): - s = os.servers.get(1234) - s.delete() - os.assert_called('DELETE', '/servers/1234') - os.servers.delete(1234) - os.assert_called('DELETE', '/servers/1234') - os.servers.delete(s) - os.assert_called('DELETE', '/servers/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_find(): - s = os.servers.find(name='sample-server') - os.assert_called('GET', '/servers/detail') - assert_equal(s.name, 'sample-server') + def test_unshare_ip(self): + s = cs.servers.get(1234) - # Find with multiple results arbitraility returns the first item - s = os.servers.find(flavorId=1) - sl = os.servers.findall(flavorId=1) - assert_equal(sl[0], s) - assert_equal([s.id for s in sl], [1234, 5678]) + # 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_share_ip(): - s = os.servers.get(1234) + def test_reboot_server(self): + 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') - # Share via instance - s.share_ip(ipgroup=1, address='1.2.3.4') - os.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4') + def test_rebuild_server(self): + 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') - # Share via manager - os.servers.share_ip(s, ipgroup=1, address='1.2.3.4', configure=False) - os.assert_called('PUT', '/servers/1234/ips/public/1.2.3.4') + def test_resize_server(self): + 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(self): + 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_unshare_ip(): - s = os.servers.get(1234) + def test_revert_resized_server(self): + 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') - # Unshare via instance - s.unshare_ip('1.2.3.4') - os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4') + def test_backup_server(self): + 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') - # Unshare via manager - os.servers.unshare_ip(s, '1.2.3.4') - os.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4') + def test_migrate_server(self): + s = cs.servers.get(1234) + s.migrate() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.migrate(s) + cs.assert_called('POST', '/servers/1234/action') + def test_add_fixed_ip(self): + s = cs.servers.get(1234) + s.add_fixed_ip(1) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.add_fixed_ip(s, 1) + cs.assert_called('POST', '/servers/1234/action') -def test_reboot_server(): - s = os.servers.get(1234) - s.reboot() - os.assert_called('POST', '/servers/1234/action') - os.servers.reboot(s, type='HARD') - os.assert_called('POST', '/servers/1234/action') - - -def test_rebuild_server(): - s = os.servers.get(1234) - s.rebuild(image=1) - os.assert_called('POST', '/servers/1234/action') - os.servers.rebuild(s, image=1) - os.assert_called('POST', '/servers/1234/action') - - -def test_resize_server(): - s = os.servers.get(1234) - s.resize(flavor=1) - os.assert_called('POST', '/servers/1234/action') - os.servers.resize(s, flavor=1) - os.assert_called('POST', '/servers/1234/action') - - -def test_confirm_resized_server(): - s = os.servers.get(1234) - s.confirm_resize() - os.assert_called('POST', '/servers/1234/action') - os.servers.confirm_resize(s) - os.assert_called('POST', '/servers/1234/action') - - -def test_revert_resized_server(): - s = os.servers.get(1234) - s.revert_resize() - os.assert_called('POST', '/servers/1234/action') - os.servers.revert_resize(s) - os.assert_called('POST', '/servers/1234/action') - - -def test_migrate_server(): - s = os.servers.get(1234) - s.migrate() - os.assert_called('POST', '/servers/1234/action') - os.servers.migrate(s) - os.assert_called('POST', '/servers/1234/action') - - -def test_add_fixed_ip(): - s = os.servers.get(1234) - s.add_fixed_ip(1) - os.assert_called('POST', '/servers/1234/action') - os.servers.add_fixed_ip(s, 1) - os.assert_called('POST', '/servers/1234/action') - - -def test_remove_fixed_ip(): - s = os.servers.get(1234) - s.remove_fixed_ip('10.0.0.1') - os.assert_called('POST', '/servers/1234/action') - os.servers.remove_fixed_ip(s, '10.0.0.1') - os.assert_called('POST', '/servers/1234/action') + def test_remove_fixed_ip(self): + s = cs.servers.get(1234) + s.remove_fixed_ip('10.0.0.1') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.remove_fixed_ip(s, '10.0.0.1') + cs.assert_called('POST', '/servers/1234/action') diff --git a/tests/v1_0/test_shell.py b/tests/v1_0/test_shell.py index bc607292e..ebde8c8ac 100644 --- a/tests/v1_0/test_shell.py +++ b/tests/v1_0/test_shell.py @@ -1,374 +1,319 @@ -from __future__ import absolute_import import os import mock -import httplib2 -from nose.tools import assert_raises, assert_equal - -from novaclient.v1_0.shell import OpenStackShell, CommandError - -from .fakes import FakeClient -from .utils import assert_in +from novaclient.shell import OpenStackComputeShell +from novaclient import exceptions +from tests.v1_0 import fakes +from tests import utils -# Patch os.environ to avoid required auth info. -def setup(): - global _old_env - fake_env = { - 'NOVA_USERNAME': 'username', - 'NOVA_API_KEY': 'password', - 'NOVA_PROJECT_ID': 'project_id' - } - _old_env, os.environ = os.environ, fake_env.copy() +class ShellTest(utils.TestCase): - # 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, assert_called_anytime - _shell = OpenStackShell() - _shell._api_class = FakeClient - assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b) - assert_called_anytime = lambda m, u, b=None: \ - _shell.cs.assert_called_anytime(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', - 'min_count': 1, 'max_count': 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', - 'min_count': 1, 'max_count': 1, - 'metadata': {'foo': 'bar', 'spam': 'eggs'}}} - ) - - -def test_boot_files(): - testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - expected_file_data = open(testfile).read().encode('base64') - - shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' % - (testfile, testfile)) - - assert_called( - 'POST', '/servers', - {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', - 'min_count': 1, 'max_count': 1, - 'personality': [ - {'path': '/tmp/bar', 'contents': expected_file_data}, - {'path': '/tmp/foo', 'contents': expected_file_data} - ]} + # Patch os.environ to avoid required auth info. + def setUp(self): + global _old_env + fake_env = { + 'NOVA_USERNAME': 'username', + 'NOVA_API_KEY': 'password', + 'NOVA_PROJECT_ID': 'project_id' } - ) + _old_env, os.environ = os.environ, fake_env.copy() + # 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, assert_called_anytime + _shell = OpenStackComputeShell() + _shell._api_class = fakes.FakeClient + assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b) + assert_called_anytime = lambda m, u, b=None: \ + _shell.cs.assert_called_anytime(m, u, b) -def test_boot_invalid_file(): - invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') - assert_raises(CommandError, shell, 'boot some-server --image 1 ' - '--file /foo=%s' % invalid_file) + def shell(cmd): + command = ['--version=1.0',] + command.extend(cmd.split()) + _shell.main(command) + def tearDown(self): + global _old_env + os.environ = _old_env -def test_boot_key_auto(): - mock_exists = mock.Mock(return_value=True) - mock_open = mock.Mock() - mock_open.return_value = mock.Mock() - mock_open.return_value.read = mock.Mock(return_value='SSHKEY') + def test_backup_schedule(self): + shell('backup-schedule 1234') + assert_called('GET', '/servers/1234/backup_schedule') - @mock.patch('os.path.exists', mock_exists) - @mock.patch('__builtin__.open', mock_open) - def test_shell_call(): - shell('boot some-server --image 1 --key') + 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(self): + shell('backup-schedule-delete 1234') + assert_called('DELETE', '/servers/1234/backup_schedule') + + def test_boot(self): + shell('boot --image 1 some-server') + assert_called( + 'POST', '/servers', + {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', + 'min_count': 1, 'max_count': 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', 'min_count': 1, 'max_count': 1, - 'personality': [{ - 'path': '/root/.ssh/authorized_keys2', - 'contents': ('SSHKEY').encode('base64')}, + 'metadata': {'foo': 'bar', 'spam': 'eggs'}}} + ) + + def test_boot_files(self): + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + expected_file_data = open(testfile).read().encode('base64') + + shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' % + (testfile, testfile)) + + assert_called( + 'POST', '/servers', + {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', + 'min_count': 1, 'max_count': 1, + 'personality': [ + {'path': '/tmp/bar', 'contents': expected_file_data}, + {'path': '/tmp/foo', 'contents': expected_file_data} ]} } ) - test_shell_call() - - -def test_boot_key_auto_no_keys(): - mock_exists = mock.Mock(return_value=False) - - @mock.patch('os.path.exists', mock_exists) - def test_shell_call(): - assert_raises(CommandError, shell, 'boot some-server --image 1 --key') - - test_shell_call() - - -def test_boot_key_file(): - testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - expected_file_data = open(testfile).read().encode('base64') - shell('boot some-server --image 1 --key %s' % testfile) - assert_called( - 'POST', '/servers', - {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', - 'min_count': 1, 'max_count': 1, - 'personality': [ - {'path': '/root/.ssh/authorized_keys2', 'contents': - expected_file_data}, - ]} - } - ) - - -def test_boot_invalid_keyfile(): - invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') - assert_raises(CommandError, shell, 'boot some-server ' - '--image 1 --key %s' % invalid_file) - - -def test_boot_ipgroup(): - shell('boot --image 1 --ipgroup 1 some-server') - assert_called( - 'POST', '/servers', - {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', - 'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}} - ) - - -def test_boot_ipgroup_name(): - shell('boot --image 1 --ipgroup group1 some-server') - assert_called( - 'POST', '/servers', - {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', - 'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}} - ) - - -def test_flavor_list(): - shell('flavor-list') - assert_called_anytime('GET', '/flavors/detail') - - -def test_image_list(): - shell('image-list') - assert_called('GET', '/images/detail') - - -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}} - ) - - -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_in(('GET', '/shared_ip_groups/detail', None), - _shell.cs.client.callstack) - assert_called('GET', '/servers/5678') - - -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_zone(): - shell('zone 1') - assert_called('GET', '/zones/1') - - shell('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx') - assert_called( - 'PUT', '/zones/1', - {'zone': {'api_url': 'http://zzz', 'username': 'frank', - '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', - 'password': 'xxx', - 'weight_offset': '0.0', 'weight_scale': '1.0'}} - ) - -def test_zone_delete(): - shell('zone-delete 1') - assert_called('DELETE', '/zones/1') - - -def test_zone_list(): - shell('zone-list') - assert_in(('GET', '/zones/detail', None), - _shell.cs.client.callstack) - - -def test_help(): - @mock.patch.object(_shell.parser, 'print_help') - def test_help(m): - shell('help') - m.assert_called() - - @mock.patch.object(_shell.subcommands['delete'], 'print_help') - def test_help_delete(m): - shell('help delete') - m.assert_called() - - test_help() - test_help_delete() - - assert_raises(CommandError, shell, 'help foofoo') - - -def test_debug(): - httplib2.debuglevel = 0 - shell('--debug list') - assert httplib2.debuglevel == 1 + def test_boot_invalid_file(self): + invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') + self.assertRaises(exceptions.CommandError, shell, 'boot some-server --image 1 ' + '--file /foo=%s' % invalid_file) + + def test_boot_key_auto(self): + mock_exists = mock.Mock(return_value=True) + mock_open = mock.Mock() + mock_open.return_value = mock.Mock() + mock_open.return_value.read = mock.Mock(return_value='SSHKEY') + + @mock.patch('os.path.exists', mock_exists) + @mock.patch('__builtin__.open', mock_open) + def test_shell_call(): + shell('boot some-server --image 1 --key') + assert_called( + 'POST', '/servers', + {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', + 'min_count': 1, 'max_count': 1, + 'personality': [{ + 'path': '/root/.ssh/authorized_keys2', + 'contents': ('SSHKEY').encode('base64')}, + ]} + } + ) + + test_shell_call() + + def test_boot_key_auto_no_keys(self): + mock_exists = mock.Mock(return_value=False) + + @mock.patch('os.path.exists', mock_exists) + def test_shell_call(): + self.assertRaises(exceptions.CommandError, shell, + 'boot some-server --image 1 --key') + + test_shell_call() + + def test_boot_key_file(self): + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + expected_file_data = open(testfile).read().encode('base64') + shell('boot some-server --image 1 --key %s' % testfile) + assert_called( + 'POST', '/servers', + {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', + 'min_count': 1, 'max_count': 1, + 'personality': [ + {'path': '/root/.ssh/authorized_keys2', 'contents': + expected_file_data}, + ]} + } + ) + + def test_boot_invalid_keyfile(self): + invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') + self.assertRaises(exceptions.CommandError, shell, 'boot some-server ' + '--image 1 --key %s' % invalid_file) + + def test_boot_ipgroup(self): + shell('boot --image 1 --ipgroup 1 some-server') + assert_called( + 'POST', '/servers', + {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', + 'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}} + ) + + def test_boot_ipgroup_name(self): + shell('boot --image 1 --ipgroup group1 some-server') + assert_called( + 'POST', '/servers', + {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', + 'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}} + ) + + def test_flavor_list(self): + shell('flavor-list') + assert_called_anytime('GET', '/flavors/detail') + + def test_image_list(self): + shell('image-list') + assert_called('GET', '/images/detail') + + def test_snapshot_create(self): + shell('image-create sample-server mysnapshot') + assert_called( + 'POST', '/images', + {'image': {'name': 'mysnapshot', 'serverId': 1234}} + ) + + def test_image_delete(self): + shell('image-delete 1') + assert_called('DELETE', '/images/1') + + def test_ip_share(self): + 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(self): + shell('ip-unshare sample-server 1.2.3.4') + assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4') + + def test_ipgroup_list(self): + shell('ipgroup-list') + assert ('GET', '/shared_ip_groups/detail', None) in \ + _shell.cs.client.callstack + assert_called('GET', '/servers/5678') + + def test_ipgroup_show(self): + 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(self): + 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(self): + shell('ipgroup-delete group1') + assert_called('DELETE', '/shared_ip_groups/1') + + def test_list(self): + shell('list') + assert_called('GET', '/servers/detail') + + def test_reboot(self): + 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(self): + shell('rebuild sample-server 1') + assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}}) + + def test_rename(self): + shell('rename sample-server newname') + assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}}) + + def test_resize(self): + shell('resize sample-server 1') + assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}}) + + def test_resize_confirm(self): + shell('resize-confirm sample-server') + assert_called('POST', '/servers/1234/action', {'confirmResize': None}) + + def test_resize_revert(self): + shell('resize-revert sample-server') + assert_called('POST', '/servers/1234/action', {'revertResize': None}) + + def test_backup(self): + 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(self): + shell('root-password sample-server') + assert_called('PUT', '/servers/1234', {'server': {'adminPass': 'p'}}) + + def test_show(self): + shell('show 1234') + # XXX need a way to test multiple calls + # assert_called('GET', '/servers/1234') + assert_called('GET', '/images/2') + + def test_delete(self): + shell('delete 1234') + assert_called('DELETE', '/servers/1234') + shell('delete sample-server') + assert_called('DELETE', '/servers/1234') + + def test_zone(self): + shell('zone 1') + assert_called('GET', '/zones/1') + + shell('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx') + assert_called( + 'PUT', '/zones/1', + {'zone': {'api_url': 'http://zzz', 'username': 'frank', + 'password': 'xxx'}} + ) + + def test_zone_add(self): + shell('zone-add http://zzz frank xxx 0.0 1.0') + assert_called( + 'POST', '/zones', + {'zone': {'api_url': 'http://zzz', 'username': 'frank', + 'password': 'xxx', + 'weight_offset': '0.0', 'weight_scale': '1.0'}} + ) + + def test_zone_delete(self): + shell('zone-delete 1') + assert_called('DELETE', '/zones/1') + + def test_zone_list(self): + shell('zone-list') + assert ('GET', '/zones/detail', None) in _shell.cs.client.callstack diff --git a/tests/v1_0/test_zones.py b/tests/v1_0/test_zones.py index 2e6aa8487..c6b96a3f1 100644 --- a/tests/v1_0/test_zones.py +++ b/tests/v1_0/test_zones.py @@ -1,83 +1,76 @@ -from __future__ import absolute_import import StringIO -from nose.tools import assert_equal - from novaclient.v1_0 import zones - -from .fakes import FakeClient -from .utils import assert_isinstance - -os = FakeClient() +from tests.v1_0 import fakes +from tests import utils -def test_list_zones(): - sl = os.zones.list() - os.assert_called('GET', '/zones/detail') - [assert_isinstance(s, zones.Zone) for s in sl] +os = fakes.FakeClient() -def test_list_zones_undetailed(): - sl = os.zones.list(detailed=False) - os.assert_called('GET', '/zones') - [assert_isinstance(s, zones.Zone) for s in sl] +class ZonesTest(utils.TestCase): + def test_list_zones(self): + sl = os.zones.list() + os.assert_called('GET', '/zones/detail') + [self.assertTrue(isinstance(s, zones.Zone)) for s in sl] -def test_get_zone_details(): - s = os.zones.get(1) - os.assert_called('GET', '/zones/1') - assert_isinstance(s, zones.Zone) - assert_equal(s.id, 1) - assert_equal(s.api_url, 'http://foo.com') + def test_list_zones_undetailed(self): + sl = os.zones.list(detailed=False) + os.assert_called('GET', '/zones') + [self.assertTrue(isinstance(s, zones.Zone)) for s in sl] + def test_get_zone_details(self): + s = os.zones.get(1) + os.assert_called('GET', '/zones/1') + self.assertTrue(isinstance(s, zones.Zone)) + self.assertEqual(s.id, 1) + self.assertEqual(s.api_url, 'http://foo.com') -def test_create_zone(): - s = os.zones.create(api_url="http://foo.com", username='bob', - password='xxx') - os.assert_called('POST', '/zones') - assert_isinstance(s, zones.Zone) + def test_create_zone(self): + s = os.zones.create(api_url="http://foo.com", username='bob', + password='xxx') + os.assert_called('POST', '/zones') + self.assertTrue(isinstance(s, zones.Zone)) + def test_update_zone(self): + s = os.zones.get(1) -def test_update_zone(): - s = os.zones.get(1) + # Update via instance + s.update(api_url='http://blah.com') + os.assert_called('PUT', '/zones/1') + s.update(api_url='http://blah.com', username='alice', password='xxx') + os.assert_called('PUT', '/zones/1') - # Update via instance - s.update(api_url='http://blah.com') - os.assert_called('PUT', '/zones/1') - s.update(api_url='http://blah.com', username='alice', password='xxx') - os.assert_called('PUT', '/zones/1') + # Silly, but not an error + s.update() - # Silly, but not an error - s.update() + # 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.assert_called('PUT', '/zones/1') + os.zones.update(s, api_url='http://blah.com', username='fred', + password='zip') + os.assert_called('PUT', '/zones/1') - # 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.assert_called('PUT', '/zones/1') - os.zones.update(s, api_url='http://blah.com', username='fred', - password='zip') - os.assert_called('PUT', '/zones/1') + def test_delete_zone(self): + s = os.zones.get(1) + s.delete() + os.assert_called('DELETE', '/zones/1') + os.zones.delete(1) + os.assert_called('DELETE', '/zones/1') + os.zones.delete(s) + os.assert_called('DELETE', '/zones/1') + def test_find_zone(self): + s = os.zones.find(password='qwerty') + os.assert_called('GET', '/zones/detail') + self.assertEqual(s.username, 'bob') -def test_delete_zone(): - s = os.zones.get(1) - s.delete() - os.assert_called('DELETE', '/zones/1') - os.zones.delete(1) - os.assert_called('DELETE', '/zones/1') - os.zones.delete(s) - os.assert_called('DELETE', '/zones/1') - - -def test_find_zone(): - s = os.zones.find(password='qwerty') - os.assert_called('GET', '/zones/detail') - assert_equal(s.username, 'bob') - - # Find with multiple results returns the first item - s = os.zones.find(api_url='http://foo.com') - sl = os.zones.findall(api_url='http://foo.com') - assert_equal(sl[0], s) - assert_equal([s.id for s in sl], [1, 2]) + # Find with multiple results returns the first item + s = os.zones.find(api_url='http://foo.com') + sl = os.zones.findall(api_url='http://foo.com') + self.assertEqual(sl[0], s) + self.assertEqual([s.id for s in sl], [1, 2]) diff --git a/tests/v1_0/testfile.txt b/tests/v1_0/testfile.txt index 90763c69f..e4e860f38 100644 --- a/tests/v1_0/testfile.txt +++ b/tests/v1_0/testfile.txt @@ -1 +1 @@ -OH HAI! \ No newline at end of file +BLAH diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index fc7793bf4..c13fe695f 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -1,86 +1,23 @@ -""" -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 urlparse -import urllib - import httplib2 +import urllib +import urlparse -from novaclient.v1_1 import Client -from novaclient.v1_1.client import HTTPClient - -from .utils import fail, assert_in, assert_not_in, assert_has_keys +from novaclient import client as base_client +from novaclient.v1_1 import client +from tests import fakes -def assert_equal(value_one, value_two): - try: - assert value_one == value_two - except AssertionError: - print "%(value_one)s does not equal %(value_two)s" % locals() - raise +class FakeClient(fakes.FakeClient, client.Client): + + def __init__(self, *args, **kwargs): + client.Client.__init__(self, 'username', 'apikey', + 'project_id', 'auth_url') + self.client = FakeHTTPClient(**kwargs) -class FakeClient(Client): - def __init__(self, username=None, password=None, project_id=None, - auth_url=None): - super(FakeClient, self).__init__('username', 'apikey', - 'project_id', 'auth_url') - self.client = FakeHTTPClient() +class FakeHTTPClient(base_client.HTTPClient): - 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 assert_called_anytime(self, method, url, body=None): - """ - Assert than an API method was called anytime in the test. - """ - expected = (method, url) - - assert self.client.callstack, \ - "Expected %s %s but no calls were made." % expected - - found = False - for entry in self.client.callstack: - called = entry[0:2] - if expected == entry[0:2]: - found = True - break - - assert found, 'Expected %s; got %s' % \ - (expected, self.client.callstack) - if body is not None: - assert_equal(entry[2], body) - - self.client.callstack = [] - - def authenticate(self): - pass - - -class FakeHTTPClient(HTTPClient): - def __init__(self): + def __init__(self, **kwargs): self.username = 'username' self.apikey = 'apikey' self.auth_url = 'auth_url' @@ -89,15 +26,15 @@ class FakeHTTPClient(HTTPClient): def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly if method in ['GET', 'DELETE']: - assert_not_in('body', kwargs) + assert 'body' not in kwargs elif method in ['PUT', 'POST']: - assert_in('body', kwargs) + assert 'body' in 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)) + raise AssertionError('Called unknown API method: %s %s' % (method, url)) # Note the call self.callstack.append((method, url, kwargs.get('body', None))) @@ -184,25 +121,27 @@ class FakeHTTPClient(HTTPClient): "name": "sample-server", "image": { "id": 2, + "name": "sample image", }, "flavor": { "id": 1, + "name": "256 MB Server", }, "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", "status": "BUILD", "progress": 60, "addresses": { "public": [{ - "addr": "1.2.3.4", "version": 4, + "addr": "1.2.3.4", }, { - "addr": "5.6.7.8", "version": 4, + "addr": "5.6.7.8", }], "private": [{ - "addr": "10.11.12.13", "version": 4, + "addr": "10.11.12.13", }], }, "metadata": { @@ -215,24 +154,26 @@ class FakeHTTPClient(HTTPClient): "name": "sample-server2", "image": { "id": 2, + "name": "sample image", }, "flavor": { "id": 1, + "name": "256 MB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", "addresses": { "public": [{ - "addr": "1.2.3.5", "version": 4, + "addr": "4.5.6.7", }, { - "addr": "5.6.7.9", "version": 4, + "addr": "5.6.9.8", }], "private": [{ - "addr": "10.13.12.13", "version": 4, + "addr": "10.13.12.13", }], }, "metadata": { @@ -242,13 +183,13 @@ class FakeHTTPClient(HTTPClient): ]}) def post_servers(self, body, **kw): - assert_equal(body.keys(), ['server']) - assert_has_keys(body['server'], + assert body.keys() == ['server'] + fakes.assert_has_keys(body['server'], required=['name', 'imageRef', 'flavorRef'], optional=['metadata', 'personality']) if 'personality' in body['server']: for pfile in body['server']['personality']: - assert_has_keys(pfile, required=['path', 'contents']) + fakes.assert_has_keys(pfile, required=['path', 'contents']) return (202, self.get_servers_1234()[1]) def get_servers_1234(self, **kw): @@ -260,8 +201,8 @@ class FakeHTTPClient(HTTPClient): return (200, r) def put_servers_1234(self, body, **kw): - assert_equal(body.keys(), ['server']) - assert_has_keys(body['server'], optional=['name', 'adminPass']) + assert body.keys() == ['server'] + fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) return (204, None) def delete_servers_1234(self, **kw): @@ -283,12 +224,6 @@ class FakeHTTPClient(HTTPClient): 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) @@ -297,27 +232,27 @@ class FakeHTTPClient(HTTPClient): # def post_servers_1234_action(self, body, **kw): - assert_equal(len(body.keys()), 1) + assert len(body.keys()) == 1 action = body.keys()[0] if action == 'reboot': - assert_equal(body[action].keys(), ['type']) - assert_in(body[action]['type'], ['HARD', 'SOFT']) + assert body[action].keys() == ['type'] + assert body[action]['type'] in ['HARD', 'SOFT'] elif action == 'rebuild': - assert_equal(body[action].keys(), ['imageRef']) + assert body[action].keys() == ['imageRef'] elif action == 'resize': - assert_equal(body[action].keys(), ['flavorRef']) + assert body[action].keys() == ['flavorRef'] elif action == 'confirmResize': - assert_equal(body[action], None) + assert body[action] is None # This one method returns a different response code return (204, None) elif action == 'revertResize': - assert_equal(body[action], None) - elif action == 'changePassword': - assert_equal(body[action].keys(), ["adminPass"]) + assert body[action] is None elif action == 'createImage': - assert_equal(body[action].keys(), ["name", "metadata"]) + assert set(body[action].keys()) == set(['name', 'metadata']) + elif action == 'changePassword': + assert body[action].keys() == ['adminPass'] else: - fail("Unexpected server action: %s" % action) + raise AssertionError("Unexpected server action: %s" % action) return (202, None) # @@ -378,119 +313,9 @@ class FakeHTTPClient(HTTPClient): 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', 'image_type', 'backup_type', 'rotation']) + assert body.keys() == ['image'] + fakes.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) - - # - # Zones - # - def get_zones(self, **kw): - return (200, {'zones': [ - {'id': 1, 'api_url': 'http://foo.com', 'username': 'bob'}, - {'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', - 'password': 'qwerty'}, - {'id': 2, 'api_url': 'http://foo.com', 'username': 'alice', - 'password': 'password'} - ]}) - - def get_zones_1(self, **kw): - r = {'zone': self.get_zones_detail()[1]['zones'][0]} - return (200, r) - - def get_zones_2(self, **kw): - r = {'zone': self.get_zones_detail()[1]['zones'][1]} - return (200, r) - - def post_zones(self, body, **kw): - assert_equal(body.keys(), ['zone']) - assert_has_keys(body['zone'], - required=['api_url', 'username', 'password'], - optional=['weight_offset', 'weight_scale']) - - return (202, self.get_zones_1()[1]) - - def put_zones_1(self, body, **kw): - assert_equal(body.keys(), ['zone']) - assert_has_keys(body['zone'], optional=['api_url', 'username', - 'password', - 'weight_offset', - 'weight_scale']) - return (204, None) - - def delete_zones_1(self, **kw): - return (202, None) - - # - # Accounts - # - def post_accounts_test_account_create_instance(self, body, **kw): - assert_equal(body.keys(), ['server']) - assert_has_keys(body['server'], - required=['name', 'imageRef', 'flavorRef'], - optional=['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]) diff --git a/tests/v1_1/test_auth.py b/tests/v1_1/test_auth.py new file mode 100644 index 000000000..5bb4e781f --- /dev/null +++ b/tests/v1_1/test_auth.py @@ -0,0 +1,74 @@ + +import httplib2 +import mock + +from novaclient.v1_1 import client +from novaclient import exceptions +from tests import utils + + +class AuthenticationTests(utils.TestCase): + + def test_authenticate_success(self): + cs = client.Client("username", "apikey", "project_id", "auth_url") + management_url = 'https://servers.api.rackspacecloud.com/v1.1/443470' + auth_response = httplib2.Response({ + 'status': 204, + 'x-server-management-url': management_url, + 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1', + }) + mock_request = mock.Mock(return_value=(auth_response, None)) + + @mock.patch.object(httplib2.Http, "request", mock_request) + def test_auth_call(): + cs.client.authenticate() + headers={ + 'X-Auth-User': 'username', + 'X-Auth-Key': 'apikey', + 'X-Auth-Project-Id': 'project_id', + 'User-Agent': cs.client.USER_AGENT + } + mock_request.assert_called_with(cs.client.auth_url, 'GET', + headers=headers) + self.assertEqual(cs.client.management_url, + auth_response['x-server-management-url']) + self.assertEqual(cs.client.auth_token, + auth_response['x-auth-token']) + + test_auth_call() + + def test_authenticate_failure(self): + cs = client.Client("username", "apikey", "project_id", "auth_url") + auth_response = httplib2.Response({'status': 401}) + mock_request = mock.Mock(return_value=(auth_response, None)) + + @mock.patch.object(httplib2.Http, "request", mock_request) + def test_auth_call(): + self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) + + test_auth_call() + + def test_auth_automatic(self): + cs = client.Client("username", "apikey", "project_id", "auth_url") + http_client = cs.client + http_client.management_url = '' + mock_request = mock.Mock(return_value=(None, None)) + + @mock.patch.object(http_client, 'request', mock_request) + @mock.patch.object(http_client, 'authenticate') + def test_auth_call(m): + http_client.get('/') + m.assert_called() + mock_request.assert_called() + + test_auth_call() + + def test_auth_manual(self): + cs = client.Client("username", "apikey", "project_id", "auth_url") + + @mock.patch.object(cs.client, 'authenticate') + def test_auth_call(m): + cs.authenticate() + m.assert_called() + + test_auth_call() diff --git a/tests/v1_1/test_base.py b/tests/v1_1/test_base.py deleted file mode 100644 index 9db08eb80..000000000 --- a/tests/v1_1/test_base.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import absolute_import - -import mock -from nose.tools import assert_equal, assert_not_equal, assert_raises - -from novaclient.v1_1 import flavors -from novaclient.v1_1 import exceptions -from novaclient.v1_1 import base - -from .fakes import FakeClient - -os = FakeClient() - - -def test_resource_repr(): - r = base.Resource(None, dict(foo="bar", baz="spam")) - assert_equal(repr(r), "") - - -def test_getid(): - assert_equal(base.getid(4), 4) - - class O(object): - id = 4 - assert_equal(base.getid(O), 4) - - -def test_resource_lazy_getattr(): - f = flavors.Flavor(os.flavors, {'id': 1}) - assert_equal(f.name, '256 MB Server') - os.assert_called('GET', '/flavors/1') - - # Missing stuff still fails after a second get - assert_raises(AttributeError, getattr, f, 'blahblah') - os.assert_called('GET', '/flavors/1') - - -def test_eq(): - # Two resources of the same type with the same id: equal - r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) - r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) - assert_equal(r1, r2) - - # Two resoruces of different types: never equal - r1 = base.Resource(None, {'id': 1}) - r2 = flavors.Flavor(None, {'id': 1}) - assert_not_equal(r1, r2) - - # Two resources with no ID: equal if their info is equal - r1 = base.Resource(None, {'name': 'joe', 'age': 12}) - r2 = base.Resource(None, {'name': 'joe', 'age': 12}) - assert_equal(r1, r2) - - -def test_findall_invalid_attribute(): - # Make sure findall with an invalid attribute doesn't cause errors. - # The following should not raise an exception. - os.flavors.findall(vegetable='carrot') - - # However, find() should raise an error - assert_raises(exceptions.NotFound, os.flavors.find, vegetable='carrot') diff --git a/tests/v1_1/test_client.py b/tests/v1_1/test_client.py deleted file mode 100644 index 7cf90e368..000000000 --- a/tests/v1_1/test_client.py +++ /dev/null @@ -1,52 +0,0 @@ -import mock -import httplib2 - -from novaclient.v1_1 import client -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 get_client(): - cl = client.HTTPClient("username", "apikey", "project_id", "auth_test") - cl.management_url = "http://example.com" - cl.auth_token = "token" - return cl - - -def test_get(): - cl = get_client() - - @mock.patch.object(httplib2.Http, "request", mock_request) - @mock.patch('time.time', mock.Mock(return_value=1234)) - def test_get_call(): - resp, body = cl.get("/hi") - mock_request.assert_called_with("http://example.com/hi?fresh=1234", - "GET", - headers={"X-Auth-Token": "token", - "X-Auth-Project-Id": "project_id", - "User-Agent": cl.USER_AGENT}) - # Automatic JSON parsing - assert_equal(body, {"hi": "there"}) - - test_get_call() - - -def test_post(): - cl = get_client() - - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_post_call(): - cl.post("/hi", body=[1, 2, 3]) - mock_request.assert_called_with("http://example.com/hi", "POST", - headers={ - "X-Auth-Token": "token", - "X-Auth-Project-Id": "project_id", - "Content-Type": "application/json", - "User-Agent": cl.USER_AGENT}, - body='[1, 2, 3]' - ) - - test_post_call() diff --git a/tests/v1_1/test_flavors.py b/tests/v1_1/test_flavors.py index 51ca7a1bf..bc30cda1b 100644 --- a/tests/v1_1/test_flavors.py +++ b/tests/v1_1/test_flavors.py @@ -1,42 +1,38 @@ -from __future__ import absolute_import - -from nose.tools import assert_raises, assert_equal +from novaclient import exceptions from novaclient.v1_1 import flavors -from novaclient.v1_1 import exceptions - -from .fakes import FakeClient -from .utils import assert_isinstance - -os = FakeClient() +from tests.v1_1 import fakes +from tests import utils -def test_list_flavors(): - fl = os.flavors.list() - os.assert_called('GET', '/flavors/detail') - [assert_isinstance(f, flavors.Flavor) for f in fl] +cs = fakes.FakeClient() -def test_list_flavors_undetailed(): - fl = os.flavors.list(detailed=False) - os.assert_called('GET', '/flavors') - [assert_isinstance(f, flavors.Flavor) for f in fl] +class FlavorsTest(utils.TestCase): + def test_list_flavors(self): + fl = cs.flavors.list() + cs.assert_called('GET', '/flavors/detail') + [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl] -def test_get_flavor_details(): - f = os.flavors.get(1) - os.assert_called('GET', '/flavors/1') - assert_isinstance(f, flavors.Flavor) - assert_equal(f.ram, 256) - assert_equal(f.disk, 10) + def test_list_flavors_undetailed(self): + fl = cs.flavors.list(detailed=False) + cs.assert_called('GET', '/flavors') + [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl] + def test_get_flavor_details(self): + f = cs.flavors.get(1) + cs.assert_called('GET', '/flavors/1') + self.assertTrue(isinstance(f, flavors.Flavor)) + self.assertEqual(f.ram, 256) + self.assertEqual(f.disk, 10) -def test_find(): - f = os.flavors.find(ram=256) - os.assert_called('GET', '/flavors/detail') - assert_equal(f.name, '256 MB Server') + def test_find(self): + f = cs.flavors.find(ram=256) + cs.assert_called('GET', '/flavors/detail') + self.assertEqual(f.name, '256 MB Server') - f = os.flavors.find(disk=20) - assert_equal(f.name, '512 MB Server') + f = cs.flavors.find(disk=20) + self.assertEqual(f.name, '512 MB Server') - assert_raises(exceptions.NotFound, os.flavors.find, disk=12345) + self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345) diff --git a/tests/v1_1/test_images.py b/tests/v1_1/test_images.py index 03b63b7cb..c2c78ed65 100644 --- a/tests/v1_1/test_images.py +++ b/tests/v1_1/test_images.py @@ -1,51 +1,40 @@ -from __future__ import absolute_import - -from nose.tools import assert_equal from novaclient.v1_1 import images - -from .fakes import FakeClient -from .utils import assert_isinstance - -os = FakeClient() +from tests.v1_1 import fakes +from tests import utils -def test_list_images(): - il = os.images.list() - os.assert_called('GET', '/images/detail') - [assert_isinstance(i, images.Image) for i in il] +cs = fakes.FakeClient() -def test_list_images_undetailed(): - il = os.images.list(detailed=False) - os.assert_called('GET', '/images') - [assert_isinstance(i, images.Image) for i in il] +class ImagesTest(utils.TestCase): + def test_list_images(self): + il = cs.images.list() + cs.assert_called('GET', '/images/detail') + [self.assertTrue(isinstance(i, images.Image)) for i in il] -def test_get_image_details(): - i = os.images.get(1) - os.assert_called('GET', '/images/1') - assert_isinstance(i, images.Image) - assert_equal(i.id, 1) - assert_equal(i.name, 'CentOS 5.2') + def test_list_images_undetailed(self): + il = cs.images.list(detailed=False) + cs.assert_called('GET', '/images') + [self.assertTrue(isinstance(i, images.Image)) for i in il] + def test_get_image_details(self): + i = cs.images.get(1) + cs.assert_called('GET', '/images/1') + self.assertTrue(isinstance(i, images.Image)) + self.assertEqual(i.id, 1) + self.assertEqual(i.name, 'CentOS 5.2') -def test_create_image(): - i = os.images.create(server=1234, name="Just in case") - os.assert_called('POST', '/images') - assert_isinstance(i, images.Image) + def test_delete_image(self): + cs.images.delete(1) + cs.assert_called('DELETE', '/images/1') + def test_find(self): + i = cs.images.find(name="CentOS 5.2") + self.assertEqual(i.id, 1) + cs.assert_called('GET', '/images/detail') -def test_delete_image(): - os.images.delete(1) - os.assert_called('DELETE', '/images/1') - - -def test_find(): - i = os.images.find(name="CentOS 5.2") - assert_equal(i.id, 1) - os.assert_called('GET', '/images/detail') - - iml = os.images.findall(status='SAVING') - assert_equal(len(iml), 1) - assert_equal(iml[0].name, 'My Server Backup') + iml = cs.images.findall(status='SAVING') + self.assertEqual(len(iml), 1) + self.assertEqual(iml[0].name, 'My Server Backup') diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index 1ffb7b959..5a7ff73ac 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -1,125 +1,114 @@ -from __future__ import absolute_import import StringIO -from nose.tools import assert_equal - from novaclient.v1_1 import servers - -from .fakes import FakeClient -from .utils import assert_isinstance +from tests.v1_1 import fakes +from tests import utils -os = FakeClient() +cs = fakes.FakeClient() -def test_list_servers(): - sl = os.servers.list() - os.assert_called('GET', '/servers/detail') - [assert_isinstance(s, servers.Server) for s in sl] +class ServersTest(utils.TestCase): + def test_list_servers(self): + sl = cs.servers.list() + cs.assert_called('GET', '/servers/detail') + [self.assertTrue(isinstance(s, servers.Server)) for s in sl] -def test_list_servers_undetailed(): - sl = os.servers.list(detailed=False) - os.assert_called('GET', '/servers') - [assert_isinstance(s, servers.Server) for s in sl] + def test_list_servers_undetailed(self): + sl = cs.servers.list(detailed=False) + cs.assert_called('GET', '/servers') + [self.assertTrue(isinstance(s, servers.Server)) for s in sl] + def test_get_server_details(self): + s = cs.servers.get(1234) + cs.assert_called('GET', '/servers/1234') + self.assertTrue(isinstance(s, servers.Server)) + self.assertEqual(s.id, 1234) + self.assertEqual(s.status, 'BUILD') -def test_get_server_details(): - s = os.servers.get(1234) - os.assert_called('GET', '/servers/1234') - assert_isinstance(s, servers.Server) - assert_equal(s.id, 1234) - assert_equal(s.status, 'BUILD') + def test_create_server(self): + s = cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': StringIO.StringIO('data') # a stream + } + ) + cs.assert_called('POST', '/servers') + self.assertTrue(isinstance(s, servers.Server)) + def test_update_server(self): + s = cs.servers.get(1234) -def test_create_server(): - s = os.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': StringIO.StringIO('data') # a stream - } - ) - os.assert_called('POST', '/servers') - assert_isinstance(s, servers.Server) + # Update via instance + s.update(name='hi') + cs.assert_called('PUT', '/servers/1234') + s.update(name='hi') + cs.assert_called('PUT', '/servers/1234') + # Silly, but not an error + s.update() -def test_update_server(): - s = os.servers.get(1234) + # Update via manager + cs.servers.update(s, name='hi') + cs.assert_called('PUT', '/servers/1234') - # Update via instance - s.update(name='hi') - os.assert_called('PUT', '/servers/1234') + def test_delete_server(self): + 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') - # Silly, but not an error - s.update() + def test_find(self): + s = cs.servers.find(name='sample-server') + cs.assert_called('GET', '/servers/detail') + self.assertEqual(s.name, 'sample-server') - # Update via manager - os.servers.update(s, name='hi') - os.assert_called('PUT', '/servers/1234') + # Find with multiple results arbitraility returns the first item + s = cs.servers.find(flavor={"id": 1, "name": "256 MB Server"}) + sl = cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) + self.assertEqual(sl[0], s) + self.assertEqual([s.id for s in sl], [1234, 5678]) + def test_reboot_server(self): + 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_delete_server(): - s = os.servers.get(1234) - s.delete() - os.assert_called('DELETE', '/servers/1234') - os.servers.delete(1234) - os.assert_called('DELETE', '/servers/1234') - os.servers.delete(s) - os.assert_called('DELETE', '/servers/1234') + def test_rebuild_server(self): + 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(self): + 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_find(): - s = os.servers.find(name='sample-server') - os.assert_called('GET', '/servers/detail') - assert_equal(s.name, 'sample-server') + def test_confirm_resized_server(self): + 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') - # Find with multiple results arbitraility returns the first item - s = os.servers.find(flavor_id=1) - sl = os.servers.findall(flavor_id=1) - assert_equal(sl[0], s) - assert_equal([s.id for s in sl], [1234, 5678]) - - -def test_reboot_server(): - s = os.servers.get(1234) - s.reboot() - os.assert_called('POST', '/servers/1234/action') - os.servers.reboot(s, type='HARD') - os.assert_called('POST', '/servers/1234/action') - - -def test_rebuild_server(): - s = os.servers.get(1234) - s.rebuild(image=1) - os.assert_called('POST', '/servers/1234/action') - os.servers.rebuild(s, image=1) - os.assert_called('POST', '/servers/1234/action') - - -def test_resize_server(): - s = os.servers.get(1234) - s.resize(flavor=1) - os.assert_called('POST', '/servers/1234/action') - os.servers.resize(s, flavor=1) - os.assert_called('POST', '/servers/1234/action') - - -def test_confirm_resized_server(): - s = os.servers.get(1234) - s.confirm_resize() - os.assert_called('POST', '/servers/1234/action') - os.servers.confirm_resize(s) - os.assert_called('POST', '/servers/1234/action') - - -def test_revert_resized_server(): - s = os.servers.get(1234) - s.revert_resize() - os.assert_called('POST', '/servers/1234/action') - os.servers.revert_resize(s) - os.assert_called('POST', '/servers/1234/action') + def test_revert_resized_server(self): + 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') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index baf117b93..958f1020b 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -1,234 +1,210 @@ -from __future__ import absolute_import import os import mock -import httplib2 -from nose.tools import assert_raises, assert_equal - -from novaclient.v1_1.shell import OpenStackShell, CommandError - -from .fakes import FakeClient -from .utils import assert_in +from novaclient.shell import OpenStackComputeShell +from novaclient import exceptions +from tests.v1_1 import fakes +from tests import utils -# Patch os.environ to avoid required auth info. -def setup(): - global _old_env - fake_env = { - 'NOVA_USERNAME': 'username', - 'NOVA_API_KEY': 'password', - 'NOVA_PROJECT_ID': 'project_id' - } - _old_env, os.environ = os.environ, fake_env.copy() +class ShellTest(utils.TestCase): - # 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, assert_called_anytime - _shell = OpenStackShell() - _shell._api_class = FakeClient - assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b) - assert_called_anytime = lambda m, u, b=None: \ - _shell.cs.assert_called_anytime(m, u, b) - shell = lambda cmd: _shell.main(cmd.split()) - - -def teardown(): - global _old_env - os.environ = _old_env - - -def test_boot(): - shell('boot --image 1 some-server') - assert_called( - 'POST', '/servers', - {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1'}} - ) - - shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ') - assert_called( - 'POST', '/servers', - {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', - 'metadata': {'foo': 'bar', 'spam': 'eggs'}}} - ) - - -def test_boot_files(): - testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - expected_file_data = open(testfile).read().encode('base64') - - shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' % - (testfile, testfile)) - - assert_called( - 'POST', '/servers', - {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', - 'personality': [ - {'path': '/tmp/bar', 'contents': expected_file_data}, - {'path': '/tmp/foo', 'contents': expected_file_data} - ]} + # Patch os.environ to avoid required auth info. + def setUp(self): + global _old_env + fake_env = { + 'NOVA_USERNAME': 'username', + 'NOVA_API_KEY': 'password', + 'NOVA_PROJECT_ID': 'project_id' } - ) + _old_env, os.environ = os.environ, fake_env.copy() + # 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, assert_called_anytime + _shell = OpenStackComputeShell() + _shell._api_class = fakes.FakeClient + assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b) + assert_called_anytime = lambda m, u, b=None: \ + _shell.cs.assert_called_anytime(m, u, b) + shell = lambda cmd: _shell.main(cmd.split()) -def test_boot_invalid_file(): - invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') - assert_raises(CommandError, shell, 'boot some-server --image 1 ' - '--file /foo=%s' % invalid_file) + def tearDown(self): + global _old_env + os.environ = _old_env - -def test_boot_key_auto(): - mock_exists = mock.Mock(return_value=True) - mock_open = mock.Mock() - mock_open.return_value = mock.Mock() - mock_open.return_value.read = mock.Mock(return_value='SSHKEY') - - @mock.patch('os.path.exists', mock_exists) - @mock.patch('__builtin__.open', mock_open) - def test_shell_call(): - shell('boot some-server --image 1 --key') + def test_boot(self): + shell('boot --image 1 some-server') assert_called( 'POST', '/servers', - {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', - 'personality': [{ - 'path': '/root/.ssh/authorized_keys2', - 'contents': ('SSHKEY').encode('base64')}, - ]} + {'server': { + 'flavorRef': 1, + 'name': 'some-server', + 'imageRef': '1', + 'personality': [], + 'metadata': {}, + }} + ) + + shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ') + assert_called( + 'POST', '/servers', + {'server': { + 'flavorRef': 1, + 'name': 'some-server', + 'imageRef': '1', + 'metadata': {'foo': 'bar', 'spam': 'eggs'}, + 'personality': [], + }} + ) + + def test_boot_files(self): + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + expected_file_data = open(testfile).read().encode('base64') + + cmd = 'boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' + shell(cmd % (testfile, testfile)) + + assert_called( + 'POST', '/servers', + {'server': { + 'flavorRef': 1, + 'name': 'some-server', + 'imageRef': '1', + 'metadata': {}, + 'personality': [ + {'path': '/tmp/bar', 'contents': expected_file_data}, + {'path': '/tmp/foo', 'contents': expected_file_data} + ]} } ) - test_shell_call() + def test_boot_invalid_file(self): + invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') + cmd = 'boot some-server --image 1 --file /foo=%s' % invalid_file + self.assertRaises(exceptions.CommandError, shell, cmd) + def test_boot_key_auto(self): + mock_exists = mock.Mock(return_value=True) + mock_open = mock.Mock() + mock_open.return_value = mock.Mock() + mock_open.return_value.read = mock.Mock(return_value='SSHKEY') -def test_boot_key_auto_no_keys(): - mock_exists = mock.Mock(return_value=False) - - @mock.patch('os.path.exists', mock_exists) - def test_shell_call(): - assert_raises(CommandError, shell, 'boot some-server --image 1 --key') - - test_shell_call() - - -def test_boot_key_file(): - testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - expected_file_data = open(testfile).read().encode('base64') - shell('boot some-server --image 1 --key %s' % testfile) - assert_called( - 'POST', '/servers', - {'server': {'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', - 'personality': [ - {'path': '/root/.ssh/authorized_keys2', 'contents': - expected_file_data}, + @mock.patch('os.path.exists', mock_exists) + @mock.patch('__builtin__.open', mock_open) + def test_shell_call(): + shell('boot some-server --image 1 --key') + assert_called( + 'POST', '/servers', + {'server': { + 'flavorRef': 1, + 'name': 'some-server', + 'imageRef': '1', + 'metadata': {}, + 'personality': [{ + 'path': '/root/.ssh/authorized_keys2', + 'contents': ('SSHKEY').encode('base64')}, ]} - } - ) + } + ) + test_shell_call() -def test_boot_invalid_keyfile(): - invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') - assert_raises(CommandError, shell, 'boot some-server ' - '--image 1 --key %s' % invalid_file) + def test_boot_key_auto_no_keys(self): + mock_exists = mock.Mock(return_value=False) + @mock.patch('os.path.exists', mock_exists) + def test_shell_call(): + self.assertRaises(exceptions.CommandError, shell, + 'boot some-server --image 1 --key') -def test_flavor_list(): - shell('flavor-list') - assert_called_anytime('GET', '/flavors/detail') + test_shell_call() + def test_boot_key_file(self): + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + expected_file_data = open(testfile).read().encode('base64') + shell('boot some-server --image 1 --key %s' % testfile) + assert_called( + 'POST', '/servers', + {'server': { + 'flavorRef': 1, + 'name': 'some-server', 'imageRef': '1', + 'metadata': {}, + 'personality': [ + {'path': '/root/.ssh/authorized_keys2', + 'contents':expected_file_data}, + ]} + } + ) -def test_image_list(): - shell('image-list') - assert_called('GET', '/images/detail') + def test_boot_invalid_keyfile(self): + invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') + self.assertRaises(exceptions.CommandError, shell, 'boot some-server ' + '--image 1 --key %s' % invalid_file) + def test_flavor_list(self): + shell('flavor-list') + assert_called_anytime('GET', '/flavors/detail') -def test_create_image(): - shell('create-image sample-server mysnapshot') - assert_called( - 'POST', '/servers/1234/action', - {'createImage': {'name': 'mysnapshot', "metadata": {}}} - ) + def test_image_list(self): + shell('image-list') + assert_called('GET', '/images/detail') + def test_create_image(self): + shell('create-image sample-server mysnapshot') + assert_called( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', 'metadata': {}}} + ) -def test_image_delete(): - shell('image-delete 1') - assert_called('DELETE', '/images/1') + def test_image_delete(self): + shell('image-delete 1') + assert_called('DELETE', '/images/1') + def test_list(self): + shell('list') + assert_called('GET', '/servers/detail') -def test_list(): - shell('list') - assert_called('GET', '/servers/detail') + def test_reboot(self): + 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(self): + shell('rebuild sample-server 1') + assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}}) -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_rename(self): + shell('rename sample-server newname') + assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}}) + def test_resize(self): + shell('resize sample-server 1') + assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 1}}) -def test_rebuild(): - shell('rebuild sample-server 1') - assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}}) + def test_resize_confirm(self): + shell('resize-confirm sample-server') + assert_called('POST', '/servers/1234/action', {'confirmResize': None}) + def test_resize_revert(self): + shell('resize-revert sample-server') + assert_called('POST', '/servers/1234/action', {'revertResize': None}) -def test_rename(): - shell('rename sample-server newname') - assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}}) + @mock.patch('getpass.getpass', mock.Mock(return_value='p')) + def test_root_password(self): + shell('root-password sample-server') + assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}}) + def test_show(self): + shell('show 1234') + # XXX need a way to test multiple calls + # assert_called('GET', '/servers/1234') + assert_called('GET', '/images/2') -def test_resize(): - shell('resize sample-server 1') - assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 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('POST', '/servers/1234/action', {'changePassword': {'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(): - @mock.patch.object(_shell.parser, 'print_help') - def test_help(m): - shell('help') - m.assert_called() - - @mock.patch.object(_shell.subcommands['delete'], 'print_help') - def test_help_delete(m): - shell('help delete') - m.assert_called() - - test_help() - test_help_delete() - - assert_raises(CommandError, shell, 'help foofoo') - - -def test_debug(): - httplib2.debuglevel = 0 - shell('--debug list') - assert httplib2.debuglevel == 1 + def test_delete(self): + shell('delete 1234') + assert_called('DELETE', '/servers/1234') + shell('delete sample-server') + assert_called('DELETE', '/servers/1234') diff --git a/tests/v1_1/testfile.txt b/tests/v1_1/testfile.txt index 90763c69f..e4e860f38 100644 --- a/tests/v1_1/testfile.txt +++ b/tests/v1_1/testfile.txt @@ -1 +1 @@ -OH HAI! \ No newline at end of file +BLAH From 9080e80832bb782fd85ae5c4b3b7a0a745379af6 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 3 Aug 2011 17:58:39 -0400 Subject: [PATCH 03/17] Updated the default version back to 1.0, as there are some quirks with 1.1 --- novaclient/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 0ab0d6945..0a063ae20 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -78,8 +78,8 @@ class OpenStackComputeShell(object): help='Defaults to env[NOVA_URL].') parser.add_argument('--version', - default='1.1', - help='Accepts 1.0 or 1.1, defaults to 1.1') + default='1.0', + help='Accepts 1.0 or 1.1, defaults to 1.0') return parser From 57ebb51e2149896984a019a8ae42cd5e87041479 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 3 Aug 2011 18:14:47 -0400 Subject: [PATCH 04/17] Tests now run correctly for v1.1 and v1.0 --- novaclient/v1_1/servers.py | 20 ++++++++++++++++---- tests/v1_1/test_shell.py | 6 +++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index e51ece70d..c1a605fcb 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -109,18 +109,30 @@ class Server(base.Resource): """ Shortcut to get this server's primary public IP address. """ - if len(self.addresses['public']) == 0: + try: + public_addresses = self.addresses["public"] + except KeyError: + public_addresses = None + + if public_addresses is None or len(public_addresses) == 0: return "" - return self.addresses['public'] + else: + return public_addresses @property def private_ip(self): """ Shortcut to get this server's primary private IP address. """ - if len(self.addresses['private']) == 0: + try: + private_addresses = self.addresses["private"] + except KeyError: + private_addresses = None + + if private_addresses is None or len(private_addresses) == 0: return "" - return self.addresses['private'] + else: + return private_addresses class ServerManager(local_base.BootingManagerWithFind): diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 958f1020b..e4ea4094e 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -28,7 +28,11 @@ class ShellTest(utils.TestCase): assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b) assert_called_anytime = lambda m, u, b=None: \ _shell.cs.assert_called_anytime(m, u, b) - shell = lambda cmd: _shell.main(cmd.split()) + + def shell(cmd): + command = ['--version=1.1',] + command.extend(cmd.split()) + _shell.main(command) def tearDown(self): global _old_env From c9422d5e3b92ff3713dad9911ab5520b1547e15c Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 3 Aug 2011 18:18:51 -0400 Subject: [PATCH 05/17] Grrrr, bad import. --- novaclient/v1_1/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index c1a605fcb..cd649a4c6 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -22,7 +22,7 @@ Server interface. import urllib from novaclient import base -from novaclient.v1_0 import base as local_base +from novaclient.v1_1 import base as local_base REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' From fc8253f2ca4a9fb22af2ea0c966d5bf122339f49 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 3 Aug 2011 18:38:29 -0400 Subject: [PATCH 06/17] Wrong client was getting loaded. --- novaclient/shell.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 0a063ae20..be22ee881 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -157,7 +157,8 @@ class OpenStackComputeShell(object): raise exceptions.CommandError("You must provide an API key, either via " "--apikey or via env[NOVA_API_KEY]") - self.cs = self.get_api_class()(user, apikey, projectid, url) + self.cs = self.get_api_class(options.version)(user, apikey, projectid, url) + try: self.cs.authenticate() except exceptions.Unauthorized: @@ -165,8 +166,17 @@ class OpenStackComputeShell(object): args.func(self.cs, args) - def get_api_class(self): - return self._api_class or shell_v1_0.CLIENT_CLASS + def get_api_class(self, version): + if self._api_class is not None: + return self._api_class + + try: + return { + "1.0": shell_v1_0.CLIENT_CLASS, + "1.1": shell_v1_1.CLIENT_CLASS, + }[version] + except KeyError: + return shell_v1_0.CLIENT_CLASS @utils.arg('command', metavar='', nargs='?', help='Display help for ') From 6c73d4c3b69b0b44299384b05907c086e7e996b4 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 4 Aug 2011 09:13:46 -0400 Subject: [PATCH 07/17] Cleaned up v1.0 and v1.1 test setup to remove globals and encapsulate custom asserts. Still duplicate code, but closer to being able to remove. Now tests set up OpenStackClient much closer to how users will do it, minus the stubbing of the client. --- novaclient/shell.py | 7 +- novaclient/v1_0/__init__.py | 1 + novaclient/v1_1/__init__.py | 1 + tests/v1_0/test_shell.py | 215 ++++++++++++++++++------------------ tests/v1_1/test_shell.py | 128 ++++++++++----------- 5 files changed, 174 insertions(+), 178 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index be22ee881..6cce91979 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -37,9 +37,6 @@ def env(e): class OpenStackComputeShell(object): - # Hook for the test suite to inject a fake server. - _api_class = None - def get_base_parser(self): parser = argparse.ArgumentParser( prog='nova', @@ -167,15 +164,13 @@ class OpenStackComputeShell(object): args.func(self.cs, args) def get_api_class(self, version): - if self._api_class is not None: - return self._api_class - try: return { "1.0": shell_v1_0.CLIENT_CLASS, "1.1": shell_v1_1.CLIENT_CLASS, }[version] except KeyError: + print "Bad API version provided, falling back to v1.0." return shell_v1_0.CLIENT_CLASS @utils.arg('command', metavar='', nargs='?', diff --git a/novaclient/v1_0/__init__.py b/novaclient/v1_0/__init__.py index e69de29bb..8fb731925 100644 --- a/novaclient/v1_0/__init__.py +++ b/novaclient/v1_0/__init__.py @@ -0,0 +1 @@ +from novaclient.v1_0.client import Client diff --git a/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py index e69de29bb..145edc77e 100644 --- a/novaclient/v1_1/__init__.py +++ b/novaclient/v1_1/__init__.py @@ -0,0 +1 @@ +from novaclient.v1_1.client import Client diff --git a/tests/v1_0/test_shell.py b/tests/v1_0/test_shell.py index ebde8c8ac..293a49ebd 100644 --- a/tests/v1_0/test_shell.py +++ b/tests/v1_0/test_shell.py @@ -10,74 +10,73 @@ from tests import utils class ShellTest(utils.TestCase): - # Patch os.environ to avoid required auth info. def setUp(self): - global _old_env - fake_env = { + """Run before each test.""" + self.old_environment = os.environ.copy() + os.environ = { 'NOVA_USERNAME': 'username', 'NOVA_API_KEY': 'password', 'NOVA_PROJECT_ID': 'project_id' } - _old_env, os.environ = os.environ, fake_env.copy() - # 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, assert_called_anytime - _shell = OpenStackComputeShell() - _shell._api_class = fakes.FakeClient - assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b) - assert_called_anytime = lambda m, u, b=None: \ - _shell.cs.assert_called_anytime(m, u, b) - - def shell(cmd): - command = ['--version=1.0',] - command.extend(cmd.split()) - _shell.main(command) + self.shell = OpenStackComputeShell() + self.shell.get_api_class = lambda *_: fakes.FakeClient def tearDown(self): - global _old_env - os.environ = _old_env + os.environ = self.old_environment + + def run_command(self, cmd): + command = ['--version=1.0'] + command.extend(cmd.split()) + print command + self.shell.main(command) + + def assert_called(self, method, url, body=None): + return self.shell.cs.assert_called(method, url, body) + + def assert_called_anytime(self, method, url, body=None): + return self.shell.cs.assert_called_anytime(method, url, body) def test_backup_schedule(self): - shell('backup-schedule 1234') - assert_called('GET', '/servers/1234/backup_schedule') + self.run_command('backup-schedule 1234') + self.assert_called('GET', '/servers/1234/backup_schedule') - shell('backup-schedule sample-server --weekly monday') - assert_called( + self.run_command('backup-schedule sample-server --weekly monday') + self.assert_called( 'POST', '/servers/1234/backup_schedule', {'backupSchedule': {'enabled': True, 'daily': 'DISABLED', 'weekly': 'MONDAY'}} ) - shell('backup-schedule sample-server ' + self.run_command('backup-schedule sample-server ' '--weekly disabled --daily h_0000_0200') - assert_called( + self.assert_called( 'POST', '/servers/1234/backup_schedule', {'backupSchedule': {'enabled': True, 'daily': 'H_0000_0200', 'weekly': 'DISABLED'}} ) - shell('backup-schedule sample-server --disable') - assert_called( + self.run_command('backup-schedule sample-server --disable') + self.assert_called( 'POST', '/servers/1234/backup_schedule', {'backupSchedule': {'enabled': False, 'daily': 'DISABLED', 'weekly': 'DISABLED'}} ) def test_backup_schedule_delete(self): - shell('backup-schedule-delete 1234') - assert_called('DELETE', '/servers/1234/backup_schedule') + self.run_command('backup-schedule-delete 1234') + self.assert_called('DELETE', '/servers/1234/backup_schedule') def test_boot(self): - shell('boot --image 1 some-server') - assert_called( + self.run_command('boot --image 1 some-server') + self.assert_called( 'POST', '/servers', {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', 'min_count': 1, 'max_count': 1}} ) - shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ') - assert_called( + self.run_command('boot --image 1 --meta foo=bar --meta spam=eggs some-server ') + self.assert_called( 'POST', '/servers', {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', 'min_count': 1, 'max_count': 1, @@ -88,10 +87,10 @@ class ShellTest(utils.TestCase): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') expected_file_data = open(testfile).read().encode('base64') - shell('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' % + self.run_command('boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' % (testfile, testfile)) - assert_called( + self.assert_called( 'POST', '/servers', {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', 'min_count': 1, 'max_count': 1, @@ -104,7 +103,7 @@ class ShellTest(utils.TestCase): def test_boot_invalid_file(self): invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') - self.assertRaises(exceptions.CommandError, shell, 'boot some-server --image 1 ' + self.assertRaises(exceptions.CommandError, self.run_command, 'boot some-server --image 1 ' '--file /foo=%s' % invalid_file) def test_boot_key_auto(self): @@ -116,8 +115,8 @@ class ShellTest(utils.TestCase): @mock.patch('os.path.exists', mock_exists) @mock.patch('__builtin__.open', mock_open) def test_shell_call(): - shell('boot some-server --image 1 --key') - assert_called( + self.run_command('boot some-server --image 1 --key') + self.assert_called( 'POST', '/servers', {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', 'min_count': 1, 'max_count': 1, @@ -135,7 +134,7 @@ class ShellTest(utils.TestCase): @mock.patch('os.path.exists', mock_exists) def test_shell_call(): - self.assertRaises(exceptions.CommandError, shell, + self.assertRaises(exceptions.CommandError, self.run_command, 'boot some-server --image 1 --key') test_shell_call() @@ -143,8 +142,8 @@ class ShellTest(utils.TestCase): def test_boot_key_file(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') expected_file_data = open(testfile).read().encode('base64') - shell('boot some-server --image 1 --key %s' % testfile) - assert_called( + self.run_command('boot some-server --image 1 --key %s' % testfile) + self.assert_called( 'POST', '/servers', {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', 'min_count': 1, 'max_count': 1, @@ -157,117 +156,117 @@ class ShellTest(utils.TestCase): def test_boot_invalid_keyfile(self): invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') - self.assertRaises(exceptions.CommandError, shell, 'boot some-server ' + self.assertRaises(exceptions.CommandError, self.run_command, 'boot some-server ' '--image 1 --key %s' % invalid_file) def test_boot_ipgroup(self): - shell('boot --image 1 --ipgroup 1 some-server') - assert_called( + self.run_command('boot --image 1 --ipgroup 1 some-server') + self.assert_called( 'POST', '/servers', {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', 'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}} ) def test_boot_ipgroup_name(self): - shell('boot --image 1 --ipgroup group1 some-server') - assert_called( + self.run_command('boot --image 1 --ipgroup group1 some-server') + self.assert_called( 'POST', '/servers', {'server': {'flavorId': 1, 'name': 'some-server', 'imageId': '1', 'sharedIpGroupId': 1, 'min_count': 1, 'max_count': 1}} ) def test_flavor_list(self): - shell('flavor-list') - assert_called_anytime('GET', '/flavors/detail') + self.run_command('flavor-list') + self.assert_called_anytime('GET', '/flavors/detail') def test_image_list(self): - shell('image-list') - assert_called('GET', '/images/detail') + self.run_command('image-list') + self.assert_called('GET', '/images/detail') def test_snapshot_create(self): - shell('image-create sample-server mysnapshot') - assert_called( + self.run_command('image-create sample-server mysnapshot') + self.assert_called( 'POST', '/images', {'image': {'name': 'mysnapshot', 'serverId': 1234}} ) def test_image_delete(self): - shell('image-delete 1') - assert_called('DELETE', '/images/1') + self.run_command('image-delete 1') + self.assert_called('DELETE', '/images/1') def test_ip_share(self): - shell('ip-share sample-server 1 1.2.3.4') - assert_called( + self.run_command('ip-share sample-server 1 1.2.3.4') + self.assert_called( 'PUT', '/servers/1234/ips/public/1.2.3.4', {'shareIp': {'sharedIpGroupId': 1, 'configureServer': True}} ) def test_ip_unshare(self): - shell('ip-unshare sample-server 1.2.3.4') - assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4') + self.run_command('ip-unshare sample-server 1.2.3.4') + self.assert_called('DELETE', '/servers/1234/ips/public/1.2.3.4') def test_ipgroup_list(self): - shell('ipgroup-list') + self.run_command('ipgroup-list') assert ('GET', '/shared_ip_groups/detail', None) in \ - _shell.cs.client.callstack - assert_called('GET', '/servers/5678') + self.shell.cs.client.callstack + self.assert_called('GET', '/servers/5678') def test_ipgroup_show(self): - shell('ipgroup-show 1') - assert_called('GET', '/shared_ip_groups/1') - shell('ipgroup-show group2') + self.run_command('ipgroup-show 1') + self.assert_called('GET', '/shared_ip_groups/1') + self.run_command('ipgroup-show group2') # does a search, not a direct GET - assert_called('GET', '/shared_ip_groups/detail') + self.assert_called('GET', '/shared_ip_groups/detail') def test_ipgroup_create(self): - shell('ipgroup-create a-group') - assert_called( + self.run_command('ipgroup-create a-group') + self.assert_called( 'POST', '/shared_ip_groups', {'sharedIpGroup': {'name': 'a-group'}} ) - shell('ipgroup-create a-group sample-server') - assert_called( + self.run_command('ipgroup-create a-group sample-server') + self.assert_called( 'POST', '/shared_ip_groups', {'sharedIpGroup': {'name': 'a-group', 'server': 1234}} ) def test_ipgroup_delete(self): - shell('ipgroup-delete group1') - assert_called('DELETE', '/shared_ip_groups/1') + self.run_command('ipgroup-delete group1') + self.assert_called('DELETE', '/shared_ip_groups/1') def test_list(self): - shell('list') - assert_called('GET', '/servers/detail') + self.run_command('list') + self.assert_called('GET', '/servers/detail') def test_reboot(self): - 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'}}) + self.run_command('reboot sample-server') + self.assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}}) + self.run_command('reboot sample-server --hard') + self.assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}}) def test_rebuild(self): - shell('rebuild sample-server 1') - assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}}) + self.run_command('rebuild sample-server 1') + self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageId': 1}}) def test_rename(self): - shell('rename sample-server newname') - assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}}) + self.run_command('rename sample-server newname') + self.assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}}) def test_resize(self): - shell('resize sample-server 1') - assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}}) + self.run_command('resize sample-server 1') + self.assert_called('POST', '/servers/1234/action', {'resize': {'flavorId': 1}}) def test_resize_confirm(self): - shell('resize-confirm sample-server') - assert_called('POST', '/servers/1234/action', {'confirmResize': None}) + self.run_command('resize-confirm sample-server') + self.assert_called('POST', '/servers/1234/action', {'confirmResize': None}) def test_resize_revert(self): - shell('resize-revert sample-server') - assert_called('POST', '/servers/1234/action', {'revertResize': None}) + self.run_command('resize-revert sample-server') + self.assert_called('POST', '/servers/1234/action', {'revertResize': None}) def test_backup(self): - shell('backup sample-server mybackup daily 1') - assert_called( + self.run_command('backup sample-server mybackup daily 1') + self.assert_called( 'POST', '/servers/1234/action', {'createBackup': {'name': 'mybackup', 'backup_type': 'daily', 'rotation': 1}} @@ -275,35 +274,35 @@ class ShellTest(utils.TestCase): @mock.patch('getpass.getpass', mock.Mock(return_value='p')) def test_root_password(self): - shell('root-password sample-server') - assert_called('PUT', '/servers/1234', {'server': {'adminPass': 'p'}}) + self.run_command('root-password sample-server') + self.assert_called('PUT', '/servers/1234', {'server': {'adminPass': 'p'}}) def test_show(self): - shell('show 1234') + self.run_command('show 1234') # XXX need a way to test multiple calls - # assert_called('GET', '/servers/1234') - assert_called('GET', '/images/2') + # self.assert_called('GET', '/servers/1234') + self.assert_called('GET', '/images/2') def test_delete(self): - shell('delete 1234') - assert_called('DELETE', '/servers/1234') - shell('delete sample-server') - assert_called('DELETE', '/servers/1234') + self.run_command('delete 1234') + self.assert_called('DELETE', '/servers/1234') + self.run_command('delete sample-server') + self.assert_called('DELETE', '/servers/1234') def test_zone(self): - shell('zone 1') - assert_called('GET', '/zones/1') + self.run_command('zone 1') + self.assert_called('GET', '/zones/1') - shell('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx') - assert_called( + self.run_command('zone 1 --api_url=http://zzz --zone_username=frank --password=xxx') + self.assert_called( 'PUT', '/zones/1', {'zone': {'api_url': 'http://zzz', 'username': 'frank', 'password': 'xxx'}} ) def test_zone_add(self): - shell('zone-add http://zzz frank xxx 0.0 1.0') - assert_called( + self.run_command('zone-add http://zzz frank xxx 0.0 1.0') + self.assert_called( 'POST', '/zones', {'zone': {'api_url': 'http://zzz', 'username': 'frank', 'password': 'xxx', @@ -311,9 +310,9 @@ class ShellTest(utils.TestCase): ) def test_zone_delete(self): - shell('zone-delete 1') - assert_called('DELETE', '/zones/1') + self.run_command('zone-delete 1') + self.assert_called('DELETE', '/zones/1') def test_zone_list(self): - shell('zone-list') - assert ('GET', '/zones/detail', None) in _shell.cs.client.callstack + self.run_command('zone-list') + assert ('GET', '/zones/detail', None) in self.shell.cs.client.callstack diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index e4ea4094e..d1249c514 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -12,35 +12,35 @@ class ShellTest(utils.TestCase): # Patch os.environ to avoid required auth info. def setUp(self): - global _old_env - fake_env = { + """Run before each test.""" + self.old_environment = os.environ.copy() + os.environ = { 'NOVA_USERNAME': 'username', 'NOVA_API_KEY': 'password', - 'NOVA_PROJECT_ID': 'project_id' + 'NOVA_PROJECT_ID': 'project_id', } - _old_env, os.environ = os.environ, fake_env.copy() - # 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, assert_called_anytime - _shell = OpenStackComputeShell() - _shell._api_class = fakes.FakeClient - assert_called = lambda m, u, b=None: _shell.cs.assert_called(m, u, b) - assert_called_anytime = lambda m, u, b=None: \ - _shell.cs.assert_called_anytime(m, u, b) - - def shell(cmd): - command = ['--version=1.1',] - command.extend(cmd.split()) - _shell.main(command) + self.shell = OpenStackComputeShell() + self.shell.get_api_class = lambda *_: fakes.FakeClient def tearDown(self): - global _old_env - os.environ = _old_env + os.environ = self.old_environment + + def run_command(self, cmd): + command = ['--version=1.1'] + command.extend(cmd.split()) + print command + self.shell.main(command) + + def assert_called(self, method, url, body=None): + return self.shell.cs.assert_called(method, url, body) + + def assert_called_anytime(self, method, url, body=None): + return self.shell.cs.assert_called_anytime(method, url, body) def test_boot(self): - shell('boot --image 1 some-server') - assert_called( + self.run_command('boot --image 1 some-server') + self.assert_called( 'POST', '/servers', {'server': { 'flavorRef': 1, @@ -51,8 +51,8 @@ class ShellTest(utils.TestCase): }} ) - shell('boot --image 1 --meta foo=bar --meta spam=eggs some-server ') - assert_called( + self.run_command('boot --image 1 --meta foo=bar --meta spam=eggs some-server ') + self.assert_called( 'POST', '/servers', {'server': { 'flavorRef': 1, @@ -68,9 +68,9 @@ class ShellTest(utils.TestCase): expected_file_data = open(testfile).read().encode('base64') cmd = 'boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' - shell(cmd % (testfile, testfile)) + self.run_command(cmd % (testfile, testfile)) - assert_called( + self.assert_called( 'POST', '/servers', {'server': { 'flavorRef': 1, @@ -87,7 +87,7 @@ class ShellTest(utils.TestCase): def test_boot_invalid_file(self): invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') cmd = 'boot some-server --image 1 --file /foo=%s' % invalid_file - self.assertRaises(exceptions.CommandError, shell, cmd) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) def test_boot_key_auto(self): mock_exists = mock.Mock(return_value=True) @@ -98,8 +98,8 @@ class ShellTest(utils.TestCase): @mock.patch('os.path.exists', mock_exists) @mock.patch('__builtin__.open', mock_open) def test_shell_call(): - shell('boot some-server --image 1 --key') - assert_called( + self.run_command('boot some-server --image 1 --key') + self.assert_called( 'POST', '/servers', {'server': { 'flavorRef': 1, @@ -120,7 +120,7 @@ class ShellTest(utils.TestCase): @mock.patch('os.path.exists', mock_exists) def test_shell_call(): - self.assertRaises(exceptions.CommandError, shell, + self.assertRaises(exceptions.CommandError, self.run_command, 'boot some-server --image 1 --key') test_shell_call() @@ -128,8 +128,8 @@ class ShellTest(utils.TestCase): def test_boot_key_file(self): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') expected_file_data = open(testfile).read().encode('base64') - shell('boot some-server --image 1 --key %s' % testfile) - assert_called( + self.run_command('boot some-server --image 1 --key %s' % testfile) + self.assert_called( 'POST', '/servers', {'server': { 'flavorRef': 1, @@ -144,71 +144,71 @@ class ShellTest(utils.TestCase): def test_boot_invalid_keyfile(self): invalid_file = os.path.join(os.path.dirname(__file__), 'asdfasdfasdfasdf') - self.assertRaises(exceptions.CommandError, shell, 'boot some-server ' + self.assertRaises(exceptions.CommandError, self.run_command, 'boot some-server ' '--image 1 --key %s' % invalid_file) def test_flavor_list(self): - shell('flavor-list') - assert_called_anytime('GET', '/flavors/detail') + self.run_command('flavor-list') + self.assert_called_anytime('GET', '/flavors/detail') def test_image_list(self): - shell('image-list') - assert_called('GET', '/images/detail') + self.run_command('image-list') + self.assert_called('GET', '/images/detail') def test_create_image(self): - shell('create-image sample-server mysnapshot') - assert_called( + self.run_command('create-image sample-server mysnapshot') + self.assert_called( 'POST', '/servers/1234/action', {'createImage': {'name': 'mysnapshot', 'metadata': {}}} ) def test_image_delete(self): - shell('image-delete 1') - assert_called('DELETE', '/images/1') + self.run_command('image-delete 1') + self.assert_called('DELETE', '/images/1') def test_list(self): - shell('list') - assert_called('GET', '/servers/detail') + self.run_command('list') + self.assert_called('GET', '/servers/detail') def test_reboot(self): - 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'}}) + self.run_command('reboot sample-server') + self.assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'SOFT'}}) + self.run_command('reboot sample-server --hard') + self.assert_called('POST', '/servers/1234/action', {'reboot': {'type': 'HARD'}}) def test_rebuild(self): - shell('rebuild sample-server 1') - assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}}) + self.run_command('rebuild sample-server 1') + self.assert_called('POST', '/servers/1234/action', {'rebuild': {'imageRef': 1}}) def test_rename(self): - shell('rename sample-server newname') - assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}}) + self.run_command('rename sample-server newname') + self.assert_called('PUT', '/servers/1234', {'server': {'name': 'newname'}}) def test_resize(self): - shell('resize sample-server 1') - assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 1}}) + self.run_command('resize sample-server 1') + self.assert_called('POST', '/servers/1234/action', {'resize': {'flavorRef': 1}}) def test_resize_confirm(self): - shell('resize-confirm sample-server') - assert_called('POST', '/servers/1234/action', {'confirmResize': None}) + self.run_command('resize-confirm sample-server') + self.assert_called('POST', '/servers/1234/action', {'confirmResize': None}) def test_resize_revert(self): - shell('resize-revert sample-server') - assert_called('POST', '/servers/1234/action', {'revertResize': None}) + self.run_command('resize-revert sample-server') + self.assert_called('POST', '/servers/1234/action', {'revertResize': None}) @mock.patch('getpass.getpass', mock.Mock(return_value='p')) def test_root_password(self): - shell('root-password sample-server') - assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}}) + self.run_command('root-password sample-server') + self.assert_called('POST', '/servers/1234/action', {'changePassword': {'adminPass': 'p'}}) def test_show(self): - shell('show 1234') + self.run_command('show 1234') # XXX need a way to test multiple calls # assert_called('GET', '/servers/1234') - assert_called('GET', '/images/2') + self.assert_called('GET', '/images/2') def test_delete(self): - shell('delete 1234') - assert_called('DELETE', '/servers/1234') - shell('delete sample-server') - assert_called('DELETE', '/servers/1234') + self.run_command('delete 1234') + self.assert_called('DELETE', '/servers/1234') + self.run_command('delete sample-server') + self.assert_called('DELETE', '/servers/1234') From c0c5f6925e2e119612cb516d3c0e8460c343f2bd Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 4 Aug 2011 10:24:36 -0400 Subject: [PATCH 08/17] osc -> novaclient --- novaclient/v1_0/shell.py | 18 +++++++++--------- novaclient/v1_1/shell.py | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/novaclient/v1_0/shell.py b/novaclient/v1_0/shell.py index f652e3db4..6be6cb228 100644 --- a/novaclient/v1_0/shell.py +++ b/novaclient/v1_0/shell.py @@ -143,17 +143,17 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): @utils.arg('--flavor', default=None, metavar='', - help="Flavor ID (see 'osc flavors'). "\ + help="Flavor ID (see 'novaclient flavors'). "\ "Defaults to 256MB RAM instance.") @utils.arg('--image', default=None, metavar='', - help="Image ID (see 'osc images'). "\ + help="Image ID (see 'novaclient images'). "\ "Defaults to Ubuntu 10.04 LTS.") @utils.arg('--ipgroup', default=None, metavar='', - help="IP group name or ID (see 'osc ipgroup-list').") + help="IP group name or ID (see 'novaclient ipgroup-list').") @utils.arg('--meta', metavar="", action='append', @@ -191,17 +191,17 @@ def do_boot(cs, args): @utils.arg('--flavor', default=None, metavar='', - help="Flavor ID (see 'osc flavors'). "\ + help="Flavor ID (see 'novaclient flavors'). "\ "Defaults to 256MB RAM instance.") @utils.arg('--image', default=None, metavar='', - help="Image ID (see 'osc images'). "\ + help="Image ID (see 'novaclient images'). "\ "Defaults to Ubuntu 10.04 LTS.") @utils.arg('--ipgroup', default=None, metavar='', - help="IP group name or ID (see 'osc ipgroup-list').") + help="IP group name or ID (see 'novaclient ipgroup-list').") @utils.arg('--meta', metavar="", action='append', @@ -240,17 +240,17 @@ def do_boot_for_account(cs, args): @utils.arg('--flavor', default=None, metavar='', - help="Flavor ID (see 'osc flavors'). "\ + help="Flavor ID (see 'novaclient flavors'). "\ "Defaults to 256MB RAM instance.") @utils.arg('--image', default=None, metavar='', - help="Image ID (see 'osc images'). "\ + help="Image ID (see 'novaclient images'). "\ "Defaults to Ubuntu 10.04 LTS.") @utils.arg('--ipgroup', default=None, metavar='', - help="IP group name or ID (see 'osc ipgroup-list').") + help="IP group name or ID (see 'novaclient ipgroup-list').") @utils.arg('--meta', metavar="", action='append', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index c4e79f120..d906089aa 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -42,12 +42,12 @@ def _translate_flavor_keys(collection): @utils.arg('--flavor', default=None, metavar='', - help="Flavor ID (see 'osc flavors'). "\ + help="Flavor ID (see 'novaclient flavors'). "\ "Defaults to 256MB RAM instance.") @utils.arg('--image', default=None, metavar='', - help="Image ID (see 'osc images'). "\ + help="Image ID (see 'novaclient images'). "\ "Defaults to Ubuntu 10.04 LTS.") @utils.arg('--meta', metavar="", From ea4b625e7bbf8a2c5581a0879ffe9e7bf7ddb80b Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 4 Aug 2011 10:52:53 -0400 Subject: [PATCH 09/17] Updated --version to default to NOVA_VERSION --- novaclient/shell.py | 4 ++-- tests/test_shell.py | 3 ++- tests/v1_0/test_shell.py | 8 +++----- tests/v1_1/test_shell.py | 6 ++---- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 6cce91979..34e4317f2 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -75,8 +75,8 @@ class OpenStackComputeShell(object): help='Defaults to env[NOVA_URL].') parser.add_argument('--version', - default='1.0', - help='Accepts 1.0 or 1.1, defaults to 1.0') + default=env('NOVA_VERSION'), + help='Accepts 1.0 or 1.1, defaults to env[NOVA_VERSION].') return parser diff --git a/tests/test_shell.py b/tests/test_shell.py index 869f2d822..54f36072b 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -16,7 +16,8 @@ class ShellTest(utils.TestCase): fake_env = { 'NOVA_USERNAME': 'username', 'NOVA_API_KEY': 'password', - 'NOVA_PROJECT_ID': 'project_id' + 'NOVA_PROJECT_ID': 'project_id', + 'NOVA_VERSION': '1.0', } _old_env, os.environ = os.environ, fake_env.copy() diff --git a/tests/v1_0/test_shell.py b/tests/v1_0/test_shell.py index 293a49ebd..d922c5464 100644 --- a/tests/v1_0/test_shell.py +++ b/tests/v1_0/test_shell.py @@ -16,7 +16,8 @@ class ShellTest(utils.TestCase): os.environ = { 'NOVA_USERNAME': 'username', 'NOVA_API_KEY': 'password', - 'NOVA_PROJECT_ID': 'project_id' + 'NOVA_PROJECT_ID': 'project_id', + 'NOVA_VERSION': '1.0', } self.shell = OpenStackComputeShell() @@ -26,10 +27,7 @@ class ShellTest(utils.TestCase): os.environ = self.old_environment def run_command(self, cmd): - command = ['--version=1.0'] - command.extend(cmd.split()) - print command - self.shell.main(command) + self.shell.main(cmd.split()) def assert_called(self, method, url, body=None): return self.shell.cs.assert_called(method, url, body) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index d1249c514..75894712f 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -18,6 +18,7 @@ class ShellTest(utils.TestCase): 'NOVA_USERNAME': 'username', 'NOVA_API_KEY': 'password', 'NOVA_PROJECT_ID': 'project_id', + 'NOVA_VERSION': '1.1', } self.shell = OpenStackComputeShell() @@ -27,10 +28,7 @@ class ShellTest(utils.TestCase): os.environ = self.old_environment def run_command(self, cmd): - command = ['--version=1.1'] - command.extend(cmd.split()) - print command - self.shell.main(command) + self.shell.main(cmd.split()) def assert_called(self, method, url, body=None): return self.shell.cs.assert_called(method, url, body) From 32fd202ab0df89ef881c33c4fa93e5ea2cfdfa9b Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 4 Aug 2011 10:55:08 -0400 Subject: [PATCH 10/17] Updated --version to default to NOVA_VERSION, quick fix. --- novaclient/shell.py | 11 +++++++---- tests/test_shell.py | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index 34e4317f2..c1fa1ecb4 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -86,10 +86,13 @@ class OpenStackComputeShell(object): self.subcommands = {} subparsers = parser.add_subparsers(metavar='') - actions_module = { - '1.0': shell_v1_0, - '1.1': shell_v1_1, - }[version] + try: + actions_module = { + '1.0': shell_v1_0, + '1.1': shell_v1_1, + }[version] + except KeyError: + actions_module = shell_v1_0 self._find_actions(subparsers, actions_module) self._find_actions(subparsers, self) diff --git a/tests/test_shell.py b/tests/test_shell.py index 54f36072b..ce7d76635 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -17,7 +17,6 @@ class ShellTest(utils.TestCase): 'NOVA_USERNAME': 'username', 'NOVA_API_KEY': 'password', 'NOVA_PROJECT_ID': 'project_id', - 'NOVA_VERSION': '1.0', } _old_env, os.environ = os.environ, fake_env.copy() From 5b6b34e0c5b715ed068d8d50a04e69c49617d039 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 4 Aug 2011 12:35:41 -0400 Subject: [PATCH 11/17] Change create-image back to image-create, and increased version to 2.6.0 --- novaclient/v1_1/shell.py | 2 +- setup.py | 6 +++--- tests/v1_1/test_shell.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index d906089aa..875d50e67 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -267,7 +267,7 @@ def do_root_password(cs, args): @utils.arg('server', metavar='', help='Name or ID of server.') @utils.arg('name', metavar='', help='Name of snapshot.') -def do_create_image(cs, args): +def do_image_create(cs, args): """Create a new image by taking a snapshot of a running server.""" server = _find_server(cs, args.server) cs.servers.create_image(server, args.name) diff --git a/setup.py b/setup.py index 6adef8c37..89b80e4b3 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ if sys.version_info < (2,6): setup( name = "python-novaclient", - version = "2.5.8", + version = "2.6.0", description = "Client library for OpenStack Nova API", long_description = read('README.rst'), url = 'https://github.com/rackspace/python-novaclient', @@ -29,10 +29,10 @@ setup( 'Programming Language :: Python', ], install_requires = requirements, - + tests_require = ["nose", "mock"], test_suite = "nose.collector", - + entry_points = { 'console_scripts': ['nova = novaclient.shell:main'] } diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 75894712f..7d0b71b0e 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -154,7 +154,7 @@ class ShellTest(utils.TestCase): self.assert_called('GET', '/images/detail') def test_create_image(self): - self.run_command('create-image sample-server mysnapshot') + self.run_command('image-create sample-server mysnapshot') self.assert_called( 'POST', '/servers/1234/action', {'createImage': {'name': 'mysnapshot', 'metadata': {}}} From e1f374c65cbb3ec7c78e3d22ade84fefb496f3b2 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 4 Aug 2011 12:37:01 -0400 Subject: [PATCH 12/17] Removed unneeded print. --- novaclient/shell.py | 1 - 1 file changed, 1 deletion(-) diff --git a/novaclient/shell.py b/novaclient/shell.py index c1fa1ecb4..9642645d3 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -173,7 +173,6 @@ class OpenStackComputeShell(object): "1.1": shell_v1_1.CLIENT_CLASS, }[version] except KeyError: - print "Bad API version provided, falling back to v1.0." return shell_v1_0.CLIENT_CLASS @utils.arg('command', metavar='', nargs='?', From 008b43facc98f77a193927270902f15848234dc4 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 4 Aug 2011 12:40:25 -0400 Subject: [PATCH 13/17] novaclient -> nova in some documentation as per feedback --- novaclient/v1_0/shell.py | 18 +++++++++--------- novaclient/v1_1/shell.py | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/novaclient/v1_0/shell.py b/novaclient/v1_0/shell.py index 6be6cb228..5731b2c88 100644 --- a/novaclient/v1_0/shell.py +++ b/novaclient/v1_0/shell.py @@ -143,17 +143,17 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): @utils.arg('--flavor', default=None, metavar='', - help="Flavor ID (see 'novaclient flavors'). "\ + help="Flavor ID (see 'nova flavors'). "\ "Defaults to 256MB RAM instance.") @utils.arg('--image', default=None, metavar='', - help="Image ID (see 'novaclient images'). "\ + help="Image ID (see 'nova images'). "\ "Defaults to Ubuntu 10.04 LTS.") @utils.arg('--ipgroup', default=None, metavar='', - help="IP group name or ID (see 'novaclient ipgroup-list').") + help="IP group name or ID (see 'nova ipgroup-list').") @utils.arg('--meta', metavar="", action='append', @@ -191,17 +191,17 @@ def do_boot(cs, args): @utils.arg('--flavor', default=None, metavar='', - help="Flavor ID (see 'novaclient flavors'). "\ + help="Flavor ID (see 'nova flavors'). "\ "Defaults to 256MB RAM instance.") @utils.arg('--image', default=None, metavar='', - help="Image ID (see 'novaclient images'). "\ + help="Image ID (see 'nova images'). "\ "Defaults to Ubuntu 10.04 LTS.") @utils.arg('--ipgroup', default=None, metavar='', - help="IP group name or ID (see 'novaclient ipgroup-list').") + help="IP group name or ID (see 'nova ipgroup-list').") @utils.arg('--meta', metavar="", action='append', @@ -240,17 +240,17 @@ def do_boot_for_account(cs, args): @utils.arg('--flavor', default=None, metavar='', - help="Flavor ID (see 'novaclient flavors'). "\ + help="Flavor ID (see 'nova flavors'). "\ "Defaults to 256MB RAM instance.") @utils.arg('--image', default=None, metavar='', - help="Image ID (see 'novaclient images'). "\ + help="Image ID (see 'nova images'). "\ "Defaults to Ubuntu 10.04 LTS.") @utils.arg('--ipgroup', default=None, metavar='', - help="IP group name or ID (see 'novaclient ipgroup-list').") + help="IP group name or ID (see 'nova ipgroup-list').") @utils.arg('--meta', metavar="", action='append', diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index 875d50e67..fec7f5b66 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -42,12 +42,12 @@ def _translate_flavor_keys(collection): @utils.arg('--flavor', default=None, metavar='', - help="Flavor ID (see 'novaclient flavors'). "\ + help="Flavor ID (see 'nova flavors'). "\ "Defaults to 256MB RAM instance.") @utils.arg('--image', default=None, metavar='', - help="Image ID (see 'novaclient images'). "\ + help="Image ID (see 'nova images'). "\ "Defaults to Ubuntu 10.04 LTS.") @utils.arg('--meta', metavar="", From 6b4e9c5c8b79671d6bdf633d22a61eda0638f01f Mon Sep 17 00:00:00 2001 From: root Date: Thu, 4 Aug 2011 18:39:21 +0000 Subject: [PATCH 14/17] formatting updates --- novaclient/utils.py | 2 -- novaclient/v1_1/servers.py | 33 ++++++------------------- novaclient/v1_1/shell.py | 50 +++++++++++++++++++++++++++++++------- 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/novaclient/utils.py b/novaclient/utils.py index 4397046d0..08fb70caf 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -39,5 +39,3 @@ def print_dict(d): pt.aligns = ['l', 'l'] [pt.add_row(list(r)) for r in d.iteritems()] pt.printt(sortby='Property') - - diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index cd649a4c6..e478ca4fe 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -105,34 +105,17 @@ class Server(base.Resource): self.manager.revert_resize(self) @property - def public_ip(self): + def networks(self): """ - Shortcut to get this server's primary public IP address. + Generate a simplified list of addresses """ + networks = {} try: - public_addresses = self.addresses["public"] - except KeyError: - public_addresses = None - - if public_addresses is None or len(public_addresses) == 0: - return "" - else: - return public_addresses - - @property - def private_ip(self): - """ - Shortcut to get this server's primary private IP address. - """ - try: - private_addresses = self.addresses["private"] - except KeyError: - private_addresses = None - - if private_addresses is None or len(private_addresses) == 0: - return "" - else: - return private_addresses + for network_label, address_list in self.addresses.items(): + networks[network_label] = [a['addr'] for a in address_list] + return networks + except Exception: + return {} class ServerManager(local_base.BootingManagerWithFind): diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index d906089aa..48ecbf44d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -75,7 +75,20 @@ def do_boot(cs, args): name, image, flavor, metadata, files = _boot(cs, args) server = cs.servers.create(args.name, image, flavor, meta=metadata, files=files) - utils.print_dict(server._info) + info = server._info + + flavor = info.get('flavor', {}) + flavor_id = flavor.get('id', '') + info['flavor'] = _find_flavor(cs, flavor_id).name + + image = info.get('image', {}) + image_id = image.get('id', '') + info['image'] = _find_image(cs, image_id).name + + info.pop('links', None) + info.pop('addresses', None) + + utils.print_dict(info) def _boot(cs, args): @@ -203,12 +216,28 @@ def do_list(cs, args): 'name': args.name, 'server_name': args.server_name, 'display_name': args.display_name} + if recurse_zones: - to_print = ['UUID', 'Name', 'Status', 'Public IP', 'Private IP'] + id_col = 'UUID' else: - to_print = ['ID', 'Name', 'Status', 'Public IP', 'Private IP'] - utils.print_list(cs.servers.list(search_opts=search_opts), - to_print) + id_col = 'ID' + + columns = [id_col, 'Name', 'Status', 'Networks'] + formatters = {'Networks': _format_servers_list_networks} + utils.print_list(cs.servers.list(search_opts=search_opts), columns, formatters) + + +def _format_servers_list_networks(server): + output = [] + for (network, addresses) in server.networks.items(): + if len(addresses) == 0: + continue + addresses_csv = ', '.join(addresses) + group = "%s=%s" % (network, addresses_csv) + output.append(group) + + return '; '.join(output) + @utils.arg('--hard', dest='reboot_type', @@ -277,11 +306,11 @@ def do_show(cs, args): """Show details about the given server.""" s = _find_server(cs, args.server) + networks = s.networks + info = s._info.copy() - addresses = info.pop('addresses', []) - for addrtype in addresses: - ips = map(lambda x: x['addr'], addresses[addrtype]) - info['%s ip' % addrtype] = ', '.join(ips) + for network_label, address_list in networks.items(): + info['%s network' % network_label] = ', '.join(address_list) flavor = info.get('flavor', {}) flavor_id = flavor.get('id', '') @@ -291,6 +320,9 @@ def do_show(cs, args): image_id = image.get('id', '') info['image'] = _find_image(cs, image_id).name + info.pop('links', None) + info.pop('addresses', None) + utils.print_dict(info) @utils.arg('server', metavar='', help='Name or ID of server.') From 46a2d5a3d905df8c9a5597ea59d748faaf247001 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 4 Aug 2011 15:53:30 -0400 Subject: [PATCH 15/17] Fix for failing tests because boot response now requests additional information. --- novaclient/v1_1/shell.py | 6 +++--- tests/v1_1/test_shell.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index a9e6dd5da..b29b85c2d 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -85,9 +85,9 @@ def do_boot(cs, args): image_id = image.get('id', '') info['image'] = _find_image(cs, image_id).name - info.pop('links', None) + info.pop('links', None) info.pop('addresses', None) - + utils.print_dict(info) @@ -320,7 +320,7 @@ def do_show(cs, args): image_id = image.get('id', '') info['image'] = _find_image(cs, image_id).name - info.pop('links', None) + info.pop('links', None) info.pop('addresses', None) utils.print_dict(info) diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 7d0b71b0e..4e79f112b 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -38,7 +38,7 @@ class ShellTest(utils.TestCase): def test_boot(self): self.run_command('boot --image 1 some-server') - self.assert_called( + self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': 1, @@ -50,7 +50,7 @@ class ShellTest(utils.TestCase): ) self.run_command('boot --image 1 --meta foo=bar --meta spam=eggs some-server ') - self.assert_called( + self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': 1, @@ -68,7 +68,7 @@ class ShellTest(utils.TestCase): cmd = 'boot some-server --image 1 --file /tmp/foo=%s --file /tmp/bar=%s' self.run_command(cmd % (testfile, testfile)) - self.assert_called( + self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': 1, @@ -97,7 +97,7 @@ class ShellTest(utils.TestCase): @mock.patch('__builtin__.open', mock_open) def test_shell_call(): self.run_command('boot some-server --image 1 --key') - self.assert_called( + self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': 1, @@ -127,7 +127,7 @@ class ShellTest(utils.TestCase): testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') expected_file_data = open(testfile).read().encode('base64') self.run_command('boot some-server --image 1 --key %s' % testfile) - self.assert_called( + self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': 1, From cc7156933fa90d98cd18c6e2f928571ae3a32cd3 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 4 Aug 2011 17:07:22 -0400 Subject: [PATCH 16/17] Merged v1.0 functionality into v1.1 so we don't lose any features by...upgrading? --- novaclient/v1_1/base.py | 19 ++- novaclient/v1_1/client.py | 2 + novaclient/v1_1/servers.py | 187 +++++++++++++++++++--- novaclient/v1_1/shell.py | 317 +++++++++++++++++++++++++++++-------- tests/fakes.py | 19 ++- tests/v1_1/fakes.py | 52 ++++++ tests/v1_1/test_servers.py | 21 +++ tests/v1_1/test_shell.py | 27 ++-- tests/v1_1/test_zones.py | 76 +++++++++ 9 files changed, 614 insertions(+), 106 deletions(-) create mode 100644 tests/v1_1/test_zones.py diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py index 9113d53bb..9762f73eb 100644 --- a/novaclient/v1_1/base.py +++ b/novaclient/v1_1/base.py @@ -128,7 +128,9 @@ class ManagerWithFind(Manager): class BootingManagerWithFind(ManagerWithFind): """Like a `ManagerWithFind`, but has the ability to boot servers.""" def _boot(self, resource_url, response_key, name, image, flavor, - meta=None, files=None, return_raw=False): + meta=None, files=None, zone_blob=None, + reservation_id=None, return_raw=False, min_count=None, + max_count=None): """ Create (boot) a new server. @@ -143,6 +145,10 @@ class BootingManagerWithFind(ManagerWithFind): 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. + :param zone_blob: a single (encrypted) string which is used internally + by Nova for routing between Zones. Users cannot populate + this field. + :param reservation_id: a UUID for the set of servers being requested. :param return_raw: If True, don't try to coearse the result into a Resource object. """ @@ -153,6 +159,17 @@ class BootingManagerWithFind(ManagerWithFind): }} if meta: body["server"]["metadata"] = meta + if reservation_id: + body["server"]["reservation_id"] = reservation_id + if zone_blob: + body["server"]["zone_blob"] = zone_blob + + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + body["server"]["min_count"] = min_count + body["server"]["max_count"] = max_count # 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 diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py index ee7195896..fbbc62c0e 100644 --- a/novaclient/v1_1/client.py +++ b/novaclient/v1_1/client.py @@ -4,6 +4,7 @@ from novaclient import client from novaclient.v1_1 import flavors from novaclient.v1_1 import images from novaclient.v1_1 import servers +from novaclient.v1_1 import zones @@ -28,6 +29,7 @@ class Client(object): self.flavors = flavors.FlavorManager(self) self.images = images.ImageManager(self) self.servers = servers.ServerManager(self) + self.zones = zones.ZoneManager(self) self.client = client.HTTPClient(username, api_key, diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index e478ca4fe..4ab7f5bc1 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -47,6 +47,72 @@ class Server(base.Resource): """ self.manager.update(self, name=name) + def add_fixed_ip(self, network_id): + """ + Add an IP address on a network. + + :param network_id: The ID of the network the IP should be on. + """ + self.manager.add_fixed_ip(self, network_id) + + def pause(self): + """ + Pause -- Pause the running server. + """ + self.manager.pause(self) + + def unpause(self): + """ + Unpause -- Unpause the paused server. + """ + self.manager.unpause(self) + + def suspend(self): + """ + Suspend -- Suspend the running server. + """ + self.manager.suspend(self) + + def resume(self): + """ + Resume -- Resume the suspended server. + """ + self.manager.resume(self) + + def rescue(self): + """ + Rescue -- Rescue the problematic server. + """ + self.manager.rescue(self) + + def unrescue(self): + """ + Unrescue -- Unrescue the rescued server. + """ + self.manager.unrescue(self) + + def diagnostics(self): + """Diagnostics -- Retrieve server diagnostics.""" + self.manager.diagnostics(self) + + def actions(self): + """Actions -- Retrieve server actions.""" + self.manager.actions(self) + + def migrate(self): + """ + Migrate a server to a new host in the same zone. + """ + self.manager.migrate(self) + + def remove_fixed_ip(self, address): + """ + Remove an IP address. + + :param address: The IP address to remove. + """ + self.manager.remove_fixed_ip(self, address) + def change_password(self, password): """ Update the password for a server. @@ -113,7 +179,7 @@ class Server(base.Resource): try: for network_label, address_list in self.addresses.items(): networks[network_label] = [a['addr'] for a in address_list] - return networks + return networks except Exception: return {} @@ -155,7 +221,78 @@ class ServerManager(local_base.BootingManagerWithFind): detail = "/detail" return self._list("/servers%s%s" % (detail, query_string), "servers") - def create(self, name, image, flavor, meta=None, files=None): + def add_fixed_ip(self, server, network_id): + """ + Add an IP address on a network. + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param network_id: The ID of the network the IP should be on. + """ + self._action('addFixedIp', server, {'networkId': network_id}) + + def remove_fixed_ip(self, server, address): + """ + Remove an IP address. + + :param server: The :class:`Server` (or its ID) to add an IP to. + :param address: The IP address to remove. + """ + self._action('removeFixedIp', server, {'address': address}) + + def pause(self, server): + """ + Pause the server. + """ + self.api.client.post('/servers/%s/pause' % base.getid(server), body={}) + + def unpause(self, server): + """ + Unpause the server. + """ + self.api.client.post('/servers/%s/unpause' % base.getid(server), + body={}) + + def suspend(self, server): + """ + Suspend the server. + """ + self.api.client.post('/servers/%s/suspend' % base.getid(server), + body={}) + + def resume(self, server): + """ + Resume the server. + """ + self.api.client.post('/servers/%s/resume' % base.getid(server), + body={}) + + def rescue(self, server): + """ + Rescue the server. + """ + self.api.client.post('/servers/%s/rescue' % base.getid(server), + body={}) + + def unrescue(self, server): + """ + Unrescue the server. + """ + self.api.client.post('/servers/%s/unrescue' % base.getid(server), + body={}) + + def diagnostics(self, server): + """Retrieve server diagnostics.""" + return self.api.client.get("/servers/%s/diagnostics" % + base.getid(server)) + + def actions(self, server): + """Retrieve server actions.""" + return self._list("/servers/%s/actions" % base.getid(server), + "actions") + + def create(self, name, image, flavor, meta=None, files=None, + zone_blob=None, reservation_id=None, min_count=None, + max_count=None): """ Create (boot) a new server. @@ -170,31 +307,21 @@ class ServerManager(local_base.BootingManagerWithFind): 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. + :param zone_blob: a single (encrypted) string which is used internally + by Nova for routing between Zones. Users cannot populate + this field. + :param reservation_id: a UUID for the set of servers being requested. """ - personality = [] - - for file_path, filelike in files.items(): - try: - data = filelike.read() - except AttributeError: - data = str(filelike) - - personality.append({ - "path": file_path, - "contents": data.encode("base64"), - }) - - body = { - "server": { - "name": name, - "imageRef": base.getid(image), - "flavorRef": base.getid(flavor), - "metadata": meta or {}, - "personality": personality, - }, - } - - return self._create("/servers", body, "server", return_raw=False) + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + if min_count > max_count: + min_count = max_count + return self._boot("/servers", "server", name, image, flavor, + meta=meta, files=files, + zone_blob=zone_blob, reservation_id=reservation_id, + min_count=min_count, max_count=max_count) def update(self, server, name=None): """ @@ -245,6 +372,14 @@ class ServerManager(local_base.BootingManagerWithFind): """ self._action('rebuild', server, {'imageRef': base.getid(image)}) + def migrate(self, server): + """ + Migrate a server to a new host in the same zone. + + :param server: The :class:`Server` (or its ID). + """ + self._action('migrate', server) + def resize(self, server, flavor): """ Resize a server's resources. diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index b29b85c2d..dfa26a14e 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -31,68 +31,16 @@ CLIENT_CLASS = client.Client AUTO_KEY = object() -def _translate_flavor_keys(collection): - convert = [('ram', 'memory_mb'), ('disk', 'local_gb')] - for item in collection: - keys = item.__dict__.keys() - 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]) - -@utils.arg('--flavor', - default=None, - metavar='', - help="Flavor ID (see 'nova flavors'). "\ - "Defaults to 256MB RAM instance.") -@utils.arg('--image', - default=None, - metavar='', - help="Image ID (see 'nova images'). "\ - "Defaults to Ubuntu 10.04 LTS.") -@utils.arg('--meta', - metavar="", - action='append', - default=[], - help="Record arbitrary key/value metadata. "\ - "May be give multiple times.") -@utils.arg('--file', - metavar="", - action='append', - dest='files', - default=[], - help="Store arbitrary files from locally to "\ - "on the new server. You may store up to 5 files.") -@utils.arg('--key', - metavar='', - nargs='?', - const=AUTO_KEY, - help="Key the server with an SSH keypair. "\ - "Looks in ~/.ssh for a key, "\ - "or takes an explicit to one.") -@utils.arg('name', metavar='', help='Name for the new server') -def do_boot(cs, args): - """Boot a new server.""" - name, image, flavor, metadata, files = _boot(cs, args) - server = cs.servers.create(args.name, image, flavor, - meta=metadata, files=files) - info = server._info - - flavor = info.get('flavor', {}) - flavor_id = flavor.get('id', '') - info['flavor'] = _find_flavor(cs, flavor_id).name - - image = info.get('image', {}) - image_id = image.get('id', '') - info['image'] = _find_image(cs, image_id).name - - info.pop('links', None) - info.pop('addresses', None) - - utils.print_dict(info) - - -def _boot(cs, args): +def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): """Boot a new server.""" + if min_count is None: + min_count = 1 + if max_count is None: + max_count = min_count + if min_count > max_count: + raise exceptions.CommandError("min_instances should be <= max_instances") + if not min_count or not max_count: + raise exceptions.CommandError("min_instances nor max_instances should be 0") flavor = args.flavor or cs.flavors.find(ram=256) image = args.image or cs.images.find(name="Ubuntu 10.04 LTS "\ @@ -129,7 +77,129 @@ def _boot(cs, args): except IOError, e: raise exceptions.CommandError("Can't open '%s': %s" % (keyfile, e)) - return (args.name, image, flavor, metadata, files) + return (args.name, image, flavor, metadata, files, + reservation_id, min_count, max_count) + +@utils.arg('--flavor', + default=None, + metavar='', + help="Flavor ID (see 'nova flavors'). "\ + "Defaults to 256MB RAM instance.") +@utils.arg('--image', + default=None, + metavar='', + help="Image ID (see 'nova images'). "\ + "Defaults to Ubuntu 10.04 LTS.") +@utils.arg('--meta', + metavar="", + action='append', + default=[], + help="Record arbitrary key/value metadata. "\ + "May be give multiple times.") +@utils.arg('--file', + metavar="", + action='append', + dest='files', + default=[], + help="Store arbitrary files from locally to "\ + "on the new server. You may store up to 5 files.") +@utils.arg('--key', + metavar='', + nargs='?', + const=AUTO_KEY, + help="Key the server with an SSH keypair. "\ + "Looks in ~/.ssh for a key, "\ + "or takes an explicit to one.") +@utils.arg('name', metavar='', help='Name for the new server') +def do_boot(cs, args): + """Boot a new server.""" + name, image, flavor, metadata, files, reservation_id, \ + min_count, max_count = _boot(cs, args) + + server = cs.servers.create(args.name, image, flavor, + meta=metadata, + files=files, + min_count=min_count, + max_count=max_count) + utils.print_dict(server._info) + + +@utils.arg('--flavor', + default=None, + metavar='', + help="Flavor ID (see 'nova flavors'). "\ + "Defaults to 256MB RAM instance.") +@utils.arg('--image', + default=None, + metavar='', + help="Image ID (see 'nova images'). "\ + "Defaults to Ubuntu 10.04 LTS.") +@utils.arg('--meta', + metavar="", + action='append', + default=[], + help="Record arbitrary key/value metadata. "\ + "May be give multiple times.") +@utils.arg('--file', + metavar="", + action='append', + dest='files', + default=[], + help="Store arbitrary files from locally to "\ + "on the new server. You may store up to 5 files.") +@utils.arg('--key', + metavar='', + nargs='?', + const=AUTO_KEY, + help="Key the server with an SSH keypair. "\ + "Looks in ~/.ssh for a key, "\ + "or takes an explicit to one.") +@utils.arg('--reservation_id', + default=None, + metavar='', + help="Reservation ID (a UUID). "\ + "If unspecified will be generated by the server.") +@utils.arg('--min_instances', + default=None, + type=int, + metavar='', + help="The minimum number of instances to build. "\ + "Defaults to 1.") +@utils.arg('--max_instances', + default=None, + type=int, + metavar='', + help="The maximum number of instances to build. "\ + "Defaults to 'min_instances' setting.") +@utils.arg('name', metavar='', help='Name for the new server') +def do_zone_boot(cs, args): + """Boot a new server, potentially across Zones.""" + reservation_id = args.reservation_id + min_count = args.min_instances + max_count = args.max_instances + name, image, flavor, metadata, \ + files, reservation_id, min_count, max_count = \ + _boot(cs, args, + reservation_id=reservation_id, + min_count=min_count, + max_count=max_count) + + reservation_id = cs.zones.boot(args.name, image, flavor, + meta=metadata, + files=files, + reservation_id=reservation_id, + min_count=min_count, + max_count=max_count) + print "Reservation ID=", reservation_id + + +def _translate_flavor_keys(collection): + convert = [('ram', 'memory_mb'), ('disk', 'local_gb')] + for item in collection: + keys = item.__dict__.keys() + 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(cs, args): @@ -282,6 +352,55 @@ def do_resize_revert(cs, args): """Revert a previous resize (and return to the previous VM).""" _find_server(cs, args.server).revert_resize() +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_migrate(cs, args): + """Migrate a server.""" + _find_server(cs, args.server).migrate() + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_pause(cs, args): + """Pause a server.""" + _find_server(cs, args.server).pause() + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_unpause(cs, args): + """Unpause a server.""" + _find_server(cs, args.server).unpause() + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_suspend(cs, args): + """Suspend a server.""" + _find_server(cs, args.server).suspend() + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_resume(cs, args): + """Resume a server.""" + _find_server(cs, args.server).resume() + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_rescue(cs, args): + """Rescue a server.""" + _find_server(cs, args.server).rescue() + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_unrescue(cs, args): + """Unrescue a server.""" + _find_server(cs, args.server).unrescue() + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_diagnostics(cs, args): + """Retrieve server diagnostics.""" + utils.print_dict(cs.servers.diagnostics(args.server)[1]) + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_actions(cs, args): + """Retrieve server actions.""" + utils.print_list( + cs.servers.actions(args.server), + ["Created_At", "Action", "Error"]) + + + @utils.arg('server', metavar='', help='Name or ID of server.') def do_root_password(cs, args): """ @@ -360,3 +479,77 @@ def _find_resource(manager, name_or_id): raise exceptions.CommandError("No %s with a name or ID of '%s' exists." % (manager.resource_class.__name__.lower(), name_or_id)) +# --zone_username is required since --username is already used. +@utils.arg('zone', metavar='', help='ID of the zone', default=None) +@utils.arg('--api_url', dest='api_url', default=None, help='New URL.') +@utils.arg('--zone_username', dest='zone_username', default=None, + help='New zone username.') +@utils.arg('--password', dest='password', default=None, help='New password.') +@utils.arg('--weight_offset', dest='weight_offset', default=None, + help='Child Zone weight offset.') +@utils.arg('--weight_scale', dest='weight_scale', default=None, + help='Child Zone weight scale.') +def do_zone(cs, args): + """Show or edit a child zone. No zone arg for this zone.""" + zone = cs.zones.get(args.zone) + + # If we have some flags, update the zone + zone_delta = {} + if args.api_url: + zone_delta['api_url'] = args.api_url + if args.zone_username: + zone_delta['username'] = args.zone_username + if args.password: + zone_delta['password'] = args.password + if args.weight_offset: + zone_delta['weight_offset'] = args.weight_offset + if args.weight_scale: + zone_delta['weight_scale'] = args.weight_scale + if zone_delta: + zone.update(**zone_delta) + else: + utils.print_dict(zone._info) + +def do_zone_info(cs, args): + """Get this zones name and capabilities.""" + zone = cs.zones.info() + utils.print_dict(zone._info) + +@utils.arg('api_url', metavar='', help="URL for the Zone's API") +@utils.arg('zone_username', metavar='', + help='Authentication username.') +@utils.arg('password', metavar='', help='Authentication password.') +@utils.arg('weight_offset', metavar='', + help='Child Zone weight offset (typically 0.0).') +@utils.arg('weight_scale', metavar='', + help='Child Zone weight scale (typically 1.0).') +def do_zone_add(cs, args): + """Add a new child zone.""" + zone = cs.zones.create(args.api_url, args.zone_username, + args.password, args.weight_offset, + args.weight_scale) + utils.print_dict(zone._info) + +@utils.arg('zone', metavar='', help='Name or ID of the zone') +def do_zone_delete(cs, args): + """Delete a zone.""" + cs.zones.delete(args.zone) + +def do_zone_list(cs, args): + """List the children of a zone.""" + utils.print_list(cs.zones.list(), ['ID', 'Name', 'Is Active', \ + 'API URL', 'Weight Offset', 'Weight Scale']) + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('network_id', metavar='', help='Network ID.') +def do_add_fixed_ip(cs, args): + """Add new IP address to network.""" + server = _find_server(cs, args.server) + server.add_fixed_ip(args.network_id) + +@utils.arg('server', metavar='', help='Name or ID of server.') +@utils.arg('address', metavar='
', help='IP Address.') +def do_remove_fixed_ip(cs, args): + """Remove an IP address from a server.""" + server = _find_server(cs, args.server) + server.remove_fixed_ip(args.address) diff --git a/tests/fakes.py b/tests/fakes.py index b63cc8087..93709b8ce 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -12,11 +12,12 @@ import novaclient.client def assert_has_keys(dict, required=[], optional=[]): keys = dict.keys() for k in required: - assert k in keys - allowed_keys = set(required) | set(optional) - extra_keys = set(keys).difference(set(required + optional)) - if extra_keys: - raise AssertionError("found unexpected keys: %s" % list(extra_keys)) + try: + assert k in keys + except AssertionError: + allowed_keys = set(required) | set(optional) + extra_keys = set(keys).difference(set(required + optional)) + raise AssertionError("found unexpected keys: %s" % list(extra_keys)) class FakeClient(object): @@ -58,7 +59,13 @@ class FakeClient(object): assert found, 'Expected %s %s; got %s' % \ (expected, self.client.callstack) if body is not None: - assert entry[2] == body + try: + assert entry[2] == body + except AssertionError: + print entry[2] + print "!=" + print body + raise self.client.callstack = [] diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py index c13fe695f..3985a7843 100644 --- a/tests/v1_1/fakes.py +++ b/tests/v1_1/fakes.py @@ -247,6 +247,12 @@ class FakeHTTPClient(base_client.HTTPClient): return (204, None) elif action == 'revertResize': assert body[action] is None + elif action == 'migrate': + assert body[action] is None + elif action == 'addFixedIp': + assert body[action].keys() == ['networkId'] + elif action == 'removeFixedIp': + assert body[action].keys() == ['address'] elif action == 'createImage': assert set(body[action].keys()) == set(['name', 'metadata']) elif action == 'changePassword': @@ -319,3 +325,49 @@ class FakeHTTPClient(base_client.HTTPClient): def delete_images_1(self, **kw): return (204, None) + + # + # Zones + # + def get_zones(self, **kw): + return (200, {'zones': [ + {'id': 1, 'api_url': 'http://foo.com', 'username': 'bob'}, + {'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', + 'password': 'qwerty'}, + {'id': 2, 'api_url': 'http://foo.com', 'username': 'alice', + 'password': 'password'} + ]}) + + def get_zones_1(self, **kw): + r = {'zone': self.get_zones_detail()[1]['zones'][0]} + return (200, r) + + def get_zones_2(self, **kw): + r = {'zone': self.get_zones_detail()[1]['zones'][1]} + return (200, r) + + def post_zones(self, body, **kw): + assert body.keys() == ['zone'] + fakes.assert_has_keys(body['zone'], + required=['api_url', 'username', 'password'], + optional=['weight_offset', 'weight_scale']) + + return (202, self.get_zones_1()[1]) + + def put_zones_1(self, body, **kw): + assert body.keys() == ['zone'] + fakes.assert_has_keys(body['zone'], optional=['api_url', 'username', + 'password', + 'weight_offset', + 'weight_scale']) + return (204, None) + + def delete_zones_1(self, **kw): + return (202, None) + + diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py index 5a7ff73ac..74192965f 100644 --- a/tests/v1_1/test_servers.py +++ b/tests/v1_1/test_servers.py @@ -112,3 +112,24 @@ class ServersTest(utils.TestCase): cs.assert_called('POST', '/servers/1234/action') cs.servers.revert_resize(s) cs.assert_called('POST', '/servers/1234/action') + + def test_migrate_server(self): + s = cs.servers.get(1234) + s.migrate() + cs.assert_called('POST', '/servers/1234/action') + cs.servers.migrate(s) + cs.assert_called('POST', '/servers/1234/action') + + def test_add_fixed_ip(self): + s = cs.servers.get(1234) + s.add_fixed_ip(1) + cs.assert_called('POST', '/servers/1234/action') + cs.servers.add_fixed_ip(s, 1) + cs.assert_called('POST', '/servers/1234/action') + + def test_remove_fixed_ip(self): + s = cs.servers.get(1234) + s.remove_fixed_ip('10.0.0.1') + cs.assert_called('POST', '/servers/1234/action') + cs.servers.remove_fixed_ip(s, '10.0.0.1') + cs.assert_called('POST', '/servers/1234/action') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 4e79f112b..870df1fff 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -44,8 +44,8 @@ class ShellTest(utils.TestCase): 'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', - 'personality': [], - 'metadata': {}, + 'min_count': 1, + 'max_count': 1, }} ) @@ -57,7 +57,8 @@ class ShellTest(utils.TestCase): 'name': 'some-server', 'imageRef': '1', 'metadata': {'foo': 'bar', 'spam': 'eggs'}, - 'personality': [], + 'min_count': 1, + 'max_count': 1, }} ) @@ -74,7 +75,8 @@ class ShellTest(utils.TestCase): 'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', - 'metadata': {}, + 'min_count': 1, + 'max_count': 1, 'personality': [ {'path': '/tmp/bar', 'contents': expected_file_data}, {'path': '/tmp/foo', 'contents': expected_file_data} @@ -103,7 +105,8 @@ class ShellTest(utils.TestCase): 'flavorRef': 1, 'name': 'some-server', 'imageRef': '1', - 'metadata': {}, + 'min_count': 1, + 'max_count': 1, 'personality': [{ 'path': '/root/.ssh/authorized_keys2', 'contents': ('SSHKEY').encode('base64')}, @@ -131,12 +134,14 @@ class ShellTest(utils.TestCase): 'POST', '/servers', {'server': { 'flavorRef': 1, - 'name': 'some-server', 'imageRef': '1', - 'metadata': {}, - 'personality': [ - {'path': '/root/.ssh/authorized_keys2', - 'contents':expected_file_data}, - ]} + 'name': 'some-server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 1, + 'personality': [ + {'path': '/root/.ssh/authorized_keys2', + 'contents':expected_file_data}, + ]} } ) diff --git a/tests/v1_1/test_zones.py b/tests/v1_1/test_zones.py new file mode 100644 index 000000000..a10382c67 --- /dev/null +++ b/tests/v1_1/test_zones.py @@ -0,0 +1,76 @@ + +import StringIO + +from novaclient.v1_1 import zones +from tests.v1_1 import fakes +from tests import utils + + +os = fakes.FakeClient() + + +class ZonesTest(utils.TestCase): + + def test_list_zones(self): + sl = os.zones.list() + os.assert_called('GET', '/zones/detail') + [self.assertTrue(isinstance(s, zones.Zone)) for s in sl] + + def test_list_zones_undetailed(self): + sl = os.zones.list(detailed=False) + os.assert_called('GET', '/zones') + [self.assertTrue(isinstance(s, zones.Zone)) for s in sl] + + def test_get_zone_details(self): + s = os.zones.get(1) + os.assert_called('GET', '/zones/1') + self.assertTrue(isinstance(s, zones.Zone)) + self.assertEqual(s.id, 1) + self.assertEqual(s.api_url, 'http://foo.com') + + def test_create_zone(self): + s = os.zones.create(api_url="http://foo.com", username='bob', + password='xxx') + os.assert_called('POST', '/zones') + self.assertTrue(isinstance(s, zones.Zone)) + + def test_update_zone(self): + s = os.zones.get(1) + + # Update via instance + s.update(api_url='http://blah.com') + os.assert_called('PUT', '/zones/1') + s.update(api_url='http://blah.com', username='alice', password='xxx') + os.assert_called('PUT', '/zones/1') + + # Silly, but not an error + s.update() + + # 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.assert_called('PUT', '/zones/1') + os.zones.update(s, api_url='http://blah.com', username='fred', + password='zip') + os.assert_called('PUT', '/zones/1') + + def test_delete_zone(self): + s = os.zones.get(1) + s.delete() + os.assert_called('DELETE', '/zones/1') + os.zones.delete(1) + os.assert_called('DELETE', '/zones/1') + os.zones.delete(s) + os.assert_called('DELETE', '/zones/1') + + def test_find_zone(self): + s = os.zones.find(password='qwerty') + os.assert_called('GET', '/zones/detail') + self.assertEqual(s.username, 'bob') + + # Find with multiple results returns the first item + s = os.zones.find(api_url='http://foo.com') + sl = os.zones.findall(api_url='http://foo.com') + self.assertEqual(sl[0], s) + self.assertEqual([s.id for s in sl], [1, 2]) From 6fa86a22409ab73911168b2a0aa70f9adb583abf Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 4 Aug 2011 17:52:12 -0400 Subject: [PATCH 17/17] Accidently had a reference to ipgroup still. --- novaclient/v1_1/zones.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/novaclient/v1_1/zones.py b/novaclient/v1_1/zones.py index 4e8baa3ef..df65175d4 100644 --- a/novaclient/v1_1/zones.py +++ b/novaclient/v1_1/zones.py @@ -115,7 +115,7 @@ class ZoneManager(base.BootingManagerWithFind): return self._create("/zones", body, "zone") - def boot(self, name, image, flavor, ipgroup=None, meta=None, files=None, + def boot(self, name, image, flavor, meta=None, files=None, zone_blob=None, reservation_id=None, min_count=None, max_count=None): """ @@ -124,7 +124,6 @@ class ZoneManager(base.BootingManagerWithFind): :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. @@ -145,7 +144,7 @@ class ZoneManager(base.BootingManagerWithFind): if not max_count: max_count = min_count return self._boot("/zones/boot", "reservation_id", name, image, flavor, - ipgroup=ipgroup, meta=meta, files=files, + meta=meta, files=files, zone_blob=zone_blob, reservation_id=reservation_id, return_raw=True, min_count=min_count, max_count=max_count)