Tests working again...merged in some work we did earlier.
This commit is contained in:
parent
f8496672cc
commit
454daa2cba
@ -1,17 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
__version__ = '2.5'
|
235
novaclient/base.py
Normal file
235
novaclient/base.py
Normal file
@ -0,0 +1,235 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
from novaclient import exceptions
|
||||
|
||||
|
||||
# Python 2.4 compat
|
||||
try:
|
||||
all
|
||||
except NameError:
|
||||
def all(iterable):
|
||||
return True not in (not x for x in iterable)
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
(UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
|
||||
# Try to return the object's UUID first, if we have a UUID.
|
||||
try:
|
||||
if obj.uuid:
|
||||
return obj.uuid
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
class Manager(object):
|
||||
"""
|
||||
Managers interact with a particular type of API (servers, flavors, images,
|
||||
etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, body=None):
|
||||
resp = None
|
||||
if body:
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
else:
|
||||
resp, body = self.api.client.get(url)
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
return [obj_class(self, res)
|
||||
for res in body[response_key] if res]
|
||||
|
||||
def _get(self, url, response_key):
|
||||
resp, body = self.api.client.get(url)
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _create(self, url, body, response_key, return_raw=False):
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
if return_raw:
|
||||
return body[response_key]
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _delete(self, url):
|
||||
resp, body = self.api.client.delete(url)
|
||||
|
||||
def _update(self, url, body):
|
||||
resp, body = self.api.client.put(url, body=body)
|
||||
|
||||
|
||||
class ManagerWithFind(Manager):
|
||||
"""
|
||||
Like a `Manager`, but with additional `find()`/`findall()` methods.
|
||||
"""
|
||||
def find(self, **kwargs):
|
||||
"""
|
||||
Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
rl = self.findall(**kwargs)
|
||||
try:
|
||||
return rl[0]
|
||||
except IndexError:
|
||||
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
|
||||
raise exceptions.NotFound(404, msg)
|
||||
|
||||
def findall(self, **kwargs):
|
||||
"""
|
||||
Find all items with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = kwargs.items()
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
found.append(obj)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
return found
|
||||
|
||||
|
||||
class BootingManagerWithFind(ManagerWithFind):
|
||||
"""Like a `ManagerWithFind`, but has the ability to boot servers."""
|
||||
def _boot(self, resource_url, response_key, name, image, flavor,
|
||||
ipgroup=None, meta=None, files=None, zone_blob=None,
|
||||
reservation_id=None, return_raw=False, min_count=None,
|
||||
max_count=None):
|
||||
"""
|
||||
Create (boot) a new server.
|
||||
|
||||
:param name: Something to name the server.
|
||||
:param image: The :class:`Image` to boot with.
|
||||
:param flavor: The :class:`Flavor` to boot onto.
|
||||
:param ipgroup: An initial :class:`IPGroup` for this server.
|
||||
:param meta: A dict of arbitrary key/value metadata to store for this
|
||||
server. A maximum of five entries is allowed, and both
|
||||
keys and values must be 255 characters or less.
|
||||
:param files: A dict of files to overrwrite on the server upon boot.
|
||||
Keys are file names (i.e. ``/etc/passwd``) and values
|
||||
are the file contents (either as a string or as a
|
||||
file-like object). A maximum of five entries is allowed,
|
||||
and each file must be 10k or less.
|
||||
:param zone_blob: a single (encrypted) string which is used internally
|
||||
by Nova for routing between Zones. Users cannot populate
|
||||
this field.
|
||||
:param reservation_id: a UUID for the set of servers being requested.
|
||||
:param return_raw: If True, don't try to coearse the result into
|
||||
a Resource object.
|
||||
"""
|
||||
body = {"server": {
|
||||
"name": name,
|
||||
"imageId": getid(image),
|
||||
"flavorId": getid(flavor),
|
||||
}}
|
||||
if ipgroup:
|
||||
body["server"]["sharedIpGroupId"] = getid(ipgroup)
|
||||
if meta:
|
||||
body["server"]["metadata"] = meta
|
||||
if reservation_id:
|
||||
body["server"]["reservation_id"] = reservation_id
|
||||
if zone_blob:
|
||||
body["server"]["zone_blob"] = zone_blob
|
||||
|
||||
if not min_count:
|
||||
min_count = 1
|
||||
if not max_count:
|
||||
max_count = min_count
|
||||
body["server"]["min_count"] = min_count
|
||||
body["server"]["max_count"] = max_count
|
||||
|
||||
# Files are a slight bit tricky. They're passed in a "personality"
|
||||
# list to the POST. Each item is a dict giving a file name and the
|
||||
# base64-encoded contents of the file. We want to allow passing
|
||||
# either an open file *or* some contents as files here.
|
||||
if files:
|
||||
personality = body['server']['personality'] = []
|
||||
for filepath, file_or_string in files.items():
|
||||
if hasattr(file_or_string, 'read'):
|
||||
data = file_or_string.read()
|
||||
else:
|
||||
data = file_or_string
|
||||
personality.append({
|
||||
'path': filepath,
|
||||
'contents': data.encode('base64'),
|
||||
})
|
||||
|
||||
return self._create(resource_url, body, response_key,
|
||||
return_raw=return_raw)
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""
|
||||
A resource represents a particular instance of an object (server, flavor,
|
||||
etc). This is pretty much just a bag for attributes.
|
||||
"""
|
||||
def __init__(self, manager, info):
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getattr__(self, k):
|
||||
self.get()
|
||||
if k not in self.__dict__:
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
|
||||
k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
def get(self):
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
157
novaclient/client.py
Normal file
157
novaclient/client.py
Normal file
@ -0,0 +1,157 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
import time
|
||||
import urlparse
|
||||
import urllib
|
||||
import httplib2
|
||||
import logging
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
# Python 2.5 compat fix
|
||||
if not hasattr(urlparse, 'parse_qsl'):
|
||||
import cgi
|
||||
urlparse.parse_qsl = cgi.parse_qsl
|
||||
|
||||
|
||||
from novaclient import exceptions
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTTPClient(httplib2.Http):
|
||||
|
||||
USER_AGENT = 'python-novaclient'
|
||||
|
||||
def __init__(self, user, apikey, projectid, auth_url, timeout=None):
|
||||
super(HTTPClient, self).__init__(timeout=timeout)
|
||||
self.user = user
|
||||
self.apikey = apikey
|
||||
self.projectid = projectid
|
||||
self.auth_url = auth_url
|
||||
self.version = 'v1.0'
|
||||
|
||||
self.management_url = None
|
||||
self.auth_token = None
|
||||
|
||||
# httplib2 overrides
|
||||
self.force_exception_to_status_code = True
|
||||
|
||||
def http_log(self, args, kwargs, resp, body):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET', 'POST'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
_logger.debug("REQ: %s\n" % "".join(string_parts))
|
||||
_logger.debug("RESP:%s %s\n", resp, body)
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
if 'body' in kwargs:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
resp, body = super(HTTPClient, self).request(*args, **kwargs)
|
||||
|
||||
self.http_log(args, kwargs, resp, body)
|
||||
|
||||
if body:
|
||||
try:
|
||||
body = json.loads(body)
|
||||
except ValueError, e:
|
||||
pass
|
||||
else:
|
||||
body = None
|
||||
|
||||
if resp.status in (400, 401, 403, 404, 408, 413, 500, 501):
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
return resp, body
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
if not self.management_url:
|
||||
self.authenticate()
|
||||
|
||||
# Perform the request once. If we get a 401 back then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
|
||||
if self.projectid:
|
||||
kwargs['headers']['X-Auth-Project-Id'] = self.projectid
|
||||
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.Unauthorized, ex:
|
||||
try:
|
||||
self.authenticate()
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.Unauthorized:
|
||||
raise ex
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
url = self._munge_get_url(url)
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._cs_request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PUT', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
|
||||
def authenticate(self):
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(
|
||||
self.auth_url)
|
||||
path_parts = path.split('/')
|
||||
for part in path_parts:
|
||||
if len(part) > 0 and part[0] == 'v':
|
||||
self.version = part
|
||||
break
|
||||
|
||||
headers = {'X-Auth-User': self.user,
|
||||
'X-Auth-Key': self.apikey}
|
||||
if self.projectid:
|
||||
headers['X-Auth-Project-Id'] = self.projectid
|
||||
resp, body = self.request(self.auth_url, 'GET', headers=headers)
|
||||
self.management_url = resp['x-server-management-url']
|
||||
|
||||
self.auth_token = resp['x-auth-token']
|
||||
|
||||
def _munge_get_url(self, url):
|
||||
"""
|
||||
Munge GET URLs to always return uncached content.
|
||||
|
||||
The OpenStack Compute API caches data *very* agressively and doesn't
|
||||
respect cache headers. To avoid stale data, then, we append a little
|
||||
bit of nonsense onto GET parameters; this appears to force the data not
|
||||
to be cached.
|
||||
"""
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(url)
|
||||
query = urlparse.parse_qsl(query)
|
||||
query.append(('fresh', str(time.time())))
|
||||
query = urllib.urlencode(query)
|
||||
return urlparse.urlunsplit((scheme, netloc, path, query, frag))
|
@ -3,7 +3,12 @@
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
class OpenStackException(Exception):
|
||||
|
||||
class CommandError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""
|
||||
The base exception class for all exceptions this library raises.
|
||||
"""
|
||||
@ -16,7 +21,7 @@ class OpenStackException(Exception):
|
||||
return "%s (HTTP %s)" % (self.message, self.code)
|
||||
|
||||
|
||||
class BadRequest(OpenStackException):
|
||||
class BadRequest(ClientException):
|
||||
"""
|
||||
HTTP 400 - Bad request: you sent some malformed data.
|
||||
"""
|
||||
@ -24,7 +29,7 @@ class BadRequest(OpenStackException):
|
||||
message = "Bad request"
|
||||
|
||||
|
||||
class Unauthorized(OpenStackException):
|
||||
class Unauthorized(ClientException):
|
||||
"""
|
||||
HTTP 401 - Unauthorized: bad credentials.
|
||||
"""
|
||||
@ -32,7 +37,7 @@ class Unauthorized(OpenStackException):
|
||||
message = "Unauthorized"
|
||||
|
||||
|
||||
class Forbidden(OpenStackException):
|
||||
class Forbidden(ClientException):
|
||||
"""
|
||||
HTTP 403 - Forbidden: your credentials don't give you access to this
|
||||
resource.
|
||||
@ -41,7 +46,7 @@ class Forbidden(OpenStackException):
|
||||
message = "Forbidden"
|
||||
|
||||
|
||||
class NotFound(OpenStackException):
|
||||
class NotFound(ClientException):
|
||||
"""
|
||||
HTTP 404 - Not found
|
||||
"""
|
||||
@ -49,7 +54,7 @@ class NotFound(OpenStackException):
|
||||
message = "Not found"
|
||||
|
||||
|
||||
class OverLimit(OpenStackException):
|
||||
class OverLimit(ClientException):
|
||||
"""
|
||||
HTTP 413 - Over limit: you're over the API limits for this time period.
|
||||
"""
|
||||
@ -58,7 +63,7 @@ class OverLimit(OpenStackException):
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HTTPNotImplemented(OpenStackException):
|
||||
class HTTPNotImplemented(ClientException):
|
||||
"""
|
||||
HTTP 501 - Not Implemented: the server does not support this operation.
|
||||
"""
|
||||
@ -69,7 +74,7 @@ class HTTPNotImplemented(OpenStackException):
|
||||
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
|
||||
# so we can do this:
|
||||
# _code_map = dict((c.http_status, c)
|
||||
# for c in OpenStackException.__subclasses__())
|
||||
# for c in ClientException.__subclasses__())
|
||||
#
|
||||
# Instead, we have to hardcode it:
|
||||
_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
|
||||
@ -78,7 +83,7 @@ _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
|
||||
|
||||
def from_response(response, body):
|
||||
"""
|
||||
Return an instance of an OpenStackException or subclass
|
||||
Return an instance of an ClientException or subclass
|
||||
based on an httplib2 response.
|
||||
|
||||
Usage::
|
||||
@ -87,7 +92,7 @@ def from_response(response, body):
|
||||
if resp.status != 200:
|
||||
raise exception_from_response(resp, body)
|
||||
"""
|
||||
cls = _code_map.get(response.status, OpenStackException)
|
||||
cls = _code_map.get(response.status, ClientException)
|
||||
if body:
|
||||
message = "n/a"
|
||||
details = "n/a"
|
204
novaclient/shell.py
Normal file
204
novaclient/shell.py
Normal file
@ -0,0 +1,204 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Command-line interface to the OpenStack Nova API.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import httplib2
|
||||
import os
|
||||
import prettytable
|
||||
import sys
|
||||
|
||||
from novaclient import exceptions
|
||||
from novaclient import utils
|
||||
from novaclient.v1_0 import shell as shell_v1_0
|
||||
from novaclient.v1_1 import shell as shell_v1_1
|
||||
|
||||
|
||||
def env(e):
|
||||
return os.environ.get(e, '')
|
||||
|
||||
|
||||
class OpenStackComputeShell(object):
|
||||
|
||||
# Hook for the test suite to inject a fake server.
|
||||
_api_class = None
|
||||
|
||||
def get_base_parser(self):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='nova',
|
||||
description=__doc__.strip(),
|
||||
epilog='See "nova help COMMAND" '\
|
||||
'for help on a specific command.',
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter,
|
||||
)
|
||||
|
||||
# Global arguments
|
||||
parser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--debug',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--username',
|
||||
default=env('NOVA_USERNAME'),
|
||||
help='Defaults to env[NOVA_USERNAME].')
|
||||
|
||||
parser.add_argument('--apikey',
|
||||
default=env('NOVA_API_KEY'),
|
||||
help='Defaults to env[NOVA_API_KEY].')
|
||||
|
||||
parser.add_argument('--projectid',
|
||||
default=env('NOVA_PROJECT_ID'),
|
||||
help='Defaults to env[NOVA_PROJECT_ID].')
|
||||
|
||||
parser.add_argument('--url',
|
||||
default=env('NOVA_URL'),
|
||||
help='Defaults to env[NOVA_URL].')
|
||||
|
||||
parser.add_argument('--version',
|
||||
default='1.1',
|
||||
help='Accepts 1.0 or 1.1, defaults to 1.1')
|
||||
|
||||
return parser
|
||||
|
||||
def get_subcommand_parser(self, version):
|
||||
parser = self.get_base_parser()
|
||||
|
||||
self.subcommands = {}
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
|
||||
actions_module = {
|
||||
'1.0': shell_v1_0,
|
||||
'1.1': shell_v1_1,
|
||||
}[version]
|
||||
|
||||
self._find_actions(subparsers, actions_module)
|
||||
self._find_actions(subparsers, self)
|
||||
|
||||
return parser
|
||||
|
||||
def _find_actions(self, subparsers, actions_module):
|
||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||
# I prefer to be hypen-separated instead of underscores.
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(actions_module, attr)
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(command,
|
||||
help=help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter
|
||||
)
|
||||
subparser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
self.subcommands[command] = subparser
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
def main(self, argv):
|
||||
# Parse args once to find version
|
||||
parser = self.get_base_parser()
|
||||
(options, args) = parser.parse_known_args(argv)
|
||||
|
||||
# build available subcommands based on version
|
||||
subcommand_parser = self.get_subcommand_parser(options.version)
|
||||
self.parser = subcommand_parser
|
||||
|
||||
# Parse args again and call whatever callback was selected
|
||||
args = subcommand_parser.parse_args(argv)
|
||||
|
||||
# Deal with global arguments
|
||||
if args.debug:
|
||||
httplib2.debuglevel = 1
|
||||
|
||||
# Short-circuit and deal with help right away.
|
||||
if args.func == self.do_help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
|
||||
user, apikey, projectid, url = args.username, args.apikey, \
|
||||
args.projectid, args.url
|
||||
|
||||
#FIXME(usrleon): Here should be restrict for project id same as
|
||||
# for username or apikey but for compatibility it is not.
|
||||
|
||||
if not user:
|
||||
raise exceptions.CommandError("You must provide a username, either via "
|
||||
"--username or via env[NOVA_USERNAME]")
|
||||
if not apikey:
|
||||
raise exceptions.CommandError("You must provide an API key, either via "
|
||||
"--apikey or via env[NOVA_API_KEY]")
|
||||
|
||||
self.cs = self.get_api_class()(user, apikey, projectid, url)
|
||||
try:
|
||||
self.cs.authenticate()
|
||||
except exceptions.Unauthorized:
|
||||
raise exceptions.CommandError("Invalid OpenStack Nova credentials.")
|
||||
|
||||
args.func(self.cs, args)
|
||||
|
||||
def get_api_class(self):
|
||||
return self._api_class or shell_v1_0.CLIENT_CLASS
|
||||
|
||||
@utils.arg('command', metavar='<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 exceptions.CommandError("'%s' is not a valid subcommand." %
|
||||
args.command)
|
||||
else:
|
||||
self.parser.print_help()
|
||||
|
||||
|
||||
# I'm picky about my shell help.
|
||||
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(OpenStackHelpFormatter, self).start_section(heading)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
OpenStackComputeShell().main(sys.argv[1:])
|
||||
|
||||
except Exception, e:
|
||||
if httplib2.debuglevel == 1:
|
||||
raise # dump stack.
|
||||
else:
|
||||
print >> sys.stderr, e
|
||||
sys.exit(1)
|
43
novaclient/utils.py
Normal file
43
novaclient/utils.py
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
import prettytable
|
||||
|
||||
|
||||
# Decorator for cli-args
|
||||
def arg(*args, **kwargs):
|
||||
def _decorator(func):
|
||||
# Because of the sematics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
|
||||
return func
|
||||
return _decorator
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters={}):
|
||||
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
||||
pt.aligns = ['l' for f in fields]
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = getattr(o, field_name, '')
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
|
||||
pt.printt(sortby=fields[0])
|
||||
|
||||
|
||||
def print_dict(d):
|
||||
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
|
||||
pt.aligns = ['l', 'l']
|
||||
[pt.add_row(list(r)) for r in d.iteritems()]
|
||||
pt.printt(sortby='Property')
|
||||
|
||||
|
@ -1,74 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from novaclient.v1_0 import accounts
|
||||
from novaclient.v1_0 import backup_schedules
|
||||
from novaclient.v1_0 import client
|
||||
from novaclient.v1_0 import exceptions
|
||||
from novaclient.v1_0 import flavors
|
||||
from novaclient.v1_0 import images
|
||||
from novaclient.v1_0 import ipgroups
|
||||
from novaclient.v1_0 import servers
|
||||
from novaclient.v1_0 import zones
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
Top-level object to access the OpenStack Compute v1.0 API.
|
||||
|
||||
Create an instance with your creds::
|
||||
|
||||
>>> os = novaclient.v1_0.Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
|
||||
|
||||
Then call methods on its managers::
|
||||
|
||||
>>> os.servers.list()
|
||||
...
|
||||
>>> os.flavors.list()
|
||||
...
|
||||
|
||||
&c.
|
||||
"""
|
||||
|
||||
def __init__(self, username, apikey, projectid, auth_url=None, timeout=None):
|
||||
"""Initialize v1.0 Openstack Client."""
|
||||
self.backup_schedules = backup_schedules.BackupScheduleManager(self)
|
||||
self.flavors = flavors.FlavorManager(self)
|
||||
self.images = images.ImageManager(self)
|
||||
self.ipgroups = ipgroups.IPGroupManager(self)
|
||||
self.servers = servers.ServerManager(self)
|
||||
self.zones = zones.ZoneManager(self)
|
||||
self.accounts = accounts.AccountManager(self)
|
||||
|
||||
auth_url = auth_url or "https://auth.api.rackspacecloud.com/v1.0"
|
||||
|
||||
self.client = client.HTTPClient(username,
|
||||
apikey,
|
||||
projectid,
|
||||
auth_url,
|
||||
timeout=timeout)
|
||||
|
||||
def authenticate(self):
|
||||
"""
|
||||
Authenticate against the server.
|
||||
|
||||
Normally this is called automatically when you first access the API,
|
||||
but you can call this method to force authentication right now.
|
||||
|
||||
Returns on success; raises :exc:`novaclient.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
self.client.authenticate()
|
@ -1,10 +1,13 @@
|
||||
from novaclient.v1_0 import base
|
||||
|
||||
from novaclient import base
|
||||
from novaclient.v1_0 import base as local_base
|
||||
|
||||
|
||||
class Account(base.Resource):
|
||||
pass
|
||||
|
||||
class AccountManager(base.BootingManagerWithFind):
|
||||
|
||||
class AccountManager(local_base.BootingManagerWithFind):
|
||||
resource_class = Account
|
||||
|
||||
def create_instance_for(self, account_id, name, image, flavor,
|
||||
|
@ -3,7 +3,8 @@
|
||||
Backup Schedule interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import base
|
||||
from novaclient import base
|
||||
|
||||
|
||||
BACKUP_WEEKLY_DISABLED = 'DISABLED'
|
||||
BACKUP_WEEKLY_SUNDAY = 'SUNDAY'
|
||||
|
@ -19,7 +19,9 @@
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import exceptions
|
||||
from novaclient import base
|
||||
from novaclient import exceptions
|
||||
|
||||
|
||||
# Python 2.4 compat
|
||||
try:
|
||||
@ -29,103 +31,7 @@ except NameError:
|
||||
return True not in (not x for x in iterable)
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
(UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
|
||||
# Try to return the object's UUID first, if we have a UUID.
|
||||
try:
|
||||
if obj.uuid:
|
||||
return obj.uuid
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
class Manager(object):
|
||||
"""
|
||||
Managers interact with a particular type of API (servers, flavors, images,
|
||||
etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, body=None):
|
||||
resp = None
|
||||
if body:
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
else:
|
||||
resp, body = self.api.client.get(url)
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
return [obj_class(self, res)
|
||||
for res in body[response_key] if res]
|
||||
|
||||
def _get(self, url, response_key):
|
||||
resp, body = self.api.client.get(url)
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _create(self, url, body, response_key, return_raw=False):
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
if return_raw:
|
||||
return body[response_key]
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _delete(self, url):
|
||||
resp, body = self.api.client.delete(url)
|
||||
|
||||
def _update(self, url, body):
|
||||
resp, body = self.api.client.put(url, body=body)
|
||||
|
||||
|
||||
class ManagerWithFind(Manager):
|
||||
"""
|
||||
Like a `Manager`, but with additional `find()`/`findall()` methods.
|
||||
"""
|
||||
def find(self, **kwargs):
|
||||
"""
|
||||
Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
rl = self.findall(**kwargs)
|
||||
try:
|
||||
return rl[0]
|
||||
except IndexError:
|
||||
raise exceptions.NotFound(404, "No %s matching %s." %
|
||||
(self.resource_class.__name__, kwargs))
|
||||
|
||||
def findall(self, **kwargs):
|
||||
"""
|
||||
Find all items with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = kwargs.items()
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
found.append(obj)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
return found
|
||||
|
||||
|
||||
class BootingManagerWithFind(ManagerWithFind):
|
||||
class BootingManagerWithFind(base.ManagerWithFind):
|
||||
"""Like a `ManagerWithFind`, but has the ability to boot servers."""
|
||||
def _boot(self, resource_url, response_key, name, image, flavor,
|
||||
ipgroup=None, meta=None, files=None, zone_blob=None,
|
||||
@ -155,11 +61,11 @@ class BootingManagerWithFind(ManagerWithFind):
|
||||
"""
|
||||
body = {"server": {
|
||||
"name": name,
|
||||
"imageId": getid(image),
|
||||
"flavorId": getid(flavor),
|
||||
"imageId": base.getid(image),
|
||||
"flavorId": base.getid(flavor),
|
||||
}}
|
||||
if ipgroup:
|
||||
body["server"]["sharedIpGroupId"] = getid(ipgroup)
|
||||
body["server"]["sharedIpGroupId"] = base.getid(ipgroup)
|
||||
if meta:
|
||||
body["server"]["metadata"] = meta
|
||||
if reservation_id:
|
||||
@ -192,43 +98,3 @@ class BootingManagerWithFind(ManagerWithFind):
|
||||
|
||||
return self._create(resource_url, body, response_key,
|
||||
return_raw=return_raw)
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""
|
||||
A resource represents a particular instance of an object (server, flavor,
|
||||
etc). This is pretty much just a bag for attributes.
|
||||
"""
|
||||
def __init__(self, manager, info):
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getattr__(self, k):
|
||||
self.get()
|
||||
if k not in self.__dict__:
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
|
||||
k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
def get(self):
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
||||
|
@ -1,154 +1,60 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
import time
|
||||
import urlparse
|
||||
import urllib
|
||||
import httplib2
|
||||
import logging
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
from novaclient import client
|
||||
from novaclient.v1_0 import accounts
|
||||
from novaclient.v1_0 import backup_schedules
|
||||
from novaclient.v1_0 import flavors
|
||||
from novaclient.v1_0 import images
|
||||
from novaclient.v1_0 import ipgroups
|
||||
from novaclient.v1_0 import servers
|
||||
from novaclient.v1_0 import zones
|
||||
|
||||
# Python 2.5 compat fix
|
||||
if not hasattr(urlparse, 'parse_qsl'):
|
||||
import cgi
|
||||
urlparse.parse_qsl = cgi.parse_qsl
|
||||
|
||||
import novaclient
|
||||
from novaclient.v1_0 import exceptions
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
class Client(object):
|
||||
"""
|
||||
Top-level object to access the OpenStack Compute API.
|
||||
|
||||
class HTTPClient(httplib2.Http):
|
||||
Create an instance with your creds::
|
||||
|
||||
USER_AGENT = 'python-novaclient/%s' % novaclient.__version__
|
||||
>>> client = Client(USERNAME, API_KEY, PROJECT_ID, AUTH_URL)
|
||||
|
||||
def __init__(self, user, apikey, projectid, auth_url, timeout=None):
|
||||
super(HTTPClient, self).__init__(timeout=timeout)
|
||||
self.user = user
|
||||
self.apikey = apikey
|
||||
self.projectid = projectid
|
||||
self.auth_url = auth_url
|
||||
self.version = 'v1.0'
|
||||
Then call methods on its managers::
|
||||
|
||||
self.management_url = None
|
||||
self.auth_token = None
|
||||
>>> client.servers.list()
|
||||
...
|
||||
>>> client.flavors.list()
|
||||
...
|
||||
|
||||
# httplib2 overrides
|
||||
self.force_exception_to_status_code = True
|
||||
"""
|
||||
|
||||
def http_log(self, args, kwargs, resp, body):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
def __init__(self, username, api_key, project_id, auth_url=None,
|
||||
timeout=None):
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET','POST'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
self.accounts = accounts.AccountManager(self)
|
||||
self.backup_schedules = backup_schedules.BackupScheduleManager(self)
|
||||
self.flavors = flavors.FlavorManager(self)
|
||||
self.images = images.ImageManager(self)
|
||||
self.ipgroups = ipgroups.IPGroupManager(self)
|
||||
self.servers = servers.ServerManager(self)
|
||||
self.zones = zones.ZoneManager(self)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
string_parts.append(' -H "%s: %s"' % (element,kwargs['headers'][element]))
|
||||
_auth_url = auth_url or 'https://auth.api.rackspacecloud.com/v1.0'
|
||||
|
||||
_logger.debug("REQ: %s\n" % "".join(string_parts))
|
||||
_logger.debug("RESP:%s %s\n", resp,body)
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
if 'body' in kwargs:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
resp, body = super(HTTPClient, self).request(*args, **kwargs)
|
||||
|
||||
self.http_log(args, kwargs, resp, body)
|
||||
|
||||
if body:
|
||||
try:
|
||||
body = json.loads(body)
|
||||
except ValueError, e:
|
||||
pass
|
||||
else:
|
||||
body = None
|
||||
|
||||
if resp.status in (400, 401, 403, 404, 408, 413, 500, 501):
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
return resp, body
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
if not self.management_url:
|
||||
self.authenticate()
|
||||
|
||||
# Perform the request once. If we get a 401 back then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
|
||||
if self.projectid:
|
||||
kwargs['headers']['X-Auth-Project-Id'] = self.projectid
|
||||
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.Unauthorized, ex:
|
||||
try:
|
||||
self.authenticate()
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.Unauthorized:
|
||||
raise ex
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
url = self._munge_get_url(url)
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._cs_request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PUT', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
self.client = client.HTTPClient(username,
|
||||
api_key,
|
||||
project_id,
|
||||
_auth_url,
|
||||
timeout=timeout)
|
||||
|
||||
def authenticate(self):
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(
|
||||
self.auth_url)
|
||||
path_parts = path.split('/')
|
||||
for part in path_parts:
|
||||
if len(part) > 0 and part[0] == 'v':
|
||||
self.version = part
|
||||
break
|
||||
|
||||
headers = {'X-Auth-User': self.user,
|
||||
'X-Auth-Key': self.apikey}
|
||||
if self.projectid:
|
||||
headers['X-Auth-Project-Id'] = self.projectid
|
||||
resp, body = self.request(self.auth_url, 'GET', headers=headers)
|
||||
self.management_url = resp['x-server-management-url']
|
||||
|
||||
self.auth_token = resp['x-auth-token']
|
||||
|
||||
def _munge_get_url(self, url):
|
||||
"""
|
||||
Munge GET URLs to always return uncached content.
|
||||
Authenticate against the server.
|
||||
|
||||
The OpenStack Nova API caches data *very* agressively and doesn't
|
||||
respect cache headers. To avoid stale data, then, we append a little
|
||||
bit of nonsense onto GET parameters; this appears to force the data not
|
||||
to be cached.
|
||||
Normally this is called automatically when you first access the API,
|
||||
but you can call this method to force authentication right now.
|
||||
|
||||
Returns on success; raises :exc:`exceptions.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
scheme, netloc, path, query, frag = urlparse.urlsplit(url)
|
||||
query = urlparse.parse_qsl(query)
|
||||
query.append(('fresh', str(time.time())))
|
||||
query = urllib.urlencode(query)
|
||||
return urlparse.urlunsplit((scheme, netloc, path, query, frag))
|
||||
self.client.authenticate()
|
||||
|
@ -3,7 +3,7 @@
|
||||
Flavor interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import base
|
||||
from novaclient import base
|
||||
|
||||
|
||||
class Flavor(base.Resource):
|
||||
|
@ -3,7 +3,7 @@
|
||||
Image interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import base
|
||||
from novaclient import base
|
||||
|
||||
|
||||
class Image(base.Resource):
|
||||
@ -46,8 +46,7 @@ class ImageManager(base.ManagerWithFind):
|
||||
detail = "/detail"
|
||||
return self._list("/images%s" % detail, "images")
|
||||
|
||||
|
||||
def create(self, server, name, image_type=None, backup_type=None, rotation=None):
|
||||
def create(self, server, name):
|
||||
"""
|
||||
Create a new image by snapshotting a running :class:`Server`
|
||||
|
||||
@ -55,23 +54,7 @@ class ImageManager(base.ManagerWithFind):
|
||||
:param server: The :class:`Server` (or its ID) to make a snapshot of.
|
||||
:rtype: :class:`Image`
|
||||
"""
|
||||
if image_type is None:
|
||||
image_type = "snapshot"
|
||||
|
||||
if image_type not in ("backup", "snapshot"):
|
||||
raise Exception("Invalid image_type: must be backup or snapshot")
|
||||
|
||||
if image_type == "backup":
|
||||
if not rotation:
|
||||
raise Exception("rotation is required for backups")
|
||||
elif not backup_type:
|
||||
raise Exception("backup_type required for backups")
|
||||
elif backup_type not in ("daily", "weekly"):
|
||||
raise Exception("Invalid backup_type: must be daily or weekly")
|
||||
|
||||
data = {"image": {"serverId": base.getid(server), "name": name,
|
||||
"image_type": image_type, "backup_type": backup_type,
|
||||
"rotation": rotation}}
|
||||
data = {"image": {"serverId": base.getid(server), "name": name}}
|
||||
return self._create("/images", data, "image")
|
||||
|
||||
def delete(self, image):
|
||||
|
@ -3,7 +3,7 @@
|
||||
IP Group interface.
|
||||
"""
|
||||
|
||||
from novaclient.v1_0 import base
|
||||
from novaclient import base
|
||||
|
||||
|
||||
class IPGroup(base.Resource):
|
||||
|
@ -20,7 +20,10 @@ Server interface.
|
||||
"""
|
||||
|
||||
import urllib
|
||||
from novaclient.v1_0 import base
|
||||
|
||||
from novaclient import base
|
||||
from novaclient.v1_0 import base as local_base
|
||||
|
||||
|
||||
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
|
||||
|
||||
@ -154,6 +157,18 @@ class Server(base.Resource):
|
||||
"""
|
||||
self.manager.resize(self, flavor)
|
||||
|
||||
def backup(self, image_name, backup_type, rotation):
|
||||
"""
|
||||
Create a server backup.
|
||||
|
||||
:param server: The :class:`Server` (or its ID).
|
||||
:param image_name: The name to assign the newly create image.
|
||||
:param backup_type: 'daily' or 'weekly'
|
||||
:param rotation: number of backups of type 'backup_type' to keep
|
||||
:returns Newly created :class:`Image` object
|
||||
"""
|
||||
return self.manager.backup(self, image_name, backup_type, rotation)
|
||||
|
||||
def confirm_resize(self):
|
||||
"""
|
||||
Confirm that the resize worked, thus removing the original server.
|
||||
@ -198,7 +213,7 @@ class Server(base.Resource):
|
||||
return self.addresses['private']
|
||||
|
||||
|
||||
class ServerManager(base.BootingManagerWithFind):
|
||||
class ServerManager(local_base.BootingManagerWithFind):
|
||||
resource_class = Server
|
||||
|
||||
def get(self, server):
|
||||
@ -370,6 +385,31 @@ class ServerManager(base.BootingManagerWithFind):
|
||||
"""
|
||||
self._action('resize', server, {'flavorId': base.getid(flavor)})
|
||||
|
||||
def backup(self, server, image_name, backup_type, rotation):
|
||||
"""
|
||||
Create a server backup.
|
||||
|
||||
:param server: The :class:`Server` (or its ID).
|
||||
:param image_name: The name to assign the newly create image.
|
||||
:param backup_type: 'daily' or 'weekly'
|
||||
:param rotation: number of backups of type 'backup_type' to keep
|
||||
:returns Newly created :class:`Image` object
|
||||
"""
|
||||
if not rotation:
|
||||
raise Exception("rotation is required for backups")
|
||||
elif not backup_type:
|
||||
raise Exception("backup_type required for backups")
|
||||
elif backup_type not in ("daily", "weekly"):
|
||||
raise Exception("Invalid backup_type: must be daily or weekly")
|
||||
|
||||
data = {
|
||||
"name": image_name,
|
||||
"rotation": rotation,
|
||||
< |