summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--novaclient/__init__.py70
-rw-r--r--novaclient/v1_0/__init__.py74
-rw-r--r--novaclient/v1_0/accounts.py (renamed from novaclient/accounts.py)3
-rw-r--r--novaclient/v1_0/backup_schedules.py (renamed from novaclient/backup_schedules.py)2
-rw-r--r--novaclient/v1_0/base.py (renamed from novaclient/base.py)6
-rw-r--r--novaclient/v1_0/client.py (renamed from novaclient/client.py)12
-rw-r--r--novaclient/v1_0/exceptions.py (renamed from novaclient/exceptions.py)0
-rw-r--r--novaclient/v1_0/flavors.py (renamed from novaclient/flavors.py)3
-rw-r--r--novaclient/v1_0/images.py (renamed from novaclient/images.py)4
-rw-r--r--novaclient/v1_0/ipgroups.py (renamed from novaclient/ipgroups.py)2
-rw-r--r--novaclient/v1_0/servers.py (renamed from novaclient/servers.py)4
-rw-r--r--novaclient/v1_0/shell.py (renamed from novaclient/shell.py)44
-rw-r--r--novaclient/v1_0/zones.py (renamed from novaclient/zones.py)4
-rw-r--r--novaclient/v1_1/__init__.py66
-rw-r--r--novaclient/v1_1/base.py214
-rw-r--r--novaclient/v1_1/client.py154
-rw-r--r--novaclient/v1_1/exceptions.py100
-rw-r--r--novaclient/v1_1/flavors.py41
-rw-r--r--novaclient/v1_1/images.py86
-rw-r--r--novaclient/v1_1/servers.py300
-rw-r--r--novaclient/v1_1/shell.py584
-rw-r--r--novaclient/v1_1/zones.py196
-rw-r--r--tests/test_backup_schedules.py58
-rw-r--r--tests/test_flavors.py37
-rw-r--r--tests/test_images.py47
-rw-r--r--tests/test_ipgroups.py48
-rw-r--r--tests/test_servers.py174
-rw-r--r--tests/v1_0/__init__.py0
-rw-r--r--tests/v1_0/fakes.py (renamed from tests/fakeserver.py)30
-rw-r--r--tests/v1_0/test_accounts.py (renamed from tests/test_accounts.py)13
-rw-r--r--tests/v1_0/test_auth.py (renamed from tests/test_auth.py)14
-rw-r--r--tests/v1_0/test_backup_schedules.py60
-rw-r--r--tests/v1_0/test_base.py (renamed from tests/test_base.py)42
-rw-r--r--tests/v1_0/test_client.py (renamed from tests/test_client.py)11
-rw-r--r--tests/v1_0/test_flavors.py42
-rw-r--r--tests/v1_0/test_images.py51
-rw-r--r--tests/v1_0/test_ipgroups.py52
-rw-r--r--tests/v1_0/test_servers.py180
-rw-r--r--tests/v1_0/test_shell.py (renamed from tests/test_shell.py)15
-rw-r--r--tests/v1_0/test_zones.py (renamed from tests/test_zones.py)21
-rw-r--r--tests/v1_0/testfile.txt (renamed from tests/testfile.txt)0
-rw-r--r--tests/v1_0/utils.py (renamed from tests/utils.py)0
-rw-r--r--tests/v1_1/__init__.py0
-rw-r--r--tests/v1_1/fakes.py496
-rw-r--r--tests/v1_1/test_base.py61
-rw-r--r--tests/v1_1/test_client.py52
-rw-r--r--tests/v1_1/test_flavors.py42
-rw-r--r--tests/v1_1/test_images.py51
-rw-r--r--tests/v1_1/test_servers.py125
-rw-r--r--tests/v1_1/test_shell.py234
-rw-r--r--tests/v1_1/testfile.txt1
-rw-r--r--tests/v1_1/utils.py29
53 files changed, 3423 insertions, 537 deletions
diff --git a/.gitignore b/.gitignore
index b32d8aa7..355cfbd2 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 a0807c71..08bd9d9a 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 00000000..7b2c9737
--- /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
index 01ff2f52..264ce843 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
index 662e3148..78f4d49f 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
index e402039f..3dbec636 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
index 103679a4..202f6da5 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
index 1709d806..1709d806 100644
--- a/novaclient/exceptions.py
+++ b/novaclient/v1_0/exceptions.py
diff --git a/novaclient/flavors.py b/novaclient/v1_0/flavors.py
index bfede1e1..4dc4ac99 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
index d911cc74..ec36fe34 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
index 86cd3cb4..66821ee7 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
index 18392e4d..a2b014f0 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
index b420e1c3..b4dcef2d 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='<server>', 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='<api_url>', help="URL for the Zone's API")
- @arg('zone_username', metavar='<zone_username>',
+ @arg('zone_username', metavar='<zone_username>',
help='Authentication username.')
@arg('password', metavar='<password>', help='Authentication password.')
@arg('weight_offset', metavar='<weight_offset>',
@@ -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='<server>', help='Name or ID of server.')
@arg('address', metavar='<address>', 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
index 5bcee550..f711ec21 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 "<Weighting: %s>" % self.name
diff --git a/novaclient/v1_1/__init__.py b/novaclient/v1_1/__init__.py
new file mode 100644
index 00000000..955b27e5
--- /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 00000000..f283d859
--- /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 00000000..a805826d
--- /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 00000000..1709d806
--- /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 00000000..f4a82a96
--- /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 "<Flavor: %s>" % 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 00000000..48d86ac8
--- /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 "<Image: %s>" % 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 00000000..fbba4ae1
--- /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 "<Server: %s>" % 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 00000000..7f9217e0
--- /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='<subcommand>')
+ 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='<subcommand>', nargs='?',
+ help='Display help for <subcommand>')
+ 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='<flavor>',
+ help="Flavor ID (see 'novaclient flavors'). "\
+ "Defaults to 256MB RAM instance.")
+ @arg('--image',
+ default=None,
+ metavar='<image>',
+ help="Image ID (see 'novaclient images'). "\
+ "Defaults to Ubuntu 10.04 LTS.")
+ @arg('--meta',
+ metavar="<key=value>",
+ action='append',
+ default=[],
+ help="Record arbitrary key/value metadata. "\
+ "May be give multiple times.")
+ @arg('--file',
+ metavar="<dst-path=src-path>",
+ action='append',
+ dest='files',
+ default=[],
+ help="Store arbitrary files from <src-path> locally to <dst-path> "\
+ "on the new server. You may store up to 5 files.")
+ @arg('--key',
+ metavar='<path>',
+ nargs='?',
+ const=AUTO_KEY,
+ help="Key the server with an SSH keypair. "\
+ "Looks in ~/.ssh for a key, "\
+ "or takes an explicit <path> to one.")
+ @arg('name', metavar='<name>', 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='<fixed_ip>',
+ default=None,
+ help='Only match against fixed IP.')
+ @arg('--reservation_id',
+ dest='reservation_id',
+ metavar='<reservation_id>',
+ 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='<ip_regexp>',
+ default=None,
+ help='Search with regular expression match by IP address')
+ @arg('--ip6',
+ dest='ip6',
+ metavar='<ip6_regexp>',
+ default=None,
+ help='Search with regular expression match by IPv6 address')
+ @arg('--server_name',
+ dest='server_name',
+ metavar='<name_regexp>',
+ default=None,
+ help='Search with regular expression match by server name')
+ @arg('--name',
+ dest='display_name',
+ metavar='<name_regexp>',
+ default=None,
+ help='Search with regular expression match by display name')
+ @arg('--instance_name',
+ dest='name',
+ metavar='<name_regexp>',
+ 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='<server>', help='Name or ID of server.')
+ @arg('name', metavar='<name>', help='Name of backup or snapshot.')
+ @arg('--image-type',
+ metavar='<backup|snapshot>',
+ default='snapshot',
+ help='type of image (default: snapshot)')
+ @arg('--backup-type',
+ metavar='<daily|weekly>',
+ default=None,
+ help='type of backup')
+ @arg('--rotation',
+ default=None,
+ type=int,
+ metavar='<rotation>',
+ 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='<image>', 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='<server>', 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='<server>', help='Name or ID of server.')
+ @arg('image', metavar='<image>', 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='<server>', help='Name (old name) or ID of server.')
+ @arg('name', metavar='<name>', 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='<server>', help='Name or ID of server.')
+ @arg('flavor', metavar='<flavor>', 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='<server>', help='Name or ID of server.')
+ def do_migrate(self, args):
+ """Migrate a server."""
+ self._find_server(args.server).migrate()
+
+ @arg('server', metavar='<server>', help='Name or ID of server.')
+ def do_pause(self, args):
+ """Pause a server."""
+ self._find_server(args.server).pause()
+
+ @arg('server', metavar='<server>', help='Name or ID of server.')
+ def do_unpause(self, args):
+ """Unpause a server."""
+ self._find_server(args.server).unpause()
+
+ @arg('server', metavar='<server>', help='Name or ID of server.')
+ def do_suspend(self, args):
+ """Suspend a server."""
+ self._find_server(args.server).suspend()
+
+ @arg('server', metavar='<server>', help='Name or ID of server.')
+ def do_resume(self, args):
+ """Resume a server."""
+ self._find_server(args.server).resume()
+
+ @arg('server', metavar='<server>', help='Name or ID of server.')
+ def do_rescue(self, args):
+ """Rescue a server."""
+ self._find_server(args.server).rescue()
+
+ @arg('server', metavar='<server>', help='Name or ID of server.')
+ def do_unrescue(self, args):
+ """Unrescue a server."""
+ self._find_server(args.server).unrescue()
+
+ @arg('server', metavar='<server>', 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='<server>', 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='<server>', 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='<server>', 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='<server>', 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='<server>', 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='<server>', 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 00000000..4e8baa3e
--- /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 "<Weighting: %s>" % 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 "<Zone: %s>" % 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 60b2ac5e..00000000
--- 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 cf4c6cfb..00000000
--- 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 1cc150a3..00000000
--- 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 98a6f151..00000000
--- 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 b88ed921..00000000
--- 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 00000000..e69de29b
--- /dev/null
+++ b/tests/v1_0/__init__.py
diff --git a/tests/fakeserver.py b/tests/v1_0/fakes.py
index 232c7b35..2ee96e0a 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
+
+
+def assert_equal(value_one, value_two):
+ assert value_one == value_two
-class FakeServer(OpenStack):
+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
index 37e37c55..93eeed92 100644
--- a/tests/test_accounts.py
+++ b/tests/v1_0/test_accounts.py
@@ -1,14 +1,13 @@
-import StringIO
+from __future__ import absolute_import
-from nose.tools import assert_equal
+import StringIO
-from fakeserver import FakeServer
-from novaclient import Account
+from .fakes import FakeClient
-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
index 206fb851..d27907cb 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 00000000..831f8106
--- /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
index 8477987f..db440bbe 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), "<Resource baz=spam, foo=bar>")
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
index b22de034..e7a63b7d 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 00000000..3c9f644c
--- /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 00000000..9e9bb473
--- /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 00000000..3c89d4c6
--- /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 00000000..e32b3d61
--- /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
index 17ba5a44..bc607292 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
index cd773ad3..2e6aa848 100644
--- a/tests/test_zones.py
+++ b/tests/v1_0/test_zones.py
@@ -1,28 +1,33 @@
+from __future__ import absolute_import
+
import StringIO
+
from nose.tools import assert_equal
-from fakeserver import FakeServer
-from utils import assert_isinstance
-from novaclient import Zone
-os = FakeServer()
+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
index 90763c69..90763c69 100644
--- a/tests/testfile.txt
+++ b/tests/v1_0/testfile.txt
diff --git a/tests/utils.py b/tests/v1_0/utils.py
index f878a5e2..f878a5e2 100644
--- a/tests/utils.py
+++ b/tests/v1_0/utils.py
diff --git a/tests/v1_1/__init__.py b/tests/v1_1/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/v1_1/__init__.py
diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py
new file mode 100644
index 00000000..fc7793bf
--- /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 00000000..9db08eb8
--- /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), "<Resource baz=spam, foo=bar>")
+
+
+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 00000000..7cf90e36
--- /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 00000000..51ca7a1b
--- /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 00000000..03b63b7c
--- /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 00000000..1ffb7b95
--- /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 00000000..baf117b9
--- /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 00000000..90763c69
--- /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 00000000..f878a5e2
--- /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))