diff options
| author | Vishvananda Ishaya <vishvananda@yahoo.com> | 2010-08-17 17:58:52 -0700 |
|---|---|---|
| committer | Vishvananda Ishaya <vishvananda@yahoo.com> | 2010-08-17 17:58:52 -0700 |
| commit | cdcbd516f62290697643eecc56550460bd48ff14 (patch) | |
| tree | 798d4da90b8025b041a3f2b5efe86f3e33e8ef53 | |
| parent | 1cd448f907e132c451d6b27c64d16c17b7530952 (diff) | |
| parent | 018ce9abbfb7047eff1e99379fba098a365e89eb (diff) | |
| download | nova-cdcbd516f62290697643eecc56550460bd48ff14.tar.gz nova-cdcbd516f62290697643eecc56550460bd48ff14.tar.xz nova-cdcbd516f62290697643eecc56550460bd48ff14.zip | |
merged trunk
54 files changed, 700 insertions, 439 deletions
diff --git a/bin/nova-rsapi b/bin/nova-rsapi index 026880d5a..e2722422e 100755 --- a/bin/nova-rsapi +++ b/bin/nova-rsapi @@ -24,11 +24,11 @@ from nova import flags from nova import utils from nova import wsgi -from nova.endpoint import rackspace +from nova.endpoint import newapi FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') if __name__ == '__main__': utils.default_flagfile() - wsgi.run_server(rackspace.API(), FLAGS.cc_port) + wsgi.run_server(newapi.APIVersionRouter(), FLAGS.cc_port) diff --git a/nova/adminclient.py b/nova/adminclient.py index 242298a75..0ca32b1e5 100644 --- a/nova/adminclient.py +++ b/nova/adminclient.py @@ -20,6 +20,7 @@ Nova User API client library. """ import base64 + import boto from boto.ec2.regioninfo import RegionInfo @@ -57,6 +58,7 @@ class UserInfo(object): elif name == 'secretkey': self.secretkey = str(value) + class UserRole(object): """ Information about a Nova user's role, as parsed through SAX. @@ -79,6 +81,7 @@ class UserRole(object): else: setattr(self, name, str(value)) + class ProjectInfo(object): """ Information about a Nova project, as parsed through SAX @@ -114,12 +117,14 @@ class ProjectInfo(object): else: setattr(self, name, str(value)) + class ProjectMember(object): """ Information about a Nova project member, as parsed through SAX. Fields include: memberId """ + def __init__(self, connection=None): self.connection = connection self.memberId = None @@ -135,6 +140,7 @@ class ProjectMember(object): self.memberId = value else: setattr(self, name, str(value)) + class HostInfo(object): """ @@ -163,6 +169,7 @@ class HostInfo(object): def endElement(self, name, value, connection): setattr(self, name, value) + class NovaAdminClient(object): def __init__(self, clc_ip='127.0.0.1', region='nova', access_key='admin', secret_key='admin', **kwargs): diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py index b420924af..bc744fa01 100644 --- a/nova/auth/fakeldap.py +++ b/nova/auth/fakeldap.py @@ -219,7 +219,6 @@ class FakeLDAP(object): raise NO_SUCH_OBJECT() return objects - @property def __redis_prefix(self): return 'ldap:' diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py index 453fa196c..6bf7fcd1e 100644 --- a/nova/auth/ldapdriver.py +++ b/nova/auth/ldapdriver.py @@ -30,6 +30,7 @@ import sys from nova import exception from nova import flags + FLAGS = flags.FLAGS flags.DEFINE_string('ldap_url', 'ldap://localhost', 'Point this at your ldap server') diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 4a813c861..c16eb0c3c 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -38,7 +38,6 @@ from nova.network import vpn FLAGS = flags.FLAGS - flags.DEFINE_list('allowed_roles', ['cloudadmin', 'itsec', 'sysadmin', 'netadmin', 'developer'], 'Allowed roles for project') @@ -53,7 +52,6 @@ flags.DEFINE_list('superuser_roles', ['cloudadmin'], flags.DEFINE_list('global_roles', ['cloudadmin', 'itsec'], 'Roles that apply to all projects') - flags.DEFINE_string('credentials_template', utils.abspath('auth/novarc.template'), 'Template for creating users rc file') @@ -68,15 +66,14 @@ flags.DEFINE_string('credential_cert_file', 'cert.pem', 'Filename of certificate in credentials zip') flags.DEFINE_string('credential_rc_file', 'novarc', 'Filename of rc in credentials zip') - flags.DEFINE_string('credential_cert_subject', '/C=US/ST=California/L=MountainView/O=AnsoLabs/' 'OU=NovaDev/CN=%s-%s', 'Subject for certificate for users') - flags.DEFINE_string('auth_driver', 'nova.auth.ldapdriver.FakeLdapDriver', 'Driver that auth manager uses') + class AuthBase(object): """Base class for objects relating to auth @@ -84,6 +81,7 @@ class AuthBase(object): an id member. They may optionally contain methods that delegate to AuthManager, but should not implement logic themselves. """ + @classmethod def safe_id(cls, obj): """Safe get object id @@ -101,6 +99,7 @@ class AuthBase(object): class User(AuthBase): """Object representing a user""" + def __init__(self, id, name, access, secret, admin): AuthBase.__init__(self) self.id = id @@ -162,6 +161,7 @@ class KeyPair(AuthBase): Even though this object is named KeyPair, only the public key and fingerprint is stored. The user's private key is not saved. """ + def __init__(self, id, name, owner_id, public_key, fingerprint): AuthBase.__init__(self) self.id = id @@ -180,6 +180,7 @@ class KeyPair(AuthBase): class Project(AuthBase): """Represents a Project returned from the datastore""" + def __init__(self, id, name, project_manager_id, description, member_ids): AuthBase.__init__(self) self.id = id @@ -233,7 +234,6 @@ class Project(AuthBase): self.member_ids) - class AuthManager(object): """Manager Singleton for dealing with Users, Projects, and Keypairs @@ -245,7 +245,9 @@ class AuthManager(object): AuthManager also manages associated data related to Auth objects that need to be more accessible, such as vpn ips and ports. """ + _instance = None + def __new__(cls, *args, **kwargs): """Returns the AuthManager singleton""" if not cls._instance: diff --git a/nova/auth/rbac.py b/nova/auth/rbac.py index 7fab9419f..1446e4e27 100644 --- a/nova/auth/rbac.py +++ b/nova/auth/rbac.py @@ -32,6 +32,7 @@ def allow(*roles): return wrapped_f return wrap + def deny(*roles): def wrap(f): def wrapped_f(self, context, *args, **kwargs): @@ -44,6 +45,7 @@ def deny(*roles): return wrapped_f return wrap + def __matches_role(context, role): if role == 'all': return True diff --git a/nova/auth/signer.py b/nova/auth/signer.py index 634f22f0d..8334806d2 100644 --- a/nova/auth/signer.py +++ b/nova/auth/signer.py @@ -48,11 +48,15 @@ import hashlib import hmac import logging import urllib -import boto # NOTE(vish): for new boto -import boto.utils # NOTE(vish): for old boto + +# NOTE(vish): for new boto +import boto +# NOTE(vish): for old boto +import boto.utils from nova.exception import Error + class Signer(object): """ hacked up code from boto/connection.py """ @@ -77,7 +81,6 @@ class Signer(object): return self._calc_signature_2(params, verb, server_string, path) raise Error('Unknown Signature Version: %s' % self.SignatureVersion) - def _get_utf8_value(self, value): if not isinstance(value, str) and not isinstance(value, unicode): value = str(value) @@ -133,5 +136,6 @@ class Signer(object): logging.debug('base64 encoded digest: %s' % b64) return b64 + if __name__ == '__main__': print Signer('foo').generate({"SignatureMethod": 'HmacSHA256', 'SignatureVersion': '2'}, "get", "server", "/foo") diff --git a/nova/cloudpipe/api.py b/nova/cloudpipe/api.py index 0bffe9aa3..56aa89834 100644 --- a/nova/cloudpipe/api.py +++ b/nova/cloudpipe/api.py @@ -21,9 +21,10 @@ Tornado REST API Request Handlers for CloudPipe """ import logging -import tornado.web import urllib +import tornado.web + from nova import crypto from nova.auth import manager diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index 5b0ed3471..2867bcb21 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -36,11 +36,11 @@ from nova.endpoint import api FLAGS = flags.FLAGS - flags.DEFINE_string('boot_script_template', utils.abspath('cloudpipe/bootscript.sh'), 'Template for script to run on cloudpipe instance boot') + class CloudPipe(object): def __init__(self, cloud_controller): self.controller = cloud_controller diff --git a/nova/compute/disk.py b/nova/compute/disk.py index 1ffcca685..c340c5a79 100644 --- a/nova/compute/disk.py +++ b/nova/compute/disk.py @@ -24,6 +24,7 @@ Includes injection of SSH PGP keys into authorized_keys file. import logging import os import tempfile + from twisted.internet import defer from nova import exception @@ -84,6 +85,7 @@ def partition(infile, outfile, local_bytes=0, local_type='ext2', execute=None): yield execute('dd if=%s of=%s bs=%d seek=%d conv=notrunc,fsync' % (infile, outfile, sector_size, primary_first)) + @defer.inlineCallbacks def inject_data(image, key=None, net=None, partition=None, execute=None): """Injects a ssh key and optionally net data into a disk image. @@ -137,6 +139,7 @@ def inject_data(image, key=None, net=None, partition=None, execute=None): # remove loopback yield execute('sudo losetup -d %s' % device) + @defer.inlineCallbacks def _inject_key_into_fs(key, fs, execute=None): sshdir = os.path.join(os.path.join(fs, 'root'), '.ssh') @@ -146,6 +149,7 @@ def _inject_key_into_fs(key, fs, execute=None): keyfile = os.path.join(sshdir, 'authorized_keys') yield execute('sudo tee -a %s' % keyfile, '\n' + key.strip() + '\n') + @defer.inlineCallbacks def _inject_net_into_fs(net, fs, execute=None): netfile = os.path.join(os.path.join(os.path.join( diff --git a/nova/compute/model.py b/nova/compute/model.py index 54d816a9c..baa41c3e0 100644 --- a/nova/compute/model.py +++ b/nova/compute/model.py @@ -231,6 +231,7 @@ class Daemon(): for x in cls.associated_to("host", hostname): yield x + class SessionToken(): """This is a short-lived auth token that is passed through web requests""" diff --git a/nova/compute/monitor.py b/nova/compute/monitor.py index 19e1a483d..268864900 100644 --- a/nova/compute/monitor.py +++ b/nova/compute/monitor.py @@ -24,14 +24,15 @@ Instance Monitoring: in the object store. """ -import boto -import boto.s3 import datetime import logging import os -import rrdtool import sys import time + +import boto +import boto.s3 +import rrdtool from twisted.internet import defer from twisted.internet import task from twisted.application import service @@ -41,13 +42,12 @@ from nova.virt import connection as virt_connection FLAGS = flags.FLAGS -flags.DEFINE_integer( - 'monitoring_instances_delay', 5, 'Sleep time between updates') -flags.DEFINE_integer( - 'monitoring_instances_step', 300, 'Interval of RRD updates') -flags.DEFINE_string( - 'monitoring_rrd_path', '/var/nova/monitor/instances', - 'Location of RRD files') +flags.DEFINE_integer('monitoring_instances_delay', 5, + 'Sleep time between updates') +flags.DEFINE_integer('monitoring_instances_step', 300, + 'Interval of RRD updates') +flags.DEFINE_string('monitoring_rrd_path', '/var/nova/monitor/instances', + 'Location of RRD files') RRD_VALUES = { @@ -61,7 +61,7 @@ RRD_VALUES = { 'RRA:MAX:0.5:6:800', 'RRA:MAX:0.5:24:800', 'RRA:MAX:0.5:288:800', - ], + ], 'net': [ 'DS:rx:COUNTER:600:0:1250000', 'DS:tx:COUNTER:600:0:1250000', @@ -73,7 +73,7 @@ RRD_VALUES = { 'RRA:MAX:0.5:6:800', 'RRA:MAX:0.5:24:800', 'RRA:MAX:0.5:288:800', - ], + ], 'disk': [ 'DS:rd:COUNTER:600:U:U', 'DS:wr:COUNTER:600:U:U', @@ -85,12 +85,13 @@ RRD_VALUES = { 'RRA:MAX:0.5:6:800', 'RRA:MAX:0.5:24:800', 'RRA:MAX:0.5:444:800', - ] -} + ] + } utcnow = datetime.datetime.utcnow + def update_rrd(instance, name, data): """ Updates the specified RRD file. @@ -106,6 +107,7 @@ def update_rrd(instance, name, data): '%d:%s' % (timestamp, data) ) + def init_rrd(instance, name): """ Initializes the specified RRD file. @@ -124,6 +126,7 @@ def init_rrd(instance, name): '--start', '0', *RRD_VALUES[name] ) + def graph_cpu(instance, duration): """ @@ -148,6 +151,7 @@ def graph_cpu(instance, duration): store_graph(instance.instance_id, filename) + def graph_net(instance, duration): """ Creates a graph of network usage for the specified instance and duration. @@ -174,6 +178,7 @@ def graph_net(instance, duration): ) store_graph(instance.instance_id, filename) + def graph_disk(instance, duration): """ @@ -202,6 +207,7 @@ def graph_disk(instance, duration): store_graph(instance.instance_id, filename) + def store_graph(instance_id, filename): """ Transmits the specified graph file to internal object store on cloud @@ -387,6 +393,7 @@ class InstanceMonitor(object, service.Service): """ Monitors the running instances of the current machine. """ + def __init__(self): """ Initialize the monitoring loop. diff --git a/nova/compute/service.py b/nova/compute/service.py index b80ef3740..13507a1bb 100644 --- a/nova/compute/service.py +++ b/nova/compute/service.py @@ -29,6 +29,7 @@ import json import logging import os import sys + from twisted.internet import defer from twisted.internet import task diff --git a/nova/crypto.py b/nova/crypto.py index cc84f5e45..b05548ea1 100644 --- a/nova/crypto.py +++ b/nova/crypto.py @@ -24,7 +24,6 @@ SSH keypairs and x509 certificates. import base64 import hashlib import logging -import M2Crypto import os import shutil import struct @@ -32,6 +31,8 @@ import tempfile import time import utils +import M2Crypto + from nova import exception from nova import flags @@ -42,11 +43,13 @@ flags.DEFINE_string('keys_path', utils.abspath('../keys'), 'Where we keep our ke flags.DEFINE_string('ca_path', utils.abspath('../CA'), 'Where we keep our root CA') flags.DEFINE_boolean('use_intermediate_ca', False, 'Should we use intermediate CAs for each project?') + def ca_path(project_id): if project_id: return "%s/INTER/%s/cacert.pem" % (FLAGS.ca_path, project_id) return "%s/cacert.pem" % (FLAGS.ca_path) + def fetch_ca(project_id=None, chain=True): if not FLAGS.use_intermediate_ca: project_id = None @@ -60,6 +63,7 @@ def fetch_ca(project_id=None, chain=True): buffer += cafile.read() return buffer + def generate_key_pair(bits=1024): # what is the magic 65537? @@ -109,6 +113,7 @@ def generate_x509_cert(subject, bits=1024): shutil.rmtree(tmpdir) return (private_key, csr) + def sign_csr(csr_text, intermediate=None): if not FLAGS.use_intermediate_ca: intermediate = None @@ -122,6 +127,7 @@ def sign_csr(csr_text, intermediate=None): os.chdir(start) return _sign_csr(csr_text, user_ca) + def _sign_csr(csr_text, ca_folder): tmpfolder = tempfile.mkdtemp() csrfile = open("%s/inbound.csr" % (tmpfolder), "w") diff --git a/nova/endpoint/__init__.py b/nova/endpoint/__init__.py index 753685149..e69de29bb 100644 --- a/nova/endpoint/__init__.py +++ b/nova/endpoint/__init__.py @@ -1,32 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# 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. - -""" -:mod:`nova.endpoint` -- Main NOVA Api endpoints -===================================================== - -.. automodule:: nova.endpoint - :platform: Unix - :synopsis: REST APIs for all nova functions -.. moduleauthor:: Jesse Andrews <jesse@ansolabs.com> -.. moduleauthor:: Devin Carlen <devin.carlen@gmail.com> -.. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com> -.. moduleauthor:: Joshua McKenty <joshua@cognition.ca> -.. moduleauthor:: Manish Singh <yosh@gimp.org> -.. moduleauthor:: Andy Smith <andy@anarkystic.com> -""" diff --git a/nova/endpoint/admin.py b/nova/endpoint/admin.py index 4f4824fca..d6f622755 100644 --- a/nova/endpoint/admin.py +++ b/nova/endpoint/admin.py @@ -37,6 +37,7 @@ def user_dict(user, base64_file=None): else: return {} + def project_dict(project): """Convert the project object to a result dict""" if project: @@ -47,6 +48,7 @@ def project_dict(project): else: return {} + def host_dict(host): """Convert a host model object to a result dict""" if host: @@ -54,6 +56,7 @@ def host_dict(host): else: return {} + def admin_only(target): """Decorator for admin-only API calls""" def wrapper(*args, **kwargs): @@ -66,6 +69,7 @@ def admin_only(target): return wrapper + class AdminController(object): """ API Controller for users, hosts, nodes, and workers. diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 78a18b9ea..40be00bb7 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -25,12 +25,13 @@ import logging import multiprocessing import random import re -import tornado.web -from twisted.internet import defer import urllib # TODO(termie): replace minidom with etree from xml.dom import minidom +import tornado.web +from twisted.internet import defer + from nova import crypto from nova import exception from nova import flags @@ -43,6 +44,7 @@ from nova.endpoint import cloud FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') + _log = logging.getLogger("api") _log.setLevel(logging.DEBUG) @@ -227,6 +229,7 @@ class MetadataRequestHandler(tornado.web.RequestHandler): self.print_data(data) self.finish() + class APIRequestHandler(tornado.web.RequestHandler): def get(self, controller_name): self.execute(controller_name) diff --git a/nova/endpoint/aws/__init__.py b/nova/endpoint/aws/__init__.py new file mode 100644 index 000000000..55cbb8fd3 --- /dev/null +++ b/nova/endpoint/aws/__init__.py @@ -0,0 +1,22 @@ +import routes +import webob.dec + +from nova import wsgi + +# TODO(gundlach): temp +class API(wsgi.Router): + """WSGI entry point for all AWS API requests.""" + + def __init__(self): + mapper = routes.Mapper() + + mapper.connect(None, "{all:.*}", controller=self.dummy) + + super(API, self).__init__(mapper) + + @webob.dec.wsgify + def dummy(self, req): + #TODO(gundlach) + msg = "dummy response -- please hook up __init__() to cloud.py instead" + return repr({ 'dummy': msg, + 'kwargs': repr(req.environ['wsgiorg.routing_args'][1]) }) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 832103579..3bc03e0b1 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -26,6 +26,7 @@ import base64 import logging import os import time + from twisted.internet import defer from nova import datastore @@ -45,7 +46,6 @@ from nova.volume import service FLAGS = flags.FLAGS - flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') @@ -363,7 +363,6 @@ class CloudController(object): 'status': volume['attach_status'], 'volumeId': volume_id}) - @rbac.allow('projectmanager', 'sysadmin') def detach_volume(self, context, volume_id, **kwargs): volume = self._get_volume(context, volume_id) diff --git a/nova/endpoint/images.py b/nova/endpoint/images.py index fe7cb5d11..2a88d66af 100644 --- a/nova/endpoint/images.py +++ b/nova/endpoint/images.py @@ -21,10 +21,11 @@ Proxy AMI-related calls from the cloud controller, to the running objectstore daemon. """ -import boto.s3.connection import json import urllib +import boto.s3.connection + from nova import flags from nova import utils from nova.auth import manager @@ -32,6 +33,7 @@ from nova.auth import manager FLAGS = flags.FLAGS + def modify(context, image_id, operation): conn(context).make_request( method='POST', @@ -53,6 +55,7 @@ def register(context, image_location): return image_id + def list(context, filter_list=[]): """ return a list of all images that a user can see @@ -68,6 +71,7 @@ def list(context, filter_list=[]): return [i for i in result if i['imageId'] in filter_list] return result + def deregister(context, image_id): """ unregister an image """ conn(context).make_request( @@ -75,6 +79,7 @@ def deregister(context, image_id): bucket='_images', query_args=qs({'image_id': image_id})) + def conn(context): access = manager.AuthManager().get_access_key(context.user, context.project) diff --git a/nova/endpoint/newapi.py b/nova/endpoint/newapi.py new file mode 100644 index 000000000..9aae933af --- /dev/null +++ b/nova/endpoint/newapi.py @@ -0,0 +1,51 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +""" +:mod:`nova.endpoint` -- Main NOVA Api endpoints +===================================================== + +.. automodule:: nova.endpoint + :platform: Unix + :synopsis: REST APIs for all nova functions +.. moduleauthor:: Jesse Andrews <jesse@ansolabs.com> +.. moduleauthor:: Devin Carlen <devin.carlen@gmail.com> +.. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com> +.. moduleauthor:: Joshua McKenty <joshua@cognition.ca> +.. moduleauthor:: Manish Singh <yosh@gimp.org> +.. moduleauthor:: Andy Smith <andy@anarkystic.com> +""" + +from nova import wsgi +import routes +from nova.endpoint import rackspace +from nova.endpoint import aws + +class APIVersionRouter(wsgi.Router): + """Routes top-level requests to the appropriate API.""" + + def __init__(self): + mapper = routes.Mapper() + + rsapi = rackspace.API() + mapper.connect(None, "/v1.0/{path_info:.*}", controller=rsapi) + + mapper.connect(None, "/ec2/{path_info:.*}", controller=aws.API()) + + super(APIVersionRouter, self).__init__(mapper) + diff --git a/nova/endpoint/rackspace.py b/nova/endpoint/rackspace.py deleted file mode 100644 index 75b828e91..000000000 --- a/nova/endpoint/rackspace.py +++ /dev/null @@ -1,183 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# 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. - -""" -Rackspace API Endpoint -""" - -import json -import time - -import webob.dec -import webob.exc - -from nova import flags -from nova import rpc -from nova import utils -from nova import wsgi -from nova.auth import manager -from nova.compute import model as compute -from nova.network import model as network - - -FLAGS = flags.FLAGS -flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') - - -class API(wsgi.Middleware): - """Entry point for all requests.""" - - def __init__(self): - super(API, self).__init__(Router(webob.exc.HTTPNotFound())) - - def __call__(self, environ, start_response): - context = {} - if "HTTP_X_AUTH_TOKEN" in environ: - context['user'] = manager.AuthManager().get_user_from_access_key( - environ['HTTP_X_AUTH_TOKEN']) - if context['user']: - context['project'] = manager.AuthManager().get_project( - context['user'].name) - if "user" not in context: - return webob.exc.HTTPForbidden()(environ, start_response) - environ['nova.context'] = context - return self.application(environ, start_response) - - -class Router(wsgi.Router): - """Route requests to the next WSGI application.""" - - def _build_map(self): - """Build routing map for authentication and cloud.""" - self._connect("/v1.0", controller=AuthenticationAPI()) - cloud = CloudServerAPI() - self._connect("/servers", controller=cloud.launch_server, - conditions={"method": ["POST"]}) - self._connect("/servers/{server_id}", controller=cloud.delete_server, - conditions={'method': ["DELETE"]}) - self._connect("/servers", controller=cloud) - - -class AuthenticationAPI(wsgi.Application): - """Handle all authorization requests through WSGI applications.""" - - @webob.dec.wsgify - def __call__(self, req): # pylint: disable-msg=W0221 - # TODO(todd): make a actual session with a unique token - # just pass the auth key back through for now - res = webob.Response() - res.status = '204 No Content' - res.headers.add('X-Server-Management-Url', req.host_url) - res.headers.add('X-Storage-Url', req.host_url) - res.headers.add('X-CDN-Managment-Url', req.host_url) - res.headers.add('X-Auth-Token', req.headers['X-Auth-Key']) - return res - - -class CloudServerAPI(wsgi.Application): - """Handle all server requests through WSGI applications.""" - - def __init__(self): - super(CloudServerAPI, self).__init__() - self.instdir = compute.InstanceDirectory() - self.network = network.PublicNetworkController() - - @webob.dec.wsgify - def __call__(self, req): # pylint: disable-msg=W0221 - value = {"servers": []} - for inst in self.instdir.all: - value["servers"].append(self.instance_details(inst)) - return json.dumps(value) - - def instance_details(self, inst): # pylint: disable-msg=R0201 - """Build the data structure to represent details for an instance.""" - return { - "id": inst.get("instance_id", None), - "imageId": inst.get("image_id", None), - "flavorId": inst.get("instacne_type", None), - "hostId": inst.get("node_name", None), - "status": inst.get("state", "pending"), - "addresses": { - "public": [network.get_public_ip_for_instance( - inst.get("instance_id", None))], - "private": [inst.get("private_dns_name", None)]}, - - # implemented only by Rackspace, not AWS - "name": inst.get("name", "Not-Specified"), - - # not supported - "progress": "Not-Supported", - "metadata": { - "Server Label": "Not-Supported", - "Image Version": "Not-Supported"}} - - @webob.dec.wsgify - def launch_server(self, req): - """Launch a new instance.""" - data = json.loads(req.body) - inst = self.build_server_instance(data, req.environ['nova.context']) - rpc.cast( - FLAGS.compute_topic, { - "method": "run_instance", - "args": {"instance_id": inst.instance_id}}) - - return json.dumps({"server": self.instance_details(inst)}) - - def build_server_instance(self, env, context): - """Build instance data structure and save it to the data store.""" - reservation = utils.generate_uid('r') - ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) - inst = self.instdir.new() - inst['name'] = env['server']['name'] - inst['image_id'] = env['server']['imageId'] - inst['instance_type'] = env['server']['flavorId'] - inst['user_id'] = context['user'].id - inst['project_id'] = context['project'].id - inst['reservation_id'] = reservation - inst['launch_time'] = ltime - inst['mac_address'] = utils.generate_mac() - address = self.network.allocate_ip( - inst['user_id'], - inst['project_id'], - mac=inst['mac_address']) - inst['private_dns_name'] = str(address) - inst['bridge_name'] = network.BridgedNetwork.get_network_for_project( - inst['user_id'], - inst['project_id'], - 'default')['bridge_name'] - # key_data, key_name, ami_launch_index - # TODO(todd): key data or root password - inst.save() - return inst - - @webob.dec.wsgify - @wsgi.route_args - def delete_server(self, req, route_args): # pylint: disable-msg=R0201 - """Delete an instance.""" - owner_hostname = None - instance = compute.Instance.lookup(route_args['server_id']) - if instance: - owner_hostname = instance["node_name"] - if not owner_hostname: - return webob.exc.HTTPNotFound("Did not find image, or it was " - "not in a running state.") - rpc_transport = "%s:%s" % (FLAGS.compute_topic, owner_hostname) - rpc.cast(rpc_transport, - {"method": "reboot_instance", - "args": {"instance_id": route_args['server_id']}}) - req.status = "202 Accepted" diff --git a/nova/endpoint/rackspace/__init__.py b/nova/endpoint/rackspace/__init__.py new file mode 100644 index 000000000..ac53ee10b --- /dev/null +++ b/nova/endpoint/rackspace/__init__.py @@ -0,0 +1,83 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +""" +Rackspace API Endpoint +""" + +import json +import time + +import webob.dec +import webob.exc +import routes + +from nova import flags +from nova import wsgi +from nova.auth import manager +from nova.endpoint.rackspace import controllers + + +class API(wsgi.Middleware): + """WSGI entry point for all Rackspace API requests.""" + + def __init__(self): + app = AuthMiddleware(APIRouter()) + super(API, self).__init__(app) + + +class AuthMiddleware(wsgi.Middleware): + """Authorize the rackspace API request or return an HTTP Forbidden.""" + + #TODO(gundlach): isn't this the old Nova API's auth? Should it be replaced + #with correct RS API auth? + + @webob.dec.wsgify + def __call__(self, req): + context = {} + if "HTTP_X_AUTH_TOKEN" in req.environ: + context['user'] = manager.AuthManager().get_user_from_access_key( + req.environ['HTTP_X_AUTH_TOKEN']) + if context['user']: + context['project'] = manager.AuthManager().get_project( + context['user'].name) + if "user" not in context: + return webob.exc.HTTPForbidden() + req.environ['nova.context'] = context + return self.application + + +class APIRouter(wsgi.Router): + """ + Routes requests on the Rackspace API to the appropriate controller + and method. + """ + + def __init__(self): + mapper = routes.Mapper() + + mapper.resource("server", "servers", + controller=controllers.ServersController()) + mapper.resource("image", "images", + controller=controllers.ImagesController()) + mapper.resource("flavor", "flavors", + controller=controllers.FlavorsController()) + mapper.resource("sharedipgroup", "sharedipgroups", + controller=controllers.SharedIpGroupsController()) + + super(APIRouter, self).__init__(mapper) diff --git a/nova/endpoint/rackspace/controllers/__init__.py b/nova/endpoint/rackspace/controllers/__init__.py new file mode 100644 index 000000000..052b6f365 --- /dev/null +++ b/nova/endpoint/rackspace/controllers/__init__.py @@ -0,0 +1,5 @@ +from nova.endpoint.rackspace.controllers.images import ImagesController +from nova.endpoint.rackspace.controllers.flavors import FlavorsController +from nova.endpoint.rackspace.controllers.servers import ServersController +from nova.endpoint.rackspace.controllers.sharedipgroups import \ + SharedIpGroupsController diff --git a/nova/endpoint/rackspace/controllers/base.py b/nova/endpoint/rackspace/controllers/base.py new file mode 100644 index 000000000..8cd44f62e --- /dev/null +++ b/nova/endpoint/rackspace/controllers/base.py @@ -0,0 +1,9 @@ +from nova import wsgi + +class BaseController(wsgi.Controller): + @classmethod + def render(cls, instance): + if isinstance(instance, list): + return { cls.entity_name : cls.render(instance) } + else: + return { "TODO": "TODO" } diff --git a/nova/endpoint/rackspace/controllers/flavors.py b/nova/endpoint/rackspace/controllers/flavors.py new file mode 100644 index 000000000..f256cc852 --- /dev/null +++ b/nova/endpoint/rackspace/controllers/flavors.py @@ -0,0 +1 @@ +class FlavorsController(object): pass diff --git a/nova/endpoint/rackspace/controllers/images.py b/nova/endpoint/rackspace/controllers/images.py new file mode 100644 index 000000000..ae2a08849 --- /dev/null +++ b/nova/endpoint/rackspace/controllers/images.py @@ -0,0 +1 @@ +class ImagesController(object): pass diff --git a/nova/endpoint/rackspace/controllers/servers.py b/nova/endpoint/rackspace/controllers/servers.py new file mode 100644 index 000000000..2f8e662d6 --- /dev/null +++ b/nova/endpoint/rackspace/controllers/servers.py @@ -0,0 +1,63 @@ +from nova import rpc +from nova.compute import model as compute +from nova.endpoint.rackspace.controllers.base import BaseController + +class ServersController(BaseController): + entity_name = 'servers' + + def index(self, **kwargs): + return [instance_details(inst) for inst in compute.InstanceDirectory().all] + + def show(self, **kwargs): + instance_id = kwargs['id'] + return compute.InstanceDirectory().get(instance_id) + + def delete(self, **kwargs): + instance_id = kwargs['id'] + instance = compute.InstanceDirectory().get(instance_id) + if not instance: + raise ServerNotFound("The requested server was not found") + instance.destroy() + return True + + def create(self, **kwargs): + inst = self.build_server_instance(kwargs['server']) + rpc.cast( + FLAGS.compute_topic, { + "method": "run_instance", + "args": {"instance_id": inst.instance_id}}) + + def update(self, **kwargs): + instance_id = kwargs['id'] + instance = compute.InstanceDirectory().get(instance_id) + if not instance: + raise ServerNotFound("The requested server was not found") + instance.update(kwargs['server']) + instance.save() + + def build_server_instance(self, env): + """Build instance data structure and save it to the data store.""" + reservation = utils.generate_uid('r') + ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) + inst = self.instdir.new() + inst['name'] = env['server']['name'] + inst['image_id'] = env['server']['imageId'] + inst['instance_type'] = env['server']['flavorId'] + inst['user_id'] = env['user']['id'] + inst['project_id'] = env['project']['id'] + inst['reservation_id'] = reservation + inst['launch_time'] = ltime + inst['mac_address'] = utils.generate_mac() + address = self.network.allocate_ip( + inst['user_id'], + inst['project_id'], + mac=inst['mac_address']) + inst['private_dns_name'] = str(address) + inst['bridge_name'] = network.BridgedNetwork.get_network_for_project( + inst['user_id'], + inst['project_id'], + 'default')['bridge_name'] + # key_data, key_name, ami_launch_index + # TODO(todd): key data or root password + inst.save() + return inst diff --git a/nova/endpoint/rackspace/controllers/sharedipgroups.py b/nova/endpoint/rackspace/controllers/sharedipgroups.py new file mode 100644 index 000000000..9d346d623 --- /dev/null +++ b/nova/endpoint/rackspace/controllers/sharedipgroups.py @@ -0,0 +1 @@ +class SharedIpGroupsController(object): pass diff --git a/nova/exception.py b/nova/exception.py index 52497a19e..29bcb17f8 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -25,31 +25,39 @@ import logging import sys import traceback + class Error(Exception): def __init__(self, message=None): super(Error, self).__init__(message) + class ApiError(Error): def __init__(self, message='Unknown', code='Unknown'): self.message = message self.code = code super(ApiError, self).__init__('%s: %s'% (code, message)) + class NotFound(Error): pass + class Duplicate(Error): pass + class NotAuthorized(Error): pass + class NotEmpty(Error): pass + class Invalid(Error): pass + def wrap_exception(f): def _wrap(*args, **kw): try: diff --git a/nova/fakerabbit.py b/nova/fakerabbit.py index 689194513..068025249 100644 --- a/nova/fakerabbit.py +++ b/nova/fakerabbit.py @@ -16,12 +16,13 @@ # License for the specific language governing permissions and limitations # under the License. -""" Based a bit on the carrot.backeds.queue backend... but a lot better """ +"""Based a bit on the carrot.backeds.queue backend... but a lot better.""" -from carrot.backends import base import logging import Queue as queue +from carrot.backends import base + class Message(base.BaseMessage): pass diff --git a/nova/flags.py b/nova/flags.py index b3bdd088f..e3feb252d 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -175,29 +175,25 @@ DEFINE_string('network_topic', 'network', 'the topic network nodes listen on') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') -DEFINE_bool('fake_network', False, 'should we use fake network devices and addresses') +DEFINE_bool('fake_network', False, + 'should we use fake network devices and addresses') DEFINE_string('rabbit_host', 'localhost', 'rabbit host') DEFINE_integer('rabbit_port', 5672, 'rabbit port') DEFINE_string('rabbit_userid', 'guest', 'rabbit userid') DEFINE_string('rabbit_password', 'guest', 'rabbit password') DEFINE_string('rabbit_virtual_host', '/', 'rabbit virtual host') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') -DEFINE_string('ec2_url', - 'http://127.0.0.1:8773/services/Cloud', - 'Url to ec2 api server') - -DEFINE_string('default_image', - 'ami-11111', - 'default image to use, testing only') -DEFINE_string('default_kernel', - 'aki-11111', - 'default kernel to use, testing only') -DEFINE_string('default_ramdisk', - 'ari-11111', - 'default ramdisk to use, testing only') -DEFINE_string('default_instance_type', - 'm1.small', - 'default instance type to use, testing only') +DEFINE_string('ec2_url', 'http://127.0.0.1:8773/services/Cloud', + 'Url to ec2 api server') + +DEFINE_string('default_image', 'ami-11111', + 'default image to use, testing only') +DEFINE_string('default_kernel', 'aki-11111', + 'default kernel to use, testing only') +DEFINE_string('default_ramdisk', 'ari-11111', + 'default ramdisk to use, testing only') +DEFINE_string('default_instance_type', 'm1.small', + 'default instance type to use, testing only') DEFINE_string('vpn_image_id', 'ami-CLOUDPIPE', 'AMI for cloudpipe vpn server') DEFINE_string('vpn_key_suffix', @@ -207,10 +203,8 @@ DEFINE_string('vpn_key_suffix', DEFINE_integer('auth_token_ttl', 3600, 'Seconds for auth tokens to linger') # UNUSED -DEFINE_string('node_availability_zone', - 'nova', - 'availability zone of this node') -DEFINE_string('node_name', - socket.gethostname(), - 'name of this node') +DEFINE_string('node_availability_zone', 'nova', + 'availability zone of this node') +DEFINE_string('node_name', socket.gethostname(), + 'name of this node') diff --git a/nova/network/exception.py b/nova/network/exception.py index 8d7aa1498..2a3f5ec14 100644 --- a/nova/network/exception.py +++ b/nova/network/exception.py @@ -20,29 +20,29 @@ Exceptions for network errors. """ -from nova.exception import Error +from nova import exception -class NoMoreAddresses(Error): +class NoMoreAddresses(exception.Error): """No More Addresses are available in the network""" pass -class AddressNotAllocated(Error): +class AddressNotAllocated(exception.Error): """The specified address has not been allocated""" pass -class AddressAlreadyAssociated(Error): +class AddressAlreadyAssociated(exception.Error): """The specified address has already been associated""" pass -class AddressNotAssociated(Error): +class AddressNotAssociated(exception.Error): """The specified address is not associated""" pass -class NotValidNetworkSize(Error): +class NotValidNetworkSize(exception.Error): """The network size is not valid""" pass diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 73b9500d2..48d71f11e 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -18,17 +18,17 @@ Implements vlans, bridges, and iptables rules using linux utilities. """ import logging -import signal import os +import signal -# todo(ja): does the definition of network_path belong here? +# TODO(ja): does the definition of network_path belong here? from nova import flags from nova import models from nova import utils -FLAGS = flags.FLAGS +FLAGS = flags.FLAGS flags.DEFINE_string('dhcpbridge_flagfile', '/etc/nova/nova-dhcpbridge.conf', 'location of flagfile for dhcpbridge') diff --git a/nova/network/service.py b/nova/network/service.py index 2b931f342..8ddc4bc84 100644 --- a/nova/network/service.py +++ b/nova/network/service.py @@ -25,17 +25,17 @@ import logging import IPy from sqlalchemy.orm import exc +from nova import exception from nova import flags from nova import models from nova import service from nova import utils from nova.auth import manager -from nova.exception import NotFound -from nova.network import exception +from nova.network import exception as network_exception from nova.network import linux_net -FLAGS = flags.FLAGS +FLAGS = flags.FLAGS flags.DEFINE_string('network_type', 'flat', 'Service Class for Networking') @@ -45,15 +45,15 @@ flags.DEFINE_list('flat_network_ips', ['192.168.0.2', '192.168.0.3', '192.168.0.4'], 'Available ips for simple network') flags.DEFINE_string('flat_network_network', '192.168.0.0', - 'Network for simple network') + 'Network for simple network') flags.DEFINE_string('flat_network_netmask', '255.255.255.0', - 'Netmask for simple network') + 'Netmask for simple network') flags.DEFINE_string('flat_network_gateway', '192.168.0.1', - 'Broadcast for simple network') + 'Broadcast for simple network') flags.DEFINE_string('flat_network_broadcast', '192.168.0.255', - 'Broadcast for simple network') + 'Broadcast for simple network') flags.DEFINE_string('flat_network_dns', '8.8.4.4', - 'Dns for simple network') + 'Dns for simple network') flags.DEFINE_integer('vlan_start', 100, 'First VLAN for private networks') flags.DEFINE_integer('vlan_end', 4093, 'Last VLAN for private networks') @@ -76,7 +76,7 @@ def type_to_class(network_type): return FlatNetworkService elif network_type == 'vlan': return VlanNetworkService - raise NotFound("Couldn't find %s network type" % network_type) + raise exception.NotFound("Couldn't find %s network type" % network_type) def setup_compute_network(project_id): @@ -129,7 +129,7 @@ class BaseNetworkService(service.Service): try: fixed_ip = query.first() except exc.NoResultFound: - raise exception.NoMoreAddresses() + raise network_exception.NoMoreAddresses() # FIXME will this set backreference? fixed_ip.instance_id = instance_id fixed_ip.allocated = True @@ -168,7 +168,7 @@ class BaseNetworkService(service.Service): try: elastic_ip = query.first() except exc.NoResultFound: - raise exception.NoMoreAddresses() + raise network_exception.NoMoreAddresses() elastic_ip.project_id = project_id session.add(elastic_ip) try: @@ -233,7 +233,7 @@ class VlanNetworkService(BaseNetworkService): if is_vpn: fixed_ip = models.FixedIp.find_by_ip_str(network.vpn_private_ip_str) if fixed_ip.allocated: - raise exception.AddressAlreadyAllocated() + raise network_exception.AddressAlreadyAllocated() # FIXME will this set backreference? fixed_ip.instance_id = instance_id fixed_ip.allocated = True @@ -263,7 +263,7 @@ class VlanNetworkService(BaseNetworkService): """Called by bridge when ip is leased""" fixed_ip = models.FixedIp.find_by_ip_str(fixed_ip_str) if not fixed_ip.allocated: - raise exception.AddressNotAllocated(fixed_ip_str) + raise network_exception.AddressNotAllocated(fixed_ip_str) logging.debug("Leasing IP %s", fixed_ip_str) fixed_ip.leased = True fixed_ip.save() @@ -325,7 +325,7 @@ class VlanNetworkService(BaseNetworkService): try: network_index = query.first() except exc.NoResultFound: - raise exception.NoMoreNetworks() + raise network_exception.NoMoreNetworks() network_index.network = network session.add(network_index) try: diff --git a/nova/objectstore/bucket.py b/nova/objectstore/bucket.py index b42a96233..c2b412dd7 100644 --- a/nova/objectstore/bucket.py +++ b/nova/objectstore/bucket.py @@ -36,6 +36,7 @@ FLAGS = flags.FLAGS flags.DEFINE_string('buckets_path', utils.abspath('../buckets'), 'path to s3 buckets') + class Bucket(object): def __init__(self, name): self.name = name diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index dfe1918e3..035e342ca 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -38,17 +38,19 @@ S3 client with this module:: """ import datetime -import logging import json +import logging import multiprocessing import os -from tornado import escape import urllib -from twisted.application import internet, service -from twisted.web.resource import Resource -from twisted.web import server, static, error - +from tornado import escape +from twisted.application import internet +from twisted.application import service +from twisted.web import error +from twisted.web import resource +from twisted.web import server +from twisted.web import static from nova import exception from nova import flags @@ -60,6 +62,7 @@ from nova.objectstore import image FLAGS = flags.FLAGS + def render_xml(request, value): assert isinstance(value, dict) and len(value) == 1 request.setHeader("Content-Type", "application/xml; charset=UTF-8") @@ -72,11 +75,13 @@ def render_xml(request, value): request.write('</' + escape.utf8(name) + '>') request.finish() + def finish(request, content=None): if content: request.write(content) request.finish() + def _render_parts(value, write_cb): if isinstance(value, basestring): write_cb(escape.xhtml_escape(value)) @@ -95,11 +100,13 @@ def _render_parts(value, write_cb): else: raise Exception("Unknown S3 value type %r", value) + def get_argument(request, key, default_value): if key in request.args: return request.args[key][0] return default_value + def get_context(request): try: # Authorization Header format: 'AWS <access>:<secret>' @@ -120,13 +127,14 @@ def get_context(request): logging.debug("Authentication Failure: %s" % ex) raise exception.NotAuthorized -class ErrorHandlingResource(Resource): + +class ErrorHandlingResource(resource.Resource): """Maps exceptions to 404 / 401 codes. Won't work for exceptions thrown after NOT_DONE_YET is returned.""" # TODO(unassigned) (calling-all-twisted-experts): This needs to be plugged in to the right place in twisted... # This doesn't look like it's the right place (consider exceptions in getChild; or after NOT_DONE_YET is returned def render(self, request): try: - return Resource.render(self, request) + return resource.Resource.render(self, request) except exception.NotFound: request.setResponseCode(404) return '' @@ -134,6 +142,7 @@ class ErrorHandlingResource(Resource): request.setResponseCode(403) return '' + class S3(ErrorHandlingResource): """Implementation of an S3-like storage server based on local files.""" def getChild(self, name, request): @@ -154,9 +163,10 @@ class S3(ErrorHandlingResource): }}) return server.NOT_DONE_YET + class BucketResource(ErrorHandlingResource): def __init__(self, name): - Resource.__init__(self) + resource.Resource.__init__(self) self.name = name def getChild(self, name, request): @@ -206,7 +216,7 @@ class BucketResource(ErrorHandlingResource): class ObjectResource(ErrorHandlingResource): def __init__(self, bucket, name): - Resource.__init__(self) + resource.Resource.__init__(self) self.bucket = bucket self.name = name @@ -245,17 +255,19 @@ class ObjectResource(ErrorHandlingResource): request.setResponseCode(204) return '' + class ImageResource(ErrorHandlingResource): isLeaf = True def __init__(self, name): - Resource.__init__(self) + resource.Resource.__init__(self) self.img = image.Image(name) def render_GET(self, request): return static.File(self.img.image_path, defaultType='application/octet-stream').render_GET(request) -class ImagesResource(Resource): + +class ImagesResource(resource.Resource): def getChild(self, name, request): if name == '': return self @@ -339,11 +351,13 @@ class ImagesResource(Resource): request.setResponseCode(204) return '' + def get_site(): root = S3() site = server.Site(root) return site + def get_application(): factory = get_site() application = service.Application("objectstore") diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index 861eb364f..fb780a0ec 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -42,6 +42,7 @@ FLAGS = flags.FLAGS flags.DEFINE_string('images_path', utils.abspath('../images'), 'path to decrypted images') + class Image(object): def __init__(self, image_id): self.image_id = image_id diff --git a/nova/objectstore/stored.py b/nova/objectstore/stored.py index 81c047b22..9829194cb 100644 --- a/nova/objectstore/stored.py +++ b/nova/objectstore/stored.py @@ -23,7 +23,7 @@ Properties of an object stored within a bucket. import os import nova.crypto -from nova.exception import NotFound, NotAuthorized +from nova import exception class Object(object): @@ -33,7 +33,7 @@ class Object(object): self.key = key self.path = bucket._object_path(key) if not os.path.isfile(self.path): - raise NotFound + raise exception.NotFound def __repr__(self): return "<Object %s/%s>" % (self.bucket, self.key) diff --git a/nova/process.py b/nova/process.py index 2dc56372f..86f29e2c4 100644 --- a/nova/process.py +++ b/nova/process.py @@ -23,6 +23,7 @@ Process pool, still buggy right now. import logging import multiprocessing import StringIO + from twisted.internet import defer from twisted.internet import error from twisted.internet import process @@ -205,6 +206,7 @@ class ProcessPool(object): self._pool.release() return rv + class SharedPool(object): _instance = None def __init__(self): @@ -213,5 +215,6 @@ class SharedPool(object): def __getattr__(self, key): return getattr(self._instance, key) + def simple_execute(cmd, **kwargs): return SharedPool().simple_execute(cmd, **kwargs) diff --git a/nova/rpc.py b/nova/rpc.py index 4ac546c2a..824a66b5b 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -21,12 +21,13 @@ AMQP-based RPC. Queues have consumers and publishers. No fan-out support yet. """ -from carrot import connection as carrot_connection -from carrot import messaging import json import logging import sys import uuid + +from carrot import connection as carrot_connection +from carrot import messaging from twisted.internet import defer from twisted.internet import task diff --git a/nova/test.py b/nova/test.py index 9cb826253..4eb5c1c53 100644 --- a/nova/test.py +++ b/nova/test.py @@ -22,11 +22,11 @@ Allows overriding of flags for use of fakes, and some black magic for inline callbacks. """ -import mox -import stubout import sys import time +import mox +import stubout from tornado import ioloop from twisted.internet import defer from twisted.trial import unittest @@ -97,7 +97,6 @@ class TrialTestCase(unittest.TestCase): setattr(FLAGS, k, v) - class BaseTestCase(TrialTestCase): # TODO(jaypipes): Can this be moved into the TrialTestCase class? """Base test case class for all unit tests.""" diff --git a/nova/utils.py b/nova/utils.py index 63db080f1..e826f9b71 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -20,7 +20,7 @@ System-level utilities and helper functions. """ -from datetime import datetime, timedelta +import datetime import inspect import logging import os @@ -32,9 +32,11 @@ import sys from nova import exception from nova import flags + FLAGS = flags.FLAGS TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + def import_class(import_str): """Returns a class from a string including module and class""" mod_str, _sep, class_str = import_str.rpartition('.') @@ -44,6 +46,7 @@ def import_class(import_str): except (ImportError, ValueError, AttributeError): raise exception.NotFound('Class %s cannot be found' % class_str) + def fetchfile(url, target): logging.debug("Fetching %s" % url) # c = pycurl.Curl() @@ -55,6 +58,7 @@ def fetchfile(url, target): # fp.close() execute("curl %s -o %s" % (url, target)) + def execute(cmd, input=None, addl_env=None): env = os.environ.copy() if addl_env: @@ -129,10 +133,12 @@ def get_my_ip(): logging.warn("Couldn't get IP, using 127.0.0.1 %s", ex) return "127.0.0.1" + def isotime(at=None): if not at: - at = datetime.utcnow() + at = datetime.datetime.utcnow() return at.strftime(TIME_FORMAT) + def parse_isotime(timestr): - return datetime.strptime(timestr, TIME_FORMAT) + return datetime.datetime.strptime(timestr, TIME_FORMAT) diff --git a/nova/validate.py b/nova/validate.py index a69306fad..21f4ed286 100644 --- a/nova/validate.py +++ b/nova/validate.py @@ -57,6 +57,7 @@ def rangetest(**argchecks): # validate ranges for both+defaults return onCall return onDecorator + def typetest(**argchecks): def onDecorator(func): import sys diff --git a/nova/virt/fake.py b/nova/virt/fake.py index d7416d250..f7ee34695 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -24,6 +24,8 @@ This module also documents the semantics of real hypervisor connections. import logging +from twisted.internet import defer + from nova.compute import power_state @@ -89,10 +91,13 @@ class FakeConnection(object): This function should use the data there to guide the creation of the new instance. - Once this function successfully completes, the instance should be + The work will be done asynchronously. This function returns a + Deferred that allows the caller to detect when it is complete. + + Once this successfully completes, the instance should be running (power_state.RUNNING). - If this function fails, any partial instance should be completely + If this fails, any partial instance should be completely cleaned up, and the virtualization platform should be in the state that it was before this call began. """ @@ -100,6 +105,7 @@ class FakeConnection(object): fake_instance = FakeInstance() self.instances[instance.id] = fake_instance fake_instance._state = power_state.RUNNING + return defer.succeed(None) def reboot(self, instance): """ @@ -107,8 +113,11 @@ class FakeConnection(object): The given parameter is an instance of nova.compute.service.Instance, and so the instance is being specified as instance.name. + + The work will be done asynchronously. This function returns a + Deferred that allows the caller to detect when it is complete. """ - pass + return defer.succeed(None) def destroy(self, instance): """ @@ -116,8 +125,12 @@ class FakeConnection(object): The given parameter is an instance of nova.compute.service.Instance, and so the instance is being specified as instance.name. + + The work will be done asynchronously. This function returns a + Deferred that allows the caller to detect when it is complete. """ del self.instances[instance.name] + return defer.succeed(None) def get_info(self, instance_id): """ diff --git a/nova/virt/images.py b/nova/virt/images.py index 1e23c48b9..a3ca72bdd 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -27,11 +27,11 @@ import urlparse from nova import flags from nova import process -from nova.auth import signer from nova.auth import manager +from nova.auth import signer -FLAGS = flags.FLAGS +FLAGS = flags.FLAGS flags.DEFINE_bool('use_s3', True, 'whether to get images from s3 or use local copy') @@ -43,6 +43,7 @@ def fetch(image, path, user, project): f = _fetch_local_image return f(image, path, user, project) + def _fetch_s3_image(image, path, user, project): url = image_url(image) @@ -66,13 +67,16 @@ def _fetch_s3_image(image, path, user, project): cmd += ['-o', path] return process.SharedPool().execute(executable=cmd[0], args=cmd[1:]) + def _fetch_local_image(image, path, user, project): source = _image_path('%s/image' % image) return process.simple_execute('cp %s %s' % (source, path)) + def _image_path(path): return os.path.join(FLAGS.images_path, path) + def image_url(image): return "http://%s:%s/_images/%s/image" % (FLAGS.s3_host, FLAGS.s3_port, image) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index c00421ab8..c4eb3f272 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -42,6 +42,7 @@ from nova.virt import images libvirt = None libxml2 = None + FLAGS = flags.FLAGS flags.DEFINE_string('libvirt_xml_template', utils.abspath('virt/libvirt.qemu.xml.template'), @@ -57,7 +58,9 @@ flags.DEFINE_string('libvirt_type', 'Libvirt domain type (valid options are: kvm, qemu, uml)') flags.DEFINE_string('libvirt_uri', '', - 'Override the default libvirt URI (which is dependent on libvirt_type)') + 'Override the default libvirt URI (which is dependent' + ' on libvirt_type)') + def get_connection(read_only): # These are loaded late so that there's no need to install these @@ -70,6 +73,7 @@ def get_connection(read_only): libxml2 = __import__('libxml2') return LibvirtConnection(read_only) + class LibvirtConnection(object): def __init__(self, read_only): self.libvirt_uri, template_file = self.get_uri_and_template() @@ -78,14 +82,12 @@ class LibvirtConnection(object): self._wrapped_conn = None self.read_only = read_only - @property def _conn(self): if not self._wrapped_conn: self._wrapped_conn = self._connect(self.libvirt_uri, self.read_only) return self._wrapped_conn - def get_uri_and_template(self): if FLAGS.libvirt_type == 'uml': uri = FLAGS.libvirt_uri or 'uml:///system' @@ -95,7 +97,6 @@ class LibvirtConnection(object): template_file = FLAGS.libvirt_xml_template return uri, template_file - def _connect(self, uri, read_only): auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], 'root', @@ -106,13 +107,10 @@ class LibvirtConnection(object): else: return libvirt.openAuth(uri, auth, 0) - - def list_instances(self): return [self._conn.lookupByID(x).name() for x in self._conn.listDomainsID()] - def destroy(self, instance): try: virt_dom = self._conn.lookupByName(instance.name) @@ -141,14 +139,12 @@ class LibvirtConnection(object): timer.start(interval=0.5, now=True) return d - def _cleanup(self, instance): target = os.path.join(FLAGS.instances_path, instance.name) logging.info("Deleting instance files at %s", target) if os.path.exists(target): shutil.rmtree(target) - @defer.inlineCallbacks @exception.wrap_exception def reboot(self, instance): @@ -174,7 +170,6 @@ class LibvirtConnection(object): timer.start(interval=0.5, now=True) yield d - @defer.inlineCallbacks @exception.wrap_exception def spawn(self, instance): @@ -204,7 +199,6 @@ class LibvirtConnection(object): timer.start(interval=0.5, now=True) yield local_d - @defer.inlineCallbacks def _create_image(self, inst, libvirt_xml): # syntactic nicety @@ -286,7 +280,6 @@ class LibvirtConnection(object): 'num_cpu': num_cpu, 'cpu_time': cpu_time} - def get_disks(self, instance_name): """ Note that this function takes an instance name, not an Instance, so @@ -329,7 +322,6 @@ class LibvirtConnection(object): return disks - def get_interfaces(self, instance_name): """ Note that this function takes an instance name, not an Instance, so @@ -372,7 +364,6 @@ class LibvirtConnection(object): return interfaces - def block_stats(self, instance_name, disk): """ Note that this function takes an instance name, not an Instance, so @@ -381,7 +372,6 @@ class LibvirtConnection(object): domain = self._conn.lookupByName(instance_name) return domain.blockStats(disk) - def interface_stats(self, instance_name, interface): """ Note that this function takes an instance name, not an Instance, so diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index 9fe15644f..2f5994983 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -33,16 +33,29 @@ from nova.virt import images XenAPI = None + FLAGS = flags.FLAGS flags.DEFINE_string('xenapi_connection_url', None, - 'URL for connection to XenServer/Xen Cloud Platform. Required if connection_type=xenapi.') + 'URL for connection to XenServer/Xen Cloud Platform.' + ' Required if connection_type=xenapi.') flags.DEFINE_string('xenapi_connection_username', 'root', - 'Username for connection to XenServer/Xen Cloud Platform. Used only if connection_type=xenapi.') + 'Username for connection to XenServer/Xen Cloud Platform.' + ' Used only if connection_type=xenapi.') flags.DEFINE_string('xenapi_connection_password', None, - 'Password for connection to XenServer/Xen Cloud Platform. Used only if connection_type=xenapi.') + 'Password for connection to XenServer/Xen Cloud Platform.' + ' Used only if connection_type=xenapi.') + + +XENAPI_POWER_STATE = { + 'Halted' : power_state.SHUTDOWN, + 'Running' : power_state.RUNNING, + 'Paused' : power_state.PAUSED, + 'Suspended': power_state.SHUTDOWN, # FIXME + 'Crashed' : power_state.CRASHED +} def get_connection(_): @@ -62,7 +75,6 @@ def get_connection(_): class XenAPIConnection(object): - def __init__(self, url, user, pw): self._conn = XenAPI.Session(url) self._conn.login_with_password(user, pw) @@ -107,7 +119,6 @@ class XenAPIConnection(object): yield self._create_vif(vm_ref, network_ref, mac_address) yield self._conn.xenapi.VM.start(vm_ref, False, False) - def create_vm(self, instance, kernel, ramdisk): mem = str(long(instance.datamodel['memory_kb']) * 1024) vcpus = str(instance.datamodel['vcpus']) @@ -145,7 +156,6 @@ class XenAPIConnection(object): logging.debug('Created VM %s as %s.', instance.name, vm_ref) return vm_ref - def create_vbd(self, vm_ref, vdi_ref, userdevice, bootable): vbd_rec = {} vbd_rec['VM'] = vm_ref @@ -166,7 +176,6 @@ class XenAPIConnection(object): vdi_ref) return vbd_ref - def _create_vif(self, vm_ref, network_ref, mac_address): vif_rec = {} vif_rec['device'] = '0' @@ -184,7 +193,6 @@ class XenAPIConnection(object): vm_ref, network_ref) return vif_ref - def _find_network_with_bridge(self, bridge): expr = 'field "bridge" = "%s"' % bridge networks = self._conn.xenapi.network.get_all_records_where(expr) @@ -195,7 +203,6 @@ class XenAPIConnection(object): else: raise Exception('Found no network for bridge %s' % bridge) - def fetch_image(self, image, user, project, use_sr): """use_sr: True to put the image as a VDI in an SR, False to place it on dom0's filesystem. The former is for VM disks, the latter for @@ -213,7 +220,6 @@ class XenAPIConnection(object): args['add_partition'] = 'true' return self._call_plugin('objectstore', fn, args) - def reboot(self, instance): vm = self.lookup(instance.name) if vm is None: @@ -231,7 +237,7 @@ class XenAPIConnection(object): if vm is None: raise Exception('instance not present %s' % instance_id) rec = self._conn.xenapi.VM.get_record(vm) - return {'state': power_state_from_xenapi[rec['power_state']], + return {'state': XENAPI_POWER_STATE[rec['power_state']], 'max_mem': long(rec['memory_static_max']) >> 10, 'mem': long(rec['memory_dynamic_max']) >> 10, 'num_cpu': rec['VCPUs_max'], @@ -247,26 +253,15 @@ class XenAPIConnection(object): else: return vms[0] - def _call_plugin(self, plugin, fn, args): return _unwrap_plugin_exceptions( self._conn.xenapi.host.call_plugin, self._get_xenapi_host(), plugin, fn, args) - def _get_xenapi_host(self): return self._conn.xenapi.session.get_this_host(self._conn.handle) -power_state_from_xenapi = { - 'Halted' : power_state.SHUTDOWN, - 'Running' : power_state.RUNNING, - 'Paused' : power_state.PAUSED, - 'Suspended': power_state.SHUTDOWN, # FIXME - 'Crashed' : power_state.CRASHED -} - - def _unwrap_plugin_exceptions(func, *args, **kwargs): try: return func(*args, **kwargs) diff --git a/nova/wsgi.py b/nova/wsgi.py index 4fd6e59e3..a0a175dc7 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -29,6 +29,8 @@ import eventlet.wsgi eventlet.patcher.monkey_patch(all=False, socket=True) import routes import routes.middleware +import webob.dec +import webob.exc logging.getLogger("routes.middleware").addHandler(logging.StreamHandler()) @@ -41,6 +43,8 @@ def run_server(application, port): class Application(object): +# TODO(gundlach): I think we should toss this class, now that it has no +# purpose. """Base WSGI application wrapper. Subclasses need to implement __call__.""" def __call__(self, environ, start_response): @@ -79,95 +83,192 @@ class Application(object): raise NotImplementedError("You must implement __call__") -class Middleware(Application): # pylint: disable-msg=W0223 - """Base WSGI middleware wrapper. These classes require an - application to be initialized that will be called next.""" +class Middleware(Application): # pylint: disable=W0223 + """ + Base WSGI middleware wrapper. These classes require an application to be + initialized that will be called next. By default the middleware will + simply call its wrapped app, or you can override __call__ to customize its + behavior. + """ - def __init__(self, application): # pylint: disable-msg=W0231 + def __init__(self, application): # pylint: disable=W0231 self.application = application + @webob.dec.wsgify + def __call__(self, req): + """Override to implement middleware behavior.""" + return self.application + class Debug(Middleware): - """Helper class that can be insertd into any WSGI application chain + """Helper class that can be inserted into any WSGI application chain to get information about the request and response.""" - def __call__(self, environ, start_response): - for key, value in environ.items(): + @webob.dec.wsgify + def __call__(self, req): + print ("*" * 40) + " REQUEST ENVIRON" + for key, value in req.environ.items(): print key, "=", value print - wrapper = debug_start_response(start_response) - return debug_print_body(self.application(environ, wrapper)) - + resp = req.get_response(self.application) -def debug_start_response(start_response): - """Wrap the start_response to capture when called.""" - - def wrapper(status, headers, exc_info=None): - """Print out all headers when start_response is called.""" - print status - for (key, value) in headers: + print ("*" * 40) + " RESPONSE HEADERS" + for (key, value) in resp.headers: print key, "=", value print - start_response(status, headers, exc_info) - - return wrapper - -def debug_print_body(body): - """Print the body of the response as it is sent back.""" + resp.app_iter = self.print_generator(resp.app_iter) - class Wrapper(object): - """Iterate through all the body parts and print before returning.""" + return resp - def __iter__(self): - for part in body: - sys.stdout.write(part) - sys.stdout.flush() - yield part - print + @staticmethod + def print_generator(app_iter): + """ + Iterator that prints the contents of a wrapper string iterator + when iterated. + """ + print ("*" * 40) + "BODY" + for part in app_iter: + sys.stdout.write(part) + sys.stdout.flush() + yield part + print - return Wrapper() +class Router(object): + """ + WSGI middleware that maps incoming requests to WSGI apps. + """ -class ParsedRoutes(Middleware): - """Processed parsed routes from routes.middleware.RoutesMiddleware - and call either the controller if found or the default application - otherwise.""" + def __init__(self, mapper): + """ + Create a router for the given routes.Mapper. - def __call__(self, environ, start_response): - if environ['routes.route'] is None: - return self.application(environ, start_response) - app = environ['wsgiorg.routing_args'][1]['controller'] - return app(environ, start_response) + Each route in `mapper` must specify a 'controller', which is a + WSGI app to call. You'll probably want to specify an 'action' as + well and have your controller be a wsgi.Controller, who will route + the request to the action method. + Examples: + mapper = routes.Mapper() + sc = ServerController() -class Router(Middleware): # pylint: disable-msg=R0921 - """Wrapper to help setup routes.middleware.RoutesMiddleware.""" + # Explicit mapping of one route to a controller+action + mapper.connect(None, "/svrlist", controller=sc, action="list") - def __init__(self, application): - self.map = routes.Mapper() - self._build_map() - application = ParsedRoutes(application) - application = routes.middleware.RoutesMiddleware(application, self.map) - super(Router, self).__init__(application) + # Actions are all implicitly defined + mapper.resource("server", "servers", controller=sc) - def __call__(self, environ, start_response): - return self.application(environ, start_response) + # Pointing to an arbitrary WSGI app. You can specify the + # {path_info:.*} parameter so the target app can be handed just that + # section of the URL. + mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp()) + """ + self.map = mapper + self._router = routes.middleware.RoutesMiddleware(self._dispatch, + self.map) - def _build_map(self): - """Method to create new connections for the routing map.""" - raise NotImplementedError("You must implement _build_map") + @webob.dec.wsgify + def __call__(self, req): + """ + Route the incoming request to a controller based on self.map. + If no match, return a 404. + """ + return self._router - def _connect(self, *args, **kwargs): - """Wrapper for the map.connect method.""" - self.map.connect(*args, **kwargs) + @webob.dec.wsgify + def _dispatch(self, req): + """ + Called by self._router after matching the incoming request to a route + and putting the information into req.environ. Either returns 404 + or the routed WSGI app's response. + """ + match = req.environ['wsgiorg.routing_args'][1] + if not match: + return webob.exc.HTTPNotFound() + app = match['controller'] + return app + + +class Controller(object): + """ + WSGI app that reads routing information supplied by RoutesMiddleware + and calls the requested action method upon itself. All action methods + must, in addition to their normal parameters, accept a 'req' argument + which is the incoming webob.Request. + """ + @webob.dec.wsgify + def __call__(self, req): + """ + Call the method specified in req.environ by RoutesMiddleware. + """ + arg_dict = req.environ['wsgiorg.routing_args'][1] + action = arg_dict['action'] + method = getattr(self, action) + del arg_dict['controller'] + del arg_dict['action'] + arg_dict['req'] = req + return method(**arg_dict) -def route_args(application): - """Decorator to make grabbing routing args more convenient.""" +class Serializer(object): + """ + Serializes a dictionary to a Content Type specified by a WSGI environment. + """ - def wrapper(self, req): - """Call application with req and parsed routing args from.""" - return application(self, req, req.environ['wsgiorg.routing_args'][1]) + def __init__(self, environ, metadata=None): + """ + Create a serializer based on the given WSGI environment. + 'metadata' is an optional dict mapping MIME types to information + needed to serialize a dictionary to that type. + """ + self.environ = environ + self.metadata = metadata or {} - return wrapper + def to_content_type(self, data): + """ + Serialize a dictionary into a string. The format of the string + will be decided based on the Content Type requested in self.environ: + by Accept: header, or by URL suffix. + """ + mimetype = 'application/xml' + # TODO(gundlach): determine mimetype from request + + if mimetype == 'application/json': + import json + return json.dumps(data) + elif mimetype == 'application/xml': + metadata = self.metadata.get('application/xml', {}) + # We expect data to contain a single key which is the XML root. + root_key = data.keys()[0] + from xml.dom import minidom + doc = minidom.Document() + node = self._to_xml_node(doc, metadata, root_key, data[root_key]) + return node.toprettyxml(indent=' ') + else: + return repr(data) + + def _to_xml_node(self, doc, metadata, nodename, data): + result = doc.createElement(nodename) + if type(data) is list: + singular = metadata.get('plurals', {}).get(nodename, None) + if singular is None: + if nodename.endswith('s'): + singular = nodename[:-1] + else: + singular = 'item' + for item in data: + node = self._to_xml_node(doc, metadata, singular, item) + result.appendChild(node) + elif type(data) is dict: + attrs = metadata.get('attributes', {}).get(nodename, {}) + for k,v in data.items(): + if k in attrs: + result.setAttribute(k, str(v)) + else: + node = self._to_xml_node(doc, metadata, k, v) + result.appendChild(node) + else: # atom + node = doc.createTextNode(str(data)) + result.appendChild(node) + return result @@ -1,5 +1,9 @@ [Messages Control] -disable-msg=C0103 +disable=C0103 +# TODOs in code comments are fine... +disable=W0511 +# *args and **kwargs are fine +disable=W0142 [Basic] # Variables can be 1 to 31 characters long, with @@ -10,10 +14,6 @@ variable-rgx=[a-z_][a-z0-9_]{0,30}$ # and be lowecased with underscores method-rgx=[a-z_][a-z0-9_]{2,50}$ -[MESSAGES CONTROL] -# TODOs in code comments are fine... -disable-msg=W0511 - [Design] max-public-methods=100 min-public-methods=0 diff --git a/run_tests.py b/run_tests.py index d90ac8175..77aa9088a 100644 --- a/run_tests.py +++ b/run_tests.py @@ -38,11 +38,11 @@ Due to our use of multiprocessing it we frequently get some ignorable 'Interrupted system call' exceptions after test completion. """ + import __main__ import os import sys - from twisted.scripts import trial as trial_script from nova import datastore @@ -65,13 +65,12 @@ from nova.tests.volume_unittest import * FLAGS = flags.FLAGS - flags.DEFINE_bool('flush_db', True, 'Flush the database before running fake tests') - flags.DEFINE_string('tests_stderr', 'run_tests.err.log', - 'Path to where to pipe STDERR during test runs. ' - 'Default = "run_tests.err.log"') + 'Path to where to pipe STDERR during test runs.' + ' Default = "run_tests.err.log"') + if __name__ == '__main__': OptionsClass = twistd.WrapTwistedOptions(trial_script.Options) diff --git a/run_tests.sh b/run_tests.sh index 85d7c8834..6ea40d95e 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,12 +1,66 @@ -#!/bin/bash +#!/bin/bash + +function usage { + echo "Usage: $0 [OPTION]..." + echo "Run Nova's test suite(s)" + echo "" + echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" + echo " -h, --help Print this usage message" + echo "" + echo "Note: with no options specified, the script will try to run the tests in a virtual environment," + echo " If no virtualenv is found, the script will ask if you would like to create one. If you " + echo " prefer to run tests NOT in a virtual environment, simply pass the -N option." + exit +} + +function process_options { + array=$1 + elements=${#array[@]} + for (( x=0;x<$elements;x++)); do + process_option ${array[${x}]} + done +} + +function process_option { + option=$1 + case $option in + -h|--help) usage;; + -V|--virtual-env) let always_venv=1; let never_venv=0;; + -N|--no-virtual-env) let always_venv=0; let never_venv=1;; + esac +} venv=.nova-venv with_venv=tools/with_venv.sh +always_venv=0 +never_venv=0 +options=("$@") + +process_options $options + +if [ $never_venv -eq 1 ]; then + # Just run the test suites in current environment + python run_tests.py + exit +fi if [ -e ${venv} ]; then ${with_venv} python run_tests.py $@ else - echo "No virtual environment found...creating one" - python tools/install_venv.py + if [ $always_venv -eq 1 ]; then + # Automatically install the virtualenv + python tools/install_venv.py + else + echo -e "No virtual environment found...create one? (Y/n) \c" + read use_ve + if [ "x$use_ve" = "xY" ]; then + # Install the virtualenv and run the test suite in it + python tools/install_venv.py + else + python run_tests.py + exit + fi + fi ${with_venv} python run_tests.py $@ fi diff --git a/tools/install_venv.py b/tools/install_venv.py index e1a270638..4e775eb33 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -38,15 +38,16 @@ def die(message, *args): def run_command(cmd, redirect_output=True, error_ok=False): - """Runs a command in an out-of-process shell, returning the - output of that command + """ + Runs a command in an out-of-process shell, returning the + output of that command. Working directory is ROOT. """ if redirect_output: stdout = subprocess.PIPE else: stdout = None - proc = subprocess.Popen(cmd, stdout=stdout) + proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) output = proc.communicate()[0] if not error_ok and proc.returncode != 0: die('Command "%s" failed.\n%s', ' '.join(cmd), output) @@ -94,6 +95,12 @@ def install_dependencies(venv=VENV): redirect_output=False) + # Tell the virtual env how to "import nova" + pathfile=os.path.join(venv, "lib", "python2.6", "site-packages", "nova.pth") + f=open(pathfile, 'w') + f.write("%s\n" % ROOT) + + def print_help(): help = """ Nova development environment setup is complete. diff --git a/tools/pip-requires b/tools/pip-requires index c173d6221..28af7bcb9 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -11,7 +11,9 @@ lockfile==0.8 python-daemon==1.5.5 python-gflags==1.3 redis==2.0.0 +routes==1.12.3 tornado==1.0 +webob==0.9.8 wsgiref==0.1.2 zope.interface==3.6.1 mox==0.5.0 |
