diff --git a/Authors b/Authors index 864679929ccf..310c0c8b9f75 100644 --- a/Authors +++ b/Authors @@ -18,6 +18,7 @@ Chiradeep Vittal Chmouel Boudjnah Chris Behrens Christian Berendt +Christopher MacGown Chuck Short Cory Wright Dan Prince diff --git a/bin/nova-ajax-console-proxy b/bin/nova-ajax-console-proxy index 2329581a23ec..0a789b4b9c13 100755 --- a/bin/nova-ajax-console-proxy +++ b/bin/nova-ajax-console-proxy @@ -24,7 +24,6 @@ from eventlet import greenthread from eventlet.green import urllib2 import exceptions -import gettext import os import sys import time @@ -38,11 +37,11 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging from nova import rpc +from nova import service from nova import utils from nova import wsgi @@ -141,5 +140,5 @@ if __name__ == '__main__': acp = AjaxConsoleProxy() acp.register_listeners() server = wsgi.Server("AJAX Console Proxy", acp, port=acp_port) - server.start() - server.wait() + service.serve(server) + service.wait() diff --git a/bin/nova-api b/bin/nova-api index fe8e833660b5..d8635978e97c 100755 --- a/bin/nova-api +++ b/bin/nova-api @@ -19,12 +19,14 @@ """Starter script for Nova API. -Starts both the EC2 and OpenStack APIs in separate processes. +Starts both the EC2 and OpenStack APIs in separate greenthreads. """ +import eventlet +eventlet.monkey_patch() + import os -import signal import sys @@ -33,32 +35,19 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath( if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): sys.path.insert(0, possible_topdir) -import nova.service -import nova.utils from nova import flags - - -FLAGS = flags.FLAGS - - -def main(): - """Launch EC2 and OSAPI services.""" - nova.utils.Bootstrapper.bootstrap_binary(sys.argv) - - launcher = nova.service.Launcher() - - for api in FLAGS.enabled_apis: - service = nova.service.WSGIService(api) - launcher.launch_service(service) - - signal.signal(signal.SIGTERM, lambda *_: launcher.stop()) - - try: - launcher.wait() - except KeyboardInterrupt: - launcher.stop() - +from nova import log as logging +from nova import service +from nova import utils if __name__ == '__main__': - sys.exit(main()) + utils.default_flagfile() + flags.FLAGS(sys.argv) + logging.setup() + utils.monkey_patch() + servers = [] + for api in flags.FLAGS.enabled_apis: + servers.append(service.WSGIService(api)) + service.serve(*servers) + service.wait() diff --git a/bin/nova-api-ec2 b/bin/nova-api-ec2 new file mode 100755 index 000000000000..9f82a69e4ae9 --- /dev/null +++ b/bin/nova-api-ec2 @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# 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. + +"""Starter script for Nova EC2 API.""" + +import eventlet +eventlet.monkey_patch() + +import os +import sys + + +possible_topdir = os.path.normpath(os.path.join(os.path.abspath( + sys.argv[0]), os.pardir, os.pardir)) +if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): + sys.path.insert(0, possible_topdir) + + +from nova import flags +from nova import log as logging +from nova import service +from nova import utils + +if __name__ == '__main__': + utils.default_flagfile() + flags.FLAGS(sys.argv) + logging.setup() + utils.monkey_patch() + server = service.WSGIService('ec2') + service.serve(server) + service.wait() diff --git a/bin/nova-api-os b/bin/nova-api-os new file mode 100755 index 000000000000..83a808987d65 --- /dev/null +++ b/bin/nova-api-os @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# 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. + +"""Starter script for Nova OS API.""" + +import eventlet +eventlet.monkey_patch() + +import os +import sys + + +possible_topdir = os.path.normpath(os.path.join(os.path.abspath( + sys.argv[0]), os.pardir, os.pardir)) +if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): + sys.path.insert(0, possible_topdir) + + +from nova import flags +from nova import log as logging +from nova import service +from nova import utils + +if __name__ == '__main__': + utils.default_flagfile() + flags.FLAGS(sys.argv) + logging.setup() + utils.monkey_patch() + server = service.WSGIService('osapi') + service.serve(server) + service.wait() diff --git a/bin/nova-compute b/bin/nova-compute index cd7c78def0ca..0c69a81290a7 100755 --- a/bin/nova-compute +++ b/bin/nova-compute @@ -22,7 +22,6 @@ import eventlet eventlet.monkey_patch() -import gettext import os import sys @@ -34,7 +33,6 @@ POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')): sys.path.insert(0, POSSIBLE_TOPDIR) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -45,5 +43,7 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - service.serve() + utils.monkey_patch() + server = service.Service.create(binary='nova-compute') + service.serve(server) service.wait() diff --git a/bin/nova-console b/bin/nova-console index 40608b995062..22f6ef171136 100755 --- a/bin/nova-console +++ b/bin/nova-console @@ -21,7 +21,6 @@ import eventlet eventlet.monkey_patch() -import gettext import os import sys @@ -33,7 +32,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -44,5 +42,6 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - service.serve() + server = service.Service.create(binary='nova-console') + service.serve(server) service.wait() diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge index a47ea7a7612f..1c9ae951e91c 100755 --- a/bin/nova-dhcpbridge +++ b/bin/nova-dhcpbridge @@ -52,7 +52,7 @@ flags.DECLARE('update_dhcp_on_disassociate', 'nova.network.manager') LOG = logging.getLogger('nova.dhcpbridge') -def add_lease(mac, ip_address, _interface): +def add_lease(mac, ip_address): """Set the IP that was assigned by the DHCP server.""" if FLAGS.fake_rabbit: LOG.debug(_("leasing ip")) @@ -66,13 +66,13 @@ def add_lease(mac, ip_address, _interface): "args": {"address": ip_address}}) -def old_lease(mac, ip_address, interface): +def old_lease(mac, ip_address): """Update just as add lease.""" LOG.debug(_("Adopted old lease or got a change of mac")) - add_lease(mac, ip_address, interface) + add_lease(mac, ip_address) -def del_lease(mac, ip_address, _interface): +def del_lease(mac, ip_address): """Called when a lease expires.""" if FLAGS.fake_rabbit: LOG.debug(_("releasing ip")) @@ -99,8 +99,6 @@ def main(): utils.default_flagfile(flagfile) argv = FLAGS(sys.argv) logging.setup() - # check ENV first so we don't break any older deploys - network_id = int(os.environ.get('NETWORK_ID')) if int(os.environ.get('TESTING', '0')): from nova.tests import fake_flags @@ -115,11 +113,19 @@ def main(): if action in ['add', 'del', 'old']: mac = argv[2] ip = argv[3] - msg = _("Called %(action)s for mac %(mac)s with ip %(ip)s" - " on interface %(interface)s") % locals() + msg = _("Called '%(action)s' for mac '%(mac)s' with ip '%(ip)s'") % \ + {"action": action, + "mac": mac, + "ip": ip} LOG.debug(msg) - globals()[action + '_lease'](mac, ip, interface) + globals()[action + '_lease'](mac, ip) else: + try: + network_id = int(os.environ.get('NETWORK_ID')) + except TypeError: + LOG.error(_("Environment variable 'NETWORK_ID' must be set.")) + sys.exit(1) + print init_leases(network_id) if __name__ == "__main__": diff --git a/bin/nova-direct-api b/bin/nova-direct-api index c6cf9b2ff99c..106e89ba9eab 100755 --- a/bin/nova-direct-api +++ b/bin/nova-direct-api @@ -20,7 +20,9 @@ """Starter script for Nova Direct API.""" -import gettext +import eventlet +eventlet.monkey_patch() + import os import sys @@ -32,12 +34,12 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import compute from nova import flags from nova import log as logging from nova import network +from nova import service from nova import utils from nova import volume from nova import wsgi @@ -97,5 +99,6 @@ if __name__ == '__main__': with_auth, host=FLAGS.direct_host, port=FLAGS.direct_port) - server.start() - server.wait() + + service.serve(server) + service.wait() diff --git a/bin/nova-manage b/bin/nova-manage index 8e6419c0b7a9..2e0bd0ecbad8 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -134,7 +134,7 @@ class VpnCommands(object): help='Project name') def list(self, project=None): """Print a listing of the VPN data for one or all projects.""" - + print "WARNING: This method only works with deprecated auth" print "%-12s\t" % 'project', print "%-20s\t" % 'ip:port', print "%-20s\t" % 'private_ip', @@ -170,17 +170,22 @@ class VpnCommands(object): def spawn(self): """Run all VPNs.""" + print "WARNING: This method only works with deprecated auth" for p in reversed(self.manager.get_projects()): if not self._vpn_for(p.id): print 'spawning %s' % p.id - self.pipe.launch_vpn_instance(p.id) + self.pipe.launch_vpn_instance(p.id, p.project_manager_id) time.sleep(10) @args('--project', dest="project_id", metavar='', help='Project name') - def run(self, project_id): - """Start the VPN for a given project.""" - self.pipe.launch_vpn_instance(project_id) + @args('--user', dest="user_id", metavar='', help='User name') + def run(self, project_id, user_id): + """Start the VPN for a given project and user.""" + if not user_id: + print "WARNING: This method only works with deprecated auth" + user_id = self.manager.get_project(project_id).project_manager_id + self.pipe.launch_vpn_instance(project_id, user_id) @args('--project', dest="project_id", metavar='', help='Project name') @@ -195,10 +200,6 @@ class VpnCommands(object): """ # TODO(tr3buchet): perhaps this shouldn't update all networks # associated with a project in the future - project = self.manager.get_project(project_id) - if not project: - print 'No project %s' % (project_id) - return admin_context = context.get_admin_context() networks = db.project_get_networks(admin_context, project_id) for network in networks: @@ -611,6 +612,8 @@ class FixedIpCommands(object): try: fixed_ip = db.fixed_ip_get_by_address(ctxt, address) + if fixed_ip is None: + raise exception.NotFound('Could not find address') db.fixed_ip_update(ctxt, fixed_ip['address'], {'reserved': reserved}) except exception.NotFound as ex: @@ -763,23 +766,26 @@ class NetworkCommands(object): def list(self): """List all created networks""" - print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % ( - _('IPv4'), - _('IPv6'), - _('start address'), - _('DNS1'), - _('DNS2'), - _('VlanID'), - 'project') + _fmt = "%-5s\t%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" + print _fmt % (_('id'), + _('IPv4'), + _('IPv6'), + _('start address'), + _('DNS1'), + _('DNS2'), + _('VlanID'), + _('project'), + _("uuid")) for network in db.network_get_all(context.get_admin_context()): - print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % ( - network.cidr, - network.cidr_v6, - network.dhcp_start, - network.dns1, - network.dns2, - network.vlan, - network.project_id) + print _fmt % (network.id, + network.cidr, + network.cidr_v6, + network.dhcp_start, + network.dns1, + network.dns2, + network.vlan, + network.project_id, + network.uuid) @args('--network', dest="fixed_range", metavar='', help='Network to delete') diff --git a/bin/nova-network b/bin/nova-network index 101761ef78b5..0f1482515bb7 100755 --- a/bin/nova-network +++ b/bin/nova-network @@ -22,7 +22,6 @@ import eventlet eventlet.monkey_patch() -import gettext import os import sys @@ -34,7 +33,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -45,5 +43,7 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - service.serve() + utils.monkey_patch() + server = service.Service.create(binary='nova-network') + service.serve(server) service.wait() diff --git a/bin/nova-objectstore b/bin/nova-objectstore index 4d5aec4450e6..757301c24ddc 100755 --- a/bin/nova-objectstore +++ b/bin/nova-objectstore @@ -17,11 +17,11 @@ # License for the specific language governing permissions and limitations # under the License. -""" - Daemon for nova objectstore. Supports S3 API. -""" +"""Daemon for nova objectstore. Supports S3 API.""" + +import eventlet +eventlet.monkey_patch() -import gettext import os import sys @@ -33,10 +33,10 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging +from nova import service from nova import utils from nova import wsgi from nova.objectstore import s3server @@ -49,10 +49,11 @@ if __name__ == '__main__': utils.default_flagfile() FLAGS(sys.argv) logging.setup() + utils.monkey_patch() router = s3server.S3Application(FLAGS.buckets_path) server = wsgi.Server("S3 Objectstore", router, port=FLAGS.s3_port, host=FLAGS.s3_host) - server.start() - server.wait() + service.serve(server) + service.wait() diff --git a/bin/nova-scheduler b/bin/nova-scheduler index 0c205a80f377..c1033a3040a2 100755 --- a/bin/nova-scheduler +++ b/bin/nova-scheduler @@ -45,5 +45,7 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - service.serve() + utils.monkey_patch() + server = service.Service.create(binary='nova-scheduler') + service.serve(server) service.wait() diff --git a/bin/nova-vncproxy b/bin/nova-vncproxy index bdbb30a7fcf6..dc08e2433465 100755 --- a/bin/nova-vncproxy +++ b/bin/nova-vncproxy @@ -19,7 +19,8 @@ """VNC Console Proxy Server.""" import eventlet -import gettext +eventlet.monkey_patch() + import os import sys @@ -29,7 +30,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -41,7 +41,7 @@ from nova.vnc import auth from nova.vnc import proxy -LOG = logging.getLogger('nova.vnc-proxy') +LOG = logging.getLogger('nova.vncproxy') FLAGS = flags.FLAGS @@ -81,7 +81,7 @@ if __name__ == "__main__": FLAGS(sys.argv) logging.setup() - LOG.audit(_("Starting nova-vnc-proxy node (version %s)"), + LOG.audit(_("Starting nova-vncproxy node (version %s)"), version.version_string_with_vcs()) if not (os.path.exists(FLAGS.vncproxy_wwwroot) and @@ -107,13 +107,10 @@ if __name__ == "__main__": else: with_auth = auth.VNCNovaAuthMiddleware(with_logging) - service.serve() - server = wsgi.Server("VNC Proxy", with_auth, host=FLAGS.vncproxy_host, port=FLAGS.vncproxy_port) - server.start() server.start_tcp(handle_flash_socket_policy, 843, host=FLAGS.vncproxy_host) - - server.wait() + service.serve(server) + service.wait() diff --git a/bin/nova-volume b/bin/nova-volume index 8dcdbc500e76..8caa0f44a994 100755 --- a/bin/nova-volume +++ b/bin/nova-volume @@ -22,7 +22,6 @@ import eventlet eventlet.monkey_patch() -import gettext import os import sys @@ -34,7 +33,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -gettext.install('nova', unicode=1) from nova import flags from nova import log as logging @@ -45,5 +43,7 @@ if __name__ == '__main__': utils.default_flagfile() flags.FLAGS(sys.argv) logging.setup() - service.serve() + utils.monkey_patch() + server = service.Service.create(binary='nova-volume') + service.serve(server) service.wait() diff --git a/doc/source/devref/distributed_scheduler.rst b/doc/source/devref/distributed_scheduler.rst index e33fda4d2386..c63e62f7f7a1 100644 --- a/doc/source/devref/distributed_scheduler.rst +++ b/doc/source/devref/distributed_scheduler.rst @@ -31,9 +31,9 @@ This is the purpose of the Distributed Scheduler (DS). The DS utilizes the Capab So, how does this all work? -This document will explain the strategy employed by the `ZoneAwareScheduler` and its derivations. You should read the :doc:`devguide/zones` documentation before reading this. +This document will explain the strategy employed by the `BaseScheduler`, which is the base for all schedulers designed to work across zones, and its derivations. You should read the :doc:`devguide/zones` documentation before reading this. - .. image:: /images/zone_aware_scheduler.png + .. image:: /images/base_scheduler.png Costs & Weights --------------- @@ -52,32 +52,32 @@ This Weight is computed for each Instance requested. If the customer asked for 1 .. image:: /images/costs_weights.png -nova.scheduler.zone_aware_scheduler.ZoneAwareScheduler +nova.scheduler.base_scheduler.BaseScheduler ------------------------------------------------------ -As we explained in the Zones documentation, each Scheduler has a `ZoneManager` object that collects "Capabilities" about child Zones and each of the services running in the current Zone. The `ZoneAwareScheduler` uses this information to make its decisions. +As we explained in the Zones documentation, each Scheduler has a `ZoneManager` object that collects "Capabilities" about child Zones and each of the services running in the current Zone. The `BaseScheduler` uses this information to make its decisions. Here is how it works: 1. The compute nodes are filtered and the nodes remaining are weighed. - 2. Filtering the hosts is a simple matter of ensuring the compute node has ample resources (CPU, RAM, Disk, etc) to fulfil the request. + 2. Filtering the hosts is a simple matter of ensuring the compute node has ample resources (CPU, RAM, Disk, etc) to fulfil the request. 3. Weighing of the remaining compute nodes assigns a number based on their suitability for the request. 4. The same request is sent to each child Zone and step #1 is done there too. The resulting weighted list is returned to the parent. 5. The parent Zone sorts and aggregates all the weights and a final build plan is constructed. 6. The build plan is executed upon. Concurrently, instance create requests are sent to each of the selected hosts, be they local or in a child zone. Child Zones may forward the requests to their child Zones as needed. - .. image:: /images/zone_aware_overview.png + .. image:: /images/zone_overview.png -`ZoneAwareScheduler` by itself is not capable of handling all the provisioning itself. Derived classes are used to select which host filtering and weighing strategy will be used. +`BaseScheduler` by itself is not capable of handling all the provisioning itself. You should also specify the filter classes and weighting classes to be used in determining which host is selected for new instance creation. Filtering and Weighing ---------------------- -The filtering (excluding compute nodes incapable of fulfilling the request) and weighing (computing the relative "fitness" of a compute node to fulfill the request) rules used are very subjective operations ... Service Providers will probably have a very different set of filtering and weighing rules than private cloud administrators. The filtering and weighing aspects of the `ZoneAwareScheduler` are flexible and extensible. +The filtering (excluding compute nodes incapable of fulfilling the request) and weighing (computing the relative "fitness" of a compute node to fulfill the request) rules used are very subjective operations ... Service Providers will probably have a very different set of filtering and weighing rules than private cloud administrators. The filtering and weighing aspects of the `BaseScheduler` are flexible and extensible. .. image:: /images/filtering.png Requesting a new instance ------------------------- -Prior to the `ZoneAwareScheduler`, to request a new instance, a call was made to `nova.compute.api.create()`. The type of instance created depended on the value of the `InstanceType` record being passed in. The `InstanceType` determined the amount of disk, CPU, RAM and network required for the instance. Administrators can add new `InstanceType` records to suit their needs. For more complicated instance requests we need to go beyond the default fields in the `InstanceType` table. +Prior to the `BaseScheduler`, to request a new instance, a call was made to `nova.compute.api.create()`. The type of instance created depended on the value of the `InstanceType` record being passed in. The `InstanceType` determined the amount of disk, CPU, RAM and network required for the instance. Administrators can add new `InstanceType` records to suit their needs. For more complicated instance requests we need to go beyond the default fields in the `InstanceType` table. `nova.compute.api.create()` performed the following actions: 1. it validated all the fields passed into it. @@ -89,11 +89,11 @@ Prior to the `ZoneAwareScheduler`, to request a new instance, a call was made to .. image:: /images/nova.compute.api.create.png -Generally, the standard schedulers (like `ChanceScheduler` and `AvailabilityZoneScheduler`) only operate in the current Zone. They have no concept of child Zones. +Generally, the simplest schedulers (like `ChanceScheduler` and `AvailabilityZoneScheduler`) only operate in the current Zone. They have no concept of child Zones. The problem with this approach is each request is scattered amongst each of the schedulers. If we are asking for 1000 instances, each scheduler gets the requests one-at-a-time. There is no possability of optimizing the requests to take into account all 1000 instances as a group. We call this Single-Shot vs. All-at-Once. -For the `ZoneAwareScheduler` we need to use the All-at-Once approach. We need to consider all the hosts across all the Zones before deciding where they should reside. In order to handle this we have a new method `nova.compute.api.create_all_at_once()`. This method does things a little differently: +For the `BaseScheduler` we need to use the All-at-Once approach. We need to consider all the hosts across all the Zones before deciding where they should reside. In order to handle this we have a new method `nova.compute.api.create_all_at_once()`. This method does things a little differently: 1. it validates all the fields passed into it. 2. it creates a single `reservation_id` for all of instances created. This is a UUID. 3. it creates a single `run_instance` request in the scheduler queue @@ -109,21 +109,19 @@ For the `ZoneAwareScheduler` we need to use the All-at-Once approach. We need to The Catch --------- -This all seems pretty straightforward but, like most things, there's a catch. Zones are expected to operate in complete isolation from each other. Each Zone has its own AMQP service, database and set of Nova services. But, for security reasons Zones should never leak information about the architectural layout internally. That means Zones cannot leak information about hostnames or service IP addresses outside of its world. +This all seems pretty straightforward but, like most things, there's a catch. Zones are expected to operate in complete isolation from each other. Each Zone has its own AMQP service, database and set of Nova services. But for security reasons Zones should never leak information about the architectural layout internally. That means Zones cannot leak information about hostnames or service IP addresses outside of its world. -When `POST /zones/select` is called to estimate which compute node to use, time passes until the `POST /servers` call is issued. If we only passed the weight back from the `select` we would have to re-compute the appropriate compute node for the create command ... and we could end up with a different host. Somehow we need to remember the results of our computations and pass them outside of the Zone. Now, we could store this information in the local database and return a reference to it, but remember that the vast majority of weights are going to be ignored. Storing them in the database would result in a flood of disk access and then we have to clean up all these entries periodically. Recall that there are going to be many many `select` calls issued to child Zones asking for estimates. +When `POST /zones/select` is called to estimate which compute node to use, time passes until the `POST /servers` call is issued. If we only passed the weight back from the `select` we would have to re-compute the appropriate compute node for the create command ... and we could end up with a different host. Somehow we need to remember the results of our computations and pass them outside of the Zone. Now, we could store this information in the local database and return a reference to it, but remember that the vast majority of weights are going to be ignored. Storing them in the database would result in a flood of disk access and then we have to clean up all these entries periodically. Recall that there are going to be many, many `select` calls issued to child Zones asking for estimates. -Instead, we take a rather innovative approach to the problem. We encrypt all the child zone internal details and pass them back the to parent Zone. If the parent zone decides to use a child Zone for the instance it simply passes the encrypted data back to the child during the `POST /servers` call as an extra parameter. The child Zone can then decrypt the hint and go directly to the Compute node previously selected. If the estimate isn't used, it is simply discarded by the parent. It's for this reason that it is so important that each Zone defines a unique encryption key via `--build_plan_encryption_key` +Instead, we take a rather innovative approach to the problem. We encrypt all the child Zone internal details and pass them back the to parent Zone. In the case of a nested Zone layout, each nesting layer will encrypt the data from all of its children and pass that to its parent Zone. In the case of nested child Zones, each Zone re-encrypts the weighted list results and passes those values to the parent. Every Zone interface adds another layer of encryption, using its unique key. -In the case of nested child Zones, each Zone re-encrypts the weighted list results and passes those values to the parent. +Once a host is selected, it will either be local to the Zone that received the initial API call, or one of its child Zones. In the latter case, the parent Zone it simply passes the encrypted data for the selected host back to each of its child Zones during the `POST /servers` call as an extra parameter. If the child Zone can decrypt the data, then it is the correct Zone for the selected host; all other Zones will not be able to decrypt the data and will discard the request. This is why it is critical that each Zone has a unique value specified in its config in `--build_plan_encryption_key`: it controls the ability to locate the selected host without having to hard-code path information or other identifying information. The child Zone can then act on the decrypted data and either go directly to the Compute node previously selected if it is located in that Zone, or repeat the process with its child Zones until the target Zone containing the selected host is reached. -Throughout the `nova.api.openstack.servers`, `nova.api.openstack.zones`, `nova.compute.api.create*` and `nova.scheduler.zone_aware_scheduler` code you'll see references to `blob` and `child_blob`. These are the encrypted hints about which Compute node to use. +Throughout the `nova.api.openstack.servers`, `nova.api.openstack.zones`, `nova.compute.api.create*` and `nova.scheduler.base_scheduler` code you'll see references to `blob` and `child_blob`. These are the encrypted hints about which Compute node to use. Reservation IDs --------------- -NOTE: The features described in this section are related to the up-coming 'merge-4' branch. - The OpenStack API allows a user to list all the instances they own via the `GET /servers/` command or the details on a particular instance via `GET /servers/###`. This mechanism is usually sufficient since OS API only allows for creating one instance at a time, unlike the EC2 API which allows you to specify a quantity of instances to be created. NOTE: currently the `GET /servers` command is not Zone-aware since all operations done in child Zones are done via a single administrative account. Therefore, asking a child Zone to `GET /servers` would return all the active instances ... and that would not be what the user intended. Later, when the Keystone Auth system is integrated with Nova, this functionality will be enabled. @@ -137,23 +135,23 @@ Finally, we need to give the user a way to get information on each of the instan Host Filter ----------- -As we mentioned earlier, filtering hosts is a very deployment-specific process. Service Providers may have a different set of criteria for filtering Compute nodes than a University. To faciliate this the `nova.scheduler.host_filter` module supports a variety of filtering strategies as well as an easy means for plugging in your own algorithms. +As we mentioned earlier, filtering hosts is a very deployment-specific process. Service Providers may have a different set of criteria for filtering Compute nodes than a University. To faciliate this the `nova.scheduler.filters` module supports a variety of filtering strategies as well as an easy means for plugging in your own algorithms. -The filter used is determined by the `--default_host_filter` flag, which points to a Python Class. By default this flag is set to `nova.scheduler.host_filter.AllHostsFilter` which simply returns all available hosts. But there are others: +The filter used is determined by the `--default_host_filters` flag, which points to a Python Class. By default this flag is set to `[AllHostsFilter]` which simply returns all available hosts. But there are others: - * `nova.scheduler.host_filter.InstanceTypeFilter` provides host filtering based on the memory and disk size specified in the `InstanceType` record passed into `run_instance`. + * `InstanceTypeFilter` provides host filtering based on the memory and disk size specified in the `InstanceType` record passed into `run_instance`. - * `nova.scheduler.host_filter.JSONFilter` filters hosts based on simple JSON expression grammar. Using a LISP-like JSON structure the caller can request instances based on criteria well beyond what `InstanceType` specifies. See `nova.tests.test_host_filter` for examples. + * `JSONFilter` filters hosts based on simple JSON expression grammar. Using a LISP-like JSON structure the caller can request instances based on criteria well beyond what `InstanceType` specifies. See `nova.tests.test_host_filter` for examples. -To create your own `HostFilter` the user simply has to derive from `nova.scheduler.host_filter.HostFilter` and implement two methods: `instance_type_to_filter` and `filter_hosts`. Since Nova is currently dependent on the `InstanceType` structure, the `instance_type_to_filter` method should take an `InstanceType` and turn it into an internal data structure usable by your filter. This is for backward compatibility with existing OpenStack and EC2 API calls. If you decide to create your own call for creating instances not based on `Flavors` or `InstanceTypes` you can ignore this method. The real work is done in `filter_hosts` which must return a list of host tuples for each appropriate host. The set of all available hosts is in the `ZoneManager` object passed into the call as well as the filter query. The host tuple contains (``, ``) where `` is whatever you want it to be. +To create your own `HostFilter` the user simply has to derive from `nova.scheduler.filters.AbstractHostFilter` and implement two methods: `instance_type_to_filter` and `filter_hosts`. Since Nova is currently dependent on the `InstanceType` structure, the `instance_type_to_filter` method should take an `InstanceType` and turn it into an internal data structure usable by your filter. This is for backward compatibility with existing OpenStack and EC2 API calls. If you decide to create your own call for creating instances not based on `Flavors` or `InstanceTypes` you can ignore this method. The real work is done in `filter_hosts` which must return a list of host tuples for each appropriate host. The set of available hosts is in the `host_list` parameter passed into the call as well as the filter query. The host tuple contains (``, ``) where `` is whatever you want it to be. By default, it is the capabilities reported by the host. Cost Scheduler Weighing ----------------------- -Every `ZoneAwareScheduler` derivation must also override the `weigh_hosts` method. This takes the list of filtered hosts (generated by the `filter_hosts` method) and returns a list of weight dicts. The weight dicts must contain two keys: `weight` and `hostname` where `weight` is simply an integer (lower is better) and `hostname` is the name of the host. The list does not need to be sorted, this will be done by the `ZoneAwareScheduler` base class when all the results have been assembled. +Every `BaseScheduler` subclass should also override the `weigh_hosts` method. This takes the list of filtered hosts (generated by the `filter_hosts` method) and returns a list of weight dicts. The weight dicts must contain two keys: `weight` and `hostname` where `weight` is simply an integer (lower is better) and `hostname` is the name of the host. The list does not need to be sorted, this will be done by the `BaseScheduler` when all the results have been assembled. -Simple Zone Aware Scheduling +Simple Scheduling Across Zones ---------------------------- -The easiest way to get started with the `ZoneAwareScheduler` is to use the `nova.scheduler.host_filter.HostFilterScheduler`. This scheduler uses the default Host Filter and the `weight_hosts` method simply returns a weight of 1 for all hosts. But, from this, you can see calls being routed from Zone to Zone and follow the flow of things. +The `BaseScheduler` uses the default `filter_hosts` method, which will use either any filters specified in the request's `filter` parameter, or, if that is not specified, the filters specified in the `FLAGS.default_host_filters` setting. Its `weight_hosts` method simply returns a weight of 1 for all hosts. But, from this, you can see calls being routed from Zone to Zone and follow the flow of things. The `--scheduler_driver` flag is how you specify the scheduler class name. @@ -168,14 +166,14 @@ All this Zone and Distributed Scheduler stuff can seem a little daunting to conf --enable_zone_routing=true --zone_name=zone1 --build_plan_encryption_key=c286696d887c9aa0611bbb3e2025a45b - --scheduler_driver=nova.scheduler.host_filter.HostFilterScheduler - --default_host_filter=nova.scheduler.host_filter.AllHostsFilter + --scheduler_driver=nova.scheduler.base_scheduler.BaseScheduler + --default_host_filter=nova.scheduler.filters.AllHostsFilter `--allow_admin_api` must be set for OS API to enable the new `/zones/*` commands. `--enable_zone_routing` must be set for OS API commands such as `create()`, `pause()` and `delete()` to get routed from Zone to Zone when looking for instances. `--zone_name` is only required in child Zones. The default Zone name is `nova`, but you may want to name your child Zones something useful. Duplicate Zone names are not an issue. `build_plan_encryption_key` is the SHA-256 key for encrypting/decrypting the Host information when it leaves a Zone. Be sure to change this key for each Zone you create. Do not duplicate keys. -`scheduler_driver` is the real workhorse of the operation. For Distributed Scheduler, you need to specify a class derived from `nova.scheduler.zone_aware_scheduler.ZoneAwareScheduler`. +`scheduler_driver` is the real workhorse of the operation. For Distributed Scheduler, you need to specify a class derived from `nova.scheduler.base_scheduler.BaseScheduler`. `default_host_filter` is the host filter to be used for filtering candidate Compute nodes. Some optional flags which are handy for debugging are: diff --git a/doc/source/images/base_scheduler.png b/doc/source/images/base_scheduler.png new file mode 100644 index 000000000000..75d029338b5e Binary files /dev/null and b/doc/source/images/base_scheduler.png differ diff --git a/doc/source/images/zone_overview.png b/doc/source/images/zone_overview.png new file mode 100755 index 000000000000..cc891df0a4c7 Binary files /dev/null and b/doc/source/images/zone_overview.png differ diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index abe8c20c439f..dafdef877fe8 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -19,8 +19,11 @@ use = egg:Paste#urlmap /1.0: ec2metadata [pipeline:ec2cloud] -pipeline = logrequest authenticate cloudrequest authorizer ec2executor -#pipeline = logrequest ec2lockout authenticate cloudrequest authorizer ec2executor +pipeline = logrequest ec2noauth cloudrequest authorizer ec2executor +# NOTE(vish): use the following pipeline for deprecated auth +#pipeline = logrequest authenticate cloudrequest authorizer ec2executor +# NOTE(vish): use the following pipeline for keystone +# pipeline = logrequest totoken authtoken keystonecontext cloudrequest authorizer ec2executor [pipeline:ec2admin] pipeline = logrequest authenticate adminrequest authorizer ec2executor @@ -37,6 +40,12 @@ paste.filter_factory = nova.api.ec2:RequestLogging.factory [filter:ec2lockout] paste.filter_factory = nova.api.ec2:Lockout.factory +[filter:totoken] +paste.filter_factory = nova.api.ec2:ToToken.factory + +[filter:ec2noauth] +paste.filter_factory = nova.api.ec2:NoAuth.factory + [filter:authenticate] paste.filter_factory = nova.api.ec2:Authenticate.factory @@ -71,10 +80,18 @@ use = egg:Paste#urlmap /v1.1: openstackapi11 [pipeline:openstackapi10] -pipeline = faultwrap auth ratelimit osapiapp10 +pipeline = faultwrap noauth ratelimit osapiapp10 +# NOTE(vish): use the following pipeline for deprecated auth +# pipeline = faultwrap auth ratelimit osapiapp10 +# NOTE(vish): use the following pipeline for keystone +#pipeline = faultwrap authtoken keystonecontext ratelimit osapiapp10 [pipeline:openstackapi11] -pipeline = faultwrap auth ratelimit extensions osapiapp11 +pipeline = faultwrap noauth ratelimit extensions osapiapp11 +# NOTE(vish): use the following pipeline for deprecated auth +# pipeline = faultwrap auth ratelimit extensions osapiapp11 +# NOTE(vish): use the following pipeline for keystone +# pipeline = faultwrap authtoken keystonecontext ratelimit extensions osapiapp11 [filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factory @@ -82,6 +99,9 @@ paste.filter_factory = nova.api.openstack:FaultWrapper.factory [filter:auth] paste.filter_factory = nova.api.openstack.auth:AuthMiddleware.factory +[filter:noauth] +paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory + [filter:ratelimit] paste.filter_factory = nova.api.openstack.limits:RateLimitingMiddleware.factory @@ -99,3 +119,22 @@ pipeline = faultwrap osversionapp [app:osversionapp] paste.app_factory = nova.api.openstack.versions:Versions.factory + +########## +# Shared # +########## + +[filter:keystonecontext] +paste.filter_factory = nova.api.auth:KeystoneContext.factory + +[filter:authtoken] +paste.filter_factory = keystone.middleware.auth_token:filter_factory +service_protocol = http +service_host = 127.0.0.1 +service_port = 808 +auth_host = 127.0.0.1 +auth_port = 5001 +auth_protocol = http +auth_uri = http://127.0.0.1:5000/ +admin_token = 999888777666 + diff --git a/nova/api/auth.py b/nova/api/auth.py new file mode 100644 index 000000000000..cd0d38b3f864 --- /dev/null +++ b/nova/api/auth.py @@ -0,0 +1,76 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 OpenStack, LLC +# +# 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. +""" +Common Auth Middleware. + +""" + +import webob.dec +import webob.exc + +from nova import context +from nova import flags +from nova import wsgi + + +FLAGS = flags.FLAGS +flags.DEFINE_boolean('use_forwarded_for', False, + 'Treat X-Forwarded-For as the canonical remote address. ' + 'Only enable this if you have a sanitizing proxy.') + + +class InjectContext(wsgi.Middleware): + """Add a 'nova.context' to WSGI environ.""" + + def __init__(self, context, *args, **kwargs): + self.context = context + super(InjectContext, self).__init__(*args, **kwargs) + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + req.environ['nova.context'] = self.context + return self.application + + +class KeystoneContext(wsgi.Middleware): + """Make a request context from keystone headers""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + try: + user_id = req.headers['X_USER'] + except KeyError: + return webob.exc.HTTPUnauthorized() + # get the roles + roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')] + project_id = req.headers['X_TENANT'] + # Get the auth token + auth_token = req.headers.get('X_AUTH_TOKEN', + req.headers.get('X_STORAGE_TOKEN')) + + # Build a context, including the auth_token... + remote_address = getattr(req, 'remote_address', '127.0.0.1') + remote_address = req.remote_addr + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + ctx = context.RequestContext(user_id, + project_id, + roles=roles, + auth_token=auth_token, + remote_address=remote_address) + + req.environ['nova.context'] = ctx + return self.application diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 96df97393e96..5430f443dabf 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -20,6 +20,7 @@ Starting point for routing EC2 requests. """ +import httplib2 import webob import webob.dec import webob.exc @@ -37,15 +38,16 @@ from nova.auth import manager FLAGS = flags.FLAGS LOG = logging.getLogger("nova.api") -flags.DEFINE_boolean('use_forwarded_for', False, - 'Treat X-Forwarded-For as the canonical remote address. ' - 'Only enable this if you have a sanitizing proxy.') flags.DEFINE_integer('lockout_attempts', 5, 'Number of failed auths before lockout.') flags.DEFINE_integer('lockout_minutes', 15, 'Number of minutes to lockout if triggered.') flags.DEFINE_integer('lockout_window', 15, 'Number of minutes for lockout window.') +flags.DEFINE_string('keystone_ec2_url', + 'http://localhost:5000/v2.0/ec2tokens', + 'URL to get token from ec2 request.') +flags.DECLARE('use_forwarded_for', 'nova.api.auth') class RequestLogging(wsgi.Middleware): @@ -138,6 +140,70 @@ class Lockout(wsgi.Middleware): return res +class ToToken(wsgi.Middleware): + """Authenticate an EC2 request with keystone and convert to token.""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + # Read request signature and access id. + try: + signature = req.params['Signature'] + access = req.params['AWSAccessKeyId'] + except KeyError: + raise webob.exc.HTTPBadRequest() + + # Make a copy of args for authentication and signature verification. + auth_params = dict(req.params) + # Not part of authentication args + auth_params.pop('Signature') + + # Authenticate the request. + client = httplib2.Http() + creds = {'ec2Credentials': {'access': access, + 'signature': signature, + 'host': req.host, + 'verb': req.method, + 'path': req.path, + 'params': auth_params, + }} + headers = {'Content-Type': 'application/json'}, + resp, content = client.request(FLAGS.keystone_ec2_url, + 'POST', + headers=headers, + body=utils.dumps(creds)) + # NOTE(vish): We could save a call to keystone by + # having keystone return token, tenant, + # user, and roles from this call. + result = utils.loads(content) + # TODO(vish): check for errors + token_id = result['auth']['token']['id'] + + # Authenticated! + req.headers['X-Auth-Token'] = token_id + return self.application + + +class NoAuth(wsgi.Middleware): + """Add user:project as 'nova.context' to WSGI environ.""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + if 'AWSAccessKeyId' not in req.params: + raise webob.exc.HTTPBadRequest() + user_id, _sep, project_id = req.params['AWSAccessKeyId'].partition(':') + project_id = project_id or user_id + remote_address = getattr(req, 'remote_address', '127.0.0.1') + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + ctx = context.RequestContext(user_id, + project_id, + is_admin=True, + remote_address=remote_address) + + req.environ['nova.context'] = ctx + return self.application + + class Authenticate(wsgi.Middleware): """Authenticate an EC2 request and add 'nova.context' to WSGI environ.""" @@ -147,7 +213,7 @@ class Authenticate(wsgi.Middleware): try: signature = req.params['Signature'] access = req.params['AWSAccessKeyId'] - except KeyError, e: + except KeyError: raise webob.exc.HTTPBadRequest() # Make a copy of args for authentication and signature verification. diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index df7876b9d756..dfbbc0a2b577 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -283,8 +283,10 @@ class AdminController(object): # NOTE(vish) import delayed because of __init__.py from nova.cloudpipe import pipelib pipe = pipelib.CloudPipe() + proj = manager.AuthManager().get_project(project) + user_id = proj.project_manager_id try: - pipe.launch_vpn_instance(project) + pipe.launch_vpn_instance(project, user_id) except db.NoMoreNetworks: raise exception.ApiError("Unable to claim IP for VPN instance" ", ensure it isn't running, and try " diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py index 1dc275c905c2..0198bf4909de 100644 --- a/nova/api/ec2/metadatarequesthandler.py +++ b/nova/api/ec2/metadatarequesthandler.py @@ -30,6 +30,7 @@ from nova.api.ec2 import cloud LOG = logging.getLogger('nova.api.ec2.metadata') FLAGS = flags.FLAGS +flags.DECLARE('use_forwarded_for', 'nova.api.auth') class MetadataRequestHandler(wsgi.Application): diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index e0c1e9d04b7c..3b74fefc9dad 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -68,6 +68,22 @@ class FaultWrapper(base_wsgi.Middleware): return faults.Fault(exc) +class ProjectMapper(routes.Mapper): + + def resource(self, member_name, collection_name, **kwargs): + if not ('parent_resource' in kwargs): + kwargs['path_prefix'] = '{project_id}/' + else: + parent_resource = kwargs['parent_resource'] + p_collection = parent_resource['collection_name'] + p_member = parent_resource['member_name'] + kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection, + p_member) + routes.Mapper.resource(self, member_name, + collection_name, + **kwargs) + + class APIRouter(base_wsgi.Router): """ Routes requests on the OpenStack API to the appropriate controller @@ -81,10 +97,13 @@ class APIRouter(base_wsgi.Router): def __init__(self, ext_mgr=None): self.server_members = {} - mapper = routes.Mapper() + mapper = self._mapper() self._setup_routes(mapper) super(APIRouter, self).__init__(mapper) + def _mapper(self): + return routes.Mapper() + def _setup_routes(self, mapper): raise NotImplementedError(_("You must implement _setup_routes.")) @@ -174,6 +193,9 @@ class APIRouterV10(APIRouter): class APIRouterV11(APIRouter): """Define routes specific to OpenStack API V1.1.""" + def _mapper(self): + return ProjectMapper() + def _setup_routes(self, mapper): self._setup_base_routes(mapper, '1.1') @@ -184,7 +206,7 @@ class APIRouterV11(APIRouter): parent_resource=dict(member_name='image', collection_name='images')) - mapper.connect("metadata", "/images/{image_id}/metadata", + mapper.connect("metadata", "/{project_id}/images/{image_id}/metadata", controller=image_metadata_controller, action='update_all', conditions={"method": ['PUT']}) @@ -196,7 +218,8 @@ class APIRouterV11(APIRouter): parent_resource=dict(member_name='server', collection_name='servers')) - mapper.connect("metadata", "/servers/{server_id}/metadata", + mapper.connect("metadata", + "/{project_id}/servers/{server_id}/metadata", controller=server_metadata_controller, action='update_all', conditions={"method": ['PUT']}) diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index d42abe1f865b..6754fea27cfe 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -28,10 +28,51 @@ from nova import flags from nova import log as logging from nova import utils from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import faults LOG = logging.getLogger('nova.api.openstack') FLAGS = flags.FLAGS +flags.DECLARE('use_forwarded_for', 'nova.api.auth') + + +class NoAuthMiddleware(wsgi.Middleware): + """Return a fake token if one isn't specified.""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + if 'X-Auth-Token' not in req.headers: + os_url = req.url + version = common.get_version_from_href(os_url) + user_id = req.headers.get('X-Auth-User', 'admin') + project_id = req.headers.get('X-Auth-Project-Id', 'admin') + if version == '1.1': + os_url += '/' + project_id + res = webob.Response() + # NOTE(vish): This is expecting and returning Auth(1.1), whereas + # keystone uses 2.0 auth. We should probably allow + # 2.0 auth here as well. + res.headers['X-Auth-Token'] = '%s:%s' % (user_id, project_id) + res.headers['X-Server-Management-Url'] = os_url + res.headers['X-Storage-Url'] = '' + res.headers['X-CDN-Management-Url'] = '' + res.content_type = 'text/plain' + res.status = '204' + return res + + token = req.headers['X-Auth-Token'] + user_id, _sep, project_id = token.partition(':') + project_id = project_id or user_id + remote_address = getattr(req, 'remote_address', '127.0.0.1') + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + ctx = context.RequestContext(user_id, + project_id, + is_admin=True, + remote_address=remote_address) + + req.environ['nova.context'] = ctx + return self.application class AuthMiddleware(wsgi.Middleware): @@ -55,21 +96,44 @@ class AuthMiddleware(wsgi.Middleware): LOG.warn(msg % locals()) return faults.Fault(webob.exc.HTTPUnauthorized()) - try: - project_id = req.headers["X-Auth-Project-Id"] - except KeyError: - # FIXME(usrleon): It needed only for compatibility - # while osapi clients don't use this header - projects = self.auth.get_projects(user_id) - if projects: - project_id = projects[0].id - else: + # Get all valid projects for the user + projects = self.auth.get_projects(user_id) + if not projects: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + project_id = "" + path_parts = req.path.split('/') + # TODO(wwolf): this v1.1 check will be temporary as + # keystone should be taking this over at some point + if len(path_parts) > 1 and path_parts[1] == 'v1.1': + project_id = path_parts[2] + # Check that the project for project_id exists, and that user + # is authorized to use it + try: + project = self.auth.get_project(project_id) + except exception.ProjectNotFound: return faults.Fault(webob.exc.HTTPUnauthorized()) + if project_id not in [p.id for p in projects]: + return faults.Fault(webob.exc.HTTPUnauthorized()) + else: + # As a fallback, set project_id from the headers, which is the v1.0 + # behavior. As a last resort, be forgiving to the user and set + # project_id based on a valid project of theirs. + try: + project_id = req.headers["X-Auth-Project-Id"] + except KeyError: + project_id = projects[0].id is_admin = self.auth.is_admin(user_id) - req.environ['nova.context'] = context.RequestContext(user_id, - project_id, - is_admin) + remote_address = getattr(req, 'remote_address', '127.0.0.1') + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + ctx = context.RequestContext(user_id, + project_id, + is_admin=is_admin, + remote_address=remote_address) + req.environ['nova.context'] = ctx + if not is_admin and not self.auth.is_project_member(user_id, project_id): msg = _("%(user_id)s must be an admin or a " @@ -95,12 +159,19 @@ class AuthMiddleware(wsgi.Middleware): LOG.warn(msg) return faults.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) + def _get_auth_header(key): + """Ensures that the KeyError returned is meaningful.""" + try: + return req.headers[key] + except KeyError as ex: + raise KeyError(key) try: - username = req.headers['X-Auth-User'] - key = req.headers['X-Auth-Key'] + username = _get_auth_header('X-Auth-User') + key = _get_auth_header('X-Auth-Key') except KeyError as ex: - LOG.warn(_("Could not find %s in request.") % ex) - return faults.Fault(webob.exc.HTTPUnauthorized()) + msg = _("Could not find %s in request.") % ex + LOG.warn(msg) + return faults.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) token, user = self._authorize_user(username, key, req) if user and token: @@ -149,6 +220,16 @@ class AuthMiddleware(wsgi.Middleware): """ ctxt = context.get_admin_context() + project_id = req.headers.get('X-Auth-Project-Id') + if project_id is None: + # If the project_id is not provided in the headers, be forgiving to + # the user and set project_id based on a valid project of theirs. + user = self.auth.get_user_from_access_key(key) + projects = self.auth.get_projects(user.id) + if not projects: + raise webob.exc.HTTPUnauthorized() + project_id = projects[0].id + try: user = self.auth.get_user_from_access_key(key) except exception.NotFound: @@ -162,7 +243,10 @@ class AuthMiddleware(wsgi.Middleware): token_dict['token_hash'] = token_hash token_dict['cdn_management_url'] = '' os_url = req.url - token_dict['server_management_url'] = os_url + token_dict['server_management_url'] = os_url.strip('/') + version = common.get_version_from_href(os_url) + if version == '1.1': + token_dict['server_management_url'] += '/' + project_id token_dict['storage_url'] = '' token_dict['user_id'] = user.id token = self.db.auth_token_create(ctxt, token_dict) diff --git a/nova/api/openstack/contrib/createserverext.py b/nova/api/openstack/contrib/createserverext.py new file mode 100644 index 000000000000..ba72fdb0b57a --- /dev/null +++ b/nova/api/openstack/contrib/createserverext.py @@ -0,0 +1,66 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# +# 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 nova.api.openstack import create_instance_helper as helper +from nova.api.openstack import extensions +from nova.api.openstack import servers +from nova.api.openstack import wsgi + + +class Createserverext(extensions.ExtensionDescriptor): + """The servers create ext + + Exposes addFixedIp and removeFixedIp actions on servers. + + """ + def get_name(self): + return "Createserverext" + + def get_alias(self): + return "os-create-server-ext" + + def get_description(self): + return "Extended support to the Create Server v1.1 API" + + def get_namespace(self): + return "http://docs.openstack.org/ext/createserverext/api/v1.1" + + def get_updated(self): + return "2011-07-19T00:00:00+00:00" + + def get_resources(self): + resources = [] + + headers_serializer = servers.HeadersSerializer() + body_serializers = { + 'application/xml': servers.ServerXMLSerializer(), + } + + body_deserializers = { + 'application/xml': helper.ServerXMLDeserializerV11(), + } + + serializer = wsgi.ResponseSerializer(body_serializers, + headers_serializer) + deserializer = wsgi.RequestDeserializer(body_deserializers) + + res = extensions.ResourceExtension('os-create-server-ext', + controller=servers.ControllerV11(), + deserializer=deserializer, + serializer=serializer) + resources.append(res) + + return resources diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 44b35c385c2d..40086f778ac1 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -15,8 +15,9 @@ # 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 webob import exc +import webob +from nova import compute from nova import exception from nova import log as logging from nova import network @@ -71,18 +72,22 @@ class FloatingIPController(object): try: floating_ip = self.network_api.get_floating_ip(context, id) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) + return faults.Fault(webob.exc.HTTPNotFound()) return _translate_floating_ip_view(floating_ip) def index(self, req): context = req.environ['nova.context'] - floating_ips = self.network_api.list_floating_ips(context) + try: + # FIXME(ja) - why does self.network_api.list_floating_ips raise? + floating_ips = self.network_api.list_floating_ips(context) + except exception.FloatingIpNotFoundForProject: + floating_ips = [] return _translate_floating_ips_view(floating_ips) - def create(self, req): + def create(self, req, body=None): context = req.environ['nova.context'] try: @@ -95,56 +100,19 @@ class FloatingIPController(object): else: raise - return {'allocated': { - "id": ip['id'], - "floating_ip": ip['address']}} + return _translate_floating_ip_view(ip) def delete(self, req, id): - context = req.environ['nova.context'] - ip = self.network_api.get_floating_ip(context, id) - - if 'fixed_ip' in ip: - self.disassociate(req, id) - - self.network_api.release_floating_ip(context, address=ip['address']) - - return {'released': { - "id": ip['id'], - "floating_ip": ip['address']}} - - def associate(self, req, id, body): - """ /floating_ips/{id}/associate fixed ip in body """ - context = req.environ['nova.context'] - floating_ip = self._get_ip_by_id(context, id) - - fixed_ip = body['associate_address']['fixed_ip'] - - try: - self.network_api.associate_floating_ip(context, - floating_ip, fixed_ip) - except rpc.RemoteError: - raise - - return {'associated': - { - "floating_ip_id": id, - "floating_ip": floating_ip, - "fixed_ip": fixed_ip}} - - def disassociate(self, req, id, body=None): - """ POST /floating_ips/{id}/disassociate """ context = req.environ['nova.context'] floating_ip = self.network_api.get_floating_ip(context, id) - address = floating_ip['address'] - fixed_ip = floating_ip['fixed_ip']['address'] - try: - self.network_api.disassociate_floating_ip(context, address) - except rpc.RemoteError: - raise + if 'fixed_ip' in floating_ip: + self.network_api.disassociate_floating_ip(context, + floating_ip['address']) - return {'disassociated': {'floating_ip': address, - 'fixed_ip': fixed_ip}} + self.network_api.release_floating_ip(context, + address=floating_ip['address']) + return webob.exc.HTTPAccepted() def _get_ip_by_id(self, context, value): """Checks that value is id and then returns its address.""" @@ -152,6 +120,47 @@ class FloatingIPController(object): class Floating_ips(extensions.ExtensionDescriptor): + def __init__(self): + self.compute_api = compute.API() + self.network_api = network.API() + super(Floating_ips, self).__init__() + + def _add_floating_ip(self, input_dict, req, instance_id): + """Associate floating_ip to an instance.""" + context = req.environ['nova.context'] + + try: + address = input_dict['addFloatingIp']['address'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Address not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + self.compute_api.associate_floating_ip(context, instance_id, address) + + return webob.Response(status_int=202) + + def _remove_floating_ip(self, input_dict, req, instance_id): + """Dissociate floating_ip from an instance.""" + context = req.environ['nova.context'] + + try: + address = input_dict['removeFloatingIp']['address'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Address not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + floating_ip = self.network_api.get_floating_ip_by_ip(context, address) + if 'fixed_ip' in floating_ip: + self.network_api.disassociate_floating_ip(context, address) + + return webob.Response(status_int=202) + def get_name(self): return "Floating_ips" @@ -172,9 +181,18 @@ class Floating_ips(extensions.ExtensionDescriptor): res = extensions.ResourceExtension('os-floating-ips', FloatingIPController(), - member_actions={ - 'associate': 'POST', - 'disassociate': 'POST'}) + member_actions={}) resources.append(res) return resources + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + extensions.ActionExtension("servers", "addFloatingIp", + self._add_floating_ip), + extensions.ActionExtension("servers", "removeFloatingIp", + self._remove_floating_ip), + ] + + return actions diff --git a/nova/api/openstack/contrib/rescue.py b/nova/api/openstack/contrib/rescue.py new file mode 100644 index 000000000000..3de12889517f --- /dev/null +++ b/nova/api/openstack/contrib/rescue.py @@ -0,0 +1,83 @@ +# Copyright 2011 Openstack, LLC. +# +# 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. + +"""The rescue mode extension.""" + +import webob +from webob import exc + +from nova import compute +from nova import log as logging +from nova.api.openstack import extensions as exts +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.contrib.rescue") + + +def wrap_errors(fn): + """"Ensure errors are not passed along.""" + def wrapped(*args): + try: + fn(*args) + except Exception, e: + return faults.Fault(exc.HTTPInternalServerError()) + return wrapped + + +class Rescue(exts.ExtensionDescriptor): + """The Rescue controller for the OpenStack API.""" + def __init__(self): + super(Rescue, self).__init__() + self.compute_api = compute.API() + + @wrap_errors + def _rescue(self, input_dict, req, instance_id): + """Rescue an instance.""" + context = req.environ["nova.context"] + self.compute_api.rescue(context, instance_id) + + return webob.Response(status_int=202) + + @wrap_errors + def _unrescue(self, input_dict, req, instance_id): + """Unrescue an instance.""" + context = req.environ["nova.context"] + self.compute_api.unrescue(context, instance_id) + + return webob.Response(status_int=202) + + def get_name(self): + return "Rescue" + + def get_alias(self): + return "os-rescue" + + def get_description(self): + return "Instance rescue mode" + + def get_namespace(self): + return "http://docs.openstack.org/ext/rescue/api/v1.1" + + def get_updated(self): + return "2011-08-18T00:00:00+00:00" + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + exts.ActionExtension("servers", "rescue", self._rescue), + exts.ActionExtension("servers", "unrescue", self._unrescue), + ] + + return actions diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py index 6c57fbb5181d..1fd64f3b8eca 100644 --- a/nova/api/openstack/contrib/security_groups.py +++ b/nova/api/openstack/contrib/security_groups.py @@ -25,10 +25,11 @@ from nova import db from nova import exception from nova import flags from nova import log as logging +from nova import rpc from nova.api.openstack import common from nova.api.openstack import extensions from nova.api.openstack import wsgi - +from nova.compute import power_state from xml.dom import minidom @@ -73,33 +74,28 @@ class SecurityGroupController(object): context, rule)] return security_group - def show(self, req, id): - """Return data about the given security group.""" - context = req.environ['nova.context'] + def _get_security_group(self, context, id): try: id = int(id) security_group = db.security_group_get(context, id) except ValueError: - msg = _("Security group id is not integer") - return exc.HTTPBadRequest(explanation=msg) + msg = _("Security group id should be integer") + raise exc.HTTPBadRequest(explanation=msg) except exception.NotFound as exp: - return exc.HTTPNotFound(explanation=unicode(exp)) + raise exc.HTTPNotFound(explanation=unicode(exp)) + return security_group + def show(self, req, id): + """Return data about the given security group.""" + context = req.environ['nova.context'] + security_group = self._get_security_group(context, id) return {'security_group': self._format_security_group(context, security_group)} def delete(self, req, id): """Delete a security group.""" context = req.environ['nova.context'] - try: - id = int(id) - security_group = db.security_group_get(context, id) - except ValueError: - msg = _("Security group id is not integer") - return exc.HTTPBadRequest(explanation=msg) - except exception.SecurityGroupNotFound as exp: - return exc.HTTPNotFound(explanation=unicode(exp)) - + security_group = self._get_security_group(context, id) LOG.audit(_("Delete security group %s"), id, context=context) db.security_group_destroy(context, security_group.id) @@ -226,9 +222,9 @@ class SecurityGroupRulesController(SecurityGroupController): security_group_rule = db.security_group_rule_create(context, values) self.compute_api.trigger_security_group_rules_refresh(context, - security_group_id=security_group['id']) + security_group_id=security_group['id']) - return {'security_group_rule': self._format_security_group_rule( + return {"security_group_rule": self._format_security_group_rule( context, security_group_rule)} @@ -336,6 +332,11 @@ class SecurityGroupRulesController(SecurityGroupController): class Security_groups(extensions.ExtensionDescriptor): + + def __init__(self): + self.compute_api = compute.API() + super(Security_groups, self).__init__() + def get_name(self): return "SecurityGroups" @@ -351,6 +352,82 @@ class Security_groups(extensions.ExtensionDescriptor): def get_updated(self): return "2011-07-21T00:00:00+00:00" + def _addSecurityGroup(self, input_dict, req, instance_id): + context = req.environ['nova.context'] + + try: + body = input_dict['addSecurityGroup'] + group_name = body['name'] + instance_id = int(instance_id) + except ValueError: + msg = _("Server id should be integer") + raise exc.HTTPBadRequest(explanation=msg) + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Security group not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + if not group_name or group_name.strip() == '': + msg = _("Security group name cannot be empty") + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + self.compute_api.add_security_group(context, instance_id, + group_name) + except exception.SecurityGroupNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + except exception.InstanceNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + except exception.Invalid as exp: + return exc.HTTPBadRequest(explanation=unicode(exp)) + + return exc.HTTPAccepted() + + def _removeSecurityGroup(self, input_dict, req, instance_id): + context = req.environ['nova.context'] + + try: + body = input_dict['removeSecurityGroup'] + group_name = body['name'] + instance_id = int(instance_id) + except ValueError: + msg = _("Server id should be integer") + raise exc.HTTPBadRequest(explanation=msg) + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Security group not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + if not group_name or group_name.strip() == '': + msg = _("Security group name cannot be empty") + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + self.compute_api.remove_security_group(context, instance_id, + group_name) + except exception.SecurityGroupNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + except exception.InstanceNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + except exception.Invalid as exp: + return exc.HTTPBadRequest(explanation=unicode(exp)) + + return exc.HTTPAccepted() + + def get_actions(self): + """Return the actions the extensions adds""" + actions = [ + extensions.ActionExtension("servers", "addSecurityGroup", + self._addSecurityGroup), + extensions.ActionExtension("servers", "removeSecurityGroup", + self._removeSecurityGroup) + ] + return actions + def get_resources(self): resources = [] diff --git a/nova/api/openstack/contrib/virtual_interfaces.py b/nova/api/openstack/contrib/virtual_interfaces.py new file mode 100644 index 000000000000..dab61efc8627 --- /dev/null +++ b/nova/api/openstack/contrib/virtual_interfaces.py @@ -0,0 +1,108 @@ +# Copyright (C) 2011 Midokura KK +# 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. + +"""The virtual interfaces extension.""" + +from webob import exc +import webob + +from nova import compute +from nova import exception +from nova import log as logging +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import faults +from nova.api.openstack import wsgi + + +LOG = logging.getLogger("nova.api.virtual_interfaces") + + +def _translate_vif_summary_view(_context, vif): + """Maps keys for VIF summary view.""" + d = {} + d['id'] = vif['uuid'] + d['mac_address'] = vif['address'] + return d + + +def _get_metadata(): + metadata = { + "attributes": { + 'virtual_interface': ["id", "mac_address"]}} + return metadata + + +class ServerVirtualInterfaceController(object): + """The instance VIF API controller for the Openstack API. + """ + + def __init__(self): + self.compute_api = compute.API() + super(ServerVirtualInterfaceController, self).__init__() + + def _items(self, req, server_id, entity_maker): + """Returns a list of VIFs, transformed through entity_maker.""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + vifs = instance['virtual_interfaces'] + limited_list = common.limited(vifs, req) + res = [entity_maker(context, vif) for vif in limited_list] + return {'virtual_interfaces': res} + + def index(self, req, server_id): + """Returns the list of VIFs for a given instance.""" + return self._items(req, server_id, + entity_maker=_translate_vif_summary_view) + + +class Virtual_interfaces(extensions.ExtensionDescriptor): + + def get_name(self): + return "VirtualInterfaces" + + def get_alias(self): + return "virtual_interfaces" + + def get_description(self): + return "Virtual interface support" + + def get_namespace(self): + return "http://docs.openstack.org/ext/virtual_interfaces/api/v1.1" + + def get_updated(self): + return "2011-08-17T00:00:00+00:00" + + def get_resources(self): + resources = [] + + metadata = _get_metadata() + body_serializers = { + 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, + xmlns=wsgi.XMLNS_V11)} + serializer = wsgi.ResponseSerializer(body_serializers, None) + res = extensions.ResourceExtension( + 'os-virtual-interfaces', + controller=ServerVirtualInterfaceController(), + parent=dict(member_name='server', collection_name='servers'), + serializer=serializer) + resources.append(res) + + return resources diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index b4a08dac01a6..483ff49855f5 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -1,4 +1,5 @@ # Copyright 2011 OpenStack LLC. +# Copyright 2011 Piston Cloud Computing, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -29,7 +30,7 @@ from nova import utils from nova.compute import instance_types from nova.api.openstack import common from nova.api.openstack import wsgi - +from nova.rpc.common import RemoteError LOG = logging.getLogger('nova.api.openstack.create_instance_helper') FLAGS = flags.FLAGS @@ -106,11 +107,26 @@ class CreateInstanceHelper(object): raise exc.HTTPBadRequest(explanation=msg) personality = server_dict.get('personality') + config_drive = server_dict.get('config_drive') injected_files = [] if personality: injected_files = self._get_injected_files(personality) + sg_names = [] + security_groups = server_dict.get('security_groups') + if security_groups is not None: + sg_names = [sg['name'] for sg in security_groups if sg.get('name')] + if not sg_names: + sg_names.append('default') + + sg_names = list(set(sg_names)) + + requested_networks = server_dict.get('networks') + if requested_networks is not None: + requested_networks = self._get_requested_networks( + requested_networks) + try: flavor_id = self.controller._flavor_id_from_req_data(body) except ValueError as error: @@ -122,6 +138,7 @@ class CreateInstanceHelper(object): raise exc.HTTPBadRequest(explanation=msg) zone_blob = server_dict.get('blob') + user_data = server_dict.get('user_data') availability_zone = server_dict.get('availability_zone') name = server_dict['name'] self._validate_server_name(name) @@ -144,6 +161,7 @@ class CreateInstanceHelper(object): extra_values = { 'instance_type': inst_type, 'image_ref': image_href, + 'config_drive': config_drive, 'password': password} return (extra_values, @@ -157,13 +175,19 @@ class CreateInstanceHelper(object): key_name=key_name, key_data=key_data, metadata=server_dict.get('metadata', {}), + access_ip_v4=server_dict.get('accessIPv4'), + access_ip_v6=server_dict.get('accessIPv6'), injected_files=injected_files, admin_password=password, zone_blob=zone_blob, reservation_id=reservation_id, min_count=min_count, max_count=max_count, - availability_zone=availability_zone)) + requested_networks=requested_networks, + security_group=sg_names, + user_data=user_data, + availability_zone=availability_zone, + config_drive=config_drive,)) except quota.QuotaError as error: self._handle_quota_error(error) except exception.ImageNotFound as error: @@ -172,6 +196,12 @@ class CreateInstanceHelper(object): except exception.FlavorNotFound as error: msg = _("Invalid flavorRef provided.") raise exc.HTTPBadRequest(explanation=msg) + except exception.SecurityGroupNotFound as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + except RemoteError as err: + msg = "%(err_type)s: %(err_msg)s" % \ + {'err_type': err.exc_type, 'err_msg': err.value} + raise exc.HTTPBadRequest(explanation=msg) # Let the caller deal with unhandled exceptions. def _handle_quota_error(self, error): @@ -300,6 +330,46 @@ class CreateInstanceHelper(object): raise exc.HTTPBadRequest(explanation=msg) return password + def _get_requested_networks(self, requested_networks): + """ + Create a list of requested networks from the networks attribute + """ + networks = [] + for network in requested_networks: + try: + network_uuid = network['uuid'] + + if not utils.is_uuid_like(network_uuid): + msg = _("Bad networks format: network uuid is not in" + " proper format (%s)") % network_uuid + raise exc.HTTPBadRequest(explanation=msg) + + #fixed IP address is optional + #if the fixed IP address is not provided then + #it will use one of the available IP address from the network + address = network.get('fixed_ip', None) + if address is not None and not utils.is_valid_ipv4(address): + msg = _("Invalid fixed IP address (%s)") % address + raise exc.HTTPBadRequest(explanation=msg) + # check if the network id is already present in the list, + # we don't want duplicate networks to be passed + # at the boot time + for id, ip in networks: + if id == network_uuid: + expl = _("Duplicate networks (%s) are not allowed")\ + % network_uuid + raise exc.HTTPBadRequest(explanation=expl) + + networks.append((network_uuid, address)) + except KeyError as key: + expl = _('Bad network format: missing %s') % key + raise exc.HTTPBadRequest(explanation=expl) + except TypeError: + expl = _('Bad networks format') + raise exc.HTTPBadRequest(explanation=expl) + + return networks + class ServerXMLDeserializer(wsgi.XMLDeserializer): """ @@ -450,7 +520,8 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): server = {} server_node = self.find_first_child_named(node, 'server') - attributes = ["name", "imageRef", "flavorRef", "adminPass"] + attributes = ["name", "imageRef", "flavorRef", "adminPass", + "accessIPv4", "accessIPv6"] for attr in attributes: if server_node.getAttribute(attr): server[attr] = server_node.getAttribute(attr) @@ -463,6 +534,14 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): if personality is not None: server["personality"] = personality + networks = self._extract_networks(server_node) + if networks is not None: + server["networks"] = networks + + security_groups = self._extract_security_groups(server_node) + if security_groups is not None: + server["security_groups"] = security_groups + return server def _extract_personality(self, server_node): @@ -479,3 +558,35 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): return personality else: return None + + def _extract_networks(self, server_node): + """Marshal the networks attribute of a parsed request""" + node = self.find_first_child_named(server_node, "networks") + if node is not None: + networks = [] + for network_node in self.find_children_named(node, + "network"): + item = {} + if network_node.hasAttribute("uuid"): + item["uuid"] = network_node.getAttribute("uuid") + if network_node.hasAttribute("fixed_ip"): + item["fixed_ip"] = network_node.getAttribute("fixed_ip") + networks.append(item) + return networks + else: + return None + + def _extract_security_groups(self, server_node): + """Marshal the security_groups attribute of a parsed request""" + node = self.find_first_child_named(server_node, "security_groups") + if node is not None: + security_groups = [] + for sg_node in self.find_children_named(node, "security_group"): + item = {} + name_node = self.find_first_child_named(sg_node, "name") + if name_node: + item["name"] = self.extract_text(name_node) + security_groups.append(item) + return security_groups + else: + return None diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index bb407a04541b..efede945ffcb 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -29,6 +29,7 @@ from nova import exception from nova import flags from nova import log as logging from nova import wsgi as base_wsgi +import nova.api.openstack from nova.api.openstack import common from nova.api.openstack import faults from nova.api.openstack import wsgi @@ -220,12 +221,13 @@ class ExtensionMiddleware(base_wsgi.Middleware): for action in ext_mgr.get_actions(): if not action.collection in action_resources.keys(): resource = ActionExtensionResource(application) - mapper.connect("/%s/:(id)/action.:(format)" % + mapper.connect("/:(project_id)/%s/:(id)/action.:(format)" % action.collection, action='action', controller=resource, conditions=dict(method=['POST'])) - mapper.connect("/%s/:(id)/action" % action.collection, + mapper.connect("/:(project_id)/%s/:(id)/action" % + action.collection, action='action', controller=resource, conditions=dict(method=['POST'])) @@ -258,7 +260,7 @@ class ExtensionMiddleware(base_wsgi.Middleware): ext_mgr = ExtensionManager(FLAGS.osapi_extensions_path) self.ext_mgr = ext_mgr - mapper = routes.Mapper() + mapper = nova.api.openstack.ProjectMapper() serializer = wsgi.ResponseSerializer( {'application/xml': ExtensionsXMLSerializer()}) @@ -269,13 +271,17 @@ class ExtensionMiddleware(base_wsgi.Middleware): if resource.serializer is None: resource.serializer = serializer - mapper.resource(resource.collection, resource.collection, + kargs = dict( controller=wsgi.Resource( resource.controller, resource.deserializer, resource.serializer), collection=resource.collection_actions, - member=resource.member_actions, - parent_resource=resource.parent) + member=resource.member_actions) + + if resource.parent: + kargs['parent_resource'] = resource.parent + + mapper.resource(resource.collection, resource.collection, **kargs) # extended actions action_resources = self._action_ext_resources(application, ext_mgr, diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index b4bda68d4e25..fd36060da41f 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -72,7 +72,8 @@ class ControllerV11(Controller): def _get_view_builder(self, req): base_url = req.application_url - return views.flavors.ViewBuilderV11(base_url) + project_id = getattr(req.environ['nova.context'], 'project_id', '') + return views.flavors.ViewBuilderV11(base_url, project_id) class FlavorXMLSerializer(wsgi.XMLDictSerializer): diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 0aabb9e565b5..1c8fc10c9737 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -166,10 +166,11 @@ class ControllerV10(Controller): class ControllerV11(Controller): """Version 1.1 specific controller logic.""" - def get_builder(self, request): + def get_builder(self, req): """Property to get the ViewBuilder class we need to use.""" - base_url = request.application_url - return images_view.ViewBuilderV11(base_url) + base_url = req.application_url + project_id = getattr(req.environ['nova.context'], 'project_id', '') + return images_view.ViewBuilderV11(base_url, project_id) def index(self, req): """Return an index listing of images available to the request. diff --git a/nova/api/openstack/schemas/v1.1/server.rng b/nova/api/openstack/schemas/v1.1/server.rng new file mode 100644 index 000000000000..dbd169a8322e --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/server.rng @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/schemas/v1.1/servers.rng b/nova/api/openstack/schemas/v1.1/servers.rng new file mode 100644 index 000000000000..4e2bb88530de --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/servers.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/schemas/v1.1/servers_index.rng b/nova/api/openstack/schemas/v1.1/servers_index.rng new file mode 100644 index 000000000000..768f0912dea2 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/servers_index.rng @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 335ecad869a2..553357404062 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -163,7 +163,7 @@ class Controller(object): @scheduler_api.redirect_handler def update(self, req, id, body): - """Update server name then pass on to version-specific controller""" + """Update server then pass on to version-specific controller""" if len(req.body) == 0: raise exc.HTTPUnprocessableEntity() @@ -178,6 +178,14 @@ class Controller(object): self.helper._validate_server_name(name) update_dict['display_name'] = name.strip() + if 'accessIPv4' in body['server']: + access_ipv4 = body['server']['accessIPv4'] + update_dict['access_ip_v4'] = access_ipv4.strip() + + if 'accessIPv6' in body['server']: + access_ipv6 = body['server']['accessIPv6'] + update_dict['access_ip_v6'] = access_ipv6.strip() + try: self.compute_api.update(ctxt, id, **update_dict) except exception.NotFound: @@ -642,14 +650,16 @@ class ControllerV11(Controller): return common.get_id_from_href(flavor_ref) def _build_view(self, req, instance, is_detail=False): + project_id = getattr(req.environ['nova.context'], 'project_id', '') base_url = req.application_url flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11( - base_url) + base_url, project_id) image_builder = nova.api.openstack.views.images.ViewBuilderV11( - base_url) + base_url, project_id) addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11() builder = nova.api.openstack.views.servers.ViewBuilderV11( - addresses_builder, flavor_builder, image_builder, base_url) + addresses_builder, flavor_builder, image_builder, + base_url, project_id) return builder.build(instance, is_detail=is_detail) @@ -837,6 +847,10 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer): node.setAttribute('created', str(server['created'])) node.setAttribute('updated', str(server['updated'])) node.setAttribute('status', server['status']) + if 'accessIPv4' in server: + node.setAttribute('accessIPv4', str(server['accessIPv4'])) + if 'accessIPv6' in server: + node.setAttribute('accessIPv6', str(server['accessIPv6'])) if 'progress' in server: node.setAttribute('progress', str(server['progress'])) @@ -923,6 +937,12 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer): node.setAttribute('adminPass', server_dict['server']['adminPass']) return self.to_xml_string(node, True) + def update(self, server_dict): + xml_doc = minidom.Document() + node = self._server_to_xml_detailed(xml_doc, + server_dict['server']) + return self.to_xml_string(node, True) + def create_resource(version='1.0'): controller = { diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index ddbf7a1448d1..8f07a228918f 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -17,9 +17,11 @@ from nova import flags from nova import utils +from nova import log as logging from nova.api.openstack import common FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack.views.addresses') class ViewBuilder(object): @@ -48,7 +50,10 @@ class ViewBuilderV11(ViewBuilder): def build(self, interfaces): networks = {} for interface in interfaces: - network_label = interface['network']['label'] + try: + network_label = self._extract_network_label(interface) + except TypeError: + continue if network_label not in networks: networks[network_label] = [] @@ -64,9 +69,14 @@ class ViewBuilderV11(ViewBuilder): return networks - def build_network(self, interfaces, network_label): + def build_network(self, interfaces, requested_network): for interface in interfaces: - if interface['network']['label'] == network_label: + try: + network_label = self._extract_network_label(interface) + except TypeError: + continue + + if network_label == requested_network: ips = list(self._extract_ipv4_addresses(interface)) ipv6 = self._extract_ipv6_address(interface) if ipv6 is not None: @@ -74,6 +84,13 @@ class ViewBuilderV11(ViewBuilder): return {network_label: ips} return None + def _extract_network_label(self, interface): + try: + return interface['network']['label'] + except (TypeError, KeyError) as exc: + LOG.exception(exc) + raise TypeError + def _extract_ipv4_addresses(self, interface): for fixed_ip in interface['fixed_ips']: yield self._build_ip_entity(fixed_ip['address'], 4) diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index 0403ece1b780..aea34b42430f 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -15,6 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. +import os.path + + from nova.api.openstack import common @@ -59,11 +62,12 @@ class ViewBuilder(object): class ViewBuilderV11(ViewBuilder): """Openstack API v1.1 flavors view builder.""" - def __init__(self, base_url): + def __init__(self, base_url, project_id=""): """ :param base_url: url of the root wsgi application """ self.base_url = base_url + self.project_id = project_id def _build_extra(self, flavor_obj): flavor_obj["links"] = self._build_links(flavor_obj) @@ -88,11 +92,10 @@ class ViewBuilderV11(ViewBuilder): def generate_href(self, flavor_id): """Create an url that refers to a specific flavor id.""" - return "%s/flavors/%s" % (self.base_url, flavor_id) + return os.path.join(self.base_url, self.project_id, + "flavors", str(flavor_id)) def generate_bookmark(self, flavor_id): """Create an url that refers to a specific flavor id.""" - return "%s/flavors/%s" % ( - common.remove_version_from_href(self.base_url), - flavor_id, - ) + return os.path.join(common.remove_version_from_href(self.base_url), + self.project_id, "flavors", str(flavor_id)) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 912303d144d0..21f1b2d3e451 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -23,9 +23,10 @@ from nova.api.openstack import common class ViewBuilder(object): """Base class for generating responses to OpenStack API image requests.""" - def __init__(self, base_url): + def __init__(self, base_url, project_id=""): """Initialize new `ViewBuilder`.""" - self._url = base_url + self.base_url = base_url + self.project_id = project_id def _format_dates(self, image): """Update all date fields to ensure standardized formatting.""" @@ -54,7 +55,7 @@ class ViewBuilder(object): def generate_href(self, image_id): """Return an href string pointing to this object.""" - return os.path.join(self._url, "images", str(image_id)) + return os.path.join(self.base_url, "images", str(image_id)) def build(self, image_obj, detail=False): """Return a standardized image structure for display by the API.""" @@ -117,6 +118,11 @@ class ViewBuilderV11(ViewBuilder): except KeyError: return + def generate_href(self, image_id): + """Return an href string pointing to this object.""" + return os.path.join(self.base_url, self.project_id, + "images", str(image_id)) + def build(self, image_obj, detail=False): """Return a standardized image structure for display by the API.""" image = ViewBuilder.build(self, image_obj, detail) @@ -142,5 +148,5 @@ class ViewBuilderV11(ViewBuilder): def generate_bookmark(self, image_id): """Create an url that refers to a specific flavor id.""" - return os.path.join(common.remove_version_from_href(self._url), - "images", str(image_id)) + return os.path.join(common.remove_version_from_href(self.base_url), + self.project_id, "images", str(image_id)) diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index edc328129082..0ec98591e77c 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010-2011 OpenStack LLC. +# Copyright 2011 Piston Cloud Computing, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -128,11 +129,12 @@ class ViewBuilderV10(ViewBuilder): class ViewBuilderV11(ViewBuilder): """Model an Openstack API V1.0 server response.""" def __init__(self, addresses_builder, flavor_builder, image_builder, - base_url): + base_url, project_id=""): ViewBuilder.__init__(self, addresses_builder) self.flavor_builder = flavor_builder self.image_builder = image_builder self.base_url = base_url + self.project_id = project_id def _build_detail(self, inst): response = super(ViewBuilderV11, self)._build_detail(inst) @@ -143,6 +145,10 @@ class ViewBuilderV11(ViewBuilder): response['server']['progress'] = 100 elif response['server']['status'] == "BUILD": response['server']['progress'] = 0 + + response['server']['accessIPv4'] = inst.get('access_ip_v4') or "" + response['server']['accessIPv6'] = inst.get('access_ip_v6') or "" + return response def _build_image(self, response, inst): @@ -182,6 +188,7 @@ class ViewBuilderV11(ViewBuilder): def _build_extra(self, response, inst): self._build_links(response, inst) response['uuid'] = inst['uuid'] + self._build_config_drive(response, inst) def _build_links(self, response, inst): href = self.generate_href(inst["id"]) @@ -200,11 +207,15 @@ class ViewBuilderV11(ViewBuilder): response["links"] = links + def _build_config_drive(self, response, inst): + response['config_drive'] = inst.get('config_drive') + def generate_href(self, server_id): """Create an url that refers to a specific server id.""" - return os.path.join(self.base_url, "servers", str(server_id)) + return os.path.join(self.base_url, self.project_id, + "servers", str(server_id)) def generate_bookmark(self, server_id): """Create an url that refers to a specific flavor id.""" return os.path.join(common.remove_version_from_href(self.base_url), - "servers", str(server_id)) + self.project_id, "servers", str(server_id)) diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 0eb47044e848..8641e960aee3 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -486,6 +486,10 @@ class Resource(wsgi.Application): msg = _("Malformed request body") return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg)) + project_id = args.pop("project_id", None) + if 'nova.context' in request.environ and project_id: + request.environ['nova.context'].project_id = project_id + try: action_result = self.dispatch(request, action, args) except webob.exc.HTTPException as ex: @@ -516,6 +520,6 @@ class Resource(wsgi.Application): controller_method = getattr(self.controller, action) try: return controller_method(req=request, **action_args) - except TypeError, exc: - LOG.debug(str(exc)) - return webob.exc.HTTPBadRequest() + except TypeError as exc: + LOG.exception(exc) + return faults.Fault(webob.exc.HTTPBadRequest()) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 6205cfb56d5c..44e6e11acd25 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -17,6 +17,9 @@ # under the License. """ +WARNING: This code is deprecated and will be removed. +Keystone is the recommended solution for auth management. + Nova authentication management """ @@ -38,10 +41,13 @@ from nova.auth import signer FLAGS = flags.FLAGS +flags.DEFINE_bool('use_deprecated_auth', + False, + 'This flag must be set to use old style auth') + flags.DEFINE_list('allowed_roles', ['cloudadmin', 'itsec', 'sysadmin', 'netadmin', 'developer'], 'Allowed roles for project') - # NOTE(vish): a user with one of these roles will be a superuser and # have access to all api commands flags.DEFINE_list('superuser_roles', ['cloudadmin'], @@ -811,7 +817,13 @@ class AuthManager(object): s3_host = host ec2_host = host rc = open(FLAGS.credentials_template).read() - rc = rc % {'access': user.access, + # NOTE(vish): Deprecated auth uses an access key, no auth uses a + # the user_id in place of it. + if FLAGS.use_deprecated_auth: + access = user.access + else: + access = user.id + rc = rc % {'access': access, 'project': pid, 'secret': user.secret, 'ec2': '%s://%s:%s%s' % (FLAGS.ec2_scheme, diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index 2c4673f9e6f3..3eb372844c05 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -34,7 +34,6 @@ from nova import exception from nova import flags from nova import log as logging from nova import utils -from nova.auth import manager # TODO(eday): Eventually changes these to something not ec2-specific from nova.api.ec2 import cloud @@ -57,7 +56,6 @@ LOG = logging.getLogger('nova.cloudpipe') class CloudPipe(object): def __init__(self): self.controller = cloud.CloudController() - self.manager = manager.AuthManager() def get_encoded_zip(self, project_id): # Make a payload.zip @@ -93,11 +91,10 @@ class CloudPipe(object): zippy.close() return encoded - def launch_vpn_instance(self, project_id): + def launch_vpn_instance(self, project_id, user_id): LOG.debug(_("Launching VPN for %s") % (project_id)) - project = self.manager.get_project(project_id) - ctxt = context.RequestContext(user=project.project_manager_id, - project=project.id) + ctxt = context.RequestContext(user_id=user_id, + project_id=project_id) key_name = self.setup_key_pair(ctxt) group_name = self.setup_security_group(ctxt) diff --git a/nova/compute/api.py b/nova/compute/api.py index e909e9959571..69f76bf4016c 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -2,6 +2,7 @@ # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Piston Cloud Computing, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -146,6 +147,16 @@ class API(base.Base): LOG.warn(msg) raise quota.QuotaError(msg, "MetadataLimitExceeded") + def _check_requested_networks(self, context, requested_networks): + """ Check if the networks requested belongs to the project + and the fixed IP address for each network provided is within + same the network block + """ + if requested_networks is None: + return + + self.network_api.validate_networks(context, requested_networks) + def _check_create_parameters(self, context, instance_type, image_href, kernel_id=None, ramdisk_id=None, min_count=None, max_count=None, @@ -153,7 +164,8 @@ class API(base.Base): key_name=None, key_data=None, security_group='default', availability_zone=None, user_data=None, metadata=None, injected_files=None, admin_password=None, zone_blob=None, - reservation_id=None): + reservation_id=None, access_ip_v4=None, access_ip_v6=None, + requested_networks=None, config_drive=None,): """Verify all the input parameters regardless of the provisioning strategy being performed.""" @@ -182,10 +194,16 @@ class API(base.Base): self._check_metadata_properties_quota(context, metadata) self._check_injected_file_quota(context, injected_files) + self._check_requested_networks(context, requested_networks) (image_service, image_id) = nova.image.get_image_service(image_href) image = image_service.show(context, image_id) + config_drive_id = None + if config_drive and config_drive is not True: + # config_drive is volume id + config_drive, config_drive_id = None, config_drive + os_type = None if 'properties' in image and 'os_type' in image['properties']: os_type = image['properties']['os_type'] @@ -213,6 +231,8 @@ class API(base.Base): image_service.show(context, kernel_id) if ramdisk_id: image_service.show(context, ramdisk_id) + if config_drive_id: + image_service.show(context, config_drive_id) self.ensure_default_security_group(context) @@ -231,6 +251,8 @@ class API(base.Base): 'image_ref': image_href, 'kernel_id': kernel_id or '', 'ramdisk_id': ramdisk_id or '', + 'config_drive_id': config_drive_id or '', + 'config_drive': config_drive or '', 'state': 0, 'state_description': 'scheduling', 'user_id': context.user_id, @@ -247,6 +269,8 @@ class API(base.Base): 'key_data': key_data, 'locked': False, 'metadata': metadata, + 'access_ip_v4': access_ip_v4, + 'access_ip_v6': access_ip_v6, 'availability_zone': availability_zone, 'os_type': os_type, 'architecture': architecture, @@ -398,9 +422,9 @@ class API(base.Base): def _ask_scheduler_to_create_instance(self, context, base_options, instance_type, zone_blob, availability_zone, injected_files, - admin_password, - image, - instance_id=None, num_instances=1): + admin_password, image, + instance_id=None, num_instances=1, + requested_networks=None): """Send the run_instance request to the schedulers for processing.""" pid = context.project_id uid = context.user_id @@ -411,12 +435,11 @@ class API(base.Base): LOG.debug(_("Casting to scheduler for %(pid)s/%(uid)s's" " (all-at-once)") % locals()) - filter_class = 'nova.scheduler.host_filter.InstanceTypeFilter' request_spec = { 'image': image, 'instance_properties': base_options, 'instance_type': instance_type, - 'filter': filter_class, + 'filter': None, 'blob': zone_blob, 'num_instances': num_instances, } @@ -429,7 +452,8 @@ class API(base.Base): "request_spec": request_spec, "availability_zone": availability_zone, "admin_password": admin_password, - "injected_files": injected_files}}) + "injected_files": injected_files, + "requested_networks": requested_networks}}) def create_all_at_once(self, context, instance_type, image_href, kernel_id=None, ramdisk_id=None, @@ -438,7 +462,9 @@ class API(base.Base): key_name=None, key_data=None, security_group='default', availability_zone=None, user_data=None, metadata=None, injected_files=None, admin_password=None, zone_blob=None, - reservation_id=None, block_device_mapping=None): + reservation_id=None, block_device_mapping=None, + access_ip_v4=None, access_ip_v6=None, + requested_networks=None, config_drive=None): """Provision the instances by passing the whole request to the Scheduler for execution. Returns a Reservation ID related to the creation of all of these instances.""" @@ -454,14 +480,15 @@ class API(base.Base): key_name, key_data, security_group, availability_zone, user_data, metadata, injected_files, admin_password, zone_blob, - reservation_id) + reservation_id, access_ip_v4, access_ip_v6, + requested_networks, config_drive) self._ask_scheduler_to_create_instance(context, base_options, instance_type, zone_blob, availability_zone, injected_files, - admin_password, - image, - num_instances=num_instances) + admin_password, image, + num_instances=num_instances, + requested_networks=requested_networks) return base_options['reservation_id'] @@ -472,7 +499,9 @@ class API(base.Base): key_name=None, key_data=None, security_group='default', availability_zone=None, user_data=None, metadata=None, injected_files=None, admin_password=None, zone_blob=None, - reservation_id=None, block_device_mapping=None): + reservation_id=None, block_device_mapping=None, + access_ip_v4=None, access_ip_v6=None, + requested_networks=None, config_drive=None,): """ Provision the instances by sending off a series of single instance requests to the Schedulers. This is fine for trival @@ -496,7 +525,8 @@ class API(base.Base): key_name, key_data, security_group, availability_zone, user_data, metadata, injected_files, admin_password, zone_blob, - reservation_id) + reservation_id, access_ip_v4, access_ip_v6, + requested_networks, config_drive) block_device_mapping = block_device_mapping or [] instances = [] @@ -510,11 +540,11 @@ class API(base.Base): instance_id = instance['id'] self._ask_scheduler_to_create_instance(context, base_options, - instance_type, zone_blob, - availability_zone, injected_files, - admin_password, - image, - instance_id=instance_id) + instance_type, zone_blob, + availability_zone, injected_files, + admin_password, image, + instance_id=instance_id, + requested_networks=requested_networks) return [dict(x.iteritems()) for x in instances] @@ -614,6 +644,78 @@ class API(base.Base): self.db.queue_get_for(context, FLAGS.compute_topic, host), {'method': 'refresh_provider_fw_rules', 'args': {}}) + def _is_security_group_associated_with_server(self, security_group, + instance_id): + """Check if the security group is already associated + with the instance. If Yes, return True. + """ + + if not security_group: + return False + + instances = security_group.get('instances') + if not instances: + return False + + inst_id = None + for inst_id in (instance['id'] for instance in instances \ + if instance_id == instance['id']): + return True + + return False + + def add_security_group(self, context, instance_id, security_group_name): + """Add security group to the instance""" + security_group = db.security_group_get_by_name(context, + context.project_id, + security_group_name) + # check if the server exists + inst = db.instance_get(context, instance_id) + #check if the security group is associated with the server + if self._is_security_group_associated_with_server(security_group, + instance_id): + raise exception.SecurityGroupExistsForInstance( + security_group_id=security_group['id'], + instance_id=instance_id) + + #check if the instance is in running state + if inst['state'] != power_state.RUNNING: + raise exception.InstanceNotRunning(instance_id=instance_id) + + db.instance_add_security_group(context.elevated(), + instance_id, + security_group['id']) + rpc.cast(context, + db.queue_get_for(context, FLAGS.compute_topic, inst['host']), + {"method": "refresh_security_group_rules", + "args": {"security_group_id": security_group['id']}}) + + def remove_security_group(self, context, instance_id, security_group_name): + """Remove the security group associated with the instance""" + security_group = db.security_group_get_by_name(context, + context.project_id, + security_group_name) + # check if the server exists + inst = db.instance_get(context, instance_id) + #check if the security group is associated with the server + if not self._is_security_group_associated_with_server(security_group, + instance_id): + raise exception.SecurityGroupNotExistsForInstance( + security_group_id=security_group['id'], + instance_id=instance_id) + + #check if the instance is in running state + if inst['state'] != power_state.RUNNING: + raise exception.InstanceNotRunning(instance_id=instance_id) + + db.instance_remove_security_group(context.elevated(), + instance_id, + security_group['id']) + rpc.cast(context, + db.queue_get_for(context, FLAGS.compute_topic, inst['host']), + {"method": "refresh_security_group_rules", + "args": {"security_group_id": security_group['id']}}) + @scheduler_api.reroute_compute("update") def update(self, context, instance_id, **kwargs): """Updates the instance in the datastore. @@ -1068,15 +1170,21 @@ class API(base.Base): """Unpause the given instance.""" self._cast_compute_message('unpause_instance', context, instance_id) + def _call_compute_message_for_host(self, action, context, host, params): + """Call method deliberately designed to make host/service only calls""" + queue = self.db.queue_get_for(context, FLAGS.compute_topic, host) + kwargs = {'method': action, 'args': params} + return rpc.call(context, queue, kwargs) + def set_host_enabled(self, context, host, enabled): """Sets the specified host's ability to accept new instances.""" - return self._call_compute_message("set_host_enabled", context, + return self._call_compute_message_for_host("set_host_enabled", context, host=host, params={"enabled": enabled}) def host_power_action(self, context, host, action): """Reboots, shuts down or powers up the host.""" - return self._call_compute_message("host_power_action", context, - host=host, params={"action": action}) + return self._call_compute_message_for_host("host_power_action", + context, host=host, params={"action": action}) @scheduler_api.reroute_compute("diagnostics") def get_diagnostics(self, context, instance_id): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 66458fb36e01..c207eccbb7d0 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -382,6 +382,8 @@ class ComputeManager(manager.SchedulerDependentManager): context = context.elevated() instance = self.db.instance_get(context, instance_id) + requested_networks = kwargs.get('requested_networks', None) + if instance['name'] in self.driver.list_instances(): raise exception.Error(_("Instance has already been created")) @@ -392,12 +394,12 @@ class ComputeManager(manager.SchedulerDependentManager): updates = {} updates['host'] = self.host updates['launched_on'] = self.host - # NOTE(vish): used by virt but not in database - updates['injected_files'] = kwargs.get('injected_files', []) - updates['admin_pass'] = kwargs.get('admin_password', None) instance = self.db.instance_update(context, instance_id, updates) + instance['injected_files'] = kwargs.get('injected_files', []) + instance['admin_pass'] = kwargs.get('admin_password', None) + self.db.instance_set_state(context, instance_id, power_state.NOSTATE, @@ -411,7 +413,8 @@ class ComputeManager(manager.SchedulerDependentManager): # will eventually also need to save the address here. if not FLAGS.stub_network: network_info = self.network_api.allocate_for_instance(context, - instance, vpn=is_vpn) + instance, vpn=is_vpn, + requested_networks=requested_networks) LOG.debug(_("instance network_info: |%s|"), network_info) else: # TODO(tr3buchet) not really sure how this should be handled. diff --git a/nova/db/api.py b/nova/db/api.py index 494d27708aa1..8726df6dda89 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -323,13 +323,13 @@ def migration_get_by_instance_and_status(context, instance_uuid, status): #################### -def fixed_ip_associate(context, address, instance_id): +def fixed_ip_associate(context, address, instance_id, network_id=None): """Associate fixed ip to instance. Raises if fixed ip is not available. """ - return IMPL.fixed_ip_associate(context, address, instance_id) + return IMPL.fixed_ip_associate(context, address, instance_id, network_id) def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None): @@ -396,7 +396,6 @@ def fixed_ip_update(context, address, values): """Create a fixed ip from the values dictionary.""" return IMPL.fixed_ip_update(context, address, values) - #################### @@ -570,6 +569,12 @@ def instance_add_security_group(context, instance_id, security_group_id): security_group_id) +def instance_remove_security_group(context, instance_id, security_group_id): + """Disassociate the given security group from the given instance.""" + return IMPL.instance_remove_security_group(context, instance_id, + security_group_id) + + def instance_action_create(context, values): """Create an instance action from the values dictionary.""" return IMPL.instance_action_create(context, values) @@ -680,7 +685,14 @@ def network_get_all(context): return IMPL.network_get_all(context) +def network_get_all_by_uuids(context, network_uuids, project_id=None): + """Return networks by ids.""" + return IMPL.network_get_all_by_uuids(context, network_uuids, project_id) + + # pylint: disable=C0103 + + def network_get_associated_fixed_ips(context, network_id): """Get all network's ips that have been associated.""" return IMPL.network_get_associated_fixed_ips(context, network_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index f14f95ab01b9..5abdd71f9ad6 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -666,23 +666,36 @@ def floating_ip_update(context, address, values): ################### -@require_context -def fixed_ip_associate(context, address, instance_id): +@require_admin_context +def fixed_ip_associate(context, address, instance_id, network_id=None): session = get_session() with session.begin(): - instance = instance_get(context, instance_id, session=session) + network_or_none = or_(models.FixedIp.network_id == network_id, + models.FixedIp.network_id == None) fixed_ip_ref = session.query(models.FixedIp).\ - filter_by(address=address).\ + filter(network_or_none).\ + filter_by(reserved=False).\ filter_by(deleted=False).\ - filter_by(instance=None).\ + filter_by(address=address).\ with_lockmode('update').\ first() # NOTE(vish): if with_lockmode isn't supported, as in sqlite, # then this has concurrency issues - if not fixed_ip_ref: - raise exception.NoMoreFixedIps() - fixed_ip_ref.instance = instance + if fixed_ip_ref is None: + raise exception.FixedIpNotFoundForNetwork(address=address, + network_id=network_id) + if fixed_ip_ref.instance is not None: + raise exception.FixedIpAlreadyInUse(address=address) + + if not fixed_ip_ref.network: + fixed_ip_ref.network = network_get(context, + network_id, + session=session) + fixed_ip_ref.instance = instance_get(context, + instance_id, + session=session) session.add(fixed_ip_ref) + return fixed_ip_ref['address'] @require_admin_context @@ -1236,7 +1249,8 @@ def instance_get_all_by_filters(context, filters): options(joinedload('security_groups')).\ options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ - options(joinedload('instance_type')) + options(joinedload('instance_type')).\ + filter_by(deleted=can_read_deleted(context)) # Make a copy of the filters dictionary to use going forward, as we'll # be modifying it and we shouldn't affect the caller's use of it. @@ -1514,6 +1528,19 @@ def instance_add_security_group(context, instance_id, security_group_id): instance_ref.save(session=session) +@require_context +def instance_remove_security_group(context, instance_id, security_group_id): + """Disassociate the given security group from the given instance""" + session = get_session() + + session.query(models.SecurityGroupInstanceAssociation).\ + filter_by(instance_id=instance_id).\ + filter_by(security_group_id=security_group_id).\ + update({'deleted': True, + 'deleted_at': utils.utcnow(), + 'updated_at': literal_column('updated_at')}) + + @require_context def instance_action_create(context, values): """Create an instance action from the values dictionary.""" @@ -1755,6 +1782,40 @@ def network_get_all(context): return result +@require_admin_context +def network_get_all_by_uuids(context, network_uuids, project_id=None): + session = get_session() + project_or_none = or_(models.Network.project_id == project_id, + models.Network.project_id == None) + result = session.query(models.Network).\ + filter(models.Network.uuid.in_(network_uuids)).\ + filter(project_or_none).\ + filter_by(deleted=False).all() + if not result: + raise exception.NoNetworksFound() + + #check if host is set to all of the networks + # returned in the result + for network in result: + if network['host'] is None: + raise exception.NetworkHostNotSet(network_id=network['id']) + + #check if the result contains all the networks + #we are looking for + for network_uuid in network_uuids: + found = False + for network in result: + if network['uuid'] == network_uuid: + found = True + break + if not found: + if project_id: + raise exception.NetworkNotFoundForProject(network_uuid=uuid, + project_id=context.project_id) + raise exception.NetworkNotFound(network_id=network_uuid) + + return result + # NOTE(vish): pylint complains because of the long method name, but # it fits with the names of the rest of the methods # pylint: disable=C0103 @@ -2576,6 +2637,7 @@ def security_group_get(context, security_group_id, session=None): filter_by(deleted=can_read_deleted(context),).\ filter_by(id=security_group_id).\ options(joinedload_all('rules')).\ + options(joinedload_all('instances')).\ first() else: result = session.query(models.SecurityGroup).\ @@ -2583,6 +2645,7 @@ def security_group_get(context, security_group_id, session=None): filter_by(id=security_group_id).\ filter_by(project_id=context.project_id).\ options(joinedload_all('rules')).\ + options(joinedload_all('instances')).\ first() if not result: raise exception.SecurityGroupNotFound( diff --git a/nova/db/sqlalchemy/migrate_repo/versions/037_instances_drop_admin_pass.py b/nova/db/sqlalchemy/migrate_repo/versions/037_instances_drop_admin_pass.py new file mode 100644 index 000000000000..b957666c29a5 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/037_instances_drop_admin_pass.py @@ -0,0 +1,37 @@ +# Copyright 2011 OpenStack LLC. +# +# 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 sqlalchemy import Column, MetaData, Table, String + +meta = MetaData() + +admin_pass = Column( + 'admin_pass', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + instances = Table('instances', meta, autoload=True, + autoload_with=migrate_engine) + instances.drop_column('admin_pass') + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + instances = Table('instances', meta, autoload=True, + autoload_with=migrate_engine) + instances.create_column(admin_pass) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/038_add_uuid_to_virtual_interfaces.py b/nova/db/sqlalchemy/migrate_repo/versions/038_add_uuid_to_virtual_interfaces.py new file mode 100644 index 000000000000..0f542cbece91 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/038_add_uuid_to_virtual_interfaces.py @@ -0,0 +1,44 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2011 Midokura KK +# +# 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 sqlalchemy import Column, Integer, MetaData, String, Table + +from nova import utils + + +meta = MetaData() + +virtual_interfaces = Table("virtual_interfaces", meta, + Column("id", Integer(), primary_key=True, + nullable=False)) +uuid_column = Column("uuid", String(36)) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + virtual_interfaces.create_column(uuid_column) + + rows = migrate_engine.execute(virtual_interfaces.select()) + for row in rows: + vif_uuid = str(utils.gen_uuid()) + migrate_engine.execute(virtual_interfaces.update()\ + .where(virtual_interfaces.c.id == row[0])\ + .values(uuid=vif_uuid)) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + virtual_interfaces.drop_column(uuid_column) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/039_add_instances_accessip.py b/nova/db/sqlalchemy/migrate_repo/versions/039_add_instances_accessip.py new file mode 100644 index 000000000000..39f0dd6cef7e --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/039_add_instances_accessip.py @@ -0,0 +1,48 @@ +# Copyright 2011 OpenStack LLC. +# +# 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 sqlalchemy import Column, Integer, MetaData, Table, String + +meta = MetaData() + +accessIPv4 = Column( + 'access_ip_v4', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + +accessIPv6 = Column( + 'access_ip_v6', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + +instances = Table('instances', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + instances.create_column(accessIPv4) + instances.create_column(accessIPv6) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta.bind = migrate_engine + instances.drop_column('access_ip_v4') + instances.drop_column('access_ip_v6') diff --git a/nova/db/sqlalchemy/migrate_repo/versions/040_add_uuid_to_networks.py b/nova/db/sqlalchemy/migrate_repo/versions/040_add_uuid_to_networks.py new file mode 100644 index 000000000000..38c543d51f0a --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/040_add_uuid_to_networks.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# +# 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 sqlalchemy import Column, Integer, MetaData, String, Table + +from nova import utils + + +meta = MetaData() + +networks = Table("networks", meta, + Column("id", Integer(), primary_key=True, nullable=False)) +uuid_column = Column("uuid", String(36)) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + networks.create_column(uuid_column) + + rows = migrate_engine.execute(networks.select()) + for row in rows: + networks_uuid = str(utils.gen_uuid()) + migrate_engine.execute(networks.update()\ + .where(networks.c.id == row[0])\ + .values(uuid=networks_uuid)) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + networks.drop_column(uuid_column) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py b/nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py new file mode 100644 index 000000000000..d3058f00d802 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/041_add_config_drive_to_instances.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2011 Piston Cloud Computing, Inc. +# +# 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 sqlalchemy import Column, Integer, MetaData, String, Table + +from nova import utils + + +meta = MetaData() + +instances = Table("instances", meta, + Column("id", Integer(), primary_key=True, nullable=False)) + +# matches the size of an image_ref +config_drive_column = Column("config_drive", String(255), nullable=True) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + instances.create_column(config_drive_column) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + instances.drop_column(config_drive_column) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/037_add_volume_types_and_extradata.py b/nova/db/sqlalchemy/migrate_repo/versions/042_add_volume_types_and_extradata.py similarity index 100% rename from nova/db/sqlalchemy/migrate_repo/versions/037_add_volume_types_and_extradata.py rename to nova/db/sqlalchemy/migrate_repo/versions/042_add_volume_types_and_extradata.py diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 4195ca1131fd..a37ccf91a12c 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -2,6 +2,7 @@ # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Piston Cloud Computing, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -173,7 +174,6 @@ class Instance(BASE, NovaBase): base_name += "-rescue" return base_name - admin_pass = Column(String(255)) user_id = Column(String(255)) project_id = Column(String(255)) @@ -231,6 +231,12 @@ class Instance(BASE, NovaBase): uuid = Column(String(36)) root_device_name = Column(String(255)) + config_drive = Column(String(255)) + + # User editable field meant to represent what ip should be used + # to connect to the instance + access_ip_v4 = Column(String(255)) + access_ip_v6 = Column(String(255)) # TODO(vish): see Ewan's email about state improvements, probably # should be in a driver base class or some such @@ -601,6 +607,7 @@ class Network(BASE, NovaBase): project_id = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) + uuid = Column(String(36)) class VirtualInterface(BASE, NovaBase): @@ -615,6 +622,8 @@ class VirtualInterface(BASE, NovaBase): instance_id = Column(Integer, ForeignKey('instances.id'), nullable=False) instance = relationship(Instance, backref=backref('virtual_interfaces')) + uuid = Column(String(36)) + @property def fixed_ipv6(self): cidr_v6 = self.network.cidr_v6 diff --git a/nova/db/sqlalchemy/session.py b/nova/db/sqlalchemy/session.py index 07f2819389b1..643e2338e668 100644 --- a/nova/db/sqlalchemy/session.py +++ b/nova/db/sqlalchemy/session.py @@ -73,9 +73,11 @@ def get_engine(): elif MySQLdb and "mysql" in connection_dict.drivername: LOG.info(_("Using mysql/eventlet db_pool.")) + # MySQLdb won't accept 'None' in the password field + password = connection_dict.password or '' pool_args = { "db": connection_dict.database, - "passwd": connection_dict.password, + "passwd": password, "host": connection_dict.host, "user": connection_dict.username, "min_size": FLAGS.sql_min_pool_size, diff --git a/nova/exception.py b/nova/exception.py index 01d676b54967..06763904248e 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -450,6 +450,15 @@ class NoNetworksFound(NotFound): message = _("No networks defined.") +class NetworkNotFoundForProject(NotFound): + message = _("Either Network uuid %(network_uuid)s is not present or " + "is not assigned to the project %(project_id)s.") + + +class NetworkHostNotSet(NovaException): + message = _("Host is not set to the network (%(network_id)s).") + + class DatastoreNotFound(NotFound): message = _("Could not find the datastore reference(s) which the VM uses.") @@ -483,6 +492,19 @@ class FixedIpNotFoundForHost(FixedIpNotFound): message = _("Host %(host)s has zero fixed ips.") +class FixedIpNotFoundForNetwork(FixedIpNotFound): + message = _("Fixed IP address (%(address)s) does not exist in " + "network (%(network_uuid)s).") + + +class FixedIpAlreadyInUse(NovaException): + message = _("Fixed IP address %(address)s is already in use.") + + +class FixedIpInvalid(Invalid): + message = _("Fixed IP address %(address)s is invalid.") + + class NoMoreFixedIps(Error): message = _("Zero fixed ips available.") @@ -568,6 +590,16 @@ class SecurityGroupNotFoundForRule(SecurityGroupNotFound): message = _("Security group with rule %(rule_id)s not found.") +class SecurityGroupExistsForInstance(Invalid): + message = _("Security group %(security_group_id)s is already associated" + " with the instance %(instance_id)s") + + +class SecurityGroupNotExistsForInstance(Invalid): + message = _("Security group %(security_group_id)s is not associated with" + " the instance %(instance_id)s") + + class MigrationNotFound(NotFound): message = _("Migration %(migration_id)s could not be found.") diff --git a/nova/flags.py b/nova/flags.py index 48d5e8168086..95000df1b63a 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -402,3 +402,14 @@ DEFINE_bool('resume_guests_state_on_host_boot', False, DEFINE_string('root_helper', 'sudo', 'Command prefix to use for running commands as root') + +DEFINE_bool('use_ipv6', False, 'use ipv6') + +DEFINE_bool('monkey_patch', False, + 'Whether to log monkey patching') + +DEFINE_list('monkey_patch_modules', + ['nova.api.ec2.cloud:nova.notifier.api.notify_decorator', + 'nova.compute.api:nova.notifier.api.notify_decorator'], + 'Module list representing monkey ' + 'patched module and decorator') diff --git a/nova/ipv6/account_identifier.py b/nova/ipv6/account_identifier.py index 258678f0a751..27bb0198882b 100644 --- a/nova/ipv6/account_identifier.py +++ b/nova/ipv6/account_identifier.py @@ -34,8 +34,12 @@ def to_global(prefix, mac, project_id): mac_addr = netaddr.IPAddress(int_addr) maskIP = netaddr.IPNetwork(prefix).ip return (project_hash ^ static_num ^ mac_addr | maskIP).format() - except TypeError: + except netaddr.AddrFormatError: raise TypeError(_('Bad mac for to_global_ipv6: %s') % mac) + except TypeError: + raise TypeError(_('Bad prefix for to_global_ipv6: %s') % prefix) + except NameError: + raise TypeError(_('Bad project_id for to_global_ipv6: %s') % project_id) def to_mac(ipv6_address): diff --git a/nova/network/api.py b/nova/network/api.py index 247768722b2b..d04474df3770 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -195,3 +195,12 @@ class API(base.Base): return rpc.call(context, FLAGS.network_topic, {'method': 'get_instance_nw_info', 'args': args}) + + def validate_networks(self, context, requested_networks): + """validate the networks passed at the time of creating + the server + """ + args = {'networks': requested_networks} + return rpc.call(context, FLAGS.network_topic, + {'method': 'validate_networks', + 'args': args}) diff --git a/nova/network/manager.py b/nova/network/manager.py index 08439b0040e9..404a3180eeab 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -106,8 +106,6 @@ flags.DEFINE_integer('create_unique_mac_address_attempts', 5, 'Number of attempts to create unique mac address') flags.DEFINE_bool('auto_assign_floating_ip', False, 'Autoassigning floating ip to VM') -flags.DEFINE_bool('use_ipv6', False, - 'use the ipv6') flags.DEFINE_string('network_host', socket.gethostname(), 'Network host to use for ip allocation in flat modes') flags.DEFINE_bool('fake_call', False, @@ -131,7 +129,15 @@ class RPCAllocateFixedIP(object): green_pool = greenpool.GreenPool() vpn = kwargs.pop('vpn') + requested_networks = kwargs.pop('requested_networks') + for network in networks: + address = None + if requested_networks is not None: + for address in (fixed_ip for (uuid, fixed_ip) in \ + requested_networks if network['uuid'] == uuid): + break + # NOTE(vish): if we are not multi_host pass to the network host if not network['multi_host']: host = network['host'] @@ -148,6 +154,7 @@ class RPCAllocateFixedIP(object): args = {} args['instance_id'] = instance_id args['network_id'] = network['id'] + args['address'] = address args['vpn'] = vpn green_pool.spawn_n(rpc.call, context, topic, @@ -155,7 +162,8 @@ class RPCAllocateFixedIP(object): 'args': args}) else: # i am the correct host, run here - self.allocate_fixed_ip(context, instance_id, network, vpn=vpn) + self.allocate_fixed_ip(context, instance_id, network, + vpn=vpn, address=address) # wait for all of the allocates (if any) to finish green_pool.waitall() @@ -199,6 +207,7 @@ class FloatingIP(object): """ instance_id = kwargs.get('instance_id') project_id = kwargs.get('project_id') + requested_networks = kwargs.get('requested_networks') LOG.debug(_("floating IP allocation for instance |%s|"), instance_id, context=context) # call the next inherited class's allocate_for_instance() @@ -380,16 +389,21 @@ class NetworkManager(manager.SchedulerDependentManager): self.compute_api.trigger_security_group_members_refresh(admin_context, group_ids) - def _get_networks_for_instance(self, context, instance_id, project_id): + def _get_networks_for_instance(self, context, instance_id, project_id, + requested_networks=None): """Determine & return which networks an instance should connect to.""" # TODO(tr3buchet) maybe this needs to be updated in the future if # there is a better way to determine which networks # a non-vlan instance should connect to - try: - networks = self.db.network_get_all(context) - except exception.NoNetworksFound: - return [] - + if requested_networks is not None and len(requested_networks) != 0: + network_uuids = [uuid for (uuid, fixed_ip) in requested_networks] + networks = self.db.network_get_all_by_uuids(context, + network_uuids) + else: + try: + networks = self.db.network_get_all(context) + except exception.NoNetworksFound: + return [] # return only networks which are not vlan networks return [network for network in networks if not network['vlan']] @@ -403,16 +417,18 @@ class NetworkManager(manager.SchedulerDependentManager): host = kwargs.pop('host') project_id = kwargs.pop('project_id') type_id = kwargs.pop('instance_type_id') + requested_networks = kwargs.get('requested_networks') vpn = kwargs.pop('vpn') admin_context = context.elevated() LOG.debug(_("network allocations for instance %s"), instance_id, context=context) - networks = self._get_networks_for_instance(admin_context, instance_id, - project_id) - LOG.warn(networks) + networks = self._get_networks_for_instance(admin_context, + instance_id, project_id, + requested_networks=requested_networks) self._allocate_mac_addresses(context, instance_id, networks) - self._allocate_fixed_ips(admin_context, instance_id, host, networks, - vpn=vpn) + self._allocate_fixed_ips(admin_context, instance_id, + host, networks, vpn=vpn, + requested_networks=requested_networks) return self.get_instance_nw_info(context, instance_id, type_id, host) def deallocate_for_instance(self, context, **kwargs): @@ -500,6 +516,7 @@ class NetworkManager(manager.SchedulerDependentManager): 'dhcp_server': dhcp_server, 'broadcast': network['broadcast'], 'mac': vif['address'], + 'vif_uuid': vif['uuid'], 'rxtx_cap': flavor['rxtx_cap'], 'dns': [], 'ips': [ip_dict(ip) for ip in network_IPs], @@ -524,7 +541,8 @@ class NetworkManager(manager.SchedulerDependentManager): for network in networks: vif = {'address': self.generate_mac_address(), 'instance_id': instance_id, - 'network_id': network['id']} + 'network_id': network['id'], + 'uuid': str(utils.gen_uuid())} # try FLAG times to create a vif record with a unique mac_address for i in range(FLAGS.create_unique_mac_address_attempts): try: @@ -568,9 +586,15 @@ class NetworkManager(manager.SchedulerDependentManager): # network_get_by_compute_host address = None if network['cidr']: - address = self.db.fixed_ip_associate_pool(context.elevated(), - network['id'], - instance_id) + address = kwargs.get('address', None) + if address: + address = self.db.fixed_ip_associate(context, + address, instance_id, + network['id']) + else: + address = self.db.fixed_ip_associate_pool(context.elevated(), + network['id'], + instance_id) self._do_trigger_security_group_members_refresh_for_instance( instance_id) get_vif = self.db.virtual_interface_get_by_instance_and_network @@ -796,6 +820,35 @@ class NetworkManager(manager.SchedulerDependentManager): """Sets up network on this host.""" raise NotImplementedError() + def validate_networks(self, context, networks): + """check if the networks exists and host + is set to each network. + """ + if networks is None or len(networks) == 0: + return + + network_uuids = [uuid for (uuid, fixed_ip) in networks] + + self._get_networks_by_uuids(context, network_uuids) + + for network_uuid, address in networks: + # check if the fixed IP address is valid and + # it actually belongs to the network + if address is not None: + if not utils.is_valid_ipv4(address): + raise exception.FixedIpInvalid(address=address) + + fixed_ip_ref = self.db.fixed_ip_get_by_address(context, + address) + if fixed_ip_ref['network']['uuid'] != network_uuid: + raise exception.FixedIpNotFoundForNetwork(address=address, + network_uuid=network_uuid) + if fixed_ip_ref['instance'] is not None: + raise exception.FixedIpAlreadyInUse(address=address) + + def _get_networks_by_uuids(self, context, network_uuids): + return self.db.network_get_all_by_uuids(context, network_uuids) + class FlatManager(NetworkManager): """Basic network where no vlans are used. @@ -830,8 +883,16 @@ class FlatManager(NetworkManager): def _allocate_fixed_ips(self, context, instance_id, host, networks, **kwargs): """Calls allocate_fixed_ip once for each network.""" + requested_networks = kwargs.pop('requested_networks') for network in networks: - self.allocate_fixed_ip(context, instance_id, network) + address = None + if requested_networks is not None: + for address in (fixed_ip for (uuid, fixed_ip) in \ + requested_networks if network['uuid'] == uuid): + break + + self.allocate_fixed_ip(context, instance_id, + network, address=address) def deallocate_fixed_ip(self, context, address, **kwargs): """Returns a fixed ip to the pool.""" @@ -925,9 +986,15 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): address, instance_id) else: - address = self.db.fixed_ip_associate_pool(context, - network['id'], - instance_id) + address = kwargs.get('address', None) + if address: + address = self.db.fixed_ip_associate(context, address, + instance_id, + network['id']) + else: + address = self.db.fixed_ip_associate_pool(context, + network['id'], + instance_id) self._do_trigger_security_group_members_refresh_for_instance( instance_id) vif = self.db.virtual_interface_get_by_instance_and_network(context, @@ -943,10 +1010,18 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): """Force adds another network to a project.""" self.db.network_associate(context, project_id, force=True) - def _get_networks_for_instance(self, context, instance_id, project_id): + def _get_networks_for_instance(self, context, instance_id, project_id, + requested_networks=None): """Determine which networks an instance should connect to.""" # get networks associated with project - return self.db.project_get_networks(context, project_id) + if requested_networks is not None and len(requested_networks) != 0: + network_uuids = [uuid for (uuid, fixed_ip) in requested_networks] + networks = self.db.network_get_all_by_uuids(context, + network_uuids, + project_id) + else: + networks = self.db.project_get_networks(context, project_id) + return networks def create_networks(self, context, **kwargs): """Create networks based on parameters.""" @@ -995,6 +1070,10 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): self.db.network_update(context, network_ref['id'], {'gateway_v6': gateway}) + def _get_networks_by_uuids(self, context, network_uuids): + return self.db.network_get_all_by_uuids(context, network_uuids, + context.project_id) + @property def _bottom_reserved_ips(self): """Number of reserved ips at the bottom of the range.""" diff --git a/nova/notifier/api.py b/nova/notifier/api.py index e18f3e280c9a..6ef4a050e36d 100644 --- a/nova/notifier/api.py +++ b/nova/notifier/api.py @@ -25,6 +25,9 @@ FLAGS = flags.FLAGS flags.DEFINE_string('default_notification_level', 'INFO', 'Default notification level for outgoing notifications') +flags.DEFINE_string('default_publisher_id', FLAGS.host, + 'Default publisher_id for outgoing notifications') + WARN = 'WARN' INFO = 'INFO' @@ -39,6 +42,30 @@ class BadPriorityException(Exception): pass +def notify_decorator(name, fn): + """ decorator for notify which is used from utils.monkey_patch() + + :param name: name of the function + :param function: - object of the function + :returns: function -- decorated function + + """ + def wrapped_func(*args, **kwarg): + body = {} + body['args'] = [] + body['kwarg'] = {} + for arg in args: + body['args'].append(arg) + for key in kwarg: + body['kwarg'][key] = kwarg[key] + notify(FLAGS.default_publisher_id, + name, + FLAGS.default_notification_level, + body) + return fn(*args, **kwarg) + return wrapped_func + + def publisher_id(service, host=None): if not host: host = FLAGS.host diff --git a/nova/scheduler/abstract_scheduler.py b/nova/scheduler/abstract_scheduler.py index 77db67773803..e8c343a4bd46 100644 --- a/nova/scheduler/abstract_scheduler.py +++ b/nova/scheduler/abstract_scheduler.py @@ -62,12 +62,13 @@ class AbstractScheduler(driver.Scheduler): host = build_plan_item['hostname'] base_options = request_spec['instance_properties'] image = request_spec['image'] + instance_type = request_spec.get('instance_type') # TODO(sandy): I guess someone needs to add block_device_mapping # support at some point? Also, OS API has no concept of security # groups. instance = compute_api.API().create_db_entry_for_new_instance(context, - image, base_options, None, []) + instance_type, image, base_options, None, []) instance_id = instance['id'] kwargs['instance_id'] = instance_id diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index 4bc5158cce9b..826a99b0a810 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -58,8 +58,6 @@ def choose_host_filter(filter_name=None): if not filter_name: filter_name = FLAGS.default_host_filter for filter_class in _get_filters(): - host_match = "%s.%s" % (filter_class.__module__, filter_class.__name__) - if (host_match.startswith("nova.scheduler.filters") and - (host_match.split(".")[-1] == filter_name)): + if filter_class.__name__ == filter_name: return filter_class() raise exception.SchedulerHostFilterNotFound(filter_name=filter_name) diff --git a/nova/service.py b/nova/service.py index 6e9eddc5a934..959e7905236f 100644 --- a/nova/service.py +++ b/nova/service.py @@ -20,13 +20,11 @@ """Generic Node baseclass for all workers that run on hosts.""" import inspect -import multiprocessing import os +import eventlet import greenlet -from eventlet import greenthread - from nova import context from nova import db from nova import exception @@ -69,30 +67,25 @@ class Launcher(object): self._services = [] @staticmethod - def run_service(service): - """Start and wait for a service to finish. + def run_server(server): + """Start and wait for a server to finish. - :param service: Service to run and wait for. + :param service: Server to run and wait for. :returns: None """ - service.start() - try: - service.wait() - except KeyboardInterrupt: - service.stop() + server.start() + server.wait() - def launch_service(self, service): - """Load and start the given service. + def launch_server(self, server): + """Load and start the given server. - :param service: The service you would like to start. + :param server: The server you would like to start. :returns: None """ - process = multiprocessing.Process(target=self.run_service, - args=(service,)) - process.start() - self._services.append(process) + gt = eventlet.spawn(self.run_server, server) + self._services.append(gt) def stop(self): """Stop all services which are currently running. @@ -101,8 +94,7 @@ class Launcher(object): """ for service in self._services: - if service.is_alive(): - service.terminate() + service.kill() def wait(self): """Waits until all services have been stopped, and then returns. @@ -111,11 +103,18 @@ class Launcher(object): """ for service in self._services: - service.join() + try: + service.wait() + except greenlet.GreenletExit: + pass class Service(object): - """Base class for workers that run on hosts.""" + """Service object for binaries running on hosts. + + A service takes a manager and enables rpc by listening to queues based + on topic. It also periodically runs tasks on the manager and reports + it state to the database services table.""" def __init__(self, host, binary, topic, manager, report_interval=None, periodic_interval=None, *args, **kwargs): @@ -173,7 +172,7 @@ class Service(object): finally: consumer_set.close() - self.consumer_set_thread = greenthread.spawn(_wait) + self.consumer_set_thread = eventlet.spawn(_wait) if self.report_interval: pulse = utils.LoopingCall(self.report_state) @@ -293,9 +292,9 @@ class WSGIService(object): """Provides ability to launch API from a 'paste' configuration.""" def __init__(self, name, loader=None): - """Initialize, but do not start the WSGI service. + """Initialize, but do not start the WSGI server. - :param name: The name of the WSGI service given to the loader. + :param name: The name of the WSGI server given to the loader. :param loader: Loads the WSGI application using the given name. :returns: None @@ -339,32 +338,32 @@ class WSGIService(object): self.server.wait() -def serve(*services): - try: - if not services: - services = [Service.create()] - except Exception: - logging.exception('in Service.create()') - raise - finally: - # After we've loaded up all our dynamic bits, check - # whether we should print help - flags.DEFINE_flag(flags.HelpFlag()) - flags.DEFINE_flag(flags.HelpshortFlag()) - flags.DEFINE_flag(flags.HelpXMLFlag()) - FLAGS.ParseNewFlags() +# NOTE(vish): the global launcher is to maintain the existing +# functionality of calling service.serve + +# service.wait +_launcher = None - name = '_'.join(x.binary for x in services) - logging.debug(_('Serving %s'), name) + +def serve(*servers): + global _launcher + if not _launcher: + _launcher = Launcher() + for server in servers: + _launcher.launch_server(server) + + +def wait(): + # After we've loaded up all our dynamic bits, check + # whether we should print help + flags.DEFINE_flag(flags.HelpFlag()) + flags.DEFINE_flag(flags.HelpshortFlag()) + flags.DEFINE_flag(flags.HelpXMLFlag()) + FLAGS.ParseNewFlags() logging.debug(_('Full set of FLAGS:')) for flag in FLAGS: flag_get = FLAGS.get(flag, None) logging.debug('%(flag)s : %(flag_get)s' % locals()) - - for x in services: - x.start() - - -def wait(): - while True: - greenthread.sleep(5) + try: + _launcher.wait() + except KeyboardInterrupt: + _launcher.stop() diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py index 458434a81ef7..7d44489a1f0e 100644 --- a/nova/tests/api/openstack/__init__.py +++ b/nova/tests/api/openstack/__init__.py @@ -44,14 +44,15 @@ class RateLimitingMiddlewareTest(test.TestCase): action = middleware.get_action_name(req) self.assertEqual(action, action_name) - verify('PUT', '/servers/4', 'PUT') - verify('DELETE', '/servers/4', 'DELETE') - verify('POST', '/images/4', 'POST') - verify('POST', '/servers/4', 'POST servers') - verify('GET', '/foo?a=4&changes-since=never&b=5', 'GET changes-since') - verify('GET', '/foo?a=4&monkeys-since=never&b=5', None) - verify('GET', '/servers/4', None) - verify('HEAD', '/servers/4', None) + verify('PUT', '/fake/servers/4', 'PUT') + verify('DELETE', '/fake/servers/4', 'DELETE') + verify('POST', '/fake/images/4', 'POST') + verify('POST', '/fake/servers/4', 'POST servers') + verify('GET', '/fake/foo?a=4&changes-since=never&b=5', + 'GET changes-since') + verify('GET', '/fake/foo?a=4&monkeys-since=never&b=5', None) + verify('GET', '/fake/servers/4', None) + verify('HEAD', '/fake/servers/4', None) def exhaust(self, middleware, method, url, username, times): req = Request.blank(url, dict(REQUEST_METHOD=method), @@ -67,13 +68,13 @@ class RateLimitingMiddlewareTest(test.TestCase): def test_single_action(self): middleware = RateLimitingMiddleware(simple_wsgi) - self.exhaust(middleware, 'DELETE', '/servers/4', 'usr1', 100) - self.exhaust(middleware, 'DELETE', '/servers/4', 'usr2', 100) + self.exhaust(middleware, 'DELETE', '/fake/servers/4', 'usr1', 100) + self.exhaust(middleware, 'DELETE', '/fake/servers/4', 'usr2', 100) def test_POST_servers_action_implies_POST_action(self): middleware = RateLimitingMiddleware(simple_wsgi) - self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) - self.exhaust(middleware, 'POST', '/images/4', 'usr2', 10) + self.exhaust(middleware, 'POST', '/fake/servers/4', 'usr1', 10) + self.exhaust(middleware, 'POST', '/fake/images/4', 'usr2', 10) self.assertTrue(set(middleware.limiter._levels) == \ set(['usr1:POST', 'usr1:POST servers', 'usr2:POST'])) @@ -81,11 +82,11 @@ class RateLimitingMiddlewareTest(test.TestCase): middleware = RateLimitingMiddleware(simple_wsgi) # Use up all of our "POST" allowance for the minute, 5 times for i in range(5): - self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) + self.exhaust(middleware, 'POST', '/fake/servers/4', 'usr1', 10) # Reset the 'POST' action counter. del middleware.limiter._levels['usr1:POST'] # All 50 daily "POST servers" actions should be all used up - self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 0) + self.exhaust(middleware, 'POST', '/fake/servers/4', 'usr1', 0) def test_proxy_ctor_works(self): middleware = RateLimitingMiddleware(simple_wsgi) diff --git a/nova/tests/api/openstack/contrib/test_createserverext.py b/nova/tests/api/openstack/contrib/test_createserverext.py new file mode 100644 index 000000000000..e5eed14fe8ab --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_createserverext.py @@ -0,0 +1,306 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-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. + +import base64 +import json +import unittest +from xml.dom import minidom + +import stubout +import webob + +from nova import exception +from nova import flags +from nova import test +from nova import utils +import nova.api.openstack +from nova.api.openstack import servers +from nova.api.openstack.contrib import createserverext +import nova.compute.api + +import nova.scheduler.api +import nova.image.fake +import nova.rpc +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS +FLAGS.verbose = True + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + +FAKE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'), + ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '10.0.2.12')] + +DUPLICATE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'), + ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12')] + +INVALID_NETWORKS = [('invalid', 'invalid-ip-address')] + + +class CreateserverextTest(test.TestCase): + + def setUp(self): + super(CreateserverextTest, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_auth(self.stubs) + fakes.stub_out_image_service(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + self.allow_admin = FLAGS.allow_admin_api + + def tearDown(self): + self.stubs.UnsetAll() + FLAGS.allow_admin_api = self.allow_admin + super(CreateserverextTest, self).tearDown() + + def _setup_mock_compute_api(self): + + class MockComputeAPI(nova.compute.API): + + def __init__(self): + self.injected_files = None + self.networks = None + + def create(self, *args, **kwargs): + if 'injected_files' in kwargs: + self.injected_files = kwargs['injected_files'] + else: + self.injected_files = None + + if 'requested_networks' in kwargs: + self.networks = kwargs['requested_networks'] + else: + self.networks = None + return [{'id': '1234', 'display_name': 'fakeinstance', + 'uuid': FAKE_UUID, + 'created_at': "", + 'updated_at': ""}] + + def set_admin_password(self, *args, **kwargs): + pass + + def make_stub_method(canned_return): + def stub_method(*args, **kwargs): + return canned_return + return stub_method + + compute_api = MockComputeAPI() + self.stubs.Set(nova.compute, 'API', make_stub_method(compute_api)) + self.stubs.Set( + nova.api.openstack.create_instance_helper.CreateInstanceHelper, + '_get_kernel_ramdisk_from_image', make_stub_method((1, 1))) + return compute_api + + def _create_networks_request_dict(self, networks): + server = {} + server['name'] = 'new-server-test' + server['imageRef'] = 1 + server['flavorRef'] = 1 + if networks is not None: + network_list = [] + for uuid, fixed_ip in networks: + network_list.append({'uuid': uuid, 'fixed_ip': fixed_ip}) + server['networks'] = network_list + return {'server': server} + + def _get_create_request_json(self, body_dict): + req = webob.Request.blank('/v1.1/123/os-create-server-ext') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body_dict) + return req + + def _run_create_instance_with_mock_compute_api(self, request): + compute_api = self._setup_mock_compute_api() + response = request.get_response(fakes.wsgi_app()) + return compute_api, response + + def _format_xml_request_body(self, body_dict): + server = body_dict['server'] + body_parts = [] + body_parts.extend([ + '', + '' % ( + server['name'], server['imageRef'], server['flavorRef'])]) + if 'metadata' in server: + metadata = server['metadata'] + body_parts.append('') + for item in metadata.iteritems(): + body_parts.append('%s' % item) + body_parts.append('') + if 'personality' in server: + personalities = server['personality'] + body_parts.append('') + for file in personalities: + item = (file['path'], file['contents']) + body_parts.append('%s' % item) + body_parts.append('') + if 'networks' in server: + networks = server['networks'] + body_parts.append('') + for network in networks: + item = (network['uuid'], network['fixed_ip']) + body_parts.append('' + % item) + body_parts.append('') + body_parts.append('') + return ''.join(body_parts) + + def _get_create_request_xml(self, body_dict): + req = webob.Request.blank('/v1.1/123/os-create-server-ext') + req.content_type = 'application/xml' + req.accept = 'application/xml' + req.method = 'POST' + req.body = self._format_xml_request_body(body_dict) + return req + + def _create_instance_with_networks_json(self, networks): + body_dict = self._create_networks_request_dict(networks) + request = self._get_create_request_json(body_dict) + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + return request, response, compute_api.networks + + def _create_instance_with_networks_xml(self, networks): + body_dict = self._create_networks_request_dict(networks) + request = self._get_create_request_xml(body_dict) + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + return request, response, compute_api.networks + + def test_create_instance_with_no_networks(self): + request, response, networks = \ + self._create_instance_with_networks_json(networks=None) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, None) + + def test_create_instance_with_no_networks_xml(self): + request, response, networks = \ + self._create_instance_with_networks_xml(networks=None) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, None) + + def test_create_instance_with_one_network(self): + request, response, networks = \ + self._create_instance_with_networks_json([FAKE_NETWORKS[0]]) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, [FAKE_NETWORKS[0]]) + + def test_create_instance_with_one_network_xml(self): + request, response, networks = \ + self._create_instance_with_networks_xml([FAKE_NETWORKS[0]]) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, [FAKE_NETWORKS[0]]) + + def test_create_instance_with_two_networks(self): + request, response, networks = \ + self._create_instance_with_networks_json(FAKE_NETWORKS) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, FAKE_NETWORKS) + + def test_create_instance_with_two_networks_xml(self): + request, response, networks = \ + self._create_instance_with_networks_xml(FAKE_NETWORKS) + self.assertEquals(response.status_int, 202) + self.assertEquals(networks, FAKE_NETWORKS) + + def test_create_instance_with_duplicate_networks(self): + request, response, networks = \ + self._create_instance_with_networks_json(DUPLICATE_NETWORKS) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_duplicate_networks_xml(self): + request, response, networks = \ + self._create_instance_with_networks_xml(DUPLICATE_NETWORKS) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_network_no_id(self): + body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]]) + del body_dict['server']['networks'][0]['uuid'] + request = self._get_create_request_json(body_dict) + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + self.assertEquals(response.status_int, 400) + self.assertEquals(compute_api.networks, None) + + def test_create_instance_with_network_no_id_xml(self): + body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]]) + request = self._get_create_request_xml(body_dict) + uuid = ' uuid="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"' + request.body = request.body.replace(uuid, '') + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + self.assertEquals(response.status_int, 400) + self.assertEquals(compute_api.networks, None) + + def test_create_instance_with_network_invalid_id(self): + request, response, networks = \ + self._create_instance_with_networks_json(INVALID_NETWORKS) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_network_invalid_id_xml(self): + request, response, networks = \ + self._create_instance_with_networks_xml(INVALID_NETWORKS) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_network_empty_fixed_ip(self): + networks = [('1', '')] + request, response, networks = \ + self._create_instance_with_networks_json(networks) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_network_non_string_fixed_ip(self): + networks = [('1', 12345)] + request, response, networks = \ + self._create_instance_with_networks_json(networks) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_network_empty_fixed_ip_xml(self): + networks = [('1', '')] + request, response, networks = \ + self._create_instance_with_networks_xml(networks) + self.assertEquals(response.status_int, 400) + self.assertEquals(networks, None) + + def test_create_instance_with_network_no_fixed_ip(self): + body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]]) + del body_dict['server']['networks'][0]['fixed_ip'] + request = self._get_create_request_json(body_dict) + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + self.assertEquals(response.status_int, 202) + self.assertEquals(compute_api.networks, + [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)]) + + def test_create_instance_with_network_no_fixed_ip_xml(self): + body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]]) + request = self._get_create_request_xml(body_dict) + request.body = request.body.replace(' fixed_ip="10.0.1.12"', '') + compute_api, response = \ + self._run_create_instance_with_mock_compute_api(request) + self.assertEquals(response.status_int, 202) + self.assertEquals(compute_api.networks, + [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)]) diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index 704d065823a3..568faf867b1d 100644 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/contrib/test_floating_ips.py @@ -17,6 +17,7 @@ import json import stubout import webob +from nova import compute from nova import context from nova import db from nova import test @@ -29,6 +30,11 @@ from nova.api.openstack.contrib.floating_ips import _translate_floating_ip_view def network_api_get_floating_ip(self, context, id): + return {'id': 1, 'address': '10.10.10.10', + 'fixed_ip': None} + + +def network_api_get_floating_ip_by_ip(self, context, address): return {'id': 1, 'address': '10.10.10.10', 'fixed_ip': {'address': '11.0.0.1'}} @@ -50,7 +56,7 @@ def network_api_release(self, context, address): pass -def network_api_associate(self, context, floating_ip, fixed_ip): +def compute_api_associate(self, context, instance_id, floating_ip): pass @@ -78,14 +84,16 @@ class FloatingIpTest(test.TestCase): fakes.stub_out_rate_limiting(self.stubs) self.stubs.Set(network.api.API, "get_floating_ip", network_api_get_floating_ip) + self.stubs.Set(network.api.API, "get_floating_ip_by_ip", + network_api_get_floating_ip) self.stubs.Set(network.api.API, "list_floating_ips", network_api_list_floating_ips) self.stubs.Set(network.api.API, "allocate_floating_ip", network_api_allocate) self.stubs.Set(network.api.API, "release_floating_ip", network_api_release) - self.stubs.Set(network.api.API, "associate_floating_ip", - network_api_associate) + self.stubs.Set(compute.api.API, "associate_floating_ip", + compute_api_associate) self.stubs.Set(network.api.API, "disassociate_floating_ip", network_api_disassociate) self.context = context.get_admin_context() @@ -112,7 +120,7 @@ class FloatingIpTest(test.TestCase): self.assertTrue('floating_ip' in view) def test_floating_ips_list(self): - req = webob.Request.blank('/v1.1/os-floating-ips') + req = webob.Request.blank('/v1.1/123/os-floating-ips') res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) res_dict = json.loads(res.body) @@ -127,65 +135,91 @@ class FloatingIpTest(test.TestCase): self.assertEqual(res_dict, response) def test_floating_ip_show(self): - req = webob.Request.blank('/v1.1/os-floating-ips/1') + req = webob.Request.blank('/v1.1/123/os-floating-ips/1') res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) res_dict = json.loads(res.body) self.assertEqual(res_dict['floating_ip']['id'], 1) self.assertEqual(res_dict['floating_ip']['ip'], '10.10.10.10') - self.assertEqual(res_dict['floating_ip']['fixed_ip'], '11.0.0.1') self.assertEqual(res_dict['floating_ip']['instance_id'], None) def test_floating_ip_allocate(self): - req = webob.Request.blank('/v1.1/os-floating-ips') + req = webob.Request.blank('/v1.1/123/os-floating-ips') req.method = 'POST' req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app()) - print res self.assertEqual(res.status_int, 200) - ip = json.loads(res.body)['allocated'] + ip = json.loads(res.body)['floating_ip'] + expected = { "id": 1, - "floating_ip": '10.10.10.10'} + "instance_id": None, + "ip": "10.10.10.10", + "fixed_ip": None} self.assertEqual(ip, expected) def test_floating_ip_release(self): - req = webob.Request.blank('/v1.1/os-floating-ips/1') + req = webob.Request.blank('/v1.1/123/os-floating-ips/1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - actual = json.loads(res.body)['released'] - expected = { - "id": 1, - "floating_ip": '10.10.10.10'} - self.assertEqual(actual, expected) + self.assertEqual(res.status_int, 202) - def test_floating_ip_associate(self): - body = dict(associate_address=dict(fixed_ip='1.2.3.4')) - req = webob.Request.blank('/v1.1/os-floating-ips/1/associate') - req.method = 'POST' + def test_add_floating_ip_to_instance(self): + body = dict(addFloatingIp=dict(address='11.0.0.1')) + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') + req.method = "POST" req.body = json.dumps(body) req.headers["content-type"] = "application/json" - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - actual = json.loads(res.body)['associated'] - expected = { - "floating_ip_id": '1', - "floating_ip": "10.10.10.10", - "fixed_ip": "1.2.3.4"} - self.assertEqual(actual, expected) + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) - def test_floating_ip_disassociate(self): - body = dict() - req = webob.Request.blank('/v1.1/os-floating-ips/1/disassociate') - req.method = 'POST' + def test_remove_floating_ip_from_instance(self): + body = dict(removeFloatingIp=dict(address='11.0.0.1')) + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') + req.method = "POST" req.body = json.dumps(body) - req.headers['Content-Type'] = 'application/json' - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - ip = json.loads(res.body)['disassociated'] - expected = { - "floating_ip": '10.10.10.10', - "fixed_ip": '11.0.0.1'} - self.assertEqual(ip, expected) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) + + def test_bad_address_param_in_remove_floating_ip(self): + body = dict(removeFloatingIp=dict(badparam='11.0.0.1')) + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 400) + + def test_missing_dict_param_in_remove_floating_ip(self): + body = dict(removeFloatingIp='11.0.0.1') + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 400) + + def test_bad_address_param_in_add_floating_ip(self): + body = dict(addFloatingIp=dict(badparam='11.0.0.1')) + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 400) + + def test_missing_dict_param_in_add_floating_ip(self): + body = dict(addFloatingIp='11.0.0.1') + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 400) diff --git a/nova/tests/api/openstack/contrib/test_keypairs.py b/nova/tests/api/openstack/contrib/test_keypairs.py index eb3bc7af0683..92e401aac29f 100644 --- a/nova/tests/api/openstack/contrib/test_keypairs.py +++ b/nova/tests/api/openstack/contrib/test_keypairs.py @@ -58,7 +58,7 @@ class KeypairsTest(test.TestCase): self.context = context.get_admin_context() def test_keypair_list(self): - req = webob.Request.blank('/v1.1/os-keypairs') + req = webob.Request.blank('/v1.1/123/os-keypairs') res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) res_dict = json.loads(res.body) @@ -67,7 +67,7 @@ class KeypairsTest(test.TestCase): def test_keypair_create(self): body = {'keypair': {'name': 'create_test'}} - req = webob.Request.blank('/v1.1/os-keypairs') + req = webob.Request.blank('/v1.1/123/os-keypairs') req.method = 'POST' req.body = json.dumps(body) req.headers['Content-Type'] = 'application/json' @@ -93,7 +93,7 @@ class KeypairsTest(test.TestCase): }, } - req = webob.Request.blank('/v1.1/os-keypairs') + req = webob.Request.blank('/v1.1/123/os-keypairs') req.method = 'POST' req.body = json.dumps(body) req.headers['Content-Type'] = 'application/json' @@ -105,7 +105,7 @@ class KeypairsTest(test.TestCase): self.assertFalse('private_key' in res_dict['keypair']) def test_keypair_delete(self): - req = webob.Request.blank('/v1.1/os-keypairs/FAKE') + req = webob.Request.blank('/v1.1/123/os-keypairs/FAKE') req.method = 'DELETE' req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app()) diff --git a/nova/tests/api/openstack/contrib/test_multinic_xs.py b/nova/tests/api/openstack/contrib/test_multinic_xs.py index ac28f6be6778..cecc4af4ffec 100644 --- a/nova/tests/api/openstack/contrib/test_multinic_xs.py +++ b/nova/tests/api/openstack/contrib/test_multinic_xs.py @@ -55,7 +55,7 @@ class FixedIpTest(test.TestCase): last_add_fixed_ip = (None, None) body = dict(addFixedIp=dict(networkId='test_net')) - req = webob.Request.blank('/v1.1/servers/test_inst/action') + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') req.method = 'POST' req.body = json.dumps(body) req.headers['content-type'] = 'application/json' @@ -69,7 +69,7 @@ class FixedIpTest(test.TestCase): last_add_fixed_ip = (None, None) body = dict(addFixedIp=dict()) - req = webob.Request.blank('/v1.1/servers/test_inst/action') + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') req.method = 'POST' req.body = json.dumps(body) req.headers['content-type'] = 'application/json' @@ -83,7 +83,7 @@ class FixedIpTest(test.TestCase): last_remove_fixed_ip = (None, None) body = dict(removeFixedIp=dict(address='10.10.10.1')) - req = webob.Request.blank('/v1.1/servers/test_inst/action') + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') req.method = 'POST' req.body = json.dumps(body) req.headers['content-type'] = 'application/json' @@ -97,7 +97,7 @@ class FixedIpTest(test.TestCase): last_remove_fixed_ip = (None, None) body = dict(removeFixedIp=dict()) - req = webob.Request.blank('/v1.1/servers/test_inst/action') + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') req.method = 'POST' req.body = json.dumps(body) req.headers['content-type'] = 'application/json' diff --git a/nova/tests/api/openstack/contrib/test_quotas.py b/nova/tests/api/openstack/contrib/test_quotas.py index f6a25385f122..7faef08b2298 100644 --- a/nova/tests/api/openstack/contrib/test_quotas.py +++ b/nova/tests/api/openstack/contrib/test_quotas.py @@ -78,7 +78,8 @@ class QuotaSetsTest(test.TestCase): self.assertEqual(qs['injected_file_content_bytes'], 10240) def test_quotas_defaults(self): - req = webob.Request.blank('/v1.1/os-quota-sets/fake_tenant/defaults') + uri = '/v1.1/fake_tenant/os-quota-sets/fake_tenant/defaults' + req = webob.Request.blank(uri) req.method = 'GET' req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app()) @@ -99,7 +100,7 @@ class QuotaSetsTest(test.TestCase): self.assertEqual(json.loads(res.body), expected) def test_quotas_show_as_admin(self): - req = webob.Request.blank('/v1.1/os-quota-sets/1234') + req = webob.Request.blank('/v1.1/1234/os-quota-sets/1234') req.method = 'GET' req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app( @@ -109,7 +110,7 @@ class QuotaSetsTest(test.TestCase): self.assertEqual(json.loads(res.body), quota_set('1234')) def test_quotas_show_as_unauthorized_user(self): - req = webob.Request.blank('/v1.1/os-quota-sets/1234') + req = webob.Request.blank('/v1.1/fake/os-quota-sets/1234') req.method = 'GET' req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app( @@ -124,7 +125,7 @@ class QuotaSetsTest(test.TestCase): 'metadata_items': 128, 'injected_files': 5, 'injected_file_content_bytes': 10240}} - req = webob.Request.blank('/v1.1/os-quota-sets/update_me') + req = webob.Request.blank('/v1.1/1234/os-quota-sets/update_me') req.method = 'PUT' req.body = json.dumps(updated_quota_set) req.headers['Content-Type'] = 'application/json' @@ -141,7 +142,7 @@ class QuotaSetsTest(test.TestCase): 'metadata_items': 128, 'injected_files': 5, 'injected_file_content_bytes': 10240}} - req = webob.Request.blank('/v1.1/os-quota-sets/update_me') + req = webob.Request.blank('/v1.1/1234/os-quota-sets/update_me') req.method = 'PUT' req.body = json.dumps(updated_quota_set) req.headers['Content-Type'] = 'application/json' diff --git a/nova/tests/api/openstack/contrib/test_rescue.py b/nova/tests/api/openstack/contrib/test_rescue.py new file mode 100644 index 000000000000..f8126d46141c --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_rescue.py @@ -0,0 +1,55 @@ +# Copyright 2011 OpenStack LLC. +# +# 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. + +import json +import webob + +from nova import compute +from nova import test +from nova.tests.api.openstack import fakes + + +def rescue(self, context, instance_id): + pass + + +def unrescue(self, context, instance_id): + pass + + +class RescueTest(test.TestCase): + def setUp(self): + super(RescueTest, self).setUp() + self.stubs.Set(compute.api.API, "rescue", rescue) + self.stubs.Set(compute.api.API, "unrescue", unrescue) + + def test_rescue(self): + body = dict(rescue=None) + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) + + def test_unrescue(self): + body = dict(unrescue=None) + req = webob.Request.blank('/v1.1/123/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 200) diff --git a/nova/tests/api/openstack/contrib/test_security_groups.py b/nova/tests/api/openstack/contrib/test_security_groups.py index 4317880cac64..bc15369114c8 100644 --- a/nova/tests/api/openstack/contrib/test_security_groups.py +++ b/nova/tests/api/openstack/contrib/test_security_groups.py @@ -15,17 +15,20 @@ # under the License. import json +import mox +import nova import unittest import webob from xml.dom import minidom +from nova import exception from nova import test from nova.api.openstack.contrib import security_groups from nova.tests.api.openstack import fakes def _get_create_request_json(body_dict): - req = webob.Request.blank('/v1.1/os-security-groups') + req = webob.Request.blank('/v1.1/123/os-security-groups') req.headers['Content-Type'] = 'application/json' req.method = 'POST' req.body = json.dumps(body_dict) @@ -51,6 +54,28 @@ def _create_security_group_request_dict(security_group): return {'security_group': sg} +def return_server(context, server_id): + return {'id': server_id, 'state': 0x01, 'host': "localhost"} + + +def return_non_running_server(context, server_id): + return {'id': server_id, 'state': 0x02, + 'host': "localhost"} + + +def return_security_group(context, project_id, group_name): + return {'id': 1, 'name': group_name, "instances": [ + {'id': 1}]} + + +def return_security_group_without_instances(context, project_id, group_name): + return {'id': 1, 'name': group_name} + + +def return_server_nonexistant(context, server_id): + raise exception.InstanceNotFound(instance_id=server_id) + + class TestSecurityGroups(test.TestCase): def setUp(self): super(TestSecurityGroups, self).setUp() @@ -84,7 +109,7 @@ class TestSecurityGroups(test.TestCase): return ''.join(body_parts) def _get_create_request_xml(self, body_dict): - req = webob.Request.blank('/v1.1/os-security-groups') + req = webob.Request.blank('/v1.1/123/os-security-groups') req.headers['Content-Type'] = 'application/xml' req.content_type = 'application/xml' req.accept = 'application/xml' @@ -99,7 +124,7 @@ class TestSecurityGroups(test.TestCase): return response def _delete_security_group(self, id): - request = webob.Request.blank('/v1.1/os-security-groups/%s' + request = webob.Request.blank('/v1.1/123/os-security-groups/%s' % id) request.method = 'DELETE' response = request.get_response(fakes.wsgi_app()) @@ -238,7 +263,7 @@ class TestSecurityGroups(test.TestCase): security_group['description'] = "group-description" response = _create_security_group_json(security_group) - req = webob.Request.blank('/v1.1/os-security-groups') + req = webob.Request.blank('/v1.1/123/os-security-groups') req.headers['Content-Type'] = 'application/json' req.method = 'GET' response = req.get_response(fakes.wsgi_app()) @@ -247,7 +272,7 @@ class TestSecurityGroups(test.TestCase): expected = {'security_groups': [ {'id': 1, 'name':"default", - 'tenant_id': "fake", + 'tenant_id': "123", "description":"default", "rules": [] }, @@ -257,7 +282,7 @@ class TestSecurityGroups(test.TestCase): { 'id': 2, 'name': "test", - 'tenant_id': "fake", + 'tenant_id': "123", "description": "group-description", "rules": [] } @@ -272,7 +297,7 @@ class TestSecurityGroups(test.TestCase): response = _create_security_group_json(security_group) res_dict = json.loads(response.body) - req = webob.Request.blank('/v1.1/os-security-groups/%s' % + req = webob.Request.blank('/v1.1/123/os-security-groups/%s' % res_dict['security_group']['id']) req.headers['Content-Type'] = 'application/json' req.method = 'GET' @@ -283,23 +308,22 @@ class TestSecurityGroups(test.TestCase): 'security_group': { 'id': 2, 'name': "test", - 'tenant_id': "fake", + 'tenant_id': "123", 'description': "group-description", 'rules': [] } } - self.assertEquals(response.status_int, 200) self.assertEquals(res_dict, expected) def test_get_security_group_by_invalid_id(self): - req = webob.Request.blank('/v1.1/os-security-groups/invalid') + req = webob.Request.blank('/v1.1/123/os-security-groups/invalid') req.headers['Content-Type'] = 'application/json' req.method = 'GET' response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 400) def test_get_security_group_by_non_existing_id(self): - req = webob.Request.blank('/v1.1/os-security-groups/111111111') + req = webob.Request.blank('/v1.1/123/os-security-groups/111111111') req.headers['Content-Type'] = 'application/json' req.method = 'GET' response = req.get_response(fakes.wsgi_app()) @@ -325,6 +349,252 @@ class TestSecurityGroups(test.TestCase): response = self._delete_security_group(11111111) self.assertEquals(response.status_int, 404) + def test_associate_by_non_existing_security_group_name(self): + body = dict(addSecurityGroup=dict(name='non-existing')) + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 404) + + def test_associate_by_invalid_server_id(self): + body = dict(addSecurityGroup=dict(name='test')) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + req = webob.Request.blank('/v1.1/123/servers/invalid/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_associate_without_body(self): + req = webob.Request.blank('/v1.1/123/servers/1/action') + body = dict(addSecurityGroup=None) + self.stubs.Set(nova.db, 'instance_get', return_server) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_associate_no_security_group_name(self): + req = webob.Request.blank('/v1.1/123/servers/1/action') + body = dict(addSecurityGroup=dict()) + self.stubs.Set(nova.db, 'instance_get', return_server) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_associate_security_group_name_with_whitespaces(self): + req = webob.Request.blank('/v1.1/123/servers/1/action') + body = dict(addSecurityGroup=dict(name=" ")) + self.stubs.Set(nova.db, 'instance_get', return_server) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_associate_non_existing_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) + body = dict(addSecurityGroup=dict(name="test")) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + req = webob.Request.blank('/v1.1/123/servers/10000/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 404) + + def test_associate_non_running_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_non_running_server) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_without_instances) + body = dict(addSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_associate_already_associated_security_group_to_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + body = dict(addSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_associate(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.mox.StubOutWithMock(nova.db, 'instance_add_security_group') + nova.db.instance_add_security_group(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_without_instances) + self.mox.ReplayAll() + + body = dict(addSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 202) + + def test_associate_xml(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.mox.StubOutWithMock(nova.db, 'instance_add_security_group') + nova.db.instance_add_security_group(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_without_instances) + self.mox.ReplayAll() + + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.headers['Content-Type'] = 'application/xml' + req.method = 'POST' + req.body = """ + test + """ + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 202) + + def test_disassociate_by_non_existing_security_group_name(self): + body = dict(removeSecurityGroup=dict(name='non-existing')) + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 404) + + def test_disassociate_by_invalid_server_id(self): + body = dict(removeSecurityGroup=dict(name='test')) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + req = webob.Request.blank('/v1.1/123/servers/invalid/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_disassociate_without_body(self): + req = webob.Request.blank('/v1.1/123/servers/1/action') + body = dict(removeSecurityGroup=None) + self.stubs.Set(nova.db, 'instance_get', return_server) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_disassociate_no_security_group_name(self): + req = webob.Request.blank('/v1.1/123/servers/1/action') + body = dict(removeSecurityGroup=dict()) + self.stubs.Set(nova.db, 'instance_get', return_server) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_disassociate_security_group_name_with_whitespaces(self): + req = webob.Request.blank('/v1.1/123/servers/1/action') + body = dict(removeSecurityGroup=dict(name=" ")) + self.stubs.Set(nova.db, 'instance_get', return_server) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_disassociate_non_existing_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) + body = dict(removeSecurityGroup=dict(name="test")) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + req = webob.Request.blank('/v1.1/123/servers/10000/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 404) + + def test_disassociate_non_running_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_non_running_server) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + body = dict(removeSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_disassociate_already_associated_security_group_to_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_without_instances) + body = dict(removeSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_disassociate(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.mox.StubOutWithMock(nova.db, 'instance_remove_security_group') + nova.db.instance_remove_security_group(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + self.mox.ReplayAll() + + body = dict(removeSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 202) + + def test_disassociate_xml(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.mox.StubOutWithMock(nova.db, 'instance_remove_security_group') + nova.db.instance_remove_security_group(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + self.mox.ReplayAll() + + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.headers['Content-Type'] = 'application/xml' + req.method = 'POST' + req.body = """ + test + """ + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 202) + class TestSecurityGroupRules(test.TestCase): def setUp(self): @@ -354,7 +624,7 @@ class TestSecurityGroupRules(test.TestCase): super(TestSecurityGroupRules, self).tearDown() def _create_security_group_rule_json(self, rules): - request = webob.Request.blank('/v1.1/os-security-group-rules') + request = webob.Request.blank('/v1.1/123/os-security-group-rules') request.headers['Content-Type'] = 'application/json' request.method = 'POST' request.body = json.dumps(rules) @@ -362,7 +632,7 @@ class TestSecurityGroupRules(test.TestCase): return response def _delete_security_group_rule(self, id): - request = webob.Request.blank('/v1.1/os-security-group-rules/%s' + request = webob.Request.blank('/v1.1/123/os-security-group-rules/%s' % id) request.method = 'DELETE' response = request.get_response(fakes.wsgi_app()) @@ -420,7 +690,7 @@ class TestSecurityGroupRules(test.TestCase): self.assertEquals(response.status_int, 400) def test_create_with_no_body_json(self): - request = webob.Request.blank('/v1.1/os-security-group-rules') + request = webob.Request.blank('/v1.1/123/os-security-group-rules') request.headers['Content-Type'] = 'application/json' request.method = 'POST' request.body = json.dumps(None) @@ -428,7 +698,7 @@ class TestSecurityGroupRules(test.TestCase): self.assertEquals(response.status_int, 422) def test_create_with_no_security_group_rule_in_body_json(self): - request = webob.Request.blank('/v1.1/os-security-group-rules') + request = webob.Request.blank('/v1.1/123/os-security-group-rules') request.headers['Content-Type'] = 'application/json' request.method = 'POST' body_dict = {'test': "test"} diff --git a/nova/tests/api/openstack/contrib/test_virtual_interfaces.py b/nova/tests/api/openstack/contrib/test_virtual_interfaces.py new file mode 100644 index 000000000000..1db253b35fcc --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_virtual_interfaces.py @@ -0,0 +1,55 @@ +# Copyright (C) 2011 Midokura KK +# 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. + +import json +import stubout +import webob + +from nova import test +from nova import compute +from nova.tests.api.openstack import fakes +from nova.api.openstack.contrib.virtual_interfaces import \ + ServerVirtualInterfaceController + + +def compute_api_get(self, context, server_id): + return {'virtual_interfaces': [ + {'uuid': '00000000-0000-0000-0000-00000000000000000', + 'address': '00-00-00-00-00-00'}, + {'uuid': '11111111-1111-1111-1111-11111111111111111', + 'address': '11-11-11-11-11-11'}]} + + +class ServerVirtualInterfaceTest(test.TestCase): + + def setUp(self): + super(ServerVirtualInterfaceTest, self).setUp() + self.controller = ServerVirtualInterfaceController() + self.stubs.Set(compute.api.API, "get", compute_api_get) + + def tearDown(self): + super(ServerVirtualInterfaceTest, self).tearDown() + + def test_get_virtual_interfaces_list(self): + req = webob.Request.blank('/v1.1/123/servers/1/os-virtual-interfaces') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + response = {'virtual_interfaces': [ + {'id': '00000000-0000-0000-0000-00000000000000000', + 'mac_address': '00-00-00-00-00-00'}, + {'id': '11111111-1111-1111-1111-11111111111111111', + 'mac_address': '11-11-11-11-11-11'}]} + self.assertEqual(res_dict, response) diff --git a/nova/tests/api/openstack/extensions/foxinsocks.py b/nova/tests/api/openstack/extensions/foxinsocks.py index 03aad007a2fa..2d8313cf6044 100644 --- a/nova/tests/api/openstack/extensions/foxinsocks.py +++ b/nova/tests/api/openstack/extensions/foxinsocks.py @@ -72,8 +72,9 @@ class Foxinsocks(object): res.body = json.dumps(data) return res - req_ext1 = extensions.RequestExtension('GET', '/v1.1/flavors/:(id)', - _goose_handler) + req_ext1 = extensions.RequestExtension('GET', + '/v1.1/:(project_id)/flavors/:(id)', + _goose_handler) request_exts.append(req_ext1) def _bands_handler(req, res): @@ -84,8 +85,9 @@ class Foxinsocks(object): res.body = json.dumps(data) return res - req_ext2 = extensions.RequestExtension('GET', '/v1.1/flavors/:(id)', - _bands_handler) + req_ext2 = extensions.RequestExtension('GET', + '/v1.1/:(project_id)/flavors/:(id)', + _bands_handler) request_exts.append(req_ext2) return request_exts diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index d11fbf7883c8..a095dd90adcb 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -32,6 +32,7 @@ from nova import utils from nova import wsgi import nova.api.openstack.auth from nova.api import openstack +from nova.api import auth as api_auth from nova.api.openstack import auth from nova.api.openstack import extensions from nova.api.openstack import versions @@ -83,9 +84,9 @@ def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True, ctxt = fake_auth_context else: ctxt = context.RequestContext('fake', 'fake') - api10 = openstack.FaultWrapper(wsgi.InjectContext(ctxt, + api10 = openstack.FaultWrapper(api_auth.InjectContext(ctxt, limits.RateLimitingMiddleware(inner_app10))) - api11 = openstack.FaultWrapper(wsgi.InjectContext(ctxt, + api11 = openstack.FaultWrapper(api_auth.InjectContext(ctxt, limits.RateLimitingMiddleware( extensions.ExtensionMiddleware(inner_app11)))) else: diff --git a/nova/tests/api/openstack/test_auth.py b/nova/tests/api/openstack/test_auth.py index 306ae1aa05ec..7fd3935a2baa 100644 --- a/nova/tests/api/openstack/test_auth.py +++ b/nova/tests/api/openstack/test_auth.py @@ -53,6 +53,7 @@ class Test(test.TestCase): req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'user1' req.headers['X-Auth-Key'] = 'user1_key' + req.headers['X-Auth-Project-Id'] = 'user1_project' result = req.get_response(fakes.wsgi_app(fake_auth=False)) self.assertEqual(result.status, '204 No Content') self.assertEqual(len(result.headers['X-Auth-Token']), 40) @@ -73,14 +74,14 @@ class Test(test.TestCase): self.assertEqual(result.status, '204 No Content') self.assertEqual(len(result.headers['X-Auth-Token']), 40) self.assertEqual(result.headers['X-Server-Management-Url'], - "http://foo/v1.0/") + "http://foo/v1.0") self.assertEqual(result.headers['X-CDN-Management-Url'], "") self.assertEqual(result.headers['X-Storage-Url'], "") token = result.headers['X-Auth-Token'] self.stubs.Set(nova.api.openstack, 'APIRouterV10', fakes.FakeRouter) - req = webob.Request.blank('/v1.0/fake') + req = webob.Request.blank('/v1.0/user1_project') req.headers['X-Auth-Token'] = token result = req.get_response(fakes.wsgi_app(fake_auth=False)) self.assertEqual(result.status, '200 OK') @@ -125,7 +126,7 @@ class Test(test.TestCase): token = result.headers['X-Auth-Token'] self.stubs.Set(nova.api.openstack, 'APIRouterV10', fakes.FakeRouter) - req = webob.Request.blank('/v1.0/fake') + req = webob.Request.blank('/v1.0/') req.headers['X-Auth-Token'] = token req.headers['X-Auth-Project-Id'] = 'user2_project' result = req.get_response(fakes.wsgi_app(fake_auth=False)) @@ -136,6 +137,7 @@ class Test(test.TestCase): req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'unknown_user' req.headers['X-Auth-Key'] = 'unknown_user_key' + req.headers['X-Auth-Project-Id'] = 'user_project' result = req.get_response(fakes.wsgi_app(fake_auth=False)) self.assertEqual(result.status, '401 Unauthorized') diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index 5d3208e1072e..9f923852dfb3 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -85,6 +85,7 @@ class ExtensionControllerTest(test.TestCase): ext_path = os.path.join(os.path.dirname(__file__), "extensions") self.flags(osapi_extensions_path=ext_path) self.ext_list = [ + "Createserverext", "FlavorExtraSpecs", "Floating_ips", "Fox In Socks", @@ -92,7 +93,9 @@ class ExtensionControllerTest(test.TestCase): "Keypairs", "Multinic", "Quotas", + "Rescue", "SecurityGroups", + "VirtualInterfaces", "Volumes", ] self.ext_list.sort() @@ -100,7 +103,7 @@ class ExtensionControllerTest(test.TestCase): def test_list_extensions_json(self): app = openstack.APIRouterV11() ext_midware = extensions.ExtensionMiddleware(app) - request = webob.Request.blank("/extensions") + request = webob.Request.blank("/123/extensions") response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) @@ -126,7 +129,7 @@ class ExtensionControllerTest(test.TestCase): def test_get_extension_json(self): app = openstack.APIRouterV11() ext_midware = extensions.ExtensionMiddleware(app) - request = webob.Request.blank("/extensions/FOXNSOX") + request = webob.Request.blank("/123/extensions/FOXNSOX") response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) @@ -142,7 +145,7 @@ class ExtensionControllerTest(test.TestCase): def test_list_extensions_xml(self): app = openstack.APIRouterV11() ext_midware = extensions.ExtensionMiddleware(app) - request = webob.Request.blank("/extensions") + request = webob.Request.blank("/123/extensions") request.accept = "application/xml" response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) @@ -169,7 +172,7 @@ class ExtensionControllerTest(test.TestCase): def test_get_extension_xml(self): app = openstack.APIRouterV11() ext_midware = extensions.ExtensionMiddleware(app) - request = webob.Request.blank("/extensions/FOXNSOX") + request = webob.Request.blank("/123/extensions/FOXNSOX") request.accept = "application/xml" response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) @@ -210,7 +213,7 @@ class ResourceExtensionTest(test.TestCase): manager = StubExtensionManager(res_ext) app = openstack.APIRouterV11() ext_midware = extensions.ExtensionMiddleware(app, manager) - request = webob.Request.blank("/tweedles") + request = webob.Request.blank("/123/tweedles") response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) self.assertEqual(response_body, response.body) @@ -221,7 +224,7 @@ class ResourceExtensionTest(test.TestCase): manager = StubExtensionManager(res_ext) app = openstack.APIRouterV11() ext_midware = extensions.ExtensionMiddleware(app, manager) - request = webob.Request.blank("/tweedles") + request = webob.Request.blank("/123/tweedles") response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) self.assertEqual(response_body, response.body) @@ -245,7 +248,7 @@ class ExtensionManagerTest(test.TestCase): def test_get_resources(self): app = openstack.APIRouterV11() ext_midware = extensions.ExtensionMiddleware(app) - request = webob.Request.blank("/foxnsocks") + request = webob.Request.blank("/123/foxnsocks") response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) self.assertEqual(response_body, response.body) @@ -278,23 +281,26 @@ class ActionExtensionTest(test.TestCase): def test_extended_action(self): body = dict(add_tweedle=dict(name="test")) - response = self._send_server_action_request("/servers/1/action", body) + url = "/123/servers/1/action" + response = self._send_server_action_request(url, body) self.assertEqual(200, response.status_int) self.assertEqual("Tweedle Beetle Added.", response.body) body = dict(delete_tweedle=dict(name="test")) - response = self._send_server_action_request("/servers/1/action", body) + response = self._send_server_action_request(url, body) self.assertEqual(200, response.status_int) self.assertEqual("Tweedle Beetle Deleted.", response.body) def test_invalid_action_body(self): body = dict(blah=dict(name="test")) # Doesn't exist - response = self._send_server_action_request("/servers/1/action", body) + url = "/123/servers/1/action" + response = self._send_server_action_request(url, body) self.assertEqual(400, response.status_int) def test_invalid_action(self): body = dict(blah=dict(name="test")) - response = self._send_server_action_request("/fdsa/1/action", body) + url = "/123/fdsa/1/action" + response = self._send_server_action_request(url, body) self.assertEqual(404, response.status_int) @@ -315,13 +321,13 @@ class RequestExtensionTest(test.TestCase): return res req_ext = extensions.RequestExtension('GET', - '/v1.1/flavors/:(id)', + '/v1.1/123/flavors/:(id)', _req_handler) manager = StubExtensionManager(None, None, req_ext) app = fakes.wsgi_app() ext_midware = extensions.ExtensionMiddleware(app, manager) - request = webob.Request.blank("/v1.1/flavors/1?chewing=bluegoo") + request = webob.Request.blank("/v1.1/123/flavors/1?chewing=bluegoo") request.environ['api.version'] = '1.1' response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) @@ -332,7 +338,7 @@ class RequestExtensionTest(test.TestCase): app = fakes.wsgi_app() ext_midware = extensions.ExtensionMiddleware(app) - request = webob.Request.blank("/v1.1/flavors/1?chewing=newblue") + request = webob.Request.blank("/v1.1/123/flavors/1?chewing=newblue") request.environ['api.version'] = '1.1' response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) diff --git a/nova/tests/api/openstack/test_flavors.py b/nova/tests/api/openstack/test_flavors.py index d0fe72001f13..812bece424c2 100644 --- a/nova/tests/api/openstack/test_flavors.py +++ b/nova/tests/api/openstack/test_flavors.py @@ -138,7 +138,7 @@ class FlavorsTest(test.TestCase): self.assertEqual(res.status_int, 404) def test_get_flavor_by_id_v1_1(self): - req = webob.Request.blank('/v1.1/flavors/12') + req = webob.Request.blank('/v1.1/fake/flavors/12') req.environ['api.version'] = '1.1' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) @@ -152,11 +152,11 @@ class FlavorsTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/12", + "href": "http://localhost/v1.1/fake/flavors/12", }, { "rel": "bookmark", - "href": "http://localhost/flavors/12", + "href": "http://localhost/fake/flavors/12", }, ], }, @@ -164,7 +164,7 @@ class FlavorsTest(test.TestCase): self.assertEqual(flavor, expected) def test_get_flavor_list_v1_1(self): - req = webob.Request.blank('/v1.1/flavors') + req = webob.Request.blank('/v1.1/fake/flavors') req.environ['api.version'] = '1.1' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) @@ -177,11 +177,11 @@ class FlavorsTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/1", + "href": "http://localhost/v1.1/fake/flavors/1", }, { "rel": "bookmark", - "href": "http://localhost/flavors/1", + "href": "http://localhost/fake/flavors/1", }, ], }, @@ -191,11 +191,11 @@ class FlavorsTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/2", + "href": "http://localhost/v1.1/fake/flavors/2", }, { "rel": "bookmark", - "href": "http://localhost/flavors/2", + "href": "http://localhost/fake/flavors/2", }, ], }, @@ -204,7 +204,7 @@ class FlavorsTest(test.TestCase): self.assertEqual(flavor, expected) def test_get_flavor_list_detail_v1_1(self): - req = webob.Request.blank('/v1.1/flavors/detail') + req = webob.Request.blank('/v1.1/fake/flavors/detail') req.environ['api.version'] = '1.1' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) @@ -219,11 +219,11 @@ class FlavorsTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/1", + "href": "http://localhost/v1.1/fake/flavors/1", }, { "rel": "bookmark", - "href": "http://localhost/flavors/1", + "href": "http://localhost/fake/flavors/1", }, ], }, @@ -235,11 +235,11 @@ class FlavorsTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/2", + "href": "http://localhost/v1.1/fake/flavors/2", }, { "rel": "bookmark", - "href": "http://localhost/flavors/2", + "href": "http://localhost/fake/flavors/2", }, ], }, @@ -252,7 +252,7 @@ class FlavorsTest(test.TestCase): return {} self.stubs.Set(nova.db.api, "instance_type_get_all", _return_empty) - req = webob.Request.blank('/v1.1/flavors') + req = webob.Request.blank('/v1.1/fake/flavors') res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) flavors = json.loads(res.body)["flavors"] @@ -274,11 +274,11 @@ class FlavorsXMLSerializationTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/12", + "href": "http://localhost/v1.1/fake/flavors/12", }, { "rel": "bookmark", - "href": "http://localhost/flavors/12", + "href": "http://localhost/fake/flavors/12", }, ], }, @@ -294,8 +294,10 @@ class FlavorsXMLSerializationTest(test.TestCase): name="asdf" ram="256" disk="10"> - - + + """.replace(" ", "")) @@ -313,11 +315,11 @@ class FlavorsXMLSerializationTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/12", + "href": "http://localhost/v1.1/fake/flavors/12", }, { "rel": "bookmark", - "href": "http://localhost/flavors/12", + "href": "http://localhost/fake/flavors/12", }, ], }, @@ -333,8 +335,10 @@ class FlavorsXMLSerializationTest(test.TestCase): name="asdf" ram="256" disk="10"> - - + + """.replace(" ", "")) @@ -353,11 +357,11 @@ class FlavorsXMLSerializationTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/23", + "href": "http://localhost/v1.1/fake/flavors/23", }, { "rel": "bookmark", - "href": "http://localhost/flavors/23", + "href": "http://localhost/fake/flavors/23", }, ], }, { @@ -368,11 +372,11 @@ class FlavorsXMLSerializationTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/13", + "href": "http://localhost/v1.1/fake/flavors/13", }, { "rel": "bookmark", - "href": "http://localhost/flavors/13", + "href": "http://localhost/fake/flavors/13", }, ], }, @@ -389,15 +393,19 @@ class FlavorsXMLSerializationTest(test.TestCase): name="flavor 23" ram="512" disk="20"> - - + + - - + + """.replace(" ", "") % locals()) @@ -417,11 +425,11 @@ class FlavorsXMLSerializationTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/23", + "href": "http://localhost/v1.1/fake/flavors/23", }, { "rel": "bookmark", - "href": "http://localhost/flavors/23", + "href": "http://localhost/fake/flavors/23", }, ], }, { @@ -432,11 +440,11 @@ class FlavorsXMLSerializationTest(test.TestCase): "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/13", + "href": "http://localhost/v1.1/fake/flavors/13", }, { "rel": "bookmark", - "href": "http://localhost/flavors/13", + "href": "http://localhost/fake/flavors/13", }, ], }, @@ -450,12 +458,16 @@ class FlavorsXMLSerializationTest(test.TestCase): - - + + - - + + """.replace(" ", "") % locals()) diff --git a/nova/tests/api/openstack/test_flavors_extra_specs.py b/nova/tests/api/openstack/test_flavors_extra_specs.py index ccd1b0d9f5e5..f382d06e946e 100644 --- a/nova/tests/api/openstack/test_flavors_extra_specs.py +++ b/nova/tests/api/openstack/test_flavors_extra_specs.py @@ -63,7 +63,7 @@ class FlavorsExtraSpecsTest(test.TestCase): def test_index(self): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get', return_flavor_extra_specs) - request = webob.Request.blank('/v1.1/flavors/1/os-extra_specs') + request = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs') res = request.get_response(fakes.wsgi_app()) self.assertEqual(200, res.status_int) res_dict = json.loads(res.body) @@ -73,7 +73,7 @@ class FlavorsExtraSpecsTest(test.TestCase): def test_index_no_data(self): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get', return_empty_flavor_extra_specs) - req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs') + req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -83,7 +83,7 @@ class FlavorsExtraSpecsTest(test.TestCase): def test_show(self): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get', return_flavor_extra_specs) - req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key5') + req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key5') res = req.get_response(fakes.wsgi_app()) self.assertEqual(200, res.status_int) res_dict = json.loads(res.body) @@ -93,7 +93,7 @@ class FlavorsExtraSpecsTest(test.TestCase): def test_show_spec_not_found(self): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get', return_empty_flavor_extra_specs) - req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key6') + req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key6') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(404, res.status_int) @@ -101,7 +101,7 @@ class FlavorsExtraSpecsTest(test.TestCase): def test_delete(self): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_delete', delete_flavor_extra_specs) - req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key5') + req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key5') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(200, res.status_int) @@ -110,7 +110,7 @@ class FlavorsExtraSpecsTest(test.TestCase): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_update_or_create', return_create_flavor_extra_specs) - req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs') + req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs') req.method = 'POST' req.body = '{"extra_specs": {"key1": "value1"}}' req.headers["content-type"] = "application/json" @@ -124,7 +124,7 @@ class FlavorsExtraSpecsTest(test.TestCase): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_update_or_create', return_create_flavor_extra_specs) - req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs') + req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs') req.method = 'POST' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) @@ -134,7 +134,7 @@ class FlavorsExtraSpecsTest(test.TestCase): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_update_or_create', return_create_flavor_extra_specs) - req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key1') + req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key1') req.method = 'PUT' req.body = '{"key1": "value1"}' req.headers["content-type"] = "application/json" @@ -148,7 +148,7 @@ class FlavorsExtraSpecsTest(test.TestCase): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_update_or_create', return_create_flavor_extra_specs) - req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key1') + req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key1') req.method = 'PUT' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) @@ -158,7 +158,7 @@ class FlavorsExtraSpecsTest(test.TestCase): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_update_or_create', return_create_flavor_extra_specs) - req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key1') + req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key1') req.method = 'PUT' req.body = '{"key1": "value1", "key2": "value2"}' req.headers["content-type"] = "application/json" @@ -169,7 +169,7 @@ class FlavorsExtraSpecsTest(test.TestCase): self.stubs.Set(nova.db.api, 'instance_type_extra_specs_update_or_create', return_create_flavor_extra_specs) - req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/bad') + req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/bad') req.method = 'PUT' req.body = '{"key1": "value1"}' req.headers["content-type"] = "application/json" diff --git a/nova/tests/api/openstack/test_image_metadata.py b/nova/tests/api/openstack/test_image_metadata.py index 21743eeef1f0..fe42e35e5dbf 100644 --- a/nova/tests/api/openstack/test_image_metadata.py +++ b/nova/tests/api/openstack/test_image_metadata.py @@ -90,7 +90,7 @@ class ImageMetaDataTest(test.TestCase): fakes.stub_out_glance(self.stubs, self.IMAGE_FIXTURES) def test_index(self): - req = webob.Request.blank('/v1.1/images/1/metadata') + req = webob.Request.blank('/v1.1/123/images/1/metadata') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -100,7 +100,7 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(value, res_dict['metadata'][key]) def test_show(self): - req = webob.Request.blank('/v1.1/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -109,12 +109,12 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual('value1', res_dict['meta']['key1']) def test_show_not_found(self): - req = webob.Request.blank('/v1.1/images/1/metadata/key9') + req = webob.Request.blank('/v1.1/fake/images/1/metadata/key9') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) def test_create(self): - req = webob.Request.blank('/v1.1/images/2/metadata') + req = webob.Request.blank('/v1.1/fake/images/2/metadata') req.method = 'POST' req.body = '{"metadata": {"key9": "value9"}}' req.headers["content-type"] = "application/json" @@ -134,7 +134,7 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(expected_output, actual_output) def test_update_all(self): - req = webob.Request.blank('/v1.1/images/2/metadata') + req = webob.Request.blank('/v1.1/fake/images/1/metadata') req.method = 'PUT' req.body = '{"metadata": {"key9": "value9"}}' req.headers["content-type"] = "application/json" @@ -152,7 +152,7 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(expected_output, actual_output) def test_update_item(self): - req = webob.Request.blank('/v1.1/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') req.method = 'PUT' req.body = '{"meta": {"key1": "zz"}}' req.headers["content-type"] = "application/json" @@ -168,7 +168,7 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(actual_output, expected_output) def test_update_item_bad_body(self): - req = webob.Request.blank('/v1.1/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') req.method = 'PUT' req.body = '{"key1": "zz"}' req.headers["content-type"] = "application/json" @@ -176,7 +176,7 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_update_item_too_many_keys(self): - req = webob.Request.blank('/v1.1/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') req.method = 'PUT' req.body = '{"meta": {"key1": "value1", "key2": "value2"}}' req.headers["content-type"] = "application/json" @@ -184,7 +184,7 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_update_item_body_uri_mismatch(self): - req = webob.Request.blank('/v1.1/images/1/metadata/bad') + req = webob.Request.blank('/v1.1/fake/images/1/metadata/bad') req.method = 'PUT' req.body = '{"meta": {"key1": "value1"}}' req.headers["content-type"] = "application/json" @@ -192,7 +192,7 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_update_item_xml(self): - req = webob.Request.blank('/v1.1/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') req.method = 'PUT' req.body = 'five' req.headers["content-type"] = "application/xml" @@ -208,14 +208,14 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(actual_output, expected_output) def test_delete(self): - req = webob.Request.blank('/v1.1/images/2/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/2/metadata/key1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(204, res.status_int) self.assertEqual('', res.body) def test_delete_not_found(self): - req = webob.Request.blank('/v1.1/images/2/metadata/blah') + req = webob.Request.blank('/v1.1/fake/images/2/metadata/blah') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -225,7 +225,7 @@ class ImageMetaDataTest(test.TestCase): for num in range(FLAGS.quota_metadata_items + 1): data['metadata']['key%i' % num] = "blah" json_string = str(data).replace("\'", "\"") - req = webob.Request.blank('/v1.1/images/2/metadata') + req = webob.Request.blank('/v1.1/fake/images/2/metadata') req.method = 'POST' req.body = json_string req.headers["content-type"] = "application/json" @@ -233,7 +233,7 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(413, res.status_int) def test_too_many_metadata_items_on_put(self): - req = webob.Request.blank('/v1.1/images/3/metadata/blah') + req = webob.Request.blank('/v1.1/fake/images/3/metadata/blah') req.method = 'PUT' req.body = '{"meta": {"blah": "blah"}}' req.headers["content-type"] = "application/json" diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 383ed2e031ea..2a7cfc3822ab 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -339,6 +339,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.stubs.UnsetAll() super(ImageControllerWithGlanceServiceTest, self).tearDown() + def _get_fake_context(self): + class Context(object): + project_id = 'fake' + return Context() + def _applicable_fixture(self, fixture, user_id): """Determine if this fixture is applicable for given user id.""" is_public = fixture["is_public"] @@ -386,13 +391,13 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(expected_image, actual_image) def test_get_image_v1_1(self): - request = webob.Request.blank('/v1.1/images/124') + request = webob.Request.blank('/v1.1/fake/images/124') response = request.get_response(fakes.wsgi_app()) actual_image = json.loads(response.body) - href = "http://localhost/v1.1/images/124" - bookmark = "http://localhost/images/124" + href = "http://localhost/v1.1/fake/images/124" + bookmark = "http://localhost/fake/images/124" server_href = "http://localhost/v1.1/servers/42" server_bookmark = "http://localhost/servers/42" @@ -508,7 +513,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(expected.toxml(), actual.toxml()) def test_get_image_404_v1_1_json(self): - request = webob.Request.blank('/v1.1/images/NonExistantImage') + request = webob.Request.blank('/v1.1/fake/images/NonExistantImage') response = request.get_response(fakes.wsgi_app()) self.assertEqual(404, response.status_int) @@ -524,7 +529,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(expected, actual) def test_get_image_404_v1_1_xml(self): - request = webob.Request.blank('/v1.1/images/NonExistantImage') + request = webob.Request.blank('/v1.1/fake/images/NonExistantImage') request.accept = "application/xml" response = request.get_response(fakes.wsgi_app()) self.assertEqual(404, response.status_int) @@ -545,7 +550,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(expected.toxml(), actual.toxml()) def test_get_image_index_v1_1(self): - request = webob.Request.blank('/v1.1/images') + request = webob.Request.blank('/v1.1/fake/images') response = request.get_response(fakes.wsgi_app()) response_dict = json.loads(response.body) @@ -558,8 +563,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): fixtures.remove(image) continue - href = "http://localhost/v1.1/images/%s" % image["id"] - bookmark = "http://localhost/images/%s" % image["id"] + href = "http://localhost/v1.1/fake/images/%s" % image["id"] + bookmark = "http://localhost/fake/images/%s" % image["id"] test_image = { "id": image["id"], "name": image["name"], @@ -637,7 +642,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertDictListMatch(expected, response_list) def test_get_image_details_v1_1(self): - request = webob.Request.blank('/v1.1/images/detail') + request = webob.Request.blank('/v1.1/fake/images/detail') response = request.get_response(fakes.wsgi_app()) response_dict = json.loads(response.body) @@ -655,11 +660,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'progress': 100, "links": [{ "rel": "self", - "href": "http://localhost/v1.1/images/123", + "href": "http://localhost/v1.1/fake/images/123", }, { "rel": "bookmark", - "href": "http://localhost/images/123", + "href": "http://localhost/fake/images/123", }], }, { @@ -686,11 +691,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, "links": [{ "rel": "self", - "href": "http://localhost/v1.1/images/124", + "href": "http://localhost/v1.1/fake/images/124", }, { "rel": "bookmark", - "href": "http://localhost/images/124", + "href": "http://localhost/fake/images/124", }], }, { @@ -717,11 +722,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, "links": [{ "rel": "self", - "href": "http://localhost/v1.1/images/125", + "href": "http://localhost/v1.1/fake/images/125", }, { "rel": "bookmark", - "href": "http://localhost/images/125", + "href": "http://localhost/fake/images/125", }], }, { @@ -748,11 +753,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, "links": [{ "rel": "self", - "href": "http://localhost/v1.1/images/126", + "href": "http://localhost/v1.1/fake/images/126", }, { "rel": "bookmark", - "href": "http://localhost/images/126", + "href": "http://localhost/fake/images/126", }], }, { @@ -779,11 +784,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, "links": [{ "rel": "self", - "href": "http://localhost/v1.1/images/127", + "href": "http://localhost/v1.1/fake/images/127", }, { "rel": "bookmark", - "href": "http://localhost/images/127", + "href": "http://localhost/fake/images/127", }], }, { @@ -796,11 +801,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): 'progress': 100, "links": [{ "rel": "self", - "href": "http://localhost/v1.1/images/129", + "href": "http://localhost/v1.1/fake/images/129", }, { "rel": "bookmark", - "href": "http://localhost/images/129", + "href": "http://localhost/fake/images/129", }], }, ] @@ -809,7 +814,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_filter_with_name(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'name': 'testname'} image_service.index(context, filters=filters).AndReturn([]) self.mox.ReplayAll() @@ -821,7 +826,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_filter_with_status(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'status': 'ACTIVE'} image_service.index(context, filters=filters).AndReturn([]) self.mox.ReplayAll() @@ -833,7 +838,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_filter_with_property(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'property-test': '3'} image_service.index(context, filters=filters).AndReturn([]) self.mox.ReplayAll() @@ -845,7 +850,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_filter_server(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() # 'server' should be converted to 'property-instance_ref' filters = {'property-instance_ref': 'http://localhost:8774/servers/12'} image_service.index(context, filters=filters).AndReturn([]) @@ -859,7 +864,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_filter_changes_since(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'changes-since': '2011-01-24T17:08Z'} image_service.index(context, filters=filters).AndReturn([]) self.mox.ReplayAll() @@ -872,7 +877,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_filter_with_type(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'property-image_type': 'BASE'} image_service.index(context, filters=filters).AndReturn([]) self.mox.ReplayAll() @@ -884,7 +889,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_filter_not_supported(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'status': 'ACTIVE'} image_service.detail(context, filters=filters).AndReturn([]) self.mox.ReplayAll() @@ -897,7 +902,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_no_filters(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {} image_service.index( context, filters=filters).AndReturn([]) @@ -911,11 +916,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_detail_filter_with_name(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'name': 'testname'} image_service.detail(context, filters=filters).AndReturn([]) self.mox.ReplayAll() - request = webob.Request.blank('/v1.1/images/detail?name=testname') + request = webob.Request.blank('/v1.1/fake/images/detail?name=testname') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.detail(request) @@ -923,11 +928,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_detail_filter_with_status(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'status': 'ACTIVE'} image_service.detail(context, filters=filters).AndReturn([]) self.mox.ReplayAll() - request = webob.Request.blank('/v1.1/images/detail?status=ACTIVE') + request = webob.Request.blank('/v1.1/fake/images/detail?status=ACTIVE') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.detail(request) @@ -935,11 +940,12 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_detail_filter_with_property(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'property-test': '3'} image_service.detail(context, filters=filters).AndReturn([]) self.mox.ReplayAll() - request = webob.Request.blank('/v1.1/images/detail?property-test=3') + request = webob.Request.blank( + '/v1.1/fake/images/detail?property-test=3') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.detail(request) @@ -947,12 +953,12 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_detail_filter_server(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() # 'server' should be converted to 'property-instance_ref' filters = {'property-instance_ref': 'http://localhost:8774/servers/12'} image_service.index(context, filters=filters).AndReturn([]) self.mox.ReplayAll() - request = webob.Request.blank('/v1.1/images/detail?server=' + request = webob.Request.blank('/v1.1/fake/images/detail?server=' 'http://localhost:8774/servers/12') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) @@ -961,11 +967,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_detail_filter_changes_since(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'changes-since': '2011-01-24T17:08Z'} image_service.index(context, filters=filters).AndReturn([]) self.mox.ReplayAll() - request = webob.Request.blank('/v1.1/images/detail?changes-since=' + request = webob.Request.blank('/v1.1/fake/images/detail?changes-since=' '2011-01-24T17:08Z') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) @@ -974,11 +980,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_detail_filter_with_type(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'property-image_type': 'BASE'} image_service.index(context, filters=filters).AndReturn([]) self.mox.ReplayAll() - request = webob.Request.blank('/v1.1/images/detail?type=BASE') + request = webob.Request.blank('/v1.1/fake/images/detail?type=BASE') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.index(request) @@ -986,11 +992,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_detail_filter_not_supported(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {'status': 'ACTIVE'} image_service.detail(context, filters=filters).AndReturn([]) self.mox.ReplayAll() - request = webob.Request.blank('/v1.1/images/detail?status=ACTIVE&' + request = webob.Request.blank('/v1.1/fake/images/detail?status=ACTIVE&' 'UNSUPPORTEDFILTER=testname') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) @@ -999,11 +1005,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_image_detail_no_filters(self): image_service = self.mox.CreateMockAnything() - context = object() + context = self._get_fake_context() filters = {} image_service.detail(context, filters=filters).AndReturn([]) self.mox.ReplayAll() - request = webob.Request.blank('/v1.1/images/detail') + request = webob.Request.blank('/v1.1/fake/images/detail') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.detail(request) @@ -1123,8 +1129,8 @@ class ImageXMLSerializationTest(test.TestCase): TIMESTAMP = "2010-10-11T10:30:22Z" SERVER_HREF = 'http://localhost/v1.1/servers/123' SERVER_BOOKMARK = 'http://localhost/servers/123' - IMAGE_HREF = 'http://localhost/v1.1/images/%s' - IMAGE_BOOKMARK = 'http://localhost/images/%s' + IMAGE_HREF = 'http://localhost/v1.1/fake/images/%s' + IMAGE_BOOKMARK = 'http://localhost/fake/images/%s' def test_show(self): serializer = images.ImageXMLSerializer() diff --git a/nova/tests/api/openstack/test_server_actions.py b/nova/tests/api/openstack/test_server_actions.py index 80a27e30f2a8..bdd6824e718b 100644 --- a/nova/tests/api/openstack/test_server_actions.py +++ b/nova/tests/api/openstack/test_server_actions.py @@ -1,17 +1,13 @@ import base64 import json -import unittest -from xml.dom import minidom import stubout import webob from nova import context -from nova import db from nova import utils from nova import flags from nova.api.openstack import create_instance_helper -from nova.compute import instance_types from nova.compute import power_state import nova.db.api from nova import test @@ -103,8 +99,6 @@ class ServerActionsTest(test.TestCase): super(ServerActionsTest, self).setUp() self.flags(verbose=True) self.stubs = stubout.StubOutForTesting() - fakes.FakeAuthManager.reset_fake_data() - fakes.FakeAuthDatabase.data = {} fakes.stub_out_auth(self.stubs) self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id) self.stubs.Set(nova.db.api, 'instance_update', instance_update) @@ -468,8 +462,6 @@ class ServerActionsTestV11(test.TestCase): self.maxDiff = None super(ServerActionsTestV11, self).setUp() self.stubs = stubout.StubOutForTesting() - fakes.FakeAuthManager.reset_fake_data() - fakes.FakeAuthDatabase.data = {} fakes.stub_out_auth(self.stubs) self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id) self.stubs.Set(nova.db.api, 'instance_update', instance_update) @@ -489,7 +481,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_bad_body(self): body = {} - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -498,7 +490,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_unknown_action(self): body = {'sockTheFox': {'fakekey': '1234'}} - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -509,7 +501,7 @@ class ServerActionsTestV11(test.TestCase): mock_method = MockSetAdminPassword() self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) body = {'changePassword': {'adminPass': '1234pass'}} - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -521,7 +513,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_change_password_xml(self): mock_method = MockSetAdminPassword() self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method) - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = "application/xml" req.body = """ @@ -535,7 +527,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_change_password_not_a_string(self): body = {'changePassword': {'adminPass': 1234}} - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -544,7 +536,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_change_password_bad_request(self): body = {'changePassword': {'pass': '12345'}} - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -553,7 +545,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_change_password_empty_string(self): body = {'changePassword': {'adminPass': ''}} - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -562,7 +554,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_change_password_none(self): body = {'changePassword': {'adminPass': None}} - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -571,7 +563,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_reboot_hard(self): body = dict(reboot=dict(type="HARD")) - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -580,7 +572,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_reboot_soft(self): body = dict(reboot=dict(type="SOFT")) - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -589,7 +581,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_reboot_incorrect_type(self): body = dict(reboot=dict(type="NOT_A_TYPE")) - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -598,7 +590,7 @@ class ServerActionsTestV11(test.TestCase): def test_server_reboot_missing_type(self): body = dict(reboot=dict()) - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -612,7 +604,7 @@ class ServerActionsTestV11(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -633,7 +625,7 @@ class ServerActionsTestV11(test.TestCase): self.stubs.Set(nova.db, 'instance_get_by_uuid', return_server_with_uuid_and_power_state(state)) - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -651,7 +643,7 @@ class ServerActionsTestV11(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -667,7 +659,7 @@ class ServerActionsTestV11(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -682,7 +674,7 @@ class ServerActionsTestV11(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -701,7 +693,7 @@ class ServerActionsTestV11(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -720,7 +712,7 @@ class ServerActionsTestV11(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) @@ -730,7 +722,7 @@ class ServerActionsTestV11(test.TestCase): def test_resize_server(self): - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.content_type = 'application/json' req.method = 'POST' body_dict = dict(resize=dict(flavorRef="http://localhost/3")) @@ -748,7 +740,7 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(self.resize_called, True) def test_resize_server_no_flavor(self): - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.content_type = 'application/json' req.method = 'POST' body_dict = dict(resize=dict()) @@ -758,7 +750,7 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(res.status_int, 400) def test_resize_server_no_flavor_ref(self): - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.content_type = 'application/json' req.method = 'POST' body_dict = dict(resize=dict(flavorRef=None)) @@ -768,7 +760,7 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(res.status_int, 400) def test_confirm_resize_server(self): - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.content_type = 'application/json' req.method = 'POST' body_dict = dict(confirmResize=None) @@ -786,7 +778,7 @@ class ServerActionsTestV11(test.TestCase): self.assertEqual(self.confirm_resize_called, True) def test_revert_resize_server(self): - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.content_type = 'application/json' req.method = 'POST' body_dict = dict(revertResize=None) @@ -809,7 +801,7 @@ class ServerActionsTestV11(test.TestCase): 'name': 'Snapshot 1', }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -828,7 +820,7 @@ class ServerActionsTestV11(test.TestCase): 'name': 'Snapshot 1', }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -842,7 +834,7 @@ class ServerActionsTestV11(test.TestCase): 'metadata': {'key': 'asdf'}, }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -860,7 +852,7 @@ class ServerActionsTestV11(test.TestCase): } for num in range(FLAGS.quota_metadata_items + 1): body['createImage']['metadata']['foo%i' % num] = "bar" - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -871,7 +863,7 @@ class ServerActionsTestV11(test.TestCase): body = { 'createImage': {}, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -885,7 +877,7 @@ class ServerActionsTestV11(test.TestCase): 'metadata': 'henry', }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -904,7 +896,7 @@ class ServerActionsTestV11(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers/1/action') + req = webob.Request.blank('/v1.1/fake/servers/1/action') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py index 8512bd518728..296bbd9dc8ef 100644 --- a/nova/tests/api/openstack/test_server_metadata.py +++ b/nova/tests/api/openstack/test_server_metadata.py @@ -83,7 +83,7 @@ class ServerMetaDataTest(test.TestCase): def test_index(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata') res = req.get_response(fakes.wsgi_app()) self.assertEqual(200, res.status_int) res_dict = json.loads(res.body) @@ -100,7 +100,7 @@ class ServerMetaDataTest(test.TestCase): def test_index_xml(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_server_metadata) - request = webob.Request.blank("/v1.1/servers/1/metadata") + request = webob.Request.blank("/v1.1/fake/servers/1/metadata") request.accept = "application/xml" response = request.get_response(fakes.wsgi_app()) self.assertEqual(200, response.status_int) @@ -120,14 +120,14 @@ class ServerMetaDataTest(test.TestCase): def test_index_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/1/metadata') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) def test_index_no_data(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_empty_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata') res = req.get_response(fakes.wsgi_app()) self.assertEqual(200, res.status_int) res_dict = json.loads(res.body) @@ -137,7 +137,7 @@ class ServerMetaDataTest(test.TestCase): def test_show(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/key2') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key2') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -147,7 +147,7 @@ class ServerMetaDataTest(test.TestCase): def test_show_xml(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_server_metadata) - request = webob.Request.blank("/v1.1/servers/1/metadata/key2") + request = webob.Request.blank("/v1.1/fake/servers/1/metadata/key2") request.accept = "application/xml" response = request.get_response(fakes.wsgi_app()) self.assertEqual(200, response.status_int) @@ -164,14 +164,14 @@ class ServerMetaDataTest(test.TestCase): def test_show_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/1/metadata/key2') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key2') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) def test_show_meta_not_found(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_empty_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/key6') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key6') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -180,7 +180,7 @@ class ServerMetaDataTest(test.TestCase): return_server_metadata) self.stubs.Set(nova.db.api, 'instance_metadata_delete', delete_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/key2') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key2') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(204, res.status_int) @@ -188,7 +188,7 @@ class ServerMetaDataTest(test.TestCase): def test_delete_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -196,7 +196,7 @@ class ServerMetaDataTest(test.TestCase): def test_delete_meta_not_found(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', return_empty_server_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/key6') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key6') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -206,7 +206,7 @@ class ServerMetaDataTest(test.TestCase): return_server_metadata) self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata') req.method = 'POST' req.content_type = "application/json" input = {"metadata": {"key9": "value9"}} @@ -227,7 +227,7 @@ class ServerMetaDataTest(test.TestCase): return_server_metadata) self.stubs.Set(nova.db.api, "instance_metadata_update", return_create_instance_metadata) - req = webob.Request.blank("/v1.1/servers/1/metadata") + req = webob.Request.blank("/v1.1/fake/servers/1/metadata") req.method = "POST" req.content_type = "application/xml" req.accept = "application/xml" @@ -258,7 +258,7 @@ class ServerMetaDataTest(test.TestCase): def test_create_empty_body(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata') req.method = 'POST' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) @@ -266,7 +266,7 @@ class ServerMetaDataTest(test.TestCase): def test_create_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/100/metadata') + req = webob.Request.blank('/v1.1/fake/servers/100/metadata') req.method = 'POST' req.body = '{"metadata": {"key1": "value1"}}' req.headers["content-type"] = "application/json" @@ -276,7 +276,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_all(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata') req.method = 'PUT' req.content_type = "application/json" expected = { @@ -294,7 +294,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_all_empty_container(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata') req.method = 'PUT' req.content_type = "application/json" expected = {'metadata': {}} @@ -307,7 +307,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_all_malformed_container(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata') req.method = 'PUT' req.content_type = "application/json" expected = {'meta': {}} @@ -318,7 +318,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_all_malformed_data(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata') req.method = 'PUT' req.content_type = "application/json" expected = {'metadata': ['asdf']} @@ -328,7 +328,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_all_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/100/metadata') + req = webob.Request.blank('/v1.1/fake/servers/100/metadata') req.method = 'PUT' req.content_type = "application/json" req.body = json.dumps({'metadata': {'key10': 'value10'}}) @@ -338,7 +338,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_item(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key1') req.method = 'PUT' req.body = '{"meta": {"key1": "value1"}}' req.headers["content-type"] = "application/json" @@ -352,7 +352,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_item_xml(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/key9') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key9') req.method = 'PUT' req.accept = "application/json" req.content_type = "application/xml" @@ -369,7 +369,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_item_nonexistant_server(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant) - req = webob.Request.blank('/v1.1/servers/asdf/metadata/key1') + req = webob.Request.blank('/v1.1/fake/servers/asdf/metadata/key1') req.method = 'PUT' req.body = '{"meta":{"key1": "value1"}}' req.headers["content-type"] = "application/json" @@ -379,7 +379,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_item_empty_body(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key1') req.method = 'PUT' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) @@ -388,7 +388,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_item_too_many_keys(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key1') req.method = 'PUT' req.body = '{"meta": {"key1": "value1", "key2": "value2"}}' req.headers["content-type"] = "application/json" @@ -398,7 +398,7 @@ class ServerMetaDataTest(test.TestCase): def test_update_item_body_uri_mismatch(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata) - req = webob.Request.blank('/v1.1/servers/1/metadata/bad') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/bad') req.method = 'PUT' req.body = '{"meta": {"key1": "value1"}}' req.headers["content-type"] = "application/json" @@ -412,7 +412,7 @@ class ServerMetaDataTest(test.TestCase): for num in range(FLAGS.quota_metadata_items + 1): data['metadata']['key%i' % num] = "blah" json_string = str(data).replace("\'", "\"") - req = webob.Request.blank('/v1.1/servers/1/metadata') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata') req.method = 'POST' req.body = json_string req.headers["content-type"] = "application/json" @@ -422,7 +422,7 @@ class ServerMetaDataTest(test.TestCase): def test_too_many_metadata_items_on_update_item(self): self.stubs.Set(nova.db.api, 'instance_metadata_update', return_create_instance_metadata_max) - req = webob.Request.blank('/v1.1/servers/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key1') req.method = 'PUT' req.body = '{"meta": {"a new key": "a new value"}}' req.headers["content-type"] = "application/json" diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index a510d7d97176..e5c1f2c34d8b 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010-2011 OpenStack LLC. +# Copyright 2011 Piston Cloud Computing, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -19,6 +20,7 @@ import base64 import datetime import json import unittest +from lxml import etree from xml.dom import minidom import webob @@ -32,6 +34,7 @@ import nova.api.openstack from nova.api.openstack import create_instance_helper from nova.api.openstack import servers from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil import nova.compute.api from nova.compute import instance_types from nova.compute import power_state @@ -46,6 +49,8 @@ from nova.tests.api.openstack import fakes FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" def fake_gen_uuid(): @@ -145,7 +150,8 @@ def instance_addresses(context, instance_id): def stub_instance(id, user_id='fake', project_id='fake', private_address=None, public_addresses=None, host=None, power_state=0, reservation_id="", uuid=FAKE_UUID, image_ref="10", - flavor_id="1", interfaces=None, name=None): + flavor_id="1", interfaces=None, name=None, + access_ipv4=None, access_ipv6=None): metadata = [] metadata.append(InstanceMetadata(key='seq', value=id)) @@ -197,6 +203,8 @@ def stub_instance(id, user_id='fake', project_id='fake', private_address=None, "display_description": "", "locked": False, "metadata": metadata, + "access_ip_v4": access_ipv4, + "access_ip_v6": access_ipv6, "uuid": uuid, "virtual_interfaces": interfaces} @@ -226,7 +234,6 @@ class MockSetAdminPassword(object): class ServersTest(test.TestCase): - def setUp(self): self.maxDiff = None super(ServersTest, self).setUp() @@ -258,6 +265,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, "get_actions", fake_compute_api) self.webreq = common.webob_factory('/v1.0/servers') + self.config_drive = None def test_get_server_by_id(self): req = webob.Request.blank('/v1.0/servers/1') @@ -297,10 +305,10 @@ class ServersTest(test.TestCase): self.assertEqual(res_dict['server']['name'], 'server1') def test_get_server_by_id_v1_1(self): - image_bookmark = "http://localhost/images/10" - flavor_ref = "http://localhost/v1.1/flavors/1" + image_bookmark = "http://localhost/fake/images/10" + flavor_ref = "http://localhost/v1.1/fake/flavors/1" flavor_id = "1" - flavor_bookmark = "http://localhost/flavors/1" + flavor_bookmark = "http://localhost/fake/flavors/1" public_ip = '192.168.0.3' private_ip = '172.19.0.1' @@ -322,7 +330,7 @@ class ServersTest(test.TestCase): interfaces=interfaces) self.stubs.Set(nova.db.api, 'instance_get', new_return_server) - req = webob.Request.blank('/v1.1/servers/1') + req = webob.Request.blank('/v1.1/fake/servers/1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) expected_server = { @@ -334,6 +342,8 @@ class ServersTest(test.TestCase): "progress": 0, "name": "server1", "status": "BUILD", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "10", @@ -370,15 +380,16 @@ class ServersTest(test.TestCase): "metadata": { "seq": "1", }, + "config_drive": None, "links": [ { "rel": "self", #FIXME(wwolf) Do we want the links to be id or uuid? - "href": "http://localhost/v1.1/servers/1", + "href": "http://localhost/v1.1/fake/servers/1", }, { "rel": "bookmark", - "href": "http://localhost/servers/1", + "href": "http://localhost/fake/servers/1", }, ], } @@ -387,12 +398,12 @@ class ServersTest(test.TestCase): self.assertDictMatch(res_dict, expected_server) def test_get_server_by_id_v1_1_xml(self): - image_bookmark = "http://localhost/images/10" - flavor_ref = "http://localhost/v1.1/flavors/1" + image_bookmark = "http://localhost/fake/images/10" + flavor_ref = "http://localhost/v1.1/fake/flavors/1" flavor_id = "1" - flavor_bookmark = "http://localhost/flavors/1" - server_href = "http://localhost/v1.1/servers/1" - server_bookmark = "http://localhost/servers/1" + flavor_bookmark = "http://localhost/fake/flavors/1" + server_href = "http://localhost/v1.1/fake/servers/1" + server_bookmark = "http://localhost/fake/servers/1" public_ip = '192.168.0.3' private_ip = '172.19.0.1' @@ -414,7 +425,7 @@ class ServersTest(test.TestCase): interfaces=interfaces) self.stubs.Set(nova.db.api, 'instance_get', new_return_server) - req = webob.Request.blank('/v1.1/servers/1') + req = webob.Request.blank('/v1.1/fake/servers/1') req.headers['Accept'] = 'application/xml' res = req.get_response(fakes.wsgi_app()) actual = minidom.parseString(res.body.replace(' ', '')) @@ -431,6 +442,8 @@ class ServersTest(test.TestCase): created="%(expected_created)s" hostId="" status="BUILD" + accessIPv4="" + accessIPv6="" progress="0"> @@ -459,10 +472,10 @@ class ServersTest(test.TestCase): self.assertEqual(expected.toxml(), actual.toxml()) def test_get_server_with_active_status_by_id_v1_1(self): - image_bookmark = "http://localhost/images/10" - flavor_ref = "http://localhost/v1.1/flavors/1" + image_bookmark = "http://localhost/fake/images/10" + flavor_ref = "http://localhost/v1.1/fake/flavors/1" flavor_id = "1" - flavor_bookmark = "http://localhost/flavors/1" + flavor_bookmark = "http://localhost/fake/flavors/1" private_ip = "192.168.0.3" public_ip = "1.2.3.4" @@ -484,7 +497,7 @@ class ServersTest(test.TestCase): interfaces=interfaces, power_state=1) self.stubs.Set(nova.db.api, 'instance_get', new_return_server) - req = webob.Request.blank('/v1.1/servers/1') + req = webob.Request.blank('/v1.1/fake/servers/1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) expected_server = { @@ -496,6 +509,8 @@ class ServersTest(test.TestCase): "progress": 100, "name": "server1", "status": "ACTIVE", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "10", @@ -532,14 +547,15 @@ class ServersTest(test.TestCase): "metadata": { "seq": "1", }, + "config_drive": None, "links": [ { "rel": "self", - "href": "http://localhost/v1.1/servers/1", + "href": "http://localhost/v1.1/fake/servers/1", }, { "rel": "bookmark", - "href": "http://localhost/servers/1", + "href": "http://localhost/fake/servers/1", }, ], } @@ -549,10 +565,10 @@ class ServersTest(test.TestCase): def test_get_server_with_id_image_ref_by_id_v1_1(self): image_ref = "10" - image_bookmark = "http://localhost/images/10" - flavor_ref = "http://localhost/v1.1/flavors/1" + image_bookmark = "http://localhost/fake/images/10" + flavor_ref = "http://localhost/v1.1/fake/flavors/1" flavor_id = "1" - flavor_bookmark = "http://localhost/flavors/1" + flavor_bookmark = "http://localhost/fake/flavors/1" private_ip = "192.168.0.3" public_ip = "1.2.3.4" @@ -575,7 +591,7 @@ class ServersTest(test.TestCase): flavor_id=flavor_id) self.stubs.Set(nova.db.api, 'instance_get', new_return_server) - req = webob.Request.blank('/v1.1/servers/1') + req = webob.Request.blank('/v1.1/fake/servers/1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) expected_server = { @@ -587,6 +603,8 @@ class ServersTest(test.TestCase): "progress": 100, "name": "server1", "status": "ACTIVE", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "10", @@ -623,14 +641,15 @@ class ServersTest(test.TestCase): "metadata": { "seq": "1", }, + "config_drive": None, "links": [ { "rel": "self", - "href": "http://localhost/v1.1/servers/1", + "href": "http://localhost/v1.1/fake/servers/1", }, { "rel": "bookmark", - "href": "http://localhost/servers/1", + "href": "http://localhost/fake/servers/1", }, ], } @@ -752,6 +771,27 @@ class ServersTest(test.TestCase): (ip,) = private_node.getElementsByTagName('ip') self.assertEquals(ip.getAttribute('addr'), private) + # NOTE(bcwaldon): lp830817 + def test_get_server_by_id_malformed_networks_v1_1(self): + ifaces = [ + { + 'network': None, + 'fixed_ips': [ + {'address': '192.168.0.3'}, + {'address': '192.168.0.4'}, + ], + }, + ] + new_return_server = return_server_with_attributes(interfaces=ifaces) + self.stubs.Set(nova.db.api, 'instance_get', new_return_server) + + req = webob.Request.blank('/v1.1/fake/servers/1') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['name'], 'server1') + def test_get_server_by_id_with_addresses_v1_1(self): self.flags(use_ipv6=True) interfaces = [ @@ -775,7 +815,7 @@ class ServersTest(test.TestCase): interfaces=interfaces) self.stubs.Set(nova.db.api, 'instance_get', new_return_server) - req = webob.Request.blank('/v1.1/servers/1') + req = webob.Request.blank('/v1.1/fake/servers/1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) @@ -819,7 +859,7 @@ class ServersTest(test.TestCase): interfaces=interfaces) self.stubs.Set(nova.db.api, 'instance_get', new_return_server) - req = webob.Request.blank('/v1.1/servers/1') + req = webob.Request.blank('/v1.1/fake/servers/1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) @@ -869,7 +909,7 @@ class ServersTest(test.TestCase): 'virtual_interface_get_by_instance', _return_vifs) - req = webob.Request.blank('/v1.1/servers/1/ips') + req = webob.Request.blank('/v1.1/fake/servers/1/ips') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) @@ -919,7 +959,7 @@ class ServersTest(test.TestCase): 'virtual_interface_get_by_instance', _return_vifs) - req = webob.Request.blank('/v1.1/servers/1/ips/network_2') + req = webob.Request.blank('/v1.1/fake/servers/1/ips/network_2') res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) res_dict = json.loads(res.body) @@ -939,7 +979,7 @@ class ServersTest(test.TestCase): 'virtual_interface_get_by_instance', _return_vifs) - req = webob.Request.blank('/v1.1/servers/1/ips/network_0') + req = webob.Request.blank('/v1.1/fake/servers/1/ips/network_0') res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 404) @@ -949,7 +989,7 @@ class ServersTest(test.TestCase): 'virtual_interface_get_by_instance', _return_vifs) - req = webob.Request.blank('/v1.1/servers/600/ips') + req = webob.Request.blank('/v1.1/fake/servers/600/ips') res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 404) @@ -1018,7 +1058,7 @@ class ServersTest(test.TestCase): i += 1 def test_get_server_list_v1_1(self): - req = webob.Request.blank('/v1.1/servers') + req = webob.Request.blank('/v1.1/fake/servers') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) @@ -1031,11 +1071,11 @@ class ServersTest(test.TestCase): expected_links = [ { "rel": "self", - "href": "http://localhost/v1.1/servers/%s" % s['id'], + "href": "http://localhost/v1.1/fake/servers/%s" % s['id'], }, { "rel": "bookmark", - "href": "http://localhost/servers/%s" % s['id'], + "href": "http://localhost/fake/servers/%s" % s['id'], }, ] @@ -1082,19 +1122,19 @@ class ServersTest(test.TestCase): self.assertTrue(res.body.find('offset param') > -1) def test_get_servers_with_marker(self): - req = webob.Request.blank('/v1.1/servers?marker=2') + req = webob.Request.blank('/v1.1/fake/servers?marker=2') res = req.get_response(fakes.wsgi_app()) servers = json.loads(res.body)['servers'] self.assertEqual([s['name'] for s in servers], ["server3", "server4"]) def test_get_servers_with_limit_and_marker(self): - req = webob.Request.blank('/v1.1/servers?limit=2&marker=1') + req = webob.Request.blank('/v1.1/fake/servers?limit=2&marker=1') res = req.get_response(fakes.wsgi_app()) servers = json.loads(res.body)['servers'] self.assertEqual([s['name'] for s in servers], ['server2', 'server3']) def test_get_servers_with_bad_marker(self): - req = webob.Request.blank('/v1.1/servers?limit=2&marker=asdf') + req = webob.Request.blank('/v1.1/fake/servers?limit=2&marker=asdf') res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) self.assertTrue(res.body.find('marker param') > -1) @@ -1120,7 +1160,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - req = webob.Request.blank('/v1.1/servers?unknownoption=whee') + req = webob.Request.blank('/v1.1/fake/servers?unknownoption=whee') res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) servers = json.loads(res.body)['servers'] @@ -1137,7 +1177,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) self.flags(allow_admin_api=False) - req = webob.Request.blank('/v1.1/servers?image=12345') + req = webob.Request.blank('/v1.1/fake/servers?image=12345') res = req.get_response(fakes.wsgi_app()) # The following assert will fail if either of the asserts in # fake_get_all() fail @@ -1157,7 +1197,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) self.flags(allow_admin_api=False) - req = webob.Request.blank('/v1.1/servers?flavor=12345') + req = webob.Request.blank('/v1.1/fake/servers?flavor=12345') res = req.get_response(fakes.wsgi_app()) # The following assert will fail if either of the asserts in # fake_get_all() fail @@ -1177,7 +1217,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) self.flags(allow_admin_api=False) - req = webob.Request.blank('/v1.1/servers?status=active') + req = webob.Request.blank('/v1.1/fake/servers?status=active') res = req.get_response(fakes.wsgi_app()) # The following assert will fail if either of the asserts in # fake_get_all() fail @@ -1191,7 +1231,7 @@ class ServersTest(test.TestCase): self.flags(allow_admin_api=False) - req = webob.Request.blank('/v1.1/servers?status=running') + req = webob.Request.blank('/v1.1/fake/servers?status=running') res = req.get_response(fakes.wsgi_app()) # The following assert will fail if either of the asserts in # fake_get_all() fail @@ -1208,7 +1248,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) self.flags(allow_admin_api=False) - req = webob.Request.blank('/v1.1/servers?name=whee.*') + req = webob.Request.blank('/v1.1/fake/servers?name=whee.*') res = req.get_response(fakes.wsgi_app()) # The following assert will fail if either of the asserts in # fake_get_all() fail @@ -1239,7 +1279,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" - req = webob.Request.blank('/v1.1/servers?%s' % query_str) + req = webob.Request.blank('/v1.1/fake/servers?%s' % query_str) # Request admin context context = nova.context.RequestContext('testuser', 'testproject', is_admin=True) @@ -1273,7 +1313,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" - req = webob.Request.blank('/v1.1/servers?%s' % query_str) + req = webob.Request.blank('/v1.1/fake/servers?%s' % query_str) # Request admin context context = nova.context.RequestContext('testuser', 'testproject', is_admin=False) @@ -1306,7 +1346,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" - req = webob.Request.blank('/v1.1/servers?%s' % query_str) + req = webob.Request.blank('/v1.1/fake/servers?%s' % query_str) # Request admin context context = nova.context.RequestContext('testuser', 'testproject', is_admin=True) @@ -1332,7 +1372,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - req = webob.Request.blank('/v1.1/servers?ip=10\..*') + req = webob.Request.blank('/v1.1/fake/servers?ip=10\..*') # Request admin context context = nova.context.RequestContext('testuser', 'testproject', is_admin=True) @@ -1358,7 +1398,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.API, 'get_all', fake_get_all) - req = webob.Request.blank('/v1.1/servers?ip6=ffff.*') + req = webob.Request.blank('/v1.1/fake/servers?ip6=ffff.*') # Request admin context context = nova.context.RequestContext('testuser', 'testproject', is_admin=True) @@ -1379,9 +1419,12 @@ class ServersTest(test.TestCase): 'display_name': 'server_test', 'uuid': FAKE_UUID, 'instance_type': dict(inst_type), + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': 'fead::1234', 'image_ref': image_ref, "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "config_drive": self.config_drive, } def server_update(context, id, params): @@ -1407,8 +1450,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.db.api, 'instance_create', instance_create) self.stubs.Set(nova.rpc, 'cast', fake_method) self.stubs.Set(nova.rpc, 'call', fake_method) - self.stubs.Set(nova.db.api, 'instance_update', - server_update) + self.stubs.Set(nova.db.api, 'instance_update', server_update) self.stubs.Set(nova.db.api, 'queue_get_for', queue_get_for) self.stubs.Set(nova.network.manager.VlanManager, 'allocate_fixed_ip', fake_method) @@ -1579,18 +1621,20 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) - def test_create_instance_v1_1(self): + def test_create_instance_with_access_ip_v1_1(self): self._setup_for_create_instance() # proper local hrefs must start with 'http://localhost/v1.1/' - image_href = 'http://localhost/v1.1/images/2' - flavor_ref = 'http://localhost/flavors/3' + image_href = 'http://localhost/v1.1/123/images/2' + flavor_ref = 'http://localhost/123/flavors/3' + access_ipv4 = '1.2.3.4' + access_ipv6 = 'fead::1234' expected_flavor = { "id": "3", "links": [ { "rel": "bookmark", - "href": 'http://localhost/flavors/3', + "href": 'http://localhost/123/flavors/3', }, ], } @@ -1599,7 +1643,69 @@ class ServersTest(test.TestCase): "links": [ { "rel": "bookmark", - "href": 'http://localhost/images/2', + "href": 'http://localhost/123/images/2', + }, + ], + } + body = { + 'server': { + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'accessIPv4': access_ipv4, + 'accessIPv6': access_ipv6, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': [ + { + "path": "/etc/banner.txt", + "contents": "MQ==", + }, + ], + }, + } + + req = webob.Request.blank('/v1.1/123/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 202) + server = json.loads(res.body)['server'] + self.assertEqual(16, len(server['adminPass'])) + self.assertEqual(1, server['id']) + self.assertEqual(0, server['progress']) + self.assertEqual('server_test', server['name']) + self.assertEqual(expected_flavor, server['flavor']) + self.assertEqual(expected_image, server['image']) + self.assertEqual(access_ipv4, server['accessIPv4']) + self.assertEqual(access_ipv6, server['accessIPv6']) + + def test_create_instance_v1_1(self): + self._setup_for_create_instance() + + # proper local hrefs must start with 'http://localhost/v1.1/' + image_href = 'http://localhost/v1.1/images/2' + flavor_ref = 'http://localhost/123/flavors/3' + expected_flavor = { + "id": "3", + "links": [ + { + "rel": "bookmark", + "href": 'http://localhost/fake/flavors/3', + }, + ], + } + expected_image = { + "id": "2", + "links": [ + { + "rel": "bookmark", + "href": 'http://localhost/fake/images/2', }, ], } @@ -1621,7 +1727,7 @@ class ServersTest(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers') + req = webob.Request.blank('/v1.1/fake/servers') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -1636,6 +1742,8 @@ class ServersTest(test.TestCase): self.assertEqual('server_test', server['name']) self.assertEqual(expected_flavor, server['flavor']) self.assertEqual(expected_image, server['image']) + self.assertEqual('1.2.3.4', server['accessIPv4']) + self.assertEqual('fead::1234', server['accessIPv6']) def test_create_instance_v1_1_invalid_flavor_href(self): self._setup_for_create_instance() @@ -1646,7 +1754,7 @@ class ServersTest(test.TestCase): name='server_test', imageRef=image_href, flavorRef=flavor_ref, metadata={'hello': 'world', 'open': 'stack'}, personality={})) - req = webob.Request.blank('/v1.1/servers') + req = webob.Request.blank('/v1.1/fake/servers') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -1656,13 +1764,13 @@ class ServersTest(test.TestCase): def test_create_instance_v1_1_invalid_flavor_id_int(self): self._setup_for_create_instance() - image_href = 'http://localhost/v1.1/images/2' + image_href = 'http://localhost/v1.1/123/images/2' flavor_ref = -1 body = dict(server=dict( name='server_test', imageRef=image_href, flavorRef=flavor_ref, metadata={'hello': 'world', 'open': 'stack'}, personality={})) - req = webob.Request.blank('/v1.1/servers') + req = webob.Request.blank('/v1.1/123/servers') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -1678,13 +1786,136 @@ class ServersTest(test.TestCase): name='server_test', imageRef=image_href, flavorRef=flavor_ref, metadata={'hello': 'world', 'open': 'stack'}, personality={})) - req = webob.Request.blank('/v1.1/servers') + req = webob.Request.blank('/v1.1/fake/servers') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_create_instance_with_config_drive_v1_1(self): + self.config_drive = True + self._setup_for_create_instance() + + image_href = 'http://localhost/v1.1/123/images/2' + flavor_ref = 'http://localhost/v1.1/123/flavors/3' + body = { + 'server': { + 'name': 'config_drive_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + 'config_drive': True, + }, + } + + req = webob.Request.blank('/v1.1/123/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + print res + self.assertEqual(res.status_int, 202) + server = json.loads(res.body)['server'] + self.assertEqual(1, server['id']) + self.assertTrue(server['config_drive']) + + def test_create_instance_with_config_drive_as_id_v1_1(self): + self.config_drive = 2 + self._setup_for_create_instance() + + image_href = 'http://localhost/v1.1/123/images/2' + flavor_ref = 'http://localhost/v1.1/123/flavors/3' + body = { + 'server': { + 'name': 'config_drive_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + 'config_drive': 2, + }, + } + + req = webob.Request.blank('/v1.1/123/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 202) + server = json.loads(res.body)['server'] + self.assertEqual(1, server['id']) + self.assertTrue(server['config_drive']) + self.assertEqual(2, server['config_drive']) + + def test_create_instance_with_bad_config_drive_v1_1(self): + self.config_drive = "asdf" + self._setup_for_create_instance() + + image_href = 'http://localhost/v1.1/123/images/2' + flavor_ref = 'http://localhost/v1.1/123/flavors/3' + body = { + 'server': { + 'name': 'config_drive_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + 'config_drive': 'asdf', + }, + } + + req = webob.Request.blank('/v1.1/123/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_create_instance_without_config_drive_v1_1(self): + self._setup_for_create_instance() + + image_href = 'http://localhost/v1.1/123/images/2' + flavor_ref = 'http://localhost/v1.1/123/flavors/3' + body = { + 'server': { + 'name': 'config_drive_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + 'config_drive': True, + }, + } + + req = webob.Request.blank('/v1.1/123/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + server = json.loads(res.body)['server'] + self.assertEqual(1, server['id']) + self.assertFalse(server['config_drive']) + def test_create_instance_v1_1_bad_href(self): self._setup_for_create_instance() @@ -1694,7 +1925,7 @@ class ServersTest(test.TestCase): name='server_test', imageRef=image_href, flavorRef=flavor_ref, metadata={'hello': 'world', 'open': 'stack'}, personality={})) - req = webob.Request.blank('/v1.1/servers') + req = webob.Request.blank('/v1.1/fake/servers') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -1711,7 +1942,7 @@ class ServersTest(test.TestCase): "links": [ { "rel": "bookmark", - "href": 'http://localhost/flavors/3', + "href": 'http://localhost/fake/flavors/3', }, ], } @@ -1720,7 +1951,7 @@ class ServersTest(test.TestCase): "links": [ { "rel": "bookmark", - "href": 'http://localhost/images/2', + "href": 'http://localhost/fake/images/2', }, ], } @@ -1732,7 +1963,7 @@ class ServersTest(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers') + req = webob.Request.blank('/v1.1/fake/servers') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -1779,7 +2010,7 @@ class ServersTest(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers') + req = webob.Request.blank('/v1.1/fake/servers') req.method = 'POST' req.body = json.dumps(body) req.headers['content-type'] = "application/json" @@ -1800,13 +2031,36 @@ class ServersTest(test.TestCase): }, } - req = webob.Request.blank('/v1.1/servers') + req = webob.Request.blank('/v1.1/fake/servers') req.method = 'POST' req.body = json.dumps(body) req.headers['content-type'] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_create_instance_whitespace_name(self): + self._setup_for_create_instance() + + body = { + 'server': { + 'name': ' ', + 'imageId': 3, + 'flavorId': 1, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': {}, + }, + } + + req = webob.Request.blank('/v1.0/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + def test_update_server_no_body(self): req = webob.Request.blank('/v1.0/servers/1') req.method = 'PUT' @@ -1874,15 +2128,37 @@ class ServersTest(test.TestCase): self.assertEqual(mock_method.password, 'bacon') def test_update_server_no_body_v1_1(self): - req = webob.Request.blank('/v1.0/servers/1') + req = webob.Request.blank('/v1.1/fake/servers/1') req.method = 'PUT' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_update_server_all_attributes_v1_1(self): + self.stubs.Set(nova.db.api, 'instance_get', + return_server_with_attributes(name='server_test', + access_ipv4='0.0.0.0', + access_ipv6='beef::0123')) + req = webob.Request.blank('/v1.1/123/servers/1') + req.method = 'PUT' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'accessIPv4': '0.0.0.0', + 'accessIPv6': 'beef::0123', + }} + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['name'], 'server_test') + self.assertEqual(res_dict['server']['accessIPv4'], '0.0.0.0') + self.assertEqual(res_dict['server']['accessIPv6'], 'beef::0123') + def test_update_server_name_v1_1(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_with_attributes(name='server_test')) - req = webob.Request.blank('/v1.1/servers/1') + req = webob.Request.blank('/v1.1/fake/servers/1') req.method = 'PUT' req.content_type = 'application/json' req.body = json.dumps({'server': {'name': 'server_test'}}) @@ -1892,6 +2168,32 @@ class ServersTest(test.TestCase): self.assertEqual(res_dict['server']['id'], 1) self.assertEqual(res_dict['server']['name'], 'server_test') + def test_update_server_access_ipv4_v1_1(self): + self.stubs.Set(nova.db.api, 'instance_get', + return_server_with_attributes(access_ipv4='0.0.0.0')) + req = webob.Request.blank('/v1.1/123/servers/1') + req.method = 'PUT' + req.content_type = 'application/json' + req.body = json.dumps({'server': {'accessIPv4': '0.0.0.0'}}) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['accessIPv4'], '0.0.0.0') + + def test_update_server_access_ipv6_v1_1(self): + self.stubs.Set(nova.db.api, 'instance_get', + return_server_with_attributes(access_ipv6='beef::0123')) + req = webob.Request.blank('/v1.1/123/servers/1') + req.method = 'PUT' + req.content_type = 'application/json' + req.body = json.dumps({'server': {'accessIPv6': 'beef::0123'}}) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['accessIPv6'], 'beef::0123') + def test_update_server_adminPass_ignored_v1_1(self): inst_dict = dict(name='server_test', adminPass='bacon') self.body = json.dumps(dict(server=inst_dict)) @@ -1905,7 +2207,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.db.api, 'instance_get', return_server_with_attributes(name='server_test')) - req = webob.Request.blank('/v1.1/servers/1') + req = webob.Request.blank('/v1.1/fake/servers/1') req.method = 'PUT' req.content_type = "application/json" req.body = self.body @@ -1938,7 +2240,7 @@ class ServersTest(test.TestCase): self.assertEqual(res.status_int, 501) def test_server_backup_schedule_deprecated_v1_1(self): - req = webob.Request.blank('/v1.1/servers/1/backup_schedule') + req = webob.Request.blank('/v1.1/fake/servers/1/backup_schedule') res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 404) @@ -1978,7 +2280,7 @@ class ServersTest(test.TestCase): "links": [ { "rel": "bookmark", - "href": 'http://localhost/flavors/1', + "href": 'http://localhost/fake/flavors/1', }, ], } @@ -1987,11 +2289,11 @@ class ServersTest(test.TestCase): "links": [ { "rel": "bookmark", - "href": 'http://localhost/images/10', + "href": 'http://localhost/fake/images/10', }, ], } - req = webob.Request.blank('/v1.1/servers/detail') + req = webob.Request.blank('/v1.1/fake/servers/detail') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) @@ -2150,7 +2452,7 @@ class ServersTest(test.TestCase): self.assertEqual(res.status_int, 422) def test_delete_server_instance_v1_1(self): - req = webob.Request.blank('/v1.1/servers/1') + req = webob.Request.blank('/v1.1/fake/servers/1') req.method = 'DELETE' self.server_delete_called = False @@ -2491,6 +2793,62 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): } self.assertEquals(request['body'], expected) + def test_access_ipv4(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv4": "1.2.3.4", + }, + } + self.assertEquals(request['body'], expected) + + def test_access_ipv6(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv6": "fead::1234", + }, + } + self.assertEquals(request['body'], expected) + + def test_access_ip(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", + }, + } + self.assertEquals(request['body'], expected) + def test_admin_pass(self): serial_request = """ + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [] + }} + self.assertEquals(request['body'], expected) + + def test_request_with_one_network(self): + serial_request = """ + + + + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_two_networks(self): + serial_request = """ + + + + + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}, + {"uuid": "2", "fixed_ip": "10.0.2.12"}], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_second_network_node_ignored(self): + serial_request = """ + + + + + + + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_one_network_missing_id(self): + serial_request = """ + + + + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"fixed_ip": "10.0.1.12"}], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_one_network_missing_fixed_ip(self): + serial_request = """ + + + + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1"}], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_one_network_empty_id(self): + serial_request = """ + + + + + """ + request = self.deserializer.deserialize(serial_request, 'create') + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "", "fixed_ip": "10.0.1.12"}], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_one_network_empty_fixed_ip(self): + serial_request = """ + + + + + """ + request = self.deserializer.deserialize(serial_request, 'create') + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": ""}], + }} + self.assertEquals(request['body'], expected) + + def test_request_with_networks_duplicate_ids(self): + serial_request = """ + + + + + + """ + request = self.deserializer.deserialize(serial_request, 'create') + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}, + {"uuid": "1", "fixed_ip": "10.0.2.12"}], + }} + self.assertEquals(request['body'], expected) + class TestAddressesXMLSerialization(test.TestCase): @@ -2712,12 +3228,14 @@ class TestServerInstanceCreation(test.TestCase): def __init__(self): self.injected_files = None + self.networks = None def create(self, *args, **kwargs): if 'injected_files' in kwargs: self.injected_files = kwargs['injected_files'] else: self.injected_files = None + return [{'id': '1234', 'display_name': 'fakeinstance', 'uuid': FAKE_UUID}] @@ -3039,24 +3557,28 @@ class ServersViewBuilderV11Test(test.TestCase): "display_description": "", "locked": False, "metadata": [], + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", #"address": , #"floating_ips": [{"address":ip} for ip in public_addresses]} "uuid": "deadbeef-feed-edee-beef-d0ea7beefedd"} return instance - def _get_view_builder(self): + def _get_view_builder(self, project_id=""): base_url = "http://localhost/v1.1" views = nova.api.openstack.views address_builder = views.addresses.ViewBuilderV11() - flavor_builder = views.flavors.ViewBuilderV11(base_url) - image_builder = views.images.ViewBuilderV11(base_url) + flavor_builder = views.flavors.ViewBuilderV11(base_url, project_id) + image_builder = views.images.ViewBuilderV11(base_url, project_id) view_builder = nova.api.openstack.views.servers.ViewBuilderV11( address_builder, flavor_builder, image_builder, - base_url) + base_url, + project_id, + ) return view_builder def test_build_server(self): @@ -3075,12 +3597,37 @@ class ServersViewBuilderV11Test(test.TestCase): "href": "http://localhost/servers/1", }, ], + "config_drive": None, } } output = self.view_builder.build(self.instance, False) self.assertDictMatch(output, expected_server) + def test_build_server_with_project_id(self): + expected_server = { + "server": { + "id": 1, + "uuid": self.instance['uuid'], + "name": "test_server", + "config_drive": None, + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/servers/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/servers/1", + }, + ], + } + } + + view_builder = self._get_view_builder(project_id='fake') + output = view_builder.build(self.instance, False) + self.assertDictMatch(output, expected_server) + def test_build_server_detail(self): image_bookmark = "http://localhost/images/5" flavor_bookmark = "http://localhost/flavors/1" @@ -3093,6 +3640,8 @@ class ServersViewBuilderV11Test(test.TestCase): "progress": 0, "name": "test_server", "status": "BUILD", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "5", @@ -3114,6 +3663,7 @@ class ServersViewBuilderV11Test(test.TestCase): }, "addresses": {}, "metadata": {}, + "config_drive": None, "links": [ { "rel": "self", @@ -3144,6 +3694,8 @@ class ServersViewBuilderV11Test(test.TestCase): "progress": 100, "name": "test_server", "status": "ACTIVE", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "5", @@ -3165,6 +3717,117 @@ class ServersViewBuilderV11Test(test.TestCase): }, "addresses": {}, "metadata": {}, + "config_drive": None, + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/servers/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/servers/1", + }, + ], + } + } + + output = self.view_builder.build(self.instance, True) + self.assertDictMatch(output, expected_server) + + def test_build_server_detail_with_accessipv4(self): + + self.instance['access_ip_v4'] = '1.2.3.4' + + image_bookmark = "http://localhost/images/5" + flavor_bookmark = "http://localhost/flavors/1" + expected_server = { + "server": { + "id": 1, + "uuid": self.instance['uuid'], + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "status": "BUILD", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": {}, + "metadata": {}, + "config_drive": None, + "accessIPv4": "1.2.3.4", + "accessIPv6": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/servers/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/servers/1", + }, + ], + } + } + + output = self.view_builder.build(self.instance, True) + self.assertDictMatch(output, expected_server) + + def test_build_server_detail_with_accessipv6(self): + + self.instance['access_ip_v6'] = 'fead::1234' + + image_bookmark = "http://localhost/images/5" + flavor_bookmark = "http://localhost/flavors/1" + expected_server = { + "server": { + "id": 1, + "uuid": self.instance['uuid'], + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "status": "BUILD", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": {}, + "metadata": {}, + "config_drive": None, + "accessIPv4": "", + "accessIPv6": "fead::1234", "links": [ { "rel": "self", @@ -3199,6 +3862,8 @@ class ServersViewBuilderV11Test(test.TestCase): "progress": 0, "name": "test_server", "status": "BUILD", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "5", @@ -3223,6 +3888,7 @@ class ServersViewBuilderV11Test(test.TestCase): "Open": "Stack", "Number": "1", }, + "config_drive": None, "links": [ { "rel": "self", @@ -3265,6 +3931,8 @@ class ServerXMLSerializationTest(test.TestCase): "name": "test_server", "status": "BUILD", "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", "image": { "id": "5", "links": [ @@ -3323,7 +3991,9 @@ class ServerXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixture, 'show') - actual = minidom.parseString(output.replace(" ", "")) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') expected_server_href = self.SERVER_HREF expected_server_bookmark = self.SERVER_BOOKMARK @@ -3331,47 +4001,57 @@ class ServerXMLSerializationTest(test.TestCase): expected_flavor_bookmark = self.FLAVOR_BOOKMARK expected_now = self.TIMESTAMP expected_uuid = FAKE_UUID - expected = minidom.parseString(""" - - - - - - - - - - - - Stack - - - 1 - - - - - - - - - - - - - - """.replace(" ", "") % (locals())) + server_dict = fixture['server'] - self.assertEqual(expected.toxml(), actual.toxml()) + for key in ['name', 'id', 'uuid', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6']: + self.assertEqual(root.get(key), str(server_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = server_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + image_root = root.find('{0}image'.format(NS)) + self.assertEqual(image_root.get('id'), server_dict['image']['id']) + link_nodes = image_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['image']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + flavor_root = root.find('{0}flavor'.format(NS)) + self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id']) + link_nodes = flavor_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['flavor']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + addresses_root = root.find('{0}addresses'.format(NS)) + addresses_dict = server_dict['addresses'] + network_elems = addresses_root.findall('{0}network'.format(NS)) + self.assertEqual(len(network_elems), 2) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) def test_create(self): serializer = servers.ServerXMLSerializer() @@ -3385,6 +4065,8 @@ class ServerXMLSerializationTest(test.TestCase): "progress": 0, "name": "test_server", "status": "BUILD", + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", "adminPass": "test_password", "image": { @@ -3445,7 +4127,9 @@ class ServerXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixture, 'create') - actual = minidom.parseString(output.replace(" ", "")) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') expected_server_href = self.SERVER_HREF expected_server_bookmark = self.SERVER_BOOKMARK @@ -3453,48 +4137,57 @@ class ServerXMLSerializationTest(test.TestCase): expected_flavor_bookmark = self.FLAVOR_BOOKMARK expected_now = self.TIMESTAMP expected_uuid = FAKE_UUID - expected = minidom.parseString(""" - - - - - - - - - - - - Stack - - - 1 - - - - - - - - - - - - - - """.replace(" ", "") % (locals())) + server_dict = fixture['server'] - self.assertEqual(expected.toxml(), actual.toxml()) + for key in ['name', 'id', 'uuid', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6', 'adminPass']: + self.assertEqual(root.get(key), str(server_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = server_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + image_root = root.find('{0}image'.format(NS)) + self.assertEqual(image_root.get('id'), server_dict['image']['id']) + link_nodes = image_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['image']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + flavor_root = root.find('{0}flavor'.format(NS)) + self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id']) + link_nodes = flavor_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['flavor']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + addresses_root = root.find('{0}addresses'.format(NS)) + addresses_dict = server_dict['addresses'] + network_elems = addresses_root.findall('{0}network'.format(NS)) + self.assertEqual(len(network_elems), 2) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) def test_index(self): serializer = servers.ServerXMLSerializer() @@ -3535,23 +4228,21 @@ class ServerXMLSerializationTest(test.TestCase): ]} output = serializer.serialize(fixture, 'index') - actual = minidom.parseString(output.replace(" ", "")) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'servers_index') + server_elems = root.findall('{0}server'.format(NS)) + self.assertEqual(len(server_elems), 2) + for i, server_elem in enumerate(server_elems): + server_dict = fixture['servers'][i] + for key in ['name', 'id']: + self.assertEqual(server_elem.get(key), str(server_dict[key])) - expected = minidom.parseString(""" - - - - - - - - - - - """.replace(" ", "") % (locals())) - - self.assertEqual(expected.toxml(), actual.toxml()) + link_nodes = server_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) def test_detail(self): serializer = servers.ServerXMLSerializer() @@ -3574,6 +4265,8 @@ class ServerXMLSerializationTest(test.TestCase): "progress": 0, "name": "test_server", "status": "BUILD", + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', "image": { "id": "5", @@ -3627,6 +4320,8 @@ class ServerXMLSerializationTest(test.TestCase): "progress": 100, "name": "test_server_2", "status": "ACTIVE", + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', "image": { "id": "5", @@ -3675,68 +4370,195 @@ class ServerXMLSerializationTest(test.TestCase): ]} output = serializer.serialize(fixture, 'detail') - actual = minidom.parseString(output.replace(" ", "")) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'servers') + server_elems = root.findall('{0}server'.format(NS)) + self.assertEqual(len(server_elems), 2) + for i, server_elem in enumerate(server_elems): + server_dict = fixture['servers'][i] - expected = minidom.parseString(""" - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - 2 - - - - - - - - - - - """.replace(" ", "") % (locals())) + for key in ['name', 'id', 'uuid', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6']: + self.assertEqual(server_elem.get(key), str(server_dict[key])) - self.assertEqual(expected.toxml(), actual.toxml()) + link_nodes = server_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = server_elem.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = server_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), + str(meta_value)) + + image_root = server_elem.find('{0}image'.format(NS)) + self.assertEqual(image_root.get('id'), server_dict['image']['id']) + link_nodes = image_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['image']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + flavor_root = server_elem.find('{0}flavor'.format(NS)) + self.assertEqual(flavor_root.get('id'), + server_dict['flavor']['id']) + link_nodes = flavor_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['flavor']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + addresses_root = server_elem.find('{0}addresses'.format(NS)) + addresses_dict = server_dict['addresses'] + network_elems = addresses_root.findall('{0}network'.format(NS)) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) + + def test_update(self): + serializer = servers.ServerXMLSerializer() + + fixture = { + "server": { + "id": 1, + "uuid": FAKE_UUID, + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + "progress": 0, + "name": "test_server", + "status": "BUILD", + "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": self.IMAGE_BOOKMARK, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": self.FLAVOR_BOOKMARK, + }, + ], + }, + "addresses": { + "network_one": [ + { + "version": 4, + "addr": "67.23.10.138", + }, + { + "version": 6, + "addr": "::babe:67.23.10.138", + }, + ], + "network_two": [ + { + "version": 4, + "addr": "67.23.10.139", + }, + { + "version": 6, + "addr": "::babe:67.23.10.139", + }, + ], + }, + "metadata": { + "Open": "Stack", + "Number": "1", + }, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + } + } + + output = serializer.serialize(fixture, 'update') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_image_bookmark = self.IMAGE_BOOKMARK + expected_flavor_bookmark = self.FLAVOR_BOOKMARK + expected_now = self.TIMESTAMP + expected_uuid = FAKE_UUID + server_dict = fixture['server'] + + for key in ['name', 'id', 'uuid', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6']: + self.assertEqual(root.get(key), str(server_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = server_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + image_root = root.find('{0}image'.format(NS)) + self.assertEqual(image_root.get('id'), server_dict['image']['id']) + link_nodes = image_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['image']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + flavor_root = root.find('{0}flavor'.format(NS)) + self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id']) + link_nodes = flavor_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['flavor']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + addresses_root = root.find('{0}addresses'.format(NS)) + addresses_dict = server_dict['addresses'] + network_elems = addresses_root.findall('{0}network'.format(NS)) + self.assertEqual(len(network_elems), 2) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index 035a35aabaa2..67c35fe6b771 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -48,6 +48,14 @@ class OpenStackApiAuthenticationException(OpenStackApiException): response) +class OpenStackApiAuthorizationException(OpenStackApiException): + def __init__(self, response=None, message=None): + if not message: + message = _("Authorization error") + super(OpenStackApiAuthorizationException, self).__init__(message, + response) + + class OpenStackApiNotFoundException(OpenStackApiException): def __init__(self, response=None, message=None): if not message: @@ -69,6 +77,8 @@ class TestOpenStackClient(object): self.auth_user = auth_user self.auth_key = auth_key self.auth_uri = auth_uri + # default project_id + self.project_id = 'openstack' def request(self, url, method='GET', body=None, headers=None): _headers = {'Content-Type': 'application/json'} @@ -105,7 +115,8 @@ class TestOpenStackClient(object): auth_uri = self.auth_uri headers = {'X-Auth-User': self.auth_user, - 'X-Auth-Key': self.auth_key} + 'X-Auth-Key': self.auth_key, + 'X-Auth-Project-Id': self.project_id} response = self.request(auth_uri, headers=headers) @@ -127,7 +138,8 @@ class TestOpenStackClient(object): # NOTE(justinsb): httplib 'helpfully' converts headers to lower case base_uri = auth_result['x-server-management-url'] - full_uri = base_uri + relative_uri + + full_uri = '%s/%s' % (base_uri, relative_uri) headers = kwargs.setdefault('headers', {}) headers['X-Auth-Token'] = auth_result['x-auth-token'] @@ -141,6 +153,8 @@ class TestOpenStackClient(object): if not http_status in check_response_status: if http_status == 404: raise OpenStackApiNotFoundException(response=response) + elif http_status == 401: + raise OpenStackApiAuthorizationException(response=response) else: raise OpenStackApiException( message=_("Unexpected status code"), @@ -256,7 +270,8 @@ class TestOpenStackClient(object): def post_server_volume(self, server_id, volume_attachment): return self.api_post('/servers/%s/os-volume_attachments' % - (server_id), volume_attachment)['volumeAttachment'] + (server_id), volume_attachment + )['volumeAttachment'] def delete_server_volume(self, server_id, attachment_id): return self.api_delete('/servers/%s/os-volume_attachments/%s' % diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index fb2f88502c18..343190427a57 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -22,10 +22,8 @@ Provides common functionality for integrated unit tests import random import string -from nova import exception from nova import service from nova import test # For the flags -from nova.auth import manager import nova.image.glance from nova.log import logging from nova.tests.integrated.api import client @@ -58,90 +56,6 @@ def generate_new_element(items, prefix, numeric=False): LOG.debug("Random collision on %s" % candidate) -class TestUser(object): - def __init__(self, name, secret, auth_url): - self.name = name - self.secret = secret - self.auth_url = auth_url - - if not auth_url: - raise exception.Error("auth_url is required") - self.openstack_api = client.TestOpenStackClient(self.name, - self.secret, - self.auth_url) - - def get_unused_server_name(self): - servers = self.openstack_api.get_servers() - server_names = [server['name'] for server in servers] - return generate_new_element(server_names, 'server') - - def get_invalid_image(self): - images = self.openstack_api.get_images() - image_ids = [image['id'] for image in images] - return generate_new_element(image_ids, '', numeric=True) - - def get_valid_image(self, create=False): - images = self.openstack_api.get_images() - if create and not images: - # TODO(justinsb): No way currently to create an image through API - #created_image = self.openstack_api.post_image(image) - #images.append(created_image) - raise exception.Error("No way to create an image through API") - - if images: - return images[0] - return None - - -class IntegratedUnitTestContext(object): - def __init__(self, auth_url): - self.auth_manager = manager.AuthManager() - - self.auth_url = auth_url - self.project_name = None - - self.test_user = None - - self.setup() - - def setup(self): - self._create_test_user() - - def _create_test_user(self): - self.test_user = self._create_unittest_user() - - # No way to currently pass this through the OpenStack API - self.project_name = 'openstack' - self._configure_project(self.project_name, self.test_user) - - def cleanup(self): - self.test_user = None - - def _create_unittest_user(self): - users = self.auth_manager.get_users() - user_names = [user.name for user in users] - auth_name = generate_new_element(user_names, 'unittest_user_') - auth_key = generate_random_alphanumeric(16) - - # Right now there's a bug where auth_name and auth_key are reversed - # bug732907 - auth_key = auth_name - - self.auth_manager.create_user(auth_name, auth_name, auth_key, False) - return TestUser(auth_name, auth_key, self.auth_url) - - def _configure_project(self, project_name, user): - projects = self.auth_manager.get_projects() - project_names = [project.name for project in projects] - if not project_name in project_names: - project = self.auth_manager.create_project(project_name, - user.name, - description=None, - member_users=None) - else: - self.auth_manager.add_to_project(user.name, project_name) - - class _IntegratedTestBase(test.TestCase): def setUp(self): super(_IntegratedTestBase, self).setUp() @@ -163,10 +77,7 @@ class _IntegratedTestBase(test.TestCase): self._start_api_service() - self.context = IntegratedUnitTestContext(self.auth_url) - - self.user = self.context.test_user - self.api = self.user.openstack_api + self.api = client.TestOpenStackClient('fake', 'fake', self.auth_url) def _start_api_service(self): osapi = service.WSGIService("osapi") @@ -174,10 +85,6 @@ class _IntegratedTestBase(test.TestCase): self.auth_url = 'http://%s:%s/v1.1' % (osapi.host, osapi.port) LOG.warn(self.auth_url) - def tearDown(self): - self.context.cleanup() - super(_IntegratedTestBase, self).tearDown() - def _get_flags(self): """An opportunity to setup flags, before the services are started.""" f = {} @@ -190,10 +97,20 @@ class _IntegratedTestBase(test.TestCase): f['fake_network'] = True return f + def get_unused_server_name(self): + servers = self.api.get_servers() + server_names = [server['name'] for server in servers] + return generate_new_element(server_names, 'server') + + def get_invalid_image(self): + images = self.api.get_images() + image_ids = [image['id'] for image in images] + return generate_new_element(image_ids, '', numeric=True) + def _build_minimal_create_server_request(self): server = {} - image = self.user.get_valid_image(create=True) + image = self.api.get_images()[0] LOG.debug("Image: %s" % image) if 'imageRef' in image: @@ -211,7 +128,7 @@ class _IntegratedTestBase(test.TestCase): server['flavorRef'] = 'http://fake.server/%s' % flavor['id'] # Set a valid server name - server_name = self.user.get_unused_server_name() + server_name = self.get_unused_server_name() server['name'] = server_name return server diff --git a/nova/tests/integrated/test_login.py b/nova/tests/integrated/test_login.py index 06359a52fd05..3a863d0f96b8 100644 --- a/nova/tests/integrated/test_login.py +++ b/nova/tests/integrated/test_login.py @@ -15,11 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. -import unittest from nova.log import logging from nova.tests.integrated import integrated_helpers -from nova.tests.integrated.api import client LOG = logging.getLogger('nova.tests.integrated') @@ -31,34 +29,3 @@ class LoginTest(integrated_helpers._IntegratedTestBase): flavors = self.api.get_flavors() for flavor in flavors: LOG.debug(_("flavor: %s") % flavor) - - def test_bad_login_password(self): - """Test that I get a 401 with a bad username.""" - bad_credentials_api = client.TestOpenStackClient(self.user.name, - "notso_password", - self.user.auth_url) - - self.assertRaises(client.OpenStackApiAuthenticationException, - bad_credentials_api.get_flavors) - - def test_bad_login_username(self): - """Test that I get a 401 with a bad password.""" - bad_credentials_api = client.TestOpenStackClient("notso_username", - self.user.secret, - self.user.auth_url) - - self.assertRaises(client.OpenStackApiAuthenticationException, - bad_credentials_api.get_flavors) - - def test_bad_login_both_bad(self): - """Test that I get a 401 with both bad username and bad password.""" - bad_credentials_api = client.TestOpenStackClient("notso_username", - "notso_password", - self.user.auth_url) - - self.assertRaises(client.OpenStackApiAuthenticationException, - bad_credentials_api.get_flavors) - - -if __name__ == "__main__": - unittest.main() diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index 725f6d529913..c2f800689787 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -51,7 +51,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): self.api.post_server, post) # With an invalid imageRef, this throws 500. - server['imageRef'] = self.user.get_invalid_image() + server['imageRef'] = self.get_invalid_image() # TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) diff --git a/nova/tests/monkey_patch_example/__init__.py b/nova/tests/monkey_patch_example/__init__.py new file mode 100644 index 000000000000..25cf9ccfe68c --- /dev/null +++ b/nova/tests/monkey_patch_example/__init__.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. +"""Example Module for testing utils.monkey_patch().""" + + +CALLED_FUNCTION = [] + + +def example_decorator(name, function): + """ decorator for notify which is used from utils.monkey_patch() + + :param name: name of the function + :param function: - object of the function + :returns: function -- decorated function + """ + def wrapped_func(*args, **kwarg): + CALLED_FUNCTION.append(name) + return function(*args, **kwarg) + return wrapped_func diff --git a/nova/tests/monkey_patch_example/example_a.py b/nova/tests/monkey_patch_example/example_a.py new file mode 100644 index 000000000000..21e79bcb0f79 --- /dev/null +++ b/nova/tests/monkey_patch_example/example_a.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. +"""Example Module A for testing utils.monkey_patch().""" + + +def example_function_a(): + return 'Example function' + + +class ExampleClassA(): + def example_method(self): + return 'Example method' + + def example_method_add(self, arg1, arg2): + return arg1 + arg2 diff --git a/nova/tests/monkey_patch_example/example_b.py b/nova/tests/monkey_patch_example/example_b.py new file mode 100644 index 000000000000..9d8f6d339ef9 --- /dev/null +++ b/nova/tests/monkey_patch_example/example_b.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +"""Example Module B for testing utils.monkey_patch().""" + + +def example_function_b(): + return 'Example function' + + +class ExampleClassB(): + def example_method(self): + return 'Example method' + + def example_method_add(self, arg1, arg2): + return arg1 + arg2 diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 2011ae756b4f..526d1c490bf7 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -32,6 +32,7 @@ from nova import context from nova import exception from nova import test from nova import wsgi +from nova.api import auth from nova.api import ec2 from nova.api.ec2 import apirequest from nova.api.ec2 import cloud @@ -199,7 +200,7 @@ class ApiEc2TestCase(test.TestCase): # NOTE(vish): skipping the Authorizer roles = ['sysadmin', 'netadmin'] ctxt = context.RequestContext('fake', 'fake', roles=roles) - self.app = wsgi.InjectContext(ctxt, + self.app = auth.InjectContext(ctxt, ec2.Requestify(ec2.Authorizer(ec2.Executor()), 'nova.api.ec2.cloud.CloudController')) diff --git a/nova/tests/test_auth.py b/nova/tests/test_auth.py index 4561eb7f26c7..1b3166af78a0 100644 --- a/nova/tests/test_auth.py +++ b/nova/tests/test_auth.py @@ -147,6 +147,7 @@ class _AuthManagerBaseTestCase(test.TestCase): '/services/Cloud')) def test_can_get_credentials(self): + self.flags(use_deprecated_auth=True) st = {'access': 'access', 'secret': 'secret'} with user_and_project_generator(self.manager, user_state=st) as (u, p): credentials = self.manager.get_environment_rc(u, p) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index b2afc53c939b..0793784f8b1f 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -487,6 +487,17 @@ class CloudTestCase(test.TestCase): db.service_destroy(self.context, comp1['id']) db.service_destroy(self.context, comp2['id']) + def test_describe_instances_deleted(self): + args1 = {'reservation_id': 'a', 'image_ref': 1, 'host': 'host1'} + inst1 = db.instance_create(self.context, args1) + args2 = {'reservation_id': 'b', 'image_ref': 1, 'host': 'host1'} + inst2 = db.instance_create(self.context, args2) + db.instance_destroy(self.context, inst1.id) + result = self.cloud.describe_instances(self.context) + result = result['reservationSet'][0]['instancesSet'] + self.assertEqual(result[0]['instanceId'], + ec2utils.id_to_ec2_id(inst2.id)) + def _block_device_mapping_create(self, instance_id, mappings): volumes = [] for bdm in mappings: diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 4f5d36f1448a..0523d73b6193 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -2,6 +2,7 @@ # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Piston Cloud Computing, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -159,6 +160,20 @@ class ComputeTestCase(test.TestCase): db.security_group_destroy(self.context, group['id']) db.instance_destroy(self.context, ref[0]['id']) + def test_create_instance_associates_config_drive(self): + """Make sure create associates a config drive.""" + + instance_id = self._create_instance(params={'config_drive': True, }) + + try: + self.compute.run_instance(self.context, instance_id) + instances = db.instance_get_all(context.get_admin_context()) + instance = instances[0] + + self.assertTrue(instance.config_drive) + finally: + db.instance_destroy(self.context, instance_id) + def test_default_hostname_generator(self): cases = [(None, 'server_1'), ('Hello, Server!', 'hello_server'), ('<}\x1fh\x10e\x08l\x02l\x05o\x12!{>', 'hello')] @@ -347,7 +362,7 @@ class ComputeTestCase(test.TestCase): self.assertEquals(msg['priority'], 'INFO') self.assertEquals(msg['event_type'], 'compute.instance.create') payload = msg['payload'] - self.assertEquals(payload['tenant_id'], self.project_id) + self.assertEquals(payload['project_id'], self.project_id) self.assertEquals(payload['user_id'], self.user_id) self.assertEquals(payload['instance_id'], instance_id) self.assertEquals(payload['instance_type'], 'm1.tiny') @@ -371,7 +386,7 @@ class ComputeTestCase(test.TestCase): self.assertEquals(msg['priority'], 'INFO') self.assertEquals(msg['event_type'], 'compute.instance.delete') payload = msg['payload'] - self.assertEquals(payload['tenant_id'], self.project_id) + self.assertEquals(payload['project_id'], self.project_id) self.assertEquals(payload['user_id'], self.user_id) self.assertEquals(payload['instance_id'], instance_id) self.assertEquals(payload['instance_type'], 'm1.tiny') @@ -454,7 +469,7 @@ class ComputeTestCase(test.TestCase): self.assertEquals(msg['priority'], 'INFO') self.assertEquals(msg['event_type'], 'compute.instance.resize.prep') payload = msg['payload'] - self.assertEquals(payload['tenant_id'], self.project_id) + self.assertEquals(payload['project_id'], self.project_id) self.assertEquals(payload['user_id'], self.user_id) self.assertEquals(payload['instance_id'], instance_id) self.assertEquals(payload['instance_type'], 'm1.tiny') diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py index 0c07cbb7c601..038c07f40557 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -76,3 +76,20 @@ class DbApiTestCase(test.TestCase): self.assertEqual(instance['id'], result['id']) self.assertEqual(result['fixed_ips'][0]['floating_ips'][0].address, '1.2.1.2') + + def test_instance_get_all_by_filters(self): + args = {'reservation_id': 'a', 'image_ref': 1, 'host': 'host1'} + inst1 = db.instance_create(self.context, args) + inst2 = db.instance_create(self.context, args) + result = db.instance_get_all_by_filters(self.context, {}) + self.assertTrue(2, len(result)) + + def test_instance_get_all_by_filters_deleted(self): + args1 = {'reservation_id': 'a', 'image_ref': 1, 'host': 'host1'} + inst1 = db.instance_create(self.context, args1) + args2 = {'reservation_id': 'b', 'image_ref': 1, 'host': 'host1'} + inst2 = db.instance_create(self.context, args2) + db.instance_destroy(self.context, inst1.id) + result = db.instance_get_all_by_filters(self.context.elevated(), {}) + self.assertEqual(1, len(result)) + self.assertEqual(result[0].id, inst2.id) diff --git a/nova/tests/test_ipv6.py b/nova/tests/test_ipv6.py index d123df6f1955..5c333b17e5a7 100644 --- a/nova/tests/test_ipv6.py +++ b/nova/tests/test_ipv6.py @@ -55,3 +55,22 @@ class IPv6AccountIdentiferTestCase(test.TestCase): def test_to_mac(self): mac = ipv6.to_mac('2001:db8::a94a:8fe5:ff33:4455') self.assertEquals(mac, '02:16:3e:33:44:55') + + def test_to_global_with_bad_mac(self): + bad_mac = '02:16:3e:33:44:5X' + self.assertRaises(TypeError, ipv6.to_global, + '2001:db8::', bad_mac, 'test') + + def test_to_global_with_bad_prefix(self): + bad_prefix = '78' + self.assertRaises(TypeError, ipv6.to_global, + bad_prefix, + '2001:db8::a94a:8fe5:ff33:4455', + 'test') + + def test_to_global_with_bad_project(self): + bad_project = 'non-existent-project-name' + self.assertRaises(TypeError, ipv6.to_global, + '2001:db8::', + '2001:db8::a94a:8fe5:ff33:4455', + bad_project) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 688518bb8baf..6a213b4f0e4a 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -836,6 +836,7 @@ class LibvirtConnTestCase(test.TestCase): count = (0 <= str(e.message).find('Unexpected method call')) shutil.rmtree(os.path.join(FLAGS.instances_path, instance.name)) + shutil.rmtree(os.path.join(FLAGS.instances_path, '_base')) self.assertTrue(count) diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py index bfc7a6d440b2..b06e5c13653d 100644 --- a/nova/tests/test_metadata.py +++ b/nova/tests/test_metadata.py @@ -23,12 +23,21 @@ import httplib import webob +from nova import exception from nova import test from nova import wsgi from nova.api.ec2 import metadatarequesthandler from nova.db.sqlalchemy import api +USER_DATA_STRING = ("This is an encoded string") +ENCODE_USER_DATA_STRING = base64.b64encode(USER_DATA_STRING) + + +def return_non_existing_server_by_address(context, address): + raise exception.NotFound() + + class MetadataTestCase(test.TestCase): """Test that metadata is returning proper values.""" @@ -79,3 +88,34 @@ class MetadataTestCase(test.TestCase): self.stubs.Set(api, 'security_group_get_by_instance', sg_get) self.assertEqual(self.request('/meta-data/security-groups'), 'default\nother') + + def test_user_data_non_existing_fixed_address(self): + self.stubs.Set(api, 'instance_get_all_by_filters', + return_non_existing_server_by_address) + request = webob.Request.blank('/user-data') + request.remote_addr = "127.1.1.1" + response = request.get_response(self.app) + self.assertEqual(response.status_int, 404) + + def test_user_data_none_fixed_address(self): + self.stubs.Set(api, 'instance_get_all_by_filters', + return_non_existing_server_by_address) + request = webob.Request.blank('/user-data') + request.remote_addr = None + response = request.get_response(self.app) + self.assertEqual(response.status_int, 500) + + def test_user_data_invalid_url(self): + request = webob.Request.blank('/user-data-invalid') + request.remote_addr = "127.0.0.1" + response = request.get_response(self.app) + self.assertEqual(response.status_int, 404) + + def test_user_data_with_use_forwarded_header(self): + self.instance['user_data'] = ENCODE_USER_DATA_STRING + self.flags(use_forwarded_for=True) + request = webob.Request.blank('/user-data') + request.remote_addr = "127.0.0.1" + response = request.get_response(self.app) + self.assertEqual(response.status_int, 200) + self.assertEqual(response.body, USER_DATA_STRING) diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 0ead680eedcf..0b85394423a3 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +from nova import context from nova import db from nova import exception from nova import log as logging @@ -41,6 +42,7 @@ class FakeModel(dict): networks = [{'id': 0, + 'uuid': "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", 'label': 'test0', 'injected': False, 'multi_host': False, @@ -60,6 +62,7 @@ networks = [{'id': 0, 'project_id': 'fake_project', 'vpn_public_address': '192.168.0.2'}, {'id': 1, + 'uuid': "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", 'label': 'test1', 'injected': False, 'multi_host': False, @@ -108,11 +111,14 @@ floating_ip_fields = {'id': 0, vifs = [{'id': 0, 'address': 'DE:AD:BE:EF:00:00', + 'uuid': '00000000-0000-0000-0000-0000000000000000', 'network_id': 0, 'network': FakeModel(**networks[0]), 'instance_id': 0}, {'id': 1, 'address': 'DE:AD:BE:EF:00:01', + 'uuid': '00000000-0000-0000-0000-0000000000000001', + 'network_id': 0, 'network_id': 1, 'network': FakeModel(**networks[1]), 'instance_id': 0}] @@ -123,6 +129,8 @@ class FlatNetworkTestCase(test.TestCase): super(FlatNetworkTestCase, self).setUp() self.network = network_manager.FlatManager(host=HOST) self.network.db = db + self.context = context.RequestContext('testuser', 'testproject', + is_admin=False) def test_get_instance_nw_info(self): self.mox.StubOutWithMock(db, 'fixed_ip_get_by_instance') @@ -163,6 +171,8 @@ class FlatNetworkTestCase(test.TestCase): 'ips': 'DONTCARE', 'label': 'test%s' % i, 'mac': 'DE:AD:BE:EF:00:0%s' % i, + 'vif_uuid': ('00000000-0000-0000-0000-000000000000000%s' % + i), 'rxtx_cap': 'DONTCARE', 'should_create_vlan': False, 'should_create_bridge': False} @@ -178,12 +188,73 @@ class FlatNetworkTestCase(test.TestCase): 'netmask': '255.255.255.0'}] self.assertDictListMatch(nw[1]['ips'], check) + def test_validate_networks(self): + self.mox.StubOutWithMock(db, 'network_get_all_by_uuids') + self.mox.StubOutWithMock(db, "fixed_ip_get_by_address") + + requested_networks = [("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "192.168.1.100")] + db.network_get_all_by_uuids(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(networks) + + fixed_ips[1]['network'] = FakeModel(**networks[1]) + fixed_ips[1]['instance'] = None + db.fixed_ip_get_by_address(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(fixed_ips[1]) + + self.mox.ReplayAll() + self.network.validate_networks(self.context, requested_networks) + + def test_validate_networks_none_requested_networks(self): + self.network.validate_networks(self.context, None) + + def test_validate_networks_empty_requested_networks(self): + requested_networks = [] + self.mox.ReplayAll() + + self.network.validate_networks(self.context, requested_networks) + + def test_validate_networks_invalid_fixed_ip(self): + self.mox.StubOutWithMock(db, 'network_get_all_by_uuids') + requested_networks = [(1, "192.168.0.100.1")] + db.network_get_all_by_uuids(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(networks) + self.mox.ReplayAll() + + self.assertRaises(exception.FixedIpInvalid, + self.network.validate_networks, None, + requested_networks) + + def test_validate_networks_empty_fixed_ip(self): + self.mox.StubOutWithMock(db, 'network_get_all_by_uuids') + + requested_networks = [(1, "")] + db.network_get_all_by_uuids(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(networks) + self.mox.ReplayAll() + + self.assertRaises(exception.FixedIpInvalid, + self.network.validate_networks, + None, requested_networks) + + def test_validate_networks_none_fixed_ip(self): + self.mox.StubOutWithMock(db, 'network_get_all_by_uuids') + + requested_networks = [(1, None)] + db.network_get_all_by_uuids(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(networks) + self.mox.ReplayAll() + + self.network.validate_networks(None, requested_networks) + class VlanNetworkTestCase(test.TestCase): def setUp(self): super(VlanNetworkTestCase, self).setUp() self.network = network_manager.VlanManager(host=HOST) self.network.db = db + self.context = context.RequestContext('testuser', 'testproject', + is_admin=False) def test_vpn_allocate_fixed_ip(self): self.mox.StubOutWithMock(db, 'fixed_ip_associate') @@ -227,7 +298,7 @@ class VlanNetworkTestCase(test.TestCase): network = dict(networks[0]) network['vpn_private_address'] = '192.168.0.2' - self.network.allocate_fixed_ip(None, 0, network) + self.network.allocate_fixed_ip(self.context, 0, network) def test_create_networks_too_big(self): self.assertRaises(ValueError, self.network.create_networks, None, @@ -238,6 +309,68 @@ class VlanNetworkTestCase(test.TestCase): num_networks=100, vlan_start=1, cidr='192.168.0.1/24', network_size=100) + def test_validate_networks(self): + self.mox.StubOutWithMock(db, 'network_get_all_by_uuids') + self.mox.StubOutWithMock(db, "fixed_ip_get_by_address") + + requested_networks = [("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "192.168.1.100")] + db.network_get_all_by_uuids(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(networks) + + fixed_ips[1]['network'] = FakeModel(**networks[1]) + fixed_ips[1]['instance'] = None + db.fixed_ip_get_by_address(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(fixed_ips[1]) + + self.mox.ReplayAll() + self.network.validate_networks(self.context, requested_networks) + + def test_validate_networks_none_requested_networks(self): + self.network.validate_networks(self.context, None) + + def test_validate_networks_empty_requested_networks(self): + requested_networks = [] + self.mox.ReplayAll() + + self.network.validate_networks(self.context, requested_networks) + + def test_validate_networks_invalid_fixed_ip(self): + self.mox.StubOutWithMock(db, 'network_get_all_by_uuids') + requested_networks = [(1, "192.168.0.100.1")] + db.network_get_all_by_uuids(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(networks) + self.mox.ReplayAll() + + self.assertRaises(exception.FixedIpInvalid, + self.network.validate_networks, self.context, + requested_networks) + + def test_validate_networks_empty_fixed_ip(self): + self.mox.StubOutWithMock(db, 'network_get_all_by_uuids') + + requested_networks = [(1, "")] + db.network_get_all_by_uuids(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(networks) + self.mox.ReplayAll() + + self.assertRaises(exception.FixedIpInvalid, + self.network.validate_networks, + self.context, requested_networks) + + def test_validate_networks_none_fixed_ip(self): + self.mox.StubOutWithMock(db, 'network_get_all_by_uuids') + + requested_networks = [(1, None)] + db.network_get_all_by_uuids(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(networks) + self.mox.ReplayAll() + self.network.validate_networks(self.context, requested_networks) + class CommonNetworkTestCase(test.TestCase): diff --git a/nova/tests/test_notifier.py b/nova/tests/test_notifier.py index 64b799a2c316..7de3a4a99832 100644 --- a/nova/tests/test_notifier.py +++ b/nova/tests/test_notifier.py @@ -134,3 +134,24 @@ class NotifierTestCase(test.TestCase): self.assertEqual(msg['event_type'], 'error_notification') self.assertEqual(msg['priority'], 'ERROR') self.assertEqual(msg['payload']['error'], 'foo') + + def test_send_notification_by_decorator(self): + self.notify_called = False + + def example_api(arg1, arg2): + return arg1 + arg2 + + example_api = nova.notifier.api.notify_decorator( + 'example_api', + example_api) + + def mock_notify(cls, *args): + self.notify_called = True + + self.stubs.Set(nova.notifier.no_op_notifier, 'notify', + mock_notify) + + class Mock(object): + pass + self.assertEqual(3, example_api(1, 2)) + self.assertEqual(self.notify_called, True) diff --git a/nova/tests/test_nova_manage.py b/nova/tests/test_nova_manage.py index 9c6563f14c71..f5ea68a03828 100644 --- a/nova/tests/test_nova_manage.py +++ b/nova/tests/test_nova_manage.py @@ -28,55 +28,45 @@ sys.dont_write_bytecode = True import imp nova_manage = imp.load_source('nova_manage.py', NOVA_MANAGE_PATH) sys.dont_write_bytecode = False +import mox +import stubout -import netaddr from nova import context from nova import db -from nova import flags +from nova import exception from nova import test - -FLAGS = flags.FLAGS +from nova.tests.db import fakes as db_fakes class FixedIpCommandsTestCase(test.TestCase): def setUp(self): super(FixedIpCommandsTestCase, self).setUp() - cidr = '10.0.0.0/24' - net = netaddr.IPNetwork(cidr) - net_info = {'bridge': 'fakebr', - 'bridge_interface': 'fakeeth', - 'dns': FLAGS.flat_network_dns, - 'cidr': cidr, - 'netmask': str(net.netmask), - 'gateway': str(net[1]), - 'broadcast': str(net.broadcast), - 'dhcp_start': str(net[2])} - self.network = db.network_create_safe(context.get_admin_context(), - net_info) - num_ips = len(net) - for index in range(num_ips): - address = str(net[index]) - reserved = (index == 1 or index == 2) - db.fixed_ip_create(context.get_admin_context(), - {'network_id': self.network['id'], - 'address': address, - 'reserved': reserved}) + self.stubs = stubout.StubOutForTesting() + db_fakes.stub_out_db_network_api(self.stubs) self.commands = nova_manage.FixedIpCommands() def tearDown(self): - db.network_delete_safe(context.get_admin_context(), self.network['id']) super(FixedIpCommandsTestCase, self).tearDown() + self.stubs.UnsetAll() def test_reserve(self): - self.commands.reserve('10.0.0.100') + self.commands.reserve('192.168.0.100') address = db.fixed_ip_get_by_address(context.get_admin_context(), - '10.0.0.100') + '192.168.0.100') self.assertEqual(address['reserved'], True) + def test_reserve_nonexistent_address(self): + self.assertRaises(SystemExit, + self.commands.reserve, + '55.55.55.55') + def test_unreserve(self): - db.fixed_ip_update(context.get_admin_context(), '10.0.0.100', - {'reserved': True}) - self.commands.unreserve('10.0.0.100') + self.commands.unreserve('192.168.0.100') address = db.fixed_ip_get_by_address(context.get_admin_context(), - '10.0.0.100') + '192.168.0.100') self.assertEqual(address['reserved'], False) + + def test_unreserve_nonexistent_address(self): + self.assertRaises(SystemExit, + self.commands.unreserve, + '55.55.55.55') diff --git a/nova/tests/test_service.py b/nova/tests/test_service.py index 8f92406ff932..760b150be84b 100644 --- a/nova/tests/test_service.py +++ b/nova/tests/test_service.py @@ -205,6 +205,6 @@ class TestLauncher(test.TestCase): def test_launch_app(self): self.assertEquals(0, self.service.port) launcher = service.Launcher() - launcher.launch_service(self.service) + launcher.launch_server(self.service) self.assertEquals(0, self.service.port) launcher.stop() diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py index ec5098a37263..1ba794a1a954 100644 --- a/nova/tests/test_utils.py +++ b/nova/tests/test_utils.py @@ -18,6 +18,7 @@ import datetime import os import tempfile +import nova from nova import exception from nova import test from nova import utils @@ -384,3 +385,57 @@ class ToPrimitiveTestCase(test.TestCase): def test_typeerror(self): x = bytearray # Class, not instance self.assertEquals(utils.to_primitive(x), u"") + + def test_nasties(self): + def foo(): + pass + x = [datetime, foo, dir] + ret = utils.to_primitive(x) + self.assertEquals(len(ret), 3) + self.assertTrue(ret[0].startswith(u"') + + +class MonkeyPatchTestCase(test.TestCase): + """Unit test for utils.monkey_patch().""" + def setUp(self): + super(MonkeyPatchTestCase, self).setUp() + self.example_package = 'nova.tests.monkey_patch_example.' + self.flags( + monkey_patch=True, + monkey_patch_modules=[self.example_package + 'example_a' + ':' + + self.example_package + 'example_decorator']) + + def test_monkey_patch(self): + utils.monkey_patch() + nova.tests.monkey_patch_example.CALLED_FUNCTION = [] + from nova.tests.monkey_patch_example import example_a, example_b + + self.assertEqual('Example function', example_a.example_function_a()) + exampleA = example_a.ExampleClassA() + exampleA.example_method() + ret_a = exampleA.example_method_add(3, 5) + self.assertEqual(ret_a, 8) + + self.assertEqual('Example function', example_b.example_function_b()) + exampleB = example_b.ExampleClassB() + exampleB.example_method() + ret_b = exampleB.example_method_add(3, 5) + + self.assertEqual(ret_b, 8) + package_a = self.example_package + 'example_a.' + self.assertTrue(package_a + 'example_function_a' + in nova.tests.monkey_patch_example.CALLED_FUNCTION) + + self.assertTrue(package_a + 'ExampleClassA.example_method' + in nova.tests.monkey_patch_example.CALLED_FUNCTION) + self.assertTrue(package_a + 'ExampleClassA.example_method_add' + in nova.tests.monkey_patch_example.CALLED_FUNCTION) + package_b = self.example_package + 'example_b.' + self.assertFalse(package_b + 'example_function_b' + in nova.tests.monkey_patch_example.CALLED_FUNCTION) + self.assertFalse(package_b + 'ExampleClassB.example_method' + in nova.tests.monkey_patch_example.CALLED_FUNCTION) + self.assertFalse(package_b + 'ExampleClassB.example_method_add' + in nova.tests.monkey_patch_example.CALLED_FUNCTION) diff --git a/nova/tests/test_versions.py b/nova/tests/test_versions.py new file mode 100644 index 000000000000..4621b042b6b8 --- /dev/null +++ b/nova/tests/test_versions.py @@ -0,0 +1,61 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Ken Pepple +# +# 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 nova import exception +from nova import test +from nova import utils +from nova import version + + +class VersionTestCase(test.TestCase): + """Test cases for Versions code""" + def setUp(self): + """setup test with unchanging values""" + super(VersionTestCase, self).setUp() + self.version = version + self.version.FINAL = False + self.version.NOVA_VERSION = ['2012', '10'] + self.version.YEAR, self.version.COUNT = self.version.NOVA_VERSION + self.version.version_info = {'branch_nick': u'LOCALBRANCH', + 'revision_id': 'LOCALREVISION', + 'revno': 0} + + def test_version_string_is_good(self): + """Ensure version string works""" + self.assertEqual("2012.10-dev", self.version.version_string()) + + def test_canonical_version_string_is_good(self): + """Ensure canonical version works""" + self.assertEqual("2012.10", self.version.canonical_version_string()) + + def test_final_version_strings_are_identical(self): + """Ensure final version strings match only at release""" + self.assertNotEqual(self.version.canonical_version_string(), + self.version.version_string()) + self.version.FINAL = True + self.assertEqual(self.version.canonical_version_string(), + self.version.version_string()) + + def test_vcs_version_string_is_good(self): + """Ensure uninstalled code generates local """ + self.assertEqual("LOCALBRANCH:LOCALREVISION", + self.version.vcs_version_string()) + + def test_version_string_with_vcs_is_good(self): + """Ensure uninstalled code get version string""" + self.assertEqual("2012.10-LOCALBRANCH:LOCALREVISION", + self.version.version_string_with_vcs()) diff --git a/nova/utils.py b/nova/utils.py index 7276b6bd50c2..21e6221b29fb 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -35,6 +35,7 @@ import sys import time import types import uuid +import pyclbr from xml.sax import saxutils from eventlet import event @@ -260,8 +261,9 @@ def default_flagfile(filename='nova.conf', args=None): filename = "./nova.conf" if not os.path.exists(filename): filename = '/etc/nova/nova.conf' - flagfile = '--flagfile=%s' % filename - args.insert(1, flagfile) + if os.path.exists(filename): + flagfile = '--flagfile=%s' % filename + args.insert(1, flagfile) def debug(arg): @@ -294,7 +296,7 @@ EASIER_PASSWORD_SYMBOLS = ('23456789' # Removed: 0, 1 def usage_from_instance(instance_ref, **kw): usage_info = dict( - tenant_id=instance_ref['project_id'], + project_id=instance_ref['project_id'], user_id=instance_ref['user_id'], instance_id=instance_ref['id'], instance_type=instance_ref['instance_type']['name'], @@ -546,11 +548,17 @@ def to_primitive(value, convert_instances=False, level=0): Therefore, convert_instances=True is lossy ... be aware. """ - if inspect.isclass(value): - return unicode(value) + nasty = [inspect.ismodule, inspect.isclass, inspect.ismethod, + inspect.isfunction, inspect.isgeneratorfunction, + inspect.isgenerator, inspect.istraceback, inspect.isframe, + inspect.iscode, inspect.isbuiltin, inspect.isroutine, + inspect.isabstract] + for test in nasty: + if test(value): + return unicode(value) if level > 3: - return [] + return '?' # The try block may not be necessary after the class check above, # but just in case ... @@ -839,37 +847,57 @@ def bool_from_str(val): return val.lower() == 'true' -class Bootstrapper(object): - """Provides environment bootstrapping capabilities for entry points.""" +def is_valid_ipv4(address): + """valid the address strictly as per format xxx.xxx.xxx.xxx. + where xxx is a value between 0 and 255. + """ + parts = address.split(".") + if len(parts) != 4: + return False + for item in parts: + try: + if not 0 <= int(item) <= 255: + return False + except ValueError: + return False + return True - @staticmethod - def bootstrap_binary(argv): - """Initialize the Nova environment using command line arguments.""" - Bootstrapper.setup_flags(argv) - Bootstrapper.setup_logging() - Bootstrapper.log_flags() - @staticmethod - def setup_logging(): - """Initialize logging and log a message indicating the Nova version.""" - logging.setup() - logging.audit(_("Nova Version (%s)") % - version.version_string_with_vcs()) +def monkey_patch(): + """ If the Flags.monkey_patch set as True, + this functuion patches a decorator + for all functions in specified modules. + You can set decorators for each modules + using FLAGS.monkey_patch_modules. + The format is "Module path:Decorator function". + Example: 'nova.api.ec2.cloud:nova.notifier.api.notify_decorator' - @staticmethod - def setup_flags(input_flags): - """Initialize flags, load flag file, and print help if needed.""" - default_flagfile(args=input_flags) - FLAGS(input_flags or []) - flags.DEFINE_flag(flags.HelpFlag()) - flags.DEFINE_flag(flags.HelpshortFlag()) - flags.DEFINE_flag(flags.HelpXMLFlag()) - FLAGS.ParseNewFlags() + Parameters of the decorator is as follows. + (See nova.notifier.api.notify_decorator) - @staticmethod - def log_flags(): - """Log the list of all active flags being used.""" - logging.audit(_("Currently active flags:")) - for key in FLAGS: - value = FLAGS.get(key, None) - logging.audit(_("%(key)s : %(value)s" % locals())) + name - name of the function + function - object of the function + """ + # If FLAGS.monkey_patch is not True, this function do nothing. + if not FLAGS.monkey_patch: + return + # Get list of modules and decorators + for module_and_decorator in FLAGS.monkey_patch_modules: + module, decorator_name = module_and_decorator.split(':') + # import decorator function + decorator = import_class(decorator_name) + __import__(module) + # Retrieve module information using pyclbr + module_data = pyclbr.readmodule_ex(module) + for key in module_data.keys(): + # set the decorator for the class methods + if isinstance(module_data[key], pyclbr.Class): + clz = import_class("%s.%s" % (module, key)) + for method, func in inspect.getmembers(clz, inspect.ismethod): + setattr(clz, method,\ + decorator("%s.%s.%s" % (module, key, method), func)) + # set the decorator for the function + if isinstance(module_data[key], pyclbr.Function): + func = import_class("%s.%s" % (module, key)) + setattr(sys.modules[module], key,\ + decorator("%s.%s" % (module, key), func)) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 19f3ec185aae..52b2881e8b4d 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -2,6 +2,9 @@ # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. +# +# Copyright 2011, Piston Cloud Computing, Inc. +# # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -22,6 +25,7 @@ Includes injection of SSH PGP keys into authorized_keys file. """ +import json import os import tempfile import time @@ -60,7 +64,8 @@ def extend(image, size): utils.execute('resize2fs', image, check_exit_code=False) -def inject_data(image, key=None, net=None, partition=None, nbd=False): +def inject_data(image, key=None, net=None, metadata=None, + partition=None, nbd=False, tune2fs=True): """Injects a ssh key and optionally net data into a disk image. it will mount the image as a fully partitioned disk and attempt to inject @@ -89,10 +94,10 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): ' only inject raw disk images): %s' % mapped_device) - # Configure ext2fs so that it doesn't auto-check every N boots - out, err = utils.execute('tune2fs', '-c', 0, '-i', 0, - mapped_device, run_as_root=True) - + if tune2fs: + # Configure ext2fs so that it doesn't auto-check every N boots + out, err = utils.execute('tune2fs', '-c', 0, '-i', 0, + mapped_device, run_as_root=True) tmpdir = tempfile.mkdtemp() try: # mount loopback to dir @@ -103,7 +108,8 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): % err) try: - inject_data_into_fs(tmpdir, key, net, utils.execute) + inject_data_into_fs(tmpdir, key, net, metadata, + utils.execute) finally: # unmount device utils.execute('umount', mapped_device, run_as_root=True) @@ -155,6 +161,7 @@ def destroy_container(target, instance, nbd=False): def _link_device(image, nbd): """Link image to device using loopback or nbd""" + if nbd: device = _allocate_device() utils.execute('qemu-nbd', '-c', device, image, run_as_root=True) @@ -190,6 +197,7 @@ def _allocate_device(): # NOTE(vish): This assumes no other processes are allocating nbd devices. # It may race cause a race condition if multiple # workers are running on a given machine. + while True: if not _DEVICES: raise exception.Error(_('No free nbd devices')) @@ -203,7 +211,7 @@ def _free_device(device): _DEVICES.append(device) -def inject_data_into_fs(fs, key, net, execute): +def inject_data_into_fs(fs, key, net, metadata, execute): """Injects data into a filesystem already mounted by the caller. Virt connections can call this directly if they mount their fs in a different way to inject_data @@ -212,6 +220,16 @@ def inject_data_into_fs(fs, key, net, execute): _inject_key_into_fs(key, fs, execute=execute) if net: _inject_net_into_fs(net, fs, execute=execute) + if metadata: + _inject_metadata_into_fs(metadata, fs, execute=execute) + + +def _inject_metadata_into_fs(metadata, fs, execute=None): + metadata_path = os.path.join(fs, "meta.js") + metadata = dict([(m.key, m.value) for m in metadata]) + + utils.execute('sudo', 'tee', metadata_path, + process_input=json.dumps(metadata)) def _inject_key_into_fs(key, fs, execute=None): diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 20af2666d6e0..93290aba7ec3 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -62,11 +62,41 @@ def block_device_info_get_mapping(block_device_info): class ComputeDriver(object): """Base class for compute drivers. - Lots of documentation is currently on fake.py. + The interface to this class talks in terms of 'instances' (Amazon EC2 and + internal Nova terminology), by which we mean 'running virtual machine' + (XenAPI terminology) or domain (Xen or libvirt terminology). + + An instance has an ID, which is the identifier chosen by Nova to represent + the instance further up the stack. This is unfortunately also called a + 'name' elsewhere. As far as this layer is concerned, 'instance ID' and + 'instance name' are synonyms. + + Note that the instance ID or name is not human-readable or + customer-controlled -- it's an internal ID chosen by Nova. At the + nova.virt layer, instances do not have human-readable names at all -- such + things are only known higher up the stack. + + Most virtualization platforms will also have their own identity schemes, + to uniquely identify a VM or domain. These IDs must stay internal to the + platform-specific layer, and never escape the connection interface. The + platform-specific layer is responsible for keeping track of which instance + ID maps to which platform-specific ID, and vice versa. + + In contrast, the list_disks and list_interfaces calls may return + platform-specific IDs. These identify a specific virtual disk or specific + virtual network interface, and these IDs are opaque to the rest of Nova. + + Some methods here take an instance of nova.compute.service.Instance. This + is the datastructure used by nova.compute to store details regarding an + instance, and pass them into this layer. This layer is responsible for + translating that generic datastructure into terms that are specific to the + virtualization platform. + """ def init_host(self, host): - """Adopt existing VM's running here""" + """Initialize anything that is necessary for the driver to function, + including catching up with currently running VM's on the given host.""" # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() @@ -74,6 +104,7 @@ class ComputeDriver(object): """Get the current status of an instance, by name (not ID!) Returns a dict containing: + :state: the running state, one of the power_state codes :max_mem: (int) the maximum memory in KBytes allowed :mem: (int) the memory in KBytes used by the domain @@ -84,6 +115,10 @@ class ComputeDriver(object): raise NotImplementedError() def list_instances(self): + """ + Return the names of all the instances known to the virtualization + layer, as a list. + """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() @@ -94,28 +129,53 @@ class ComputeDriver(object): def spawn(self, context, instance, network_info=None, block_device_info=None): - """Launch a VM for the specified instance""" + """ + Create a new instance/VM/domain on the virtualization platform. + + Once this successfully completes, the instance should be + running (power_state.RUNNING). + + 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. + + :param context: security context + :param instance: Instance of {nova.compute.service.Instance}. + This function should use the data there to guide + the creation of the new instance. + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param block_device_info: + """ raise NotImplementedError() def destroy(self, instance, network_info, cleanup=True): """Destroy (shutdown and delete) the specified instance. 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 - task that allows the caller to detect when it is complete. If the instance is not found (for example if networking failed), this function should still succeed. It's probably a good idea to log a warning in that case. + :param instance: Instance of {nova.compute.service.Instance} and so + the instance is being specified as instance.name. + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param cleanup: + """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def reboot(self, instance, network_info): - """Reboot specified VM""" + """Reboot the specified instance. + + :param instance: Instance of {nova.compute.service.Instance} and so + the instance is being specified as instance.name. + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() @@ -140,31 +200,60 @@ class ComputeDriver(object): raise NotImplementedError() def get_host_ip_addr(self): + """ + Retrieves the IP address of the dom0 + """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def attach_volume(self, context, instance_id, volume_id, mountpoint): + """Attach the disk at device_path to the instance at mountpoint""" raise NotImplementedError() def detach_volume(self, context, instance_id, volume_id): + """Detach the disk attached to the instance at mountpoint""" raise NotImplementedError() - def compare_cpu(self, context, cpu_info): + def compare_cpu(self, cpu_info): + """Compares given cpu info against host + + Before attempting to migrate a VM to this host, + compare_cpu is called to ensure that the VM will + actually run here. + + :param cpu_info: (str) JSON structure describing the source CPU. + :returns: None if migration is acceptable + :raises: :py:class:`~nova.exception.InvalidCPUInfo` if migration + is not acceptable. + """ raise NotImplementedError() def migrate_disk_and_power_off(self, instance, dest): - """Transfers the VHD of a running instance to another host, then shuts - off the instance copies over the COW disk""" + """ + Transfers the disk of a running instance in multiple phases, turning + off the instance before the end. + """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def snapshot(self, context, instance, image_id): - """Create snapshot from a running VM instance.""" + """ + Snapshots the specified instance. + + The given parameter is an instance of nova.compute.service.Instance, + and so the instance is being specified as instance.name. + + The second parameter is the name of the snapshot. + """ raise NotImplementedError() def finish_migration(self, context, instance, disk_info, network_info, resize_instance): - """Completes a resize, turning on the migrated instance""" + """Completes a resize, turning on the migrated instance + + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + """ raise NotImplementedError() def revert_migration(self, instance): @@ -173,7 +262,7 @@ class ComputeDriver(object): raise NotImplementedError() def pause(self, instance, callback): - """Pause VM instance""" + """Pause the specified instance.""" # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() @@ -218,15 +307,15 @@ class ComputeDriver(object): post_method, recover_method): """Spawning live_migration operation for distributing high-load. - :params ctxt: security context - :params instance_ref: + :param ctxt: security context + :param instance_ref: nova.db.sqlalchemy.models.Instance object instance object that is migrated. - :params dest: destination host - :params post_method: + :param dest: destination host + :param post_method: post operation method. expected nova.compute.manager.post_live_migration. - :params recover_method: + :param recover_method: recovery method when any exception occurs. expected nova.compute.manager.recover_live_migration. @@ -235,15 +324,69 @@ class ComputeDriver(object): raise NotImplementedError() def refresh_security_group_rules(self, security_group_id): + """This method is called after a change to security groups. + + All security groups and their associated rules live in the datastore, + and calling this method should apply the updated rules to instances + running the specified security group. + + An error should be raised if the operation cannot complete. + + """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def refresh_security_group_members(self, security_group_id): + """This method is called when a security group is added to an instance. + + This message is sent to the virtualization drivers on hosts that are + running an instance that belongs to a security group that has a rule + that references the security group identified by `security_group_id`. + It is the responsiblity of this method to make sure any rules + that authorize traffic flow with members of the security group are + updated and any new members can communicate, and any removed members + cannot. + + Scenario: + * we are running on host 'H0' and we have an instance 'i-0'. + * instance 'i-0' is a member of security group 'speaks-b' + * group 'speaks-b' has an ingress rule that authorizes group 'b' + * another host 'H1' runs an instance 'i-1' + * instance 'i-1' is a member of security group 'b' + + When 'i-1' launches or terminates we will recieve the message + to update members of group 'b', at which time we will make + any changes needed to the rules for instance 'i-0' to allow + or deny traffic coming from 'i-1', depending on if it is being + added or removed from the group. + + In this scenario, 'i-1' could just as easily have been running on our + host 'H0' and this method would still have been called. The point was + that this method isn't called on the host where instances of that + group are running (as is the case with + :method:`refresh_security_group_rules`) but is called where references + are made to authorizing those instances. + + An error should be raised if the operation cannot complete. + + """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def refresh_provider_fw_rules(self, security_group_id): - """See: nova/virt/fake.py for docs.""" + """This triggers a firewall update based on database changes. + + When this is called, rules have either been added or removed from the + datastore. You can retrieve rules with + :method:`nova.db.api.provider_fw_rule_get_all`. + + Provider rules take precedence over security group rules. If an IP + would be allowed by a security group ingress rule, but blocked by + a provider rule, then packets from the IP are dropped. This includes + intra-project traffic in the case of the allow_project_net_traffic + flag for the libvirt-derived classes. + + """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() @@ -284,18 +427,38 @@ class ComputeDriver(object): raise NotImplementedError() def set_admin_password(self, context, instance_id, new_pass=None): - """Set the root/admin password for an instance on this server.""" + """ + Set the root password on the specified instance. + + The first parameter is an instance of nova.compute.service.Instance, + and so the instance is being specified as instance.name. The second + parameter is the value of the new password. + """ raise NotImplementedError() def inject_file(self, instance, b64_path, b64_contents): - """Create a file on the VM instance. The file path and contents - should be base64-encoded. + """ + Writes a file on the specified instance. + + The first parameter is an instance of nova.compute.service.Instance, + and so the instance is being specified as instance.name. The second + parameter is the base64-encoded path to which the file is to be + written on the instance; the third is the contents of the file, also + base64-encoded. """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() def agent_update(self, instance, url, md5hash): - """Update agent on the VM instance.""" + """ + Update agent on the specified instance. + + The first parameter is an instance of nova.compute.service.Instance, + and so the instance is being specified as instance.name. The second + parameter is the URL of the agent to be fetched and updated on the + instance; the third is the md5 hash of the file for verification + purposes. + """ # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() @@ -322,3 +485,83 @@ class ComputeDriver(object): """Plugs in VIFs to networks.""" # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() + + def update_host_status(self): + """Refresh host stats""" + raise NotImplementedError() + + def get_host_stats(self, refresh=False): + """Return currently known host stats""" + raise NotImplementedError() + + def list_disks(self, instance_name): + """ + Return the IDs of all the virtual disks attached to the specified + instance, as a list. These IDs are opaque to the caller (they are + only useful for giving back to this layer as a parameter to + disk_stats). These IDs only need to be unique for a given instance. + + Note that this function takes an instance ID. + """ + raise NotImplementedError() + + def list_interfaces(self, instance_name): + """ + Return the IDs of all the virtual network interfaces attached to the + specified instance, as a list. These IDs are opaque to the caller + (they are only useful for giving back to this layer as a parameter to + interface_stats). These IDs only need to be unique for a given + instance. + + Note that this function takes an instance ID. + """ + raise NotImplementedError() + + def resize(self, instance, flavor): + """ + Resizes/Migrates the specified instance. + + The flavor parameter determines whether or not the instance RAM and + disk space are modified, and if so, to what size. + """ + raise NotImplementedError() + + def block_stats(self, instance_name, disk_id): + """ + Return performance counters associated with the given disk_id on the + given instance_name. These are returned as [rd_req, rd_bytes, wr_req, + wr_bytes, errs], where rd indicates read, wr indicates write, req is + the total number of I/O requests made, bytes is the total number of + bytes transferred, and errs is the number of requests held up due to a + full pipeline. + + All counters are long integers. + + This method is optional. On some platforms (e.g. XenAPI) performance + statistics can be retrieved directly in aggregate form, without Nova + having to do the aggregation. On those platforms, this method is + unused. + + Note that this function takes an instance ID. + """ + raise NotImplementedError() + + def interface_stats(self, instance_name, iface_id): + """ + Return performance counters associated with the given iface_id on the + given instance_id. These are returned as [rx_bytes, rx_packets, + rx_errs, rx_drop, tx_bytes, tx_packets, tx_errs, tx_drop], where rx + indicates receive, tx indicates transmit, bytes and packets indicate + the total number of bytes or packets transferred, and errs and dropped + is the total number of packets failed / dropped. + + All counters are long integers. + + This method is optional. On some platforms (e.g. XenAPI) performance + statistics can be retrieved directly in aggregate form, without Nova + having to do the aggregation. On those platforms, this method is + unused. + + Note that this function takes an instance ID. + """ + raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index dc06287723ad..13b7aeab5ff9 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -48,37 +48,7 @@ class FakeInstance(object): class FakeConnection(driver.ComputeDriver): - """ - The interface to this class talks in terms of 'instances' (Amazon EC2 and - internal Nova terminology), by which we mean 'running virtual machine' - (XenAPI terminology) or domain (Xen or libvirt terminology). - - An instance has an ID, which is the identifier chosen by Nova to represent - the instance further up the stack. This is unfortunately also called a - 'name' elsewhere. As far as this layer is concerned, 'instance ID' and - 'instance name' are synonyms. - - Note that the instance ID or name is not human-readable or - customer-controlled -- it's an internal ID chosen by Nova. At the - nova.virt layer, instances do not have human-readable names at all -- such - things are only known higher up the stack. - - Most virtualization platforms will also have their own identity schemes, - to uniquely identify a VM or domain. These IDs must stay internal to the - platform-specific layer, and never escape the connection interface. The - platform-specific layer is responsible for keeping track of which instance - ID maps to which platform-specific ID, and vice versa. - - In contrast, the list_disks and list_interfaces calls may return - platform-specific IDs. These identify a specific virtual disk or specific - virtual network interface, and these IDs are opaque to the rest of Nova. - - Some methods here take an instance of nova.compute.service.Instance. This - is the datastructure used by nova.compute to store details regarding an - instance, and pass them into this layer. This layer is responsible for - translating that generic datastructure into terms that are specific to the - virtualization platform. - """ + """Fake hypervisor driver""" def __init__(self): self.instances = {} @@ -105,17 +75,9 @@ class FakeConnection(driver.ComputeDriver): return cls._instance def init_host(self, host): - """ - Initialize anything that is necessary for the driver to function, - including catching up with currently running VM's on the given host. - """ return def list_instances(self): - """ - Return the names of all the instances known to the virtualization - layer, as a list. - """ return self.instances.keys() def _map_to_instance_info(self, instance): @@ -131,167 +93,54 @@ class FakeConnection(driver.ComputeDriver): def spawn(self, context, instance, network_info=None, block_device_info=None): - """ - Create a new instance/VM/domain on the virtualization platform. - - The given parameter is an instance of nova.compute.service.Instance. - This function should use the data there to guide the creation of - the new instance. - - The work will be done asynchronously. This function returns a - task that allows the caller to detect when it is complete. - - Once this successfully completes, the instance should be - running (power_state.RUNNING). - - 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. - """ - name = instance.name state = power_state.RUNNING fake_instance = FakeInstance(name, state) self.instances[name] = fake_instance def snapshot(self, context, instance, name): - """ - Snapshots the specified instance. - - The given parameter is an instance of nova.compute.service.Instance, - and so the instance is being specified as instance.name. - - The second parameter is the name of the snapshot. - - The work will be done asynchronously. This function returns a - task that allows the caller to detect when it is complete. - """ pass def reboot(self, instance, network_info): - """ - Reboot the specified instance. - - 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 - task that allows the caller to detect when it is complete. - """ pass def get_host_ip_addr(self): - """ - Retrieves the IP address of the dom0 - """ - pass + return '192.168.0.1' def resize(self, instance, flavor): - """ - Resizes/Migrates the specified instance. - - The flavor parameter determines whether or not the instance RAM and - disk space are modified, and if so, to what size. - - The work will be done asynchronously. This function returns a task - that allows the caller to detect when it is complete. - """ pass def set_admin_password(self, instance, new_pass): - """ - Set the root password on the specified instance. - - The first parameter is an instance of nova.compute.service.Instance, - and so the instance is being specified as instance.name. The second - parameter is the value of the new password. - - The work will be done asynchronously. This function returns a - task that allows the caller to detect when it is complete. - """ pass def inject_file(self, instance, b64_path, b64_contents): - """ - Writes a file on the specified instance. - - The first parameter is an instance of nova.compute.service.Instance, - and so the instance is being specified as instance.name. The second - parameter is the base64-encoded path to which the file is to be - written on the instance; the third is the contents of the file, also - base64-encoded. - - The work will be done asynchronously. This function returns a - task that allows the caller to detect when it is complete. - """ pass def agent_update(self, instance, url, md5hash): - """ - Update agent on the specified instance. - - The first parameter is an instance of nova.compute.service.Instance, - and so the instance is being specified as instance.name. The second - parameter is the URL of the agent to be fetched and updated on the - instance; the third is the md5 hash of the file for verification - purposes. - - The work will be done asynchronously. This function returns a - task that allows the caller to detect when it is complete. - """ pass def rescue(self, context, instance, callback, network_info): - """ - Rescue the specified instance. - """ pass def unrescue(self, instance, callback, network_info): - """ - Unrescue the specified instance. - """ pass def poll_rescued_instances(self, timeout): - """Poll for rescued instances""" pass def migrate_disk_and_power_off(self, instance, dest): - """ - Transfers the disk of a running instance in multiple phases, turning - off the instance before the end. - """ - pass - - def attach_disk(self, instance, disk_info): - """ - Attaches the disk to an instance given the metadata disk_info - """ pass def pause(self, instance, callback): - """ - Pause the specified instance. - """ pass def unpause(self, instance, callback): - """ - Unpause the specified instance. - """ pass def suspend(self, instance, callback): - """ - suspend the specified instance - """ pass def resume(self, instance, callback): - """ - resume the specified instance - """ pass def destroy(self, instance, network_info, cleanup=True): @@ -303,25 +152,12 @@ class FakeConnection(driver.ComputeDriver): (key, self.instances)) def attach_volume(self, instance_name, device_path, mountpoint): - """Attach the disk at device_path to the instance at mountpoint""" return True def detach_volume(self, instance_name, mountpoint): - """Detach the disk attached to the instance at mountpoint""" return True def get_info(self, instance_name): - """ - Get a block of information about the given instance. This is returned - as a dictionary containing 'state': The power_state of the instance, - 'max_mem': The maximum memory for the instance, in KiB, 'mem': The - current memory the instance has, in KiB, 'num_cpu': The current number - of virtual CPUs the instance has, 'cpu_time': The total CPU time used - by the instance, in nanoseconds. - - This method should raise exception.NotFound if the hypervisor has no - knowledge of the instance - """ if instance_name not in self.instances: raise exception.InstanceNotFound(instance_id=instance_name) i = self.instances[instance_name] @@ -332,69 +168,18 @@ class FakeConnection(driver.ComputeDriver): 'cpu_time': 0} def get_diagnostics(self, instance_name): - pass + return {} def list_disks(self, instance_name): - """ - Return the IDs of all the virtual disks attached to the specified - instance, as a list. These IDs are opaque to the caller (they are - only useful for giving back to this layer as a parameter to - disk_stats). These IDs only need to be unique for a given instance. - - Note that this function takes an instance ID. - """ return ['A_DISK'] def list_interfaces(self, instance_name): - """ - Return the IDs of all the virtual network interfaces attached to the - specified instance, as a list. These IDs are opaque to the caller - (they are only useful for giving back to this layer as a parameter to - interface_stats). These IDs only need to be unique for a given - instance. - - Note that this function takes an instance ID. - """ return ['A_VIF'] def block_stats(self, instance_name, disk_id): - """ - Return performance counters associated with the given disk_id on the - given instance_name. These are returned as [rd_req, rd_bytes, wr_req, - wr_bytes, errs], where rd indicates read, wr indicates write, req is - the total number of I/O requests made, bytes is the total number of - bytes transferred, and errs is the number of requests held up due to a - full pipeline. - - All counters are long integers. - - This method is optional. On some platforms (e.g. XenAPI) performance - statistics can be retrieved directly in aggregate form, without Nova - having to do the aggregation. On those platforms, this method is - unused. - - Note that this function takes an instance ID. - """ return [0L, 0L, 0L, 0L, None] def interface_stats(self, instance_name, iface_id): - """ - Return performance counters associated with the given iface_id on the - given instance_id. These are returned as [rx_bytes, rx_packets, - rx_errs, rx_drop, tx_bytes, tx_packets, tx_errs, tx_drop], where rx - indicates receive, tx indicates transmit, bytes and packets indicate - the total number of bytes or packets transferred, and errs and dropped - is the total number of packets failed / dropped. - - All counters are long integers. - - This method is optional. On some platforms (e.g. XenAPI) performance - statistics can be retrieved directly in aggregate form, without Nova - having to do the aggregation. On those platforms, this method is - unused. - - Note that this function takes an instance ID. - """ return [0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L] def get_console_output(self, instance): @@ -416,67 +201,12 @@ class FakeConnection(driver.ComputeDriver): 'password': 'fakepassword'} def refresh_security_group_rules(self, security_group_id): - """This method is called after a change to security groups. - - All security groups and their associated rules live in the datastore, - and calling this method should apply the updated rules to instances - running the specified security group. - - An error should be raised if the operation cannot complete. - - """ return True def refresh_security_group_members(self, security_group_id): - """This method is called when a security group is added to an instance. - - This message is sent to the virtualization drivers on hosts that are - running an instance that belongs to a security group that has a rule - that references the security group identified by `security_group_id`. - It is the responsiblity of this method to make sure any rules - that authorize traffic flow with members of the security group are - updated and any new members can communicate, and any removed members - cannot. - - Scenario: - * we are running on host 'H0' and we have an instance 'i-0'. - * instance 'i-0' is a member of security group 'speaks-b' - * group 'speaks-b' has an ingress rule that authorizes group 'b' - * another host 'H1' runs an instance 'i-1' - * instance 'i-1' is a member of security group 'b' - - When 'i-1' launches or terminates we will recieve the message - to update members of group 'b', at which time we will make - any changes needed to the rules for instance 'i-0' to allow - or deny traffic coming from 'i-1', depending on if it is being - added or removed from the group. - - In this scenario, 'i-1' could just as easily have been running on our - host 'H0' and this method would still have been called. The point was - that this method isn't called on the host where instances of that - group are running (as is the case with - :method:`refresh_security_group_rules`) but is called where references - are made to authorizing those instances. - - An error should be raised if the operation cannot complete. - - """ return True def refresh_provider_fw_rules(self): - """This triggers a firewall update based on database changes. - - When this is called, rules have either been added or removed from the - datastore. You can retrieve rules with - :method:`nova.db.api.provider_fw_rule_get_all`. - - Provider rules take precedence over security group rules. If an IP - would be allowed by a security group ingress rule, but blocked by - a provider rule, then packets from the IP are dropped. This includes - intra-project traffic in the case of the allow_project_net_traffic - flag for the libvirt-derived classes. - - """ pass def update_available_resource(self, ctxt, host): diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index 210e2b0fb111..6a02cfa24bb7 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -106,6 +106,13 @@ #end for #end if + #if $getVar('config_drive', False) + + + + + + #end if #end if #for $nic in $nics diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index e8a657bac02c..4388291db5f9 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -4,6 +4,7 @@ # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # Copyright (c) 2010 Citrix Systems, Inc. +# Copyright (c) 2011 Piston Cloud Computing, Inc # # 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 @@ -130,6 +131,10 @@ flags.DEFINE_string('libvirt_vif_type', 'bridge', flags.DEFINE_string('libvirt_vif_driver', 'nova.virt.libvirt.vif.LibvirtBridgeDriver', 'The libvirt VIF driver to configure the VIFs.') +flags.DEFINE_string('default_local_format', + None, + 'The default format a local_volume will be formatted with ' + 'on creation.') def get_connection(read_only): @@ -586,6 +591,7 @@ class LibvirtConnection(driver.ComputeDriver): self.firewall_driver.prepare_instance_filter(instance, network_info) self._create_image(context, instance, xml, network_info=network_info, block_device_info=block_device_info) + domain = self._create_new_domain(xml) LOG.debug(_("instance %s: is running"), instance['name']) self.firewall_driver.apply_instance_filter(instance, network_info) @@ -759,10 +765,15 @@ class LibvirtConnection(driver.ComputeDriver): if size: disk.extend(target, size) - def _create_local(self, target, local_gb): + def _create_local(self, target, local_size, prefix='G', fs_format=None): """Create a blank image of specified size""" - utils.execute('truncate', target, '-s', "%dG" % local_gb) - # TODO(vish): should we format disk by default? + + if not fs_format: + fs_format = FLAGS.default_local_format + + utils.execute('truncate', target, '-s', "%d%c" % (local_size, prefix)) + if fs_format: + utils.execute('mkfs', '-t', fs_format, target) def _create_swap(self, target, swap_gb): """Create a swap file of specified size""" @@ -849,14 +860,14 @@ class LibvirtConnection(driver.ComputeDriver): target=basepath('disk.local'), fname="local_%s" % local_gb, cow=FLAGS.use_cow_images, - local_gb=local_gb) + local_size=local_gb) for eph in driver.block_device_info_get_ephemerals(block_device_info): self._cache_image(fn=self._create_local, target=basepath(_get_eph_disk(eph)), fname="local_%s" % eph['size'], cow=FLAGS.use_cow_images, - local_gb=eph['size']) + local_size=eph['size']) swap_gb = 0 @@ -882,9 +893,24 @@ class LibvirtConnection(driver.ComputeDriver): if not inst['kernel_id']: target_partition = "1" - if FLAGS.libvirt_type == 'lxc': + config_drive_id = inst.get('config_drive_id') + config_drive = inst.get('config_drive') + + if any((FLAGS.libvirt_type == 'lxc', config_drive, config_drive_id)): target_partition = None + if config_drive_id: + fname = '%08x' % int(config_drive_id) + self._cache_image(fn=self._fetch_image, + target=basepath('disk.config'), + fname=fname, + image_id=config_drive_id, + user=user, + project=project) + elif config_drive: + self._create_local(basepath('disk.config'), 64, prefix="M", + fs_format='msdos') # 64MB + if inst['key_data']: key = str(inst['key_data']) else: @@ -928,19 +954,29 @@ class LibvirtConnection(driver.ComputeDriver): searchList=[{'interfaces': nets, 'use_ipv6': FLAGS.use_ipv6}])) - if key or net: + metadata = inst.get('metadata') + if any((key, net, metadata)): inst_name = inst['name'] - img_id = inst.image_ref - if key: - LOG.info(_('instance %(inst_name)s: injecting key into' - ' image %(img_id)s') % locals()) - if net: - LOG.info(_('instance %(inst_name)s: injecting net into' - ' image %(img_id)s') % locals()) + + if config_drive: # Should be True or None by now. + injection_path = basepath('disk.config') + img_id = 'config-drive' + tune2fs = False + else: + injection_path = basepath('disk') + img_id = inst.image_ref + tune2fs = True + + for injection in ('metadata', 'key', 'net'): + if locals()[injection]: + LOG.info(_('instance %(inst_name)s: injecting ' + '%(injection)s into image %(img_id)s' + % locals())) try: - disk.inject_data(basepath('disk'), key, net, + disk.inject_data(injection_path, key, net, metadata, partition=target_partition, - nbd=FLAGS.use_cow_images) + nbd=FLAGS.use_cow_images, + tune2fs=tune2fs) if FLAGS.libvirt_type == 'lxc': disk.setup_container(basepath('disk'), @@ -1070,6 +1106,10 @@ class LibvirtConnection(driver.ComputeDriver): block_device_info)): xml_info['swap_device'] = self.default_swap_device + config_drive = False + if instance.get('config_drive') or instance.get('config_drive_id'): + xml_info['config_drive'] = xml_info['basepath'] + "/disk.config" + if FLAGS.vnc_enabled and FLAGS.libvirt_type not in ('lxc', 'uml'): xml_info['vncserver_host'] = FLAGS.vncserver_host xml_info['vnc_keymap'] = FLAGS.vnc_keymap diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index 5a91a4e2837f..0b74380119cc 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -100,10 +100,12 @@ class LibvirtBridgeDriver(VIFDriver): class LibvirtOpenVswitchDriver(VIFDriver): """VIF driver for Open vSwitch.""" + def get_dev_name(_self, iface_id): + return "tap-" + iface_id[0:15] + def plug(self, instance, network, mapping): - vif_id = str(instance['id']) + "-" + str(network['id']) - dev = "tap-%s" % vif_id - iface_id = "nova-" + vif_id + iface_id = mapping['vif_uuid'] + dev = self.get_dev_name(iface_id) if not linux_net._device_exists(dev): utils.execute('ip', 'tuntap', 'add', dev, 'mode', 'tap', run_as_root=True) @@ -127,11 +129,10 @@ class LibvirtOpenVswitchDriver(VIFDriver): def unplug(self, instance, network, mapping): """Unplug the VIF from the network by deleting the port from the bridge.""" - vif_id = str(instance['id']) + "-" + str(network['id']) - dev = "tap-%s" % vif_id + dev = self.get_dev_name(mapping['vif_uuid']) try: utils.execute('ovs-vsctl', 'del-port', - network['bridge'], dev, run_as_root=True) + FLAGS.libvirt_ovs_bridge, dev, run_as_root=True) utils.execute('ip', 'link', 'delete', dev, run_as_root=True) except exception.ProcessExecutionError: LOG.warning(_("Failed while unplugging vif of instance '%s'"), diff --git a/nova/virt/xenapi/vif.py b/nova/virt/xenapi/vif.py index 5276022439f8..2f25efeb2b73 100644 --- a/nova/virt/xenapi/vif.py +++ b/nova/virt/xenapi/vif.py @@ -128,12 +128,12 @@ class XenAPIOpenVswitchDriver(VIFDriver): vif_rec['VM'] = vm_ref vif_rec['MAC'] = network_mapping['mac'] vif_rec['MTU'] = '1500' - vif_id = "nova-" + str(instance['id']) + "-" + str(network['id']) vif_rec['qos_algorithm_type'] = "" vif_rec['qos_algorithm_params'] = {} # OVS on the hypervisor monitors this key and uses it to # set the iface-id attribute - vif_rec['other_config'] = {"nicira-iface-id": vif_id} + vif_rec['other_config'] = \ + {"nicira-iface-id": network_mapping['vif_uuid']} return vif_rec def unplug(self, instance, network, mapping): diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 4a1f07bb1d40..efbea7076f62 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright (c) 2010 Citrix Systems, Inc. +# Copyright 2011 Piston Cloud Computing, Inc. # # 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 @@ -740,13 +741,14 @@ class VMHelper(HelperBase): # if at all, so determine whether it's required first, and then do # everything mount_required = False - key, net = _prepare_injectables(instance, network_info) - mount_required = key or net + key, net, metadata = _prepare_injectables(instance, network_info) + mount_required = key or net or metadata if not mount_required: return with_vdi_attached_here(session, vdi_ref, False, - lambda dev: _mounted_processing(dev, key, net)) + lambda dev: _mounted_processing(dev, key, net, + metadata)) @classmethod def lookup_kernel_ramdisk(cls, session, vm): @@ -1198,7 +1200,7 @@ def _find_guest_agent(base_dir, agent_rel_path): return False -def _mounted_processing(device, key, net): +def _mounted_processing(device, key, net, metadata): """Callback which runs with the image VDI attached""" dev_path = '/dev/' + device + '1' # NB: Partition 1 hardcoded @@ -1212,7 +1214,7 @@ def _mounted_processing(device, key, net): if not _find_guest_agent(tmpdir, FLAGS.xenapi_agent_path): LOG.info(_('Manipulating interface files ' 'directly')) - disk.inject_data_into_fs(tmpdir, key, net, + disk.inject_data_into_fs(tmpdir, key, net, metadata, utils.execute) finally: utils.execute('umount', dev_path, run_as_root=True) @@ -1235,6 +1237,7 @@ def _prepare_injectables(inst, networks_info): template = t.Template template_data = open(FLAGS.injected_network_template).read() + metadata = inst['metadata'] key = str(inst['key_data']) net = None if networks_info: @@ -1272,4 +1275,4 @@ def _prepare_injectables(inst, networks_info): net = str(template(template_data, searchList=[{'interfaces': interfaces_info, 'use_ipv6': FLAGS.use_ipv6}])) - return key, net + return key, net, metadata diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index eb0a846b5ccc..64c106f4759a 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -239,8 +239,9 @@ class VMOps(object): self._attach_disks(instance, disk_image_type, vm_ref, first_vdi_ref, vdis) - # Alter the image before VM start for, e.g. network injection - if FLAGS.flat_injected: + # Alter the image before VM start for, e.g. network injection also + # alter the image if there's metadata. + if FLAGS.flat_injected or instance['metadata']: VMHelper.preconfigure_instance(self._session, instance, first_vdi_ref, network_info) @@ -709,9 +710,6 @@ class VMOps(object): if resp['returncode'] != '0': LOG.error(_('Failed to update password: %(resp)r') % locals()) return None - db.instance_update(nova_context.get_admin_context(), - instance['id'], - dict(admin_pass=new_pass)) return resp['message'] def inject_file(self, instance, path, contents): diff --git a/nova/wsgi.py b/nova/wsgi.py index c8ddb97d7f0c..09b45be5a645 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -39,9 +39,6 @@ from nova import log as logging from nova import utils -eventlet.patcher.monkey_patch(socket=True, time=True) - - FLAGS = flags.FLAGS LOG = logging.getLogger('nova.wsgi') @@ -274,18 +271,6 @@ class Middleware(Application): return self.process_response(response) -class InjectContext(Middleware): - """Add a 'nova.context' to WSGI environ.""" - def __init__(self, context, *args, **kwargs): - self.context = context - super(InjectContext, self).__init__(*args, **kwargs) - - @webob.dec.wsgify(RequestClass=Request) - def __call__(self, req): - req.environ['nova.context'] = self.context - return self.application - - class Debug(Middleware): """Helper class for debugging a WSGI application. diff --git a/po/ast.po b/po/ast.po index 449cddb07579..48682ec90954 100644 --- a/po/ast.po +++ b/po/ast.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:11+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:43+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 diff --git a/po/cs.po b/po/cs.po index 2dc763838805..07bdf1928149 100644 --- a/po/cs.po +++ b/po/cs.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:11+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:43+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 diff --git a/po/da.po b/po/da.po index 570629119940..0b379c9d7f4f 100644 --- a/po/da.po +++ b/po/da.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:43+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 diff --git a/po/de.po b/po/de.po index 772ae236c646..1f652c373644 100644 --- a/po/de.po +++ b/po/de.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2833,3 +2833,21 @@ msgstr "" #~ msgid "Data store %s is unreachable. Trying again in %d seconds." #~ msgstr "" #~ "Datastore %s ist nicht erreichbar. Versuche es erneut in %d Sekunden." + +#~ msgid "Full set of FLAGS:" +#~ msgstr "Alle vorhandenen FLAGS:" + +#, python-format +#~ msgid "pidfile %s does not exist. Daemon not running?\n" +#~ msgstr "PID-Datei %s existiert nicht. Läuft der Daemon nicht?\n" + +#, python-format +#~ msgid "Starting %s" +#~ msgstr "%s wird gestartet" + +#~ msgid "No such process" +#~ msgstr "Kein passender Prozess gefunden" + +#, python-format +#~ msgid "Serving %s" +#~ msgstr "Bedient %s" diff --git a/po/en_AU.po b/po/en_AU.po index 3fa62c006400..a51b9ff2de1f 100644 --- a/po/en_AU.po +++ b/po/en_AU.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 diff --git a/po/en_GB.po b/po/en_GB.po index b204c93a1487..59247f4fae0b 100644 --- a/po/en_GB.po +++ b/po/en_GB.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2812,3 +2812,24 @@ msgstr "" #, python-format msgid "Removing user %(user)s from project %(project)s" msgstr "" + +#~ msgid "Wrong number of arguments." +#~ msgstr "Wrong number of arguments." + +#~ msgid "No such process" +#~ msgstr "No such process" + +#~ msgid "Full set of FLAGS:" +#~ msgstr "Full set of FLAGS:" + +#, python-format +#~ msgid "pidfile %s does not exist. Daemon not running?\n" +#~ msgstr "pidfile %s does not exist. Daemon not running?\n" + +#, python-format +#~ msgid "Starting %s" +#~ msgstr "Starting %s" + +#, python-format +#~ msgid "Serving %s" +#~ msgstr "Serving %s" diff --git a/po/es.po b/po/es.po index f97434041e85..7371eae8ca8f 100644 --- a/po/es.po +++ b/po/es.po @@ -8,20 +8,20 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-06-30 16:42+0000\n" -"Last-Translator: David Caro \n" +"PO-Revision-Date: 2011-08-01 03:23+0000\n" +"Last-Translator: Juan Alfredo Salas Santillana \n" "Language-Team: Spanish \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 #: ../nova/scheduler/simple.py:122 msgid "No hosts found" -msgstr "No se han encontrado hosts" +msgstr "No se encontro anfitriones." #: ../nova/exception.py:33 msgid "Unexpected error while running command." @@ -2566,7 +2566,7 @@ msgstr "" #: ../nova/auth/manager.py:289 #, python-format msgid "User %(uid)s is not a member of project %(pjid)s" -msgstr "" +msgstr "El usuario %(uid)s no es miembro del proyecto %(pjid)s" #: ../nova/auth/manager.py:298 ../nova/auth/manager.py:309 #, python-format @@ -2584,7 +2584,7 @@ msgstr "Debes especificar un proyecto" #: ../nova/auth/manager.py:414 #, python-format msgid "The %s role can not be found" -msgstr "El rol %s no se ha podido encontrar" +msgstr "" #: ../nova/auth/manager.py:416 #, python-format @@ -2614,27 +2614,27 @@ msgstr "" #: ../nova/auth/manager.py:515 #, python-format msgid "Created project %(name)s with manager %(manager_user)s" -msgstr "" +msgstr "Creado el proyecto %(name)s con administrador %(manager_user)s" #: ../nova/auth/manager.py:533 #, python-format msgid "modifying project %s" -msgstr "modificando proyecto %s" +msgstr "Modificando proyecto %s" #: ../nova/auth/manager.py:545 #, python-format msgid "Adding user %(uid)s to project %(pid)s" -msgstr "" +msgstr "Agregando usuario %(uid)s para el proyecto %(pid)s" #: ../nova/auth/manager.py:566 #, python-format msgid "Remove user %(uid)s from project %(pid)s" -msgstr "" +msgstr "Borrar usuario %(uid)s del proyecto %(pid)s" #: ../nova/auth/manager.py:592 #, python-format msgid "Deleting project %s" -msgstr "Eliminando proyecto %s" +msgstr "Borrando proyecto %s" #: ../nova/auth/manager.py:650 #, python-format @@ -2644,7 +2644,7 @@ msgstr "" #: ../nova/auth/manager.py:659 #, python-format msgid "Deleting user %s" -msgstr "Eliminando usuario %s" +msgstr "Borrando usuario %s" #: ../nova/auth/manager.py:669 #, python-format @@ -2710,7 +2710,7 @@ msgstr "" #: ../nova/auth/ldapdriver.py:478 #, python-format msgid "Group can't be created because user %s doesn't exist" -msgstr "" +msgstr "El grupo no se puede crear porque el usuario %s no existe" #: ../nova/auth/ldapdriver.py:495 #, python-format @@ -2730,18 +2730,20 @@ msgstr "" #: ../nova/auth/ldapdriver.py:513 #, python-format msgid "User %(uid)s is already a member of the group %(group_dn)s" -msgstr "" +msgstr "El usuario %(uid)s es actualmente miembro del grupo %(group_dn)s" #: ../nova/auth/ldapdriver.py:524 #, python-format msgid "" "User %s can't be removed from the group because the user doesn't exist" msgstr "" +"El usuario %s no se pudo borrar de el grupo a causa de que el usuario no " +"existe" #: ../nova/auth/ldapdriver.py:528 #, python-format msgid "User %s is not a member of the group" -msgstr "" +msgstr "El usuario %s no es miembro de el grupo" #: ../nova/auth/ldapdriver.py:542 #, python-format @@ -2877,6 +2879,10 @@ msgstr "Eliminando el usuario %(user)s del proyecto %(project)s" #~ msgstr "" #~ "El almacen de datos %s es inalcanzable. Reintentandolo en %d segundos." +#, python-format +#~ msgid "Serving %s" +#~ msgstr "Sirviendo %s" + #, python-format #~ msgid "Couldn't get IP, using 127.0.0.1 %s" #~ msgstr "No puedo obtener IP, usando 127.0.0.1 %s" @@ -3037,10 +3043,24 @@ msgstr "Eliminando el usuario %(user)s del proyecto %(project)s" #~ msgid "Detach volume %s from mountpoint %s on instance %s" #~ msgstr "Desvinculando volumen %s del punto de montaje %s en la instancia %s" +#~ msgid "unexpected exception getting connection" +#~ msgstr "excepción inexperada al obtener la conexión" + +#~ msgid "unexpected error during update" +#~ msgstr "error inesperado durante la actualización" + #, python-format #~ msgid "Cannot get blockstats for \"%s\" on \"%s\"" #~ msgstr "No puedo obtener estadísticas del bloque para \"%s\" en \"%s\"" +#, python-format +#~ msgid "updating %s..." +#~ msgstr "actualizando %s..." + +#, python-format +#~ msgid "Found instance: %s" +#~ msgstr "Encontrada interfaz: %s" + #, python-format #~ msgid "Cannot get ifstats for \"%s\" on \"%s\"" #~ msgstr "No puedo obtener estadísticas de la interfaz para \"%s\" en \"%s\"" @@ -3319,3 +3339,20 @@ msgstr "Eliminando el usuario %(user)s del proyecto %(project)s" #, python-format #~ msgid "Spawning VM %s created %s." #~ msgstr "Iniciando VM %s creado %s." + +#~ msgid "No such process" +#~ msgstr "No existe el proceso" + +#~ msgid "Full set of FLAGS:" +#~ msgstr "Conjunto completo de opciones (FLAGS):" + +#~ msgid "Wrong number of arguments." +#~ msgstr "Cantidad de argumentos incorrecta" + +#, python-format +#~ msgid "pidfile %s does not exist. Daemon not running?\n" +#~ msgstr "El \"pidfile\" %s no existe. Quizás el servicio no este corriendo.\n" + +#, python-format +#~ msgid "Starting %s" +#~ msgstr "Iniciando %s" diff --git a/po/fr.po b/po/fr.po index 83e4e7af0de6..7cb298a942bc 100644 --- a/po/fr.po +++ b/po/fr.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:43+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2929,3 +2929,51 @@ msgstr "Ajout de l'utilisateur %(user)s au projet %(project)s" #, python-format msgid "Removing user %(user)s from project %(project)s" msgstr "Suppression de l'utilisateur %(user)s du projet %(project)s" + +#~ msgid "Wrong number of arguments." +#~ msgstr "Nombre d'arguments incorrect." + +#~ msgid "No such process" +#~ msgstr "Aucun processus de ce type" + +#, python-format +#~ msgid "Starting %s" +#~ msgstr "Démarrage de %s" + +#~ msgid "Full set of FLAGS:" +#~ msgstr "Ensemble de propriétés complet :" + +#, python-format +#~ msgid "pidfile %s does not exist. Daemon not running?\n" +#~ msgstr "" +#~ "Le fichier pid %s n'existe pas. Est-ce que le processus est en cours " +#~ "d'exécution ?\n" + +#, python-format +#~ msgid "Serving %s" +#~ msgstr "En train de servir %s" + +#, python-format +#~ msgid "Cannot get blockstats for \"%(disk)s\" on \"%(iid)s\"" +#~ msgstr "Ne peut pas récupérer blockstats pour \"%(disk)s\" sur \"%(iid)s\"" + +#, python-format +#~ msgid "Cannot get ifstats for \"%(interface)s\" on \"%(iid)s\"" +#~ msgstr "Ne peut pas récupérer ifstats pour \"%(interface)s\" sur \"%(iid)s\"" + +#~ msgid "unexpected error during update" +#~ msgstr "erreur inopinée pendant la ise à jour" + +#, python-format +#~ msgid "updating %s..." +#~ msgstr "mise à jour %s..." + +#, python-format +#~ msgid "Found instance: %s" +#~ msgstr "Instance trouvée : %s" + +#~ msgid "unexpected exception getting connection" +#~ msgstr "erreur inopinée pendant la connexion" + +#~ msgid "Starting instance monitor" +#~ msgstr "Démarrage du superviseur d'instance" diff --git a/po/it.po b/po/it.po index 6bfcf127437a..e166297f1c12 100644 --- a/po/it.po +++ b/po/it.po @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-02-22 19:34+0000\n" -"Last-Translator: Armando Migliaccio \n" +"PO-Revision-Date: 2011-08-21 22:50+0000\n" +"Last-Translator: Guido Davide Dall'Olio \n" "Language-Team: Italian \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-22 04:48+0000\n" +"X-Generator: Launchpad (build 13697)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -449,24 +449,24 @@ msgstr "" #: ../nova/scheduler/simple.py:53 #, python-format msgid "Host %s is not alive" -msgstr "" +msgstr "L'host %s non è attivo" #: ../nova/scheduler/simple.py:65 msgid "All hosts have too many cores" -msgstr "" +msgstr "Gli host hanno troppi core" #: ../nova/scheduler/simple.py:87 #, python-format msgid "Host %s not available" -msgstr "" +msgstr "Host %s non disponibile" #: ../nova/scheduler/simple.py:99 msgid "All hosts have too many gigabytes" -msgstr "" +msgstr "Gli Host hanno troppy gigabyte" #: ../nova/scheduler/simple.py:119 msgid "All hosts have too many networks" -msgstr "" +msgstr "Gli host hanno troppe reti" #: ../nova/volume/manager.py:85 #, python-format @@ -496,7 +496,7 @@ msgstr "" #: ../nova/volume/manager.py:123 #, python-format msgid "volume %s: created successfully" -msgstr "" +msgstr "volume %s: creato con successo" #: ../nova/volume/manager.py:131 msgid "Volume is still attached" @@ -514,12 +514,12 @@ msgstr "" #: ../nova/volume/manager.py:138 #, python-format msgid "volume %s: deleting" -msgstr "" +msgstr "volume %s: rimuovendo" #: ../nova/volume/manager.py:147 #, python-format msgid "volume %s: deleted successfully" -msgstr "" +msgstr "volume %s: rimosso con successo" #: ../nova/virt/xenapi/fake.py:74 #, python-format @@ -529,7 +529,7 @@ msgstr "" #: ../nova/virt/xenapi/fake.py:304 ../nova/virt/xenapi/fake.py:404 #: ../nova/virt/xenapi/fake.py:422 ../nova/virt/xenapi/fake.py:478 msgid "Raising NotImplemented" -msgstr "" +msgstr "Sollevando NotImplemented" #: ../nova/virt/xenapi/fake.py:306 #, python-format @@ -539,7 +539,7 @@ msgstr "" #: ../nova/virt/xenapi/fake.py:341 #, python-format msgid "Calling %(localname)s %(impl)s" -msgstr "" +msgstr "Chiamando %(localname)s %(impl)s" #: ../nova/virt/xenapi/fake.py:346 #, python-format @@ -564,17 +564,17 @@ msgstr "" #: ../nova/virt/connection.py:73 msgid "Failed to open connection to the hypervisor" -msgstr "" +msgstr "Fallita l'apertura della connessione verso l'hypervisor" #: ../nova/network/linux_net.py:187 #, python-format msgid "Starting VLAN inteface %s" -msgstr "" +msgstr "Avviando l'interfaccia VLAN %s" #: ../nova/network/linux_net.py:208 #, python-format msgid "Starting Bridge interface for %s" -msgstr "" +msgstr "Avviando l'interfaccia Bridge per %s" #. pylint: disable=W0703 #: ../nova/network/linux_net.py:314 @@ -632,7 +632,7 @@ msgstr "Il risultato é %s" #: ../nova/utils.py:159 #, python-format msgid "Running cmd (SSH): %s" -msgstr "" +msgstr "Eseguendo cmd (SSH): %s" #: ../nova/utils.py:217 #, python-format @@ -642,7 +642,7 @@ msgstr "debug in callback: %s" #: ../nova/utils.py:222 #, python-format msgid "Running %s" -msgstr "" +msgstr "Eseguendo %s" #: ../nova/utils.py:262 #, python-format @@ -697,12 +697,12 @@ msgstr "" #: ../nova/virt/xenapi/vm_utils.py:135 ../nova/virt/hyperv.py:171 #, python-format msgid "Created VM %s..." -msgstr "" +msgstr "Creata VM %s.." #: ../nova/virt/xenapi/vm_utils.py:138 #, python-format msgid "Created VM %(instance_name)s as %(vm_ref)s." -msgstr "" +msgstr "Creata VM %(instance_name)s come %(vm_ref)s" #: ../nova/virt/xenapi/vm_utils.py:168 #, python-format @@ -771,7 +771,7 @@ msgstr "" #: ../nova/virt/xenapi/vm_utils.py:332 #, python-format msgid "Glance image %s" -msgstr "" +msgstr "Immagine Glance %s" #. we need to invoke a plugin for copying VDI's #. content into proper path @@ -783,7 +783,7 @@ msgstr "" #: ../nova/virt/xenapi/vm_utils.py:352 #, python-format msgid "Kernel/Ramdisk VDI %s destroyed" -msgstr "" +msgstr "Kernel/Ramdisk VDI %s distrutti" #: ../nova/virt/xenapi/vm_utils.py:361 #, python-format @@ -793,7 +793,7 @@ msgstr "" #: ../nova/virt/xenapi/vm_utils.py:386 ../nova/virt/xenapi/vm_utils.py:402 #, python-format msgid "Looking up vdi %s for PV kernel" -msgstr "" +msgstr "Cercando vdi %s per kernel PV" #: ../nova/virt/xenapi/vm_utils.py:397 #, python-format @@ -2802,37 +2802,24 @@ msgstr "" msgid "Removing user %(user)s from project %(project)s" msgstr "" +#~ msgid "Full set of FLAGS:" +#~ msgstr "Insieme di FLAGS:" + #, python-format -#~ msgid "" -#~ "%s\n" -#~ "Command: %s\n" -#~ "Exit code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" +#~ msgid "pidfile %s does not exist. Daemon not running?\n" #~ msgstr "" -#~ "%s\n" -#~ "Comando: %s\n" -#~ "Exit code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" +#~ "Il pidfile %s non esiste. Assicurarsi che il demone é in esecuzione.\n" #, python-format -#~ msgid "(%s) publish (key: %s) %s" -#~ msgstr "(%s) pubblica (chiave: %s) %s" +#~ msgid "Starting %s" +#~ msgstr "Avvio di %s" #, python-format -#~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." -#~ msgstr "" -#~ "Il server AMQP su %s:%d non é raggiungibile. Riprovare in %d secondi." +#~ msgid "Serving %s" +#~ msgstr "Servire %s" -#, python-format -#~ msgid "Binding %s to %s with key %s" -#~ msgstr "Collegando %s a %s con la chiave %s" +#~ msgid "Wrong number of arguments." +#~ msgstr "Numero errato di argomenti" -#, python-format -#~ msgid "Starting %s node" -#~ msgstr "Avviando il nodo %s" - -#, python-format -#~ msgid "Data store %s is unreachable. Trying again in %d seconds." -#~ msgstr "Datastore %s é irrangiungibile. Riprovare in %d seconds." +#~ msgid "No such process" +#~ msgstr "Nessun processo trovato" diff --git a/po/ja.po b/po/ja.po index a7906ede83d7..179302b559d0 100644 --- a/po/ja.po +++ b/po/ja.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2878,6 +2878,17 @@ msgstr "ユーザ %(user)s をプロジェクト %(project)s から削除しま #~ msgid "Data store %s is unreachable. Trying again in %d seconds." #~ msgstr "データストア %s に接続できません。 %d 秒後に再接続します。" +#, python-format +#~ msgid "Serving %s" +#~ msgstr "%s サービスの開始" + +#~ msgid "Full set of FLAGS:" +#~ msgstr "FLAGSの一覧:" + +#, python-format +#~ msgid "pidfile %s does not exist. Daemon not running?\n" +#~ msgstr "pidfile %s が存在しません。デーモンは実行中ですか?\n" + #, python-format #~ msgid "Couldn't get IP, using 127.0.0.1 %s" #~ msgstr "IPを取得できません。127.0.0.1 を %s として使います。" @@ -3038,6 +3049,13 @@ msgstr "ユーザ %(user)s をプロジェクト %(project)s から削除しま #~ msgid "Detach volume %s from mountpoint %s on instance %s" #~ msgstr "Detach volume: ボリューム %s をマウントポイント %s (インスタンス%s)からデタッチします。" +#, python-format +#~ msgid "updating %s..." +#~ msgstr "%s の情報の更新…" + +#~ msgid "unexpected error during update" +#~ msgstr "更新の最中に予期しないエラーが発生しました。" + #, python-format #~ msgid "Cannot get blockstats for \"%s\" on \"%s\"" #~ msgstr "ブロックデバイス \"%s\" の統計を \"%s\" について取得できません。" @@ -3046,6 +3064,13 @@ msgstr "ユーザ %(user)s をプロジェクト %(project)s から削除しま #~ msgid "Cannot get ifstats for \"%s\" on \"%s\"" #~ msgstr "インタフェース \"%s\" の統計を \"%s\" について取得できません。" +#~ msgid "unexpected exception getting connection" +#~ msgstr "接続に際し予期しないエラーが発生しました。" + +#, python-format +#~ msgid "Found instance: %s" +#~ msgstr "インスタンス %s が見つかりました。" + #, python-format #~ msgid "No service for %s, %s" #~ msgstr "%s, %s のserviceが存在しません。" @@ -3318,3 +3343,24 @@ msgstr "ユーザ %(user)s をプロジェクト %(project)s から削除しま #, python-format #~ msgid "volume %s: creating lv of size %sG" #~ msgstr "ボリューム%sの%sGのlv (論理ボリューム) を作成します。" + +#~ msgid "Wrong number of arguments." +#~ msgstr "引数の数が異なります。" + +#~ msgid "No such process" +#~ msgstr "そのようなプロセスはありません" + +#, python-format +#~ msgid "Cannot get blockstats for \"%(disk)s\" on \"%(iid)s\"" +#~ msgstr "\"%(iid)s\" 上の \"%(disk)s\" 用のブロック統計(blockstats)が取得できません" + +#, python-format +#~ msgid "Cannot get ifstats for \"%(interface)s\" on \"%(iid)s\"" +#~ msgstr "\"%(iid)s\" 上の %(interface)s\" 用インターフェース統計(ifstats)が取得できません" + +#~ msgid "Starting instance monitor" +#~ msgstr "インスタンスモニタを開始しています" + +#, python-format +#~ msgid "Starting %s" +#~ msgstr "%s を起動中" diff --git a/po/pt_BR.po b/po/pt_BR.po index b3aefce44246..d6d57a9b1103 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-03-24 14:51+0000\n" +"PO-Revision-Date: 2011-07-25 17:40+0000\n" "Last-Translator: msinhore \n" "Language-Team: Brazilian Portuguese \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -36,6 +36,11 @@ msgid "" "Stdout: %(stdout)r\n" "Stderr: %(stderr)r" msgstr "" +"%(description)s\n" +"Comando: %(cmd)s\n" +"Código de saída: %(exit_code)s\n" +"Saída padrão: %(stdout)r\n" +"Erro: %(stderr)r" #: ../nova/exception.py:107 msgid "DB exception wrapped" @@ -392,7 +397,7 @@ msgstr "instância %s: suspendendo" #: ../nova/compute/manager.py:472 #, python-format msgid "instance %s: resuming" -msgstr "" +msgstr "instância %s: resumindo" #: ../nova/compute/manager.py:491 #, python-format @@ -407,12 +412,12 @@ msgstr "instância %s: desbloqueando" #: ../nova/compute/manager.py:513 #, python-format msgid "instance %s: getting locked state" -msgstr "" +msgstr "instância %s: obtendo estado de bloqueio" #: ../nova/compute/manager.py:526 #, python-format msgid "instance %s: reset network" -msgstr "" +msgstr "instância %s: reset da rede" #: ../nova/compute/manager.py:535 ../nova/api/ec2/cloud.py:515 #, python-format @@ -429,6 +434,7 @@ msgstr "instância %s: obtendo console ajax" msgid "" "instance %(instance_id)s: attaching volume %(volume_id)s to %(mountpoint)s" msgstr "" +"instância %(instance_id)s: atachando volume %(volume_id)s para %(mountpoint)s" #. pylint: disable=W0702 #. NOTE(vish): The inline callback eats the exception info so we @@ -438,6 +444,8 @@ msgstr "" #, python-format msgid "instance %(instance_id)s: attach failed %(mountpoint)s, removing" msgstr "" +"instância %(instance_id)s: falha ao atachar ponto de montagem " +"%(mountpoint)s, removendo" #: ../nova/compute/manager.py:585 #, python-format @@ -458,7 +466,7 @@ msgstr "Host %s não está ativo" #: ../nova/scheduler/simple.py:65 msgid "All hosts have too many cores" -msgstr "" +msgstr "Todos os hosts tem muitos núcleos de CPU" #: ../nova/scheduler/simple.py:87 #, python-format @@ -783,7 +791,7 @@ msgstr "Tamanho da imagem %(image)s:%(virtual_size)d" #: ../nova/virt/xenapi/vm_utils.py:332 #, python-format msgid "Glance image %s" -msgstr "" +msgstr "Visão geral da imagem %s" #. we need to invoke a plugin for copying VDI's #. content into proper path @@ -815,7 +823,7 @@ msgstr "Kernel PV no VDI: %s" #: ../nova/virt/xenapi/vm_utils.py:405 #, python-format msgid "Running pygrub against %s" -msgstr "" +msgstr "Rodando pygrub novamente %s" #: ../nova/virt/xenapi/vm_utils.py:411 #, python-format @@ -849,12 +857,12 @@ msgstr "(VM_UTILS) xenapi power_state -> |%s|" #: ../nova/virt/xenapi/vm_utils.py:525 #, python-format msgid "VHD %(vdi_uuid)s has parent %(parent_ref)s" -msgstr "" +msgstr "O VHD %(vdi_uuid)s tem pai %(parent_ref)s" #: ../nova/virt/xenapi/vm_utils.py:542 #, python-format msgid "Re-scanning SR %s" -msgstr "" +msgstr "Re-escaneando SR %s" #: ../nova/virt/xenapi/vm_utils.py:567 #, python-format @@ -2857,6 +2865,17 @@ msgstr "" #~ "Repositório de dados %s não pode ser atingido. Tentando novamente em %d " #~ "segundos." +#~ msgid "Full set of FLAGS:" +#~ msgstr "Conjunto completo de FLAGS:" + +#, python-format +#~ msgid "Starting %s" +#~ msgstr "Iniciando %s" + +#, python-format +#~ msgid "Serving %s" +#~ msgstr "Servindo %s" + #, python-format #~ msgid "Couldn't get IP, using 127.0.0.1 %s" #~ msgstr "Não foi possível obter IP, usando 127.0.0.1 %s" @@ -2965,3 +2984,14 @@ msgstr "" #, python-format #~ msgid "Created user %s (admin: %r)" #~ msgstr "Criado usuário %s (administrador: %r)" + +#~ msgid "No such process" +#~ msgstr "Processo inexistente" + +#, python-format +#~ msgid "pidfile %s does not exist. Daemon not running?\n" +#~ msgstr "" +#~ "Arquivo do id do processo (pidfile) %s não existe. O Daemon está parado?\n" + +#~ msgid "Wrong number of arguments." +#~ msgstr "Número errado de argumentos." diff --git a/po/ru.po b/po/ru.po index 1bf672fc363c..746db964a596 100644 --- a/po/ru.po +++ b/po/ru.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2789,6 +2789,10 @@ msgstr "" msgid "Removing user %(user)s from project %(project)s" msgstr "" +#, python-format +#~ msgid "Starting %s" +#~ msgstr "Запускается %s" + #, python-format #~ msgid "arg: %s\t\tval: %s" #~ msgstr "arg: %s\t\tval: %s" @@ -2841,6 +2845,13 @@ msgstr "" #~ msgid "Adding role %s to user %s in project %s" #~ msgstr "Добавление роли %s для пользователя %s в проект %s" +#~ msgid "unexpected error during update" +#~ msgstr "неожиданная ошибка во время обновления" + +#, python-format +#~ msgid "updating %s..." +#~ msgstr "обновление %s..." + #, python-format #~ msgid "Getting object: %s / %s" #~ msgstr "Получение объекта: %s / %s" @@ -2891,6 +2902,10 @@ msgstr "" #~ msgid "Couldn't get IP, using 127.0.0.1 %s" #~ msgstr "Не удалось получить IP, используем 127.0.0.1 %s" +#, python-format +#~ msgid "pidfile %s does not exist. Daemon not running?\n" +#~ msgstr "pidfile %s не обнаружен. Демон не запущен?\n" + #, python-format #~ msgid "Getting from %s: %s" #~ msgstr "Получение из %s: %s" @@ -2906,3 +2921,6 @@ msgstr "" #, python-format #~ msgid "Authenticated Request For %s:%s)" #~ msgstr "Запрос аутентификации для %s:%s)" + +#~ msgid "Wrong number of arguments." +#~ msgstr "Неверное число аргументов." diff --git a/po/tl.po b/po/tl.po index 1ae59330bcb9..84e9d26e69bf 100644 --- a/po/tl.po +++ b/po/tl.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 diff --git a/po/uk.po b/po/uk.po index 481851e1c02b..bcc53fed3fbc 100644 --- a/po/uk.po +++ b/po/uk.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2792,6 +2792,14 @@ msgstr "" #~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." #~ msgstr "AMQP сервер %s:%d недоступний. Спроба під'єднання через %d секунд." +#, python-format +#~ msgid "Starting %s" +#~ msgstr "Запускається %s" + +#, python-format +#~ msgid "Serving %s" +#~ msgstr "Обслуговування %s" + #, python-format #~ msgid "Couldn't get IP, using 127.0.0.1 %s" #~ msgstr "Не вдалось отримати IP, використовуючи 127.0.0.1 %s" diff --git a/po/zh_CN.po b/po/zh_CN.po index d0ddcd2f7339..6284ee46cdf2 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -8,14 +8,18 @@ msgstr "" "Project-Id-Version: nova\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2011-02-21 10:03-0500\n" -"PO-Revision-Date: 2011-06-14 14:44+0000\n" -"Last-Translator: chong \n" +"PO-Revision-Date: 2011-08-19 09:26+0000\n" +"Last-Translator: zhangjunfeng \n" "Language-Team: Chinese (Simplified) \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-20 05:06+0000\n" +"X-Generator: Launchpad (build 13697)\n" + +#, python-format +#~ msgid "Starting %s" +#~ msgstr "启动 %s 中" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -44,7 +48,7 @@ msgstr "" #: ../nova/exception.py:107 msgid "DB exception wrapped" -msgstr "" +msgstr "数据库异常" #. exc_type, exc_value, exc_traceback = sys.exc_info() #: ../nova/exception.py:120 @@ -84,7 +88,7 @@ msgstr "获取外网IP失败" #: ../nova/api/openstack/servers.py:152 #, python-format msgid "%(param)s property not found for image %(_image_id)s" -msgstr "" +msgstr "没有找到镜像文件%(_image_id)s 的属性 %(param)s" #: ../nova/api/openstack/servers.py:168 msgid "No keypairs defined" @@ -93,55 +97,55 @@ msgstr "未定义密钥对" #: ../nova/api/openstack/servers.py:238 #, python-format msgid "Compute.api::lock %s" -msgstr "" +msgstr "compute.api::加锁 %s" #: ../nova/api/openstack/servers.py:253 #, python-format msgid "Compute.api::unlock %s" -msgstr "" +msgstr "compute.api::解锁 %s" #: ../nova/api/openstack/servers.py:267 #, python-format msgid "Compute.api::get_lock %s" -msgstr "" +msgstr "Compute.api::得到锁 %s" #: ../nova/api/openstack/servers.py:281 #, python-format msgid "Compute.api::reset_network %s" -msgstr "" +msgstr "Compute.api::重置网络 %s" #: ../nova/api/openstack/servers.py:292 #, python-format msgid "Compute.api::pause %s" -msgstr "" +msgstr "Compute.api::暂停 %s" #: ../nova/api/openstack/servers.py:303 #, python-format msgid "Compute.api::unpause %s" -msgstr "" +msgstr "Compute.api::继续 %s" #: ../nova/api/openstack/servers.py:314 #, python-format msgid "compute.api::suspend %s" -msgstr "" +msgstr "compute.api::挂起 %s" #: ../nova/api/openstack/servers.py:325 #, python-format msgid "compute.api::resume %s" -msgstr "" +msgstr "compute.api::回复 %s" #: ../nova/virt/xenapi/volumeops.py:48 ../nova/virt/xenapi/volumeops.py:101 #: ../nova/db/sqlalchemy/api.py:731 ../nova/virt/libvirt_conn.py:741 #: ../nova/api/ec2/__init__.py:317 #, python-format msgid "Instance %s not found" -msgstr "" +msgstr "实例 %s 没有找到" #. NOTE: No Resource Pool concept so far #: ../nova/virt/xenapi/volumeops.py:51 #, python-format msgid "Attach_volume: %(instance_name)s, %(device_path)s, %(mountpoint)s" -msgstr "" +msgstr "挂载卷:%(instance_name)s, %(device_path)s, %(mountpoint)s" #: ../nova/virt/xenapi/volumeops.py:69 #, python-format @@ -2666,12 +2670,12 @@ msgstr "用户 %s 不存在" #: ../nova/auth/ldapdriver.py:472 #, python-format msgid "Group can't be created because group %s already exists" -msgstr "" +msgstr "组不能被创建,因为组 %s 已经存在" #: ../nova/auth/ldapdriver.py:478 #, python-format msgid "Group can't be created because user %s doesn't exist" -msgstr "" +msgstr "组不能被创建,因为用户 %s 不存在" #: ../nova/auth/ldapdriver.py:495 #, python-format @@ -2686,50 +2690,50 @@ msgstr "" #: ../nova/auth/ldapdriver.py:510 ../nova/auth/ldapdriver.py:521 #, python-format msgid "The group at dn %s doesn't exist" -msgstr "" +msgstr "识别名为 %s 的组不存在" #: ../nova/auth/ldapdriver.py:513 #, python-format msgid "User %(uid)s is already a member of the group %(group_dn)s" -msgstr "" +msgstr "用户 %(uid)s 已经是 组 %(group_dn)s 中的成员" #: ../nova/auth/ldapdriver.py:524 #, python-format msgid "" "User %s can't be removed from the group because the user doesn't exist" -msgstr "" +msgstr "用户 %s 不能从组中删除,因为这个用户不存在" #: ../nova/auth/ldapdriver.py:528 #, python-format msgid "User %s is not a member of the group" -msgstr "" +msgstr "用户 %s 不是这个组的成员" #: ../nova/auth/ldapdriver.py:542 #, python-format msgid "" "Attempted to remove the last member of a group. Deleting the group at %s " "instead." -msgstr "" +msgstr "尝试删除组中最后一个成员,用删除组 %s 来代替。" #: ../nova/auth/ldapdriver.py:549 #, python-format msgid "User %s can't be removed from all because the user doesn't exist" -msgstr "" +msgstr "用户 %s 不能从系统中删除,因为这个用户不存在" #: ../nova/auth/ldapdriver.py:564 #, python-format msgid "Group at dn %s doesn't exist" -msgstr "" +msgstr "可识别名为 %s 的组不存在" #: ../nova/virt/xenapi/network_utils.py:40 #, python-format msgid "Found non-unique network for bridge %s" -msgstr "" +msgstr "发现网桥 %s 的网络不唯一" #: ../nova/virt/xenapi/network_utils.py:43 #, python-format msgid "Found no network for bridge %s" -msgstr "" +msgstr "发现网桥 %s 没有网络" #: ../nova/api/ec2/admin.py:97 #, python-format @@ -2744,22 +2748,22 @@ msgstr "删除用户: %s" #: ../nova/api/ec2/admin.py:127 #, python-format msgid "Adding role %(role)s to user %(user)s for project %(project)s" -msgstr "" +msgstr "添加角色 %(role)s 给项目 %(project)s 中的用户 %(user)s" #: ../nova/api/ec2/admin.py:131 #, python-format msgid "Adding sitewide role %(role)s to user %(user)s" -msgstr "" +msgstr "给用户 %(user)s 添加站点角色 %(role)s" #: ../nova/api/ec2/admin.py:137 #, python-format msgid "Removing role %(role)s from user %(user)s for project %(project)s" -msgstr "" +msgstr "删除项目 %(project)s中用户 %(user)s的角色 %(role)s" #: ../nova/api/ec2/admin.py:141 #, python-format msgid "Removing sitewide role %(role)s from user %(user)s" -msgstr "" +msgstr "删除用户 %(user)s 的站点角色 %(role)s" #: ../nova/api/ec2/admin.py:146 ../nova/api/ec2/admin.py:223 msgid "operation must be add or remove" @@ -2768,22 +2772,22 @@ msgstr "操作必须为添加或删除" #: ../nova/api/ec2/admin.py:159 #, python-format msgid "Getting x509 for user: %(name)s on project: %(project)s" -msgstr "" +msgstr "获得用户: %(name)s 在项目 :%(project)s中的x509" #: ../nova/api/ec2/admin.py:177 #, python-format msgid "Create project %(name)s managed by %(manager_user)s" -msgstr "" +msgstr "创建被%(manager_user)s 管理的项目 %(name)s" #: ../nova/api/ec2/admin.py:190 #, python-format msgid "Modify project: %(name)s managed by %(manager_user)s" -msgstr "" +msgstr "更改被 %(manager_user)s 管理的项目: %(name)s" #: ../nova/api/ec2/admin.py:200 #, python-format msgid "Delete project: %s" -msgstr "删除工程 %s" +msgstr "" #: ../nova/api/ec2/admin.py:214 #, python-format @@ -2795,94 +2799,19 @@ msgstr "添加用户 %(user)s 到项目 %(project)s 中" msgid "Removing user %(user)s from project %(project)s" msgstr "从项目 %(project)s 中移除用户 %(user)s" -#, python-format -#~ msgid "" -#~ "%s\n" -#~ "Command: %s\n" -#~ "Exit code: %s\n" -#~ "Stdout: %r\n" -#~ "Stderr: %r" -#~ msgstr "" -#~ "%s\n" -#~ "命令:%s\n" -#~ "退出代码:%s\n" -#~ "标准输出(stdout):%r\n" -#~ "标准错误(stderr):%r" +#~ msgid "Full set of FLAGS:" +#~ msgstr "FLAGS全集:" + +#~ msgid "No such process" +#~ msgstr "没有该进程" #, python-format -#~ msgid "Binding %s to %s with key %s" -#~ msgstr "将%s绑定到%s(以%s键值)" +#~ msgid "Serving %s" +#~ msgstr "正在为 %s 服务" #, python-format -#~ msgid "AMQP server on %s:%d is unreachable. Trying again in %d seconds." -#~ msgstr "位于%s:%d的AMQP服务器不可用。%d秒后重试。" +#~ msgid "pidfile %s does not exist. Daemon not running?\n" +#~ msgstr "pidfile %s 不存在,守护进程是否运行?\n" -#, python-format -#~ msgid "Getting from %s: %s" -#~ msgstr "从%s获得如下内容:%s" - -#, python-format -#~ msgid "Starting %s node" -#~ msgstr "启动%s节点" - -#, python-format -#~ msgid "Data store %s is unreachable. Trying again in %d seconds." -#~ msgstr "数据储存服务%s不可用。%d秒之后继续尝试。" - -#, python-format -#~ msgid "(%s) publish (key: %s) %s" -#~ msgstr "(%s)发布(键值:%s)%s" - -#, python-format -#~ msgid "Couldn't get IP, using 127.0.0.1 %s" -#~ msgstr "不能获取IP,将使用 127.0.0.1 %s" - -#, python-format -#~ msgid "" -#~ "Access key %s has had %d failed authentications and will be locked out for " -#~ "%d minutes." -#~ msgstr "访问键 %s时,存在%d个失败的认证,将于%d分钟后解锁" - -#, python-format -#~ msgid "Authenticated Request For %s:%s)" -#~ msgstr "为%s:%s申请认证" - -#, python-format -#~ msgid "arg: %s\t\tval: %s" -#~ msgstr "键为: %s\t\t值为: %s" - -#, python-format -#~ msgid "Getting x509 for user: %s on project: %s" -#~ msgstr "为用户 %s从工程%s中获取 x509" - -#, python-format -#~ msgid "Create project %s managed by %s" -#~ msgstr "创建工程%s,此工程由%s管理" - -#, python-format -#~ msgid "Unsupported API request: controller = %s,action = %s" -#~ msgstr "不支持的API请求: 控制器 = %s,执行 = %s" - -#, python-format -#~ msgid "Adding sitewide role %s to user %s" -#~ msgstr "增加站点范围的 %s角色给用户 %s" - -#, python-format -#~ msgid "Adding user %s to project %s" -#~ msgstr "增加用户%s到%s工程" - -#, python-format -#~ msgid "Unauthorized request for controller=%s and action=%s" -#~ msgstr "对控制器=%s及动作=%s未经授权" - -#, python-format -#~ msgid "Removing user %s from project %s" -#~ msgstr "正将用户%s从工程%s中移除" - -#, python-format -#~ msgid "Adding role %s to user %s for project %s" -#~ msgstr "正将%s角色赋予用户%s(在工程%s中)" - -#, python-format -#~ msgid "Removing role %s from user %s for project %s" -#~ msgstr "正将角色%s从用户%s在工程%s中移除" +#~ msgid "Wrong number of arguments." +#~ msgstr "错误参数个数。" diff --git a/po/zh_TW.po b/po/zh_TW.po index 896e696189ed..a5a826aa0d0f 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -14,8 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-07-23 05:12+0000\n" -"X-Generator: Launchpad (build 13405)\n" +"X-Launchpad-Export-Date: 2011-08-03 04:44+0000\n" +"X-Generator: Launchpad (build 13573)\n" #: ../nova/scheduler/chance.py:37 ../nova/scheduler/zone.py:55 #: ../nova/scheduler/simple.py:75 ../nova/scheduler/simple.py:110 @@ -2787,3 +2787,14 @@ msgstr "" #, python-format msgid "Removing user %(user)s from project %(project)s" msgstr "" + +#~ msgid "No such process" +#~ msgstr "沒有此一程序" + +#, python-format +#~ msgid "pidfile %s does not exist. Daemon not running?\n" +#~ msgstr "pidfile %s 不存在. Daemon未啟動?\n" + +#, python-format +#~ msgid "Starting %s" +#~ msgstr "正在啟動 %s" diff --git a/run_tests.sh b/run_tests.sh index 8f2b51757c86..871332b4ae9f 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -67,7 +67,7 @@ function run_tests { ERRSIZE=`wc -l run_tests.log | awk '{print \$1}'` if [ "$ERRSIZE" -lt "40" ]; then - cat run_tests.log + cat run_tests.log fi fi return $RESULT