From ceaa125915c4f1432ba802396a84a6204a6678df Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 5 Aug 2010 10:30:06 -0500 Subject: added bin/nova-listinstances, which is mostly just a duplication of euca-describe-instances but doesn't go through the API. --- bin/nova-listinstances | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100755 bin/nova-listinstances diff --git a/bin/nova-listinstances b/bin/nova-listinstances new file mode 100755 index 000000000..2f8ff28f9 --- /dev/null +++ b/bin/nova-listinstances @@ -0,0 +1,19 @@ +#!/usr/bin/python + +# +# Duplicates the functionality of euca-describe-instances, but doesn't require +# going through the API. Does a direct query to the datastore. This is +# mostly a test program written for the scheduler +# + +from nova.compute import model + +data_needed = ['image_id', 'memory_kb', 'local_gb', 'node_name', 'vcpus'] + +instances = model.InstanceDirectory().all + +for instance in instances: + print 'Instance: %s' % instance['instance_id'] + for x in data_needed: + print ' %s: %s' % (x, instance[x]) + -- cgit From 5f41e9c764d2d064590e61018e655b9da8b17e9c Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 5 Aug 2010 12:52:55 -0500 Subject: compute nodes should store total memory and disk space available for VMs --- nova/compute/model.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nova/compute/model.py b/nova/compute/model.py index 212830d3c..3913d8738 100644 --- a/nova/compute/model.py +++ b/nova/compute/model.py @@ -53,10 +53,14 @@ from nova import utils FLAGS = flags.FLAGS - +flags.DEFINE_integer('total_memory_mb', 1000, + 'amount of memory a node has for VMs in MB') +flags.DEFINE_integer('total_disk_gb', 1000, + 'amount of disk space a node has for VMs in GB') # TODO(todd): Implement this at the class level for Instance class InstanceDirectory(object): + """an api for interacting with the global state of instances""" def get(self, instance_id): @@ -200,6 +204,8 @@ class Daemon(datastore.BasicModel): def default_state(self): return {"hostname": self.hostname, "binary": self.binary, + "total_memory_mb": FLAGS.total_memory_mb, + "total_disk_gb": FLAGS.total_disk_gb, "updated_at": utils.isotime() } -- cgit From bf0ea2deaf24419d85cae684e0700241e4c03f8c Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 5 Aug 2010 12:54:13 -0500 Subject: remove extra line accidentally added --- nova/compute/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/compute/model.py b/nova/compute/model.py index 3913d8738..edd49a5c0 100644 --- a/nova/compute/model.py +++ b/nova/compute/model.py @@ -60,7 +60,6 @@ flags.DEFINE_integer('total_disk_gb', 1000, # TODO(todd): Implement this at the class level for Instance class InstanceDirectory(object): - """an api for interacting with the global state of instances""" def get(self, instance_id): -- cgit From f42be0875d06a5d3ec0d5304d2f01a41b1f6a477 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 5 Aug 2010 16:11:59 -0500 Subject: almost there on random scheduler. not pushing to correct compute node topic, yet, apparently... --- bin/nova-scheduler | 32 +++++++++++++++++ nova/endpoint/cloud.py | 2 +- nova/flags.py | 1 + nova/scheduler/__init__.py | 33 ++++++++++++++++++ nova/scheduler/service.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100755 bin/nova-scheduler create mode 100644 nova/scheduler/__init__.py create mode 100644 nova/scheduler/service.py diff --git a/bin/nova-scheduler b/bin/nova-scheduler new file mode 100755 index 000000000..1ad41bbd3 --- /dev/null +++ b/bin/nova-scheduler @@ -0,0 +1,32 @@ +#!/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. + +""" + Twistd daemon for the nova scheduler nodes. +""" + +from nova import twistd +from nova.scheduler import service + + +if __name__ == '__main__': + twistd.serve(__file__) + +if __name__ == '__builtin__': + application = service.SchedulerService.create() diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 0ee278f84..a808e54c3 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -576,7 +576,7 @@ class CloudController(object): inst['private_dns_name'] = str(address) # TODO: allocate expresses on the router node inst.save() - rpc.cast(FLAGS.compute_topic, + rpc.cast(FLAGS.scheduler_topic, {"method": "run_instance", "args": {"instance_id" : inst.instance_id}}) logging.debug("Casting to node for %s's instance with IP of %s" % diff --git a/nova/flags.py b/nova/flags.py index f35f5fa10..7f92e3f70 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -41,6 +41,7 @@ DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') #DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') DEFINE_string('compute_topic', 'compute', 'the topic compute nodes listen on') +DEFINE_string('scheduler_topic', 'scheduler', 'the topic scheduler nodes listen on') DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on') DEFINE_string('network_topic', 'network', 'the topic network nodes listen on') diff --git a/nova/scheduler/__init__.py b/nova/scheduler/__init__.py new file mode 100644 index 000000000..516ea61bc --- /dev/null +++ b/nova/scheduler/__init__.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +:mod:`nova.scheduler` -- Scheduler Nodes +===================================================== + +.. automodule:: nova.scheduler + :platform: Unix + :synopsis: Daemon that picks a host for a VM instance. +.. moduleauthor:: Jesse Andrews +.. moduleauthor:: Devin Carlen +.. moduleauthor:: Vishvananda Ishaya +.. moduleauthor:: Joshua McKenty +.. moduleauthor:: Manish Singh +.. moduleauthor:: Andy Smith +.. moduleauthor:: Chris Behrens +""" diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py new file mode 100644 index 000000000..aca5b5db6 --- /dev/null +++ b/nova/scheduler/service.py @@ -0,0 +1,87 @@ +# 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. + +""" +Scheduler Service +""" + +import logging +import random +import sys +from twisted.internet import defer +from twisted.internet import task + +from nova import exception +from nova import flags +from nova import process +from nova import rpc +from nova import service +from nova import utils +from nova.compute import model +from nova.datastore import Redis + +FLAGS = flags.FLAGS + + +class SchedulerService(service.Service): + """ + Manages the running instances. + """ + def __init__(self): + super(SchedulerService, self).__init__() + self.instdir = model.InstanceDirectory() + + def noop(self): + """ simple test of an AMQP message call """ + return defer.succeed('PONG') + + @defer.inlineCallbacks + def report_state(self, nodename, daemon): + # TODO(termie): make this pattern be more elegant. -todd + try: + record = model.Daemon(nodename, daemon) + record.heartbeat() + if getattr(self, "model_disconnected", False): + self.model_disconnected = False + logging.error("Recovered model server connection!") + + except model.ConnectionError, ex: + if not getattr(self, "model_disconnected", False): + self.model_disconnected = True + logging.exception("model server went away") + yield + + @property + def compute_identifiers(self): + return [identifier for identifier in Redis.instance().smembers("daemons") if (identifier.split(':')[1] == "nova-compute")] + + def pick_node(self, instance_id, **_kwargs): + identifiers = self.compute_identifiers + return identifiers[int(random.random() * len(identifiers))].split(':')[0] + + @exception.wrap_exception + def run_instance(self, instance_id, **_kwargs): + node = self.pick_node(instance_id, **_kwargs) + + rpc.cast('%s:%s' % (FLAGS.compute_topic, node), + {"method": "run_instance", + "args": {"instance_id" : instance_id}}) + logging.debug("Casting to node %s for instance %s" % + (node, instance_id)) + + -- cgit From fd5000e70a724d9bea69754d4e7b99630d2d5ea2 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 5 Aug 2010 16:19:21 -0500 Subject: compute topic for a node is compute.node not compute:node! --- nova/scheduler/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py index aca5b5db6..2875dd554 100644 --- a/nova/scheduler/service.py +++ b/nova/scheduler/service.py @@ -78,7 +78,7 @@ class SchedulerService(service.Service): def run_instance(self, instance_id, **_kwargs): node = self.pick_node(instance_id, **_kwargs) - rpc.cast('%s:%s' % (FLAGS.compute_topic, node), + rpc.cast('%s.%s' % (FLAGS.compute_topic, node), {"method": "run_instance", "args": {"instance_id" : instance_id}}) logging.debug("Casting to node %s for instance %s" % -- cgit From c7e5faf0aa97ae8f0894b19a9f851d3868e578c3 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Thu, 5 Aug 2010 15:10:56 -0700 Subject: fixed doc string --- nova/scheduler/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py index 2875dd554..46e541a4f 100644 --- a/nova/scheduler/service.py +++ b/nova/scheduler/service.py @@ -40,7 +40,7 @@ FLAGS = flags.FLAGS class SchedulerService(service.Service): """ - Manages the running instances. + Picks nodes for instances to run. """ def __init__(self): super(SchedulerService, self).__init__() -- cgit From 869f33c9bf4a70e2a4ca4d1034114890d458f983 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Fri, 6 Aug 2010 14:40:24 -0500 Subject: Start breaking out scheduler classes... --- nova/scheduler/service.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py index 46e541a4f..3a226322f 100644 --- a/nova/scheduler/service.py +++ b/nova/scheduler/service.py @@ -23,6 +23,7 @@ Scheduler Service import logging import random import sys +import time from twisted.internet import defer from twisted.internet import task @@ -36,11 +37,12 @@ from nova.compute import model from nova.datastore import Redis FLAGS = flags.FLAGS - +flags.DEFINE_integer('node_down_time', 60, + 'seconds without heartbeat that determines a compute node to be down') class SchedulerService(service.Service): """ - Picks nodes for instances to run. + Manages the running instances. """ def __init__(self): super(SchedulerService, self).__init__() @@ -67,12 +69,20 @@ class SchedulerService(service.Service): yield @property - def compute_identifiers(self): - return [identifier for identifier in Redis.instance().smembers("daemons") if (identifier.split(':')[1] == "nova-compute")] + def compute_nodes(self): + return [identifier.split(':')[0] for identifier in Redis.instance().smembers("daemons") if (identifier.split(':')[1] == "nova-compute")] + + def compute_node_is_up(self, node): + time_str = Redis.instance().hget('%s:%s:%s' % ('daemon', node, 'nova-compute'), 'updated_at') + return(time_str and + (time.time() - (int(time.mktime(time.strptime(time_str.replace('Z', 'UTC'), '%Y-%m-%dT%H:%M:%S%Z'))) - time.timezone) < FLAGS.node_down_time)) + + def compute_nodes_up(self): + return [node for node in self.compute_nodes if self.compute_node_is_up(node)] def pick_node(self, instance_id, **_kwargs): - identifiers = self.compute_identifiers - return identifiers[int(random.random() * len(identifiers))].split(':')[0] + """You DEFINITELY want to define this in your subclass""" + raise NotImplementedError("Your subclass should define pick_node") @exception.wrap_exception def run_instance(self, instance_id, **_kwargs): @@ -84,4 +94,16 @@ class SchedulerService(service.Service): logging.debug("Casting to node %s for instance %s" % (node, instance_id)) +class RandomService(SchedulerService): + """ + Implements SchedulerService as a random node selector + """ + + def __init__(self): + super(RandomService, self).__init__() + + def pick_node(self, instance_id, **_kwargs): + nodes = self.compute_nodes_up() + return nodes[int(random.random() * len(nodes))] + -- cgit From 6c4e257b6df94b8c8e0745e8c3d0701293ae588e Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Fri, 6 Aug 2010 17:40:10 -0500 Subject: Moved Scheduler classes into scheduler.py. Created a way to specify scheduler class that the SchedulerService uses... --- nova/scheduler/scheduler.py | 82 +++++++++++++++++++++++++++++++++++++++++++++ nova/scheduler/service.py | 52 ++++++++-------------------- 2 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 nova/scheduler/scheduler.py diff --git a/nova/scheduler/scheduler.py b/nova/scheduler/scheduler.py new file mode 100644 index 000000000..0da7b95cf --- /dev/null +++ b/nova/scheduler/scheduler.py @@ -0,0 +1,82 @@ +# 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. + +""" +Scheduler Classes +""" + +import logging +import random +import sys +import time + +from nova import exception +from nova import flags +from nova.datastore import Redis + +FLAGS = flags.FLAGS +flags.DEFINE_integer('node_down_time', + 60, + 'seconds without heartbeat that determines a compute node to be down') + + +class Scheduler(object): + """ + The base class that all Scheduler clases should inherit from + """ + + @property + def compute_nodes(self): + return [identifier.split(':')[0] for identifier in Redis.instance().smembers("daemons") if (identifier.split(':')[1] == "nova-compute")] + + def compute_node_is_up(self, node): + time_str = Redis.instance().hget('%s:%s:%s' % ('daemon', node, 'nova-compute'), 'updated_at') + # Would be a lot easier if we stored heartbeat time in epoch :) + return(time_str and + (time.time() - (int(time.mktime(time.strptime(time_str.replace('Z', 'UTC'), '%Y-%m-%dT%H:%M:%S%Z'))) - time.timezone) < FLAGS.node_down_time)) + + def compute_nodes_up(self): + return [node for node in self.compute_nodes if self.compute_node_is_up(node)] + + def pick_node(self, instance_id, **_kwargs): + """You DEFINITELY want to define this in your subclass""" + raise NotImplementedError("Your subclass should define pick_node") + +class RandomScheduler(Scheduler): + """ + Implements Scheduler as a random node selector + """ + + def __init__(self): + super(RandomScheduler, self).__init__() + + def pick_node(self, instance_id, **_kwargs): + nodes = self.compute_nodes_up() + return nodes[int(random.random() * len(nodes))] + +class BestFitScheduler(Scheduler): + """ + Implements Scheduler as a best-fit node selector + """ + + def __init__(self): + super(BestFitScheduler, self).__init__() + + def pick_node(self, instance_id, **_kwargs): + raise NotImplementedError("BestFitScheduler is not done yet") + diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py index 3a226322f..3a86cefbe 100644 --- a/nova/scheduler/service.py +++ b/nova/scheduler/service.py @@ -21,32 +21,34 @@ Scheduler Service """ import logging -import random -import sys -import time from twisted.internet import defer -from twisted.internet import task from nova import exception from nova import flags -from nova import process from nova import rpc from nova import service -from nova import utils from nova.compute import model -from nova.datastore import Redis +from nova.scheduler import scheduler FLAGS = flags.FLAGS -flags.DEFINE_integer('node_down_time', 60, - 'seconds without heartbeat that determines a compute node to be down') - +flags.DEFINE_string('scheduler_type', + 'random', + 'the scheduler to use') + +scheduler_classes = { + 'random': scheduler.RandomScheduler, + 'bestfit': scheduler.BestFitScheduler + } + class SchedulerService(service.Service): """ Manages the running instances. """ def __init__(self): super(SchedulerService, self).__init__() - self.instdir = model.InstanceDirectory() + if (FLAGS.scheduler_type not in scheduler_classes): + raise exception.Error("Scheduler '%s' does not exist" % FLAGS.scheduler_type) + self._scheduler_class = scheduler_classes[FLAGS.scheduler_type] def noop(self): """ simple test of an AMQP message call """ @@ -68,21 +70,8 @@ class SchedulerService(service.Service): logging.exception("model server went away") yield - @property - def compute_nodes(self): - return [identifier.split(':')[0] for identifier in Redis.instance().smembers("daemons") if (identifier.split(':')[1] == "nova-compute")] - - def compute_node_is_up(self, node): - time_str = Redis.instance().hget('%s:%s:%s' % ('daemon', node, 'nova-compute'), 'updated_at') - return(time_str and - (time.time() - (int(time.mktime(time.strptime(time_str.replace('Z', 'UTC'), '%Y-%m-%dT%H:%M:%S%Z'))) - time.timezone) < FLAGS.node_down_time)) - - def compute_nodes_up(self): - return [node for node in self.compute_nodes if self.compute_node_is_up(node)] - def pick_node(self, instance_id, **_kwargs): - """You DEFINITELY want to define this in your subclass""" - raise NotImplementedError("Your subclass should define pick_node") + return self._scheduler_class().pick_node(instance_id, **_kwargs) @exception.wrap_exception def run_instance(self, instance_id, **_kwargs): @@ -94,16 +83,3 @@ class SchedulerService(service.Service): logging.debug("Casting to node %s for instance %s" % (node, instance_id)) -class RandomService(SchedulerService): - """ - Implements SchedulerService as a random node selector - """ - - def __init__(self): - super(RandomService, self).__init__() - - def pick_node(self, instance_id, **_kwargs): - nodes = self.compute_nodes_up() - return nodes[int(random.random() * len(nodes))] - - -- cgit From 094d64334e419d86a550c913ea4f0b8f086777bd Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Fri, 6 Aug 2010 18:10:41 -0500 Subject: fix copyrights for new files, etc --- bin/nova-listinstances | 21 ++++++++++++++++++++- nova/scheduler/__init__.py | 12 ++---------- nova/scheduler/scheduler.py | 4 +--- nova/scheduler/service.py | 4 +--- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/bin/nova-listinstances b/bin/nova-listinstances index 2f8ff28f9..386283d2f 100755 --- a/bin/nova-listinstances +++ b/bin/nova-listinstances @@ -1,4 +1,19 @@ -#!/usr/bin/python +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 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. # # Duplicates the functionality of euca-describe-instances, but doesn't require @@ -6,6 +21,10 @@ # mostly a test program written for the scheduler # +""" +List instances by doing a direct query to the datastore +""" + from nova.compute import model data_needed = ['image_id', 'memory_kb', 'local_gb', 'node_name', 'vcpus'] diff --git a/nova/scheduler/__init__.py b/nova/scheduler/__init__.py index 516ea61bc..8359a7aeb 100644 --- a/nova/scheduler/__init__.py +++ b/nova/scheduler/__init__.py @@ -1,8 +1,6 @@ # 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. +# Copyright (c) 2010 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 @@ -22,12 +20,6 @@ .. automodule:: nova.scheduler :platform: Unix - :synopsis: Daemon that picks a host for a VM instance. -.. moduleauthor:: Jesse Andrews -.. moduleauthor:: Devin Carlen -.. moduleauthor:: Vishvananda Ishaya -.. moduleauthor:: Joshua McKenty -.. moduleauthor:: Manish Singh -.. moduleauthor:: Andy Smith + :synopsis: Module that picks a compute node to run a VM instance. .. moduleauthor:: Chris Behrens """ diff --git a/nova/scheduler/scheduler.py b/nova/scheduler/scheduler.py index 0da7b95cf..79ed9dc06 100644 --- a/nova/scheduler/scheduler.py +++ b/nova/scheduler/scheduler.py @@ -1,8 +1,6 @@ # 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. +# Copyright (c) 2010 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 diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py index 3a86cefbe..39bfd6e07 100644 --- a/nova/scheduler/service.py +++ b/nova/scheduler/service.py @@ -1,8 +1,6 @@ # 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. +# Copyright (c) 2010 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 -- cgit From ba3b5ac30d9cd72e1cb757919ea76843112b307e Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Fri, 6 Aug 2010 18:54:45 -0500 Subject: pep8 and pylint cleanups --- nova/scheduler/scheduler.py | 29 +++++++++++++++++++---------- nova/scheduler/service.py | 18 +++++++++--------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/nova/scheduler/scheduler.py b/nova/scheduler/scheduler.py index 79ed9dc06..49ef40f06 100644 --- a/nova/scheduler/scheduler.py +++ b/nova/scheduler/scheduler.py @@ -18,19 +18,17 @@ Scheduler Classes """ -import logging import random -import sys import time -from nova import exception from nova import flags from nova.datastore import Redis FLAGS = flags.FLAGS flags.DEFINE_integer('node_down_time', 60, - 'seconds without heartbeat that determines a compute node to be down') + 'seconds without heartbeat that determines a ' + 'compute node to be down') class Scheduler(object): @@ -40,21 +38,32 @@ class Scheduler(object): @property def compute_nodes(self): - return [identifier.split(':')[0] for identifier in Redis.instance().smembers("daemons") if (identifier.split(':')[1] == "nova-compute")] + return [identifier.split(':')[0] + for identifier in Redis.instance().smembers("daemons") + if (identifier.split(':')[1] == "nova-compute")] def compute_node_is_up(self, node): - time_str = Redis.instance().hget('%s:%s:%s' % ('daemon', node, 'nova-compute'), 'updated_at') + time_str = Redis.instance().hget('%s:%s:%s' % + ('daemon', node, 'nova-compute'), + 'updated_at') + if not time_str: + return False + # Would be a lot easier if we stored heartbeat time in epoch :) - return(time_str and - (time.time() - (int(time.mktime(time.strptime(time_str.replace('Z', 'UTC'), '%Y-%m-%dT%H:%M:%S%Z'))) - time.timezone) < FLAGS.node_down_time)) + time_str = time_str.replace('Z', 'UTC') + time_split = time.strptime(time_str, '%Y-%m-%dT%H:%M:%S%Z') + epoch_time = int(time.mktime(time_split)) - time.timezone + return (time.time() - epoch_time) < FLAGS.node_down_time def compute_nodes_up(self): - return [node for node in self.compute_nodes if self.compute_node_is_up(node)] + return [node for node in self.compute_nodes + if self.compute_node_is_up(node)] def pick_node(self, instance_id, **_kwargs): """You DEFINITELY want to define this in your subclass""" raise NotImplementedError("Your subclass should define pick_node") + class RandomScheduler(Scheduler): """ Implements Scheduler as a random node selector @@ -67,6 +76,7 @@ class RandomScheduler(Scheduler): nodes = self.compute_nodes_up() return nodes[int(random.random() * len(nodes))] + class BestFitScheduler(Scheduler): """ Implements Scheduler as a best-fit node selector @@ -77,4 +87,3 @@ class BestFitScheduler(Scheduler): def pick_node(self, instance_id, **_kwargs): raise NotImplementedError("BestFitScheduler is not done yet") - diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py index 39bfd6e07..1246b6e72 100644 --- a/nova/scheduler/service.py +++ b/nova/scheduler/service.py @@ -33,19 +33,20 @@ flags.DEFINE_string('scheduler_type', 'random', 'the scheduler to use') -scheduler_classes = { - 'random': scheduler.RandomScheduler, - 'bestfit': scheduler.BestFitScheduler - } - +scheduler_classes = {'random': scheduler.RandomScheduler, + 'bestfit': scheduler.BestFitScheduler} + + class SchedulerService(service.Service): """ Manages the running instances. """ + def __init__(self): super(SchedulerService, self).__init__() if (FLAGS.scheduler_type not in scheduler_classes): - raise exception.Error("Scheduler '%s' does not exist" % FLAGS.scheduler_type) + raise exception.Error("Scheduler '%s' does not exist" % + FLAGS.scheduler_type) self._scheduler_class = scheduler_classes[FLAGS.scheduler_type] def noop(self): @@ -77,7 +78,6 @@ class SchedulerService(service.Service): rpc.cast('%s.%s' % (FLAGS.compute_topic, node), {"method": "run_instance", - "args": {"instance_id" : instance_id}}) - logging.debug("Casting to node %s for instance %s" % + "args": {"instance_id": instance_id}}) + logging.debug("Casting to node %s for running instance %s" % (node, instance_id)) - -- cgit From 795b32fc66f243239d05a5434f939a76800c0052 Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 9 Aug 2010 09:37:50 -0500 Subject: remove duplicated report_state that exists in the base class more pylint fixes --- nova/scheduler/service.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py index 1246b6e72..9d2d35f13 100644 --- a/nova/scheduler/service.py +++ b/nova/scheduler/service.py @@ -25,7 +25,6 @@ from nova import exception from nova import flags from nova import rpc from nova import service -from nova.compute import model from nova.scheduler import scheduler FLAGS = flags.FLAGS @@ -33,7 +32,7 @@ flags.DEFINE_string('scheduler_type', 'random', 'the scheduler to use') -scheduler_classes = {'random': scheduler.RandomScheduler, +SCHEDULER_CLASSES = {'random': scheduler.RandomScheduler, 'bestfit': scheduler.BestFitScheduler} @@ -44,40 +43,33 @@ class SchedulerService(service.Service): def __init__(self): super(SchedulerService, self).__init__() - if (FLAGS.scheduler_type not in scheduler_classes): + if (FLAGS.scheduler_type not in SCHEDULER_CLASSES): raise exception.Error("Scheduler '%s' does not exist" % FLAGS.scheduler_type) - self._scheduler_class = scheduler_classes[FLAGS.scheduler_type] + self._scheduler_class = SCHEDULER_CLASSES[FLAGS.scheduler_type] def noop(self): """ simple test of an AMQP message call """ return defer.succeed('PONG') - @defer.inlineCallbacks - def report_state(self, nodename, daemon): - # TODO(termie): make this pattern be more elegant. -todd - try: - record = model.Daemon(nodename, daemon) - record.heartbeat() - if getattr(self, "model_disconnected", False): - self.model_disconnected = False - logging.error("Recovered model server connection!") - - except model.ConnectionError, ex: - if not getattr(self, "model_disconnected", False): - self.model_disconnected = True - logging.exception("model server went away") - yield - def pick_node(self, instance_id, **_kwargs): + """ + Return a node to use based on the selected Scheduler + """ + return self._scheduler_class().pick_node(instance_id, **_kwargs) @exception.wrap_exception def run_instance(self, instance_id, **_kwargs): + """ + Picks a node for a running VM and casts the run_instance request + """ + node = self.pick_node(instance_id, **_kwargs) rpc.cast('%s.%s' % (FLAGS.compute_topic, node), {"method": "run_instance", "args": {"instance_id": instance_id}}) - logging.debug("Casting to node %s for running instance %s" % - (node, instance_id)) + logging.debug("Casting to node %s for running instance %s", + node, instance_id) + -- cgit From 3e01acd4e70f9e850487c5ac4067ab2c2c1a18eb Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Mon, 9 Aug 2010 12:56:32 -0500 Subject: separated scheduler types into own modules --- nova/scheduler/base.py | 65 +++++++++++++++++++++++++++++++++ nova/scheduler/bestfit.py | 30 +++++++++++++++ nova/scheduler/chance.py | 33 +++++++++++++++++ nova/scheduler/scheduler.py | 89 --------------------------------------------- nova/scheduler/service.py | 10 ++--- 5 files changed, 133 insertions(+), 94 deletions(-) create mode 100644 nova/scheduler/base.py create mode 100644 nova/scheduler/bestfit.py create mode 100644 nova/scheduler/chance.py delete mode 100644 nova/scheduler/scheduler.py diff --git a/nova/scheduler/base.py b/nova/scheduler/base.py new file mode 100644 index 000000000..5c359943e --- /dev/null +++ b/nova/scheduler/base.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 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. + +""" +Scheduler base class that all Schedulers should inherit from +""" + +import time + +from nova import flags +from nova.datastore import Redis + +FLAGS = flags.FLAGS +flags.DEFINE_integer('node_down_time', + 60, + 'seconds without heartbeat that determines a ' + 'compute node to be down') + + +class Scheduler(object): + """ + The base class that all Scheduler clases should inherit from + """ + + @property + def compute_nodes(self): + return [identifier.split(':')[0] + for identifier in Redis.instance().smembers("daemons") + if (identifier.split(':')[1] == "nova-compute")] + + def compute_node_is_up(self, node): + time_str = Redis.instance().hget('%s:%s:%s' % + ('daemon', node, 'nova-compute'), + 'updated_at') + if not time_str: + return False + + # Would be a lot easier if we stored heartbeat time in epoch :) + + # The 'str()' here is to get rid of a pylint error + time_str = str(time_str).replace('Z', 'UTC') + time_split = time.strptime(time_str, '%Y-%m-%dT%H:%M:%S%Z') + epoch_time = int(time.mktime(time_split)) - time.timezone + return (time.time() - epoch_time) < FLAGS.node_down_time + + def compute_nodes_up(self): + return [node for node in self.compute_nodes + if self.compute_node_is_up(node)] + + def pick_node(self, instance_id, **_kwargs): + """You DEFINITELY want to define this in your subclass""" + raise NotImplementedError("Your subclass should define pick_node") diff --git a/nova/scheduler/bestfit.py b/nova/scheduler/bestfit.py new file mode 100644 index 000000000..1bd24456a --- /dev/null +++ b/nova/scheduler/bestfit.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 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. + +""" +Best Fit Scheduler +""" + +from nova.scheduler.base import Scheduler + + +class BestFitScheduler(Scheduler): + """ + Implements Scheduler as a best-fit node selector + """ + + def pick_node(self, instance_id, **_kwargs): + raise NotImplementedError("BestFitScheduler is not done yet") diff --git a/nova/scheduler/chance.py b/nova/scheduler/chance.py new file mode 100644 index 000000000..c57c346f5 --- /dev/null +++ b/nova/scheduler/chance.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 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. + +""" +Chance (Random) Scheduler implementation +""" + +import random + +from nova.scheduler.base import Scheduler + + +class ChanceScheduler(Scheduler): + """ + Implements Scheduler as a random node selector + """ + + def pick_node(self, instance_id, **_kwargs): + nodes = self.compute_nodes_up() + return nodes[int(random.random() * len(nodes))] diff --git a/nova/scheduler/scheduler.py b/nova/scheduler/scheduler.py deleted file mode 100644 index 49ef40f06..000000000 --- a/nova/scheduler/scheduler.py +++ /dev/null @@ -1,89 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2010 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. - -""" -Scheduler Classes -""" - -import random -import time - -from nova import flags -from nova.datastore import Redis - -FLAGS = flags.FLAGS -flags.DEFINE_integer('node_down_time', - 60, - 'seconds without heartbeat that determines a ' - 'compute node to be down') - - -class Scheduler(object): - """ - The base class that all Scheduler clases should inherit from - """ - - @property - def compute_nodes(self): - return [identifier.split(':')[0] - for identifier in Redis.instance().smembers("daemons") - if (identifier.split(':')[1] == "nova-compute")] - - def compute_node_is_up(self, node): - time_str = Redis.instance().hget('%s:%s:%s' % - ('daemon', node, 'nova-compute'), - 'updated_at') - if not time_str: - return False - - # Would be a lot easier if we stored heartbeat time in epoch :) - time_str = time_str.replace('Z', 'UTC') - time_split = time.strptime(time_str, '%Y-%m-%dT%H:%M:%S%Z') - epoch_time = int(time.mktime(time_split)) - time.timezone - return (time.time() - epoch_time) < FLAGS.node_down_time - - def compute_nodes_up(self): - return [node for node in self.compute_nodes - if self.compute_node_is_up(node)] - - def pick_node(self, instance_id, **_kwargs): - """You DEFINITELY want to define this in your subclass""" - raise NotImplementedError("Your subclass should define pick_node") - - -class RandomScheduler(Scheduler): - """ - Implements Scheduler as a random node selector - """ - - def __init__(self): - super(RandomScheduler, self).__init__() - - def pick_node(self, instance_id, **_kwargs): - nodes = self.compute_nodes_up() - return nodes[int(random.random() * len(nodes))] - - -class BestFitScheduler(Scheduler): - """ - Implements Scheduler as a best-fit node selector - """ - - def __init__(self): - super(BestFitScheduler, self).__init__() - - def pick_node(self, instance_id, **_kwargs): - raise NotImplementedError("BestFitScheduler is not done yet") diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py index 9d2d35f13..44b30ecb5 100644 --- a/nova/scheduler/service.py +++ b/nova/scheduler/service.py @@ -25,15 +25,16 @@ from nova import exception from nova import flags from nova import rpc from nova import service -from nova.scheduler import scheduler +from nova.scheduler import chance +from nova.scheduler import bestfit FLAGS = flags.FLAGS flags.DEFINE_string('scheduler_type', - 'random', + 'chance', 'the scheduler to use') -SCHEDULER_CLASSES = {'random': scheduler.RandomScheduler, - 'bestfit': scheduler.BestFitScheduler} +SCHEDULER_CLASSES = {'chance': chance.ChanceScheduler, + 'bestfit': bestfit.BestFitScheduler} class SchedulerService(service.Service): @@ -72,4 +73,3 @@ class SchedulerService(service.Service): "args": {"instance_id": instance_id}}) logging.debug("Casting to node %s for running instance %s", node, instance_id) - -- cgit From d1982a50561f7b35ffc76ce5d45aaec11e76a23c Mon Sep 17 00:00:00 2001 From: Chris Behrens Date: Tue, 10 Aug 2010 18:48:33 -0500 Subject: more pylint fixes --- nova/scheduler/base.py | 23 +++++++++++++++++++---- nova/scheduler/bestfit.py | 4 ++++ nova/scheduler/chance.py | 4 ++++ nova/scheduler/service.py | 3 ++- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/nova/scheduler/base.py b/nova/scheduler/base.py index 5c359943e..2872ae6fe 100644 --- a/nova/scheduler/base.py +++ b/nova/scheduler/base.py @@ -35,13 +35,23 @@ class Scheduler(object): The base class that all Scheduler clases should inherit from """ - @property - def compute_nodes(self): + @staticmethod + def compute_nodes(): + """ + Return a list of compute nodes + """ + return [identifier.split(':')[0] for identifier in Redis.instance().smembers("daemons") if (identifier.split(':')[1] == "nova-compute")] - def compute_node_is_up(self, node): + @staticmethod + def compute_node_is_up(node): + """ + Given a node name, return whether the node is considered 'up' by + if it's sent a heartbeat recently + """ + time_str = Redis.instance().hget('%s:%s:%s' % ('daemon', node, 'nova-compute'), 'updated_at') @@ -57,9 +67,14 @@ class Scheduler(object): return (time.time() - epoch_time) < FLAGS.node_down_time def compute_nodes_up(self): - return [node for node in self.compute_nodes + """ + Return the list of compute nodes that are considered 'up' + """ + + return [node for node in self.compute_nodes() if self.compute_node_is_up(node)] def pick_node(self, instance_id, **_kwargs): """You DEFINITELY want to define this in your subclass""" + raise NotImplementedError("Your subclass should define pick_node") diff --git a/nova/scheduler/bestfit.py b/nova/scheduler/bestfit.py index 1bd24456a..bdd4fcbdc 100644 --- a/nova/scheduler/bestfit.py +++ b/nova/scheduler/bestfit.py @@ -27,4 +27,8 @@ class BestFitScheduler(Scheduler): """ def pick_node(self, instance_id, **_kwargs): + """ + Picks a node that is up and is a best fit for the new instance + """ + raise NotImplementedError("BestFitScheduler is not done yet") diff --git a/nova/scheduler/chance.py b/nova/scheduler/chance.py index c57c346f5..719c37674 100644 --- a/nova/scheduler/chance.py +++ b/nova/scheduler/chance.py @@ -29,5 +29,9 @@ class ChanceScheduler(Scheduler): """ def pick_node(self, instance_id, **_kwargs): + """ + Picks a node that is up at random + """ + nodes = self.compute_nodes_up() return nodes[int(random.random() * len(nodes))] diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py index 44b30ecb5..136f262c2 100644 --- a/nova/scheduler/service.py +++ b/nova/scheduler/service.py @@ -49,7 +49,8 @@ class SchedulerService(service.Service): FLAGS.scheduler_type) self._scheduler_class = SCHEDULER_CLASSES[FLAGS.scheduler_type] - def noop(self): + @staticmethod + def noop(): """ simple test of an AMQP message call """ return defer.succeed('PONG') -- cgit From 6ae66c595d4f85802045734ed1b230a292f9c953 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Fri, 20 Aug 2010 13:26:24 +0100 Subject: Better error message on subprocess spawn fail, and it's a ProcessExecutionException irrespective of how the process is run. --- nova/process.py | 65 +++++++++++++++++++++++++++------------------------------ nova/utils.py | 17 +++++++++++++-- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/nova/process.py b/nova/process.py index 425d9f162..259e3f92e 100644 --- a/nova/process.py +++ b/nova/process.py @@ -29,28 +29,12 @@ from twisted.internet import protocol from twisted.internet import reactor from nova import flags +from nova.utils import ProcessExecutionError FLAGS = flags.FLAGS flags.DEFINE_integer('process_pool_size', 4, 'Number of processes to use in the process pool') - -# NOTE(termie): this is copied from twisted.internet.utils but since -# they don't export it I've copied and modified -class UnexpectedErrorOutput(IOError): - """ - Standard error data was received where it was not expected. This is a - subclass of L{IOError} to preserve backward compatibility with the previous - error behavior of L{getProcessOutput}. - - @ivar processEnded: A L{Deferred} which will fire when the process which - produced the data on stderr has ended (exited and all file descriptors - closed). - """ - def __init__(self, stdout=None, stderr=None): - IOError.__init__(self, "got stdout: %r\nstderr: %r" % (stdout, stderr)) - - # This is based on _BackRelay from twister.internal.utils, but modified to # capture both stdout and stderr, without odd stderr handling, and also to # handle stdin @@ -62,22 +46,23 @@ class BackRelayWithInput(protocol.ProcessProtocol): @ivar deferred: A L{Deferred} which will be called back with all of stdout and all of stderr as well (as a tuple). C{terminate_on_stderr} is true and any bytes are received over stderr, this will fire with an - L{_UnexpectedErrorOutput} instance and the attribute will be set to + L{_ProcessExecutionError} instance and the attribute will be set to C{None}. @ivar onProcessEnded: If C{terminate_on_stderr} is false and bytes are received over stderr, this attribute will refer to a L{Deferred} which will be called back when the process ends. This C{Deferred} is also - associated with the L{_UnexpectedErrorOutput} which C{deferred} fires + associated with the L{_ProcessExecutionError} which C{deferred} fires with earlier in this case so that users can determine when the process has actually ended, in addition to knowing when bytes have been received via stderr. """ - def __init__(self, deferred, started_deferred=None, + def __init__(self, deferred, cmd, started_deferred=None, terminate_on_stderr=False, check_exit_code=True, process_input=None): self.deferred = deferred + self.cmd = cmd self.stdout = StringIO.StringIO() self.stderr = StringIO.StringIO() self.started_deferred = started_deferred @@ -85,14 +70,18 @@ class BackRelayWithInput(protocol.ProcessProtocol): self.check_exit_code = check_exit_code self.process_input = process_input self.on_process_ended = None - + + def _build_execution_error(self, exit_code=None): + return ProcessExecutionError( cmd=self.cmd, + exit_code=exit_code, + stdout=self.stdout.getvalue(), + stderr=self.stderr.getvalue()) + def errReceived(self, text): self.stderr.write(text) if self.terminate_on_stderr and (self.deferred is not None): self.on_process_ended = defer.Deferred() - self.deferred.errback(UnexpectedErrorOutput( - stdout=self.stdout.getvalue(), - stderr=self.stderr.getvalue())) + self.deferred.errback(self._build_execution_error()) self.deferred = None self.transport.loseConnection() @@ -102,15 +91,19 @@ class BackRelayWithInput(protocol.ProcessProtocol): def processEnded(self, reason): if self.deferred is not None: stdout, stderr = self.stdout.getvalue(), self.stderr.getvalue() - try: - if self.check_exit_code: - reason.trap(error.ProcessDone) - self.deferred.callback((stdout, stderr)) - except: - # NOTE(justinsb): This logic is a little suspicious to me... - # If the callback throws an exception, then errback will be - # called also. However, this is what the unit tests test for... - self.deferred.errback(UnexpectedErrorOutput(stdout, stderr)) + exit_code = reason.value.exitCode + if self.check_exit_code and exit_code <> 0: + self.deferred.errback(self._build_execution_error(exit_code)) + else: + try: + if self.check_exit_code: + reason.trap(error.ProcessDone) + self.deferred.callback((stdout, stderr)) + except: + # NOTE(justinsb): This logic is a little suspicious to me... + # If the callback throws an exception, then errback will be + # called also. However, this is what the unit tests test for... + self.deferred.errback(_build_execution_error(exit-code)) elif self.on_process_ended is not None: self.on_process_ended.errback(reason) @@ -131,8 +124,12 @@ def get_process_output(executable, args=None, env=None, path=None, args = args and args or () env = env and env and {} deferred = defer.Deferred() + cmd = executable + if args: + cmd = cmd + " " + ' '.join(args) process_handler = BackRelayWithInput( - deferred, + deferred, + cmd, started_deferred=started_deferred, check_exit_code=check_exit_code, process_input=process_input, diff --git a/nova/utils.py b/nova/utils.py index dc3c626ec..b8abb5388 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -36,6 +36,16 @@ from nova import flags FLAGS = flags.FLAGS TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" +class ProcessExecutionError(IOError): + def __init__( self, stdout=None, stderr=None, exit_code=None, cmd=None, + description=None): + if description is None: + description = "Unexpected error while running command." + if exit_code is None: + exit_code = '-' + message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( + description, cmd, exit_code, stdout, stderr) + IOError.__init__(self, message) def import_class(import_str): """Returns a class from a string including module and class""" @@ -73,8 +83,11 @@ def execute(cmd, process_input=None, addl_env=None, check_exit_code=True): if obj.returncode: logging.debug("Result was %s" % (obj.returncode)) if check_exit_code and obj.returncode <> 0: - raise Exception( "Unexpected exit code: %s. result=%s" - % (obj.returncode, result)) + (stdout, stderr) = result + raise ProcessExecutionError(exit_code=obj.returncode, + stdout=stdout, + stderr=stderr, + cmd=cmd) return result -- cgit From 41864e2653286fd46c7b69ee992d4be492b014c6 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Fri, 20 Aug 2010 14:50:43 +0100 Subject: Fixed typo --- nova/process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/process.py b/nova/process.py index 259e3f92e..81262a506 100644 --- a/nova/process.py +++ b/nova/process.py @@ -103,7 +103,7 @@ class BackRelayWithInput(protocol.ProcessProtocol): # NOTE(justinsb): This logic is a little suspicious to me... # If the callback throws an exception, then errback will be # called also. However, this is what the unit tests test for... - self.deferred.errback(_build_execution_error(exit-code)) + self.deferred.errback(_build_execution_error(exit_code)) elif self.on_process_ended is not None: self.on_process_ended.errback(reason) -- cgit From c4bf107b7e4fd64376dab7ebe39e4531f64879c5 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Sat, 21 Aug 2010 11:54:03 +0100 Subject: Added missing "self." --- nova/process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/process.py b/nova/process.py index 81262a506..069310802 100644 --- a/nova/process.py +++ b/nova/process.py @@ -103,7 +103,7 @@ class BackRelayWithInput(protocol.ProcessProtocol): # NOTE(justinsb): This logic is a little suspicious to me... # If the callback throws an exception, then errback will be # called also. However, this is what the unit tests test for... - self.deferred.errback(_build_execution_error(exit_code)) + self.deferred.errback(self._build_execution_error(exit_code)) elif self.on_process_ended is not None: self.on_process_ended.errback(reason) -- cgit From 7f666230e37745b174998a485fe1d7626c4862ae Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 25 Aug 2010 16:45:59 +0000 Subject: A few small changes to install_venv to let venv builds work on the tarmac box. --- tools/install_venv.py | 4 ++++ tools/pip-requires | 1 + 2 files changed, 5 insertions(+) diff --git a/tools/install_venv.py b/tools/install_venv.py index 1f0fa3cc7..e764efff6 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -88,6 +88,10 @@ def create_virtualenv(venv=VENV): def install_dependencies(venv=VENV): print 'Installing dependencies with pip (this can take a while)...' + # Install greenlet by hand - just listing it in the requires file does not + # get it in stalled in the right order + run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, 'greenlet'], + redirect_output=False) run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, '-r', PIP_REQUIRES], redirect_output=False) run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, TWISTED_NOVA], diff --git a/tools/pip-requires b/tools/pip-requires index 13e8e5f45..9853252dc 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -18,3 +18,4 @@ wsgiref==0.1.2 zope.interface==3.6.1 mox==0.5.0 -f http://pymox.googlecode.com/files/mox-0.5.0.tar.gz +greenlet==0.3.1 -- cgit From 7edff9298f7f01e158f90c93432384903d71e033 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 2 Sep 2010 11:32:37 -0700 Subject: scheduler + unittests --- bin/nova-scheduler | 4 +- nova/db/api.py | 13 +++++ nova/db/sqlalchemy/api.py | 31 ++++++++++++ nova/endpoint/cloud.py | 6 +-- nova/flags.py | 2 + nova/scheduler/base.py | 80 ------------------------------ nova/scheduler/bestfit.py | 34 ------------- nova/scheduler/chance.py | 37 +++++++++++--- nova/scheduler/driver.py | 62 +++++++++++++++++++++++ nova/scheduler/manager.py | 60 +++++++++++++++++++++++ nova/scheduler/service.py | 76 ----------------------------- nova/scheduler/simple.py | 81 ++++++++++++++++++++++++++++++ nova/tests/compute_unittest.py | 2 +- nova/tests/scheduler_unittest.py | 103 +++++++++++++++++++++++++++++++++++++++ run_tests.py | 1 + 15 files changed, 390 insertions(+), 202 deletions(-) delete mode 100644 nova/scheduler/base.py delete mode 100644 nova/scheduler/bestfit.py create mode 100644 nova/scheduler/driver.py create mode 100644 nova/scheduler/manager.py delete mode 100644 nova/scheduler/service.py create mode 100644 nova/scheduler/simple.py create mode 100644 nova/tests/scheduler_unittest.py diff --git a/bin/nova-scheduler b/bin/nova-scheduler index 1ad41bbd3..97f98b17f 100755 --- a/bin/nova-scheduler +++ b/bin/nova-scheduler @@ -21,12 +21,12 @@ Twistd daemon for the nova scheduler nodes. """ +from nova import service from nova import twistd -from nova.scheduler import service if __name__ == '__main__': twistd.serve(__file__) if __name__ == '__builtin__': - application = service.SchedulerService.create() + application = service.Service.create() diff --git a/nova/db/api.py b/nova/db/api.py index 6cb49b7e4..07eebd017 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -62,6 +62,19 @@ def daemon_get_by_args(context, host, binary): return IMPL.daemon_get_by_args(context, host, binary) +def daemon_get_all_by_topic(context, topic): + """Get all compute daemons for a given topi """ + return IMPL.daemon_get_all_by_topic(context, topic) + + +def daemon_get_all_compute_sorted(context): + """Get all compute daemons sorted by instance count + + Returns a list of (Daemon, instance_count) tuples + """ + return IMPL.daemon_get_all_compute_sorted(context) + + def daemon_create(context, values): """Create a daemon from the values dictionary.""" return IMPL.daemon_create(context, values) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 5d98ee5bf..aabd74984 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -25,6 +25,7 @@ from nova import flags from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import managed_session from sqlalchemy import or_ +from sqlalchemy.sql import func FLAGS = flags.FLAGS @@ -43,6 +44,36 @@ def daemon_get_by_args(_context, host, binary): return models.Daemon.find_by_args(host, binary) +def daemon_get_all_by_topic(context, topic): + with managed_session() as session: + return session.query(models.Daemon) \ + .filter_by(deleted=False) \ + .filter_by(topic=topic) \ + .all() + + +def daemon_get_all_compute_sorted(_context): + with managed_session() as session: + # NOTE(vish): The intended query is below + # SELECT daemons.*, inst_count.instance_count + # FROM daemons LEFT OUTER JOIN + # (SELECT host, count(*) AS instance_count + # FROM instances GROUP BY host) AS inst_count + print 'instance', models.Instance.find(1).host + subq = session.query(models.Instance.host, + func.count('*').label('instance_count')) \ + .filter_by(deleted=False) \ + .group_by(models.Instance.host) \ + .subquery() + topic = 'compute' + return session.query(models.Daemon, subq.c.instance_count) \ + .filter_by(topic=topic) \ + .filter_by(deleted=False) \ + .outerjoin((subq, models.Daemon.host == subq.c.host)) \ + .order_by(subq.c.instance_count) \ + .all() + + def daemon_create(_context, values): daemon_ref = models.Daemon() for (key, value) in values.iteritems(): diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 4e86145db..2c88ef406 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -488,9 +488,9 @@ class CloudController(object): host = db.network_get_host(context, network_ref['id']) if not host: host = yield rpc.call(FLAGS.network_topic, - {"method": "set_network_host", - "args": {"context": None, - "project_id": context.project.id}}) + {"method": "set_network_host", + "args": {"context": None, + "project_id": context.project.id}}) defer.returnValue(db.queue_get_for(context, FLAGS.network_topic, host)) @rbac.allow('projectmanager', 'sysadmin') diff --git a/nova/flags.py b/nova/flags.py index aa9648843..40ce9c736 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -220,5 +220,7 @@ DEFINE_string('network_manager', 'nova.network.manager.VlanManager', 'Manager for network') DEFINE_string('volume_manager', 'nova.volume.manager.AOEManager', 'Manager for volume') +DEFINE_string('scheduler_manager', 'nova.scheduler.manager.SchedulerManager', + 'Manager for scheduler') diff --git a/nova/scheduler/base.py b/nova/scheduler/base.py deleted file mode 100644 index 2872ae6fe..000000000 --- a/nova/scheduler/base.py +++ /dev/null @@ -1,80 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2010 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. - -""" -Scheduler base class that all Schedulers should inherit from -""" - -import time - -from nova import flags -from nova.datastore import Redis - -FLAGS = flags.FLAGS -flags.DEFINE_integer('node_down_time', - 60, - 'seconds without heartbeat that determines a ' - 'compute node to be down') - - -class Scheduler(object): - """ - The base class that all Scheduler clases should inherit from - """ - - @staticmethod - def compute_nodes(): - """ - Return a list of compute nodes - """ - - return [identifier.split(':')[0] - for identifier in Redis.instance().smembers("daemons") - if (identifier.split(':')[1] == "nova-compute")] - - @staticmethod - def compute_node_is_up(node): - """ - Given a node name, return whether the node is considered 'up' by - if it's sent a heartbeat recently - """ - - time_str = Redis.instance().hget('%s:%s:%s' % - ('daemon', node, 'nova-compute'), - 'updated_at') - if not time_str: - return False - - # Would be a lot easier if we stored heartbeat time in epoch :) - - # The 'str()' here is to get rid of a pylint error - time_str = str(time_str).replace('Z', 'UTC') - time_split = time.strptime(time_str, '%Y-%m-%dT%H:%M:%S%Z') - epoch_time = int(time.mktime(time_split)) - time.timezone - return (time.time() - epoch_time) < FLAGS.node_down_time - - def compute_nodes_up(self): - """ - Return the list of compute nodes that are considered 'up' - """ - - return [node for node in self.compute_nodes() - if self.compute_node_is_up(node)] - - def pick_node(self, instance_id, **_kwargs): - """You DEFINITELY want to define this in your subclass""" - - raise NotImplementedError("Your subclass should define pick_node") diff --git a/nova/scheduler/bestfit.py b/nova/scheduler/bestfit.py deleted file mode 100644 index bdd4fcbdc..000000000 --- a/nova/scheduler/bestfit.py +++ /dev/null @@ -1,34 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2010 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. - -""" -Best Fit Scheduler -""" - -from nova.scheduler.base import Scheduler - - -class BestFitScheduler(Scheduler): - """ - Implements Scheduler as a best-fit node selector - """ - - def pick_node(self, instance_id, **_kwargs): - """ - Picks a node that is up and is a best fit for the new instance - """ - - raise NotImplementedError("BestFitScheduler is not done yet") diff --git a/nova/scheduler/chance.py b/nova/scheduler/chance.py index 719c37674..12321cec1 100644 --- a/nova/scheduler/chance.py +++ b/nova/scheduler/chance.py @@ -1,6 +1,9 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright (c) 2010 Openstack, LLC. +# 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 @@ -20,18 +23,40 @@ Chance (Random) Scheduler implementation import random -from nova.scheduler.base import Scheduler +from nova.scheduler import driver -class ChanceScheduler(Scheduler): +class ChanceScheduler(driver.Scheduler): """ Implements Scheduler as a random node selector """ - def pick_node(self, instance_id, **_kwargs): + def pick_compute_host(self, context, instance_id, **_kwargs): """ - Picks a node that is up at random + Picks a host that is up at random """ - nodes = self.compute_nodes_up() - return nodes[int(random.random() * len(nodes))] + hosts = self.hosts_up(context, 'compute') + if not hosts: + raise driver.NoValidHost("No hosts found") + return hosts[int(random.random() * len(hosts))] + + def pick_volume_host(self, context, volume_id, **_kwargs): + """ + Picks a host that is up at random + """ + + hosts = self.hosts_up(context, 'volume') + if not hosts: + raise driver.NoValidHost("No hosts found") + return hosts[int(random.random() * len(hosts))] + + def pick_network_host(self, context, network_id, **_kwargs): + """ + Picks a host that is up at random + """ + + hosts = self.hosts_up(context, 'network') + if not hosts: + raise driver.NoValidHost("No hosts found") + return hosts[int(random.random() * len(hosts))] diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py new file mode 100644 index 000000000..1618342c0 --- /dev/null +++ b/nova/scheduler/driver.py @@ -0,0 +1,62 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Openstack, LLC. +# 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. + +""" +Scheduler base class that all Schedulers should inherit from +""" + +import datetime + +from nova import db +from nova import exception +from nova import flags + +FLAGS = flags.FLAGS +flags.DEFINE_integer('daemon_down_time', + 60, + 'seconds without heartbeat that determines a ' + 'compute node to be down') + +class NoValidHost(exception.Error): + """There is no valid host for the command""" + pass + +class Scheduler(object): + """ + The base class that all Scheduler clases should inherit from + """ + + @staticmethod + def daemon_is_up(daemon): + """ + Given a daemon, return whether the deamon is considered 'up' by + if it's sent a heartbeat recently + """ + elapsed = datetime.datetime.now() - daemon['updated_at'] + return elapsed < datetime.timedelta(seconds=FLAGS.daemon_down_time) + + def hosts_up(self, context, topic): + """ + Return the list of hosts that have a running daemon for topic + """ + + daemons = db.daemon_get_all_by_topic(context, topic) + return [daemon.host + for daemon in daemons + if self.daemon_is_up(daemon)] diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py new file mode 100644 index 000000000..a75b4ac41 --- /dev/null +++ b/nova/scheduler/manager.py @@ -0,0 +1,60 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Openstack, LLC. +# 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. + +""" +Scheduler Service +""" + +import logging + +from nova import db +from nova import flags +from nova import manager +from nova import rpc +from nova import utils + +FLAGS = flags.FLAGS +flags.DEFINE_string('scheduler_driver', + 'nova.scheduler.chance.ChanceScheduler', + 'Driver to use for the scheduler') + + +class SchedulerManager(manager.Manager): + """ + Chooses a host to run instances on. + """ + def __init__(self, scheduler_driver=None, *args, **kwargs): + if not scheduler_driver: + scheduler_driver = FLAGS.scheduler_driver + self.driver = utils.import_object(scheduler_driver) + super(SchedulerManager, self).__init__(*args, **kwargs) + + def run_instance(self, context, instance_id, **_kwargs): + """ + Picks a node for a running VM and casts the run_instance request + """ + + host = self.driver.pick_host(context, instance_id, **_kwargs) + + rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), + {"method": "run_instance", + "args": {"context": context, + "instance_id": instance_id}}) + logging.debug("Casting to compute %s for running instance %s", + host, instance_id) diff --git a/nova/scheduler/service.py b/nova/scheduler/service.py deleted file mode 100644 index 136f262c2..000000000 --- a/nova/scheduler/service.py +++ /dev/null @@ -1,76 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2010 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. - -""" -Scheduler Service -""" - -import logging -from twisted.internet import defer - -from nova import exception -from nova import flags -from nova import rpc -from nova import service -from nova.scheduler import chance -from nova.scheduler import bestfit - -FLAGS = flags.FLAGS -flags.DEFINE_string('scheduler_type', - 'chance', - 'the scheduler to use') - -SCHEDULER_CLASSES = {'chance': chance.ChanceScheduler, - 'bestfit': bestfit.BestFitScheduler} - - -class SchedulerService(service.Service): - """ - Manages the running instances. - """ - - def __init__(self): - super(SchedulerService, self).__init__() - if (FLAGS.scheduler_type not in SCHEDULER_CLASSES): - raise exception.Error("Scheduler '%s' does not exist" % - FLAGS.scheduler_type) - self._scheduler_class = SCHEDULER_CLASSES[FLAGS.scheduler_type] - - @staticmethod - def noop(): - """ simple test of an AMQP message call """ - return defer.succeed('PONG') - - def pick_node(self, instance_id, **_kwargs): - """ - Return a node to use based on the selected Scheduler - """ - - return self._scheduler_class().pick_node(instance_id, **_kwargs) - - @exception.wrap_exception - def run_instance(self, instance_id, **_kwargs): - """ - Picks a node for a running VM and casts the run_instance request - """ - - node = self.pick_node(instance_id, **_kwargs) - - rpc.cast('%s.%s' % (FLAGS.compute_topic, node), - {"method": "run_instance", - "args": {"instance_id": instance_id}}) - logging.debug("Casting to node %s for running instance %s", - node, instance_id) diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py new file mode 100644 index 000000000..6c76fd322 --- /dev/null +++ b/nova/scheduler/simple.py @@ -0,0 +1,81 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Openstack, LLC. +# 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. + +""" +Simple Scheduler +""" + +from nova import db +from nova import flags +from nova.scheduler import driver + +FLAGS = flags.FLAGS +flags.DEFINE_integer("max_instances", 16, + "maximum number of instances to allow per host") +flags.DEFINE_integer("max_volumes", 100, + "maximum number of volumes to allow per host") +flags.DEFINE_integer("max_networks", 1000, + "maximum number of networks to allow per host") + +class SimpleScheduler(driver.Scheduler): + """ + Implements Naive Scheduler that tries to find least loaded host + """ + + def pick_compute_host(self, context, instance_id, **_kwargs): + """ + Picks a host that is up and has the fewest running instances + """ + + results = db.daemon_get_all_compute_sorted(context) + for result in results: + (daemon, instance_count) = result + if instance_count >= FLAGS.max_instances: + raise driver.NoValidHost("All hosts have too many instances") + if self.daemon_is_up(daemon): + return daemon['host'] + raise driver.NoValidHost("No hosts found") + + def pick_volume_host(self, context, volume_id, **_kwargs): + """ + Picks a host that is up and has the fewest volumes + """ + + results = db.daemon_get_all_volume_sorted(context) + for result in results: + (daemon, instance_count) = result + if instance_count >= FLAGS.max_volumes: + raise driver.NoValidHost("All hosts have too many volumes") + if self.daemon_is_up(daemon): + return daemon['host'] + raise driver.NoValidHost("No hosts found") + + def pick_network_host(self, context, network_id, **_kwargs): + """ + Picks a host that is up and has the fewest networks + """ + + results = db.daemon_get_all_network_sorted(context) + for result in results: + (daemon, instance_count) = result + if instance_count >= FLAGS.max_networks: + raise driver.NoValidHost("All hosts have too many networks") + if self.daemon_is_up(daemon): + return daemon['host'] + raise driver.NoValidHost("No hosts found") diff --git a/nova/tests/compute_unittest.py b/nova/tests/compute_unittest.py index 867b572f3..23013e4c7 100644 --- a/nova/tests/compute_unittest.py +++ b/nova/tests/compute_unittest.py @@ -61,7 +61,7 @@ class ComputeTestCase(test.TrialTestCase): inst['instance_type'] = 'm1.tiny' inst['mac_address'] = utils.generate_mac() inst['ami_launch_index'] = 0 - return db.instance_create(None, inst) + return db.instance_create(self.context, inst) @defer.inlineCallbacks def test_run_terminate(self): diff --git a/nova/tests/scheduler_unittest.py b/nova/tests/scheduler_unittest.py new file mode 100644 index 000000000..d3616dd6f --- /dev/null +++ b/nova/tests/scheduler_unittest.py @@ -0,0 +1,103 @@ +# 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. +""" +Tests For Scheduler +""" +import logging + +from twisted.internet import defer + +from nova import db +from nova import flags +from nova import service +from nova import test +from nova import utils +from nova.auth import manager as auth_manager +from nova.scheduler import manager + + +FLAGS = flags.FLAGS + + +class SchedulerTestCase(test.TrialTestCase): + """Test case for scheduler""" + def setUp(self): # pylint: disable-msg=C0103 + super(SchedulerTestCase, self).setUp() + self.flags(connection_type='fake', + scheduler_driver='nova.scheduler.simple.SimpleScheduler') + self.scheduler = manager.SchedulerManager() + self.context = None + self.manager = auth_manager.AuthManager() + self.user = self.manager.create_user('fake', 'fake', 'fake') + self.project = self.manager.create_project('fake', 'fake', 'fake') + self.context = None + + def tearDown(self): # pylint: disable-msg=C0103 + self.manager.delete_user(self.user) + self.manager.delete_project(self.project) + + def _create_instance(self): + """Create a test instance""" + inst = {} + inst['image_id'] = 'ami-test' + inst['reservation_id'] = 'r-fakeres' + inst['launch_time'] = '10' + inst['user_id'] = self.user.id + inst['project_id'] = self.project.id + inst['instance_type'] = 'm1.tiny' + inst['mac_address'] = utils.generate_mac() + inst['ami_launch_index'] = 0 + return db.instance_create(self.context, inst) + + def test_hosts_are_up(self): + # NOTE(vish): constructing service without create method + # because we are going to use it without queue + service1 = service.Service('host1', + 'nova-compute', + 'compute', + FLAGS.compute_manager) + service2 = service.Service('host2', + 'nova-compute', + 'compute', + FLAGS.compute_manager) + service1.report_state() + service2.report_state() + hosts = self.scheduler.driver.hosts_up(self.context, 'compute') + self.assertEqual(len(hosts), 2) + + def test_least_busy_host_gets_instance(self): + # NOTE(vish): constructing service without create method + # because we are going to use it without queue + service1 = service.Service('host1', + 'nova-compute', + 'compute', + FLAGS.compute_manager) + service2 = service.Service('host2', + 'nova-compute', + 'compute', + FLAGS.compute_manager) + service1.report_state() + service2.report_state() + instance_id = self._create_instance() + FLAGS.host = 'host1' + service1.run_instance(self.context, + instance_id) + print type(self.scheduler.driver) + host = self.scheduler.driver.pick_compute_host(self.context, + instance_id) + self.assertEqual(host, 'host2') diff --git a/run_tests.py b/run_tests.py index c47cbe2ec..5d76a74ca 100644 --- a/run_tests.py +++ b/run_tests.py @@ -60,6 +60,7 @@ from nova.tests.network_unittest import * from nova.tests.objectstore_unittest import * from nova.tests.process_unittest import * from nova.tests.rpc_unittest import * +from nova.tests.scheduler_unittest import * from nova.tests.service_unittest import * from nova.tests.validator_unittest import * from nova.tests.volume_unittest import * -- cgit From 68d8f54e00c153eccd426256a25c8a70ccce2dcc Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 2 Sep 2010 15:15:39 -0700 Subject: test for too many instances work --- nova/scheduler/driver.py | 21 +++++++------- nova/scheduler/simple.py | 25 ++++++++-------- nova/tests/scheduler_unittest.py | 63 ++++++++++++++++------------------------ 3 files changed, 48 insertions(+), 61 deletions(-) diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index 1618342c0..830f05b13 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -28,7 +28,7 @@ from nova import exception from nova import flags FLAGS = flags.FLAGS -flags.DEFINE_integer('daemon_down_time', +flags.DEFINE_integer('service_down_time', 60, 'seconds without heartbeat that determines a ' 'compute node to be down') @@ -43,20 +43,21 @@ class Scheduler(object): """ @staticmethod - def daemon_is_up(daemon): + def service_is_up(service): """ - Given a daemon, return whether the deamon is considered 'up' by + Given a service, return whether the service is considered 'up' by if it's sent a heartbeat recently """ - elapsed = datetime.datetime.now() - daemon['updated_at'] - return elapsed < datetime.timedelta(seconds=FLAGS.daemon_down_time) + last_heartbeat = service['updated_at'] or service['created_at'] + elapsed = datetime.datetime.now() - last_heartbeat + return elapsed < datetime.timedelta(seconds=FLAGS.service_down_time) def hosts_up(self, context, topic): """ - Return the list of hosts that have a running daemon for topic + Return the list of hosts that have a running service for topic """ - daemons = db.daemon_get_all_by_topic(context, topic) - return [daemon.host - for daemon in daemons - if self.daemon_is_up(daemon)] + services = db.service_get_all_by_topic(context, topic) + return [service.host + for service in services + if self.service_is_up(service)] diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py index 294dc1118..832417208 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -43,14 +43,13 @@ class SimpleScheduler(driver.Scheduler): Picks a host that is up and has the fewest running instances """ - results = db.daemon_get_all_compute_sorted(context) + results = db.service_get_all_compute_sorted(context) for result in results: - (daemon, instance_count) = result - print daemon.host, instance_count + (service, instance_count) = result if instance_count >= FLAGS.max_instances: raise driver.NoValidHost("All hosts have too many instances") - if self.daemon_is_up(daemon): - return daemon['host'] + if self.service_is_up(service): + return service['host'] raise driver.NoValidHost("No hosts found") def pick_volume_host(self, context, volume_id, **_kwargs): @@ -58,13 +57,13 @@ class SimpleScheduler(driver.Scheduler): Picks a host that is up and has the fewest volumes """ - results = db.daemon_get_all_volume_sorted(context) + results = db.service_get_all_volume_sorted(context) for result in results: - (daemon, instance_count) = result + (service, instance_count) = result if instance_count >= FLAGS.max_volumes: raise driver.NoValidHost("All hosts have too many volumes") - if self.daemon_is_up(daemon): - return daemon['host'] + if self.service_is_up(service): + return service['host'] raise driver.NoValidHost("No hosts found") def pick_network_host(self, context, network_id, **_kwargs): @@ -72,11 +71,11 @@ class SimpleScheduler(driver.Scheduler): Picks a host that is up and has the fewest networks """ - results = db.daemon_get_all_network_sorted(context) + results = db.service_get_all_network_sorted(context) for result in results: - (daemon, instance_count) = result + (service, instance_count) = result if instance_count >= FLAGS.max_networks: raise driver.NoValidHost("All hosts have too many networks") - if self.daemon_is_up(daemon): - return daemon['host'] + if self.service_is_up(service): + return service['host'] raise driver.NoValidHost("No hosts found") diff --git a/nova/tests/scheduler_unittest.py b/nova/tests/scheduler_unittest.py index 45ffac438..bdd77713a 100644 --- a/nova/tests/scheduler_unittest.py +++ b/nova/tests/scheduler_unittest.py @@ -18,9 +18,6 @@ """ Tests For Scheduler """ -import logging - -from twisted.internet import defer from nova import db from nova import flags @@ -36,10 +33,10 @@ FLAGS = flags.FLAGS flags.DECLARE('max_instances', 'nova.scheduler.simple') -class SchedulerTestCase(test.TrialTestCase): +class SimpleSchedulerTestCase(test.TrialTestCase): """Test case for scheduler""" def setUp(self): # pylint: disable-msg=C0103 - super(SchedulerTestCase, self).setUp() + super(SimpleSchedulerTestCase, self).setUp() self.flags(connection_type='fake', max_instances=4, scheduler_driver='nova.scheduler.simple.SimpleScheduler') @@ -49,10 +46,20 @@ class SchedulerTestCase(test.TrialTestCase): self.user = self.manager.create_user('fake', 'fake', 'fake') self.project = self.manager.create_project('fake', 'fake', 'fake') self.context = None + self.service1 = service.Service('host1', + 'nova-compute', + 'compute', + FLAGS.compute_manager) + self.service2 = service.Service('host2', + 'nova-compute', + 'compute', + FLAGS.compute_manager) def tearDown(self): # pylint: disable-msg=C0103 self.manager.delete_user(self.user) self.manager.delete_project(self.project) + self.service1.kill() + self.service2.kill() def _create_instance(self): """Create a test instance""" @@ -70,53 +77,33 @@ class SchedulerTestCase(test.TrialTestCase): def test_hosts_are_up(self): # NOTE(vish): constructing service without create method # because we are going to use it without queue - service1 = service.Service('host1', - 'nova-compute', - 'compute', - FLAGS.compute_manager) - service2 = service.Service('host2', - 'nova-compute', - 'compute', - FLAGS.compute_manager) - hosts = self.scheduler.driver.hosts_up(self.context, 'compute') - self.assertEqual(len(hosts), 0) - service1.report_state() - service2.report_state() hosts = self.scheduler.driver.hosts_up(self.context, 'compute') self.assertEqual(len(hosts), 2) def test_least_busy_host_gets_instance(self): - service1 = service.Service('host1', - 'nova-compute', - 'compute', - FLAGS.compute_manager) - service2 = service.Service('host2', - 'nova-compute', - 'compute', - FLAGS.compute_manager) - service1.report_state() - service2.report_state() instance_id = self._create_instance() - service1.run_instance(self.context, instance_id) + self.service1.run_instance(self.context, instance_id) host = self.scheduler.driver.pick_compute_host(self.context, instance_id) self.assertEqual(host, 'host2') - service1.terminate_instance(self.context, instance_id) + self.service1.terminate_instance(self.context, instance_id) def test_too_many_instances(self): - service1 = service.Service('host', - 'nova-compute', - 'compute', - FLAGS.compute_manager) - instance_ids = [] + instance_ids1 = [] + instance_ids2 = [] for index in xrange(FLAGS.max_instances): instance_id = self._create_instance() - service1.run_instance(self.context, instance_id) - instance_ids.append(instance_id) + self.service1.run_instance(self.context, instance_id) + instance_ids1.append(instance_id) + instance_id = self._create_instance() + self.service2.run_instance(self.context, instance_id) + instance_ids2.append(instance_id) instance_id = self._create_instance() self.assertRaises(driver.NoValidHost, self.scheduler.driver.pick_compute_host, self.context, instance_id) - for instance_id in instance_ids: - service1.terminate_instance(self.context, instance_id) + for instance_id in instance_ids1: + self.service1.terminate_instance(self.context, instance_id) + for instance_id in instance_ids2: + self.service2.terminate_instance(self.context, instance_id) -- cgit From e555ce94ee29013901796b570b752f39194ddb12 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 2 Sep 2010 17:06:01 -0700 Subject: send ultimate topic in to scheduler --- nova/endpoint/cloud.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 2c88ef406..1ff22042a 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -567,6 +567,7 @@ class CloudController(object): rpc.cast(FLAGS.scheduler_topic, {"method": "run_instance", "args": {"context": None, + "topic": FLAGS.compute_topic, "instance_id": inst_id}}) logging.debug("Casting to scheduler for %s/%s's instance %s" % (context.project.name, context.user.name, inst_id)) -- cgit From 22aa51638dc221e78de60f7e2ddb10eb0ddf4db3 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 3 Sep 2010 00:53:41 -0700 Subject: removed extra file and updated sql note --- bin/nova-listinstances | 38 -------------------------------------- nova/db/sqlalchemy/api.py | 1 + 2 files changed, 1 insertion(+), 38 deletions(-) delete mode 100755 bin/nova-listinstances diff --git a/bin/nova-listinstances b/bin/nova-listinstances deleted file mode 100755 index 386283d2f..000000000 --- a/bin/nova-listinstances +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2010 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. - -# -# Duplicates the functionality of euca-describe-instances, but doesn't require -# going through the API. Does a direct query to the datastore. This is -# mostly a test program written for the scheduler -# - -""" -List instances by doing a direct query to the datastore -""" - -from nova.compute import model - -data_needed = ['image_id', 'memory_kb', 'local_gb', 'node_name', 'vcpus'] - -instances = model.InstanceDirectory().all - -for instance in instances: - print 'Instance: %s' % instance['instance_id'] - for x in data_needed: - print ' %s: %s' % (x, instance[x]) - diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4ae55eaf4..4fa85b74b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -69,6 +69,7 @@ def service_get_all_compute_sorted(context): # FROM services LEFT OUTER JOIN # (SELECT host, count(*) AS instance_count # FROM instances GROUP BY host) AS inst_count + # ON services.host == inst_count.host topic = 'compute' label = 'instance_count' subq = session.query(models.Instance.host, -- cgit From a983660008d09276d2749077c1141313381d6eb6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 4 Sep 2010 11:42:15 -0700 Subject: removed extra equals --- nova/db/sqlalchemy/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4fa85b74b..cb94023f5 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -69,7 +69,7 @@ def service_get_all_compute_sorted(context): # FROM services LEFT OUTER JOIN # (SELECT host, count(*) AS instance_count # FROM instances GROUP BY host) AS inst_count - # ON services.host == inst_count.host + # ON services.host = inst_count.host topic = 'compute' label = 'instance_count' subq = session.query(models.Instance.host, -- cgit From 19d4c3a6f411b3b96d4a3dffc16b9b272a01971f Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 5 Sep 2010 05:33:56 +0100 Subject: Bug #630636: XenAPI VM destroy fails when the VM is still running When destroying a VM using the XenAPI backend, if the VM is still running (the usual case) the destroy fails. It needs to be powered-off first. --- nova/virt/xenapi.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index b44ac383a..04069e459 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -274,9 +274,19 @@ class XenAPIConnection(object): def destroy(self, instance): vm = yield self._lookup(instance.name) if vm is None: - raise Exception('instance not present %s' % instance.name) - task = yield self._call_xenapi('Async.VM.destroy', vm) - yield self._wait_for_task(task) + # Don't complain, just return. This lets us clean up instances + # that have already disappeared from the underlying platform. + defer.returnValue(None) + try: + task = yield self._call_xenapi('Async.VM.hard_shutdown', vm) + yield self._wait_for_task(task) + except Exception, exn: + logging.warn(exn) + try: + task = yield self._call_xenapi('Async.VM.destroy', vm) + yield self._wait_for_task(task) + except Exception, exn: + logging.warn(exn) def get_info(self, instance_id): vm = self._lookup_blocking(instance_id) -- cgit From b049c032a9f950d67bbfe709802288a7fe28bdd6 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 5 Sep 2010 05:56:53 +0100 Subject: Bug #630640: Duplicated power state constants Remove power state constants that have ended up duplicated following a bad merge. They were moved from nova.compute.node.Instance into nova.compute.power_state at the same time that Instance was moved into nova.compute.service. We've ended up with these constants in both places. Remove the ones from service, in favour of the ones in power_state. --- nova/compute/service.py | 8 -------- nova/tests/cloud_unittest.py | 3 ++- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/nova/compute/service.py b/nova/compute/service.py index e59f3fb34..3321c2c00 100644 --- a/nova/compute/service.py +++ b/nova/compute/service.py @@ -224,14 +224,6 @@ class ProductCode(object): class Instance(object): - NOSTATE = 0x00 - RUNNING = 0x01 - BLOCKED = 0x02 - PAUSED = 0x03 - SHUTDOWN = 0x04 - SHUTOFF = 0x05 - CRASHED = 0x06 - def __init__(self, conn, name, data): """ spawn an instance with a given name """ self._conn = conn diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 900ff5a97..19aa23b9e 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -28,6 +28,7 @@ from nova import flags from nova import rpc from nova import test from nova.auth import manager +from nova.compute import power_state from nova.compute import service from nova.endpoint import api from nova.endpoint import cloud @@ -95,7 +96,7 @@ class CloudTestCase(test.BaseTestCase): rv = yield defer.succeed(time.sleep(1)) info = self.cloud._get_instance(instance['instance_id']) logging.debug(info['state']) - if info['state'] == node.Instance.RUNNING: + if info['state'] == power_state.RUNNING: break self.assert_(rv) -- cgit From 71566b41619166f61a3fe478524f66908364364b Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 7 Sep 2010 13:01:21 -0700 Subject: fix docstrings and formatting --- nova/scheduler/chance.py | 8 ++------ nova/scheduler/driver.py | 25 +++++++------------------ nova/scheduler/manager.py | 4 +--- nova/scheduler/simple.py | 19 ++++++------------- 4 files changed, 16 insertions(+), 40 deletions(-) diff --git a/nova/scheduler/chance.py b/nova/scheduler/chance.py index 1054cdbf5..7fd09b053 100644 --- a/nova/scheduler/chance.py +++ b/nova/scheduler/chance.py @@ -27,14 +27,10 @@ from nova.scheduler import driver class ChanceScheduler(driver.Scheduler): - """ - Implements Scheduler as a random node selector - """ + """Implements Scheduler as a random node selector.""" def schedule(self, context, topic, *_args, **_kwargs): - """ - Picks a host that is up at random - """ + """Picks a host that is up at random.""" hosts = self.hosts_up(context, topic) if not hosts: diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index f5872e9c8..2e6a5a835 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -28,34 +28,25 @@ from nova import exception from nova import flags FLAGS = flags.FLAGS -flags.DEFINE_integer('service_down_time', - 60, - 'seconds without heartbeat that determines a ' - 'compute node to be down') +flags.DEFINE_integer('service_down_time', 60, + 'maximum time since last checkin for up service') class NoValidHost(exception.Error): - """There is no valid host for the command""" + """There is no valid host for the command.""" pass class Scheduler(object): - """ - The base class that all Scheduler clases should inherit from - """ + """The base class that all Scheduler clases should inherit from.""" @staticmethod def service_is_up(service): - """ - Given a service, return whether the service is considered 'up' by - if it's sent a heartbeat recently - """ + """Check whether a service is up based on last heartbeat.""" last_heartbeat = service['updated_at'] or service['created_at'] elapsed = datetime.datetime.now() - last_heartbeat return elapsed < datetime.timedelta(seconds=FLAGS.service_down_time) def hosts_up(self, context, topic): - """ - Return the list of hosts that have a running service for topic - """ + """Return the list of hosts that have a running service for topic.""" services = db.service_get_all_by_topic(context, topic) return [service.host @@ -63,7 +54,5 @@ class Scheduler(object): if self.service_is_up(service)] def schedule(self, context, topic, *_args, **_kwargs): - """ - Must override at least this method for scheduler to work - """ + """Must override at least this method for scheduler to work.""" raise NotImplementedError("Must implement a fallback schedule") diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 1755a6fef..1cabd82c6 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -37,9 +37,7 @@ flags.DEFINE_string('scheduler_driver', class SchedulerManager(manager.Manager): - """ - Chooses a host to run instances on. - """ + """Chooses a host to run instances on.""" def __init__(self, scheduler_driver=None, *args, **kwargs): if not scheduler_driver: scheduler_driver = FLAGS.scheduler_driver diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py index d10ddabac..ea4eef98e 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -35,14 +35,10 @@ flags.DEFINE_integer("max_networks", 1000, "maximum number of networks to allow per host") class SimpleScheduler(chance.ChanceScheduler): - """ - Implements Naive Scheduler that tries to find least loaded host - """ + """Implements Naive Scheduler that tries to find least loaded host.""" def schedule_run_instance(self, context, _instance_id, *_args, **_kwargs): - """ - Picks a host that is up and has the fewest running instances - """ + """Picks a host that is up and has the fewest running instances.""" results = db.service_get_all_compute_sorted(context) for result in results: @@ -54,9 +50,7 @@ class SimpleScheduler(chance.ChanceScheduler): raise driver.NoValidHost("No hosts found") def schedule_create_volume(self, context, _volume_id, *_args, **_kwargs): - """ - Picks a host that is up and has the fewest volumes - """ + """Picks a host that is up and has the fewest volumes.""" results = db.service_get_all_volume_sorted(context) for result in results: @@ -67,10 +61,9 @@ class SimpleScheduler(chance.ChanceScheduler): return service['host'] raise driver.NoValidHost("No hosts found") - def schedule_set_network_host(self, context, _network_id, *_args, **_kwargs): - """ - Picks a host that is up and has the fewest networks - """ + def schedule_set_network_host(self, context, _network_id, + *_args, **_kwargs): + """Picks a host that is up and has the fewest networks.""" results = db.service_get_all_network_sorted(context) for result in results: -- cgit From c3531537aef54b2c27a6e1f28308eac98aec08ba Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 7 Sep 2010 18:32:08 -0700 Subject: whitespace fixes --- nova/process.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/nova/process.py b/nova/process.py index 069310802..259e62358 100644 --- a/nova/process.py +++ b/nova/process.py @@ -35,8 +35,8 @@ FLAGS = flags.FLAGS flags.DEFINE_integer('process_pool_size', 4, 'Number of processes to use in the process pool') -# This is based on _BackRelay from twister.internal.utils, but modified to -# capture both stdout and stderr, without odd stderr handling, and also to +# This is based on _BackRelay from twister.internal.utils, but modified to +# capture both stdout and stderr, without odd stderr handling, and also to # handle stdin class BackRelayWithInput(protocol.ProcessProtocol): """ @@ -46,21 +46,21 @@ class BackRelayWithInput(protocol.ProcessProtocol): @ivar deferred: A L{Deferred} which will be called back with all of stdout and all of stderr as well (as a tuple). C{terminate_on_stderr} is true and any bytes are received over stderr, this will fire with an - L{_ProcessExecutionError} instance and the attribute will be set to + L{_ProcessExecutionError} instance and the attribute will be set to C{None}. - @ivar onProcessEnded: If C{terminate_on_stderr} is false and bytes are - received over stderr, this attribute will refer to a L{Deferred} which - will be called back when the process ends. This C{Deferred} is also - associated with the L{_ProcessExecutionError} which C{deferred} fires - with earlier in this case so that users can determine when the process + @ivar onProcessEnded: If C{terminate_on_stderr} is false and bytes are + received over stderr, this attribute will refer to a L{Deferred} which + will be called back when the process ends. This C{Deferred} is also + associated with the L{_ProcessExecutionError} which C{deferred} fires + with earlier in this case so that users can determine when the process has actually ended, in addition to knowing when bytes have been received via stderr. """ - def __init__(self, deferred, cmd, started_deferred=None, - terminate_on_stderr=False, check_exit_code=True, - process_input=None): + def __init__(self, deferred, cmd, started_deferred=None, + terminate_on_stderr=False, check_exit_code=True, + process_input=None): self.deferred = deferred self.cmd = cmd self.stdout = StringIO.StringIO() @@ -70,12 +70,12 @@ class BackRelayWithInput(protocol.ProcessProtocol): self.check_exit_code = check_exit_code self.process_input = process_input self.on_process_ended = None - + def _build_execution_error(self, exit_code=None): - return ProcessExecutionError( cmd=self.cmd, - exit_code=exit_code, - stdout=self.stdout.getvalue(), - stderr=self.stderr.getvalue()) + return ProcessExecutionError(cmd=self.cmd, + exit_code=exit_code, + stdout=self.stdout.getvalue(), + stderr=self.stderr.getvalue()) def errReceived(self, text): self.stderr.write(text) @@ -101,7 +101,7 @@ class BackRelayWithInput(protocol.ProcessProtocol): self.deferred.callback((stdout, stderr)) except: # NOTE(justinsb): This logic is a little suspicious to me... - # If the callback throws an exception, then errback will be + # If the callback throws an exception, then errback will be # called also. However, this is what the unit tests test for... self.deferred.errback(self._build_execution_error(exit_code)) elif self.on_process_ended is not None: @@ -115,8 +115,8 @@ class BackRelayWithInput(protocol.ProcessProtocol): self.transport.write(self.process_input) self.transport.closeStdin() -def get_process_output(executable, args=None, env=None, path=None, - process_reactor=None, check_exit_code=True, +def get_process_output(executable, args=None, env=None, path=None, + process_reactor=None, check_exit_code=True, process_input=None, started_deferred=None, terminate_on_stderr=False): if process_reactor is None: @@ -130,8 +130,8 @@ def get_process_output(executable, args=None, env=None, path=None, process_handler = BackRelayWithInput( deferred, cmd, - started_deferred=started_deferred, - check_exit_code=check_exit_code, + started_deferred=started_deferred, + check_exit_code=check_exit_code, process_input=process_input, terminate_on_stderr=terminate_on_stderr) # NOTE(vish): commands come in as unicode, but self.executes needs @@ -139,7 +139,7 @@ def get_process_output(executable, args=None, env=None, path=None, executable = str(executable) if not args is None: args = [str(x) for x in args] - process_reactor.spawnProcess( process_handler, executable, + process_reactor.spawnProcess( process_handler, executable, (executable,)+tuple(args), env, path) return deferred -- cgit From 6591ac066f1c6f7ca74c540fe5f39033fb41cd10 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 7 Sep 2010 18:32:31 -0700 Subject: one more whitespace fix --- nova/process.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/process.py b/nova/process.py index 259e62358..c3b077dc2 100644 --- a/nova/process.py +++ b/nova/process.py @@ -139,8 +139,8 @@ def get_process_output(executable, args=None, env=None, path=None, executable = str(executable) if not args is None: args = [str(x) for x in args] - process_reactor.spawnProcess( process_handler, executable, - (executable,)+tuple(args), env, path) + process_reactor.spawnProcess(process_handler, executable, + (executable,)+tuple(args), env, path) return deferred -- cgit From fc5e1c6f0bee14fdb85ad138324062ceaa598eee Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 7 Sep 2010 21:53:40 -0700 Subject: a few formatting fixes and moved exception --- nova/exception.py | 12 ++++++++++++ nova/process.py | 4 ++-- nova/utils.py | 17 +++++------------ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/nova/exception.py b/nova/exception.py index 29bcb17f8..b8894758f 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -26,6 +26,18 @@ import sys import traceback +class ProcessExecutionError(IOError): + def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, + description=None): + if description is None: + description = "Unexpected error while running command." + if exit_code is None: + exit_code = '-' + message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( + description, cmd, exit_code, stdout, stderr) + IOError.__init__(self, message) + + class Error(Exception): def __init__(self, message=None): super(Error, self).__init__(message) diff --git a/nova/process.py b/nova/process.py index c3b077dc2..5a5d8cbd2 100644 --- a/nova/process.py +++ b/nova/process.py @@ -29,7 +29,7 @@ from twisted.internet import protocol from twisted.internet import reactor from nova import flags -from nova.utils import ProcessExecutionError +from nova.exception import ProcessExecutionError FLAGS = flags.FLAGS flags.DEFINE_integer('process_pool_size', 4, @@ -126,7 +126,7 @@ def get_process_output(executable, args=None, env=None, path=None, deferred = defer.Deferred() cmd = executable if args: - cmd = cmd + " " + ' '.join(args) + cmd = " ".join([cmd] + args) process_handler = BackRelayWithInput( deferred, cmd, diff --git a/nova/utils.py b/nova/utils.py index b8abb5388..d302412ad 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -31,21 +31,12 @@ import sys from nova import exception from nova import flags +from nova.exception import ProcessExecutionError FLAGS = flags.FLAGS TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" -class ProcessExecutionError(IOError): - def __init__( self, stdout=None, stderr=None, exit_code=None, cmd=None, - description=None): - if description is None: - description = "Unexpected error while running command." - if exit_code is None: - exit_code = '-' - message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( - description, cmd, exit_code, stdout, stderr) - IOError.__init__(self, message) def import_class(import_str): """Returns a class from a string including module and class""" @@ -118,8 +109,10 @@ def runthis(prompt, cmd, check_exit_code = True): exit_code = subprocess.call(cmd.split(" ")) logging.debug(prompt % (exit_code)) if check_exit_code and exit_code <> 0: - raise Exception( "Unexpected exit code: %s from cmd: %s" - % (exit_code, cmd)) + raise ProcessExecutionError(exit_code=exit_code, + stdout=None, + stderr=None, + cmd=cmd) def generate_uid(topic, size=8): -- cgit From 607162ffe86d7d2b5bd9eb6f16a6ee4405892fc6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 8 Sep 2010 01:53:07 -0700 Subject: make timestamps for instances and volumes, includes additions to get deleted objects from db using deleted flag. --- nova/compute/manager.py | 5 +++++ nova/db/sqlalchemy/api.py | 42 ++++++++++++++++++++++++++---------------- nova/db/sqlalchemy/models.py | 20 ++++++++++++-------- nova/tests/compute_unittest.py | 20 ++++++++++++++++++++ 4 files changed, 63 insertions(+), 24 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 878205a36..7f6b49f90 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -21,6 +21,7 @@ Handles all code relating to instances (guest vms) """ import base64 +import datetime import logging import os @@ -83,6 +84,8 @@ class ComputeManager(manager.Manager): try: yield self.driver.spawn(instance_ref) + now = datetime.datetime.now() + self.db.instance_update(None, instance_id, {'launched_at': now}) except Exception: # pylint: disable-msg=W0702 logging.exception("instance %s: Failed to spawn", instance_ref['name']) @@ -107,6 +110,8 @@ class ComputeManager(manager.Manager): power_state.NOSTATE, 'shutting_down') yield self.driver.destroy(instance_ref) + now = datetime.datetime.now() + self.db.instance_update(None, instance_id, {'terminated_at': now}) # TODO(ja): should we keep it in a terminated state for a bit? self.db.instance_destroy(context, instance_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 391892214..fa9c77181 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -28,9 +28,19 @@ from sqlalchemy import or_ FLAGS = flags.FLAGS + # NOTE(vish): disabling docstring pylint because the docstrings are # in the interface definition # pylint: disable-msg=C0111 +def _deleted(context): + """Calcultates whether to include deleted objects based on context. + + Currently just looks for a flag called deleted in the context dict. + """ + if not context: + return False + return context.get('deleted', False) + ################### @@ -236,19 +246,19 @@ def instance_destroy(_context, instance_id): instance_ref.delete(session=session) -def instance_get(_context, instance_id): - return models.Instance.find(instance_id) +def instance_get(context, instance_id): + return models.Instance.find(instance_id, deleted=_deleted(context)) -def instance_get_all(_context): - return models.Instance.all() +def instance_get_all(context): + return models.Instance.all(deleted=_deleted(context)) -def instance_get_by_project(_context, project_id): +def instance_get_by_project(context, project_id): session = get_session() return session.query(models.Instance ).filter_by(project_id=project_id - ).filter_by(deleted=False + ).filter_by(deleted=_deleted(context) ).all() @@ -260,8 +270,8 @@ def instance_get_by_reservation(_context, reservation_id): ).all() -def instance_get_by_str(_context, str_id): - return models.Instance.find_by_str(str_id) +def instance_get_by_str(context, str_id): + return models.Instance.find_by_str(str_id, deleted=_deleted(context)) def instance_get_fixed_address(_context, instance_id): @@ -562,24 +572,24 @@ def volume_detached(_context, volume_id): volume_ref.save(session=session) -def volume_get(_context, volume_id): - return models.Volume.find(volume_id) +def volume_get(context, volume_id): + return models.Volume.find(volume_id, deleted=_deleted(context)) -def volume_get_all(_context): - return models.Volume.all() +def volume_get_all(context): + return models.Volume.all(deleted=_deleted(context)) -def volume_get_by_project(_context, project_id): +def volume_get_by_project(context, project_id): session = get_session() return session.query(models.Volume ).filter_by(project_id=project_id - ).filter_by(deleted=False + ).filter_by(deleted=_deleted(context) ).all() -def volume_get_by_str(_context, str_id): - return models.Volume.find_by_str(str_id) +def volume_get_by_str(context, str_id): + return models.Volume.find_by_str(str_id, deleted=_deleted(context)) def volume_get_host(context, volume_id): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index fe3a77a52..064894e97 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -48,45 +48,46 @@ class NovaBase(object): __prefix__ = 'none' created_at = Column(DateTime, default=func.now()) updated_at = Column(DateTime, onupdate=datetime.datetime.now) + deleted_at = Column(DateTime) deleted = Column(Boolean, default=False) @classmethod - def all(cls, session=None): + def all(cls, session=None, deleted=False): """Get all objects of this type""" if not session: session = get_session() return session.query(cls) \ - .filter_by(deleted=False) \ + .filter_by(deleted=deleted) \ .all() @classmethod - def count(cls, session=None): + def count(cls, session=None, deleted=False): """Count objects of this type""" if not session: session = get_session() return session.query(cls) \ - .filter_by(deleted=False) \ + .filter_by(deleted=deleted) \ .count() @classmethod - def find(cls, obj_id, session=None): + def find(cls, obj_id, session=None, deleted=False): """Find object by id""" if not session: session = get_session() try: return session.query(cls) \ .filter_by(id=obj_id) \ - .filter_by(deleted=False) \ + .filter_by(deleted=deleted) \ .one() except exc.NoResultFound: new_exc = exception.NotFound("No model for id %s" % obj_id) raise new_exc.__class__, new_exc, sys.exc_info()[2] @classmethod - def find_by_str(cls, str_id, session=None): + def find_by_str(cls, str_id, session=None, deleted=False): """Find object by str_id""" int_id = int(str_id.rpartition('-')[2]) - return cls.find(int_id, session=session) + return cls.find(int_id, session=session, deleted=deleted) @property def str_id(self): @@ -103,6 +104,7 @@ class NovaBase(object): def delete(self, session=None): """Delete this object""" self.deleted = True + self.deleted_at = datetime.datetime.now() self.save(session=session) def __setitem__(self, key, value): @@ -230,6 +232,8 @@ class Instance(BASE, NovaBase): reservation_id = Column(String(255)) mac_address = Column(String(255)) + launched_at = Column(DateTime) + terminated_at = Column(DateTime) # TODO(vish): see Ewan's email about state improvements, probably # should be in a driver base class or some such # vmstate_state = running, halted, suspended, paused diff --git a/nova/tests/compute_unittest.py b/nova/tests/compute_unittest.py index 746c035d6..e5da6b054 100644 --- a/nova/tests/compute_unittest.py +++ b/nova/tests/compute_unittest.py @@ -18,6 +18,8 @@ """ Tests For Compute """ + +import datetime import logging from twisted.internet import defer @@ -79,6 +81,24 @@ class ComputeTestCase(test.TrialTestCase): logging.info("After terminating instances: %s", instances) self.assertEqual(len(instances), 0) + @defer.inlineCallbacks + def test_run_terminate_timestamps(self): + """Make sure it is possible to run and terminate instance""" + instance_id = self._create_instance() + instance_ref = db.instance_get(self.context, instance_id) + self.assertEqual(instance_ref['launched_at'], None) + self.assertEqual(instance_ref['terminated_at'], None) + launch = datetime.datetime.now() + yield self.compute.run_instance(self.context, instance_id) + instance_ref = db.instance_get(self.context, instance_id) + self.assert_(instance_ref['launched_at'] > launch) + self.assertEqual(instance_ref['terminated_at'], None) + terminate = datetime.datetime.now() + yield self.compute.terminate_instance(self.context, instance_id) + instance_ref = db.instance_get({'deleted': True}, instance_id) + self.assert_(instance_ref['launched_at'] < terminate) + self.assert_(instance_ref['terminated_at'] > terminate) + @defer.inlineCallbacks def test_reboot(self): """Ensure instance can be rebooted""" -- cgit From 37ca50b1731a975d3106af05cd46b02d3f7a2a06 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 8 Sep 2010 02:02:41 -0700 Subject: deleted typo --- nova/db/sqlalchemy/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index cab0c63b5..58f6d4f61 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -335,7 +335,7 @@ class FixedIp(BASE, NovaBase): return self.address @classmethod - def find_by_str(cls, str_id, session=None, deleted=deleted): + def find_by_str(cls, str_id, session=None, deleted=False): if not session: session = get_session() try: @@ -364,7 +364,7 @@ class FloatingIp(BASE, NovaBase): return self.address @classmethod - def find_by_str(cls, str_id, session=None, deleted=deleted): + def find_by_str(cls, str_id, session=None, deleted=False): if not session: session = get_session() try: -- cgit From 010a1d2b49f50d7cd763b3789bfd2d6789e2279b Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 02:23:31 -0700 Subject: missing deleted ref --- nova/db/sqlalchemy/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 58f6d4f61..d460fbb4b 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -168,7 +168,7 @@ class Service(BASE, NovaBase): report_count = Column(Integer, nullable=False, default=0) @classmethod - def find_by_args(cls, host, binary, session=None): + def find_by_args(cls, host, binary, session=None, deleted=False): if not session: session = get_session() try: -- cgit From 83402810be11111e3f61f3a9c3771bb96161e551 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 02:30:07 -0700 Subject: put soren's fancy path code in scheduler bin as well --- bin/nova-scheduler | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bin/nova-scheduler b/bin/nova-scheduler index 97f98b17f..38a8f213f 100755 --- a/bin/nova-scheduler +++ b/bin/nova-scheduler @@ -21,6 +21,17 @@ Twistd daemon for the nova scheduler nodes. """ +import os +import sys + +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +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 service from nova import twistd -- cgit From e6369486f43423e9649a7b4d046d3c92bf1c85e9 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 03:33:12 -0700 Subject: don't fail in db if context isn't a dict, since we're still using a class based context in the api --- nova/db/sqlalchemy/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index f777dcc69..817ff9ac3 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -33,11 +33,11 @@ FLAGS = flags.FLAGS # in the interface definition # pylint: disable-msg=C0111 def _deleted(context): - """Calcultates whether to include deleted objects based on context. + """Calculates whether to include deleted objects based on context. Currently just looks for a flag called deleted in the context dict. """ - if not context: + if not hasattr(context, 'get'): return False return context.get('deleted', False) -- cgit From b8aaebee171876ffd0e115ea3a19d4524ca16d99 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 06:06:29 -0700 Subject: switch to using utcnow --- nova/compute/manager.py | 4 ++-- nova/db/sqlalchemy/models.py | 4 ++-- nova/tests/compute_unittest.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 4b29add2d..ae7099812 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -84,7 +84,7 @@ class ComputeManager(manager.Manager): try: yield self.driver.spawn(instance_ref) - now = datetime.datetime.now() + now = datetime.datetime.utcnow() self.db.instance_update(None, instance_id, {'launched_at': now}) except Exception: # pylint: disable-msg=W0702 logging.exception("instance %s: Failed to spawn", @@ -112,7 +112,7 @@ class ComputeManager(manager.Manager): power_state.NOSTATE, 'shutting_down') yield self.driver.destroy(instance_ref) - now = datetime.datetime.now() + now = datetime.datetime.utcnow() self.db.instance_update(None, instance_id, {'terminated_at': now}) # TODO(ja): should we keep it in a terminated state for a bit? diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index d460fbb4b..4977fc0f1 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -46,8 +46,8 @@ class NovaBase(object): __table_args__ = {'mysql_engine': 'InnoDB'} __table_initialized__ = False __prefix__ = 'none' - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, onupdate=datetime.datetime.now) + created_at = Column(DateTime, default=datetime.datetime.utcnow) + updated_at = Column(DateTime, onupdate=datetime.datetime.utcnow) deleted_at = Column(DateTime) deleted = Column(Boolean, default=False) diff --git a/nova/tests/compute_unittest.py b/nova/tests/compute_unittest.py index e5da6b054..8a7f7b649 100644 --- a/nova/tests/compute_unittest.py +++ b/nova/tests/compute_unittest.py @@ -88,12 +88,12 @@ class ComputeTestCase(test.TrialTestCase): instance_ref = db.instance_get(self.context, instance_id) self.assertEqual(instance_ref['launched_at'], None) self.assertEqual(instance_ref['terminated_at'], None) - launch = datetime.datetime.now() + launch = datetime.datetime.utcnow() yield self.compute.run_instance(self.context, instance_id) instance_ref = db.instance_get(self.context, instance_id) self.assert_(instance_ref['launched_at'] > launch) self.assertEqual(instance_ref['terminated_at'], None) - terminate = datetime.datetime.now() + terminate = datetime.datetime.utcnow() yield self.compute.terminate_instance(self.context, instance_id) instance_ref = db.instance_get({'deleted': True}, instance_id) self.assert_(instance_ref['launched_at'] < terminate) -- cgit From 64d073ca080f194680c14ccdf3b2b08e50d8eade Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 06:55:54 -0700 Subject: speed up describe by loading fixed and floating ips --- nova/db/sqlalchemy/api.py | 9 ++++++++- nova/endpoint/cloud.py | 8 ++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 817ff9ac3..958036707 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -25,6 +25,7 @@ from nova import flags from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ +from sqlalchemy.orm import joinedload_all FLAGS = flags.FLAGS @@ -251,12 +252,17 @@ def instance_get(context, instance_id): def instance_get_all(context): - return models.Instance.all(deleted=_deleted(context)) + session = get_session() + return session.query(models.Instance + ).options(joinedload_all('fixed_ip.floating_ips') + ).filter_by(deleted=_deleted(context) + ).all() def instance_get_by_project(context, project_id): session = get_session() return session.query(models.Instance + ).options(joinedload_all('fixed_ip.floating_ips') ).filter_by(project_id=project_id ).filter_by(deleted=_deleted(context) ).all() @@ -265,6 +271,7 @@ def instance_get_by_project(context, project_id): def instance_get_by_reservation(_context, reservation_id): session = get_session() return session.query(models.Instance + ).options(joinedload_all('fixed_ip.floating_ips') ).filter_by(reservation_id=reservation_id ).filter_by(deleted=False ).all() diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 709c967bb..6958eacfe 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -379,11 +379,11 @@ class CloudController(object): 'code': instance['state'], 'name': instance['state_description'] } - floating_addr = db.instance_get_floating_address(context, - instance['id']) + floating_addr = None + if instance['fixed_ip']['floating_ips']: + floating_addr = instance['fixed_ip']['floating_ips'][0]['str_id'] i['publicDnsName'] = floating_addr - fixed_addr = db.instance_get_fixed_address(context, - instance['id']) + fixed_addr = instance['fixed_ip']['str_id'] i['privateDnsName'] = fixed_addr if not i['publicDnsName']: i['publicDnsName'] = i['privateDnsName'] -- cgit From 33631c21e71d85910a20997881735aa43160d36a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 07:47:30 -0700 Subject: floating ip commands --- bin/nova-manage | 31 +++++++++++++++++++++++++++++++ nova/db/api.py | 25 ++++++++++++++++++++----- nova/db/sqlalchemy/api.py | 32 ++++++++++++++++++++++++++------ 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index ecef5d555..408a2d9c8 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -26,6 +26,8 @@ import os import sys import time +import IPy + # If ../nova/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), @@ -218,12 +220,41 @@ class ProjectCommands(object): with open(filename, 'w') as f: f.write(zip_file) +class FloatingIpCommands(object): + """Class for managing floating ip.""" + + def create(self, host, range): + """Creates floating ips for host by range + arguments: host ip_range""" + for address in IPy.IP(range): + db.floating_ip_create(None, {'address': str(address), + 'host': host}) + + def delete(self, ip_range): + """Deletes floating ips by range + arguments: range""" + for address in IPy.IP(ip_range): + db.floating_ip_destroy(None, str(address)) + + + def list(self, host=None): + """Lists all floating ips (optionally by host) + arguments: [host]""" + if host == None: + floating_ips = db.floating_ip_get_all(None) + else: + floating_ips = db.floating_ip_get_all_by_host(None, host) + for floating_ip in floating_ips: + print "%s\t%s\ti-%s" % (floating_ip['host'], + floating_ip['address'], + floating_ip['instance_id']) CATEGORIES = [ ('user', UserCommands), ('project', ProjectCommands), ('role', RoleCommands), ('vpn', VpnCommands), + ('floating', FloatingIpCommands) ] diff --git a/nova/db/api.py b/nova/db/api.py index 59313b0af..d263f8c94 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -91,6 +91,16 @@ def floating_ip_create(context, values): return IMPL.floating_ip_create(context, values) +def floating_ip_deallocate(context, address): + """Deallocate an floating ip by address""" + return IMPL.floating_ip_deallocate(context, address) + + +def floating_ip_destroy(context, address): + """Destroy the floating_ip or raise if it does not exist.""" + return IMPL.floating_ip_destroy(context, address) + + def floating_ip_disassociate(context, address): """Disassociate an floating ip from a fixed ip by address. @@ -99,11 +109,6 @@ def floating_ip_disassociate(context, address): return IMPL.floating_ip_disassociate(context, address) -def floating_ip_deallocate(context, address): - """Deallocate an floating ip by address""" - return IMPL.floating_ip_deallocate(context, address) - - def floating_ip_fixed_ip_associate(context, floating_address, fixed_address): """Associate an floating ip to a fixed_ip by address.""" return IMPL.floating_ip_fixed_ip_associate(context, @@ -111,6 +116,16 @@ def floating_ip_fixed_ip_associate(context, floating_address, fixed_address): fixed_address) +def floating_ip_get_all(context): + """Get all floating ips.""" + return IMPL.floating_ip_get_all(context) + + +def floating_ip_get_all_by_host(context, host): + """Get all floating ips.""" + return IMPL.floating_ip_get_all_by_host(context, host) + + def floating_ip_get_by_address(context, address): """Get a floating ip by address or raise if it doesn't exist.""" return IMPL.floating_ip_get_by_address(context, address) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 8b94f6036..9180a2a55 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -100,6 +100,23 @@ def floating_ip_fixed_ip_associate(_context, floating_address, fixed_address): floating_ip_ref.save(session=session) +def floating_ip_deallocate(_context, address): + session = get_session() + with session.begin(): + floating_ip_ref = models.FloatingIp.find_by_str(address, + session=session) + floating_ip_ref['project_id'] = None + floating_ip_ref.save(session=session) + + +def floating_ip_destroy(_context, address): + session = get_session() + with session.begin(): + floating_ip_ref = models.FloatingIp.find_by_str(address, + session=session) + floating_ip_ref.delete(session=session) + + def floating_ip_disassociate(_context, address): session = get_session() with session.begin(): @@ -115,14 +132,17 @@ def floating_ip_disassociate(_context, address): return fixed_ip_address -def floating_ip_deallocate(_context, address): +def floating_ip_get_all(_context): + return models.FloatingIp.all() + + +def floating_ip_get_all_by_host(_context, host): session = get_session() with session.begin(): - floating_ip_ref = models.FloatingIp.find_by_str(address, - session=session) - floating_ip_ref['project_id'] = None - floating_ip_ref.save(session=session) - + return session.query(models.FloatingIp + ).filter_by(host=host + ).filter_by(deleted=False + ).all() def floating_ip_get_by_address(_context, address): return models.FloatingIp.find_by_str(address) -- cgit From 4dcc4bc4b459b454431ca60bec0dead2146f52af Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 07:53:57 -0700 Subject: list command for floating ips --- bin/nova-manage | 9 ++++++--- nova/db/sqlalchemy/api.py | 16 ++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 408a2d9c8..56191252a 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -245,9 +245,12 @@ class FloatingIpCommands(object): else: floating_ips = db.floating_ip_get_all_by_host(None, host) for floating_ip in floating_ips: - print "%s\t%s\ti-%s" % (floating_ip['host'], - floating_ip['address'], - floating_ip['instance_id']) + instance = None + if floating_ip['fixed_ip']: + instance = floating_ip['fixed_ip']['instance']['str_id'] + print "%s\t%s\t%s" % (floating_ip['host'], + floating_ip['address'], + instance) CATEGORIES = [ ('user', UserCommands), diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4330c86a9..eb39166ac 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -144,16 +144,20 @@ def floating_ip_disassociate(_context, address): def floating_ip_get_all(_context): - return models.FloatingIp.all() + session = get_session() + return session.query(models.FloatingIp + ).options(joinedload_all('fixed_ip.instance') + ).filter_by(deleted=False + ).all() def floating_ip_get_all_by_host(_context, host): session = get_session() - with session.begin(): - return session.query(models.FloatingIp - ).filter_by(host=host - ).filter_by(deleted=False - ).all() + return session.query(models.FloatingIp + ).options(joinedload_all('fixed_ip.instance') + ).filter_by(host=host + ).filter_by(deleted=False + ).all() def floating_ip_get_by_address(_context, address): return models.FloatingIp.find_by_str(address) -- cgit From 2cd0ac795a67bb7416df8c8a6fccccf78fc5e430 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 08:55:09 -0700 Subject: fixed logic in set_state code to stop endless loops --- nova/virt/libvirt_conn.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index febb0ce9b..d868e083c 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -139,10 +139,9 @@ class LibvirtConnection(object): timer = task.LoopingCall(f=None) def _wait_for_shutdown(): try: - db.instance_set_state(None, - instance['id'], - self.get_info(instance['name'])['state']) - if instance.state == power_state.SHUTDOWN: + state = self.get_info(instance['name'])['state'] + db.instance_set_state(None, instance['id'], state) + if state == power_state.SHUTDOWN: timer.stop() d.callback(None) except Exception: @@ -190,10 +189,9 @@ class LibvirtConnection(object): timer = task.LoopingCall(f=None) def _wait_for_reboot(): try: - db.instance_set_state(None, - instance['id'], - self.get_info(instance['name'])['state']) - if instance.state == power_state.RUNNING: + state = self.get_info(instance['name'])['state'] + db.instance_set_state(None, instance['id'], state) + if state == power_state.RUNNING: logging.debug('instance %s: rebooted', instance['name']) timer.stop() d.callback(None) @@ -226,10 +224,9 @@ class LibvirtConnection(object): timer = task.LoopingCall(f=None) def _wait_for_boot(): try: - db.instance_set_state(None, - instance['id'], - self.get_info(instance['name'])['state']) - if instance.state == power_state.RUNNING: + state = self.get_info(instance['name'])['state'] + db.instance_set_state(None, instance['id'], state) + if state == power_state.RUNNING: logging.debug('instance %s: booted', instance['name']) timer.stop() local_d.callback(None) -- cgit From 6c4d301eab48b841b4b6ca19a96b3e9748f27b57 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 09:52:24 -0700 Subject: fix logging for scheduler to properly display method name --- nova/scheduler/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 1cabd82c6..0ad7ca86b 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -63,4 +63,4 @@ class SchedulerManager(manager.Manager): rpc.cast(db.queue_get_for(context, topic, host), {"method": method, "args": kwargs}) - logging.debug("Casting to %s %s for %s", topic, host, self.method) + logging.debug("Casting to %s %s for %s", topic, host, method) -- cgit From e88cb0063157d13a590a414b6989d875c6a1ba8a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 09:59:55 -0700 Subject: fix volume delete issue and volume hostname display --- nova/endpoint/cloud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 709c967bb..c7355ccd2 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -260,7 +260,7 @@ class CloudController(object): v['status'] = '%s (%s, %s, %s, %s)' % ( volume['status'], volume['user_id'], - 'host', + volume['host'], volume['instance_id'], volume['mountpoint']) if volume['attach_status'] == 'attached': @@ -635,7 +635,7 @@ class CloudController(object): # TODO: return error if not authorized volume_ref = db.volume_get_by_str(context, volume_id) host = db.volume_get_host(context, volume_ref['id']) - rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), + rpc.cast(db.queue_get_for(context, FLAGS.volume_topic, host), {"method": "delete_volume", "args": {"context": None, "volume_id": volume_id}}) -- cgit From 1f5524f64a09502a1d225001f4c5d3039551fa07 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 10:38:32 -0700 Subject: pass volume['id'] instead of string id to delete volume --- nova/endpoint/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index c7355ccd2..cb625bfa8 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -638,7 +638,7 @@ class CloudController(object): rpc.cast(db.queue_get_for(context, FLAGS.volume_topic, host), {"method": "delete_volume", "args": {"context": None, - "volume_id": volume_id}}) + "volume_id": volume_ref['id']}}) return defer.succeed(True) @rbac.allow('all') -- cgit From 9165579a501cf9e248ac5d2d43a80f4abbb58365 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 10:43:19 -0700 Subject: remove extraneous get_host calls that were requiring an extra db trip --- nova/db/api.py | 15 --------------- nova/db/sqlalchemy/api.py | 15 --------------- nova/endpoint/cloud.py | 12 ++++++------ 3 files changed, 6 insertions(+), 36 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index 59313b0af..0cab7db8e 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -220,11 +220,6 @@ def instance_get_by_str(context, str_id): return IMPL.instance_get_by_str(context, str_id) -def instance_get_host(context, instance_id): - """Get the host that the instance is running on.""" - return IMPL.instance_get_host(context, instance_id) - - def instance_is_vpn(context, instance_id): """True if instance is a vpn.""" return IMPL.instance_is_vpn(context, instance_id) @@ -298,11 +293,6 @@ def network_get_by_bridge(context, bridge): return IMPL.network_get_by_bridge(context, bridge) -def network_get_host(context, network_id): - """Get host assigned to network or raise""" - return IMPL.network_get_host(context, network_id) - - def network_get_index(context, network_id): """Get non-conflicting index for network""" return IMPL.network_get_index(context, network_id) @@ -424,11 +414,6 @@ def volume_get_by_str(context, str_id): return IMPL.volume_get_by_str(context, str_id) -def volume_get_host(context, volume_id): - """Get the host that the volume is running on.""" - return IMPL.volume_get_host(context, volume_id) - - def volume_get_shelf_and_blade(context, volume_id): """Get the shelf and blade allocated to the volume.""" return IMPL.volume_get_shelf_and_blade(context, volume_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 8b94f6036..326a01593 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -285,11 +285,6 @@ def instance_get_floating_address(_context, instance_id): return instance_ref.fixed_ip.floating_ips[0]['address'] -def instance_get_host(context, instance_id): - instance_ref = instance_get(context, instance_id) - return instance_ref['host'] - - def instance_is_vpn(context, instance_id): # TODO(vish): Move this into image code somewhere instance_ref = instance_get(context, instance_id) @@ -404,11 +399,6 @@ def network_get_by_bridge(_context, bridge): return rv -def network_get_host(context, network_id): - network_ref = network_get(context, network_id) - return network_ref['host'] - - def network_get_index(_context, network_id): session = get_session() with session.begin(): @@ -582,11 +572,6 @@ def volume_get_by_str(_context, str_id): return models.Volume.find_by_str(str_id) -def volume_get_host(context, volume_id): - volume_ref = volume_get(context, volume_id) - return volume_ref['host'] - - def volume_get_instance(_context, volume_id): session = get_session() with session.begin(): diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index cb625bfa8..6cda79406 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -299,7 +299,7 @@ class CloudController(object): if volume_ref['attach_status'] == "attached": raise exception.ApiError("Volume is already attached") instance_ref = db.instance_get_by_str(context, instance_id) - host = db.instance_get_host(context, instance_ref['id']) + host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "attach_volume", "args": {"context": None, @@ -323,7 +323,7 @@ class CloudController(object): if volume_ref['status'] == "available": raise exception.Error("Volume is already detached") try: - host = db.instance_get_host(context, instance_ref['id']) + host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "detach_volume", "args": {"context": None, @@ -483,7 +483,7 @@ class CloudController(object): def _get_network_topic(self, context): """Retrieves the network host for a project""" network_ref = db.project_get_network(context, context.project.id) - host = db.network_get_host(context, network_ref['id']) + host = network_ref['host'] if not host: host = yield rpc.call(FLAGS.network_topic, {"method": "set_network_host", @@ -608,7 +608,7 @@ class CloudController(object): # we will need to cast here. db.fixed_ip_deallocate(context, address) - host = db.instance_get_host(context, instance_ref['id']) + host = instance_ref['host'] if host: rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "terminate_instance", @@ -623,7 +623,7 @@ class CloudController(object): """instance_id is a list of instance ids""" for id_str in instance_id: instance_ref = db.instance_get_by_str(context, id_str) - host = db.instance_get_host(context, instance_ref['id']) + host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "reboot_instance", "args": {"context": None, @@ -634,7 +634,7 @@ class CloudController(object): def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized volume_ref = db.volume_get_by_str(context, volume_id) - host = db.volume_get_host(context, volume_ref['id']) + host = volume_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.volume_topic, host), {"method": "delete_volume", "args": {"context": None, -- cgit From b3503ebcd7def01b523e0724ccec6fad9be12c93 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 11:02:37 -0700 Subject: fix describe addresses --- nova/endpoint/cloud.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 6cda79406..9a09454a2 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -420,10 +420,12 @@ class CloudController(object): iterator = db.floating_ip_get_by_project(context, context.project.id) for floating_ip_ref in iterator: - address = floating_ip_ref['id_str'] - instance_ref = db.floating_ip_get_instance(address) + address = floating_ip_ref['str_id'] + instance_id = None + if floating_ip_ref['instance']: + instance_id = floating_ip_ref['instance']['str_id'] address_rv = {'public_ip': address, - 'instance_id': instance_ref['id_str']} + 'instance_id': instance_id} if context.user.is_admin(): details = "%s (%s)" % (address_rv['instance_id'], floating_ip_ref['project_id']) -- cgit From c08c21d6ceeeb2d8241ae5222b744bed64d327f3 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 11:07:10 -0700 Subject: solution that works with this version --- nova/endpoint/cloud.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 9a09454a2..f84360c9c 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -421,9 +421,8 @@ class CloudController(object): context.project.id) for floating_ip_ref in iterator: address = floating_ip_ref['str_id'] - instance_id = None - if floating_ip_ref['instance']: - instance_id = floating_ip_ref['instance']['str_id'] + instance_ref = db.floating_ip_get_instance(context, address) + instance_id = instance_ref['str_id'] address_rv = {'public_ip': address, 'instance_id': instance_id} if context.user.is_admin(): -- cgit From 1f1422d5f262b20f4fa6266a3d62615d013d832c Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 11:17:14 -0700 Subject: faster describe_addresses --- nova/endpoint/cloud.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 2866474e6..26bae0652 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -421,8 +421,10 @@ class CloudController(object): context.project.id) for floating_ip_ref in iterator: address = floating_ip_ref['str_id'] - instance_ref = db.floating_ip_get_instance(context, address) - instance_id = instance_ref['str_id'] + instance_id = None + if (floating_ip_ref['fixed_ip'] + and floating_ip_ref['fixed_ip']['instance']): + instance_id = floating_ip_ref['fixed_ip']['instance']['str_id'] address_rv = {'public_ip': address, 'instance_id': instance_id} if context.user.is_admin(): -- cgit From 0173a908aa35d110cdcf11822e8419b95f0de410 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 12:38:33 -0700 Subject: floating_address is the name for the cast --- nova/endpoint/cloud.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 2866474e6..bb24d1f06 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -449,9 +449,9 @@ class CloudController(object): floating_ip_ref = db.floating_ip_get_by_address(context, public_ip) network_topic = yield self._get_network_topic(context) rpc.cast(network_topic, - {"method": "deallocate_floating_ip", - "args": {"context": None, - "floating_ip": floating_ip_ref['str_id']}}) + {"method": "deallocate_floating_ip", + "args": {"context": None, + "floating_address": floating_ip_ref['str_id']}}) defer.returnValue({'releaseResponse': ["Address released."]}) @rbac.allow('netadmin') @@ -462,11 +462,11 @@ class CloudController(object): floating_ip_ref = db.floating_ip_get_by_address(context, public_ip) network_topic = yield self._get_network_topic(context) rpc.cast(network_topic, - {"method": "associate_floating_ip", - "args": {"context": None, - "floating_ip": floating_ip_ref['str_id'], - "fixed_ip": fixed_ip_ref['str_id'], - "instance_id": instance_ref['id']}}) + {"method": "associate_floating_ip", + "args": {"context": None, + "floating_address": floating_ip_ref['str_id'], + "fixed_address": fixed_ip_ref['str_id'], + "instance_id": instance_ref['id']}}) defer.returnValue({'associateResponse': ["Address associated."]}) @rbac.allow('netadmin') @@ -475,9 +475,9 @@ class CloudController(object): floating_ip_ref = db.floating_ip_get_by_address(context, public_ip) network_topic = yield self._get_network_topic(context) rpc.cast(network_topic, - {"method": "disassociate_floating_ip", - "args": {"context": None, - "floating_ip": floating_ip_ref['str_id']}}) + {"method": "disassociate_floating_ip", + "args": {"context": None, + "floating_address": floating_ip_ref['str_id']}}) defer.returnValue({'disassociateResponse': ["Address disassociated."]}) @defer.inlineCallbacks @@ -487,9 +487,9 @@ class CloudController(object): host = network_ref['host'] if not host: host = yield rpc.call(FLAGS.network_topic, - {"method": "set_network_host", - "args": {"context": None, - "project_id": context.project.id}}) + {"method": "set_network_host", + "args": {"context": None, + "project_id": context.project.id}}) defer.returnValue(db.queue_get_for(context, FLAGS.network_topic, host)) @rbac.allow('projectmanager', 'sysadmin') -- cgit From 8b59df67277dab6533b0076569fecc50b437ec75 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 12:45:51 -0700 Subject: don't need to pass instance_id to network on associate --- nova/endpoint/cloud.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index bb24d1f06..397c9c554 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -465,8 +465,7 @@ class CloudController(object): {"method": "associate_floating_ip", "args": {"context": None, "floating_address": floating_ip_ref['str_id'], - "fixed_address": fixed_ip_ref['str_id'], - "instance_id": instance_ref['id']}}) + "fixed_address": fixed_ip_ref['str_id']}}) defer.returnValue({'associateResponse': ["Address associated."]}) @rbac.allow('netadmin') -- cgit From 0aabb8a6febca8d98a750d1bdc78f3160b9684fe Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 13:40:18 -0700 Subject: mocking out quotas --- nova/db/sqlalchemy/models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 679a44d21..2fcade7de 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -266,6 +266,20 @@ class Volume(BASE, NovaBase): attach_status = Column(String(255)) # TODO(vish): enum +class Quota(BASE, NovaBase): + """Represents quota overrides for a project""" + __tablename__ = 'quotas' + id = Column(Integer, primary_key=True) + + project_id = Column(String(255)) + + instances = Column(Integer) + cores = Column(Integer) + volumes = Column(Integer) + gigabytes = Column(Integer) + floating_ips = Column(Integer) + + class ExportDevice(BASE, NovaBase): """Represates a shelf and blade that a volume can be exported on""" __tablename__ = 'export_devices' -- cgit From c577e91ee3a3eb87a393da2449cab95069a785f4 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 20:10:31 -0700 Subject: database support for quotas --- nova/db/api.py | 27 +++++++++++++++++++++ nova/db/sqlalchemy/api.py | 43 +++++++++++++++++++++++++++++++++- nova/db/sqlalchemy/models.py | 21 +++++++++++++++++ nova/endpoint/cloud.py | 53 +++++++++++++++++++++++++++++++++++++----- nova/tests/compute_unittest.py | 1 + run_tests.py | 1 + 6 files changed, 139 insertions(+), 7 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index d81673fad..c22c84768 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -195,6 +195,10 @@ def instance_create(context, values): return IMPL.instance_create(context, values) +def instance_data_get_for_project(context, project_id): + """Get (instance_count, core_count) for project.""" + return IMPL.instance_data_get_for_project(context, project_id) + def instance_destroy(context, instance_id): """Destroy the instance or raise if it does not exist.""" return IMPL.instance_destroy(context, instance_id) @@ -379,6 +383,29 @@ def export_device_create(context, values): ################### +def quota_create(context, values): + """Create a quota from the values dictionary.""" + return IMPL.quota_create(context, values) + + +def quota_get(context, project_id): + """Retrieve a quota or raise if it does not exist.""" + return IMPL.quota_get(context, project_id) + + +def quota_update(context, project_id, values): + """Update a quota from the values dictionary.""" + return IMPL.quota_update(context, project_id, values) + + +def quota_destroy(context, project_id): + """Destroy the quota or raise if it does not exist.""" + return IMPL.quota_destroy(context, project_id) + + +################### + + def volume_allocate_shelf_and_blade(context, volume_id): """Atomically allocate a free shelf and blade from the pool.""" return IMPL.volume_allocate_shelf_and_blade(context, volume_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4ea7a9071..4b01725ce 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -26,6 +26,7 @@ from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ from sqlalchemy.orm import joinedload_all +from sqlalchemy.sql import func FLAGS = flags.FLAGS @@ -264,6 +265,15 @@ def instance_create(_context, values): return instance_ref.id +def instance_data_get_for_project(_context, project_id): + session = get_session() + return session.query(func.count(models.Instance.id), + func.sum(models.Instance.vcpus) + ).filter_by(project_id=project_id + ).filter_by(deleted=False + ).first() + + def instance_destroy(_context, instance_id): session = get_session() with session.begin(): @@ -534,6 +544,37 @@ def export_device_create(_context, values): ################### +def quota_create(_context, values): + quota_ref = models.Quota() + for (key, value) in values.iteritems(): + quota_ref[key] = value + quota_ref.save() + return quota_ref + + +def quota_get(_context, project_id): + return models.Quota.find_by_str(project_id) + + +def quota_update(_context, project_id, values): + session = get_session() + with session.begin(): + quota_ref = models.Quota.find_by_str(project_id, session=session) + for (key, value) in values.iteritems(): + quota_ref[key] = value + quota_ref.save(session=session) + + +def quota_destroy(_context, project_id): + session = get_session() + with session.begin(): + quota_ref = models.Quota.find_by_str(project_id, session=session) + quota_ref.delete(session=session) + + +################### + + def volume_allocate_shelf_and_blade(_context, volume_id): session = get_session() with session.begin(): @@ -621,7 +662,7 @@ def volume_get_instance(_context, volume_id): def volume_get_shelf_and_blade(_context, volume_id): session = get_session() - export_device = session.query(models.ExportDevice + export_device = session.query(models.exportdevice ).filter_by(volume_id=volume_id ).first() if not export_device: diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 2fcade7de..7f510301a 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -222,6 +222,11 @@ class Instance(BASE, NovaBase): state = Column(Integer) state_description = Column(String(255)) + memory_mb = Column(Integer) + vcpus = Column(Integer) + local_gb = Column(Integer) + + hostname = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) @@ -279,6 +284,22 @@ class Quota(BASE, NovaBase): gigabytes = Column(Integer) floating_ips = Column(Integer) + @property + def str_id(self): + return self.project_id + + @classmethod + def find_by_str(cls, str_id, session=None, deleted=False): + if not session: + session = get_session() + try: + return session.query(cls + ).filter_by(project_id=str_id + ).filter_by(deleted=deleted + ).one() + except exc.NoResultFound: + new_exc = exception.NotFound("No model for project_id %s" % str_id) + raise new_exc.__class__, new_exc, sys.exc_info()[2] class ExportDevice(BASE, NovaBase): """Represates a shelf and blade that a volume can be exported on""" diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 2866474e6..b8a00075b 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -32,6 +32,7 @@ from twisted.internet import defer from nova import db from nova import exception from nova import flags +from nova import quota from nova import rpc from nova import utils from nova.auth import rbac @@ -44,6 +45,11 @@ FLAGS = flags.FLAGS flags.DECLARE('storage_availability_zone', 'nova.volume.manager') +class QuotaError(exception.ApiError): + """Quota Exceeeded""" + pass + + def _gen_key(user_id, key_name): """ Tuck this into AuthManager """ try: @@ -276,6 +282,14 @@ class CloudController(object): @rbac.allow('projectmanager', 'sysadmin') def create_volume(self, context, size, **kwargs): + # check quota + size = int(size) + if quota.allowed_volumes(context, 1, size) < 1: + logging.warn("Quota exceeeded for %s, tried to create %sG volume", + context.project.id, size) + raise QuotaError("Volume quota exceeded. You cannot " + "create a volume of size %s" % + size) vol = {} vol['size'] = size vol['user_id'] = context.user.id @@ -435,6 +449,12 @@ class CloudController(object): @rbac.allow('netadmin') @defer.inlineCallbacks def allocate_address(self, context, **kwargs): + # check quota + if quota.allowed_floating_ips(context, 1) < 1: + logging.warn("Quota exceeeded for %s, tried to allocate address", + context.project.id) + raise QuotaError("Address quota exceeded. You cannot " + "allocate any more addresses") network_topic = yield self._get_network_topic(context) public_ip = yield rpc.call(network_topic, {"method": "allocate_floating_ip", @@ -487,14 +507,30 @@ class CloudController(object): host = network_ref['host'] if not host: host = yield rpc.call(FLAGS.network_topic, - {"method": "set_network_host", - "args": {"context": None, - "project_id": context.project.id}}) + {"method": "set_network_host", + "args": {"context": None, + "project_id": context.project.id}}) defer.returnValue(db.queue_get_for(context, FLAGS.network_topic, host)) @rbac.allow('projectmanager', 'sysadmin') @defer.inlineCallbacks def run_instances(self, context, **kwargs): + instance_type = kwargs.get('instance_type', 'm1.small') + if instance_type not in INSTANCE_TYPES: + raise exception.ApiError("Unknown instance type: %s", + instance_type) + # check quota + max_instances = int(kwargs.get('max_count', 1)) + min_instances = int(kwargs.get('min_count', max_instances)) + num_instances = quota.allowed_instances(context, + max_instances, + instance_type) + if num_instances < min_instances: + logging.warn("Quota exceeeded for %s, tried to run %s instances", + context.project.id, min_instances) + raise QuotaError("Instance quota exceeded. You can only " + "run %s more instances of this type." % + num_instances) # make sure user can access the image # vpn image is private so it doesn't show up on lists vpn = kwargs['image_id'] == FLAGS.vpn_image_id @@ -516,7 +552,7 @@ class CloudController(object): images.get(context, kernel_id) images.get(context, ramdisk_id) - logging.debug("Going to run instances...") + logging.debug("Going to run %s instances...", num_instances) launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) key_data = None if kwargs.has_key('key_name'): @@ -540,10 +576,15 @@ class CloudController(object): base_options['user_id'] = context.user.id base_options['project_id'] = context.project.id base_options['user_data'] = kwargs.get('user_data', '') - base_options['instance_type'] = kwargs.get('instance_type', 'm1.small') base_options['security_group'] = security_group + base_options['instance_type'] = instance_type + + type_data = INSTANCE_TYPES['instance_type'] + base_options['memory_mb'] = type_data['memory_mb'] + base_options['vcpus'] = type_data['vcpus'] + base_options['local_gb'] = type_data['local_gb'] - for num in range(int(kwargs['max_count'])): + for num in range(): inst_id = db.instance_create(context, base_options) inst = {} diff --git a/nova/tests/compute_unittest.py b/nova/tests/compute_unittest.py index 8a7f7b649..b45367eb2 100644 --- a/nova/tests/compute_unittest.py +++ b/nova/tests/compute_unittest.py @@ -50,6 +50,7 @@ class ComputeTestCase(test.TrialTestCase): def tearDown(self): # pylint: disable-msg=C0103 self.manager.delete_user(self.user) self.manager.delete_project(self.project) + super(ComputeTestCase, self).tearDown() def _create_instance(self): """Create a test instance""" diff --git a/run_tests.py b/run_tests.py index d5dc5f934..73bf57f97 100644 --- a/run_tests.py +++ b/run_tests.py @@ -58,6 +58,7 @@ from nova.tests.flags_unittest import * from nova.tests.network_unittest import * from nova.tests.objectstore_unittest import * from nova.tests.process_unittest import * +from nova.tests.quota_unittest import * from nova.tests.rpc_unittest import * from nova.tests.service_unittest import * from nova.tests.validator_unittest import * -- cgit From f40c194977b53b7b99a4234f2c1a3b3bfb39c00e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 21:29:00 -0700 Subject: kwargs don't work if you prepend an underscore --- nova/scheduler/simple.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py index ea4eef98e..e53e9fa7e 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -37,7 +37,7 @@ flags.DEFINE_integer("max_networks", 1000, class SimpleScheduler(chance.ChanceScheduler): """Implements Naive Scheduler that tries to find least loaded host.""" - def schedule_run_instance(self, context, _instance_id, *_args, **_kwargs): + def schedule_run_instance(self, context, *_args, **_kwargs): """Picks a host that is up and has the fewest running instances.""" results = db.service_get_all_compute_sorted(context) @@ -49,7 +49,7 @@ class SimpleScheduler(chance.ChanceScheduler): return service['host'] raise driver.NoValidHost("No hosts found") - def schedule_create_volume(self, context, _volume_id, *_args, **_kwargs): + def schedule_create_volume(self, context, *_args, **_kwargs): """Picks a host that is up and has the fewest volumes.""" results = db.service_get_all_volume_sorted(context) @@ -61,8 +61,7 @@ class SimpleScheduler(chance.ChanceScheduler): return service['host'] raise driver.NoValidHost("No hosts found") - def schedule_set_network_host(self, context, _network_id, - *_args, **_kwargs): + def schedule_set_network_host(self, context, *_args, **_kwargs): """Picks a host that is up and has the fewest networks.""" results = db.service_get_all_network_sorted(context) -- cgit From 56779ebfec9cd382f170e307a1dc6403e339807f Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 21:42:18 -0700 Subject: add missing files for quota --- nova/quota.py | 91 +++++++++++++++++++++++++++++++ nova/tests/quota_unittest.py | 127 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 nova/quota.py create mode 100644 nova/tests/quota_unittest.py diff --git a/nova/quota.py b/nova/quota.py new file mode 100644 index 000000000..f0e51feeb --- /dev/null +++ b/nova/quota.py @@ -0,0 +1,91 @@ +# 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. +""" +Quotas for instances, volumes, and floating ips +""" + +from nova import db +from nova import exception +from nova import flags +from nova.compute import instance_types + +FLAGS = flags.FLAGS + +flags.DEFINE_integer('quota_instances', 10, + 'number of instances allowed per project') +flags.DEFINE_integer('quota_cores', 20, + 'number of instance cores allowed per project') +flags.DEFINE_integer('quota_volumes', 10, + 'number of volumes allowed per project') +flags.DEFINE_integer('quota_gigabytes', 1000, + 'number of volume gigabytes allowed per project') +flags.DEFINE_integer('quota_floating_ips', 10, + 'number of floating ips allowed per project') + +def _get_quota(context, project_id): + rval = {'instances': FLAGS.quota_instances, + 'cores': FLAGS.quota_cores, + 'volumes': FLAGS.quota_volumes, + 'gigabytes': FLAGS.quota_gigabytes, + 'floating_ips': FLAGS.quota_floating_ips} + try: + quota = db.quota_get(context, project_id) + for key in rval.keys(): + if quota[key] is not None: + rval[key] = quota[key] + except exception.NotFound: + pass + return rval + +def allowed_instances(context, num_instances, instance_type): + """Check quota and return min(num_instances, allowed_instances)""" + project_id = context.project.id + used_instances, used_cores = db.instance_data_get_for_project(context, + project_id) + quota = _get_quota(context, project_id) + allowed_instances = quota['instances'] - used_instances + allowed_cores = quota['cores'] - used_cores + type_cores = instance_types.INSTANCE_TYPES[instance_type]['vcpus'] + num_cores = num_instances * type_cores + allowed_instances = min(allowed_instances, + int(allowed_cores // type_cores)) + return min(num_instances, allowed_instances) + + +def allowed_volumes(context, num_volumes, size): + """Check quota and return min(num_volumes, allowed_volumes)""" + project_id = context.project.id + used_volumes, used_gigabytes = db.volume_data_get_for_project(context, + project_id) + quota = _get_quota(context, project_id) + allowed_volumes = quota['volumes'] - used_volumes + allowed_gigabytes = quota['gigabytes'] - used_gigabytes + num_gigabytes = num_volumes * size + allowed_volumes = min(allowed_volumes, + int(allowed_gigabytes // size)) + return min(num_volumes, allowed_volumes) + + +def allowed_floating_ips(context, num_floating_ips): + """Check quota and return min(num_floating_ips, allowed_floating_ips)""" + project_id = context.project.id + used_floating_ips = db.floating_ip_count_by_project(context, project_id) + quota = _get_quota(context, project_id) + allowed_floating_ips = quota['floating_ips'] - used_floating_ips + return min(num_floating_ips, allowed_floating_ips) + diff --git a/nova/tests/quota_unittest.py b/nova/tests/quota_unittest.py new file mode 100644 index 000000000..bf3506c78 --- /dev/null +++ b/nova/tests/quota_unittest.py @@ -0,0 +1,127 @@ +# 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. + +import logging + +from nova import db +from nova import flags +from nova import quota +from nova import test +from nova import utils +from nova.auth import manager +from nova.endpoint import cloud +from nova.endpoint import api + + +FLAGS = flags.FLAGS + + +class QuotaTestCase(test.TrialTestCase): + def setUp(self): # pylint: disable-msg=C0103 + logging.getLogger().setLevel(logging.DEBUG) + super(QuotaTestCase, self).setUp() + self.flags(connection_type='fake', + quota_instances=2, + quota_cores=4, + quota_volumes=2, + quota_gigabytes=20, + quota_floating_ips=2) + + self.cloud = cloud.CloudController() + self.manager = manager.AuthManager() + self.user = self.manager.create_user('admin', 'admin', 'admin', True) + self.project = self.manager.create_project('admin', 'admin', 'admin') + self.context = api.APIRequestContext(handler=None, + project=self.project, + user=self.user) + + def tearDown(self): # pylint: disable-msg=C0103 + manager.AuthManager().delete_project(self.project) + manager.AuthManager().delete_user(self.user) + super(QuotaTestCase, self).tearDown() + + def _create_instance(self, cores=2): + """Create a test instance""" + inst = {} + inst['image_id'] = 'ami-test' + inst['reservation_id'] = 'r-fakeres' + inst['user_id'] = self.user.id + inst['project_id'] = self.project.id + inst['instance_type'] = 'm1.large' + inst['vcpus'] = cores + inst['mac_address'] = utils.generate_mac() + return db.instance_create(self.context, inst) + + def _create_volume(self, size=10): + """Create a test volume""" + vol = {} + vol['user_id'] = self.user.id + vol['project_id'] = self.project.id + vol['size'] = size + return db.volume_create(self.context, vol)['id'] + + def test_quota_overrides(self): + """Make sure overriding a projects quotas works""" + num_instances = quota.allowed_instances(self.context, 100, 'm1.small') + self.assertEqual(num_instances, 2) + db.quota_create(self.context, {'project_id': self.project.id, + 'instances': 10}) + num_instances = quota.allowed_instances(self.context, 100, 'm1.small') + self.assertEqual(num_instances, 4) + db.quota_update(self.context, self.project.id, {'cores': 100}) + num_instances = quota.allowed_instances(self.context, 100, 'm1.small') + self.assertEqual(num_instances, 10) + db.quota_destroy(self.context, self.project.id) + + def test_too_many_instances(self): + instance_ids = [] + for i in range(FLAGS.quota_instances): + instance_id = self._create_instance() + instance_ids.append(instance_id) + self.assertFailure(self.cloud.run_instances(self.context, + min_count=1, + max_count=1, + instance_type='m1.small'), + cloud.QuotaError) + for instance_id in instance_ids: + db.instance_destroy(self.context, instance_id) + + def test_too_many_cores(self): + instance_ids = [] + instance_id = self._create_instance(cores=4) + instance_ids.append(instance_id) + self.assertFailure(self.cloud.run_instances(self.context, + min_count=1, + max_count=1, + instance_type='m1.small'), + cloud.QuotaError) + for instance_id in instance_ids: + db.instance_destroy(self.context, instance_id) + + def test_too_many_volumes(self): + volume_ids = [] + for i in range(FLAGS.quota_volumes): + volume_id = self._create_volume() + volume_ids.append(volume_id) + self.assertRaises(cloud.QuotaError, + self.cloud.create_volume, + self.context, + size=10) + for volume_id in volume_ids: + db.volume_destroy(self.context, volume_id) + -- cgit From 6f5c16b62c441c97ade4f2f4b4878e8015c9281e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 21:52:06 -0700 Subject: make the db creates return refs instead of ids --- nova/api/rackspace/servers.py | 2 +- nova/db/sqlalchemy/api.py | 4 ++-- nova/endpoint/cloud.py | 2 +- nova/service.py | 9 +++++---- nova/tests/compute_unittest.py | 2 +- nova/tests/network_unittest.py | 8 ++++---- nova/tests/service_unittest.py | 4 ++-- nova/tests/volume_unittest.py | 2 +- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 44174ca52..1815f7523 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -72,7 +72,7 @@ class Controller(base.Controller): inst['reservation_id'] = reservation inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() - inst_id = db.instance_create(None, inst) + inst_id = db.instance_create(None, inst)['id'] address = self.network_manager.allocate_fixed_ip(None, inst_id) # key_data, key_name, ami_launch_index # TODO(todd): key data or root password diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4ea7a9071..02ebdd222 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -59,7 +59,7 @@ def service_create(_context, values): for (key, value) in values.iteritems(): service_ref[key] = value service_ref.save() - return service_ref.id + return service_ref def service_update(_context, service_id, values): @@ -261,7 +261,7 @@ def instance_create(_context, values): for (key, value) in values.iteritems(): instance_ref[key] = value instance_ref.save() - return instance_ref.id + return instance_ref def instance_destroy(_context, instance_id): diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 397c9c554..7f4a901c8 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -543,7 +543,7 @@ class CloudController(object): base_options['security_group'] = security_group for num in range(int(kwargs['max_count'])): - inst_id = db.instance_create(context, base_options) + inst_id = db.instance_create(context, base_options)['id'] inst = {} inst['mac_address'] = utils.generate_mac() diff --git a/nova/service.py b/nova/service.py index 60583dcdb..870dd6ceb 100644 --- a/nova/service.py +++ b/nova/service.py @@ -62,10 +62,11 @@ class Service(object, service.Service): def _create_service_ref(self): - self.service_id = db.service_create(None, {'host': self.host, - 'binary': self.binary, - 'topic': self.topic, - 'report_count': 0}) + service_ref = db.service_create(None, {'host': self.host, + 'binary': self.binary, + 'topic': self.topic, + 'report_count': 0}) + self.service_id = service_ref['id'] def __getattr__(self, key): try: diff --git a/nova/tests/compute_unittest.py b/nova/tests/compute_unittest.py index 8a7f7b649..de2bf3d3b 100644 --- a/nova/tests/compute_unittest.py +++ b/nova/tests/compute_unittest.py @@ -62,7 +62,7 @@ class ComputeTestCase(test.TrialTestCase): inst['instance_type'] = 'm1.tiny' inst['mac_address'] = utils.generate_mac() inst['ami_launch_index'] = 0 - return db.instance_create(self.context, inst) + return db.instance_create(self.context, inst)['id'] @defer.inlineCallbacks def test_run_terminate(self): diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index a89f1d622..9958600e0 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -56,12 +56,12 @@ class NetworkTestCase(test.TrialTestCase): name)) # create the necessary network data for the project self.network.set_network_host(self.context, self.projects[i].id) - instance_id = db.instance_create(None, + instance_ref = db.instance_create(None, {'mac_address': utils.generate_mac()}) - self.instance_id = instance_id - instance_id = db.instance_create(None, + self.instance_id = instance_ref['id'] + instance_ref = db.instance_create(None, {'mac_address': utils.generate_mac()}) - self.instance2_id = instance_id + self.instance2_id = instance_ref['id'] def tearDown(self): # pylint: disable-msg=C0103 super(NetworkTestCase, self).tearDown() diff --git a/nova/tests/service_unittest.py b/nova/tests/service_unittest.py index 097a045e0..01da0eb8a 100644 --- a/nova/tests/service_unittest.py +++ b/nova/tests/service_unittest.py @@ -87,7 +87,7 @@ class ServiceTestCase(test.BaseTestCase): host, binary).AndRaise(exception.NotFound()) service.db.service_create(None, - service_create).AndReturn(service_ref['id']) + service_create).AndReturn(service_ref) self.mox.ReplayAll() app = service.Service.create(host=host, binary=binary) @@ -131,7 +131,7 @@ class ServiceTestCase(test.BaseTestCase): host, binary).AndRaise(exception.NotFound()) service.db.service_create(None, - service_create).AndReturn(service_ref['id']) + service_create).AndReturn(service_ref) service.db.service_get(None, service_ref['id']).AndReturn(service_ref) service.db.service_update(None, service_ref['id'], mox.ContainsKeyValue('report_count', 1)) diff --git a/nova/tests/volume_unittest.py b/nova/tests/volume_unittest.py index 9e35d2a1c..1d665b502 100644 --- a/nova/tests/volume_unittest.py +++ b/nova/tests/volume_unittest.py @@ -108,7 +108,7 @@ class VolumeTestCase(test.TrialTestCase): inst['instance_type'] = 'm1.tiny' inst['mac_address'] = utils.generate_mac() inst['ami_launch_index'] = 0 - instance_id = db.instance_create(self.context, inst) + instance_id = db.instance_create(self.context, inst)['id'] mountpoint = "/dev/sdf" volume_id = self._create_volume() yield self.volume.create_volume(self.context, volume_id) -- cgit From aa4d83308ef19138996c68cfa21f34f3914f50c2 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 21:56:46 -0700 Subject: fix rare condition where describe is called before instance has an ip --- nova/endpoint/cloud.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 281c4535a..5ff69edf1 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -362,12 +362,14 @@ class CloudController(object): def _format_instances(self, context, reservation_id=None): reservations = {} if reservation_id: - instances = db.instance_get_by_reservation(context, reservation_id) + instances = db.instance_get_by_reservation(context, + reservation_id) else: if not context.user.is_admin(): instances = db.instance_get_all(context) else: - instances = db.instance_get_by_project(context, context.project.id) + instances = db.instance_get_by_project(context, + context.project.id) for instance in instances: if not context.user.is_admin(): if instance['image_id'] == FLAGS.vpn_image_id: @@ -379,12 +381,15 @@ class CloudController(object): 'code': instance['state'], 'name': instance['state_description'] } + fixed_addr = None floating_addr = None - if instance['fixed_ip']['floating_ips']: - floating_addr = instance['fixed_ip']['floating_ips'][0]['str_id'] - i['publicDnsName'] = floating_addr - fixed_addr = instance['fixed_ip']['str_id'] + if instance['fixed_ip']: + fixed_addr = instance['fixed_ip']['str_id'] + if instance['fixed_ip']['floating_ips']: + fixed = instance['fixed_ip'] + floating_addr = fixed['floating_ips'][0]['str_id'] i['privateDnsName'] = fixed_addr + i['publicDnsName'] = floating_addr if not i['publicDnsName']: i['publicDnsName'] = i['privateDnsName'] i['dnsName'] = None -- cgit From c5bfa37c92bd066fa2bc3565b251edced3255438 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 21:59:09 -0700 Subject: fix unittest --- nova/tests/scheduler_unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/scheduler_unittest.py b/nova/tests/scheduler_unittest.py index 51b9aeaad..09e45ea68 100644 --- a/nova/tests/scheduler_unittest.py +++ b/nova/tests/scheduler_unittest.py @@ -109,7 +109,7 @@ class SimpleDriverTestCase(test.TrialTestCase): inst['instance_type'] = 'm1.tiny' inst['mac_address'] = utils.generate_mac() inst['ami_launch_index'] = 0 - return db.instance_create(self.context, inst) + return db.instance_create(self.context, inst)['id'] def test_hosts_are_up(self): # NOTE(vish): constructing service without create method -- cgit From 5cb90074df70daa60241930da9940e093a3812ba Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 22:13:38 -0700 Subject: quota tests --- nova/endpoint/cloud.py | 1 + nova/tests/quota_unittest.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 5209ec906..b5ac5be4d 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -450,6 +450,7 @@ class CloudController(object): @defer.inlineCallbacks def allocate_address(self, context, **kwargs): # check quota + print quota.allowed_floating_ips(context, 1) if quota.allowed_floating_ips(context, 1) < 1: logging.warn("Quota exceeeded for %s, tried to allocate address", context.project.id) diff --git a/nova/tests/quota_unittest.py b/nova/tests/quota_unittest.py index e3f23b84e..d7c07bfab 100644 --- a/nova/tests/quota_unittest.py +++ b/nova/tests/quota_unittest.py @@ -19,6 +19,7 @@ import logging from nova import db +from nova import exception from nova import flags from nova import quota from nova import test @@ -46,6 +47,7 @@ class QuotaTestCase(test.TrialTestCase): self.manager = manager.AuthManager() self.user = self.manager.create_user('admin', 'admin', 'admin', True) self.project = self.manager.create_project('admin', 'admin', 'admin') + self.network = utils.import_object(FLAGS.network_manager) self.context = api.APIRequestContext(handler=None, project=self.project, user=self.user) @@ -125,3 +127,26 @@ class QuotaTestCase(test.TrialTestCase): for volume_id in volume_ids: db.volume_destroy(self.context, volume_id) + def test_too_many_gigabytes(self): + volume_ids = [] + volume_id = self._create_volume(size=20) + volume_ids.append(volume_id) + self.assertRaises(cloud.QuotaError, + self.cloud.create_volume, + self.context, + size=10) + for volume_id in volume_ids: + db.volume_destroy(self.context, volume_id) + + def test_too_many_addresses(self): + address = '192.168.0.100' + try: + db.floating_ip_get_by_address(None, address) + except exception.NotFound: + db.floating_ip_create(None, {'address': address, + 'host': FLAGS.host}) + #float_addr = self.network.allocate_floating_ip(self.context, + # self.project.id) + self.assertFailure(self.cloud.allocate_address(self.context), + cloud.QuotaError) + -- cgit From ece1c84203890e87834bb53acaf98420fdeee6dc Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 22:53:31 -0700 Subject: address test almost works --- nova/tests/quota_unittest.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/nova/tests/quota_unittest.py b/nova/tests/quota_unittest.py index d7c07bfab..9d697ccd3 100644 --- a/nova/tests/quota_unittest.py +++ b/nova/tests/quota_unittest.py @@ -41,7 +41,7 @@ class QuotaTestCase(test.TrialTestCase): quota_cores=4, quota_volumes=2, quota_gigabytes=20, - quota_floating_ips=2) + quota_floating_ips=1) self.cloud = cloud.CloudController() self.manager = manager.AuthManager() @@ -145,8 +145,18 @@ class QuotaTestCase(test.TrialTestCase): except exception.NotFound: db.floating_ip_create(None, {'address': address, 'host': FLAGS.host}) - #float_addr = self.network.allocate_floating_ip(self.context, - # self.project.id) + float_addr = self.network.allocate_floating_ip(self.context, + self.project.id) + # NOTE(vish): This assert doesn't work. When cloud attempts to + # make an rpc.call, the test just finishes with OK. It + # appears to be something in the magic inline callbacks + # that is breaking. self.assertFailure(self.cloud.allocate_address(self.context), cloud.QuotaError) + try: + yield self.cloud.allocate_address(self.context) + self.fail('Should have raised QuotaError') + except cloud.QuotaError: + pass + -- cgit From a7a46ea93186ca68ca90efdcd86b4d2a7d3bd8e8 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 23:04:30 -0700 Subject: quotas working and tests passing --- nova/endpoint/cloud.py | 1 - nova/tests/quota_unittest.py | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index b5ac5be4d..5209ec906 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -450,7 +450,6 @@ class CloudController(object): @defer.inlineCallbacks def allocate_address(self, context, **kwargs): # check quota - print quota.allowed_floating_ips(context, 1) if quota.allowed_floating_ips(context, 1) < 1: logging.warn("Quota exceeeded for %s, tried to allocate address", context.project.id) diff --git a/nova/tests/quota_unittest.py b/nova/tests/quota_unittest.py index 9d697ccd3..cab9f663d 100644 --- a/nova/tests/quota_unittest.py +++ b/nova/tests/quota_unittest.py @@ -147,16 +147,9 @@ class QuotaTestCase(test.TrialTestCase): 'host': FLAGS.host}) float_addr = self.network.allocate_floating_ip(self.context, self.project.id) - # NOTE(vish): This assert doesn't work. When cloud attempts to + # NOTE(vish): This assert never fails. When cloud attempts to # make an rpc.call, the test just finishes with OK. It # appears to be something in the magic inline callbacks # that is breaking. self.assertFailure(self.cloud.allocate_address(self.context), cloud.QuotaError) - try: - yield self.cloud.allocate_address(self.context) - self.fail('Should have raised QuotaError') - except cloud.QuotaError: - pass - - -- cgit From d534655b636563fa71ca78758340b2dd49bc2527 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 23:32:43 -0700 Subject: don't pass topic into schedule_run_instance --- nova/scheduler/manager.py | 2 ++ nova/tests/scheduler_unittest.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 0ad7ca86b..af76334a8 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -54,6 +54,8 @@ class SchedulerManager(manager.Manager): Falls back to schedule(context, topic) if method doesn't exist. """ driver_method = 'schedule_%s' % method + print topic + print args try: host = getattr(self.driver, driver_method)(context, *args, **kwargs) except AttributeError: diff --git a/nova/tests/scheduler_unittest.py b/nova/tests/scheduler_unittest.py index 09e45ea68..27e100fa0 100644 --- a/nova/tests/scheduler_unittest.py +++ b/nova/tests/scheduler_unittest.py @@ -121,7 +121,6 @@ class SimpleDriverTestCase(test.TrialTestCase): instance_id = self._create_instance() self.service1.run_instance(self.context, instance_id) host = self.scheduler.driver.schedule_run_instance(self.context, - 'compute', instance_id) self.assertEqual(host, 'host2') self.service1.terminate_instance(self.context, instance_id) -- cgit From ffb2d740a1d8fba997c043cc3066282afedebae8 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 23:37:08 -0700 Subject: removed extra quotes around instance_type --- nova/endpoint/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 5209ec906..ad5db6668 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -578,7 +578,7 @@ class CloudController(object): base_options['security_group'] = security_group base_options['instance_type'] = instance_type - type_data = INSTANCE_TYPES['instance_type'] + type_data = INSTANCE_TYPES[instance_type] base_options['memory_mb'] = type_data['memory_mb'] base_options['vcpus'] = type_data['vcpus'] base_options['local_gb'] = type_data['local_gb'] -- cgit From 1867c2aae81e4a73374bde0169b4e16cd8e18846 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 9 Sep 2010 23:43:51 -0700 Subject: remove print statements --- nova/scheduler/manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index af76334a8..0ad7ca86b 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -54,8 +54,6 @@ class SchedulerManager(manager.Manager): Falls back to schedule(context, topic) if method doesn't exist. """ driver_method = 'schedule_%s' % method - print topic - print args try: host = getattr(self.driver, driver_method)(context, *args, **kwargs) except AttributeError: -- cgit From 1c01b37a5f2372f4e61fdff8a16a9efe6f6b7e7b Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 01:13:11 -0700 Subject: set host when item is scheduled --- nova/scheduler/simple.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py index e53e9fa7e..48be4c1a6 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -37,7 +37,7 @@ flags.DEFINE_integer("max_networks", 1000, class SimpleScheduler(chance.ChanceScheduler): """Implements Naive Scheduler that tries to find least loaded host.""" - def schedule_run_instance(self, context, *_args, **_kwargs): + def schedule_run_instance(self, context, instance_id, *_args, **_kwargs): """Picks a host that is up and has the fewest running instances.""" results = db.service_get_all_compute_sorted(context) @@ -46,10 +46,13 @@ class SimpleScheduler(chance.ChanceScheduler): if instance_count >= FLAGS.max_instances: raise driver.NoValidHost("All hosts have too many instances") if self.service_is_up(service): + db.instance_update(context, + instance_id, + {'host': service['host']}) return service['host'] raise driver.NoValidHost("No hosts found") - def schedule_create_volume(self, context, *_args, **_kwargs): + def schedule_create_volume(self, context, volume_id, *_args, **_kwargs): """Picks a host that is up and has the fewest volumes.""" results = db.service_get_all_volume_sorted(context) @@ -58,6 +61,9 @@ class SimpleScheduler(chance.ChanceScheduler): if instance_count >= FLAGS.max_volumes: raise driver.NoValidHost("All hosts have too many volumes") if self.service_is_up(service): + db.instance_update(context, + volume_id, + {'host': service['host']}) return service['host'] raise driver.NoValidHost("No hosts found") -- cgit From f1e45e3294622e22e6044027c1d2514f107d6134 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Fri, 10 Sep 2010 10:56:22 +0100 Subject: Change "exn" to "exc" to fit with the common style. --- nova/virt/xenapi.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index 04069e459..1c6de4403 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -280,13 +280,13 @@ class XenAPIConnection(object): try: task = yield self._call_xenapi('Async.VM.hard_shutdown', vm) yield self._wait_for_task(task) - except Exception, exn: - logging.warn(exn) + except Exception, exc: + logging.warn(exc) try: task = yield self._call_xenapi('Async.VM.destroy', vm) yield self._wait_for_task(task) - except Exception, exn: - logging.warn(exn) + except Exception, exc: + logging.warn(exc) def get_info(self, instance_id): vm = self._lookup_blocking(instance_id) @@ -340,9 +340,9 @@ class XenAPIConnection(object): error_info) deferred.errback(XenAPI.Failure(error_info)) #logging.debug('Polling task %s done.', task) - except Exception, exn: - logging.warn(exn) - deferred.errback(exn) + except Exception, exc: + logging.warn(exc) + deferred.errback(exc) @utils.deferredToThread def _call_xenapi(self, method, *args): @@ -368,21 +368,21 @@ class XenAPIConnection(object): def _unwrap_plugin_exceptions(func, *args, **kwargs): try: return func(*args, **kwargs) - except XenAPI.Failure, exn: - logging.debug("Got exception: %s", exn) - if (len(exn.details) == 4 and - exn.details[0] == 'XENAPI_PLUGIN_EXCEPTION' and - exn.details[2] == 'Failure'): + except XenAPI.Failure, exc: + logging.debug("Got exception: %s", exc) + if (len(exc.details) == 4 and + exc.details[0] == 'XENAPI_PLUGIN_EXCEPTION' and + exc.details[2] == 'Failure'): params = None try: - params = eval(exn.details[3]) + params = eval(exc.details[3]) except: - raise exn + raise exc raise XenAPI.Failure(params) else: raise - except xmlrpclib.ProtocolError, exn: - logging.debug("Got exception: %s", exn) + except xmlrpclib.ProtocolError, exc: + logging.debug("Got exception: %s", exc) raise -- cgit From 9330ebc110aeb7591567c66939b39f4345b5778d Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 04:52:48 -0700 Subject: added modify project command to allow project manager and description to be updated --- nova/auth/fakeldap.py | 5 ++++- nova/auth/ldapdriver.py | 18 ++++++++++++++++++ nova/auth/manager.py | 20 ++++++++++++++++++++ nova/tests/auth_unittest.py | 6 ++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py index bfc3433c5..2791dfde6 100644 --- a/nova/auth/fakeldap.py +++ b/nova/auth/fakeldap.py @@ -33,6 +33,7 @@ SCOPE_ONELEVEL = 1 # not implemented SCOPE_SUBTREE = 2 MOD_ADD = 0 MOD_DELETE = 1 +MOD_REPLACE = 2 class NO_SUCH_OBJECT(Exception): # pylint: disable-msg=C0103 @@ -175,7 +176,7 @@ class FakeLDAP(object): Args: dn -- a dn attrs -- a list of tuples in the following form: - ([MOD_ADD | MOD_DELETE], attribute, value) + ([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value) """ redis = datastore.Redis.instance() @@ -185,6 +186,8 @@ class FakeLDAP(object): values = _from_json(redis.hget(key, k)) if cmd == MOD_ADD: values.append(v) + elif cmd == MOD_REPLACE: + values = [v] else: values.remove(v) values = redis.hset(key, k, _to_json(values)) diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py index 74ba011b5..cc8e2caa3 100644 --- a/nova/auth/ldapdriver.py +++ b/nova/auth/ldapdriver.py @@ -202,6 +202,24 @@ class LdapDriver(object): self.conn.add_s('cn=%s,%s' % (name, FLAGS.ldap_project_subtree), attr) return self.__to_project(dict(attr)) + def modify_project(self, project_id, manager_uid=None, description=None): + """Modify an existing project""" + if not manager_uid and not description: + return + attr = [] + if manager_uid: + if not self.__user_exists(manager_uid): + raise exception.NotFound("Project can't be modified because " + "manager %s doesn't exist" % + manager_uid) + manager_dn = self.__uid_to_dn(manager_uid) + attr.append((self.ldap.MOD_REPLACE, 'projectManager', manager_dn)) + if description: + attr.append((self.ldap.MOD_REPLACE, 'description', description)) + self.conn.modify_s('cn=%s,%s' % (project_id, + FLAGS.ldap_project_subtree), + attr) + def add_to_project(self, uid, project_id): """Add user to project""" dn = 'cn=%s,%s' % (project_id, FLAGS.ldap_project_subtree) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 284b29502..d094bb7e1 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -525,6 +525,26 @@ class AuthManager(object): if project_dict: return Project(**project_dict) + def modify_project(self, project, manager_user=None, description=None): + """Modify a project + + @type name: Project or project_id + @param project: The project to modify. + + @type manager_user: User or uid + @param manager_user: This user will be the new project manager. + + @type description: str + @param project: This will be the new description of the project. + + """ + if manager_user: + manager_user = User.safe_id(manager_user) + with self.driver() as drv: + drv.modify_project(Project.safe_id(project), + manager_user, + description) + def add_to_project(self, user, project): """Add user to project""" with self.driver() as drv: diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index 0b404bfdc..2fc780640 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -206,6 +206,12 @@ class AuthTestCase(test.BaseTestCase): self.assert_(len(self.manager.get_projects()) > 1) self.assertEqual(len(self.manager.get_projects('test2')), 1) + def test_220_can_modify_project(self): + self.manager.modify_project('testproj', 'test2', 'new description') + project = self.manager.get_project('testproj') + self.assertEqual(project.project_manager_id, 'test2') + self.assertEqual(project.description, 'new description') + def test_299_can_delete_project(self): self.manager.delete_project('testproj') self.assertFalse(filter(lambda p: p.name == 'testproj', self.manager.get_projects())) -- cgit From 282c1263c610287f1a99d2f84db58f6dcfd03239 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 05:25:57 -0700 Subject: fixed messed up call in metadata --- nova/endpoint/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 5ff69edf1..6f8cf94fd 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -84,7 +84,7 @@ class CloudController(object): def _get_mpi_data(self, project_id): result = {} - for instance in db.instance_get_by_project(project_id): + for instance in db.instance_get_by_project(None, project_id): line = '%s slots=%d' % (instance.fixed_ip['str_id'], INSTANCE_TYPES[instance['instance_type']]['vcpus']) if instance['key_name'] in result: -- cgit From c107d10eaf4072769249441dc340c725d77c8112 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 05:38:59 -0700 Subject: typo in metadata call --- nova/endpoint/cloud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 6f8cf94fd..bf2f07ad4 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -108,8 +108,8 @@ class CloudController(object): else: keys = '' hostname = instance_ref['hostname'] - floating_ip = db.instance_get_floating_ip_address(None, - instance_ref['id']) + floating_ip = db.instance_get_floating_address(None, + instance_ref['id']) data = { 'user-data': base64.b64decode(instance_ref['user_data']), 'meta-data': { -- cgit From 953b79702500d129d40b557db668f095c303910d Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 05:49:36 -0700 Subject: couple more errors in metadata --- nova/endpoint/cloud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index bf2f07ad4..c85383ef9 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -114,7 +114,7 @@ class CloudController(object): 'user-data': base64.b64decode(instance_ref['user_data']), 'meta-data': { 'ami-id': instance_ref['image_id'], - 'ami-launch-index': instance_ref['ami_launch_index'], + 'ami-launch-index': instance_ref['launch_index'], 'ami-manifest-path': 'FIXME', 'block-device-mapping': { # TODO(vish): replace with real data 'ami': 'sda1', @@ -130,7 +130,7 @@ class CloudController(object): 'local-ipv4': address, 'kernel-id': instance_ref['kernel_id'], 'placement': { - 'availaibility-zone': instance_ref['availability_zone'], + 'availability-zone': 'nova' # TODO(vish): real zone }, 'public-hostname': hostname, 'public-ipv4': floating_ip or '', -- cgit From f8a25024ff4a3225b3c7ba7de0927916b39126fc Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 07:34:10 -0700 Subject: add a simple iterator to NovaBase to support converting into dictionary --- nova/db/sqlalchemy/models.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 679a44d21..6818f838c 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -24,8 +24,7 @@ import sys import datetime # TODO(vish): clean up these imports -from sqlalchemy.orm import relationship, backref, validates, exc -from sqlalchemy.sql import func +from sqlalchemy.orm import relationship, backref, exc, object_mapper from sqlalchemy import Column, Integer, String from sqlalchemy import ForeignKey, DateTime, Boolean, Text from sqlalchemy.ext.declarative import declarative_base @@ -113,6 +112,14 @@ class NovaBase(object): def __getitem__(self, key): return getattr(self, key) + def __iter__(self): + self._i = iter(object_mapper(self).columns) + return self + + def next(self): + n = self._i.next().name + return n, getattr(self, n) + # TODO(vish): Store images in the database instead of file system #class Image(BASE, NovaBase): # """Represents an image in the datastore""" -- cgit From 214f15b5eac2100937473ee8990f8ec8a31fb142 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 12:25:45 -0700 Subject: dhcpbridge needed host instead of node name --- bin/nova-dhcpbridge | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge index c4795cca2..42eaf4bcb 100755 --- a/bin/nova-dhcpbridge +++ b/bin/nova-dhcpbridge @@ -53,7 +53,7 @@ def add_lease(_mac, ip_address, _hostname, _interface): network_manager = utils.import_object(FLAGS.network_manager) network_manager.lease_fixed_ip(None, ip_address) else: - rpc.cast("%s.%s" % (FLAGS.network_topic, FLAGS.node_name), + rpc.cast("%s.%s" % (FLAGS.network_topic, FLAGS.host), {"method": "lease_fixed_ip", "args": {"context": None, "address": ip_address}}) @@ -71,7 +71,7 @@ def del_lease(_mac, ip_address, _hostname, _interface): network_manager = utils.import_object(FLAGS.network_manager) network_manager.release_fixed_ip(None, ip_address) else: - rpc.cast("%s.%s" % (FLAGS.network_topic, FLAGS.node_name), + rpc.cast("%s.%s" % (FLAGS.network_topic, FLAGS.host), {"method": "release_fixed_ip", "args": {"context": None, "address": ip_address}}) -- cgit From a50e419953fb0fba20246c7f1ebf9946788f3202 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 12:34:45 -0700 Subject: hostname should be string id --- nova/endpoint/cloud.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index c85383ef9..2406e8202 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -550,12 +550,13 @@ class CloudController(object): base_options['security_group'] = security_group for num in range(int(kwargs['max_count'])): - inst_id = db.instance_create(context, base_options)['id'] + instance_ref = db.instance_create(context, base_options) + inst_id = instance_ref['id'] inst = {} inst['mac_address'] = utils.generate_mac() inst['launch_index'] = num - inst['hostname'] = inst_id + inst['hostname'] = instance_ref['str_id'] db.instance_update(context, inst_id, inst) address = self.network_manager.allocate_fixed_ip(context, inst_id, -- cgit From f16e427317f2558e74e8774b9104068b0c7e8ef8 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 14:16:14 -0700 Subject: fix mpi 500 on fixed ip --- nova/endpoint/cloud.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 2406e8202..925d14e16 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -85,12 +85,13 @@ class CloudController(object): def _get_mpi_data(self, project_id): result = {} for instance in db.instance_get_by_project(None, project_id): - line = '%s slots=%d' % (instance.fixed_ip['str_id'], - INSTANCE_TYPES[instance['instance_type']]['vcpus']) - if instance['key_name'] in result: - result[instance['key_name']].append(line) - else: - result[instance['key_name']] = [line] + if instance['fixed_ip']: + line = '%s slots=%d' % (instance['fixed_ip']['str_id'], + INSTANCE_TYPES[instance['instance_type']]['vcpus']) + if instance['key_name'] in result: + result[instance['key_name']].append(line) + else: + result[instance['key_name']] = [line] return result def get_metadata(self, address): -- cgit From a5b6e1dc8f3aa3135f633daac2e489e5e6ee67cb Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 14:24:35 -0700 Subject: just warn if an ip was already deallocated --- nova/network/manager.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 83de5d023..3212a7eab 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -68,11 +68,6 @@ class AddressAlreadyAllocated(exception.Error): pass -class AddressNotAllocated(exception.Error): - """Address has not been allocated""" - pass - - class NetworkManager(manager.Manager): """Implements common network manager functionality @@ -236,7 +231,7 @@ class VlanManager(NetworkManager): logging.debug("Leasing IP %s", address) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) if not fixed_ip_ref['allocated']: - raise AddressNotAllocated(address) + logging.warn("IP %s leased that was already deallocated", address) self.db.fixed_ip_update(context, fixed_ip_ref['str_id'], {'leased': True}) -- cgit From fc666c244a8de66ac73add034df3af2544a59790 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 15:04:52 -0700 Subject: set dnsName on describe --- nova/endpoint/cloud.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 925d14e16..6ca6855ca 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -391,9 +391,7 @@ class CloudController(object): floating_addr = fixed['floating_ips'][0]['str_id'] i['privateDnsName'] = fixed_addr i['publicDnsName'] = floating_addr - if not i['publicDnsName']: - i['publicDnsName'] = i['privateDnsName'] - i['dnsName'] = None + i['dnsName'] = i['publicDnsName'] or i['privateDnsName'] i['keyName'] = instance['key_name'] if context.user.is_admin(): i['keyName'] = '%s (%s, %s)' % (i['keyName'], -- cgit From ac27df3f4bea1a1a05a84de99c098dc91741a7ee Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 16:40:49 -0700 Subject: make api error messages more readable --- nova/endpoint/api.py | 5 ++++- nova/endpoint/cloud.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 40be00bb7..12eedfe67 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -304,7 +304,10 @@ class APIRequestHandler(tornado.web.RequestHandler): try: failure.raiseException() except exception.ApiError as ex: - self._error(type(ex).__name__ + "." + ex.code, ex.message) + if ex.code: + self._error(ex.code, ex.message) + else: + self._error(type(ex).__name__, ex.message) # TODO(vish): do something more useful with unknown exceptions except Exception as ex: self._error(type(ex).__name__, str(ex)) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index ad5db6668..adb63351f 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -529,7 +529,7 @@ class CloudController(object): context.project.id, min_instances) raise QuotaError("Instance quota exceeded. You can only " "run %s more instances of this type." % - num_instances) + num_instances, "InstanceLimitExceeded") # make sure user can access the image # vpn image is private so it doesn't show up on lists vpn = kwargs['image_id'] == FLAGS.vpn_image_id -- cgit From bc265bbc9b3b42e46e044c18252218a375192123 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 17:12:49 -0700 Subject: multi-region flag for describe regions --- nova/endpoint/cloud.py | 15 ++++++++++++--- nova/flags.py | 15 +++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 8e2beb1e3..180af0540 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -174,9 +174,18 @@ class CloudController(object): @rbac.allow('all') def describe_regions(self, context, region_name=None, **kwargs): - # TODO(vish): region_name is an array. Support filtering - return {'regionInfo': [{'regionName': 'nova', - 'regionUrl': FLAGS.ec2_url}]} + if FLAGS.region_list: + regions = [] + for region in FLAGS.region_list: + name, _sep, url = region.partition(',') + regions.append({'regionName': name, + 'regionUrl': url}) + else: + regions = [{'regionName': 'nova', + 'regionUrl': FLAGS.ec2_url}] + if region_name: + regions = [r for r in regions if r['regionName'] in region_name] + return {'regionInfo': regions } @rbac.allow('all') def describe_snapshots(self, diff --git a/nova/flags.py b/nova/flags.py index 2bca36f7e..19dcb96ba 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -34,7 +34,7 @@ class FlagValues(gflags.FlagValues): Unknown flags will be ignored when parsing the command line, but the command line will be kept so that it can be replayed if new flags are defined after the initial parsing. - + """ def __init__(self): @@ -50,7 +50,7 @@ class FlagValues(gflags.FlagValues): # leftover args at the end sneaky_unparsed_args = {"value": None} original_argv = list(argv) - + if self.IsGnuGetOpt(): orig_getopt = getattr(getopt, 'gnu_getopt') orig_name = 'gnu_getopt' @@ -81,7 +81,7 @@ class FlagValues(gflags.FlagValues): args = argv[:1] finally: setattr(getopt, orig_name, orig_getopt) - + # Store the arguments for later, we'll need them for new flags # added at runtime self.__dict__['__stored_argv'] = original_argv @@ -92,7 +92,7 @@ class FlagValues(gflags.FlagValues): def SetDirty(self, name): """Mark a flag as dirty so that accessing it will case a reparse.""" self.__dict__['__dirty'].append(name) - + def IsDirty(self, name): return name in self.__dict__['__dirty'] @@ -113,12 +113,12 @@ class FlagValues(gflags.FlagValues): for k in self.__dict__['__dirty']: setattr(self, k, getattr(new_flags, k)) self.ClearDirty() - + def __setitem__(self, name, flag): gflags.FlagValues.__setitem__(self, name, flag) if self.WasAlreadyParsed(): self.SetDirty(name) - + def __getitem__(self, name): if self.IsDirty(name): self.ParseNewFlags() @@ -166,6 +166,9 @@ def DECLARE(name, module_string, flag_values=FLAGS): # Define any app-specific flags in their own files, docs at: # http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#39 +DEFINE_list('region_list', + [], + 'list of region,url pairs') DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') -- cgit From ee206cd08bd2d82bb5d64b84b6804ba51ab56b37 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 18:51:22 -0700 Subject: moved keypairs to db using the same interface --- nova/auth/manager.py | 36 +++++++++++++++--------------------- nova/db/api.py | 23 +++++++++++++++++++++++ nova/db/sqlalchemy/api.py | 32 ++++++++++++++++++++++++++++++++ nova/db/sqlalchemy/models.py | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 21 deletions(-) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index d5fbec7c5..4cb23bea6 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -668,42 +668,36 @@ class AuthManager(object): with self.driver() as drv: if not drv.get_user(uid): raise exception.NotFound("User %s doesn't exist" % user) - if drv.get_key_pair(uid, key_name): - raise exception.Duplicate("The keypair %s already exists" - % key_name) + try: + db.keypair_get(None, uid, key_name) + raise exception.Duplicate("The keypair %s already exists" + % key_name) + except exception.NotFound: + pass private_key, public_key, fingerprint = crypto.generate_key_pair() self.create_key_pair(uid, key_name, public_key, fingerprint) return private_key, fingerprint def create_key_pair(self, user, key_name, public_key, fingerprint): """Creates a key pair for user""" - with self.driver() as drv: - kp_dict = drv.create_key_pair(User.safe_id(user), - key_name, - public_key, - fingerprint) - if kp_dict: - return KeyPair(**kp_dict) + key = {} + key['user_id'] = User.safe_id(user) + key['name'] = key_name + key['public_key'] = public_key + key['fingerprint'] = fingerprint + return db.keypair_create(None, key) def get_key_pair(self, user, key_name): """Retrieves a key pair for user""" - with self.driver() as drv: - kp_dict = drv.get_key_pair(User.safe_id(user), key_name) - if kp_dict: - return KeyPair(**kp_dict) + return db.keypair_get(None, User.safe_id(user), key_name) def get_key_pairs(self, user): """Retrieves all key pairs for user""" - with self.driver() as drv: - kp_list = drv.get_key_pairs(User.safe_id(user)) - if not kp_list: - return [] - return [KeyPair(**kp_dict) for kp_dict in kp_list] + return db.keypair_get_all_by_user(None, User.safe_id(user)) def delete_key_pair(self, user, key_name): """Deletes a key pair for user""" - with self.driver() as drv: - drv.delete_key_pair(User.safe_id(user), key_name) + return db.keypair_destroy(None, User.safe_id(user), key_name) def get_credentials(self, user, project=None): """Get credential zip for user in project""" diff --git a/nova/db/api.py b/nova/db/api.py index d81673fad..1db978c52 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -254,6 +254,29 @@ def instance_update(context, instance_id, values): return IMPL.instance_update(context, instance_id, values) +################### + + +def keypair_create(context, values): + """Create a keypair from the values dictionary.""" + return IMPL.keypair_create(context, values) + + +def keypair_destroy(context, user_id, name): + """Destroy the keypair or raise if it does not exist.""" + return IMPL.keypair_destroy(context, user_id, name) + + +def keypair_get(context, user_id, name): + """Get a keypair or raise if it does not exist.""" + return IMPL.keypair_get(context, user_id, name) + + +def keypair_get_all_by_user(context, user_id): + """Get all keypairs by user.""" + return IMPL.keypair_get_all_by_user(context, user_id) + + #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 02ebdd222..b3a307043 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -355,6 +355,38 @@ def instance_update(_context, instance_id, values): ################### +def keypair_create(_context, values): + keypair_ref = models.Keypair() + for (key, value) in values.iteritems(): + keypair_ref[key] = value + keypair_ref.save() + return keypair_ref + + +def keypair_destroy(_context, user_id, name): + session = get_session() + with session.begin(): + keypair_ref = models.Keypair.find_by_args(user_id, + name, + session=session) + keypair_ref.delete(session=session) + + +def keypair_get(_context, user_id, name): + return models.Keypair.find_by_args(user_id, name) + + +def keypair_get_all_by_user(_context, user_id): + session = get_session() + return session.query(models.Keypair + ).filter_by(user_id=user_id + ).filter_by(deleted=False + ).all() + + +################### + + def network_count(_context): return models.Network.count() diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 6818f838c..81c0a77a8 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -284,6 +284,42 @@ class ExportDevice(BASE, NovaBase): uselist=False)) +class Keypair(BASE, NovaBase): + """Represents a keypair""" + __tablename__ = 'keypairs' + id = Column(Integer, primary_key=True) + name = Column(String(255)) + + user_id = Column(String(255)) + + fingerprint = Column(String(255)) + public_key = Column(Text) + + @property + def str_id(self): + return '%s.%s' % (self.user_id, self.name) + + @classmethod + def find_by_str(cls, str_id, session=None, deleted=False): + user_id, _sep, name = str_id.partition('.') + return cls.find_by_str(user_id, name, session, deleted) + + @classmethod + def find_by_args(cls, user_id, name, session=None, deleted=False): + if not session: + session = get_session() + try: + return session.query(cls + ).filter_by(user_id=user_id + ).filter_by(name=name + ).filter_by(deleted=deleted + ).one() + except exc.NoResultFound: + new_exc = exception.NotFound("No model for user %s, name %s" % + (user_id, name)) + raise new_exc.__class__, new_exc, sys.exc_info()[2] + + class Network(BASE, NovaBase): """Represents a network""" __tablename__ = 'networks' -- cgit From d3273e594daf5f94f09c7904bac53fbb895ffeb6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 18:55:11 -0700 Subject: remove keypair from driver --- nova/auth/ldapdriver.py | 60 ------------------------------------------------- nova/auth/manager.py | 23 ------------------- 2 files changed, 83 deletions(-) diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py index 74ba011b5..4e9afc858 100644 --- a/nova/auth/ldapdriver.py +++ b/nova/auth/ldapdriver.py @@ -99,13 +99,6 @@ class LdapDriver(object): dn = FLAGS.ldap_user_subtree return self.__to_user(self.__find_object(dn, query)) - def get_key_pair(self, uid, key_name): - """Retrieve key pair by uid and key name""" - dn = 'cn=%s,%s' % (key_name, - self.__uid_to_dn(uid)) - attr = self.__find_object(dn, '(objectclass=novaKeyPair)') - return self.__to_key_pair(uid, attr) - def get_project(self, pid): """Retrieve project by id""" dn = 'cn=%s,%s' % (pid, @@ -119,12 +112,6 @@ class LdapDriver(object): '(objectclass=novaUser)') return [self.__to_user(attr) for attr in attrs] - def get_key_pairs(self, uid): - """Retrieve list of key pairs""" - attrs = self.__find_objects(self.__uid_to_dn(uid), - '(objectclass=novaKeyPair)') - return [self.__to_key_pair(uid, attr) for attr in attrs] - def get_projects(self, uid=None): """Retrieve list of projects""" pattern = '(objectclass=novaProject)' @@ -154,21 +141,6 @@ class LdapDriver(object): self.conn.add_s(self.__uid_to_dn(name), attr) return self.__to_user(dict(attr)) - def create_key_pair(self, uid, key_name, public_key, fingerprint): - """Create a key pair""" - # TODO(vish): possibly refactor this to store keys in their own ou - # and put dn reference in the user object - attr = [ - ('objectclass', ['novaKeyPair']), - ('cn', [key_name]), - ('sshPublicKey', [public_key]), - ('keyFingerprint', [fingerprint]), - ] - self.conn.add_s('cn=%s,%s' % (key_name, - self.__uid_to_dn(uid)), - attr) - return self.__to_key_pair(uid, dict(attr)) - def create_project(self, name, manager_uid, description=None, member_uids=None): """Create a project""" @@ -265,19 +237,10 @@ class LdapDriver(object): """Delete a user""" if not self.__user_exists(uid): raise exception.NotFound("User %s doesn't exist" % uid) - self.__delete_key_pairs(uid) self.__remove_from_all(uid) self.conn.delete_s('uid=%s,%s' % (uid, FLAGS.ldap_user_subtree)) - def delete_key_pair(self, uid, key_name): - """Delete a key pair""" - if not self.__key_pair_exists(uid, key_name): - raise exception.NotFound("Key Pair %s doesn't exist for user %s" % - (key_name, uid)) - self.conn.delete_s('cn=%s,uid=%s,%s' % (key_name, uid, - FLAGS.ldap_user_subtree)) - def delete_project(self, project_id): """Delete a project""" project_dn = 'cn=%s,%s' % (project_id, FLAGS.ldap_project_subtree) @@ -288,10 +251,6 @@ class LdapDriver(object): """Check if user exists""" return self.get_user(uid) != None - def __key_pair_exists(self, uid, key_name): - """Check if key pair exists""" - return self.get_key_pair(uid, key_name) != None - def __project_exists(self, project_id): """Check if project exists""" return self.get_project(project_id) != None @@ -341,13 +300,6 @@ class LdapDriver(object): """Check if group exists""" return self.__find_object(dn, '(objectclass=groupOfNames)') != None - def __delete_key_pairs(self, uid): - """Delete all key pairs for user""" - keys = self.get_key_pairs(uid) - if keys != None: - for key in keys: - self.delete_key_pair(uid, key['name']) - @staticmethod def __role_to_dn(role, project_id=None): """Convert role to corresponding dn""" @@ -472,18 +424,6 @@ class LdapDriver(object): 'secret': attr['secretKey'][0], 'admin': (attr['isAdmin'][0] == 'TRUE')} - @staticmethod - def __to_key_pair(owner, attr): - """Convert ldap attributes to KeyPair object""" - if attr == None: - return None - return { - 'id': attr['cn'][0], - 'name': attr['cn'][0], - 'owner_id': owner, - 'public_key': attr['sshPublicKey'][0], - 'fingerprint': attr['keyFingerprint'][0]} - def __to_project(self, attr): """Convert ldap attributes to Project object""" if attr == None: diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 4cb23bea6..ef6a5a486 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -154,29 +154,6 @@ class User(AuthBase): self.admin) -class KeyPair(AuthBase): - """Represents an ssh key returned from the datastore - - Even though this object is named KeyPair, only the public key and - fingerprint is stored. The user's private key is not saved. - """ - - def __init__(self, id, name, owner_id, public_key, fingerprint): - AuthBase.__init__(self) - self.id = id - self.name = name - self.owner_id = owner_id - self.public_key = public_key - self.fingerprint = fingerprint - - def __repr__(self): - return "KeyPair('%s', '%s', '%s', '%s', '%s')" % (self.id, - self.name, - self.owner_id, - self.public_key, - self.fingerprint) - - class Project(AuthBase): """Represents a Project returned from the datastore""" -- cgit From adb9cf9e71908844fd720e6f9bab9588610878e1 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 19:03:35 -0700 Subject: delete keypairs when a user is deleted --- nova/auth/manager.py | 8 ++++++-- nova/db/api.py | 5 +++++ nova/db/sqlalchemy/api.py | 8 ++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index ef6a5a486..e2bb748b0 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -620,9 +620,13 @@ class AuthManager(object): return User(**user_dict) def delete_user(self, user): - """Deletes a user""" + """Deletes a user + + Additionally deletes all users keypairs""" + uid = User.safe_id(user) + db.keypair_destroy_all_by_user(None, uid) with self.driver() as drv: - drv.delete_user(User.safe_id(user)) + drv.delete_user(uid) def generate_key_pair(self, user, key_name): """Generates a key pair for a user diff --git a/nova/db/api.py b/nova/db/api.py index 1db978c52..e96d803db 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -267,6 +267,11 @@ def keypair_destroy(context, user_id, name): return IMPL.keypair_destroy(context, user_id, name) +def keypair_destroy_all_by_user(context, user_id): + """Destroy all keypairs by user.""" + return IMPL.keypair_destroy_all_by_user(context, user_id) + + def keypair_get(context, user_id, name): """Get a keypair or raise if it does not exist.""" return IMPL.keypair_get(context, user_id, name) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b3a307043..4fd1bf216 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -372,6 +372,14 @@ def keypair_destroy(_context, user_id, name): keypair_ref.delete(session=session) +def keypair_destroy_all_by_user(_context, user_id): + session = get_session() + with session.begin(): + # TODO(vish): do we have to use sql here? + session.execute('update keypairs set deleted=1 where user_id=:id', + {'id': user_id}) + + def keypair_get(_context, user_id, name): return models.Keypair.find_by_args(user_id, name) -- cgit From 8e834931087c54585a7aa2716c7a0708fd658f30 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 22:13:36 -0700 Subject: move keypair generation out of auth and fix tests --- nova/auth/manager.py | 70 -------------------------------------------- nova/endpoint/cloud.py | 48 +++++++++++++++++++++--------- nova/tests/api_unittest.py | 7 +++-- nova/tests/auth_unittest.py | 31 -------------------- nova/tests/cloud_unittest.py | 53 ++++++++++++++++++++++++++++----- 5 files changed, 83 insertions(+), 126 deletions(-) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index e2bb748b0..fb87847d5 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -128,24 +128,6 @@ class User(AuthBase): def is_project_manager(self, project): return AuthManager().is_project_manager(self, project) - def generate_key_pair(self, name): - return AuthManager().generate_key_pair(self.id, name) - - def create_key_pair(self, name, public_key, fingerprint): - return AuthManager().create_key_pair(self.id, - name, - public_key, - fingerprint) - - def get_key_pair(self, name): - return AuthManager().get_key_pair(self.id, name) - - def delete_key_pair(self, name): - return AuthManager().delete_key_pair(self.id, name) - - def get_key_pairs(self): - return AuthManager().get_key_pairs(self.id) - def __repr__(self): return "User('%s', '%s', '%s', '%s', %s)" % (self.id, self.name, @@ -628,58 +610,6 @@ class AuthManager(object): with self.driver() as drv: drv.delete_user(uid) - def generate_key_pair(self, user, key_name): - """Generates a key pair for a user - - Generates a public and private key, stores the public key using the - key_name, and returns the private key and fingerprint. - - @type user: User or uid - @param user: User for which to create key pair. - - @type key_name: str - @param key_name: Name to use for the generated KeyPair. - - @rtype: tuple (private_key, fingerprint) - @return: A tuple containing the private_key and fingerprint. - """ - # NOTE(vish): generating key pair is slow so check for legal - # creation before creating keypair - uid = User.safe_id(user) - with self.driver() as drv: - if not drv.get_user(uid): - raise exception.NotFound("User %s doesn't exist" % user) - try: - db.keypair_get(None, uid, key_name) - raise exception.Duplicate("The keypair %s already exists" - % key_name) - except exception.NotFound: - pass - private_key, public_key, fingerprint = crypto.generate_key_pair() - self.create_key_pair(uid, key_name, public_key, fingerprint) - return private_key, fingerprint - - def create_key_pair(self, user, key_name, public_key, fingerprint): - """Creates a key pair for user""" - key = {} - key['user_id'] = User.safe_id(user) - key['name'] = key_name - key['public_key'] = public_key - key['fingerprint'] = fingerprint - return db.keypair_create(None, key) - - def get_key_pair(self, user, key_name): - """Retrieves a key pair for user""" - return db.keypair_get(None, User.safe_id(user), key_name) - - def get_key_pairs(self, user): - """Retrieves all key pairs for user""" - return db.keypair_get_all_by_user(None, User.safe_id(user)) - - def delete_key_pair(self, user, key_name): - """Deletes a key pair for user""" - return db.keypair_destroy(None, User.safe_id(user), key_name) - def get_credentials(self, user, project=None): """Get credential zip for user in project""" if not isinstance(user, User): diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 6ca6855ca..172c65d79 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -29,13 +29,13 @@ import time from twisted.internet import defer +from nova import crypto from nova import db from nova import exception from nova import flags from nova import rpc from nova import utils from nova.auth import rbac -from nova.auth import manager from nova.compute.instance_types import INSTANCE_TYPES from nova.endpoint import images @@ -44,14 +44,30 @@ FLAGS = flags.FLAGS flags.DECLARE('storage_availability_zone', 'nova.volume.manager') -def _gen_key(user_id, key_name): - """ Tuck this into AuthManager """ +def _gen_key(context, user_id, key_name): + """Generate a key + + This is a module level method because it is slow and we need to defer + it into a process pool.""" try: - mgr = manager.AuthManager() - private_key, fingerprint = mgr.generate_key_pair(user_id, key_name) + # NOTE(vish): generating key pair is slow so check for legal + # creation before creating keypair + try: + db.keypair_get(context, user_id, key_name) + raise exception.Duplicate("The keypair %s already exists" + % key_name) + except exception.NotFound: + pass + private_key, public_key, fingerprint = crypto.generate_key_pair() + key = {} + key['user_id'] = user_id + key['name'] = key_name + key['public_key'] = public_key + key['fingerprint'] = fingerprint + db.keypair_create(context, key) + return {'private_key': private_key, 'fingerprint': fingerprint} except Exception as ex: return {'exception': ex} - return {'private_key': private_key, 'fingerprint': fingerprint} class CloudController(object): @@ -177,18 +193,18 @@ class CloudController(object): @rbac.allow('all') def describe_key_pairs(self, context, key_name=None, **kwargs): - key_pairs = context.user.get_key_pairs() + key_pairs = db.keypair_get_all_by_user(context, context.user.id) if not key_name is None: - key_pairs = [x for x in key_pairs if x.name in key_name] + key_pairs = [x for x in key_pairs if x['name'] in key_name] result = [] for key_pair in key_pairs: # filter out the vpn keys suffix = FLAGS.vpn_key_suffix - if context.user.is_admin() or not key_pair.name.endswith(suffix): + if context.user.is_admin() or not key_pair['name'].endswith(suffix): result.append({ - 'keyName': key_pair.name, - 'keyFingerprint': key_pair.fingerprint, + 'keyName': key_pair['name'], + 'keyFingerprint': key_pair['fingerprint'], }) return {'keypairsSet': result} @@ -204,14 +220,18 @@ class CloudController(object): dcall.callback({'keyName': key_name, 'keyFingerprint': kwargs['fingerprint'], 'keyMaterial': kwargs['private_key']}) - pool.apply_async(_gen_key, [context.user.id, key_name], + # TODO(vish): when context is no longer an object, pass it here + pool.apply_async(_gen_key, [None, context.user.id, key_name], callback=_complete) return dcall @rbac.allow('all') def delete_key_pair(self, context, key_name, **kwargs): - context.user.delete_key_pair(key_name) - # aws returns true even if the key doens't exist + try: + db.keypair_destroy(context, context.user.id, key_name) + except exception.NotFound: + # aws returns true even if the key doesn't exist + pass return True @rbac.allow('all') diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 462d1b295..fdb9e21d8 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -41,8 +41,8 @@ FLAGS = flags.FLAGS # it's pretty damn circuitous so apologies if you have to fix # a bug in it # NOTE(jaypipes) The pylint disables here are for R0913 (too many args) which -# isn't controllable since boto's HTTPRequest needs that many -# args, and for the version-differentiated import of tornado's +# isn't controllable since boto's HTTPRequest needs that many +# args, and for the version-differentiated import of tornado's # httputil. # NOTE(jaypipes): The disable-msg=E1101 and E1103 below is because pylint is # unable to introspect the deferred's return value properly @@ -224,7 +224,8 @@ class ApiEc2TestCase(test.BaseTestCase): for x in range(random.randint(4, 8))) user = self.manager.create_user('fake', 'fake', 'fake') project = self.manager.create_project('fake', 'fake', 'fake') - self.manager.generate_key_pair(user.id, keyname) + # NOTE(vish): create depends on pool, so call helper directly + cloud._gen_key(None, user.id, keyname) rv = self.ec2.get_all_key_pairs() results = [k for k in rv if k.name == keyname] diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index b54e68274..1b4e12677 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -17,8 +17,6 @@ # under the License. import logging -from M2Crypto import BIO -from M2Crypto import RSA from M2Crypto import X509 import unittest @@ -65,35 +63,6 @@ class AuthTestCase(test.BaseTestCase): 'export S3_URL="http://127.0.0.1:3333/"\n' + 'export EC2_USER_ID="test1"\n') - def test_006_test_key_storage(self): - user = self.manager.get_user('test1') - user.create_key_pair('public', 'key', 'fingerprint') - key = user.get_key_pair('public') - self.assertEqual('key', key.public_key) - self.assertEqual('fingerprint', key.fingerprint) - - def test_007_test_key_generation(self): - user = self.manager.get_user('test1') - private_key, fingerprint = user.generate_key_pair('public2') - key = RSA.load_key_string(private_key, callback=lambda: None) - bio = BIO.MemoryBuffer() - public_key = user.get_key_pair('public2').public_key - key.save_pub_key_bio(bio) - converted = crypto.ssl_pub_to_ssh_pub(bio.read()) - # assert key fields are equal - self.assertEqual(public_key.split(" ")[1].strip(), - converted.split(" ")[1].strip()) - - def test_008_can_list_key_pairs(self): - keys = self.manager.get_user('test1').get_key_pairs() - self.assertTrue(filter(lambda k: k.name == 'public', keys)) - self.assertTrue(filter(lambda k: k.name == 'public2', keys)) - - def test_009_can_delete_key_pair(self): - self.manager.get_user('test1').delete_key_pair('public') - keys = self.manager.get_user('test1').get_key_pairs() - self.assertFalse(filter(lambda k: k.name == 'public', keys)) - def test_010_can_list_users(self): users = self.manager.get_users() logging.warn(users) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 29947e03c..4bad25c2b 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -17,13 +17,18 @@ # under the License. import logging +from M2Crypto import BIO +from M2Crypto import RSA import StringIO import time + from tornado import ioloop from twisted.internet import defer import unittest from xml.etree import ElementTree +from nova import crypto +from nova import db from nova import flags from nova import rpc from nova import test @@ -54,16 +59,21 @@ class CloudTestCase(test.BaseTestCase): proxy=self.compute) self.injected.append(self.compute_consumer.attach_to_tornado(self.ioloop)) - try: - manager.AuthManager().create_user('admin', 'admin', 'admin') - except: pass - admin = manager.AuthManager().get_user('admin') - project = manager.AuthManager().create_project('proj', 'admin', 'proj') - self.context = api.APIRequestContext(handler=None,project=project,user=admin) + self.manager = manager.AuthManager() + self.user = self.manager.create_user('admin', 'admin', 'admin', True) + self.project = self.manager.create_project('proj', 'admin', 'proj') + self.context = api.APIRequestContext(handler=None, + user=self.user, + project=self.project) def tearDown(self): - manager.AuthManager().delete_project('proj') - manager.AuthManager().delete_user('admin') + self.manager.delete_project(self.project) + self.manager.delete_user(self.user) + super(CloudTestCase, self).setUp() + + def _create_key(self, name): + # NOTE(vish): create depends on pool, so just call helper directly + return cloud._gen_key(self.context, self.context.user.id, name) def test_console_output(self): if FLAGS.connection_type == 'fake': @@ -76,6 +86,33 @@ class CloudTestCase(test.BaseTestCase): self.assert_(output) rv = yield self.compute.terminate_instance(instance_id) + + def test_key_generation(self): + result = self._create_key('test') + private_key = result['private_key'] + key = RSA.load_key_string(private_key, callback=lambda: None) + bio = BIO.MemoryBuffer() + public_key = db.keypair_get(self.context, + self.context.user.id, + 'test')['public_key'] + key.save_pub_key_bio(bio) + converted = crypto.ssl_pub_to_ssh_pub(bio.read()) + # assert key fields are equal + self.assertEqual(public_key.split(" ")[1].strip(), + converted.split(" ")[1].strip()) + + def test_describe_key_pairs(self): + self._create_key('test1') + self._create_key('test2') + result = self.cloud.describe_key_pairs(self.context) + keys = result["keypairsSet"] + self.assertTrue(filter(lambda k: k['keyName'] == 'test1', keys)) + self.assertTrue(filter(lambda k: k['keyName'] == 'test2', keys)) + + def test_delete_key_pair(self): + self._create_key('test') + self.cloud.delete_key_pair(self.context, 'test') + def test_run_instances(self): if FLAGS.connection_type == 'fake': logging.debug("Can't test instances without a real virtual env.") -- cgit From 38070f19036dcf24367429bcc79ffb55fad4b3cd Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 22:42:51 -0700 Subject: it is called regionEndpoint, and use pipe as a separator --- nova/endpoint/cloud.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 180af0540..02eb50b19 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -177,12 +177,12 @@ class CloudController(object): if FLAGS.region_list: regions = [] for region in FLAGS.region_list: - name, _sep, url = region.partition(',') + name, _sep, url = region.partition('|') regions.append({'regionName': name, - 'regionUrl': url}) + 'regionEndpoint': url}) else: regions = [{'regionName': 'nova', - 'regionUrl': FLAGS.ec2_url}] + 'regionEndpoint': FLAGS.ec2_url}] if region_name: regions = [r for r in regions if r['regionName'] in region_name] return {'regionInfo': regions } -- cgit From 9003fe35cfd2a6daa49d717bf256f2229171f7c6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 00:16:12 -0700 Subject: improved network error case handling for fixed ips --- bin/nova-dhcpbridge | 10 ++++++---- nova/network/manager.py | 27 +++++++++++++++++++++++++-- nova/tests/network_unittest.py | 41 ++++++++++++++++++++++++++++++++++------- 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge index 42eaf4bcb..2f75bf43b 100755 --- a/bin/nova-dhcpbridge +++ b/bin/nova-dhcpbridge @@ -46,16 +46,17 @@ flags.DECLARE('network_size', 'nova.network.manager') flags.DECLARE('num_networks', 'nova.network.manager') -def add_lease(_mac, ip_address, _hostname, _interface): +def add_lease(mac, ip_address, _hostname, _interface): """Set the IP that was assigned by the DHCP server.""" if FLAGS.fake_rabbit: logging.debug("leasing ip") network_manager = utils.import_object(FLAGS.network_manager) - network_manager.lease_fixed_ip(None, ip_address) + network_manager.lease_fixed_ip(None, mac, ip_address) else: rpc.cast("%s.%s" % (FLAGS.network_topic, FLAGS.host), {"method": "lease_fixed_ip", "args": {"context": None, + "mac": mac, "address": ip_address}}) @@ -64,16 +65,17 @@ def old_lease(_mac, _ip_address, _hostname, _interface): logging.debug("Adopted old lease or got a change of mac/hostname") -def del_lease(_mac, ip_address, _hostname, _interface): +def del_lease(mac, ip_address, _hostname, _interface): """Called when a lease expires.""" if FLAGS.fake_rabbit: logging.debug("releasing ip") network_manager = utils.import_object(FLAGS.network_manager) - network_manager.release_fixed_ip(None, ip_address) + network_manager.release_fixed_ip(None, mac, ip_address) else: rpc.cast("%s.%s" % (FLAGS.network_topic, FLAGS.host), {"method": "release_fixed_ip", "args": {"context": None, + "mac": mac, "address": ip_address}}) diff --git a/nova/network/manager.py b/nova/network/manager.py index 3212a7eab..79280384c 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -226,19 +226,42 @@ class VlanManager(NetworkManager): network_ref['vpn_private_address']) self.driver.update_dhcp(context, network_ref['id']) - def lease_fixed_ip(self, context, address): + def lease_fixed_ip(self, context, mac, address): """Called by dhcp-bridge when ip is leased""" logging.debug("Leasing IP %s", address) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) if not fixed_ip_ref['allocated']: logging.warn("IP %s leased that was already deallocated", address) + return + instance_ref = self.db.fixed_ip_get_instance(context, address) + if not instance_ref: + raise exception.Error("IP %s leased that isn't associated" % + address) + if instance_ref['mac_address'] != mac: + raise exception.Error("IP %s leased to bad mac %s vs %s" % + (address, instance_ref['mac_address'], mac)) self.db.fixed_ip_update(context, fixed_ip_ref['str_id'], {'leased': True}) - def release_fixed_ip(self, context, address): + def release_fixed_ip(self, context, mac, address): """Called by dhcp-bridge when ip is released""" logging.debug("Releasing IP %s", address) + fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) + if not fixed_ip_ref['leased']: + logging.warn("IP %s released that was not leased", address) + return + instance_ref = self.db.fixed_ip_get_instance(context, address) + if not instance_ref: + raise exception.Error("IP %s released that isn't associated" % + address) + if instance_ref['mac_address'] != mac: + raise exception.Error("IP %s released from bad mac %s vs %s" % + (address, instance_ref['mac_address'], mac)) + if fixed_ip_ref['allocated']: + logging.warn("IP %s released that is still allocated", address) + self.db.fixed_ip_update(context, address, {'leased': False}) + return self.db.fixed_ip_update(context, address, {'allocated': False, 'leased': False}) self.db.fixed_ip_instance_disassociate(context, address) diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index 9958600e0..d8d4ec0c3 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -147,10 +147,23 @@ class NetworkTestCase(test.TrialTestCase): """Makes sure that private ips don't overlap""" first = self._create_address(0) lease_ip(first) + instance_ids = [] for i in range(1, 5): - address = self._create_address(i) - address2 = self._create_address(i) - address3 = self._create_address(i) + mac = utils.generate_mac() + instance_ref = db.instance_create(None, + {'mac_address': mac}) + instance_ids.append(instance_ref['id']) + address = self._create_address(i, instance_ref['id']) + mac = utils.generate_mac() + instance_ref = db.instance_create(None, + {'mac_address': mac}) + instance_ids.append(instance_ref['id']) + address2 = self._create_address(i, instance_ref['id']) + mac = utils.generate_mac() + instance_ref = db.instance_create(None, + {'mac_address': mac}) + instance_ids.append(instance_ref['id']) + address3 = self._create_address(i, instance_ref['id']) lease_ip(address) lease_ip(address2) lease_ip(address3) @@ -166,6 +179,8 @@ class NetworkTestCase(test.TrialTestCase): release_ip(address) release_ip(address2) release_ip(address3) + for instance_id in instance_ids: + db.instance_destroy(None, instance_id) release_ip(first) db.fixed_ip_deallocate(None, first) @@ -226,8 +241,13 @@ class NetworkTestCase(test.TrialTestCase): num_available_ips = db.network_count_available_ips(None, network['id']) addresses = [] + instance_ids = [] for i in range(num_available_ips): - address = self._create_address(0) + mac = utils.generate_mac() + instance_ref = db.instance_create(None, + {'mac_address': mac}) + instance_ids.append(instance_ref['id']) + address = self._create_address(0, instance_ref['id']) addresses.append(address) lease_ip(address) @@ -238,9 +258,10 @@ class NetworkTestCase(test.TrialTestCase): None, network['id']) - for i in range(len(addresses)): + for i in range(num_available_ips): db.fixed_ip_deallocate(None, addresses[i]) release_ip(addresses[i]) + db.instance_destroy(None, instance_ids[i]) self.assertEqual(db.network_count_available_ips(None, network['id']), num_available_ips) @@ -263,7 +284,10 @@ def binpath(script): def lease_ip(private_ip): """Run add command on dhcpbridge""" network_ref = db.fixed_ip_get_network(None, private_ip) - cmd = "%s add fake %s fake" % (binpath('nova-dhcpbridge'), private_ip) + instance_ref = db.fixed_ip_get_instance(None, private_ip) + cmd = "%s add %s %s fake" % (binpath('nova-dhcpbridge'), + instance_ref['mac_address'], + private_ip) env = {'DNSMASQ_INTERFACE': network_ref['bridge'], 'TESTING': '1', 'FLAGFILE': FLAGS.dhcpbridge_flagfile} @@ -274,7 +298,10 @@ def lease_ip(private_ip): def release_ip(private_ip): """Run del command on dhcpbridge""" network_ref = db.fixed_ip_get_network(None, private_ip) - cmd = "%s del fake %s fake" % (binpath('nova-dhcpbridge'), private_ip) + instance_ref = db.fixed_ip_get_instance(None, private_ip) + cmd = "%s del %s %s fake" % (binpath('nova-dhcpbridge'), + instance_ref['mac_address'], + private_ip) env = {'DNSMASQ_INTERFACE': network_ref['bridge'], 'TESTING': '1', 'FLAGFILE': FLAGS.dhcpbridge_flagfile} -- cgit From 6083273c9949b0e49a0c0af7cfc8f0fb83ea7c79 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 03:06:27 -0700 Subject: fix network association issue --- nova/db/sqlalchemy/api.py | 1 + nova/network/manager.py | 27 +++++++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 02ebdd222..bcdea4b67 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -185,6 +185,7 @@ def fixed_ip_allocate(_context, network_id): ).filter_by(allocated=False ).filter_by(leased=False ).filter_by(deleted=False + ).filter_by(instance=None ).with_lockmode('update' ).first() # NOTE(vish): if with_lockmode isn't supported, as in sqlite, diff --git a/nova/network/manager.py b/nova/network/manager.py index 79280384c..18a8ec0a1 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -96,6 +96,10 @@ class NetworkManager(manager.Manager): """Gets a fixed ip from the pool""" raise NotImplementedError() + def deallocate_fixed_ip(self, context, instance_id, *args, **kwargs): + """Returns a fixed ip to the pool""" + raise NotImplementedError() + def setup_fixed_ip(self, context, address): """Sets up rules for fixed ip""" raise NotImplementedError() @@ -174,6 +178,11 @@ class FlatManager(NetworkManager): self.db.fixed_ip_instance_associate(context, address, instance_id) return address + def deallocate_fixed_ip(self, context, address, *args, **kwargs): + """Returns a fixed ip to the pool""" + self.db.fixed_ip_deallocate(context, address) + self.db.fixed_ip_instance_disassociate(context, address) + def setup_compute_network(self, context, project_id): """Network is created manually""" pass @@ -216,6 +225,14 @@ class VlanManager(NetworkManager): self.db.fixed_ip_instance_associate(context, address, instance_id) return address + def deallocate_fixed_ip(self, context, address, *args, **kwargs): + """Returns a fixed ip to the pool""" + self.db.fixed_ip_deallocate(context, address) + fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) + if not fixed_ip_ref['leased']: + self.db.fixed_ip_instance_disassociate(context, address) + + def setup_fixed_ip(self, context, address): """Sets forwarding rules and dhcp for fixed ip""" fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) @@ -258,13 +275,11 @@ class VlanManager(NetworkManager): if instance_ref['mac_address'] != mac: raise exception.Error("IP %s released from bad mac %s vs %s" % (address, instance_ref['mac_address'], mac)) - if fixed_ip_ref['allocated']: + self.db.fixed_ip_update(context, address, {'leased': False}) + if not fixed_ip_ref['allocated']: + self.db.fixed_ip_instance_disassociate(context, address) + else: logging.warn("IP %s released that is still allocated", address) - self.db.fixed_ip_update(context, address, {'leased': False}) - return - self.db.fixed_ip_update(context, address, {'allocated': False, - 'leased': False}) - self.db.fixed_ip_instance_disassociate(context, address) def allocate_network(self, context, project_id): """Set up the network""" -- cgit From 2f3a63ac73176ed91cfcf8b011a2769fbf88201a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 03:31:40 -0700 Subject: simplified network instance association --- nova/db/api.py | 30 ++++++++++++-------------- nova/db/sqlalchemy/api.py | 54 +++++++++++++++++++++++++---------------------- nova/network/manager.py | 41 +++++++++++++---------------------- 3 files changed, 58 insertions(+), 67 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index d81673fad..6a0386bad 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -139,12 +139,20 @@ def floating_ip_get_instance(context, address): #################### -def fixed_ip_allocate(context, network_id): - """Allocate free fixed ip and return the address. +def fixed_ip_associate(context, address, instance_id): + """Associate fixed ip to instance. + + Raises if fixed ip is not available. + """ + return IMPL.fixed_ip_allocate(context, address, instance_id) + + +def fixed_ip_associate_pool(context, network_id, instance_id): + """Find free ip in network and associate it to instance. Raises if one is not available. """ - return IMPL.fixed_ip_allocate(context, network_id) + return IMPL.fixed_ip_allocate(context, network_id, instance_id) def fixed_ip_create(context, values): @@ -152,9 +160,9 @@ def fixed_ip_create(context, values): return IMPL.fixed_ip_create(context, values) -def fixed_ip_deallocate(context, address): - """Deallocate a fixed ip by address.""" - return IMPL.fixed_ip_deallocate(context, address) +def fixed_ip_disassociate(context, address): + """Disassociate a fixed ip from an instance by address.""" + return IMPL.fixed_ip_instance_disassociate(context, address) def fixed_ip_get_by_address(context, address): @@ -172,16 +180,6 @@ def fixed_ip_get_network(context, address): return IMPL.fixed_ip_get_network(context, address) -def fixed_ip_instance_associate(context, address, instance_id): - """Associate a fixed ip to an instance by address.""" - return IMPL.fixed_ip_instance_associate(context, address, instance_id) - - -def fixed_ip_instance_disassociate(context, address): - """Disassociate a fixed ip from an instance by address.""" - return IMPL.fixed_ip_instance_disassociate(context, address) - - def fixed_ip_update(context, address, values): """Create a fixed ip from the values dictionary.""" return IMPL.fixed_ip_update(context, address, values) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index bcdea4b67..485dca2b0 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -174,7 +174,25 @@ def floating_ip_get_instance(_context, address): ################### -def fixed_ip_allocate(_context, network_id): +def fixed_ip_associate(_context, address, instance_id): + session = get_session() + with session.begin(): + fixed_ip_ref = session.query(models.FixedIp + ).filter_by(address=address + ).filter_by(deleted=False + ).filter_by(instance=None + ).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 db.NoMoreAddresses() + fixed_ip_ref.instance = models.Instance.find(instance_id, + session=session) + session.add(fixed_ip_ref) + + +def fixed_ip_associate_pool(_context, network_id, instance_id): session = get_session() with session.begin(): network_or_none = or_(models.FixedIp.network_id == network_id, @@ -182,8 +200,6 @@ def fixed_ip_allocate(_context, network_id): fixed_ip_ref = session.query(models.FixedIp ).filter(network_or_none ).filter_by(reserved=False - ).filter_by(allocated=False - ).filter_by(leased=False ).filter_by(deleted=False ).filter_by(instance=None ).with_lockmode('update' @@ -195,7 +211,8 @@ def fixed_ip_allocate(_context, network_id): if not fixed_ip_ref.network: fixed_ip_ref.network = models.Network.find(network_id, session=session) - fixed_ip_ref['allocated'] = True + fixed_ip_ref.instance = models.Instance.find(instance_id, + session=session) session.add(fixed_ip_ref) return fixed_ip_ref['address'] @@ -208,6 +225,14 @@ def fixed_ip_create(_context, values): return fixed_ip_ref['address'] +def fixed_ip_disassociate(_context, address): + session = get_session() + with session.begin(): + fixed_ip_ref = models.FixedIp.find_by_str(address, session=session) + fixed_ip_ref.instance = None + fixed_ip_ref.save(session=session) + + def fixed_ip_get_by_address(_context, address): return models.FixedIp.find_by_str(address) @@ -224,27 +249,6 @@ def fixed_ip_get_network(_context, address): return models.FixedIp.find_by_str(address, session=session).network -def fixed_ip_deallocate(context, address): - db.fixed_ip_update(context, address, {'allocated': False}) - - -def fixed_ip_instance_associate(_context, address, instance_id): - session = get_session() - with session.begin(): - fixed_ip_ref = models.FixedIp.find_by_str(address, session=session) - instance_ref = models.Instance.find(instance_id, session=session) - fixed_ip_ref.instance = instance_ref - fixed_ip_ref.save(session=session) - - -def fixed_ip_instance_disassociate(_context, address): - session = get_session() - with session.begin(): - fixed_ip_ref = models.FixedIp.find_by_str(address, session=session) - fixed_ip_ref.instance = None - fixed_ip_ref.save(session=session) - - def fixed_ip_update(_context, address, values): session = get_session() with session.begin(): diff --git a/nova/network/manager.py b/nova/network/manager.py index 18a8ec0a1..fbc4e2b26 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -174,14 +174,16 @@ class FlatManager(NetworkManager): def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): """Gets a fixed ip from the pool""" network_ref = self.db.project_get_network(context, context.project.id) - address = self.db.fixed_ip_allocate(context, network_ref['id']) - self.db.fixed_ip_instance_associate(context, address, instance_id) + address = self.db.fixed_ip_associate_pool(context, + network_ref['id'], + instance_id) + self.db.fixed_ip_update(context, address, {'allocated': True}) return address def deallocate_fixed_ip(self, context, address, *args, **kwargs): """Returns a fixed ip to the pool""" - self.db.fixed_ip_deallocate(context, address) - self.db.fixed_ip_instance_disassociate(context, address) + self.db.fixed_ip_update(context, address, {'allocated': False}) + self.db.fixed_ip_disassociate(context, address) def setup_compute_network(self, context, project_id): """Network is created manually""" @@ -218,19 +220,21 @@ class VlanManager(NetworkManager): """Gets a fixed ip from the pool""" network_ref = self.db.project_get_network(context, context.project.id) if kwargs.get('vpn', None): - address = self._allocate_vpn_ip(context, network_ref['id']) + address = network_ref['vpn_private_address'] + self.db.fixed_ip_associate(context, address, instance_id) else: - address = self.db.fixed_ip_allocate(context, - network_ref['id']) - self.db.fixed_ip_instance_associate(context, address, instance_id) + address = self.db.fixed_ip_associate_pool(context, + network_ref['id'], + instance_id) + self.db.fixed_ip_update(context, address, {'allocated': True}) return address def deallocate_fixed_ip(self, context, address, *args, **kwargs): """Returns a fixed ip to the pool""" - self.db.fixed_ip_deallocate(context, address) + self.db.fixed_ip_update(context, address, {'allocated': False}) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) if not fixed_ip_ref['leased']: - self.db.fixed_ip_instance_disassociate(context, address) + self.db.fixed_ip_disassociate(context, address) def setup_fixed_ip(self, context, address): @@ -277,9 +281,7 @@ class VlanManager(NetworkManager): (address, instance_ref['mac_address'], mac)) self.db.fixed_ip_update(context, address, {'leased': False}) if not fixed_ip_ref['allocated']: - self.db.fixed_ip_instance_disassociate(context, address) - else: - logging.warn("IP %s released that is still allocated", address) + self.db.fixed_ip_disassociate(context, address) def allocate_network(self, context, project_id): """Set up the network""" @@ -321,19 +323,6 @@ class VlanManager(NetworkManager): # TODO(vish): Implement this pass - @staticmethod - def _allocate_vpn_ip(context, network_id): - """Allocate vpn ip for network""" - # TODO(vish): There is a possible concurrency issue here. - network_ref = db.network_get(context, network_id) - address = network_ref['vpn_private_address'] - fixed_ip_ref = db.fixed_ip_get_by_address(context, address) - # TODO(vish): Should this be fixed_ip_is_allocated? - if fixed_ip_ref['allocated']: - raise AddressAlreadyAllocated() - db.fixed_ip_update(context, fixed_ip_ref['id'], {'allocated': True}) - return fixed_ip_ref['str_id'] - def _ensure_indexes(self, context): """Ensure the indexes for the network exist -- cgit From b574d88fd6b27ac59bc51867e824f4ec9e1f7632 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 04:01:44 -0700 Subject: fixed tests, added a flag for updating dhcp on disassociate --- nova/db/api.py | 6 +++--- nova/endpoint/cloud.py | 2 +- nova/network/manager.py | 14 ++++++++++++++ nova/tests/network_unittest.py | 37 ++++++++++++++++++------------------- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index 6a0386bad..d749ae50a 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -144,7 +144,7 @@ def fixed_ip_associate(context, address, instance_id): Raises if fixed ip is not available. """ - return IMPL.fixed_ip_allocate(context, address, instance_id) + return IMPL.fixed_ip_associate(context, address, instance_id) def fixed_ip_associate_pool(context, network_id, instance_id): @@ -152,7 +152,7 @@ def fixed_ip_associate_pool(context, network_id, instance_id): Raises if one is not available. """ - return IMPL.fixed_ip_allocate(context, network_id, instance_id) + return IMPL.fixed_ip_associate_pool(context, network_id, instance_id) def fixed_ip_create(context, values): @@ -162,7 +162,7 @@ def fixed_ip_create(context, values): def fixed_ip_disassociate(context, address): """Disassociate a fixed ip from an instance by address.""" - return IMPL.fixed_ip_instance_disassociate(context, address) + return IMPL.fixed_ip_disassociate(context, address) def fixed_ip_get_by_address(context, address): diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 6ca6855ca..622b4e2a4 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -613,7 +613,7 @@ class CloudController(object): # NOTE(vish): Currently, nothing needs to be done on the # network node until release. If this changes, # we will need to cast here. - db.fixed_ip_deallocate(context, address) + self.network.deallocate_fixed_ip(context, address) host = instance_ref['host'] if host: diff --git a/nova/network/manager.py b/nova/network/manager.py index fbc4e2b26..d0036c7d9 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -61,6 +61,8 @@ flags.DEFINE_integer('cnt_vpn_clients', 5, 'Number of addresses reserved for vpn clients') flags.DEFINE_string('network_driver', 'nova.network.linux_net', 'Driver to use for network creation') +flags.DEFINE_boool('update_dhcp_on_disassocate', False, + 'Whether to update dhcp when fixed_ip is disassocated') class AddressAlreadyAllocated(exception.Error): @@ -235,6 +237,12 @@ class VlanManager(NetworkManager): fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) if not fixed_ip_ref['leased']: self.db.fixed_ip_disassociate(context, address) + # NOTE(vish): dhcp server isn't updated until next setup, this + # means there will stale entries in the conf file + # the code below will update the file if necessary + if FLAGS.update_dhcp_on_disassociate: + network_ref = self.db.fixed_ip_get_network(context, address) + self.driver.update_dhcp(context, network_ref['id']) def setup_fixed_ip(self, context, address): @@ -282,6 +290,12 @@ class VlanManager(NetworkManager): self.db.fixed_ip_update(context, address, {'leased': False}) if not fixed_ip_ref['allocated']: self.db.fixed_ip_disassociate(context, address) + # NOTE(vish): dhcp server isn't updated until next setup, this + # means there will stale entries in the conf file + # the code below will update the file if necessary + if FLAGS.update_dhcp_on_disassociate: + network_ref = self.db.fixed_ip_get_network(context, address) + self.driver.update_dhcp(context, network_ref['id']) def allocate_network(self, context, project_id): """Set up the network""" diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index d8d4ec0c3..dc5277f02 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -28,6 +28,7 @@ from nova import flags from nova import test from nova import utils from nova.auth import manager +from nova.endpoint import api FLAGS = flags.FLAGS @@ -48,7 +49,7 @@ class NetworkTestCase(test.TrialTestCase): self.user = self.manager.create_user('netuser', 'netuser', 'netuser') self.projects = [] self.network = utils.import_object(FLAGS.network_manager) - self.context = None + self.context = api.APIRequestContext(None, project=None, user=self.user) for i in range(5): name = 'project%s' % i self.projects.append(self.manager.create_project(name, @@ -75,12 +76,10 @@ class NetworkTestCase(test.TrialTestCase): def _create_address(self, project_num, instance_id=None): """Create an address in given project num""" - net = db.project_get_network(None, self.projects[project_num].id) - address = db.fixed_ip_allocate(None, net['id']) if instance_id is None: instance_id = self.instance_id - db.fixed_ip_instance_associate(None, address, instance_id) - return address + self.context.project = self.projects[project_num] + return self.network.allocate_fixed_ip(self.context, instance_id) def test_public_network_association(self): """Makes sure that we can allocaate a public ip""" @@ -103,14 +102,14 @@ class NetworkTestCase(test.TrialTestCase): address = db.instance_get_floating_address(None, self.instance_id) self.assertEqual(address, None) self.network.deallocate_floating_ip(self.context, float_addr) - db.fixed_ip_deallocate(None, fix_addr) + self.network.deallocate_fixed_ip(self.context, fix_addr) def test_allocate_deallocate_fixed_ip(self): """Makes sure that we can allocate and deallocate a fixed ip""" address = self._create_address(0) self.assertTrue(is_allocated_in_project(address, self.projects[0].id)) lease_ip(address) - db.fixed_ip_deallocate(None, address) + self.network.deallocate_fixed_ip(self.context, address) # Doesn't go away until it's dhcp released self.assertTrue(is_allocated_in_project(address, self.projects[0].id)) @@ -131,14 +130,14 @@ class NetworkTestCase(test.TrialTestCase): lease_ip(address) lease_ip(address2) - db.fixed_ip_deallocate(None, address) + self.network.deallocate_fixed_ip(self.context, address) release_ip(address) self.assertFalse(is_allocated_in_project(address, self.projects[0].id)) # First address release shouldn't affect the second self.assertTrue(is_allocated_in_project(address2, self.projects[1].id)) - db.fixed_ip_deallocate(None, address2) + self.network.deallocate_fixed_ip(self.context, address2) release_ip(address2) self.assertFalse(is_allocated_in_project(address2, self.projects[1].id)) @@ -173,16 +172,16 @@ class NetworkTestCase(test.TrialTestCase): self.projects[0].id)) self.assertFalse(is_allocated_in_project(address3, self.projects[0].id)) - db.fixed_ip_deallocate(None, address) - db.fixed_ip_deallocate(None, address2) - db.fixed_ip_deallocate(None, address3) + self.network.deallocate_fixed_ip(self.context, address) + self.network.deallocate_fixed_ip(self.context, address2) + self.network.deallocate_fixed_ip(self.context, address3) release_ip(address) release_ip(address2) release_ip(address3) for instance_id in instance_ids: db.instance_destroy(None, instance_id) release_ip(first) - db.fixed_ip_deallocate(None, first) + self.network.deallocate_fixed_ip(self.context, first) def test_vpn_ip_and_port_looks_valid(self): """Ensure the vpn ip and port are reasonable""" @@ -209,12 +208,12 @@ class NetworkTestCase(test.TrialTestCase): """Makes sure that ip addresses that are deallocated get reused""" address = self._create_address(0) lease_ip(address) - db.fixed_ip_deallocate(None, address) + self.network.deallocate_fixed_ip(self.context, address) release_ip(address) address2 = self._create_address(0) self.assertEqual(address, address2) - db.fixed_ip_deallocate(None, address2) + self.network.deallocate_fixed_ip(self.context, address2) def test_available_ips(self): """Make sure the number of available ips for the network is correct @@ -254,12 +253,12 @@ class NetworkTestCase(test.TrialTestCase): self.assertEqual(db.network_count_available_ips(None, network['id']), 0) self.assertRaises(db.NoMoreAddresses, - db.fixed_ip_allocate, - None, - network['id']) + self.network.allocate_fixed_ip, + self.context, + 'foo') for i in range(num_available_ips): - db.fixed_ip_deallocate(None, addresses[i]) + self.network.deallocate_fixed_ip(self.context, addresses[i]) release_ip(addresses[i]) db.instance_destroy(None, instance_ids[i]) self.assertEqual(db.network_count_available_ips(None, -- cgit From fe78b3651c9064e527b8e3b74d7669d3d364daab Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 04:06:22 -0700 Subject: typo fixes, add flag to nova-dhcpbridge --- bin/nova-dhcpbridge | 1 + nova/network/manager.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge index 2f75bf43b..a127ed03c 100755 --- a/bin/nova-dhcpbridge +++ b/bin/nova-dhcpbridge @@ -44,6 +44,7 @@ flags.DECLARE('auth_driver', 'nova.auth.manager') flags.DECLARE('redis_db', 'nova.datastore') flags.DECLARE('network_size', 'nova.network.manager') flags.DECLARE('num_networks', 'nova.network.manager') +flags.DECLARE('update_dhcp_on_disassociate', 'nova.network.manager') def add_lease(mac, ip_address, _hostname, _interface): diff --git a/nova/network/manager.py b/nova/network/manager.py index d0036c7d9..7a3bcfc2f 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -61,8 +61,8 @@ flags.DEFINE_integer('cnt_vpn_clients', 5, 'Number of addresses reserved for vpn clients') flags.DEFINE_string('network_driver', 'nova.network.linux_net', 'Driver to use for network creation') -flags.DEFINE_boool('update_dhcp_on_disassocate', False, - 'Whether to update dhcp when fixed_ip is disassocated') +flags.DEFINE_bool('update_dhcp_on_disassociate', False, + 'Whether to update dhcp when fixed_ip is disassocated') class AddressAlreadyAllocated(exception.Error): -- cgit From 66c583b1883af6e3452271df4b302fd32d1ee25d Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 04:18:30 -0700 Subject: fixed old key reference and made keypair name constistent -> key_pair --- nova/auth/manager.py | 4 ++-- nova/cloudpipe/pipelib.py | 4 ++-- nova/crypto.py | 2 +- nova/db/api.py | 30 +++++++++++++++--------------- nova/db/sqlalchemy/api.py | 28 ++++++++++++++-------------- nova/db/sqlalchemy/models.py | 6 +++--- nova/endpoint/cloud.py | 21 ++++++++++----------- nova/tests/cloud_unittest.py | 2 +- 8 files changed, 48 insertions(+), 49 deletions(-) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index fb87847d5..4e321c1bd 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -604,9 +604,9 @@ class AuthManager(object): def delete_user(self, user): """Deletes a user - Additionally deletes all users keypairs""" + Additionally deletes all users key_pairs""" uid = User.safe_id(user) - db.keypair_destroy_all_by_user(None, uid) + db.key_pair_destroy_all_by_user(None, uid) with self.driver() as drv: drv.delete_user(uid) diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index 2867bcb21..de6a97fb6 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -58,7 +58,7 @@ class CloudPipe(object): z.write(FLAGS.boot_script_template,'autorun.sh') z.close() - key_name = self.setup_keypair(project.project_manager_id, project_id) + key_name = self.setup_key_pair(project.project_manager_id, project_id) zippy = open(zippath, "r") context = api.APIRequestContext(handler=None, user=project.project_manager, project=project) @@ -74,7 +74,7 @@ class CloudPipe(object): security_groups=["vpn-secgroup"]) zippy.close() - def setup_keypair(self, user_id, project_id): + def setup_key_pair(self, user_id, project_id): key_name = '%s%s' % (project_id, FLAGS.vpn_key_suffix) try: private_key, fingerprint = self.manager.generate_key_pair(user_id, key_name) diff --git a/nova/crypto.py b/nova/crypto.py index b05548ea1..1c6fe57ad 100644 --- a/nova/crypto.py +++ b/nova/crypto.py @@ -18,7 +18,7 @@ """ Wrappers around standard crypto, including root and intermediate CAs, -SSH keypairs and x509 certificates. +SSH key_pairs and x509 certificates. """ import base64 diff --git a/nova/db/api.py b/nova/db/api.py index e96d803db..507f70dd5 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -257,29 +257,29 @@ def instance_update(context, instance_id, values): ################### -def keypair_create(context, values): - """Create a keypair from the values dictionary.""" - return IMPL.keypair_create(context, values) +def key_pair_create(context, values): + """Create a key_pair from the values dictionary.""" + return IMPL.key_pair_create(context, values) -def keypair_destroy(context, user_id, name): - """Destroy the keypair or raise if it does not exist.""" - return IMPL.keypair_destroy(context, user_id, name) +def key_pair_destroy(context, user_id, name): + """Destroy the key_pair or raise if it does not exist.""" + return IMPL.key_pair_destroy(context, user_id, name) -def keypair_destroy_all_by_user(context, user_id): - """Destroy all keypairs by user.""" - return IMPL.keypair_destroy_all_by_user(context, user_id) +def key_pair_destroy_all_by_user(context, user_id): + """Destroy all key_pairs by user.""" + return IMPL.key_pair_destroy_all_by_user(context, user_id) -def keypair_get(context, user_id, name): - """Get a keypair or raise if it does not exist.""" - return IMPL.keypair_get(context, user_id, name) +def key_pair_get(context, user_id, name): + """Get a key_pair or raise if it does not exist.""" + return IMPL.key_pair_get(context, user_id, name) -def keypair_get_all_by_user(context, user_id): - """Get all keypairs by user.""" - return IMPL.keypair_get_all_by_user(context, user_id) +def key_pair_get_all_by_user(context, user_id): + """Get all key_pairs by user.""" + return IMPL.key_pair_get_all_by_user(context, user_id) #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4fd1bf216..ce97f6710 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -355,38 +355,38 @@ def instance_update(_context, instance_id, values): ################### -def keypair_create(_context, values): - keypair_ref = models.Keypair() +def key_pair_create(_context, values): + key_pair_ref = models.KeyPair() for (key, value) in values.iteritems(): - keypair_ref[key] = value - keypair_ref.save() - return keypair_ref + key_pair_ref[key] = value + key_pair_ref.save() + return key_pair_ref -def keypair_destroy(_context, user_id, name): +def key_pair_destroy(_context, user_id, name): session = get_session() with session.begin(): - keypair_ref = models.Keypair.find_by_args(user_id, + key_pair_ref = models.KeyPair.find_by_args(user_id, name, session=session) - keypair_ref.delete(session=session) + key_pair_ref.delete(session=session) -def keypair_destroy_all_by_user(_context, user_id): +def key_pair_destroy_all_by_user(_context, user_id): session = get_session() with session.begin(): # TODO(vish): do we have to use sql here? - session.execute('update keypairs set deleted=1 where user_id=:id', + session.execute('update key_pairs set deleted=1 where user_id=:id', {'id': user_id}) -def keypair_get(_context, user_id, name): - return models.Keypair.find_by_args(user_id, name) +def key_pair_get(_context, user_id, name): + return models.KeyPair.find_by_args(user_id, name) -def keypair_get_all_by_user(_context, user_id): +def key_pair_get_all_by_user(_context, user_id): session = get_session() - return session.query(models.Keypair + return session.query(models.KeyPair ).filter_by(user_id=user_id ).filter_by(deleted=False ).all() diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 81c0a77a8..0ecc48bae 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -284,9 +284,9 @@ class ExportDevice(BASE, NovaBase): uselist=False)) -class Keypair(BASE, NovaBase): - """Represents a keypair""" - __tablename__ = 'keypairs' +class KeyPair(BASE, NovaBase): + """Represents a public key pair for ssh""" + __tablename__ = 'key_pairs' id = Column(Integer, primary_key=True) name = Column(String(255)) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 172c65d79..f30565aca 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -51,10 +51,10 @@ def _gen_key(context, user_id, key_name): it into a process pool.""" try: # NOTE(vish): generating key pair is slow so check for legal - # creation before creating keypair + # creation before creating key_pair try: - db.keypair_get(context, user_id, key_name) - raise exception.Duplicate("The keypair %s already exists" + db.key_pair_get(context, user_id, key_name) + raise exception.Duplicate("The key_pair %s already exists" % key_name) except exception.NotFound: pass @@ -64,7 +64,7 @@ def _gen_key(context, user_id, key_name): key['name'] = key_name key['public_key'] = public_key key['fingerprint'] = fingerprint - db.keypair_create(context, key) + db.key_pair_create(context, key) return {'private_key': private_key, 'fingerprint': fingerprint} except Exception as ex: return {'exception': ex} @@ -193,7 +193,7 @@ class CloudController(object): @rbac.allow('all') def describe_key_pairs(self, context, key_name=None, **kwargs): - key_pairs = db.keypair_get_all_by_user(context, context.user.id) + key_pairs = db.key_pair_get_all_by_user(context, context.user.id) if not key_name is None: key_pairs = [x for x in key_pairs if x['name'] in key_name] @@ -228,7 +228,7 @@ class CloudController(object): @rbac.allow('all') def delete_key_pair(self, context, key_name, **kwargs): try: - db.keypair_destroy(context, context.user.id, key_name) + db.key_pair_destroy(context, context.user.id, key_name) except exception.NotFound: # aws returns true even if the key doesn't exist pass @@ -545,11 +545,10 @@ class CloudController(object): launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) key_data = None if kwargs.has_key('key_name'): - key_pair = context.user.get_key_pair(kwargs['key_name']) - if not key_pair: - raise exception.ApiError('Key Pair %s not found' % - kwargs['key_name']) - key_data = key_pair.public_key + key_pair_ref = db.key_pair_get(context, + context.user.id, + kwargs['key_name']) + key_data = key_pair_ref['public_key'] # TODO: Get the real security group of launch in here security_group = "default" diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 4bad25c2b..e56ea6ac2 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -92,7 +92,7 @@ class CloudTestCase(test.BaseTestCase): private_key = result['private_key'] key = RSA.load_key_string(private_key, callback=lambda: None) bio = BIO.MemoryBuffer() - public_key = db.keypair_get(self.context, + public_key = db.key_pair_get(self.context, self.context.user.id, 'test')['public_key'] key.save_pub_key_bio(bio) -- cgit From 619e9fd636854b55e7f3334f93ed759ff82759f0 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 04:48:37 -0700 Subject: fixed typo network => network_manager in cloud.py --- nova/endpoint/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 622b4e2a4..0eedd9fec 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -613,7 +613,7 @@ class CloudController(object): # NOTE(vish): Currently, nothing needs to be done on the # network node until release. If this changes, # we will need to cast here. - self.network.deallocate_fixed_ip(context, address) + self.network_manager.deallocate_fixed_ip(context, address) host = instance_ref['host'] if host: -- cgit From 5b9908ff2601adfac3565ff900ef254df27102b9 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 06:29:13 -0700 Subject: fixed reversed admin logic on describe instances --- nova/endpoint/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 0eedd9fec..a25598dc8 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -366,7 +366,7 @@ class CloudController(object): instances = db.instance_get_by_reservation(context, reservation_id) else: - if not context.user.is_admin(): + if context.user.is_admin(): instances = db.instance_get_all(context) else: instances = db.instance_get_by_project(context, -- cgit From c000a1f88141c7887943a96a8a7ced3b79d70f7e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 08:43:48 -0700 Subject: added terminated_at to volume and moved setting of terminated_at into cloud --- nova/compute/manager.py | 8 ++++---- nova/db/sqlalchemy/models.py | 2 ++ nova/endpoint/cloud.py | 9 +++++++++ nova/volume/manager.py | 3 +++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index ae7099812..954227b42 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -85,7 +85,9 @@ class ComputeManager(manager.Manager): try: yield self.driver.spawn(instance_ref) now = datetime.datetime.utcnow() - self.db.instance_update(None, instance_id, {'launched_at': now}) + self.db.instance_update(context, + instance_id, + {'launched_at': now}) except Exception: # pylint: disable-msg=W0702 logging.exception("instance %s: Failed to spawn", instance_ref['name']) @@ -100,8 +102,8 @@ class ComputeManager(manager.Manager): def terminate_instance(self, context, instance_id): """Terminate an instance on this machine.""" logging.debug("instance %s: terminating", instance_id) - instance_ref = self.db.instance_get(context, instance_id) + instance_ref = self.db.instance_get(context, instance_id) if instance_ref['state'] == power_state.SHUTOFF: self.db.instance_destroy(context, instance_id) raise exception.Error('trying to destroy already destroyed' @@ -112,8 +114,6 @@ class ComputeManager(manager.Manager): power_state.NOSTATE, 'shutting_down') yield self.driver.destroy(instance_ref) - now = datetime.datetime.utcnow() - self.db.instance_update(None, instance_id, {'terminated_at': now}) # TODO(ja): should we keep it in a terminated state for a bit? self.db.instance_destroy(context, instance_id) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 09bd4b4dc..fde153dc4 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -274,6 +274,8 @@ class Volume(BASE, NovaBase): attach_status = Column(String(255)) # TODO(vish): enum scheduled_at = Column(DateTime) + launched_at = Column(DateTime) + terminated_at = Column(DateTime) class ExportDevice(BASE, NovaBase): """Represates a shelf and blade that a volume can be exported on""" diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 4b82e6d4d..faa646b53 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -23,6 +23,7 @@ datastore. """ import base64 +import datetime import logging import os import time @@ -594,6 +595,10 @@ class CloudController(object): % id_str) continue + now = datetime.datetime.utcnow() + self.db.instance_update(context, + instance_ref['id'], + {'terminated_at': now}) # FIXME(ja): where should network deallocate occur? address = db.instance_get_floating_address(context, instance_ref['id']) @@ -643,6 +648,10 @@ class CloudController(object): def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized volume_ref = db.volume_get_by_str(context, volume_id) + now = datetime.datetime.utcnow() + self.db.volume_update(context, + volume_ref['id'], + {'terminated_at': now}) host = volume_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.volume_topic, host), {"method": "delete_volume", diff --git a/nova/volume/manager.py b/nova/volume/manager.py index a6f4a6baf..7ca03b319 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -22,6 +22,7 @@ destroying persistent storage volumes, ala EBS. """ import logging +import datetime from twisted.internet import defer @@ -97,6 +98,8 @@ class AOEManager(manager.Manager): logging.debug("volume %s: re-exporting all values", volume_id) yield self.driver.ensure_exports() + now = datetime.datetime.utcnow() + self.db.volume_update(context, volume_id, {'launched_at': now}) logging.debug("volume %s: created successfully", volume_id) defer.returnValue(volume_id) -- cgit From 023c7c018cfad28d0f53a73fa7d211427ad8339b Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 17:12:43 -0700 Subject: db not self.db --- nova/endpoint/cloud.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index faa646b53..9d8e45f30 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -596,9 +596,9 @@ class CloudController(object): continue now = datetime.datetime.utcnow() - self.db.instance_update(context, - instance_ref['id'], - {'terminated_at': now}) + db.instance_update(context, + instance_ref['id'], + {'terminated_at': now}) # FIXME(ja): where should network deallocate occur? address = db.instance_get_floating_address(context, instance_ref['id']) @@ -649,9 +649,7 @@ class CloudController(object): # TODO: return error if not authorized volume_ref = db.volume_get_by_str(context, volume_id) now = datetime.datetime.utcnow() - self.db.volume_update(context, - volume_ref['id'], - {'terminated_at': now}) + db.volume_update(context, volume_ref['id'], {'terminated_at': now}) host = volume_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.volume_topic, host), {"method": "delete_volume", -- cgit From 06a799d2668723bbaead7ca2afbfb4b0cbf28abb Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 18:16:10 -0700 Subject: use a string version of key name when constructing mpi dict because None doesn't work well in lookup --- nova/endpoint/cloud.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 622b4e2a4..45291ca34 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -88,10 +88,11 @@ class CloudController(object): if instance['fixed_ip']: line = '%s slots=%d' % (instance['fixed_ip']['str_id'], INSTANCE_TYPES[instance['instance_type']]['vcpus']) - if instance['key_name'] in result: - result[instance['key_name']].append(line) + key = str(instance['key_name']) + if key in result: + result[key].append(line) else: - result[instance['key_name']] = [line] + result[key] = [line] return result def get_metadata(self, address): -- cgit From 8e4f102819a1424a25f89ed34040b1298ed9563a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 18:45:15 -0700 Subject: use gigabytes and cores --- nova/db/sqlalchemy/api.py | 8 ++++---- nova/scheduler/simple.py | 26 +++++++++++++------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 735e88145..8ca0f790b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -85,9 +85,9 @@ def service_get_all_compute_sorted(context): # FROM instances GROUP BY host) AS inst_count # ON services.host = inst_count.host topic = 'compute' - label = 'instance_count' + label = 'instance_cores' subq = session.query(models.Instance.host, - func.count('*').label(label) + func.sum('cores').label(label) ).filter_by(deleted=False ).group_by(models.Instance.host ).subquery() @@ -119,9 +119,9 @@ def service_get_all_volume_sorted(context): session = get_session() with session.begin(): topic = 'volume' - label = 'volume_count' + label = 'volume_gigabytes' subq = session.query(models.Volume.host, - func.count('*').label(label) + func.count('size').label(label) ).filter_by(deleted=False ).group_by(models.Volume.host ).subquery() diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py index 48be4c1a6..6e77debf3 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -27,10 +27,10 @@ from nova.scheduler import driver from nova.scheduler import chance FLAGS = flags.FLAGS -flags.DEFINE_integer("max_instances", 16, - "maximum number of instances to allow per host") -flags.DEFINE_integer("max_volumes", 100, - "maximum number of volumes to allow per host") +flags.DEFINE_integer("max_cores", 16, + "maximum number of instance cores to allow per host") +flags.DEFINE_integer("max_gigabytes", 10000, + "maximum number of volume gigabytes to allow per host") flags.DEFINE_integer("max_networks", 1000, "maximum number of networks to allow per host") @@ -42,9 +42,9 @@ class SimpleScheduler(chance.ChanceScheduler): results = db.service_get_all_compute_sorted(context) for result in results: - (service, instance_count) = result - if instance_count >= FLAGS.max_instances: - raise driver.NoValidHost("All hosts have too many instances") + (service, instance_cores) = result + if instance_cores >= FLAGS.max_cores: + raise driver.NoValidHost("All hosts have too many cores") if self.service_is_up(service): db.instance_update(context, instance_id, @@ -57,13 +57,13 @@ class SimpleScheduler(chance.ChanceScheduler): results = db.service_get_all_volume_sorted(context) for result in results: - (service, instance_count) = result - if instance_count >= FLAGS.max_volumes: - raise driver.NoValidHost("All hosts have too many volumes") + (service, volume_gigabytes) = result + if volume_gigabytes >= FLAGS.max_gigabytes: + raise driver.NoValidHost("All hosts have too many gigabytes") if self.service_is_up(service): - db.instance_update(context, - volume_id, - {'host': service['host']}) + db.volume_update(context, + volume_id, + {'host': service['host']}) return service['host'] raise driver.NoValidHost("No hosts found") -- cgit From 68ff059c7a6287825871f96cde8039f04aec1f37 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 18:57:15 -0700 Subject: update query and test --- nova/db/sqlalchemy/api.py | 12 ++++++------ nova/scheduler/simple.py | 1 + nova/tests/scheduler_unittest.py | 10 +++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 574a6f460..75131e093 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -79,15 +79,15 @@ def service_get_all_compute_sorted(context): session = get_session() with session.begin(): # NOTE(vish): The intended query is below - # SELECT services.*, inst_count.instance_count + # SELECT services.*, inst_cores.instance_cores # FROM services LEFT OUTER JOIN - # (SELECT host, count(*) AS instance_count - # FROM instances GROUP BY host) AS inst_count - # ON services.host = inst_count.host + # (SELECT host, sum(instances.vcpus) AS instance_cores + # FROM instances GROUP BY host) AS inst_cores + # ON services.host = inst_cores.host topic = 'compute' label = 'instance_cores' subq = session.query(models.Instance.host, - func.sum('cores').label(label) + func.sum(models.Instance.vcpus).label(label) ).filter_by(deleted=False ).group_by(models.Instance.host ).subquery() @@ -121,7 +121,7 @@ def service_get_all_volume_sorted(context): topic = 'volume' label = 'volume_gigabytes' subq = session.query(models.Volume.host, - func.count('size').label(label) + func.sum(models.Volume.size).label(label) ).filter_by(deleted=False ).group_by(models.Volume.host ).subquery() diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py index 6e77debf3..3feeca846 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -43,6 +43,7 @@ class SimpleScheduler(chance.ChanceScheduler): results = db.service_get_all_compute_sorted(context) for result in results: (service, instance_cores) = result + print service, instance_cores if instance_cores >= FLAGS.max_cores: raise driver.NoValidHost("All hosts have too many cores") if self.service_is_up(service): diff --git a/nova/tests/scheduler_unittest.py b/nova/tests/scheduler_unittest.py index 27e100fa0..b9371e86d 100644 --- a/nova/tests/scheduler_unittest.py +++ b/nova/tests/scheduler_unittest.py @@ -33,7 +33,7 @@ from nova.scheduler import driver FLAGS = flags.FLAGS -flags.DECLARE('max_instances', 'nova.scheduler.simple') +flags.DECLARE('max_cores', 'nova.scheduler.simple') class TestDriver(driver.Scheduler): """Scheduler Driver for Tests""" @@ -75,7 +75,7 @@ class SimpleDriverTestCase(test.TrialTestCase): def setUp(self): # pylint: disable-msg=C0103 super(SimpleDriverTestCase, self).setUp() self.flags(connection_type='fake', - max_instances=4, + max_cores=4, scheduler_driver='nova.scheduler.simple.SimpleScheduler') self.scheduler = manager.SchedulerManager() self.context = None @@ -109,6 +109,7 @@ class SimpleDriverTestCase(test.TrialTestCase): inst['instance_type'] = 'm1.tiny' inst['mac_address'] = utils.generate_mac() inst['ami_launch_index'] = 0 + inst['vcpus'] = 1 return db.instance_create(self.context, inst)['id'] def test_hosts_are_up(self): @@ -125,10 +126,10 @@ class SimpleDriverTestCase(test.TrialTestCase): self.assertEqual(host, 'host2') self.service1.terminate_instance(self.context, instance_id) - def test_too_many_instances(self): + def test_too_many_cores(self): instance_ids1 = [] instance_ids2 = [] - for index in xrange(FLAGS.max_instances): + for index in xrange(FLAGS.max_cores): instance_id = self._create_instance() self.service1.run_instance(self.context, instance_id) instance_ids1.append(instance_id) @@ -139,7 +140,6 @@ class SimpleDriverTestCase(test.TrialTestCase): self.assertRaises(driver.NoValidHost, self.scheduler.driver.schedule_run_instance, self.context, - 'compute', instance_id) for instance_id in instance_ids1: self.service1.terminate_instance(self.context, instance_id) -- cgit From d05fe5d18ba3a62a1792634e7ba3c2f11d7b89bd Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 19:40:38 -0700 Subject: tests for volumes work --- nova/db/sqlalchemy/api.py | 9 +-- nova/scheduler/simple.py | 9 ++- nova/tests/scheduler_unittest.py | 126 ++++++++++++++++++++++++++++++++------- 3 files changed, 114 insertions(+), 30 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 75131e093..d612fe669 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -67,7 +67,7 @@ def service_get_all_by_topic(context, topic): def _service_get_all_topic_subquery(_context, session, topic, subq, label): sort_value = getattr(subq.c, label) - return session.query(models.Service, sort_value + return session.query(models.Service, func.coalesce(sort_value, 0) ).filter_by(topic=topic ).filter_by(deleted=False ).outerjoin((subq, models.Service.host == subq.c.host) @@ -79,9 +79,10 @@ def service_get_all_compute_sorted(context): session = get_session() with session.begin(): # NOTE(vish): The intended query is below - # SELECT services.*, inst_cores.instance_cores + # SELECT services.*, COALESCE(inst_cores.instance_cores, + # 0) # FROM services LEFT OUTER JOIN - # (SELECT host, sum(instances.vcpus) AS instance_cores + # (SELECT host, SUM(instances.vcpus) AS instance_cores # FROM instances GROUP BY host) AS inst_cores # ON services.host = inst_cores.host topic = 'compute' @@ -104,7 +105,7 @@ def service_get_all_network_sorted(context): topic = 'network' label = 'network_count' subq = session.query(models.Network.host, - func.count('*').label(label) + func.count(models.Network.id).label(label) ).filter_by(deleted=False ).group_by(models.Network.host ).subquery() diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py index 3feeca846..c4ba17caf 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -39,12 +39,11 @@ class SimpleScheduler(chance.ChanceScheduler): def schedule_run_instance(self, context, instance_id, *_args, **_kwargs): """Picks a host that is up and has the fewest running instances.""" - + instance_ref = db.instance_get(context, instance_id) results = db.service_get_all_compute_sorted(context) for result in results: (service, instance_cores) = result - print service, instance_cores - if instance_cores >= FLAGS.max_cores: + if instance_cores + instance_ref['vcpus'] > FLAGS.max_cores: raise driver.NoValidHost("All hosts have too many cores") if self.service_is_up(service): db.instance_update(context, @@ -55,11 +54,11 @@ class SimpleScheduler(chance.ChanceScheduler): def schedule_create_volume(self, context, volume_id, *_args, **_kwargs): """Picks a host that is up and has the fewest volumes.""" - + volume_ref = db.volume_get(context, volume_id) results = db.service_get_all_volume_sorted(context) for result in results: (service, volume_gigabytes) = result - if volume_gigabytes >= FLAGS.max_gigabytes: + if volume_gigabytes + volume_ref['size'] > FLAGS.max_gigabytes: raise driver.NoValidHost("All hosts have too many gigabytes") if self.service_is_up(service): db.volume_update(context, diff --git a/nova/tests/scheduler_unittest.py b/nova/tests/scheduler_unittest.py index b9371e86d..fde30f81e 100644 --- a/nova/tests/scheduler_unittest.py +++ b/nova/tests/scheduler_unittest.py @@ -19,8 +19,6 @@ Tests For Scheduler """ -import mox - from nova import db from nova import flags from nova import service @@ -76,6 +74,8 @@ class SimpleDriverTestCase(test.TrialTestCase): super(SimpleDriverTestCase, self).setUp() self.flags(connection_type='fake', max_cores=4, + max_gigabytes=4, + volume_driver='nova.volume.driver.FakeAOEDriver', scheduler_driver='nova.scheduler.simple.SimpleScheduler') self.scheduler = manager.SchedulerManager() self.context = None @@ -83,27 +83,16 @@ class SimpleDriverTestCase(test.TrialTestCase): self.user = self.manager.create_user('fake', 'fake', 'fake') self.project = self.manager.create_project('fake', 'fake', 'fake') self.context = None - self.service1 = service.Service('host1', - 'nova-compute', - 'compute', - FLAGS.compute_manager) - self.service2 = service.Service('host2', - 'nova-compute', - 'compute', - FLAGS.compute_manager) def tearDown(self): # pylint: disable-msg=C0103 self.manager.delete_user(self.user) self.manager.delete_project(self.project) - self.service1.kill() - self.service2.kill() def _create_instance(self): """Create a test instance""" inst = {} inst['image_id'] = 'ami-test' inst['reservation_id'] = 'r-fakeres' - inst['launch_time'] = '10' inst['user_id'] = self.user.id inst['project_id'] = self.project.id inst['instance_type'] = 'm1.tiny' @@ -112,29 +101,70 @@ class SimpleDriverTestCase(test.TrialTestCase): inst['vcpus'] = 1 return db.instance_create(self.context, inst)['id'] + def _create_volume(self): + """Create a test volume""" + vol = {} + vol['image_id'] = 'ami-test' + vol['reservation_id'] = 'r-fakeres' + vol['size'] = 1 + return db.volume_create(self.context, vol)['id'] + def test_hosts_are_up(self): + """Ensures driver can find the hosts that are up""" # NOTE(vish): constructing service without create method # because we are going to use it without queue + compute1 = service.Service('host1', + 'nova-compute', + 'compute', + FLAGS.compute_manager) + compute2 = service.Service('host2', + 'nova-compute', + 'compute', + FLAGS.compute_manager) hosts = self.scheduler.driver.hosts_up(self.context, 'compute') self.assertEqual(len(hosts), 2) + compute1.kill() + compute2.kill() def test_least_busy_host_gets_instance(self): - instance_id = self._create_instance() - self.service1.run_instance(self.context, instance_id) + """Ensures the host with less cores gets the next one""" + compute1 = service.Service('host1', + 'nova-compute', + 'compute', + FLAGS.compute_manager) + compute2 = service.Service('host2', + 'nova-compute', + 'compute', + FLAGS.compute_manager) + instance_id1 = self._create_instance() + compute1.run_instance(self.context, instance_id1) + instance_id2 = self._create_instance() host = self.scheduler.driver.schedule_run_instance(self.context, - instance_id) + instance_id2) self.assertEqual(host, 'host2') - self.service1.terminate_instance(self.context, instance_id) + compute1.terminate_instance(self.context, instance_id1) + db.instance_destroy(self.context, instance_id2) + compute1.kill() + compute2.kill() def test_too_many_cores(self): + """Ensures we don't go over max cores""" + compute1 = service.Service('host1', + 'nova-compute', + 'compute', + FLAGS.compute_manager) + compute2 = service.Service('host2', + 'nova-compute', + 'compute', + FLAGS.compute_manager) instance_ids1 = [] instance_ids2 = [] for index in xrange(FLAGS.max_cores): instance_id = self._create_instance() - self.service1.run_instance(self.context, instance_id) + compute1.run_instance(self.context, instance_id) instance_ids1.append(instance_id) instance_id = self._create_instance() - self.service2.run_instance(self.context, instance_id) + compute2.run_instance(self.context, instance_id) instance_ids2.append(instance_id) instance_id = self._create_instance() self.assertRaises(driver.NoValidHost, @@ -142,6 +172,60 @@ class SimpleDriverTestCase(test.TrialTestCase): self.context, instance_id) for instance_id in instance_ids1: - self.service1.terminate_instance(self.context, instance_id) + compute1.terminate_instance(self.context, instance_id) for instance_id in instance_ids2: - self.service2.terminate_instance(self.context, instance_id) + compute2.terminate_instance(self.context, instance_id) + compute1.kill() + compute2.kill() + + def test_least_busy_host_gets_volume(self): + """Ensures the host with less gigabytes gets the next one""" + volume1 = service.Service('host1', + 'nova-volume', + 'volume', + FLAGS.volume_manager) + volume2 = service.Service('host2', + 'nova-volume', + 'volume', + FLAGS.volume_manager) + volume_id1 = self._create_volume() + volume1.create_volume(self.context, volume_id1) + volume_id2 = self._create_volume() + host = self.scheduler.driver.schedule_create_volume(self.context, + volume_id2) + self.assertEqual(host, 'host2') + volume1.delete_volume(self.context, volume_id1) + db.volume_destroy(self.context, volume_id2) + volume1.kill() + volume2.kill() + + def test_too_many_gigabytes(self): + """Ensures we don't go over max gigabytes""" + volume1 = service.Service('host1', + 'nova-volume', + 'volume', + FLAGS.volume_manager) + volume2 = service.Service('host2', + 'nova-volume', + 'volume', + FLAGS.volume_manager) + volume_ids1 = [] + volume_ids2 = [] + for index in xrange(FLAGS.max_gigabytes): + volume_id = self._create_volume() + volume1.create_volume(self.context, volume_id) + volume_ids1.append(volume_id) + volume_id = self._create_volume() + volume2.create_volume(self.context, volume_id) + volume_ids2.append(volume_id) + volume_id = self._create_volume() + self.assertRaises(driver.NoValidHost, + self.scheduler.driver.schedule_create_volume, + self.context, + volume_id) + for volume_id in volume_ids1: + volume1.delete_volume(self.context, volume_id) + for volume_id in volume_ids2: + volume2.delete_volume(self.context, volume_id) + volume1.kill() + volume2.kill() -- cgit From 15ca1fe1670cfd95880f2e1c2a5270be787c6035 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 19:43:02 -0700 Subject: move volume to the scheduler --- nova/endpoint/cloud.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 4fda484e3..584c9c643 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -300,9 +300,11 @@ class CloudController(object): vol['attach_status'] = "detached" volume_ref = db.volume_create(context, vol) - rpc.cast(FLAGS.volume_topic, {"method": "create_volume", - "args": {"context": None, - "volume_id": volume_ref['id']}}) + rpc.cast(FLAGS.scheduler_topic, + {"method": "create_volume", + "args": {"context": None, + "topic": FLAGS.volume_topic, + "volume_id": volume_ref['id']}}) return {'volumeSet': [self._format_volume(context, volume_ref)]} -- cgit From ef1913292dd8a88041f603d79c09c738a7ecbb04 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 20:00:02 -0700 Subject: fix instance time --- nova/db/sqlalchemy/models.py | 2 +- nova/tests/compute_unittest.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index fde153dc4..c16f684fe 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -103,7 +103,7 @@ class NovaBase(object): def delete(self, session=None): """Delete this object""" self.deleted = True - self.deleted_at = datetime.datetime.now() + self.deleted_at = datetime.datetime.utcnow() self.save(session=session) def __setitem__(self, key, value): diff --git a/nova/tests/compute_unittest.py b/nova/tests/compute_unittest.py index de2bf3d3b..c983e05c9 100644 --- a/nova/tests/compute_unittest.py +++ b/nova/tests/compute_unittest.py @@ -83,21 +83,21 @@ class ComputeTestCase(test.TrialTestCase): @defer.inlineCallbacks def test_run_terminate_timestamps(self): - """Make sure it is possible to run and terminate instance""" + """Make sure timestamps are set for launched and destroyed""" instance_id = self._create_instance() instance_ref = db.instance_get(self.context, instance_id) self.assertEqual(instance_ref['launched_at'], None) - self.assertEqual(instance_ref['terminated_at'], None) + self.assertEqual(instance_ref['deleted_at'], None) launch = datetime.datetime.utcnow() yield self.compute.run_instance(self.context, instance_id) instance_ref = db.instance_get(self.context, instance_id) self.assert_(instance_ref['launched_at'] > launch) - self.assertEqual(instance_ref['terminated_at'], None) + self.assertEqual(instance_ref['deleted_at'], None) terminate = datetime.datetime.utcnow() yield self.compute.terminate_instance(self.context, instance_id) instance_ref = db.instance_get({'deleted': True}, instance_id) self.assert_(instance_ref['launched_at'] < terminate) - self.assert_(instance_ref['terminated_at'] > terminate) + self.assert_(instance_ref['deleted_at'] > terminate) @defer.inlineCallbacks def test_reboot(self): -- cgit From c7921fd14e680288c5626294105761005684b343 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 22:48:59 -0700 Subject: don't allow deletion or attachment of volume unless it is available --- nova/endpoint/cloud.py | 8 ++++++-- nova/volume/manager.py | 7 ++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 94a04fb1b..6e2fedd69 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -314,6 +314,8 @@ class CloudController(object): def attach_volume(self, context, volume_id, instance_id, device, **kwargs): volume_ref = db.volume_get_by_str(context, volume_id) # TODO(vish): abstract status checking? + if volume_ref['status'] != "available": + raise exception.ApiError("Volume status must be available") if volume_ref['attach_status'] == "attached": raise exception.ApiError("Volume is already attached") instance_ref = db.instance_get_by_str(context, instance_id) @@ -336,10 +338,10 @@ class CloudController(object): volume_ref = db.volume_get_by_str(context, volume_id) instance_ref = db.volume_get_instance(context, volume_ref['id']) if not instance_ref: - raise exception.Error("Volume isn't attached to anything!") + raise exception.ApiError("Volume isn't attached to anything!") # TODO(vish): abstract status checking? if volume_ref['status'] == "available": - raise exception.Error("Volume is already detached") + raise exception.ApiError("Volume is already detached") try: host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), @@ -691,6 +693,8 @@ class CloudController(object): def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized volume_ref = db.volume_get_by_str(context, volume_id) + if volume_ref['status'] != "available": + raise exception.ApiError("Volume status must be available") now = datetime.datetime.utcnow() db.volume_update(context, volume_ref['id'], {'terminated_at': now}) host = volume_ref['host'] diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 7ca03b319..034763512 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -90,16 +90,13 @@ class AOEManager(manager.Manager): yield self.driver.create_export(volume_ref['str_id'], shelf_id, blade_id) - # TODO(joshua): We need to trigger a fanout message - # for aoe-discover on all the nodes - - self.db.volume_update(context, volume_id, {'status': 'available'}) logging.debug("volume %s: re-exporting all values", volume_id) yield self.driver.ensure_exports() now = datetime.datetime.utcnow() - self.db.volume_update(context, volume_id, {'launched_at': now}) + self.db.volume_update(context, volume_id, {'status': 'available', + 'launched_at': now}) logging.debug("volume %s: created successfully", volume_id) defer.returnValue(volume_id) -- cgit From 6cbf8b736cc2c9929c2ad69ddc8e8b4fc2d0f4ae Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 12 Sep 2010 23:09:15 -0700 Subject: removed second copy of ProcessExecutionError --- nova/utils.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/nova/utils.py b/nova/utils.py index 8939043e6..d18dd9843 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -39,17 +39,6 @@ from nova.exception import ProcessExecutionError FLAGS = flags.FLAGS TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" -class ProcessExecutionError(IOError): - def __init__( self, stdout=None, stderr=None, exit_code=None, cmd=None, - description=None): - if description is None: - description = "Unexpected error while running command." - if exit_code is None: - exit_code = '-' - message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( - description, cmd, exit_code, stdout, stderr) - IOError.__init__(self, message) - def import_class(import_str): """Returns a class from a string including module and class""" mod_str, _sep, class_str = import_str.rpartition('.') -- cgit From cb13f09d7fe886bc8340770ff8c7011b6dbab0db Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 13 Sep 2010 09:03:14 +0200 Subject: Move vol.destroy() call out of the _check method in test_multiple_volume_race_condition test and into a callback of the DeferredList. This should fix the intermittent failure of that test. I /think/ test_too_many_volumes's failure was caused by test_multiple_volume_race_condition failure, since I have not been able to reproduce its failure after fixing this one. --- nova/tests/volume_unittest.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/nova/tests/volume_unittest.py b/nova/tests/volume_unittest.py index 2a07afe69..540b71585 100644 --- a/nova/tests/volume_unittest.py +++ b/nova/tests/volume_unittest.py @@ -128,7 +128,6 @@ class VolumeTestCase(test.TrialTestCase): volume_service.get_volume, volume_id) - @defer.inlineCallbacks def test_multiple_volume_race_condition(self): vol_size = "5" user_id = "fake" @@ -137,17 +136,28 @@ class VolumeTestCase(test.TrialTestCase): def _check(volume_id): vol = volume_service.get_volume(volume_id) shelf_blade = '%s.%s' % (vol['shelf_id'], vol['blade_id']) - self.assert_(shelf_blade not in shelf_blades) + self.assertTrue(shelf_blade not in shelf_blades, + "Same shelf/blade tuple came back twice") shelf_blades.append(shelf_blade) logging.debug("got %s" % shelf_blade) - vol.destroy() + return vol deferreds = [] for i in range(5): d = self.volume.create_volume(vol_size, user_id, project_id) d.addCallback(_check) d.addErrback(self.fail) deferreds.append(d) - yield defer.DeferredList(deferreds) + def destroy_volumes(retvals): + overall_succes = True + for success, volume in retvals: + if not success: + overall_succes = False + else: + volume.destroy() + self.assertTrue(overall_succes) + d = defer.DeferredList(deferreds) + d.addCallback(destroy_volumes) + return d def test_multi_node(self): # TODO(termie): Figure out how to test with two nodes, -- cgit From c35f0961a030ebefb19c0fbf4a666a0d6ce6be4c Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Mon, 13 Sep 2010 00:06:32 -0700 Subject: add a shell to nova-manage, which respects flags (taken from django) --- bin/nova-manage | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/bin/nova-manage b/bin/nova-manage index d2fd49d8d..6e5266767 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -17,6 +17,37 @@ # License for the specific language governing permissions and limitations # under the License. +# Interactive shell based on Django: +# +# Copyright (c) 2005, the Lawrence Journal-World +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of Django nor the names of its contributors may be used +# to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + """ CLI interface for nova management. Connects to the running ADMIN api in the api daemon. @@ -103,6 +134,29 @@ class VpnCommands(object): self.pipe.launch_vpn_instance(project_id) +class ShellCommands(object): + def run(self): + "Runs a Python interactive interpreter. Tries to use IPython, if it's available." + try: + import IPython + # Explicitly pass an empty list as arguments, because otherwise IPython + # would use sys.argv from this script. + shell = IPython.Shell.IPShell(argv=[]) + shell.mainloop() + except ImportError: + import code + try: # Try activating rlcompleter, because it's handy. + import readline + except ImportError: + pass + else: + # We don't have to wrap the following import in a 'try', because + # we already know 'readline' was imported successfully. + import rlcompleter + readline.parse_and_bind("tab:complete") + code.interact() + + class RoleCommands(object): """Class for managing roles.""" @@ -225,6 +279,7 @@ CATEGORIES = [ ('user', UserCommands), ('project', ProjectCommands), ('role', RoleCommands), + ('shell', ShellCommands), ('vpn', VpnCommands), ] -- cgit From 2774466197a0dda3763569fe7aa1a578baf5e059 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 13 Sep 2010 02:15:02 -0700 Subject: added missing yield in detach_volume --- nova/compute/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 954227b42..24538e4f1 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -189,7 +189,7 @@ class ComputeManager(manager.Manager): volume_id) instance_ref = self.db.instance_get(context, instance_id) volume_ref = self.db.volume_get(context, volume_id) - self.driver.detach_volume(instance_ref['str_id'], - volume_ref['mountpoint']) + yield self.driver.detach_volume(instance_ref['str_id'], + volume_ref['mountpoint']) self.db.volume_detached(context, volume_id) defer.returnValue(True) -- cgit From 3d68f1f74cd7fe6ddb9eec003a9e31f8ad036b27 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 14 Sep 2010 16:26:19 -0400 Subject: Add ratelimiting package into Nova. After Austin it'll be pulled out into PyPI. --- nova/api/rackspace/ratelimiting/__init__.py | 103 ++++++++++++++++++++++++++++ nova/api/rackspace/ratelimiting/tests.py | 60 ++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 nova/api/rackspace/ratelimiting/__init__.py create mode 100644 nova/api/rackspace/ratelimiting/tests.py diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py new file mode 100644 index 000000000..176e7d66e --- /dev/null +++ b/nova/api/rackspace/ratelimiting/__init__.py @@ -0,0 +1,103 @@ +"""Rate limiting of arbitrary actions.""" + +import time +import urllib +import webob.dec +import webob.exc + + +# Convenience constants for the limits dictionary passed to Limiter(). +PER_SECOND = 1 +PER_MINUTE = 60 +PER_HOUR = 60 * 60 +PER_DAY = 60 * 60 * 24 + +class Limiter(object): + + """Class providing rate limiting of arbitrary actions.""" + + def __init__(self, limits): + """Create a rate limiter. + + limits: a dict mapping from action name to a tuple. The tuple contains + the number of times the action may be performed, and the time period + (in seconds) during which the number must not be exceeded for this + action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would + allow 10 'reboot' actions per minute. + """ + self.limits = limits + self._levels = {} + + def perform(self, action_name, username='nobody'): + """Attempt to perform an action by the given username. + + action_name: the string name of the action to perform. This must + be a key in the limits dict passed to the ctor. + + username: an optional string name of the user performing the action. + Each user has her own set of rate limiting counters. Defaults to + 'nobody' (so that if you never specify a username when calling + perform(), a single set of counters will be used.) + + Return None if the action may proceed. If the action may not proceed + because it has been rate limited, return the float number of seconds + until the action would succeed. + """ + # Think of rate limiting as a bucket leaking water at 1cc/second. The + # bucket can hold as many ccs as there are seconds in the rate + # limiting period (e.g. 3600 for per-hour ratelimits), and if you can + # perform N actions in that time, each action fills the bucket by + # 1/Nth of its volume. You may only perform an action if the bucket + # would not overflow. + now = time.time() + key = '%s:%s' % (username, action_name) + last_time_performed, water_level = self._levels.get(key, (now, 0)) + # The bucket leaks 1cc/second. + water_level -= (now - last_time_performed) + if water_level < 0: + water_level = 0 + num_allowed_per_period, period_in_secs = self.limits[action_name] + # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. + capacity = period_in_secs + new_level = water_level + (capacity * 1.0 / num_allowed_per_period) + if new_level > capacity: + # Delay this many seconds. + return new_level - capacity + self._levels[key] = (now, new_level) + return None + + +# If one instance of this WSGIApps is unable to handle your load, put a +# sharding app in front that shards by username to one of many backends. + +class WSGIApp(object): + + """Application that tracks rate limits in memory. Send requests to it of + this form: + + POST /limiter// + + and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header + containing the number of seconds to wait before the action would succeed. + """ + + def __init__(self, limiter): + """Create the WSGI application using the given Limiter instance.""" + self.limiter = limiter + + @webob.dec.wsgify + def __call__(req): + parts = req.path_info.split('/') + # format: /limiter// + if req.method != 'POST': + raise webob.exc.HTTPMethodNotAllowed() + if len(parts) != 4 or parts[1] != 'limiter': + raise webob.exc.HTTPNotFound() + username = parts[2] + action_name = urllib.unquote(parts[3]) + delay = self.limiter.perform(action_name, username) + if delay: + return webob.exc.HTTPForbidden( + headers={'X-Wait-Seconds': delay}) + else: + return '' # 200 OK diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py new file mode 100644 index 000000000..1983cdea8 --- /dev/null +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -0,0 +1,60 @@ +import ratelimiting +import time +import unittest + +class Test(unittest.TestCase): + + def setUp(self): + self.limits = { + 'a': (5, ratelimiting.PER_SECOND), + 'b': (5, ratelimiting.PER_MINUTE), + 'c': (5, ratelimiting.PER_HOUR), + 'd': (1, ratelimiting.PER_SECOND), + 'e': (100, ratelimiting.PER_SECOND)} + self.rl = ratelimiting.Limiter(self.limits) + + def exhaust(self, action, times_until_exhausted, **kwargs): + for i in range(times_until_exhausted): + when = self.rl.perform(action, **kwargs) + self.assertEqual(when, None) + num, period = self.limits[action] + delay = period * 1.0 / num + # Verify that we are now thoroughly delayed + for i in range(10): + when = self.rl.perform(action, **kwargs) + self.assertAlmostEqual(when, delay, 2) + + def test_second(self): + self.exhaust('a', 5) + time.sleep(0.2) + self.exhaust('a', 1) + time.sleep(1) + self.exhaust('a', 5) + + def test_minute(self): + self.exhaust('b', 5) + + def test_one_per_period(self): + def allow_once_and_deny_once(): + when = self.rl.perform('d') + self.assertEqual(when, None) + when = self.rl.perform('d') + self.assertAlmostEqual(when, 1, 2) + return when + time.sleep(allow_once_and_deny_once()) + time.sleep(allow_once_and_deny_once()) + allow_once_and_deny_once() + + def test_we_can_go_indefinitely_if_we_spread_out_requests(self): + for i in range(200): + when = self.rl.perform('e') + self.assertEqual(when, None) + time.sleep(0.01) + + def test_users_get_separate_buckets(self): + self.exhaust('c', 5, username='alice') + self.exhaust('c', 5, username='bob') + self.exhaust('c', 5, username='chuck') + self.exhaust('c', 0, username='chuck') + self.exhaust('c', 0, username='bob') + self.exhaust('c', 0, username='alice') -- cgit From 8138a35d3672e08640762b7533c1c527568d0b4f Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 14 Sep 2010 18:59:02 -0400 Subject: RateLimitingMiddleware --- nova/api/rackspace/__init__.py | 52 ++++++++++++++++++++++++++++++++- nova/tests/api/rackspace/__init__.py | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index b4d666d63..e35109b43 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -31,6 +31,7 @@ from nova import flags from nova import wsgi from nova.api.rackspace import flavors from nova.api.rackspace import images +from nova.api.rackspace import ratelimiting from nova.api.rackspace import servers from nova.api.rackspace import sharedipgroups from nova.auth import manager @@ -40,7 +41,7 @@ class API(wsgi.Middleware): """WSGI entry point for all Rackspace API requests.""" def __init__(self): - app = AuthMiddleware(APIRouter()) + app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) super(API, self).__init__(app) @@ -65,6 +66,55 @@ class AuthMiddleware(wsgi.Middleware): return self.application +class RateLimitingMiddleware(wsgi.Middleware): + """Rate limit incoming requests according to the OpenStack rate limits.""" + + def __init__(self, application): + super(RateLimitingMiddleware, self).__init__(application) + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = ratelimiting.Limiter(limits={ + 'DELETE': (100, ratelimiting.PER_MINUTE), + 'PUT': (10, ratelimiting.PER_MINUTE), + 'POST': (10, ratelimiting.PER_MINUTE), + 'POST servers': (50, ratelimiting.PER_DAY), + 'GET changes-since': (3, ratelimiting.PER_MINUTE), + }) + + @webob.dec.wsgify + def __call__(self, req): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + username = req.headers['X-Auth-User'] + action_name = self.get_action_name(req) + if not action_name: # not rate limited + return self.application + delay = self.limiter.perform(action_name, username=username) + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + delay2 = self.limiter.perform('POST', username=username) + delay = max(delay or 0, delay2 or 0) + if delay: + # TODO(gundlach): Get the retry-after format correct. + raise webob.exc.HTTPRequestEntityTooLarge(headers={ + 'Retry-After': time.time() + delay}) + else: + return self.application + + def get_action_name(self, req): + """Return the action name for this request.""" + if req.method == 'GET' and 'changes-since' in req.GET: + return 'GET changes-since' + if req.method == 'POST' and req.path_info.starts_with('/servers'): + return 'POST servers' + if req.method in ['PUT', 'POST', 'DELETE']: + return req.method + return None + + class APIRouter(wsgi.Router): """ Routes requests on the Rackspace API to the appropriate controller diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py index e69de29bb..f7537a4e7 100644 --- a/nova/tests/api/rackspace/__init__.py +++ b/nova/tests/api/rackspace/__init__.py @@ -0,0 +1,56 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 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 unittest + +from nova.api.rackspace.ratelimiting import RateLimitingMiddleware +from nova.tests.api.test_helper import * +from webob import Request + +class RateLimitingMiddlewareTest(unittest.TestCase): + def setUp(self): + self.middleware = RateLimitingMiddleware(APIStub()) + self.stubs = stubout.StubOutForTesting() + + def tearDown(self): + self.stubs.UnsetAll() + + def test_get_action_name(self): + middleware = RateLimitingMiddleware(APIStub()) + def verify(method, url, action_name): + req = Request(url) + req.method = method + 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) + + def TODO_test_call(self): + pass + #mw = make_middleware() + #req = build_request('DELETE', '/servers/4') + #for i in range(5): + # resp = req.get_response(mw) + # assert resp is OK + #resp = req.get_response(mw) + #assert resp is rate limited -- cgit From 63ad073efd0b20f59f02bc37182c0180cac3f405 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 09:25:53 -0400 Subject: RateLimitingMiddleware tests --- nova/api/rackspace/__init__.py | 24 ++++++++++----- nova/api/rackspace/ratelimiting/tests.py | 3 ++ nova/tests/api/rackspace/__init__.py | 51 +++++++++++++++++++++----------- 3 files changed, 52 insertions(+), 26 deletions(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index e35109b43..66d80a5b7 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -92,23 +92,31 @@ class RateLimitingMiddleware(wsgi.Middleware): action_name = self.get_action_name(req) if not action_name: # not rate limited return self.application - delay = self.limiter.perform(action_name, username=username) - if action_name == 'POST servers': - # "POST servers" is a POST, so it counts against "POST" too. - delay2 = self.limiter.perform('POST', username=username) - delay = max(delay or 0, delay2 or 0) + delay = self.get_delay(action_name, username) if delay: # TODO(gundlach): Get the retry-after format correct. raise webob.exc.HTTPRequestEntityTooLarge(headers={ 'Retry-After': time.time() + delay}) - else: - return self.application + return self.application + + def get_delay(self, action_name, username): + """Return the delay for the given action and username, or None if + the action would not be rate limited. + """ + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + # Attempt the "POST" first, lest we are rate limited by "POST" but + # use up a precious "POST servers" call. + delay = self.limiter.perform("POST", username=username) + if delay: + return delay + return self.limiter.perform(action_name, username=username) def get_action_name(self, req): """Return the action name for this request.""" if req.method == 'GET' and 'changes-since' in req.GET: return 'GET changes-since' - if req.method == 'POST' and req.path_info.starts_with('/servers'): + if req.method == 'POST' and req.path_info.startswith('/servers'): return 'POST servers' if req.method in ['PUT', 'POST', 'DELETE']: return req.method diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index 1983cdea8..545e1d1b6 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -58,3 +58,6 @@ class Test(unittest.TestCase): self.exhaust('c', 0, username='chuck') self.exhaust('c', 0, username='bob') self.exhaust('c', 0, username='alice') + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py index f7537a4e7..2fab1a4da 100644 --- a/nova/tests/api/rackspace/__init__.py +++ b/nova/tests/api/rackspace/__init__.py @@ -17,22 +17,15 @@ import unittest -from nova.api.rackspace.ratelimiting import RateLimitingMiddleware +from nova.api.rackspace import RateLimitingMiddleware from nova.tests.api.test_helper import * from webob import Request class RateLimitingMiddlewareTest(unittest.TestCase): - def setUp(self): - self.middleware = RateLimitingMiddleware(APIStub()) - self.stubs = stubout.StubOutForTesting() - - def tearDown(self): - self.stubs.UnsetAll() - def test_get_action_name(self): middleware = RateLimitingMiddleware(APIStub()) def verify(method, url, action_name): - req = Request(url) + req = Request.blank(url) req.method = method action = middleware.get_action_name(req) self.assertEqual(action, action_name) @@ -45,12 +38,34 @@ class RateLimitingMiddlewareTest(unittest.TestCase): verify('GET', '/servers/4', None) verify('HEAD', '/servers/4', None) - def TODO_test_call(self): - pass - #mw = make_middleware() - #req = build_request('DELETE', '/servers/4') - #for i in range(5): - # resp = req.get_response(mw) - # assert resp is OK - #resp = req.get_response(mw) - #assert resp is rate limited + def exhaust(self, middleware, method, url, username, times): + req = Request.blank(url, dict(REQUEST_METHOD=method), + headers={'X-Auth-User': username}) + for i in range(times): + resp = req.get_response(middleware) + self.assertEqual(resp.status_int, 200) + resp = req.get_response(middleware) + self.assertEqual(resp.status_int, 413) + self.assertTrue('Retry-After' in resp.headers) + + def test_single_action(self): + middleware = RateLimitingMiddleware(APIStub()) + self.exhaust(middleware, 'DELETE', '/servers/4', 'usr1', 100) + self.exhaust(middleware, 'DELETE', '/servers/4', 'usr2', 100) + + def test_POST_servers_action_implies_POST_action(self): + middleware = RateLimitingMiddleware(APIStub()) + self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) + self.exhaust(middleware, 'POST', '/images/4', 'usr2', 10) + self.assertTrue(set(middleware.limiter._levels) == + set(['usr1:POST', 'usr1:POST servers', 'usr2:POST'])) + + def test_POST_servers_action_correctly_ratelimited(self): + middleware = RateLimitingMiddleware(APIStub()) + # 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) + # 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) -- cgit From fd4d5787d5b6f6e550d33c13eb76f4562a87a118 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 11:23:08 -0400 Subject: Test the WSGIApp --- nova/api/rackspace/ratelimiting/__init__.py | 2 +- nova/api/rackspace/ratelimiting/tests.py | 69 ++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py index 176e7d66e..64d5fff2c 100644 --- a/nova/api/rackspace/ratelimiting/__init__.py +++ b/nova/api/rackspace/ratelimiting/__init__.py @@ -86,7 +86,7 @@ class WSGIApp(object): self.limiter = limiter @webob.dec.wsgify - def __call__(req): + def __call__(self, req): parts = req.path_info.split('/') # format: /limiter// if req.method != 'POST': diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index 545e1d1b6..f924e7805 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -1,8 +1,10 @@ -import ratelimiting import time import unittest +import webob -class Test(unittest.TestCase): +import nova.api.rackspace.ratelimiting as ratelimiting + +class LimiterTest(unittest.TestCase): def setUp(self): self.limits = { @@ -59,5 +61,68 @@ class Test(unittest.TestCase): self.exhaust('c', 0, username='bob') self.exhaust('c', 0, username='alice') + +class WSGIAppTest(unittest.TestCase): + + def setUp(self): + test = self + class FakeLimiter(object): + def __init__(self): + self._action = self._username = self._delay = None + def mock(self, action, username, delay): + self._action = action + self._username = username + self._delay = delay + def perform(self, action, username): + test.assertEqual(action, self._action) + test.assertEqual(username, self._username) + return self._delay + self.limiter = FakeLimiter() + self.app = ratelimiting.WSGIApp(self.limiter) + + def test_invalid_methods(self): + requests = [] + for method in ['GET', 'PUT', 'DELETE']: + req = webob.Request.blank('/limits/michael/breakdance', + dict(REQUEST_METHOD=method)) + requests.append(req) + for req in requests: + self.assertEqual(req.get_response(self.app).status_int, 405) + + def test_invalid_urls(self): + requests = [] + for prefix in ['limit', '', 'limiter2', 'limiter/limits', 'limiter/1']: + req = webob.Request.blank('/%s/michael/breakdance' % prefix, + dict(REQUEST_METHOD='POST')) + requests.append(req) + for req in requests: + self.assertEqual(req.get_response(self.app).status_int, 404) + + def verify(self, url, username, action, delay=None): + """Make sure that POSTing to the given url causes the given username + to perform the given action. Make the internal rate limiter return + delay and make sure that the WSGI app returns the correct response. + """ + req = webob.Request.blank(url, dict(REQUEST_METHOD='POST')) + self.limiter.mock(action, username, delay) + resp = req.get_response(self.app) + if not delay: + self.assertEqual(resp.status_int, 200) + else: + self.assertEqual(resp.status_int, 403) + self.assertEqual(resp.headers['X-Wait-Seconds'], delay) + + def test_good_urls(self): + self.verify('/limiter/michael/hoot', 'michael', 'hoot') + + def test_escaping(self): + self.verify('/limiter/michael/jump%20up', 'michael', 'jump up') + + def test_response_to_delays(self): + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1) + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1.56) + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) + + if __name__ == '__main__': unittest.main() -- cgit From f200587ce068482ab94e777154de3ac777269fa0 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 13:54:38 -0400 Subject: Add support for middleware proxying to a ratelimiting.WSGIApp, for deployments that use more than one API Server and thus can't store ratelimiting counters in memory. --- nova/api/rackspace/__init__.py | 29 ++++-- nova/api/rackspace/ratelimiting/__init__.py | 21 ++++- nova/api/rackspace/ratelimiting/tests.py | 140 +++++++++++++++++++++++++--- nova/tests/api/rackspace/__init__.py | 8 ++ 4 files changed, 173 insertions(+), 25 deletions(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index 66d80a5b7..ac5365310 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -69,17 +69,26 @@ class AuthMiddleware(wsgi.Middleware): class RateLimitingMiddleware(wsgi.Middleware): """Rate limit incoming requests according to the OpenStack rate limits.""" - def __init__(self, application): + def __init__(self, application, service_host=None): + """Create a rate limiting middleware that wraps the given application. + + By default, rate counters are stored in memory. If service_host is + specified, the middleware instead relies on the ratelimiting.WSGIApp + at the given host+port to keep rate counters. + """ super(RateLimitingMiddleware, self).__init__(application) - #TODO(gundlach): These limits were based on limitations of Cloud - #Servers. We should revisit them in Nova. - self.limiter = ratelimiting.Limiter(limits={ - 'DELETE': (100, ratelimiting.PER_MINUTE), - 'PUT': (10, ratelimiting.PER_MINUTE), - 'POST': (10, ratelimiting.PER_MINUTE), - 'POST servers': (50, ratelimiting.PER_DAY), - 'GET changes-since': (3, ratelimiting.PER_MINUTE), - }) + if not service_host: + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = ratelimiting.Limiter(limits={ + 'DELETE': (100, ratelimiting.PER_MINUTE), + 'PUT': (10, ratelimiting.PER_MINUTE), + 'POST': (10, ratelimiting.PER_MINUTE), + 'POST servers': (50, ratelimiting.PER_DAY), + 'GET changes-since': (3, ratelimiting.PER_MINUTE), + }) + else: + self.limiter = ratelimiting.WSGIAppProxy(service_host) @webob.dec.wsgify def __call__(self, req): diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py index 64d5fff2c..f843bac0f 100644 --- a/nova/api/rackspace/ratelimiting/__init__.py +++ b/nova/api/rackspace/ratelimiting/__init__.py @@ -1,5 +1,6 @@ """Rate limiting of arbitrary actions.""" +import httplib import time import urllib import webob.dec @@ -98,6 +99,24 @@ class WSGIApp(object): delay = self.limiter.perform(action_name, username) if delay: return webob.exc.HTTPForbidden( - headers={'X-Wait-Seconds': delay}) + headers={'X-Wait-Seconds': "%.2f" % delay}) else: return '' # 200 OK + + +class WSGIAppProxy(object): + + """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" + + def __init__(self, service_host): + """Creates a proxy pointing to a ratelimiting.WSGIApp at the given + host.""" + self.service_host = service_host + + def perform(self, action, username='nobody'): + conn = httplib.HTTPConnection(self.service_host) + conn.request('POST', '/limiter/%s/%s' % (username, action)) + resp = conn.getresponse() + if resp.status == 200: + return None # no delay + return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index f924e7805..13a47989b 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -1,3 +1,5 @@ +import httplib +import StringIO import time import unittest import webob @@ -62,22 +64,25 @@ class LimiterTest(unittest.TestCase): self.exhaust('c', 0, username='alice') +class FakeLimiter(object): + """Fake Limiter class that you can tell how to behave.""" + def __init__(self, test): + self._action = self._username = self._delay = None + self.test = test + def mock(self, action, username, delay): + self._action = action + self._username = username + self._delay = delay + def perform(self, action, username): + self.test.assertEqual(action, self._action) + self.test.assertEqual(username, self._username) + return self._delay + + class WSGIAppTest(unittest.TestCase): def setUp(self): - test = self - class FakeLimiter(object): - def __init__(self): - self._action = self._username = self._delay = None - def mock(self, action, username, delay): - self._action = action - self._username = username - self._delay = delay - def perform(self, action, username): - test.assertEqual(action, self._action) - test.assertEqual(username, self._username) - return self._delay - self.limiter = FakeLimiter() + self.limiter = FakeLimiter(self) self.app = ratelimiting.WSGIApp(self.limiter) def test_invalid_methods(self): @@ -110,7 +115,7 @@ class WSGIAppTest(unittest.TestCase): self.assertEqual(resp.status_int, 200) else: self.assertEqual(resp.status_int, 403) - self.assertEqual(resp.headers['X-Wait-Seconds'], delay) + self.assertEqual(resp.headers['X-Wait-Seconds'], "%.2f" % delay) def test_good_urls(self): self.verify('/limiter/michael/hoot', 'michael', 'hoot') @@ -124,5 +129,112 @@ class WSGIAppTest(unittest.TestCase): self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) +class FakeHttplibSocket(object): + """a fake socket implementation for httplib.HTTPResponse, trivial""" + + def __init__(self, response_string): + self._buffer = StringIO.StringIO(response_string) + + def makefile(self, _mode, _other): + """Returns the socket's internal buffer""" + return self._buffer + + +class FakeHttplibConnection(object): + """A fake httplib.HTTPConnection + + Requests made via this connection actually get translated and routed into + our WSGI app, we then wait for the response and turn it back into + an httplib.HTTPResponse. + """ + def __init__(self, app, host, is_secure=False): + self.app = app + self.host = host + + def request(self, method, path, data='', headers={}): + req = webob.Request.blank(path) + req.method = method + req.body = data + req.headers = headers + req.host = self.host + # Call the WSGI app, get the HTTP response + resp = str(req.get_response(self.app)) + # For some reason, the response doesn't have "HTTP/1.0 " prepended; I + # guess that's a function the web server usually provides. + resp = "HTTP/1.0 %s" % resp + sock = FakeHttplibSocket(resp) + self.http_response = httplib.HTTPResponse(sock) + self.http_response.begin() + + def getresponse(self): + return self.http_response + + +def wire_HTTPConnection_to_WSGI(host, app): + """Monkeypatches HTTPConnection so that if you try to connect to host, you + are instead routed straight to the given WSGI app. + + After calling this method, when any code calls + + httplib.HTTPConnection(host) + + the connection object will be a fake. Its requests will be sent directly + to the given WSGI app rather than through a socket. + + Code connecting to hosts other than host will not be affected. + + This method may be called multiple times to map different hosts to + different apps. + """ + class HTTPConnectionDecorator(object): + """Wraps the real HTTPConnection class so that when you instantiate + the class you might instead get a fake instance.""" + def __init__(self, wrapped): + self.wrapped = wrapped + def __call__(self, connection_host, *args, **kwargs): + if connection_host == host: + return FakeHttplibConnection(app, host) + else: + return self.wrapped(connection_host, *args, **kwargs) + httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) + + +class WSGIAppProxyTest(unittest.TestCase): + + def setUp(self): + """Our WSGIAppProxy is going to call across an HTTPConnection to a + WSGIApp running a limiter. The proxy will send input, and the proxy + should receive that same input, pass it to the limiter who gives a + result, and send the expected result back. + + The HTTPConnection isn't real -- it's monkeypatched to point straight + at the WSGIApp. And the limiter isn't real -- it's a fake that + behaves the way we tell it to. + """ + self.limiter = FakeLimiter(self) + app = ratelimiting.WSGIApp(self.limiter) + wire_HTTPConnection_to_WSGI('100.100.100.100:80', app) + self.proxy = ratelimiting.WSGIAppProxy('100.100.100.100:80') + + def test_200(self): + self.limiter.mock('conquer', 'caesar', None) + when = self.proxy.perform('conquer', 'caesar') + self.assertEqual(when, None) + + def test_403(self): + self.limiter.mock('grumble', 'proletariat', 1.5) + when = self.proxy.perform('grumble', 'proletariat') + self.assertEqual(when, 1.5) + + def test_failure(self): + self.limiter.mock('murder', 'brutus', None) + try: + when = self.proxy.perform('stab', 'brutus') + except AssertionError: + pass + else: + self.fail("I didn't perform the action I expected") + + if __name__ == '__main__': unittest.main() diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py index 2fab1a4da..622cb4335 100644 --- a/nova/tests/api/rackspace/__init__.py +++ b/nova/tests/api/rackspace/__init__.py @@ -21,7 +21,9 @@ from nova.api.rackspace import RateLimitingMiddleware from nova.tests.api.test_helper import * from webob import Request + class RateLimitingMiddlewareTest(unittest.TestCase): + def test_get_action_name(self): middleware = RateLimitingMiddleware(APIStub()) def verify(method, url, action_name): @@ -69,3 +71,9 @@ class RateLimitingMiddlewareTest(unittest.TestCase): 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) + + def test_proxy_ctor_works(self): + middleware = RateLimitingMiddleware(APIStub()) + self.assertEqual(middleware.limiter.__class__.__name__, "Limiter") + middleware = RateLimitingMiddleware(APIStub(), service_host='foobar') + self.assertEqual(middleware.limiter.__class__.__name__, "WSGIAppProxy") -- cgit From 7437df558f3277e21a4c34a5b517a1cae5dd5a74 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 17:17:20 -0400 Subject: Support querying version list --- nova/api/__init__.py | 13 +++++++++++++ nova/tests/api/__init__.py | 5 +++-- run_tests.py | 4 +++- tools/pip-requires | 4 ++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/nova/api/__init__.py b/nova/api/__init__.py index b9b9e3988..9f116dada 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -21,6 +21,7 @@ Root WSGI middleware for all API controllers. """ import routes +import webob.dec from nova import wsgi from nova.api import ec2 @@ -32,6 +33,18 @@ class API(wsgi.Router): def __init__(self): mapper = routes.Mapper() + mapper.connect("/", controller=self.versions) mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) mapper.connect("/ec2/{path_info:.*}", controller=ec2.API()) super(API, self).__init__(mapper) + + @webob.dec.wsgify + def versions(self, req): + """Respond to a request for all OpenStack API versions.""" + response = { + "versions": [ + dict(status="CURRENT", id="v1.0")]} + metadata = { + "application/xml": { + "attributes": dict(version=["status", "id"])}} + return wsgi.Serializer(req.environ, metadata).to_content_type(response) diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py index 59c4adc3d..4682c094e 100644 --- a/nova/tests/api/__init__.py +++ b/nova/tests/api/__init__.py @@ -52,8 +52,9 @@ class Test(unittest.TestCase): result = webob.Request.blank('/test/cloud').get_response(api.API()) self.assertNotEqual(result.body, "/cloud") - def test_query_api_version(self): - pass + def test_query_api_versions(self): + result = webob.Request.blank('/').get_response(api.API()) + self.assertTrue('CURRENT' in result.body) if __name__ == '__main__': unittest.main() diff --git a/run_tests.py b/run_tests.py index 77aa9088a..cf37b820e 100644 --- a/run_tests.py +++ b/run_tests.py @@ -50,8 +50,10 @@ from nova import flags from nova import twistd from nova.tests.access_unittest import * -from nova.tests.auth_unittest import * from nova.tests.api_unittest import * +from nova.tests.api import * +from nova.tests.api.rackspace import * +from nova.tests.auth_unittest import * from nova.tests.cloud_unittest import * from nova.tests.compute_unittest import * from nova.tests.flags_unittest import * diff --git a/tools/pip-requires b/tools/pip-requires index 13e8e5f45..9b8027451 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -6,14 +6,14 @@ amqplib==0.6.1 anyjson==0.2.4 boto==2.0b1 carrot==0.10.5 -eventlet==0.9.10 +eventlet==0.9.12 lockfile==0.8 python-daemon==1.5.5 python-gflags==1.3 redis==2.0.0 routes==1.12.3 tornado==1.0 -webob==0.9.8 +WebOb==0.9.8 wsgiref==0.1.2 zope.interface==3.6.1 mox==0.5.0 -- cgit From 5ff47a4513c3b5a7f8f90c417e1e62113797de8c Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Sun, 19 Sep 2010 18:23:41 -0700 Subject: updated docstring --- nova/flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/flags.py b/nova/flags.py index efa14f9d7..64dd9d456 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -169,7 +169,7 @@ def DECLARE(name, module_string, flag_values=FLAGS): DEFINE_list('region_list', [], - 'list of region,url pairs') + 'list of region|url pairs separated by commas') DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') -- cgit From ae760b13c5382f2f4719dde445235c156cc27d18 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 20 Sep 2010 14:49:05 -0400 Subject: Use assertRaises --- nova/api/rackspace/ratelimiting/tests.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index 13a47989b..4c9510917 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -227,13 +227,10 @@ class WSGIAppProxyTest(unittest.TestCase): self.assertEqual(when, 1.5) def test_failure(self): - self.limiter.mock('murder', 'brutus', None) - try: - when = self.proxy.perform('stab', 'brutus') - except AssertionError: - pass - else: - self.fail("I didn't perform the action I expected") + def shouldRaise(): + self.limiter.mock('murder', 'brutus', None) + self.proxy.perform('stab', 'brutus') + self.assertRaises(AssertionError, shouldRaise) if __name__ == '__main__': -- cgit From fc93548e99dea561dbf2f198b0fccc84467dbf8b Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 20 Sep 2010 17:02:32 -0400 Subject: Undo run_tests.py modification in the hopes of making this merge --- run_tests.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/run_tests.py b/run_tests.py index cf37b820e..77aa9088a 100644 --- a/run_tests.py +++ b/run_tests.py @@ -50,10 +50,8 @@ from nova import flags from nova import twistd from nova.tests.access_unittest import * -from nova.tests.api_unittest import * -from nova.tests.api import * -from nova.tests.api.rackspace import * from nova.tests.auth_unittest import * +from nova.tests.api_unittest import * from nova.tests.cloud_unittest import * from nova.tests.compute_unittest import * from nova.tests.flags_unittest import * -- cgit From ce1a8086f7ec947dd148855910a1a5a9696e33f7 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Mon, 20 Sep 2010 23:56:17 -0400 Subject: Don't use something the shell will escape as a separator. | is now =. --- nova/endpoint/cloud.py | 2 +- nova/flags.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index b28bb0dc3..2b67af96f 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -158,7 +158,7 @@ class CloudController(object): if FLAGS.region_list: regions = [] for region in FLAGS.region_list: - name, _sep, url = region.partition('|') + name, _sep, url = region.partition('=') regions.append({'regionName': name, 'regionEndpoint': url}) else: diff --git a/nova/flags.py b/nova/flags.py index 64dd9d456..c5dee2855 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -169,7 +169,7 @@ def DECLARE(name, module_string, flag_values=FLAGS): DEFINE_list('region_list', [], - 'list of region|url pairs separated by commas') + 'list of region=url pairs separated by commas') DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') -- cgit