From 1278af43c16daea1bcbd2d47cd2d81919fe2c37e Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 27 Feb 2011 12:51:19 -0500 Subject: Add lxc libvirt driver --- nova/virt/libvirt.xml.template | 14 ++++++++++++++ nova/virt/libvirt_conn.py | 13 +++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index 88bfbc668..87dea9039 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -2,6 +2,12 @@ ${name} ${memory_kb} +#if $type == 'lxc' + #set $disk_prefix = '' + #set $disk_bus = '' + exe + /sbin/init +#else #if $type == 'uml' #set $disk_prefix = 'ubd' #set $disk_bus = 'uml' @@ -37,6 +43,7 @@ #end if #end if + #end if #end if @@ -44,6 +51,12 @@ ${vcpus} +#if $type == 'lxc' + + + + +#else #if $getVar('rescue', False) @@ -68,6 +81,7 @@ #end if + #end if #end if diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 4e0fd106f..f97c10765 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -20,7 +20,7 @@ """ A connection to a hypervisor through libvirt. -Supports KVM, QEMU, UML, and XEN. +Supports KVM, LXC, QEMU, UML, and XEN. **Related Flags** @@ -83,7 +83,7 @@ flags.DEFINE_string('libvirt_xml_template', flags.DEFINE_string('libvirt_type', 'kvm', 'Libvirt domain type (valid options are: ' - 'kvm, qemu, uml, xen)') + 'kvm, lxc, qemu, uml, xen)') flags.DEFINE_string('libvirt_uri', '', 'Override the default libvirt URI (which is dependent' @@ -202,6 +202,8 @@ class LibvirtConnection(object): uri = FLAGS.libvirt_uri or 'uml:///system' elif FLAGS.libvirt_type == 'xen': uri = FLAGS.libvirt_uri or 'xen:///' + elif FLAGS.libvirt_type == 'lxc': + uri = FLAGS.libvirt_uri or 'lxc:///' else: uri = FLAGS.libvirt_uri or 'qemu:///system' return uri @@ -565,6 +567,10 @@ class LibvirtConnection(object): f.write(libvirt_xml) f.close() + if FLAGS.libvirt_type == 'lxc': + container_dir = '%s/rootfs' % basepath(suffix='') + utils.execute('mkdir -p %s' % container_dir) + # NOTE(vish): No need add the suffix to console.log os.close(os.open(basepath('console.log', ''), os.O_CREAT | os.O_WRONLY, 0660)) @@ -622,6 +628,9 @@ class LibvirtConnection(object): if not inst['kernel_id']: target_partition = "1" + if FLAGS.libvirt_type == 'lxc': + target_partition = None + key = str(inst['key_data']) net = None network_ref = db.network_get_by_instance(context.get_admin_context(), -- cgit From 2d6987d8c33477af19179e7664bbedf689bc08dc Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 27 Feb 2011 13:05:26 -0500 Subject: Add ability to mount containers --- nova/virt/disk.py | 26 ++++++++++++++++++++++++++ nova/virt/libvirt_conn.py | 6 ++++++ 2 files changed, 32 insertions(+) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 2bded07a4..7cbc4a3be 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -114,6 +114,32 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): _unlink_device(device, nbd) +def setup_container(image, container_dir=None, partition=None, nbd=False): + """Setup the LXC container + + It will mount the loopback image to the container directory in order + to create the root filesystem for the container + """ + device = _link_device(image, nbd) + try: + if not partition is None: + # create partition + utils.execute('sudo kpartx -a %s' % device) + mapped_device = '/dev/mapper/%p%s' % (device.split('/')[-1], + partition) + else: + mapped_device = device + + utils.execute('sudo mount %s %s' %(mapped_device, container_dir)) + + except Exception as e: + LOG.warn(_('Unable to mount container')) + if not partition is None: + # remove partitions + utils.execute('sudo kpartx -s %s' % device) + _unlink_device(device, nbd) + + def _link_device(image, nbd): """Link image to device using loopback or nbd""" if nbd: diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index f97c10765..f7807a851 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -661,6 +661,12 @@ class LibvirtConnection(object): disk.inject_data(basepath('disk'), key, net, partition=target_partition, nbd=FLAGS.use_cow_images) + + if FLAGS.libvirt_type == 'lxc': + disk.setup_container(basepath('disk'), + container_dir=container_dir, + partition=target_partition, + nbd=FLAGS.use_cow_images) except Exception as e: # This could be a windows image, or a vmdk format disk LOG.warn(_('instance %(inst_name)s: ignoring error injecting' -- cgit From 33d7edb9d02d8f4dcb0b6ee85caf10434f060e1b Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 27 Feb 2011 13:29:05 -0500 Subject: Clean up the mount points when it shutsdown --- nova/virt/disk.py | 1 - nova/virt/libvirt_conn.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 7cbc4a3be..28a4c11fb 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -139,7 +139,6 @@ def setup_container(image, container_dir=None, partition=None, nbd=False): utils.execute('sudo kpartx -s %s' % device) _unlink_device(device, nbd) - def _link_device(image, nbd): """Link image to device using loopback or nbd""" if nbd: diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index f7807a851..5e76edb0b 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -259,6 +259,8 @@ class LibvirtConnection(object): instance_name = instance['name'] LOG.info(_('instance %(instance_name)s: deleting instance files' ' %(target)s') % locals()) + if FLAGS.libvirt_type == 'lxc': + utils.execute('sudo umount %s/rootfs' % target) if os.path.exists(target): shutil.rmtree(target) -- cgit From 031205a26ae6fe906a47eefa716bbd575687c479 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 27 Feb 2011 13:35:10 -0500 Subject: Add lxc to the libvirt tests --- nova/tests/test_virt.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index f151ae911..04f943a1f 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -112,12 +112,15 @@ class LibvirtConnTestCase(test.TestCase): 'uml': ('uml:///system', [(lambda t: t.find('.').get('type'), 'uml'), (lambda t: t.find('./os/type').text, 'uml')]), + 'lxc': ('lxc://', + [(lambda t: t.find('.').get('type'), 'lxc'), + (lambda t: t.find('./os/type').text, 'lxc')]), 'xen': ('xen:///', [(lambda t: t.find('.').get('type'), 'xen'), (lambda t: t.find('./os/type').text, 'linux')]), } - for hypervisor_type in ['qemu', 'kvm', 'xen']: + for hypervisor_type in ['qemu', 'kvm', 'lxc', 'xen']: check_list = type_uri_map[hypervisor_type][1] if rescue: -- cgit From 7825b7ce81dec97e997d296c3e30b5d143948abc Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 2 Mar 2011 01:21:54 -0800 Subject: initial commit of vnc support --- nova/api/ec2/cloud.py | 6 ++ nova/compute/api.py | 17 +++++ nova/compute/manager.py | 9 +++ nova/flags.py | 6 ++ nova/virt/libvirt.xml.template | 1 + nova/virt/libvirt_conn.py | 17 +++++ tools/euca-get-vnc-console | 163 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 219 insertions(+) create mode 100755 tools/euca-get-vnc-console diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 844ccbe5e..aa9c6824e 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -539,6 +539,12 @@ class CloudController(object): return self.compute_api.get_ajax_console(context, instance_id=instance_id) + def get_vnc_console(self, context, instance_id, **kwargs): + ec2_id = instance_id + instance_id = ec2_id_to_id(ec2_id) + return self.compute_api.get_vnc_console(context, + instance_id=instance_id) + def describe_volumes(self, context, volume_id=None, **kwargs): if volume_id: volumes = [] diff --git a/nova/compute/api.py b/nova/compute/api.py index 625778b66..cec978d75 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -476,6 +476,23 @@ class API(base.Base): return {'url': '%s/?token=%s' % (FLAGS.ajax_console_proxy_url, output['token'])} + def get_vnc_console(self, context, instance_id): + """Get a url to an AJAX Console""" + instance = self.get(context, instance_id) + output = self._call_compute_message('get_vnc_console', + context, + instance_id) + rpc.cast(context, '%s' % FLAGS.vnc_console_proxy_topic, + {'method': 'authorize_vnc_console', + 'args': {'token': output['token'], 'host': output['host'], + 'port': output['port']}}) + + time.sleep(1) + + return {'url': '%s/vnc_auto.html?token=%s&host=%s&port=%s' % + (FLAGS.vnc_console_proxy_url, + output['token'], 'hostignore', 'portignore')} + def get_console_output(self, context, instance_id): """Get console output for an an instance""" return self._call_compute_message('get_console_output', diff --git a/nova/compute/manager.py b/nova/compute/manager.py index d659712ad..e53b36b34 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -557,6 +557,15 @@ class ComputeManager(manager.Manager): return self.driver.get_ajax_console(instance_ref) + @exception.wrap_exception + def get_vnc_console(self, context, instance_id): + """Return connection information for an vnc console""" + context = context.elevated() + LOG.debug(_("instance %s: getting vnc console"), instance_id) + instance_ref = self.db.instance_get(context, instance_id) + + return self.driver.get_vnc_console(instance_ref) + @checks_instance_lock def attach_volume(self, context, instance_id, volume_id, mountpoint): """Attach a volume to an instance.""" diff --git a/nova/flags.py b/nova/flags.py index 8cf199b2f..4f2be82b6 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -281,6 +281,12 @@ DEFINE_string('ajax_console_proxy_url', in the form "http://127.0.0.1:8000"') DEFINE_string('ajax_console_proxy_port', 8000, 'port that ajax_console_proxy binds') +DEFINE_string('vnc_console_proxy_topic', 'vnc_proxy', + 'the topic vnc proxy nodes listen on') +DEFINE_string('vnc_console_proxy_url', + 'http://127.0.0.1:6080', + 'location of vnc console proxy, \ + in the form "http://127.0.0.1:6080"') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') DEFINE_bool('fake_network', False, diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index 88bfbc668..7b4c23211 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -101,5 +101,6 @@ + diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 4e0fd106f..4fca84639 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -511,6 +511,23 @@ class LibvirtConnection(object): subprocess.Popen(cmd, shell=True) return {'token': token, 'host': host, 'port': port} + @exception.wrap_exception + def get_vnc_console(self, instance): + def get_vnc_port_for_instance(instance_name): + virt_dom = self._conn.lookupByName(instance_name) + xml = virt_dom.XMLDesc(0) + dom = minidom.parseString(xml) + + for graphic in dom.getElementsByTagName('graphics'): + if graphic.getAttribute('type') == 'vnc': + return graphic.getAttribute('port') + + port = get_vnc_port_for_instance(instance['name']) + token = str(uuid.uuid4()) + host = instance['host'] + + return {'token': token, 'host': host, 'port': port} + def _cache_image(self, fn, target, fname, cow=False, *args, **kwargs): """Wrapper for a method that creates an image that caches the image. diff --git a/tools/euca-get-vnc-console b/tools/euca-get-vnc-console new file mode 100755 index 000000000..bd2788f03 --- /dev/null +++ b/tools/euca-get-vnc-console @@ -0,0 +1,163 @@ +#!/usr/bin/env python +# pylint: disable-msg=C0103 +# 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. + +"""Euca add-on to use vnc console""" + +import getopt +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) + +import boto +import nova +from boto.ec2.connection import EC2Connection +from euca2ools import Euca2ool, InstanceValidationError, Util, ConnectionFailed + +usage_string = """ +Retrieves a url to an vnc console terminal + +euca-get-vnc-console [-h, --help] [--version] [--debug] instance_id + +REQUIRED PARAMETERS + +instance_id: unique identifier for the instance show the console output for. + +OPTIONAL PARAMETERS + +""" + + +# This class extends boto to add VNCConsole functionality +class NovaEC2Connection(EC2Connection): + + def get_vnc_console(self, instance_id): + """ + Retrieves a console connection for the specified instance. + + :type instance_id: string + :param instance_id: The instance ID of a running instance on the cloud. + + :rtype: :class:`VNCConsole` + """ + + class VNCConsole: + def __init__(self, parent=None): + self.parent = parent + self.instance_id = None + self.url = None + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'instanceId': + self.instance_id = value + elif name == 'url': + self.url = value + else: + setattr(self, name, value) + + params = {} + return self.get_object('GetVNCConsole', + {'InstanceId': instance_id}, VNCConsole) + + +def override_connect_ec2(aws_access_key_id=None, + aws_secret_access_key=None, **kwargs): + return NovaEC2Connection(aws_access_key_id, + aws_secret_access_key, **kwargs) + +# override boto's connect_ec2 method, so that we can use NovaEC2Connection +boto.connect_ec2 = override_connect_ec2 + + +def usage(status=1): + print usage_string + Util().usage() + sys.exit(status) + + +def version(): + print Util().version() + sys.exit() + + +def display_console_output(console_output): + print console_output.instance_id + print console_output.timestamp + print console_output.output + + +def display_vnc_console_output(console_output): + print console_output.url + + +def main(): + try: + euca = Euca2ool() + except Exception, e: + print e + usage() + + instance_id = None + + for name, value in euca.opts: + if name in ('-h', '--help'): + usage(0) + elif name == '--version': + version() + elif name == '--debug': + debug = True + + for arg in euca.args: + instance_id = arg + break + + if instance_id: + try: + euca.validate_instance_id(instance_id) + except InstanceValidationError: + print 'Invalid instance id' + sys.exit(1) + + try: + euca_conn = euca.make_connection() + except ConnectionFailed, e: + print e.message + sys.exit(1) + try: + console_output = euca_conn.get_vnc_console(instance_id) + except Exception, ex: + euca.display_error_and_exit('%s' % ex) + + display_vnc_console_output(console_output) + else: + print 'instance_id must be specified' + usage() + +if __name__ == "__main__": + main() -- cgit From e502ad0243962aca98cc28bfa5cf69f8cd08991c Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 9 Mar 2011 21:43:45 -0500 Subject: Moved umount container to disk.py and try to remove loopback when destroying the container --- nova/virt/disk.py | 8 ++++++++ nova/virt/libvirt_conn.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index e1b0171b5..484317dd8 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -140,6 +140,14 @@ def setup_container(image, container_dir=None, partition=None, nbd=False): utils.execute('sudo kpartx -s %s' % device) _unlink_device(device, nbd) +def destroy_container(target, instance, nbd=False): + """Destroy the container once it terminates""" + try: + utils.execute('sudo umount %s/rootfs' % target) + image = os.path.join(FLAGS.instances_path, instance['name'], '' + 'disk') + except Exception as e: + LOG.warn(_('Unable to umount contianer')) + def _link_device(image, nbd): """Link image to device using loopback or nbd""" if nbd: diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 97997513b..ef0eb20cd 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -260,7 +260,7 @@ class LibvirtConnection(object): LOG.info(_('instance %(instance_name)s: deleting instance files' ' %(target)s') % locals()) if FLAGS.libvirt_type == 'lxc': - utils.execute('sudo umount %s/rootfs' % target) + disk.destroy_container(target, instance, nbd=FLAGS.use_cow_images) if os.path.exists(target): shutil.rmtree(target) -- cgit From b76b61dbec03455824b90c427eb816c15e284013 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Fri, 11 Mar 2011 10:32:09 -0800 Subject: Added volume api from previous megapatch --- nova/api/openstack/__init__.py | 6 ++ nova/api/openstack/volumes.py | 160 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 nova/api/openstack/volumes.py diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index ab9dbb780..a7b639669 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -34,6 +34,7 @@ from nova.api.openstack import flavors from nova.api.openstack import images from nova.api.openstack import servers from nova.api.openstack import shared_ip_groups +from nova.api.openstack import volumes from nova.api.openstack import zones @@ -111,6 +112,11 @@ class APIRouter(wsgi.Router): collection={'detail': 'GET'}, controller=shared_ip_groups.Controller()) + #NOTE(justinsb): volumes is not yet part of the official API + mapper.resource("volume", "volumes", + controller=volumes.Controller(), + collection={'detail': 'GET'}) + super(APIRouter, self).__init__(mapper) diff --git a/nova/api/openstack/volumes.py b/nova/api/openstack/volumes.py new file mode 100644 index 000000000..99300421e --- /dev/null +++ b/nova/api/openstack/volumes.py @@ -0,0 +1,160 @@ +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from webob import exc + +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + +FLAGS = flags.FLAGS + + +def _translate_detail_view(context, inst): + """ Maps keys for details view""" + + inst_dict = _translate_summary_view(context, inst) + + # No additional data / lookups at the moment + + return inst_dict + + +def _translate_summary_view(context, volume): + """ Maps keys for summary view""" + v = {} + + instance_id = None + # instance_data = None + attached_to = volume.get('instance') + if attached_to: + instance_id = attached_to['id'] + # instance_data = '%s[%s]' % (instance_ec2_id, + # attached_to['host']) + v['id'] = volume['id'] + v['status'] = volume['status'] + v['size'] = volume['size'] + v['availabilityZone'] = volume['availability_zone'] + v['createdAt'] = volume['created_at'] + # if context.is_admin: + # v['status'] = '%s (%s, %s, %s, %s)' % ( + # volume['status'], + # volume['user_id'], + # volume['host'], + # instance_data, + # volume['mountpoint']) + if volume['attach_status'] == 'attached': + v['attachments'] = [{'attachTime': volume['attach_time'], + 'deleteOnTermination': False, + 'mountpoint': volume['mountpoint'], + 'instanceId': instance_id, + 'status': 'attached', + 'volumeId': volume['id']}] + else: + v['attachments'] = [{}] + + v['displayName'] = volume['display_name'] + v['displayDescription'] = volume['display_description'] + return v + + +class Controller(wsgi.Controller): + """ The Volumes API controller for the OpenStack API """ + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + ]}}} + + def __init__(self): + self.volume_api = volume.API() + super(Controller, self).__init__() + + def show(self, req, id): + """Return data about the given volume""" + context = req.environ['nova.context'] + + try: + volume = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_detail_view(context, volume)} + + def delete(self, req, id): + """ Delete a volume """ + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + def index(self, req): + """ Returns a summary list of volumes""" + return self._items(req, entity_maker=_translate_summary_view) + + def detail(self, req): + """ Returns a detailed list of volumes """ + return self._items(req, entity_maker=_translate_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, inst) for inst in limited_list] + return {'volumes': res} + + def create(self, req): + """Creates a new volume""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + vol = env['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) + + # Work around problem that instance is lazy-loaded... + volume['instance'] = None + + retval = _translate_detail_view(context, volume) + + return {'volume': retval} -- cgit From 743e82c0acac0fda78a55a8bbb65e601c4cb652c Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 14 Mar 2011 21:14:39 -0400 Subject: Refactor setup contianer/destroy container --- nova/virt/disk.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index f0b391efb..a3db1d882 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -122,31 +122,26 @@ def setup_container(image, container_dir=None, partition=None, nbd=False): to create the root filesystem for the container """ device = _link_device(image, nbd) - try: - if not partition is None: - # create partition - utils.execute('sudo kpartx -a %s' % device) - mapped_device = '/dev/mapper/%p%s' % (device.split('/')[-1], - partition) - else: - mapped_device = device - - utils.execute('sudo mount %s %s' %(mapped_device, container_dir)) - - except Exception as e: - LOG.warn(_('Unable to mount container')) - if not partition is None: - # remove partitions - utils.execute('sudo kpartx -s %s' % device) + err = utils.execute('sudo', 'mount', mapped_device, container_dir) + if err: + raise exception.Error(_('Failed to mount filesystem: %s') + % err) _unlink_device(device, nbd) def destroy_container(target, instance, nbd=False): """Destroy the container once it terminates""" try: - utils.execute('sudo umount %s/rootfs' % target) + container_dir = '%s/rootfs' % target + utils.execute('sudo', 'umount', container_dir) + finally: image = os.path.join(FLAGS.instances_path, instance['name'], '' + 'disk') - except Exception as e: - LOG.warn(_('Unable to umount contianer')) + out, err = utils.execute('sudo', 'losetup', '--find', '--show', image) + device = out.strip() + if err: + raise execption.Error(_('Could not find loopback image: %s') + %err) + utils.execute('sudo', 'losetup', '--detach', device) + def _link_device(image, nbd): """Link image to device using loopback or nbd""" -- cgit From 48d3dd7f9d2633d8955080b6dccc7c97bc8ef7c3 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 15 Mar 2011 07:56:26 -0400 Subject: Mount the right device --- nova/virt/disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index a3db1d882..2c0460f39 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -122,7 +122,7 @@ def setup_container(image, container_dir=None, partition=None, nbd=False): to create the root filesystem for the container """ device = _link_device(image, nbd) - err = utils.execute('sudo', 'mount', mapped_device, container_dir) + err = utils.execute('sudo', 'mount', device, container_dir) if err: raise exception.Error(_('Failed to mount filesystem: %s') % err) -- cgit From f60c9d0da8171b09bd7971fea52e9e032f98a143 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 15 Mar 2011 08:05:45 -0400 Subject: Add comments about the destroy container function --- nova/virt/disk.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 2c0460f39..dd4352957 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -129,7 +129,11 @@ def setup_container(image, container_dir=None, partition=None, nbd=False): _unlink_device(device, nbd) def destroy_container(target, instance, nbd=False): - """Destroy the container once it terminates""" + """Destroy the container once it terminates + + It will umount the container that is mounted, try to find the loopback + device associated with the container and delete it. + """ try: container_dir = '%s/rootfs' % target utils.execute('sudo', 'umount', container_dir) -- cgit From 3c10c1ee1bcc3f3aad90e4e28761d1413ab203a9 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 15 Mar 2011 09:36:02 -0400 Subject: Really delete the loop --- nova/virt/disk.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index dd4352957..a44995613 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -23,6 +23,7 @@ Includes injection of SSH PGP keys into authorized_keys file. """ import os +import string import tempfile import time @@ -138,13 +139,10 @@ def destroy_container(target, instance, nbd=False): container_dir = '%s/rootfs' % target utils.execute('sudo', 'umount', container_dir) finally: - image = os.path.join(FLAGS.instances_path, instance['name'], '' + 'disk') - out, err = utils.execute('sudo', 'losetup', '--find', '--show', image) - device = out.strip() - if err: - raise execption.Error(_('Could not find loopback image: %s') - %err) - utils.execute('sudo', 'losetup', '--detach', device) + for loop in os.popen('sudo losetup -a').readlines(): + if instance['name'] in loop: + device = string.split(loop, ':') + utils.execute('sudo', 'losetup', '--detach', device) def _link_device(image, nbd): -- cgit From 7fbf061666516705e74592c3660155e86d3da895 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 16 Mar 2011 09:15:46 -0400 Subject: Fix up testsuite for lxc --- nova/tests/test_virt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index c149c9307..b3ca241cb 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -256,9 +256,9 @@ class LibvirtConnTestCase(test.TestCase): 'uml': ('uml:///system', [(lambda t: t.find('.').get('type'), 'uml'), (lambda t: t.find('./os/type').text, 'uml')]), - 'lxc': ('lxc://', + 'lxc': ('lxc://;', [(lambda t: t.find('.').get('type'), 'lxc'), - (lambda t: t.find('./os/type').text, 'lxc')]), + (lambda t: t.find('./os/type').text, 'exe')]), 'xen': ('xen:///', [(lambda t: t.find('.').get('type'), 'xen'), (lambda t: t.find('./os/type').text, 'linux')]), -- cgit From 8f9a5ecb7d3907456b9a77f3321ed09feb5c5f2f Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 16 Mar 2011 09:24:17 -0400 Subject: More execvp fallout --- nova/virt/libvirt_conn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index af2cbdce5..a79e0a065 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -599,7 +599,7 @@ class LibvirtConnection(object): if FLAGS.libvirt_type == 'lxc': container_dir = '%s/rootfs' % basepath(suffix='') - utils.execute('mkdir -p %s' % container_dir) + utils.execute('mkdir', '-p', container_dir) # NOTE(vish): No need add the suffix to console.log os.close(os.open(basepath('console.log', ''), -- cgit From a21efc63be6bad3bbde41eb96d6a1752e6d8174d Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 16 Mar 2011 09:26:37 -0400 Subject: Really fix testcase --- nova/tests/test_virt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index b3ca241cb..fed8ff803 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -256,7 +256,7 @@ class LibvirtConnTestCase(test.TestCase): 'uml': ('uml:///system', [(lambda t: t.find('.').get('type'), 'uml'), (lambda t: t.find('./os/type').text, 'uml')]), - 'lxc': ('lxc://;', + 'lxc': ('lxc:///', [(lambda t: t.find('.').get('type'), 'lxc'), (lambda t: t.find('./os/type').text, 'exe')]), 'xen': ('xen:///', -- cgit From c44ab013f5f5a078b27c4965e2e3c4abbfe30c59 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 16 Mar 2011 20:42:39 -0400 Subject: Revert testsuite changes --- nova/tests/test_virt.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index fed8ff803..b214f5ce7 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -256,15 +256,12 @@ class LibvirtConnTestCase(test.TestCase): 'uml': ('uml:///system', [(lambda t: t.find('.').get('type'), 'uml'), (lambda t: t.find('./os/type').text, 'uml')]), - 'lxc': ('lxc:///', - [(lambda t: t.find('.').get('type'), 'lxc'), - (lambda t: t.find('./os/type').text, 'exe')]), 'xen': ('xen:///', [(lambda t: t.find('.').get('type'), 'xen'), (lambda t: t.find('./os/type').text, 'linux')]), } - for hypervisor_type in ['qemu', 'kvm', 'lxc', 'xen']: + for hypervisor_type in ['qemu', 'kvm', 'xen']: check_list = type_uri_map[hypervisor_type][1] if rescue: -- cgit From fea850245835f867aa4cc741b612445e56e64236 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Wed, 16 Mar 2011 20:52:14 -0400 Subject: Add basic tests for lxc containers. --- nova/tests/test_virt.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index b214f5ce7..fab05de10 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -227,6 +227,42 @@ class LibvirtConnTestCase(test.TestCase): self._check_xml_and_uri(instance_data, expect_kernel=True, expect_ramdisk=True, rescue=True) + def test_lxc_container_and_uri(self): + instance_data = dict(self.test_instace) + self._check_xml_and_container(instance_data) + + def _check_xml_and_container(self, instance): + user_context = context.RequestContext(project=self.project, + user=self.user) + instance_ref = db.instance_create(user_context,instance) + host = self.network.get_network_host(user_context.elevated()) + network_ref= db.project_get_network(context.get_admin_context(), + self.project.id) + + fixed_ip = {'address': self.test_ip, + 'network_id': network_ref['id']} + + ctxt = context.get_admin_context() + fixed_ip_ref = db.fixed_ip_create(ctxt, fixed_ip) + db.fixed_ip_update(ctxt, self.test_ip, + {'allocated': True, + 'instance_id': instance_ref['id']}) + + FLAGS.libvirt_type = 'lxc' + self.assertEquals(uri, 'lxc:///') + + xml = conn.to_xml(instance_ref) + tree = xml_to_tree(xml) + + check = [ + (lambda t: t.find('.').get('type'), 'lxc'), + (lambda t: t.find('./os/type').text, 'exe') + ] + + for i (check, expected_result) in enumerate(check): + self.aseertEqual(check(time), + expected_result, + '%s failed common check %d' % (xml, i)) def _check_xml_and_uri(self, instance, expect_ramdisk, expect_kernel, rescue=False): user_context = context.RequestContext(project=self.project, -- cgit From 7f837b1f22922cb968b0ffb42bdb4d56c0d9f3c3 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 07:06:58 -0400 Subject: Update Authors and testsuite --- Authors | 1 + nova/tests/test_virt.py | 41 ++++++++++++++++++++++++----------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/Authors b/Authors index 9aad104a7..12d2f02de 100644 --- a/Authors +++ b/Authors @@ -11,6 +11,7 @@ Chiradeep Vittal Chmouel Boudjnah Chris Behrens Christian Berendt +Chuck Short Cory Wright Dan Prince David Pravec diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index fab05de10..92c80272b 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -228,16 +228,16 @@ class LibvirtConnTestCase(test.TestCase): expect_ramdisk=True, rescue=True) def test_lxc_container_and_uri(self): - instance_data = dict(self.test_instace) + instance_data = dict(self.test_instance) self._check_xml_and_container(instance_data) def _check_xml_and_container(self, instance): user_context = context.RequestContext(project=self.project, user=self.user) - instance_ref = db.instance_create(user_context,instance) + instance_ref = db.instance_create(user_context, instance) host = self.network.get_network_host(user_context.elevated()) - network_ref= db.project_get_network(context.get_admin_context(), - self.project.id) + network_ref = db.project_get_network(context.get_admin_context(), + self.project.id) fixed_ip = {'address': self.test_ip, 'network_id': network_ref['id']} @@ -245,24 +245,28 @@ class LibvirtConnTestCase(test.TestCase): ctxt = context.get_admin_context() fixed_ip_ref = db.fixed_ip_create(ctxt, fixed_ip) db.fixed_ip_update(ctxt, self.test_ip, - {'allocated': True, - 'instance_id': instance_ref['id']}) + {'allocated': True, + 'instance_id': instance_ref['id']}) FLAGS.libvirt_type = 'lxc' + conn = libvirt_conn.LibvirtConnection(True) + + uri = conn.get_uri() self.assertEquals(uri, 'lxc:///') xml = conn.to_xml(instance_ref) tree = xml_to_tree(xml) check = [ - (lambda t: t.find('.').get('type'), 'lxc'), - (lambda t: t.find('./os/type').text, 'exe') + (lambda t: t.find('.').get('type'), 'lxc'), + (lambda t: t.find('./os/type').text, 'exe'), ] - for i (check, expected_result) in enumerate(check): - self.aseertEqual(check(time), + for i, (check, expected_result) in enumerate(check): + self.assertEqual(check(tree), expected_result, '%s failed common check %d' % (xml, i)) + def _check_xml_and_uri(self, instance, expect_ramdisk, expect_kernel, rescue=False): user_context = context.RequestContext(project=self.project, @@ -322,6 +326,7 @@ class LibvirtConnTestCase(test.TestCase): check = (lambda t: t.find('./os/initrd'), None) check_list.append(check) + common_checks = [ (lambda t: t.find('.').tag, 'domain'), (lambda t: t.find( @@ -338,8 +343,9 @@ class LibvirtConnTestCase(test.TestCase): (lambda t: t.find('./devices/serial/source').get( 'path').split('/')[1], 'console.log'), (lambda t: t.find('./memory').text, '2097152')] + if rescue: - common_checks += [ + common_checks = [ (lambda t: t.findall('./devices/disk/source')[0].get( 'file').split('/')[1], 'disk.rescue'), (lambda t: t.findall('./devices/disk/source')[1].get( @@ -362,14 +368,15 @@ class LibvirtConnTestCase(test.TestCase): xml = conn.to_xml(instance_ref, rescue) tree = xml_to_tree(xml) for i, (check, expected_result) in enumerate(checks): - self.assertEqual(check(tree), - expected_result, - '%s failed check %d' % (xml, i)) + self.assertEqual(check(tree), + expected_result, + '%s failed check %d' % (xml, i)) + for i, (check, expected_result) in enumerate(common_checks): - self.assertEqual(check(tree), - expected_result, - '%s failed common check %d' % (xml, i)) + self.assertEqual(check(tree), + expected_result, + '%s failed common check %d' % (xml, i)) # This test is supposed to make sure we don't # override a specifically set uri -- cgit From 7701edd34f1fc9fa26b3dfcc77ff87018622bedc Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 07:13:31 -0400 Subject: get_console_output is not supported by lxc and libvirt --- nova/virt/libvirt_conn.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index d08ca8b6a..9bfd3f841 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -490,6 +490,9 @@ class LibvirtConnection(object): instance['name']) data = self._flush_xen_console(virsh_output) fpath = self._append_to_file(data, console_log) + elif FLAGS.libvirt_type == 'lxc': + # LXC is also special + LOG.info(_("Unable to read LXC console")) else: fpath = console_log -- cgit From 70cd1a51ada85f4724190d2562130172e9495e5e Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 07:53:25 -0400 Subject: Update authors again --- Authors | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Authors b/Authors index 12d2f02de..22a9fb8eb 100644 --- a/Authors +++ b/Authors @@ -11,7 +11,7 @@ Chiradeep Vittal Chmouel Boudjnah Chris Behrens Christian Berendt -Chuck Short +Chuck Short Cory Wright Dan Prince David Pravec -- cgit From 9ba500c304595dff037da296f26cb13d02bfbc04 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 07:57:06 -0400 Subject: Fix pep8 errors --- nova/tests/test_virt.py | 2 -- nova/virt/disk.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 92c80272b..7d50960a3 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -326,7 +326,6 @@ class LibvirtConnTestCase(test.TestCase): check = (lambda t: t.find('./os/initrd'), None) check_list.append(check) - common_checks = [ (lambda t: t.find('.').tag, 'domain'), (lambda t: t.find( @@ -372,7 +371,6 @@ class LibvirtConnTestCase(test.TestCase): expected_result, '%s failed check %d' % (xml, i)) - for i, (check, expected_result) in enumerate(common_checks): self.assertEqual(check(tree), expected_result, diff --git a/nova/virt/disk.py b/nova/virt/disk.py index a44995613..26976940e 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -129,6 +129,7 @@ def setup_container(image, container_dir=None, partition=None, nbd=False): % err) _unlink_device(device, nbd) + def destroy_container(target, instance, nbd=False): """Destroy the container once it terminates -- cgit From bb6096c51cde91dccaad0e9f584f2dc26057da1f Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 08:49:52 -0400 Subject: Update mailmap --- .mailmap | 1 + 1 file changed, 1 insertion(+) diff --git a/.mailmap b/.mailmap index ccf2109a7..78cfef53b 100644 --- a/.mailmap +++ b/.mailmap @@ -10,6 +10,7 @@ + -- cgit From c98cead470f33041e928a6f82be801efeb94ccc3 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 08:52:52 -0400 Subject: Remove nbd=FLAGS.use_cow_images for destroy container --- nova/virt/disk.py | 7 +++---- nova/virt/libvirt_conn.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 26976940e..90d3cf499 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -23,7 +23,6 @@ Includes injection of SSH PGP keys into authorized_keys file. """ import os -import string import tempfile import time @@ -130,7 +129,7 @@ def setup_container(image, container_dir=None, partition=None, nbd=False): _unlink_device(device, nbd) -def destroy_container(target, instance, nbd=False): +def destroy_container(target, instance): """Destroy the container once it terminates It will umount the container that is mounted, try to find the loopback @@ -140,9 +139,9 @@ def destroy_container(target, instance, nbd=False): container_dir = '%s/rootfs' % target utils.execute('sudo', 'umount', container_dir) finally: - for loop in os.popen('sudo losetup -a').readlines(): + for loop in utils.popen('sudo losetup -a').readlines(): if instance['name'] in loop: - device = string.split(loop, ':') + device = loop.split(loop, ':') utils.execute('sudo', 'losetup', '--detach', device) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 9bfd3f841..9e0d0538d 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -275,7 +275,7 @@ class LibvirtConnection(object): LOG.info(_('instance %(instance_name)s: deleting instance files' ' %(target)s') % locals()) if FLAGS.libvirt_type == 'lxc': - disk.destroy_container(target, instance, nbd=FLAGS.use_cow_images) + disk.destroy_container(target, instance) if os.path.exists(target): shutil.rmtree(target) -- cgit From 6bd017262a5c61d915ede2e58ef2758f1f190ff3 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 08:54:05 -0400 Subject: Remove target_partition for setup_container but still hardcode because its needed when you inject the keys into the image. --- nova/virt/libvirt_conn.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 9e0d0538d..0ca27d629 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -700,8 +700,7 @@ class LibvirtConnection(object): if FLAGS.libvirt_type == 'lxc': disk.setup_container(basepath('disk'), container_dir=container_dir, - partition=target_partition, - nbd=FLAGS.use_cow_images) + partition=target_partition) except Exception as e: # This could be a windows image, or a vmdk format disk LOG.warn(_('instance %(inst_name)s: ignoring error injecting' -- cgit From cc716f9648355bc3737dd749a35dc327ebda1e6f Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 09:15:33 -0400 Subject: Fixed typo when I was trying to add test cases for lxc --- nova/tests/test_virt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 7d50960a3..c2b8ba0a1 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -344,7 +344,7 @@ class LibvirtConnTestCase(test.TestCase): (lambda t: t.find('./memory').text, '2097152')] if rescue: - common_checks = [ + common_checks += [ (lambda t: t.findall('./devices/disk/source')[0].get( 'file').split('/')[1], 'disk.rescue'), (lambda t: t.findall('./devices/disk/source')[1].get( -- cgit From 174d8ca2da7e2e53c9105ccc5e5d9a97bc12c0b8 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 09:17:42 -0400 Subject: Set nbd to false when mounting the image --- nova/virt/disk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 90d3cf499..a84425de7 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -121,6 +121,7 @@ def setup_container(image, container_dir=None, partition=None, nbd=False): It will mount the loopback image to the container directory in order to create the root filesystem for the container """ + nbd=False device = _link_device(image, nbd) err = utils.execute('sudo', 'mount', device, container_dir) if err: -- cgit From bc0ef2c7aead759504eedcb4e2ab6d96dba7c266 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 10:02:40 -0400 Subject: Fix up setup container --- nova/virt/disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index a84425de7..2fa7819d7 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -115,7 +115,7 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): _unlink_device(device, nbd) -def setup_container(image, container_dir=None, partition=None, nbd=False): +def setup_container(image, container_dir=None, partition=None): """Setup the LXC container It will mount the loopback image to the container directory in order -- cgit From abb86555f7417225a72126872beb377268acfdb1 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 10:16:37 -0400 Subject: Remove me from mailmap --- .mailmap | 1 - 1 file changed, 1 deletion(-) diff --git a/.mailmap b/.mailmap index 78cfef53b..ccf2109a7 100644 --- a/.mailmap +++ b/.mailmap @@ -10,7 +10,6 @@ - -- cgit From 36285d3acb940c39dc1827699c1e3c0cc9846529 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 10:22:57 -0400 Subject: Fix more pep8 errors --- nova/tests/test_virt.py | 12 ++++++------ nova/virt/disk.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index c2b8ba0a1..222975adc 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -367,14 +367,14 @@ class LibvirtConnTestCase(test.TestCase): xml = conn.to_xml(instance_ref, rescue) tree = xml_to_tree(xml) for i, (check, expected_result) in enumerate(checks): - self.assertEqual(check(tree), - expected_result, - '%s failed check %d' % (xml, i)) + self.assertEqual(check(tree), + expected_result, + '%s failed check %d' % (xml, i)) for i, (check, expected_result) in enumerate(common_checks): - self.assertEqual(check(tree), - expected_result, - '%s failed common check %d' % (xml, i)) + self.assertEqual(check(tree), + expected_result, + '%s failed common check %d' % (xml, i)) # This test is supposed to make sure we don't # override a specifically set uri diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 2fa7819d7..6c5f126bd 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -121,7 +121,7 @@ def setup_container(image, container_dir=None, partition=None): It will mount the loopback image to the container directory in order to create the root filesystem for the container """ - nbd=False + nbd = False device = _link_device(image, nbd) err = utils.execute('sudo', 'mount', device, container_dir) if err: @@ -132,7 +132,7 @@ def setup_container(image, container_dir=None, partition=None): def destroy_container(target, instance): """Destroy the container once it terminates - + It will umount the container that is mounted, try to find the loopback device associated with the container and delete it. """ -- cgit From a06203c66af05c96c161b80511f4a6607ffe4905 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 10:41:55 -0400 Subject: Fix up tests --- nova/tests/test_virt.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 222975adc..fefc11f0d 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -239,8 +239,8 @@ class LibvirtConnTestCase(test.TestCase): network_ref = db.project_get_network(context.get_admin_context(), self.project.id) - fixed_ip = {'address': self.test_ip, - 'network_id': network_ref['id']} + fixed_ip = {'address': self.test_ip, + 'network_id': network_ref['id']} ctxt = context.get_admin_context() fixed_ip_ref = db.fixed_ip_create(ctxt, fixed_ip) @@ -259,13 +259,13 @@ class LibvirtConnTestCase(test.TestCase): check = [ (lambda t: t.find('.').get('type'), 'lxc'), - (lambda t: t.find('./os/type').text, 'exe'), + (lambda t: t.find('./os/type').text, 'exe') ] for i, (check, expected_result) in enumerate(check): - self.assertEqual(check(tree), - expected_result, - '%s failed common check %d' % (xml, i)) + self.assertEqual(check(tree), + expected_result, + '%s failed common check %d' % (xml, i)) def _check_xml_and_uri(self, instance, expect_ramdisk, expect_kernel, rescue=False): @@ -342,7 +342,6 @@ class LibvirtConnTestCase(test.TestCase): (lambda t: t.find('./devices/serial/source').get( 'path').split('/')[1], 'console.log'), (lambda t: t.find('./memory').text, '2097152')] - if rescue: common_checks += [ (lambda t: t.findall('./devices/disk/source')[0].get( -- cgit From dee8a59b5d575a0327464e27115d0d870cde97be Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 10:43:48 -0400 Subject: more pep8 fixes --- nova/tests/test_virt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index fefc11f0d..2510525fc 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -263,9 +263,9 @@ class LibvirtConnTestCase(test.TestCase): ] for i, (check, expected_result) in enumerate(check): - self.assertEqual(check(tree), - expected_result, - '%s failed common check %d' % (xml, i)) + self.assertEqual(check(tree), + expected_result, + '%s failed common check %d' % (xml, i)) def _check_xml_and_uri(self, instance, expect_ramdisk, expect_kernel, rescue=False): -- cgit From 4364a158fd31bdfcfa3ae835a2fd9c0f47d3632f Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 10:45:31 -0400 Subject: pep8 fixes --- nova/tests/test_virt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 2510525fc..45f98fcde 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -259,8 +259,7 @@ class LibvirtConnTestCase(test.TestCase): check = [ (lambda t: t.find('.').get('type'), 'lxc'), - (lambda t: t.find('./os/type').text, 'exe') - ] + (lambda t: t.find('./os/type').text, 'exe')] for i, (check, expected_result) in enumerate(check): self.assertEqual(check(tree), -- cgit From b0e3b8e58a925ebf52fa741883f757ed2bc4383c Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 17 Mar 2011 10:47:19 -0400 Subject: more pep8 fixes --- nova/virt/disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 6c5f126bd..f6e6795d6 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -132,7 +132,7 @@ def setup_container(image, container_dir=None, partition=None): def destroy_container(target, instance): """Destroy the container once it terminates - + It will umount the container that is mounted, try to find the loopback device associated with the container and delete it. """ -- cgit From 00787af795023b6f2104b33b206356442072996e Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 22 Mar 2011 13:25:53 -0700 Subject: add in eventlet version of vnc proxy --- bin/nova-vnc-proxy | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 bin/nova-vnc-proxy diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy new file mode 100644 index 000000000..5f913a82c --- /dev/null +++ b/bin/nova-vnc-proxy @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# pylint: disable-msg=C0103 +# 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. + +"""VNC Console Proxy Server""" + +from base64 import b64encode, b64decode +import eventlet +from eventlet import wsgi +from eventlet import websocket +import os +import random +import sys +import time +from webob import Request + +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + +from nova import flags +from nova import log as logging +from nova import rpc +from nova import utils + +FLAGS = flags.FLAGS +flags.DEFINE_string('vnc_novnc_dir', '/code/noVNC/vnclet/noVNC', + 'Full path to noVNC directory') +flags.DEFINE_boolean('vnc_debug', True, + 'Enable debugging features, like token bypassing') +flags.DEFINE_integer('vnc_proxy_port', 7000, + 'Port that the VNC proxy should bind to') +flags.DEFINE_string('vnc_proxy_address', '0.0.0.0', + 'Address that the VNC proxy should bind to') + + +class WebsocketVNCProxy(object): + """Class to proxy from websocket to vnc server""" + + def sock2ws(self, source, dest): + try: + while True: + d = source.recv(32384) + if d == '': + break + d = b64encode(d) + dest.send(d) + except: + source.close() + dest.close() + + def ws2sock(self, source, dest): + try: + while True: + d = source.wait() + if d is None: + break + d = b64decode(d) + dest.sendall(d) + except: + source.close() + dest.close() + + def proxy_connection(self, environ, start_response): + @websocket.WebSocketWSGI + def _handle(client): + server = eventlet.connect((client.environ['vnc_host'], + client.environ['vnc_port'])) + t1 = eventlet.spawn(self.ws2sock, client, server) + t2 = eventlet.spawn(self.sock2ws, server, client) + t1.wait() + t2.wait() + _handle(environ, start_response) + + def serve(self, environ, start_response): + req = Request(environ) + if req.path == '/data': + return self.proxy_connection(environ, start_response) + else: + if req.path == '/': + fname = '/vnc_auto.html' + else: + fname = req.path + + fname = FLAGS.vnc_novnc_dir + fname + + base, ext = os.path.splitext(fname) + if ext == '.js': + mimetype = 'application/javascript' + elif ext == '.css': + mimetype = 'text/css' + elif ext in ['.svg', '.jpg', '.png', '.gif']: + mimetype = 'image' + else: + mimetype = 'text/html' + + start_response('200 OK', [('content-type', mimetype)]) + return [open(os.path.join(fname)).read()] + + +class DebugAuthMiddleware(object): + """ Debug middleware for testing purposes. Skips security check + and allows host and port of vnc endpoint to be specified in + the url. + """ + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + req = Request(environ) + environ['vnc_host'] = req.params.get('host') + environ['vnc_port'] = int(req.params.get('port')) + resp = req.get_response(self.app) + return resp(environ, start_response) + + +class NovaAuthMiddleware(object): + """Implementation of Middleware to Handle Nova Auth""" + + def __init__(self, app): + self.app = app + self.register_listeners() + + def __call__(self, environ, start_response): + req = Request(environ) + + if req.path == '/data': + token = req.params.get('token') + if not token in self.tokens: + start_response('403 Forbidden', + [('content-type', 'text/html')]) + return 'Not Authorized' + + environ['vnc_host'] = self.tokens[token]['args']['host'] + environ['vnc_port'] = int(self.tokens[token]['args']['port']) + + resp = req.get_response(self.app) + return resp(environ, start_response) + + def register_listeners(self): + middleware = self + middleware.tokens = {} + + class Callback: + def __call__(self, data, message): + if data['method'] == 'authorize_vnc_console': + middleware.tokens[data['args']['token']] = \ + {'args': data['args'], 'last_activity_at': time.time()} + + def delete_expired_tokens(): + now = time.time() + to_delete = [] + for k, v in middleware.tokens.items(): + if now - v['last_activity_at'] > 600: + to_delete.append(k) + + for k in to_delete: + del middleware.tokens[k] + + conn = rpc.Connection.instance(new=True) + consumer = rpc.TopicConsumer( + connection=conn, + topic=FLAGS.vnc_console_proxy_topic) + consumer.register_callback(Callback()) + + utils.LoopingCall(consumer.fetch, auto_ack=True, + enable_callbacks=True).start(0.1) + utils.LoopingCall(delete_expired_tokens).start(1) + + +if __name__ == "__main__": + utils.default_flagfile() + FLAGS(sys.argv) + logging.setup() + + listener = eventlet.listen((FLAGS.vnc_proxy_address, FLAGS.vnc_proxy_port)) + proxy = WebsocketVNCProxy() + + if FLAGS.vnc_debug: + proxy = DebugAuthMiddleware(proxy.serve) + else: + proxy = NovaAuthMiddleware(proxy.serve) + + wsgi.server(listener, proxy, max_size=1000) -- cgit From 4ba57654ca03d687da3b994c127665c7118ab9a5 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 22 Mar 2011 13:26:23 -0700 Subject: intermediate progress on vnc-nova integration. checking in to show vish. --- nova/flags.py | 4 ++++ nova/virt/libvirt.xml.template | 4 +++- nova/virt/libvirt_conn.py | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/nova/flags.py b/nova/flags.py index 4f2be82b6..0360b1e3a 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -287,6 +287,10 @@ DEFINE_string('vnc_console_proxy_url', 'http://127.0.0.1:6080', 'location of vnc console proxy, \ in the form "http://127.0.0.1:6080"') +DEFINE_string('vnc_host_iface', '0.0.0.0', + 'the compute host interface on which vnc server should listen') +DEFINE_bool('vnc_enabled', True, + 'enable vnc related features') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') DEFINE_bool('fake_network', False, diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index 7b4c23211..037cd0902 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -101,6 +101,8 @@ - +#if $getVar('vnc_host_iface', False) + +#end if diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 4fca84639..51f263ce9 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -734,6 +734,8 @@ class LibvirtConnection(object): 'local': instance_type['local_gb'], 'driver_type': driver_type} + if FLAGS.vnc_enabled: + xml_info['vnc_host_iface'] = FLAGS.vnc_host_iface if ra_server: xml_info['ra_server'] = ra_server + "/128" if not rescue: -- cgit From 8048fb9902d80c6a14786f89e672ebff8407d0dd Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 22 Mar 2011 13:28:19 -0700 Subject: make executable --- bin/nova-vnc-proxy | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/nova-vnc-proxy diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy old mode 100644 new mode 100755 -- cgit From 9c75878e5f6f1b90695e725d7bc8e6e9002cabbb Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 23 Mar 2011 01:57:38 -0700 Subject: separating out components of vnc console --- bin/nova-vnc-proxy | 177 ++++++++------------------------------------------- nova/vnc/__init__.py | 0 nova/vnc/auth.py | 83 ++++++++++++++++++++++++ nova/vnc/proxy.py | 111 ++++++++++++++++++++++++++++++++ 4 files changed, 220 insertions(+), 151 deletions(-) create mode 100644 nova/vnc/__init__.py create mode 100644 nova/vnc/auth.py create mode 100644 nova/vnc/proxy.py diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy index 5f913a82c..52e966090 100755 --- a/bin/nova-vnc-proxy +++ b/bin/nova-vnc-proxy @@ -20,15 +20,10 @@ """VNC Console Proxy Server""" -from base64 import b64encode, b64decode import eventlet -from eventlet import wsgi -from eventlet import websocket +import gettext import os -import random import sys -import time -from webob import Request possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), os.pardir, @@ -36,168 +31,48 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) +gettext.install('nova', unicode=1) + from nova import flags from nova import log as logging -from nova import rpc from nova import utils +from nova import wsgi +from nova.vnc import auth +from nova.vnc import proxy FLAGS = flags.FLAGS -flags.DEFINE_string('vnc_novnc_dir', '/code/noVNC/vnclet/noVNC', +flags.DEFINE_string('vnc_proxy_wwwroot', '/code/noVNC/vnclet/noVNC', 'Full path to noVNC directory') -flags.DEFINE_boolean('vnc_debug', True, +flags.DEFINE_boolean('vnc_debug', False, 'Enable debugging features, like token bypassing') flags.DEFINE_integer('vnc_proxy_port', 7000, 'Port that the VNC proxy should bind to') -flags.DEFINE_string('vnc_proxy_address', '0.0.0.0', +flags.DEFINE_string('vnc_proxy_host', '0.0.0.0', 'Address that the VNC proxy should bind to') - - -class WebsocketVNCProxy(object): - """Class to proxy from websocket to vnc server""" - - def sock2ws(self, source, dest): - try: - while True: - d = source.recv(32384) - if d == '': - break - d = b64encode(d) - dest.send(d) - except: - source.close() - dest.close() - - def ws2sock(self, source, dest): - try: - while True: - d = source.wait() - if d is None: - break - d = b64decode(d) - dest.sendall(d) - except: - source.close() - dest.close() - - def proxy_connection(self, environ, start_response): - @websocket.WebSocketWSGI - def _handle(client): - server = eventlet.connect((client.environ['vnc_host'], - client.environ['vnc_port'])) - t1 = eventlet.spawn(self.ws2sock, client, server) - t2 = eventlet.spawn(self.sock2ws, server, client) - t1.wait() - t2.wait() - _handle(environ, start_response) - - def serve(self, environ, start_response): - req = Request(environ) - if req.path == '/data': - return self.proxy_connection(environ, start_response) - else: - if req.path == '/': - fname = '/vnc_auto.html' - else: - fname = req.path - - fname = FLAGS.vnc_novnc_dir + fname - - base, ext = os.path.splitext(fname) - if ext == '.js': - mimetype = 'application/javascript' - elif ext == '.css': - mimetype = 'text/css' - elif ext in ['.svg', '.jpg', '.png', '.gif']: - mimetype = 'image' - else: - mimetype = 'text/html' - - start_response('200 OK', [('content-type', mimetype)]) - return [open(os.path.join(fname)).read()] - - -class DebugAuthMiddleware(object): - """ Debug middleware for testing purposes. Skips security check - and allows host and port of vnc endpoint to be specified in - the url. - """ - - def __init__(self, app): - self.app = app - - def __call__(self, environ, start_response): - req = Request(environ) - environ['vnc_host'] = req.params.get('host') - environ['vnc_port'] = int(req.params.get('port')) - resp = req.get_response(self.app) - return resp(environ, start_response) - - -class NovaAuthMiddleware(object): - """Implementation of Middleware to Handle Nova Auth""" - - def __init__(self, app): - self.app = app - self.register_listeners() - - def __call__(self, environ, start_response): - req = Request(environ) - - if req.path == '/data': - token = req.params.get('token') - if not token in self.tokens: - start_response('403 Forbidden', - [('content-type', 'text/html')]) - return 'Not Authorized' - - environ['vnc_host'] = self.tokens[token]['args']['host'] - environ['vnc_port'] = int(self.tokens[token]['args']['port']) - - resp = req.get_response(self.app) - return resp(environ, start_response) - - def register_listeners(self): - middleware = self - middleware.tokens = {} - - class Callback: - def __call__(self, data, message): - if data['method'] == 'authorize_vnc_console': - middleware.tokens[data['args']['token']] = \ - {'args': data['args'], 'last_activity_at': time.time()} - - def delete_expired_tokens(): - now = time.time() - to_delete = [] - for k, v in middleware.tokens.items(): - if now - v['last_activity_at'] > 600: - to_delete.append(k) - - for k in to_delete: - del middleware.tokens[k] - - conn = rpc.Connection.instance(new=True) - consumer = rpc.TopicConsumer( - connection=conn, - topic=FLAGS.vnc_console_proxy_topic) - consumer.register_callback(Callback()) - - utils.LoopingCall(consumer.fetch, auto_ack=True, - enable_callbacks=True).start(0.1) - utils.LoopingCall(delete_expired_tokens).start(1) - +flags.DEFINE_flag(flags.HelpFlag()) +flags.DEFINE_flag(flags.HelpshortFlag()) +flags.DEFINE_flag(flags.HelpXMLFlag()) if __name__ == "__main__": utils.default_flagfile() FLAGS(sys.argv) logging.setup() - listener = eventlet.listen((FLAGS.vnc_proxy_address, FLAGS.vnc_proxy_port)) - proxy = WebsocketVNCProxy() + app = proxy.WebsocketVNCProxy(FLAGS.vnc_proxy_wwwroot) if FLAGS.vnc_debug: - proxy = DebugAuthMiddleware(proxy.serve) + app = proxy.DebugMiddleware(app.serve) else: - proxy = NovaAuthMiddleware(proxy.serve) + app = auth.NovaAuthMiddleware(app.serve) + + + listener = eventlet.listen((FLAGS.vnc_proxy_host, FLAGS.vnc_proxy_port)) + + + from eventlet import wsgi + wsgi.server(listener, app, max_size=1000) + - wsgi.server(listener, proxy, max_size=1000) +# server = wsgi.Server() +# server.start(app, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_host) +# server.wait() diff --git a/nova/vnc/__init__.py b/nova/vnc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py new file mode 100644 index 000000000..2596bdd24 --- /dev/null +++ b/nova/vnc/auth.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# pylint: disable-msg=C0103 +# 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. + +"""Auth Components for VNC Console""" + +import time +from webob import Request +from nova import flags +from nova import log as logging +from nova import rpc +from nova import utils +from nova import wsgi + + +class NovaAuthMiddleware(object): + """Implementation of Middleware to Handle Nova Auth""" + + def __init__(self, app): + self.app = app + self.register_listeners() + + def __call__(self, environ, start_response): + req = Request(environ) + + if req.path == '/data': + token = req.params.get('token') + if not token in self.tokens: + start_response('403 Forbidden', + [('content-type', 'text/html')]) + return 'Not Authorized' + + environ['vnc_host'] = self.tokens[token]['args']['host'] + environ['vnc_port'] = int(self.tokens[token]['args']['port']) + + resp = req.get_response(self.app) + return resp(environ, start_response) + + def register_listeners(self): + middleware = self + middleware.tokens = {} + + class Callback: + def __call__(self, data, message): + if data['method'] == 'authorize_vnc_console': + middleware.tokens[data['args']['token']] = \ + {'args': data['args'], 'last_activity_at': time.time()} + + def delete_expired_tokens(): + now = time.time() + to_delete = [] + for k, v in middleware.tokens.items(): + if now - v['last_activity_at'] > 600: + to_delete.append(k) + + for k in to_delete: + del middleware.tokens[k] + + conn = rpc.Connection.instance(new=True) + consumer = rpc.TopicConsumer( + connection=conn, + topic=FLAGS.vnc_console_proxy_topic) + consumer.register_callback(Callback()) + + utils.LoopingCall(consumer.fetch, auto_ack=True, + enable_callbacks=True).start(0.1) + utils.LoopingCall(delete_expired_tokens).start(1) diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py new file mode 100644 index 000000000..3f218e744 --- /dev/null +++ b/nova/vnc/proxy.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# pylint: disable-msg=C0103 +# 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. + +"""Eventlet WSGI Services to proxy VNC. No nova deps.""" + +from base64 import b64encode, b64decode +import eventlet +from eventlet import wsgi +from eventlet import websocket +import os +from webob import Request +import webob + + +class WebsocketVNCProxy(object): + """Class to proxy from websocket to vnc server""" + + def __init__(self, wwwroot): + self.wwwroot = wwwroot + + def sock2ws(self, source, dest): + try: + while True: + d = source.recv(32384) + if d == '': + break + d = b64encode(d) + dest.send(d) + except: + source.close() + dest.close() + + def ws2sock(self, source, dest): + try: + while True: + d = source.wait() + if d is None: + break + d = b64decode(d) + dest.sendall(d) + except: + source.close() + dest.close() + + def proxy_connection(self, environ, start_response): + @websocket.WebSocketWSGI + def _handle(client): + server = eventlet.connect((client.environ['vnc_host'], + client.environ['vnc_port'])) + t1 = eventlet.spawn(self.ws2sock, client, server) + t2 = eventlet.spawn(self.sock2ws, server, client) + t1.wait() + t2.wait() + _handle(environ, start_response) + + def serve(self, environ, start_response): + req = Request(environ) + if req.path == '/data': + return self.proxy_connection(environ, start_response) + else: + if req.path == '/': + fname = '/vnc_auto.html' + else: + fname = req.path + + fname = self.wwwroot + fname + + base, ext = os.path.splitext(fname) + if ext == '.js': + mimetype = 'application/javascript' + elif ext == '.css': + mimetype = 'text/css' + elif ext in ['.svg', '.jpg', '.png', '.gif']: + mimetype = 'image' + else: + mimetype = 'text/html' + + start_response('200 OK', [('content-type', mimetype)]) + return open(os.path.join(fname)).read() + + +class DebugMiddleware(object): + """Debug middleware. Skip auth, get vnc port and host from query string""" + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + req = Request(environ) + if req.path == '/data': + environ['vnc_host'] = req.params.get('host') + environ['vnc_port'] = int(req.params.get('port')) + resp = req.get_response(self.app) + return resp(environ, start_response) -- cgit From e2f085eae874012784e53416f6e6213dcfde4859 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 23 Mar 2011 02:06:16 -0700 Subject: use the nova Server object --- bin/nova-vnc-proxy | 18 +++++------------- nova/vnc/proxy.py | 2 +- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy index 52e966090..5891652c4 100755 --- a/bin/nova-vnc-proxy +++ b/bin/nova-vnc-proxy @@ -61,18 +61,10 @@ if __name__ == "__main__": app = proxy.WebsocketVNCProxy(FLAGS.vnc_proxy_wwwroot) if FLAGS.vnc_debug: - app = proxy.DebugMiddleware(app.serve) + app = proxy.DebugMiddleware(app) else: - app = auth.NovaAuthMiddleware(app.serve) + app = auth.NovaAuthMiddleware(app) - - listener = eventlet.listen((FLAGS.vnc_proxy_host, FLAGS.vnc_proxy_port)) - - - from eventlet import wsgi - wsgi.server(listener, app, max_size=1000) - - -# server = wsgi.Server() -# server.start(app, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_host) -# server.wait() + server = wsgi.Server() + server.start(app, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_host) + server.wait() diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py index 3f218e744..5dc83fcb1 100644 --- a/nova/vnc/proxy.py +++ b/nova/vnc/proxy.py @@ -70,7 +70,7 @@ class WebsocketVNCProxy(object): t2.wait() _handle(environ, start_response) - def serve(self, environ, start_response): + def __call__(self, environ, start_response): req = Request(environ) if req.path == '/data': return self.proxy_connection(environ, start_response) -- cgit From 5cdf8f63fb2dbccea0152d17f00bf80352f8fa1a Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 23 Mar 2011 02:33:11 -0700 Subject: more progress --- bin/nova-vnc-proxy | 14 +++++++++++--- nova/vnc/auth.py | 35 +++++++++++++++++++++++++++-------- nova/vnc/proxy.py | 11 +++++------ 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy index 5891652c4..838c871d0 100755 --- a/bin/nova-vnc-proxy +++ b/bin/nova-vnc-proxy @@ -37,9 +37,12 @@ from nova import flags from nova import log as logging from nova import utils from nova import wsgi +from nova import version from nova.vnc import auth from nova.vnc import proxy +LOG = logging.getLogger('nova.vnc-proxy') + FLAGS = flags.FLAGS flags.DEFINE_string('vnc_proxy_wwwroot', '/code/noVNC/vnclet/noVNC', 'Full path to noVNC directory') @@ -58,13 +61,18 @@ if __name__ == "__main__": FLAGS(sys.argv) logging.setup() + LOG.audit(_("Starting nova-vnc-proxy node (version %s)"), + version.version_string_with_vcs()) + app = proxy.WebsocketVNCProxy(FLAGS.vnc_proxy_wwwroot) + with_logging = auth.LoggingMiddleware(app) + if FLAGS.vnc_debug: - app = proxy.DebugMiddleware(app) + with_auth = proxy.DebugMiddleware(with_logging) else: - app = auth.NovaAuthMiddleware(app) + with_auth = auth.NovaAuthMiddleware(with_logging) server = wsgi.Server() - server.start(app, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_host) + server.start(with_auth, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_host) server.wait() diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index 2596bdd24..9b30b08b8 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -27,6 +27,10 @@ from nova import log as logging from nova import rpc from nova import utils from nova import wsgi +import webob + +LOG = logging.getLogger('nova.vnc-proxy') +FLAGS = flags.FLAGS class NovaAuthMiddleware(object): @@ -36,9 +40,8 @@ class NovaAuthMiddleware(object): self.app = app self.register_listeners() - def __call__(self, environ, start_response): - req = Request(environ) - + @webob.dec.wsgify + def __call__(self, req): if req.path == '/data': token = req.params.get('token') if not token in self.tokens: @@ -46,11 +49,10 @@ class NovaAuthMiddleware(object): [('content-type', 'text/html')]) return 'Not Authorized' - environ['vnc_host'] = self.tokens[token]['args']['host'] - environ['vnc_port'] = int(self.tokens[token]['args']['port']) + req.environ['vnc_host'] = self.tokens[token]['args']['host'] + req.environ['vnc_port'] = int(self.tokens[token]['args']['port']) - resp = req.get_response(self.app) - return resp(environ, start_response) + return req.get_response(self.app) def register_listeners(self): middleware = self @@ -59,7 +61,9 @@ class NovaAuthMiddleware(object): class Callback: def __call__(self, data, message): if data['method'] == 'authorize_vnc_console': - middleware.tokens[data['args']['token']] = \ + token = data['args']['token'] + LOG.info(_("Received Token: %s)"), token) + middleware.tokens[token] = \ {'args': data['args'], 'last_activity_at': time.time()} def delete_expired_tokens(): @@ -81,3 +85,18 @@ class NovaAuthMiddleware(object): utils.LoopingCall(consumer.fetch, auto_ack=True, enable_callbacks=True).start(0.1) utils.LoopingCall(delete_expired_tokens).start(1) + + +class LoggingMiddleware(object): + def __init__(self, app): + self.app = app + + @webob.dec.wsgify + def __call__(self, req): + + if req.path == '/data': + LOG.info(_("Received Websocket Request: %s)"), req.url) + else: + LOG.info(_("Received Request: %s)"), req.url) + + return req.get_response(self.app) diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py index 5dc83fcb1..354c2405f 100644 --- a/nova/vnc/proxy.py +++ b/nova/vnc/proxy.py @@ -102,10 +102,9 @@ class DebugMiddleware(object): def __init__(self, app): self.app = app - def __call__(self, environ, start_response): - req = Request(environ) + @webob.dec.wsgify + def __call__(self, req): if req.path == '/data': - environ['vnc_host'] = req.params.get('host') - environ['vnc_port'] = int(req.params.get('port')) - resp = req.get_response(self.app) - return resp(environ, start_response) + req.environ['vnc_host'] = req.params.get('host') + req.environ['vnc_port'] = int(req.params.get('port')) + return req.get_response(self.app) -- cgit From e0289dd26821545a6ef2ca91eb2dba7c11c2cc9f Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 23 Mar 2011 15:53:46 -0700 Subject: general cleanup, use whitelist for webserver security --- bin/nova-vnc-proxy | 22 ++++++++++++++++++---- nova/flags.py | 2 +- nova/virt/libvirt.xml.template | 2 +- nova/virt/libvirt_conn.py | 2 +- nova/vnc/auth.py | 34 ++++++++++++++++++++++------------ nova/vnc/proxy.py | 28 +++++++++++++++++++++++++--- 6 files changed, 68 insertions(+), 22 deletions(-) diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy index 838c871d0..4cd1e9082 100755 --- a/bin/nova-vnc-proxy +++ b/bin/nova-vnc-proxy @@ -44,14 +44,16 @@ from nova.vnc import proxy LOG = logging.getLogger('nova.vnc-proxy') FLAGS = flags.FLAGS -flags.DEFINE_string('vnc_proxy_wwwroot', '/code/noVNC/vnclet/noVNC', +flags.DEFINE_string('vnc_proxy_wwwroot', '/code/noVNC/', 'Full path to noVNC directory') flags.DEFINE_boolean('vnc_debug', False, 'Enable debugging features, like token bypassing') -flags.DEFINE_integer('vnc_proxy_port', 7000, +flags.DEFINE_integer('vnc_proxy_port', 6080, 'Port that the VNC proxy should bind to') -flags.DEFINE_string('vnc_proxy_host', '0.0.0.0', +flags.DEFINE_string('vnc_proxy_iface', '0.0.0.0', 'Address that the VNC proxy should bind to') +flags.DEFINE_integer('vnc_token_ttl', 300, + 'How many seconds before deleting tokens') flags.DEFINE_flag(flags.HelpFlag()) flags.DEFINE_flag(flags.HelpshortFlag()) flags.DEFINE_flag(flags.HelpXMLFlag()) @@ -64,8 +66,20 @@ if __name__ == "__main__": LOG.audit(_("Starting nova-vnc-proxy node (version %s)"), version.version_string_with_vcs()) + if not os.path.exists(FLAGS.vnc_proxy_wwwroot): + LOG.info(_("Missing vnc_proxy_wwwroot (version %s)"), + FLAGS.vnc_proxy_wwwroot) + LOG.info(_("You need a slightly modified version of noVNC " + "to work with the nova-vnc-proxy")) + LOG.info(_("Check out the most recent nova noVNC code here: %s"), + "git://github.com/sleepsonthefloor/noVNC.git") + exit(1) + app = proxy.WebsocketVNCProxy(FLAGS.vnc_proxy_wwwroot) + LOG.audit(_("Allowing access to the following files: %s"), + app.get_whitelist()) + with_logging = auth.LoggingMiddleware(app) if FLAGS.vnc_debug: @@ -74,5 +88,5 @@ if __name__ == "__main__": with_auth = auth.NovaAuthMiddleware(with_logging) server = wsgi.Server() - server.start(with_auth, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_host) + server.start(with_auth, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_iface) server.wait() diff --git a/nova/flags.py b/nova/flags.py index 0360b1e3a..a0ea10795 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -287,7 +287,7 @@ DEFINE_string('vnc_console_proxy_url', 'http://127.0.0.1:6080', 'location of vnc console proxy, \ in the form "http://127.0.0.1:6080"') -DEFINE_string('vnc_host_iface', '0.0.0.0', +DEFINE_string('vnc_compute_host_iface', '0.0.0.0', 'the compute host interface on which vnc server should listen') DEFINE_bool('vnc_enabled', True, 'enable vnc related features') diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index 037cd0902..bcc6b3aed 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -101,7 +101,7 @@ -#if $getVar('vnc_host_iface', False) +#if $getVar('vnc_compute_host_iface', False) #end if diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 51f263ce9..c3529f512 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -735,7 +735,7 @@ class LibvirtConnection(object): 'driver_type': driver_type} if FLAGS.vnc_enabled: - xml_info['vnc_host_iface'] = FLAGS.vnc_host_iface + xml_info['vnc_compute_host_iface'] = FLAGS.vnc_compute_host_iface if ra_server: xml_info['ra_server'] = ra_server + "/128" if not rescue: diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index 9b30b08b8..676cb2360 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -21,13 +21,16 @@ """Auth Components for VNC Console""" import time +import urlparse +import webob from webob import Request + from nova import flags from nova import log as logging from nova import rpc from nova import utils from nova import wsgi -import webob +from nova import vnc LOG = logging.getLogger('nova.vnc-proxy') FLAGS = flags.FLAGS @@ -42,13 +45,19 @@ class NovaAuthMiddleware(object): @webob.dec.wsgify def __call__(self, req): - if req.path == '/data': - token = req.params.get('token') - if not token in self.tokens: - start_response('403 Forbidden', - [('content-type', 'text/html')]) - return 'Not Authorized' + token = req.params.get('token') + + if not token: + referrer = req.environ.get('HTTP_REFERER') + auth_params = urlparse.parse_qs(urlparse.urlparse(referrer).query) + if 'token' in auth_params: + token = auth_params['token'][0] + + if not token in self.tokens: + LOG.audit(_("Unauthorized Access: (%s)"), req.environ) + return webob.exc.HTTPForbidden(detail='Unauthorized') + if req.path == vnc.proxy.WS_ENDPOINT: req.environ['vnc_host'] = self.tokens[token]['args']['host'] req.environ['vnc_port'] = int(self.tokens[token]['args']['port']) @@ -62,7 +71,7 @@ class NovaAuthMiddleware(object): def __call__(self, data, message): if data['method'] == 'authorize_vnc_console': token = data['args']['token'] - LOG.info(_("Received Token: %s)"), token) + LOG.audit(_("Received Token: %s)"), token) middleware.tokens[token] = \ {'args': data['args'], 'last_activity_at': time.time()} @@ -70,10 +79,11 @@ class NovaAuthMiddleware(object): now = time.time() to_delete = [] for k, v in middleware.tokens.items(): - if now - v['last_activity_at'] > 600: + if now - v['last_activity_at'] > FLAGS.vnc_token_ttl: to_delete.append(k) for k in to_delete: + LOG.audit(_("Deleting Token: %s)"), k) del middleware.tokens[k] conn = rpc.Connection.instance(new=True) @@ -94,9 +104,9 @@ class LoggingMiddleware(object): @webob.dec.wsgify def __call__(self, req): - if req.path == '/data': - LOG.info(_("Received Websocket Request: %s)"), req.url) + if req.path == vnc.proxy.WS_ENDPOINT: + LOG.info(_("Received Websocket Request: %s"), req.url) else: - LOG.info(_("Received Request: %s)"), req.url) + LOG.info(_("Received Request: %s"), req.url) return req.get_response(self.app) diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py index 354c2405f..70ebd022a 100644 --- a/nova/vnc/proxy.py +++ b/nova/vnc/proxy.py @@ -28,12 +28,30 @@ import os from webob import Request import webob +WS_ENDPOINT = '/data' + class WebsocketVNCProxy(object): """Class to proxy from websocket to vnc server""" def __init__(self, wwwroot): self.wwwroot = wwwroot + self.whitelist = {} + for root, dirs, files in os.walk(wwwroot): + hidden_dirs = [] + for d in dirs: + if d.startswith('.'): + hidden_dirs.append(d) + for d in hidden_dirs: + dirs.remove(d) + for name in files: + if not str(name).startswith('.'): + filename = os.path.join(root, name) + self.whitelist[filename] = True + + + def get_whitelist(self): + return self.whitelist.keys() def sock2ws(self, source, dest): try: @@ -72,7 +90,7 @@ class WebsocketVNCProxy(object): def __call__(self, environ, start_response): req = Request(environ) - if req.path == '/data': + if req.path == WS_ENDPOINT: return self.proxy_connection(environ, start_response) else: if req.path == '/': @@ -80,7 +98,11 @@ class WebsocketVNCProxy(object): else: fname = req.path - fname = self.wwwroot + fname + fname = (self.wwwroot + fname).replace('//','/') + if not fname in self.whitelist: + start_response('404 Not Found', + [('content-type', 'text/html')]) + return "Not Found" base, ext = os.path.splitext(fname) if ext == '.js': @@ -104,7 +126,7 @@ class DebugMiddleware(object): @webob.dec.wsgify def __call__(self, req): - if req.path == '/data': + if req.path == WS_ENDPOINT: req.environ['vnc_host'] = req.params.get('host') req.environ['vnc_port'] = int(req.params.get('port')) return req.get_response(self.app) -- cgit From 3b381792c2cce1e43f68e39f2fc9c73ba2760024 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 23 Mar 2011 15:55:37 -0700 Subject: clean some pep8 issues --- nova/vnc/proxy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py index 70ebd022a..dea838e3d 100644 --- a/nova/vnc/proxy.py +++ b/nova/vnc/proxy.py @@ -48,7 +48,6 @@ class WebsocketVNCProxy(object): if not str(name).startswith('.'): filename = os.path.join(root, name) self.whitelist[filename] = True - def get_whitelist(self): return self.whitelist.keys() @@ -98,7 +97,7 @@ class WebsocketVNCProxy(object): else: fname = req.path - fname = (self.wwwroot + fname).replace('//','/') + fname = (self.wwwroot + fname).replace('//', '/') if not fname in self.whitelist: start_response('404 Not Found', [('content-type', 'text/html')]) -- cgit From 85ad729e4448bb4211b79e325cef897fc4e2b0bb Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 23 Mar 2011 16:11:50 -0700 Subject: make missing noVNC error condition a bit more fool-proof --- bin/nova-vnc-proxy | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy index 4cd1e9082..ea2533dc3 100755 --- a/bin/nova-vnc-proxy +++ b/bin/nova-vnc-proxy @@ -44,7 +44,7 @@ from nova.vnc import proxy LOG = logging.getLogger('nova.vnc-proxy') FLAGS = flags.FLAGS -flags.DEFINE_string('vnc_proxy_wwwroot', '/code/noVNC/', +flags.DEFINE_string('vnc_proxy_wwwroot', '/var/lib/nova/noVNC/', 'Full path to noVNC directory') flags.DEFINE_boolean('vnc_debug', False, 'Enable debugging features, like token bypassing') @@ -66,13 +66,15 @@ if __name__ == "__main__": LOG.audit(_("Starting nova-vnc-proxy node (version %s)"), version.version_string_with_vcs()) - if not os.path.exists(FLAGS.vnc_proxy_wwwroot): + if not (os.path.exists(FLAGS.vnc_proxy_wwwroot) and + os.path.exists(FLAGS.vnc_proxy_wwwroot + '/vnc_auto.html')): LOG.info(_("Missing vnc_proxy_wwwroot (version %s)"), FLAGS.vnc_proxy_wwwroot) LOG.info(_("You need a slightly modified version of noVNC " - "to work with the nova-vnc-proxy")) - LOG.info(_("Check out the most recent nova noVNC code here: %s"), - "git://github.com/sleepsonthefloor/noVNC.git") + "to work with the nova-vnc-proxy")) + LOG.info(_("Check out the most recent nova noVNC code: %s"), + "git://github.com/sleepsonthefloor/noVNC.git") + LOG.info(_("And drop it in %s"), FLAGS.vnc_proxy_wwwroot) exit(1) app = proxy.WebsocketVNCProxy(FLAGS.vnc_proxy_wwwroot) -- cgit From d83ec9a667f7b9787a6ad9d7af78069f6d0f2cda Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 24 Mar 2011 00:10:28 -0700 Subject: minor tweak from termie feedback --- bin/nova-vnc-proxy | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy index ea2533dc3..e7b647c00 100755 --- a/bin/nova-vnc-proxy +++ b/bin/nova-vnc-proxy @@ -1,5 +1,4 @@ #!/usr/bin/env python -# pylint: disable-msg=C0103 # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the @@ -50,7 +49,7 @@ flags.DEFINE_boolean('vnc_debug', False, 'Enable debugging features, like token bypassing') flags.DEFINE_integer('vnc_proxy_port', 6080, 'Port that the VNC proxy should bind to') -flags.DEFINE_string('vnc_proxy_iface', '0.0.0.0', +flags.DEFINE_string('vnc_proxy_host', '0.0.0.0', 'Address that the VNC proxy should bind to') flags.DEFINE_integer('vnc_token_ttl', 300, 'How many seconds before deleting tokens') @@ -90,5 +89,5 @@ if __name__ == "__main__": with_auth = auth.NovaAuthMiddleware(with_logging) server = wsgi.Server() - server.start(with_auth, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_iface) + server.start(with_auth, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_host) server.wait() -- cgit From 1894937e1ef6769a5f76c0a382931480e2547ce8 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 01:03:41 -0700 Subject: Added volume_attachments --- .bzrignore | 2 + nova/api/openstack/__init__.py | 6 ++ nova/api/openstack/volume_attachments.py | 154 +++++++++++++++++++++++++++++++ nova/api/openstack/volumes.py | 60 ++++++------ 4 files changed, 192 insertions(+), 30 deletions(-) create mode 100644 nova/api/openstack/volume_attachments.py diff --git a/.bzrignore b/.bzrignore index d22b62629..f10df621d 100644 --- a/.bzrignore +++ b/.bzrignore @@ -14,3 +14,5 @@ CA/newcerts/*.pem CA/private/cakey.pem nova/vcsversion.py *.DS_Store +.project +.pydevproject diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 474c1d0e6..af3f8c5ce 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -38,6 +38,7 @@ from nova.api.openstack import servers from nova.api.openstack import shared_ip_groups from nova.api.openstack import users from nova.api.openstack import volumes +from nova.api.openstack import volume_attachments from nova.api.openstack import zones @@ -109,6 +110,11 @@ class APIRouter(wsgi.Router): parent_resource=dict(member_name='server', collection_name='servers')) + mapper.resource("volume_attachment", "volume_attachment", + controller=volume_attachments.Controller(), + parent_resource=dict(member_name='server', + collection_name='servers')) + mapper.resource("console", "consoles", controller=consoles.Controller(), parent_resource=dict(member_name='server', diff --git a/nova/api/openstack/volume_attachments.py b/nova/api/openstack/volume_attachments.py new file mode 100644 index 000000000..fbcec7c29 --- /dev/null +++ b/nova/api/openstack/volume_attachments.py @@ -0,0 +1,154 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from webob import exc + +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + +FLAGS = flags.FLAGS + + +def _translate_detail_view(context, volume): + """ Maps keys for details view""" + + v = _translate_summary_view(context, volume) + + # No additional data / lookups at the moment + + return v + + +def _translate_summary_view(context, volume): + """ Maps keys for summary view""" + v = {} + + volume_id = volume['id'] + + # NOTE(justinsb): We use the volume id as the id of the attachment object + v['id'] = volume_id + + v['volumeId'] = volume_id + v['serverId'] = volume['instance_id'] + v['device'] = volume['mountpoint'] + + return v + + +class Controller(wsgi.Controller): + """ The volume attachment API controller for the Openstack API + + A child resource of the server. Note that we use the volume id + as the ID of the attachment (though this is not guaranteed externally)""" + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'volumeAttachment': [ 'id', + 'serverId', + 'volumeId', + 'device' ]}}} + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(Controller, self).__init__() + + def index(self, req, server_id): + """ Returns the list of volume attachments for a given instance """ + return self._items(req, server_id, + entity_maker=_translate_summary_view) + + def show(self, req, id): + """Return data about the given volume""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_detail_view(context, vol)} + + def create(self, req, server_id): + """ Attach a volume to an instance """ + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + instance_id = server_id + volume_id = env['volumeAttachment']['volumeId'] + device = env['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + vol = self.volume_api.get(context, volume_id) + + retval = _translate_detail_view(context, vol) + + return {'volumeAttachment': retval} + + def update(self, _req, _server_id, _id): + """ Update a volume attachment. We don't currently support this.""" + return faults.Fault(exc.HTTPBadRequest()) + + def delete(self, req, server_id, id): + """ Detach a volume from an instance """ + context = req.environ['nova.context'] + + volume_id = id + LOG.audit(_("Detach volume %s"), volume_id, context=context) + + vol = self.volume_api.get(context, volume_id) + if vol['instance_id'] != server_id: + return faults.Fault(exc.HTTPNotFound()) + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return exc.HTTPAccepted() + + def _items(self, req, server_id, entity_maker): + """Returns a list of attachments, transformed through entity_maker""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + volumes = instance['volumes'] + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumeAttachments': res} diff --git a/nova/api/openstack/volumes.py b/nova/api/openstack/volumes.py index 99300421e..ea2dc4aab 100644 --- a/nova/api/openstack/volumes.py +++ b/nova/api/openstack/volumes.py @@ -29,52 +29,52 @@ LOG = logging.getLogger("nova.api.volumes") FLAGS = flags.FLAGS -def _translate_detail_view(context, inst): +def _translate_detail_view(context, vol): """ Maps keys for details view""" - inst_dict = _translate_summary_view(context, inst) + d = _translate_summary_view(context, vol) # No additional data / lookups at the moment - return inst_dict + return d -def _translate_summary_view(context, volume): +def _translate_summary_view(_context, vol): """ Maps keys for summary view""" - v = {} + d = {} instance_id = None # instance_data = None - attached_to = volume.get('instance') + attached_to = vol.get('instance') if attached_to: instance_id = attached_to['id'] # instance_data = '%s[%s]' % (instance_ec2_id, # attached_to['host']) - v['id'] = volume['id'] - v['status'] = volume['status'] - v['size'] = volume['size'] - v['availabilityZone'] = volume['availability_zone'] - v['createdAt'] = volume['created_at'] + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] # if context.is_admin: # v['status'] = '%s (%s, %s, %s, %s)' % ( - # volume['status'], - # volume['user_id'], - # volume['host'], + # vol['status'], + # vol['user_id'], + # vol['host'], # instance_data, - # volume['mountpoint']) - if volume['attach_status'] == 'attached': - v['attachments'] = [{'attachTime': volume['attach_time'], + # vol['mountpoint']) + if vol['attach_status'] == 'attached': + d['attachments'] = [{'attachTime': vol['attach_time'], 'deleteOnTermination': False, - 'mountpoint': volume['mountpoint'], + 'mountpoint': vol['mountpoint'], 'instanceId': instance_id, 'status': 'attached', - 'volumeId': volume['id']}] + 'volumeId': vol['id']}] else: - v['attachments'] = [{}] + d['attachments'] = [{}] - v['displayName'] = volume['display_name'] - v['displayDescription'] = volume['display_description'] - return v + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d class Controller(wsgi.Controller): @@ -102,11 +102,11 @@ class Controller(wsgi.Controller): context = req.environ['nova.context'] try: - volume = self.volume_api.get(context, id) + vol = self.volume_api.get(context, id) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) - return {'volume': _translate_detail_view(context, volume)} + return {'volume': _translate_detail_view(context, vol)} def delete(self, req, id): """ Delete a volume """ @@ -134,7 +134,7 @@ class Controller(wsgi.Controller): volumes = self.volume_api.get_all(context) limited_list = common.limited(volumes, req) - res = [entity_maker(context, inst) for inst in limited_list] + res = [entity_maker(context, vol) for vol in limited_list] return {'volumes': res} def create(self, req): @@ -148,13 +148,13 @@ class Controller(wsgi.Controller): vol = env['volume'] size = vol['size'] LOG.audit(_("Create volume of %s GB"), size, context=context) - volume = self.volume_api.create(context, size, - vol.get('display_name'), - vol.get('display_description')) + new_volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) # Work around problem that instance is lazy-loaded... volume['instance'] = None - retval = _translate_detail_view(context, volume) + retval = _translate_detail_view(context, new_volume) return {'volume': retval} -- cgit From 694c2cfd2afdc0ed293f205890bda977968dc079 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 01:13:20 -0700 Subject: Created simple test case for server creation, so that we can have something to attach to... --- .bzrignore | 3 + nova/image/fake.py | 109 ++++++++++++++ nova/tests/integrated/integrated_helpers.py | 22 +++ nova/tests/integrated/test_servers.py | 218 ++++++++++++++++++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 nova/image/fake.py create mode 100644 nova/tests/integrated/test_servers.py diff --git a/.bzrignore b/.bzrignore index f10df621d..b751ad825 100644 --- a/.bzrignore +++ b/.bzrignore @@ -16,3 +16,6 @@ nova/vcsversion.py *.DS_Store .project .pydevproject +clean.sqlite +run_tests.log +tests.sqlite diff --git a/nova/image/fake.py b/nova/image/fake.py new file mode 100644 index 000000000..bc63780d3 --- /dev/null +++ b/nova/image/fake.py @@ -0,0 +1,109 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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. +"""Implementation of an fake image service""" + +from nova import exception +from nova import flags +from nova import log as logging +from nova.image import service + + +LOG = logging.getLogger('nova.image.fake') + +FLAGS = flags.FLAGS + + +class MockImageService(service.BaseImageService): + """Mock (fake) image service for unit testing""" + + def __init__(self): + self.images = {} + # NOTE(justinsb): The OpenStack API can't upload an image??? + # So, make sure we've got one.. + image = {'id': '123456', + 'status': 'active', + 'type': 'machine', + 'disk_format': 'ami', + 'properties': {'kernel_id': FLAGS.null_kernel, + 'ramdisk_id': FLAGS.null_kernel, + } + } + self.create(None, image) + super(MockImageService, self).__init__() + + def index(self, context): + """Returns list of images""" + return self.images.values() + + def detail(self, context): + """Return list of detailed image information""" + return self.images.values() + + def show(self, context, image_id): + """ + Returns a dict containing image data for the given opaque image id. + """ + image_id = int(image_id) + image = self.images.get(image_id) + if image: + return image + LOG.warn("Unable to find image id %s. Have images: %s", + image_id, self.images) + raise exception.NotFound + + def create(self, context, data): + """ + Store the image data and return the new image id. + + :raises AlreadyExists if the image already exist. + + """ + image_id = int(data['id']) + if self.images.get(image_id): + #TODO(justinsb): Where is this AlreadyExists exception?? + raise exception.Error("AlreadyExists") + + self.images[image_id] = data + + def update(self, context, image_id, data): + """Replace the contents of the given image with the new data. + + :raises NotFound if the image does not exist. + + """ + image_id = int(image_id) + if not self.images.get(image_id): + raise exception.NotFound + self.images[image_id] = data + + def delete(self, context, image_id): + """ + Delete the given image. + + :raises NotFound if the image does not exist. + + """ + image_id = int(image_id) + removed = self.images.pop(image_id, None) + if not removed: + raise exception.NotFound + + def delete_all(self): + """ + Clears out all images + """ + self.images.clear() diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index 47093636e..dc6897e08 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -73,6 +73,28 @@ class TestUser(object): self.secret, self.auth_url) + def get_unused_server_name(self): + servers = self.openstack_api.get_servers() + server_names = [server['name'] for server in servers] + return generate_new_element(server_names, 'server') + + def get_invalid_image(self): + images = self.openstack_api.get_images() + image_ids = [image['id'] for image in images] + return generate_new_element(image_ids, '', numeric=True) + + def get_valid_image(self, create=False): + images = self.openstack_api.get_images() + if create and not images: + # TODO(justinsb): No way to create an image through API??? + #created_image = self.openstack_api.post_image(image) + #images.append(created_image) + raise exception.Error("No way to create an image through API??") + + if images: + return images[0] + return None + class IntegratedUnitTestContext(object): __INSTANCE = None diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py new file mode 100644 index 000000000..3c38295c5 --- /dev/null +++ b/nova/tests/integrated/test_servers.py @@ -0,0 +1,218 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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 time +import unittest + +from nova import flags +from nova import test +from nova.log import logging +from nova.tests.integrated import integrated_helpers +from nova.tests.integrated.api import client + + +LOG = logging.getLogger('nova.tests.integrated') + +FLAGS = flags.FLAGS +FLAGS.verbose = True + + +class ServersTest(test.TestCase): + def setUp(self): + super(ServersTest, self).setUp() + + self.flags(image_service='nova.image.fake.MockImageService') + + context = integrated_helpers.IntegratedUnitTestContext.startup() + self.user = context.test_user + self.api = self.user.openstack_api + + def tearDown(self): + integrated_helpers.IntegratedUnitTestContext.shutdown() + super(ServersTest, self).tearDown() + + def test_get_servers(self): + """Simple check that listing servers works.""" + servers = self.api.get_servers() + for server in servers: + LOG.debug("server: %s" % server) + + def test_create_and_delete_server(self): + """Creates and deletes a server""" + + # Create server + + # Build the server data gradually, checking errors along the way + server = {} + good_server = self._build_minimal_create_server_request() + + post = {'server': server} + + # Without an imageId, this throws 500. + # TODO(justinsb): Check whatever the spec says should be thrown here + self.assertRaises(client.OpenStackApiException, + self.api.post_server, post) + + # With an invalid imageId, this throws 500. + server['imageId'] = self.user.get_invalid_image() + # TODO(justinsb): Check whatever the spec says should be thrown here + self.assertRaises(client.OpenStackApiException, + self.api.post_server, post) + + # Add a valid imageId + server['imageId'] = good_server['imageId'] + + # Without flavorId, this throws 500 + # TODO(justinsb): Check whatever the spec says should be thrown here + self.assertRaises(client.OpenStackApiException, + self.api.post_server, post) + + # Set a valid flavorId + server['flavorId'] = good_server['flavorId'] + + # Without a name, this throws 500 + # TODO(justinsb): Check whatever the spec says should be thrown here + self.assertRaises(client.OpenStackApiException, + self.api.post_server, post) + + # Set a valid server name + server['name'] = good_server['name'] + + created_server = self.api.post_server(post) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # Check it's there + found_server = self.api.get_server(created_server_id) + self.assertEqual(created_server_id, found_server['id']) + + # It should also be in the all-servers list + servers = self.api.get_servers() + server_ids = [server['id'] for server in servers] + self.assertTrue(created_server_id in server_ids) + + # Wait (briefly) for creation + retries = 0 + while found_server['status'] == 'build': + LOG.debug("found server: %s" % found_server) + time.sleep(1) + found_server = self.api.get_server(created_server_id) + retries = retries + 1 + if retries > 5: + break + + # It should be available... + # TODO(justinsb): Mock doesn't yet do this... + #self.assertEqual('available', found_server['status']) + + self._delete_server(created_server_id) + + def _delete_server(self, server_id): + # Delete the server + self.api.delete_server(server_id) + + # Wait (briefly) for deletion + for _retries in range(5): + try: + found_server = self.api.get_server(server_id) + except client.OpenStackApiNotFoundException: + found_server = None + LOG.debug("Got 404, proceeding") + break + + LOG.debug("Found_server=%s" % found_server) + + # TODO(justinsb): Mock doesn't yet do accurate state changes + #if found_server['status'] != 'deleting': + # break + time.sleep(1) + + # Should be gone + self.assertFalse(found_server) + + def _build_minimal_create_server_request(self): + server = {} + + image = self.user.get_valid_image(create=True) + image_id = image['id'] + + #TODO(justinsb): This is FUBAR + image_id = abs(hash(image_id)) + + # We now have a valid imageId + server['imageId'] = image_id + + # Set a valid flavorId + flavor = self.api.get_flavors()[0] + LOG.debug("Using flavor: %s" % flavor) + server['flavorId'] = flavor['id'] + + # Set a valid server name + server_name = self.user.get_unused_server_name() + server['name'] = server_name + + return server + +# TODO(justinsb): Enable this unit test when the metadata bug is fixed +# def test_create_server_with_metadata(self): +# """Creates a server with metadata""" +# +# # Build the server data gradually, checking errors along the way +# server = self._build_minimal_create_server_request() +# +# for metadata_count in range(30): +# metadata = {} +# for i in range(metadata_count): +# metadata['key_%s' % i] = 'value_%s' % i +# server['metadata'] = metadata +# +# post = {'server': server} +# created_server = self.api.post_server(post) +# LOG.debug("created_server: %s" % created_server) +# self.assertTrue(created_server['id']) +# created_server_id = created_server['id'] +# # Reenable when bug fixed +# # self.assertEqual(metadata, created_server.get('metadata')) +# +# # Check it's there +# found_server = self.api.get_server(created_server_id) +# self.assertEqual(created_server_id, found_server['id']) +# self.assertEqual(metadata, found_server.get('metadata')) +# +# # The server should also be in the all-servers details list +# servers = self.api.get_servers(detail=True) +# server_map = dict((server['id'], server) for server in servers) +# found_server = server_map.get(created_server_id) +# self.assertTrue(found_server) +# # Details do include metadata +# self.assertEqual(metadata, found_server.get('metadata')) +# +# # The server should also be in the all-servers summary list +# servers = self.api.get_servers(detail=False) +# server_map = dict((server['id'], server) for server in servers) +# found_server = server_map.get(created_server_id) +# self.assertTrue(found_server) +# # Summary should not include metadata +# self.assertFalse(found_server.get('metadata')) +# +# # Cleanup +# self._delete_server(created_server_id) + + +if __name__ == "__main__": + unittest.main() -- cgit From 699adb4311fdd86525fae022f4119401fd1c0168 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 01:37:14 -0700 Subject: Added simple nova volume tests --- nova/api/openstack/volume_attachments.py | 2 +- nova/api/openstack/volumes.py | 4 +- nova/tests/integrated/api/client.py | 15 +++ nova/tests/integrated/integrated_helpers.py | 17 +++- nova/tests/integrated/test_volumes.py | 137 ++++++++++++++++++++++++++++ nova/volume/driver.py | 67 ++++++++++++++ 6 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 nova/tests/integrated/test_volumes.py diff --git a/nova/api/openstack/volume_attachments.py b/nova/api/openstack/volume_attachments.py index fbcec7c29..1cb2c9494 100644 --- a/nova/api/openstack/volume_attachments.py +++ b/nova/api/openstack/volume_attachments.py @@ -97,7 +97,7 @@ class Controller(wsgi.Controller): """ Attach a volume to an instance """ context = req.environ['nova.context'] - env = self._deserialize(req.body, req) + env = self._deserialize(req.body, req.get_content_type()) if not env: return faults.Fault(exc.HTTPUnprocessableEntity()) diff --git a/nova/api/openstack/volumes.py b/nova/api/openstack/volumes.py index ea2dc4aab..ec3b9a6c8 100644 --- a/nova/api/openstack/volumes.py +++ b/nova/api/openstack/volumes.py @@ -141,7 +141,7 @@ class Controller(wsgi.Controller): """Creates a new volume""" context = req.environ['nova.context'] - env = self._deserialize(req.body, req) + env = self._deserialize(req.body, req.get_content_type()) if not env: return faults.Fault(exc.HTTPUnprocessableEntity()) @@ -153,7 +153,7 @@ class Controller(wsgi.Controller): vol.get('display_description')) # Work around problem that instance is lazy-loaded... - volume['instance'] = None + new_volume['instance'] = None retval = _translate_detail_view(context, new_volume) diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index fc7c344e7..7a4c3198e 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -90,6 +90,7 @@ class TestOpenStackClient(object): LOG.info(_("Doing %(method)s on %(relative_url)s") % locals()) if body: LOG.info(_("Body: %s") % body) + headers.setdefault('Content-Type', 'application/json') conn.request(method, relative_url, body, headers) response = conn.getresponse() @@ -208,3 +209,17 @@ class TestOpenStackClient(object): def delete_flavor(self, flavor_id): return self.api_delete('/flavors/%s' % flavor_id) + + def get_volume(self, volume_id): + return self.api_get('/volumes/%s' % volume_id)['volume'] + + def get_volumes(self, detail=True): + rel_url = '/volumes/detail' if detail else '/volumes' + return self.api_get(rel_url)['volumes'] + + def post_volume(self, volume): + return self.api_post('/volumes', volume)['volume'] + + def delete_volume(self, volume_id): + return self.api_delete('/volumes/%s' % volume_id) + diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index dc6897e08..f24759032 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -58,7 +58,7 @@ def generate_new_element(items, prefix, numeric=False): candidate = prefix + generate_random_alphanumeric(8) if not candidate in items: return candidate - print "Random collision on %s" % candidate + LOG.debug("Random collision on %s" % candidate) class TestUser(object): @@ -125,11 +125,26 @@ class IntegratedUnitTestContext(object): self._configure_project(self.project_name, self.test_user) def _start_services(self): + self._start_volume_service() + self._start_scheduler_service() + # WSGI shutdown broken :-( # bug731668 if not self.api_service: self._start_api_service() + def _start_volume_service(self): + volume_service = service.Service.create(binary='nova-volume') + volume_service.start() + self.services.append(volume_service) + return volume_service + + def _start_scheduler_service(self): + scheduler_service = service.Service.create(binary='nova-scheduler') + scheduler_service.start() + self.services.append(scheduler_service) + return scheduler_service + def cleanup(self): for service in self.services: service.kill() diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py new file mode 100644 index 000000000..66b773db2 --- /dev/null +++ b/nova/tests/integrated/test_volumes.py @@ -0,0 +1,137 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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 +import time + +from nova import flags +from nova import test +from nova.log import logging +from nova.tests.integrated import integrated_helpers +from nova.tests.integrated.api import client +from nova.volume import driver + + +LOG = logging.getLogger('nova.tests.integrated') + +FLAGS = flags.FLAGS +FLAGS.verbose = True + + +class VolumesTest(test.TestCase): + def setUp(self): + super(VolumesTest, self).setUp() + + self.flags(image_service='nova.image.fake.MockImageService', + volume_driver='nova.volume.driver.LoggingVolumeDriver') + + context = integrated_helpers.IntegratedUnitTestContext.startup() + self.user = context.test_user + self.api = self.user.openstack_api + + def tearDown(self): + integrated_helpers.IntegratedUnitTestContext.shutdown() + super(VolumesTest, self).tearDown() + + def test_get_volumes(self): + """Simple check that listing volumes works""" + volumes = self.api.get_volumes() + for volume in volumes: + LOG.debug("volume: %s" % volume) + + def test_create_and_delete_volume(self): + """Creates and deletes a volume""" + + # Create volume with name + created_volume = self.api.post_volume({'volume': {'size': 1}}) + LOG.debug("created_volume: %s" % created_volume) + self.assertTrue(created_volume['id']) + created_volume_id = created_volume['id'] + + # Check it's there + found_volume = self.api.get_volume(created_volume_id) + self.assertEqual(created_volume_id, found_volume['id']) + + # It should also be in the all-volume list + volumes = self.api.get_volumes() + volume_names = [volume['id'] for volume in volumes] + self.assertTrue(created_volume_id in volume_names) + + # Wait (briefly) for creation. Delay is due to the 'message queue' + retries = 0 + while found_volume['status'] == 'creating': + LOG.debug("Found %s" % found_volume) + time.sleep(1) + found_volume = self.api.get_volume(created_volume_id) + retries = retries + 1 + if retries > 5: + break + + # It should be available... + self.assertEqual('available', found_volume['status']) + + # Delete the volume + self.api.delete_volume(created_volume_id) + + # Wait (briefly) for deletion. Delay is due to the 'message queue' + for retries in range(5): + try: + found_volume = self.api.get_volume(created_volume_id) + except client.OpenStackApiNotFoundException: + found_volume = None + LOG.debug("Got 404, proceeding") + break + + LOG.debug("Found_volume=%s" % found_volume) + if found_volume['status'] != 'deleting': + break + time.sleep(1) + + # Should be gone + self.assertFalse(found_volume) + + LOG.debug("Logs: %s" % driver.LoggingVolumeDriver.all_logs()) + + create_actions = driver.LoggingVolumeDriver.logs_like( + 'create_volume', + id=created_volume_id) + LOG.debug("Create_Actions: %s" % create_actions) + + self.assertEquals(1, len(create_actions)) + create_action = create_actions[0] + self.assertEquals(create_action['id'], created_volume_id) + self.assertEquals(create_action['availability_zone'], 'nova') + self.assertEquals(create_action['size'], 1) + + export_actions = driver.LoggingVolumeDriver.logs_like( + 'create_export', + id=created_volume_id) + self.assertEquals(1, len(export_actions)) + export_action = export_actions[0] + self.assertEquals(export_action['id'], created_volume_id) + self.assertEquals(export_action['availability_zone'], 'nova') + + delete_actions = driver.LoggingVolumeDriver.logs_like( + 'delete_volume', + id=created_volume_id) + self.assertEquals(1, len(delete_actions)) + delete_action = export_actions[0] + self.assertEquals(delete_action['id'], created_volume_id) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 779b46755..148e5facd 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -629,3 +629,70 @@ class SheepdogDriver(VolumeDriver): def undiscover_volume(self, volume): """Undiscover volume on a remote host""" pass + + +class LoggingVolumeDriver(VolumeDriver): + """Logs and records calls, for unit tests.""" + + def check_for_setup_error(self): + pass + + def create_volume(self, volume): + self.log_action('create_volume', volume) + + def delete_volume(self, volume): + self.log_action('delete_volume', volume) + + def local_path(self, volume): + print "local_path not implemented" + raise NotImplementedError() + + def ensure_export(self, context, volume): + self.log_action('ensure_export', volume) + + def create_export(self, context, volume): + self.log_action('create_export', volume) + + def remove_export(self, context, volume): + self.log_action('remove_export', volume) + + def discover_volume(self, volume): + self.log_action('discover_volume', volume) + + def undiscover_volume(self, volume): + self.log_action('undiscover_volume', volume) + + def check_for_export(self, context, volume_id): + self.log_action('check_for_export', volume_id) + + _LOGS = [] + + @staticmethod + def log_action(action, parameters): + """Logs the command.""" + LOG.debug(_("LoggingVolumeDriver: %s") % (action)) + log_dictionary = {} + if parameters: + log_dictionary = dict(parameters) + log_dictionary['action'] = action + LOG.debug(_("LoggingVolumeDriver: %s") % (log_dictionary)) + LoggingVolumeDriver._LOGS.append(log_dictionary) + + @staticmethod + def all_logs(): + return LoggingVolumeDriver._LOGS + + @staticmethod + def logs_like(action, **kwargs): + matches = [] + for entry in LoggingVolumeDriver._LOGS: + if entry['action'] != action: + continue + match = True + for k, v in kwargs.iteritems(): + if entry.get(k) != v: + match = False + break + if match: + matches.append(entry) + return matches -- cgit From 230d07e9002371bdb0030c9199df35fc6360a0a2 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 03:26:32 -0700 Subject: Test for attach / detach (and associated fixes) --- nova/api/openstack/__init__.py | 2 +- nova/api/openstack/volume_attachments.py | 77 ++++++---- nova/tests/integrated/api/client.py | 15 ++ nova/tests/integrated/integrated_helpers.py | 49 +++++++ nova/tests/integrated/test_login.py | 12 +- nova/tests/integrated/test_servers.py | 37 +---- nova/tests/integrated/test_volumes.py | 215 +++++++++++++++++++++++----- nova/volume/driver.py | 12 +- 8 files changed, 312 insertions(+), 107 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 54d8a738d..e8aa4821b 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -134,7 +134,7 @@ class APIRouter(wsgi.Router): controller=volumes.Controller(), collection={'detail': 'GET'}) - mapper.resource("volume_attachment", "volume_attachment", + mapper.resource("volume_attachment", "volume_attachments", controller=volume_attachments.Controller(), parent_resource=dict(member_name='server', collection_name='servers')) diff --git a/nova/api/openstack/volume_attachments.py b/nova/api/openstack/volume_attachments.py index 1cb2c9494..2ce681e19 100644 --- a/nova/api/openstack/volume_attachments.py +++ b/nova/api/openstack/volume_attachments.py @@ -35,27 +35,29 @@ FLAGS = flags.FLAGS def _translate_detail_view(context, volume): """ Maps keys for details view""" - v = _translate_summary_view(context, volume) + d = _translate_summary_view(context, volume) # No additional data / lookups at the moment - return v + return d -def _translate_summary_view(context, volume): +def _translate_summary_view(context, vol): """ Maps keys for summary view""" - v = {} + d = {} + + volume_id = vol['id'] - volume_id = volume['id'] - # NOTE(justinsb): We use the volume id as the id of the attachment object - v['id'] = volume_id - - v['volumeId'] = volume_id - v['serverId'] = volume['instance_id'] - v['device'] = volume['mountpoint'] + d['id'] = volume_id - return v + d['volumeId'] = volume_id + if vol.get('instance_id'): + d['serverId'] = vol['instance_id'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d class Controller(wsgi.Controller): @@ -82,16 +84,22 @@ class Controller(wsgi.Controller): return self._items(req, server_id, entity_maker=_translate_summary_view) - def show(self, req, id): + def show(self, req, server_id, id): """Return data about the given volume""" context = req.environ['nova.context'] + volume_id = id try: - vol = self.volume_api.get(context, id) + vol = self.volume_api.get(context, volume_id) except exception.NotFound: + LOG.debug("volume_id not found") return faults.Fault(exc.HTTPNotFound()) - return {'volume': _translate_detail_view(context, vol)} + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + return {'volumeAttachment': _translate_detail_view(context, vol)} def create(self, req, server_id): """ Attach a volume to an instance """ @@ -109,15 +117,29 @@ class Controller(wsgi.Controller): " at %(device)s") % locals() LOG.audit(msg, context=context) - self.compute_api.attach_volume(context, - instance_id=instance_id, - volume_id=volume_id, - device=device) - vol = self.volume_api.get(context, volume_id) + try: + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + # The attach is async + attachment = {} + attachment['id'] = volume_id + attachment['volumeId'] = volume_id - retval = _translate_detail_view(context, vol) + # NOTE(justinsb): And now, we have a problem... + # The attach is async, so there's a window in which we don't see + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. + # For now, we'll probably have to rely on libraries being smart - return {'volumeAttachment': retval} + # TODO: How do I return "accepted" here?? + return {'volumeAttachment': attachment} def update(self, _req, _server_id, _id): """ Update a volume attachment. We don't currently support this.""" @@ -130,10 +152,15 @@ class Controller(wsgi.Controller): volume_id = id LOG.audit(_("Detach volume %s"), volume_id, context=context) - vol = self.volume_api.get(context, volume_id) - if vol['instance_id'] != server_id: + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") return faults.Fault(exc.HTTPNotFound()) - + self.compute_api.detach_volume(context, volume_id=volume_id) diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index 7a4c3198e..deb7fd981 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -223,3 +223,18 @@ class TestOpenStackClient(object): def delete_volume(self, volume_id): return self.api_delete('/volumes/%s' % volume_id) + def get_server_volume(self, server_id, attachment_id): + return self.api_get('/servers/%s/volume_attachments/%s' % + (server_id, attachment_id))['volumeAttachment'] + + def get_server_volumes(self, server_id): + return self.api_get('/servers/%s/volume_attachments' % + (server_id))['volumeAttachments'] + + def post_server_volume(self, server_id, volume_attachment): + return self.api_post('/servers/%s/volume_attachments' % + (server_id), volume_attachment)['volumeAttachment'] + + def delete_server_volume(self, server_id, attachment_id): + return self.api_delete('/servers/%s/volume_attachments/%s' % + (server_id, attachment_id)) diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index f24759032..b520cc5d3 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -125,6 +125,7 @@ class IntegratedUnitTestContext(object): self._configure_project(self.project_name, self.test_user) def _start_services(self): + self._start_compute_service() self._start_volume_service() self._start_scheduler_service() @@ -133,6 +134,12 @@ class IntegratedUnitTestContext(object): if not self.api_service: self._start_api_service() + def _start_compute_service(self): + compute_service = service.Service.create(binary='nova-compute') + compute_service.start() + self.services.append(compute_service) + return compute_service + def _start_volume_service(self): volume_service = service.Service.create(binary='nova-volume') volume_service.start() @@ -223,3 +230,45 @@ class IntegratedUnitTestContext(object): # WSGI shutdown broken :-( # bug731668 #IntegratedUnitTestContext.__INSTANCE = None + + +class _IntegratedTestBase(test.TestCase): + def setUp(self): + super(_IntegratedTestBase, self).setUp() + + self._setup_flags() + + context = IntegratedUnitTestContext.startup() + self.user = context.test_user + self.api = self.user.openstack_api + + def tearDown(self): + IntegratedUnitTestContext.shutdown() + super(_IntegratedTestBase, self).tearDown() + + def _setup_flags(self): + """An opportunity to setup flags, before the services are started""" + pass + + def _build_minimal_create_server_request(self): + server = {} + + image = self.user.get_valid_image(create=True) + image_id = image['id'] + + #TODO(justinsb): This is FUBAR + image_id = abs(hash(image_id)) + + # We now have a valid imageId + server['imageId'] = image_id + + # Set a valid flavorId + flavor = self.api.get_flavors()[0] + LOG.debug("Using flavor: %s" % flavor) + server['flavorId'] = flavor['id'] + + # Set a valid server name + server_name = self.user.get_unused_server_name() + server['name'] = server_name + + return server diff --git a/nova/tests/integrated/test_login.py b/nova/tests/integrated/test_login.py index 501f8c919..cc3d555d0 100644 --- a/nova/tests/integrated/test_login.py +++ b/nova/tests/integrated/test_login.py @@ -30,17 +30,7 @@ FLAGS = flags.FLAGS FLAGS.verbose = True -class LoginTest(test.TestCase): - def setUp(self): - super(LoginTest, self).setUp() - context = integrated_helpers.IntegratedUnitTestContext.startup() - self.user = context.test_user - self.api = self.user.openstack_api - - def tearDown(self): - integrated_helpers.IntegratedUnitTestContext.shutdown() - super(LoginTest, self).tearDown() - +class LoginTest(integrated_helpers._IntegratedTestBase): def test_login(self): """Simple check - we list flavors - so we know we're logged in""" flavors = self.api.get_flavors() diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index 3c38295c5..2b5d3324a 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -31,20 +31,10 @@ FLAGS = flags.FLAGS FLAGS.verbose = True -class ServersTest(test.TestCase): - def setUp(self): - super(ServersTest, self).setUp() - +class ServersTest(integrated_helpers._IntegratedTestBase): + def _setup_flags(self): self.flags(image_service='nova.image.fake.MockImageService') - context = integrated_helpers.IntegratedUnitTestContext.startup() - self.user = context.test_user - self.api = self.user.openstack_api - - def tearDown(self): - integrated_helpers.IntegratedUnitTestContext.shutdown() - super(ServersTest, self).tearDown() - def test_get_servers(self): """Simple check that listing servers works.""" servers = self.api.get_servers() @@ -145,29 +135,6 @@ class ServersTest(test.TestCase): # Should be gone self.assertFalse(found_server) - def _build_minimal_create_server_request(self): - server = {} - - image = self.user.get_valid_image(create=True) - image_id = image['id'] - - #TODO(justinsb): This is FUBAR - image_id = abs(hash(image_id)) - - # We now have a valid imageId - server['imageId'] = image_id - - # Set a valid flavorId - flavor = self.api.get_flavors()[0] - LOG.debug("Using flavor: %s" % flavor) - server['flavorId'] = flavor['id'] - - # Set a valid server name - server_name = self.user.get_unused_server_name() - server['name'] = server_name - - return server - # TODO(justinsb): Enable this unit test when the metadata bug is fixed # def test_create_server_with_metadata(self): # """Creates a server with metadata""" diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py index 66b773db2..f69361fb0 100644 --- a/nova/tests/integrated/test_volumes.py +++ b/nova/tests/integrated/test_volumes.py @@ -32,20 +32,15 @@ FLAGS = flags.FLAGS FLAGS.verbose = True -class VolumesTest(test.TestCase): +class VolumesTest(integrated_helpers._IntegratedTestBase): def setUp(self): super(VolumesTest, self).setUp() + driver.LoggingVolumeDriver.clear_logs() + def _setup_flags(self): self.flags(image_service='nova.image.fake.MockImageService', volume_driver='nova.volume.driver.LoggingVolumeDriver') - - context = integrated_helpers.IntegratedUnitTestContext.startup() - self.user = context.test_user - self.api = self.user.openstack_api - - def tearDown(self): - integrated_helpers.IntegratedUnitTestContext.shutdown() - super(VolumesTest, self).tearDown() + self.flags(use_local_volumes=False) # Avoids calling local_path def test_get_volumes(self): """Simple check that listing volumes works""" @@ -53,10 +48,34 @@ class VolumesTest(test.TestCase): for volume in volumes: LOG.debug("volume: %s" % volume) + def _poll_while(self, volume_id, continue_states, max_retries=5): + """ Poll (briefly) while the state is in continue_states""" + retries = 0 + while True: + try: + found_volume = self.api.get_volume(volume_id) + except client.OpenStackApiNotFoundException: + found_volume = None + LOG.debug("Got 404, proceeding") + break + + LOG.debug("Found %s" % found_volume) + + self.assertEqual(volume_id, found_volume['id']) + + if not found_volume['status'] in continue_states: + break + + time.sleep(1) + retries = retries + 1 + if retries > max_retries: + break + return found_volume + def test_create_and_delete_volume(self): """Creates and deletes a volume""" - # Create volume with name + # Create volume created_volume = self.api.post_volume({'volume': {'size': 1}}) LOG.debug("created_volume: %s" % created_volume) self.assertTrue(created_volume['id']) @@ -72,14 +91,7 @@ class VolumesTest(test.TestCase): self.assertTrue(created_volume_id in volume_names) # Wait (briefly) for creation. Delay is due to the 'message queue' - retries = 0 - while found_volume['status'] == 'creating': - LOG.debug("Found %s" % found_volume) - time.sleep(1) - found_volume = self.api.get_volume(created_volume_id) - retries = retries + 1 - if retries > 5: - break + found_volume = self._poll_while(created_volume_id, ['creating']) # It should be available... self.assertEqual('available', found_volume['status']) @@ -88,18 +100,7 @@ class VolumesTest(test.TestCase): self.api.delete_volume(created_volume_id) # Wait (briefly) for deletion. Delay is due to the 'message queue' - for retries in range(5): - try: - found_volume = self.api.get_volume(created_volume_id) - except client.OpenStackApiNotFoundException: - found_volume = None - LOG.debug("Got 404, proceeding") - break - - LOG.debug("Found_volume=%s" % found_volume) - if found_volume['status'] != 'deleting': - break - time.sleep(1) + found_volume = self._poll_while(created_volume_id, ['deleting']) # Should be gone self.assertFalse(found_volume) @@ -110,7 +111,7 @@ class VolumesTest(test.TestCase): 'create_volume', id=created_volume_id) LOG.debug("Create_Actions: %s" % create_actions) - + self.assertEquals(1, len(create_actions)) create_action = create_actions[0] self.assertEquals(create_action['id'], created_volume_id) @@ -132,6 +133,156 @@ class VolumesTest(test.TestCase): delete_action = export_actions[0] self.assertEquals(delete_action['id'], created_volume_id) + def test_attach_and_detach_volume(self): + """Creates, attaches, detaches and deletes a volume""" + + # Create server + server_req = {'server': self._build_minimal_create_server_request()} + # NOTE(justinsb): Create an extra server so that server_id != volume_id + self.api.post_server(server_req) + created_server = self.api.post_server(server_req) + LOG.debug("created_server: %s" % created_server) + server_id = created_server['id'] + + # Create volume + created_volume = self.api.post_volume({'volume': {'size': 1}}) + LOG.debug("created_volume: %s" % created_volume) + volume_id = created_volume['id'] + self._poll_while(volume_id, ['creating']) + + # Check we've got different IDs + self.assertNotEqual(server_id, volume_id) + + # List current server attachments - should be none + attachments = self.api.get_server_volumes(server_id) + self.assertEquals([], attachments) + + # Template attach request + device = '/dev/sdc' + attach_req = { 'device': device } + post_req = { 'volumeAttachment': attach_req } + + # Try to attach to a non-existent volume; should fail + attach_req['volumeId'] = 3405691582 + self.assertRaises(client.OpenStackApiNotFoundException, + self.api.post_server_volume, server_id, post_req) + + # Try to attach to a non-existent server; should fail + attach_req['volumeId'] = volume_id + self.assertRaises(client.OpenStackApiNotFoundException, + self.api.post_server_volume, 3405691582, post_req) + + # Should still be no attachments... + attachments = self.api.get_server_volumes(server_id) + self.assertEquals([], attachments) + + # Do a real attach + attach_req['volumeId'] = volume_id + attach_result = self.api.post_server_volume(server_id, post_req) + LOG.debug(_("Attachment = %s") % attach_result) + + attachment_id = attach_result['id'] + self.assertEquals(volume_id, attach_result['volumeId']) + + # These fields aren't set because it's async + #self.assertEquals(server_id, attach_result['serverId']) + #self.assertEquals(device, attach_result['device']) + + # This is just an implementation detail, but let's check it... + self.assertEquals(volume_id, attachment_id) + + # NOTE(justinsb): There's an issue with the attach code, in that + # it's currently asynchronous and not recorded until the attach + # completes. So the caller must be 'smart', like this... + attach_done = None + retries = 0 + while True: + try: + attach_done = self.api.get_server_volume(server_id, + attachment_id) + break + except client.OpenStackApiNotFoundException: + LOG.debug("Got 404, waiting") + + time.sleep(1) + retries = retries + 1 + if retries > 10: + break + + expect_attach = {} + expect_attach['id'] = volume_id + expect_attach['volumeId'] = volume_id + expect_attach['serverId'] = server_id + expect_attach['device'] = device + + self.assertEqual(expect_attach, attach_done) + + # Should be one attachemnt + attachments = self.api.get_server_volumes(server_id) + self.assertEquals([expect_attach], attachments) + + # Should be able to get details + attachment_info = self.api.get_server_volume(server_id, attachment_id) + self.assertEquals(expect_attach, attachment_info) + + # Getting details on a different id should fail + self.assertRaises(client.OpenStackApiNotFoundException, + self.api.get_server_volume, server_id, 3405691582) + self.assertRaises(client.OpenStackApiNotFoundException, + self.api.get_server_volume, + 3405691582, attachment_id) + + # Trying to detach a different id should fail + self.assertRaises(client.OpenStackApiNotFoundException, + self.api.delete_server_volume, server_id, 3405691582) + + # Detach should work + self.api.delete_server_volume(server_id, attachment_id) + + # Again, it's async, so wait... + retries = 0 + while True: + try: + attachment = self.api.get_server_volume(server_id, + attachment_id) + LOG.debug("Attachment still there: %s" % attachment) + except client.OpenStackApiNotFoundException: + LOG.debug("Got 404, delete done") + break + + time.sleep(1) + retries = retries + 1 + self.assertTrue(retries < 10) + + # Should be no attachments again + attachments = self.api.get_server_volumes(server_id) + self.assertEquals([], attachments) + + LOG.debug("Logs: %s" % driver.LoggingVolumeDriver.all_logs()) + + # Discover_volume and undiscover_volume are called from compute + # on attach/detach + + disco_moves = driver.LoggingVolumeDriver.logs_like( + 'discover_volume', + id=volume_id) + LOG.debug("discover_volume actions: %s" % disco_moves) + + self.assertEquals(1, len(disco_moves)) + disco_move = disco_moves[0] + self.assertEquals(disco_move['id'], volume_id) + + last_days_of_disco_moves = driver.LoggingVolumeDriver.logs_like( + 'undiscover_volume', + id=volume_id) + LOG.debug("undiscover_volume actions: %s" % last_days_of_disco_moves) + + self.assertEquals(1, len(last_days_of_disco_moves)) + undisco_move = last_days_of_disco_moves[0] + self.assertEquals(undisco_move['id'], volume_id) + self.assertEquals(undisco_move['mountpoint'], device) + self.assertEquals(undisco_move['instance_id'], server_id) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 148e5facd..045974fa3 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -135,7 +135,7 @@ class VolumeDriver(object): """Removes an export for a logical volume.""" raise NotImplementedError() - def discover_volume(self, volume): + def discover_volume(self, context, volume): """Discover volume on a remote host.""" raise NotImplementedError() @@ -574,6 +574,8 @@ class RBDDriver(VolumeDriver): def discover_volume(self, volume): """Discover volume on a remote host""" + #NOTE(justinsb): This is messed up... discover_volume takes 3 args + # but then that would break local_path return "rbd:%s/%s" % (FLAGS.rbd_pool, volume['name']) def undiscover_volume(self, volume): @@ -622,7 +624,7 @@ class SheepdogDriver(VolumeDriver): """Removes an export for a logical volume""" pass - def discover_volume(self, volume): + def discover_volume(self, context, volume): """Discover volume on a remote host""" return "sheepdog:%s" % volume['name'] @@ -656,7 +658,7 @@ class LoggingVolumeDriver(VolumeDriver): def remove_export(self, context, volume): self.log_action('remove_export', volume) - def discover_volume(self, volume): + def discover_volume(self, context, volume): self.log_action('discover_volume', volume) def undiscover_volume(self, volume): @@ -667,6 +669,10 @@ class LoggingVolumeDriver(VolumeDriver): _LOGS = [] + @staticmethod + def clear_logs(): + LoggingVolumeDriver._LOGS = [] + @staticmethod def log_action(action, parameters): """Logs the command.""" -- cgit From d49219f8b6dd626b868b99bee8a22c4ac5495af1 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 03:28:59 -0700 Subject: pep8 fixes --- nova/api/openstack/__init__.py | 1 + nova/api/openstack/volume_attachments.py | 10 +++++----- nova/tests/integrated/api/client.py | 2 +- nova/tests/integrated/test_volumes.py | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index e8aa4821b..030974482 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -141,6 +141,7 @@ class APIRouter(wsgi.Router): super(APIRouter, self).__init__(mapper) + class APIRouterV10(APIRouter): """Define routes specific to OpenStack API V1.0.""" diff --git a/nova/api/openstack/volume_attachments.py b/nova/api/openstack/volume_attachments.py index 2ce681e19..58a9a727b 100644 --- a/nova/api/openstack/volume_attachments.py +++ b/nova/api/openstack/volume_attachments.py @@ -62,17 +62,17 @@ def _translate_summary_view(context, vol): class Controller(wsgi.Controller): """ The volume attachment API controller for the Openstack API - + A child resource of the server. Note that we use the volume id as the ID of the attachment (though this is not guaranteed externally)""" _serialization_metadata = { 'application/xml': { 'attributes': { - 'volumeAttachment': [ 'id', - 'serverId', - 'volumeId', - 'device' ]}}} + 'volumeAttachment': ['id', + 'serverId', + 'volumeId', + 'device']}}} def __init__(self): self.compute_api = compute.API() diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index deb7fd981..023871bda 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -226,7 +226,7 @@ class TestOpenStackClient(object): def get_server_volume(self, server_id, attachment_id): return self.api_get('/servers/%s/volume_attachments/%s' % (server_id, attachment_id))['volumeAttachment'] - + def get_server_volumes(self, server_id): return self.api_get('/servers/%s/volume_attachments' % (server_id))['volumeAttachments'] diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py index f69361fb0..aa90301a5 100644 --- a/nova/tests/integrated/test_volumes.py +++ b/nova/tests/integrated/test_volumes.py @@ -159,8 +159,8 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): # Template attach request device = '/dev/sdc' - attach_req = { 'device': device } - post_req = { 'volumeAttachment': attach_req } + attach_req = {'device': device} + post_req = {'volumeAttachment': attach_req} # Try to attach to a non-existent volume; should fail attach_req['volumeId'] = 3405691582 -- cgit From bebc9504bb34934147705512413267d1ae4af170 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 04:02:31 -0700 Subject: Fake out network service as well, otherwise we can't terminate the instance in test_servers now that we've started a compute service --- nova/tests/integrated/integrated_helpers.py | 17 ++++++++++++++--- nova/tests/integrated/test_login.py | 1 - nova/tests/integrated/test_servers.py | 7 ++++--- nova/tests/integrated/test_volumes.py | 11 ++++++----- nova/virt/fake.py | 8 ++++++++ 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index b520cc5d3..3b7caecd6 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -128,6 +128,7 @@ class IntegratedUnitTestContext(object): self._start_compute_service() self._start_volume_service() self._start_scheduler_service() + self._start_network_service() # WSGI shutdown broken :-( # bug731668 @@ -140,6 +141,12 @@ class IntegratedUnitTestContext(object): self.services.append(compute_service) return compute_service + def _start_network_service(self): + network_service = service.Service.create(binary='nova-network') + network_service.start() + self.services.append(network_service) + return network_service + def _start_volume_service(self): volume_service = service.Service.create(binary='nova-volume') volume_service.start() @@ -236,7 +243,8 @@ class _IntegratedTestBase(test.TestCase): def setUp(self): super(_IntegratedTestBase, self).setUp() - self._setup_flags() + f = self._get_flags() + self.flags(**f) context = IntegratedUnitTestContext.startup() self.user = context.test_user @@ -246,9 +254,12 @@ class _IntegratedTestBase(test.TestCase): IntegratedUnitTestContext.shutdown() super(_IntegratedTestBase, self).tearDown() - def _setup_flags(self): + def _get_flags(self): """An opportunity to setup flags, before the services are started""" - pass + f = {} + #f['network_driver'] = 'nova.network.fake.FakeNetworkDriver' + f['fake_network'] = True + return f def _build_minimal_create_server_request(self): server = {} diff --git a/nova/tests/integrated/test_login.py b/nova/tests/integrated/test_login.py index cc3d555d0..d6e067c29 100644 --- a/nova/tests/integrated/test_login.py +++ b/nova/tests/integrated/test_login.py @@ -18,7 +18,6 @@ import unittest from nova import flags -from nova import test from nova.log import logging from nova.tests.integrated import integrated_helpers from nova.tests.integrated.api import client diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index 2b5d3324a..ed6522c38 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -19,7 +19,6 @@ import time import unittest from nova import flags -from nova import test from nova.log import logging from nova.tests.integrated import integrated_helpers from nova.tests.integrated.api import client @@ -32,8 +31,10 @@ FLAGS.verbose = True class ServersTest(integrated_helpers._IntegratedTestBase): - def _setup_flags(self): - self.flags(image_service='nova.image.fake.MockImageService') + def _get_flags(self): + f = super(ServersTest, self)._get_flags() + f['image_service'] = 'nova.image.fake.MockImageService' + return f def test_get_servers(self): """Simple check that listing servers works.""" diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py index aa90301a5..701e9fe3c 100644 --- a/nova/tests/integrated/test_volumes.py +++ b/nova/tests/integrated/test_volumes.py @@ -19,7 +19,6 @@ import unittest import time from nova import flags -from nova import test from nova.log import logging from nova.tests.integrated import integrated_helpers from nova.tests.integrated.api import client @@ -37,10 +36,12 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): super(VolumesTest, self).setUp() driver.LoggingVolumeDriver.clear_logs() - def _setup_flags(self): - self.flags(image_service='nova.image.fake.MockImageService', - volume_driver='nova.volume.driver.LoggingVolumeDriver') - self.flags(use_local_volumes=False) # Avoids calling local_path + def _get_flags(self): + f = super(VolumesTest, self)._get_flags() + f['use_local_volumes'] = False # Avoids calling local_path + f['image_service'] = 'nova.image.fake.MockImageService' + f['volume_driver'] = 'nova.volume.driver.LoggingVolumeDriver' + return f def test_get_volumes(self): """Simple check that listing volumes works""" diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 3a06284a1..505ce0959 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -26,9 +26,13 @@ semantics of real hypervisor connections. """ from nova import exception +from nova import log as logging from nova.compute import power_state +LOG = logging.getLogger('nova.compute.disk') + + def get_connection(_): # The read_only parameter is ignored. return FakeConnection.instance() @@ -244,6 +248,10 @@ class FakeConnection(object): The work will be done asynchronously. This function returns a task that allows the caller to detect when it is complete. """ + key = instance.name + if not key in self.instances: + LOG.warning("Key '%s' not in instances '%s'" % + (key, self.instances)) del self.instances[instance.name] def attach_volume(self, instance_name, device_path, mountpoint): -- cgit From 83b25c2c8214462ab7f6b6ba76efdfba8c1de937 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 04:37:21 -0700 Subject: Grrr... because we're not recycling the API yet, we have to configure flags the first time it's called. --- nova/tests/integrated/integrated_helpers.py | 7 +++++-- nova/tests/integrated/test_volumes.py | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index 3b7caecd6..953af2e75 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -19,6 +19,7 @@ Provides common functionality for integrated unit tests """ +import os import random import string @@ -255,9 +256,11 @@ class _IntegratedTestBase(test.TestCase): super(_IntegratedTestBase, self).tearDown() def _get_flags(self): - """An opportunity to setup flags, before the services are started""" + """An opportunity to setup flags, before the services are started + + Warning - this is a bit flaky till the WSGI recycle code lands""" f = {} - #f['network_driver'] = 'nova.network.fake.FakeNetworkDriver' + f['image_service'] = 'nova.image.fake.MockImageService' f['fake_network'] = True return f diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py index 701e9fe3c..f173efea7 100644 --- a/nova/tests/integrated/test_volumes.py +++ b/nova/tests/integrated/test_volumes.py @@ -39,7 +39,6 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): def _get_flags(self): f = super(VolumesTest, self)._get_flags() f['use_local_volumes'] = False # Avoids calling local_path - f['image_service'] = 'nova.image.fake.MockImageService' f['volume_driver'] = 'nova.volume.driver.LoggingVolumeDriver' return f -- cgit From 1378db7ac86b69b8a966448b63415b2136b6b5bc Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 24 Mar 2011 09:07:57 -0400 Subject: Fix up formatting of libvirt.xml.template --- nova/virt/libvirt.xml.template | 132 ++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index 77c1b1997..26f528cb1 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -3,47 +3,47 @@ ${memory_kb} #if $type == 'lxc' - #set $disk_prefix = '' + #set $disk prefix = '' #set $disk_bus = '' exe /sbin/init #else -#if $type == 'uml' - #set $disk_prefix = 'ubd' - #set $disk_bus = 'uml' - uml - /usr/bin/linux - /dev/ubda -#else - #if $type == 'xen' - #set $disk_prefix = 'sd' - #set $disk_bus = 'scsi' - linux - /dev/xvda - #else - #set $disk_prefix = 'vd' - #set $disk_bus = 'virtio' - hvm - #end if - #if $getVar('rescue', False) - ${basepath}/kernel.rescue - ${basepath}/ramdisk.rescue - #else - #if $getVar('kernel', None) - ${kernel} - #if $type == 'xen' - ro - #else - root=/dev/vda console=ttyS0 - #end if - #if $getVar('ramdisk', None) - ${ramdisk} - #end if - #else - - #end if - #end if - #end if + #if $type == 'uml' + #set $disk_prefix = 'ubd' + #set $disk_bus = 'uml' + uml + /usr/bin/linux + /dev/ubda + #else + #if $type == 'xen' + #set $disk_prefix = 'sd' + #set $disk_bus = 'scsi' + linux + /dev/xvda + #else + #set $disk_prefix = 'vd' + #set $disk_bus = 'virtio' + hvm + #end if + #if $getVar('rescue', False) + ${basepath}/kernel.rescue + ${basepath}/ramdisk.rescue + #else + #if $getVar('kernel', None) + ${kernel} + #if $type == 'xen' + ro + #else + root=/dev/vda console=ttyS0 + #end if + #if $getVar('ramdisk', None) + ${ramdisk} + #end if + #else + + #end if + #end if + #end if #end if @@ -52,36 +52,36 @@ ${vcpus} #if $type == 'lxc' - - - - -#else -#if $getVar('rescue', False) - - - - - - - - - - + + + + #else - - - - - - #if $getVar('local', False) - - - - - - #end if - #end if + #if $getVar('rescue', False) + + + + + + + + + + + #else + + + + + + #if $getVar('local', False) + + + + + + #end if +#end if #end if -- cgit From da159d18b56af44f93cbf2c5e80b6aa3c98d5187 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Thu, 24 Mar 2011 09:12:24 -0400 Subject: Dont use popen in dettaching the lxc loop --- nova/virt/disk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index f6e6795d6..0bdb04cde 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -140,7 +140,8 @@ def destroy_container(target, instance): container_dir = '%s/rootfs' % target utils.execute('sudo', 'umount', container_dir) finally: - for loop in utils.popen('sudo losetup -a').readlines(): + out, err = utils('sudo', 'losetup', '-a') + for loop in out.splitlines(): if instance['name'] in loop: device = loop.split(loop, ':') utils.execute('sudo', 'losetup', '--detach', device) -- cgit From a10f719cdbc666171d8923ae1fd65bac3d6ebda7 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 10:49:19 -0700 Subject: Forgot one set of flags --- nova/tests/integrated/test_servers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index ed6522c38..c4676adc8 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -31,11 +31,6 @@ FLAGS.verbose = True class ServersTest(integrated_helpers._IntegratedTestBase): - def _get_flags(self): - f = super(ServersTest, self)._get_flags() - f['image_service'] = 'nova.image.fake.MockImageService' - return f - def test_get_servers(self): """Simple check that listing servers works.""" servers = self.api.get_servers() -- cgit From 71bd388a6c04df68e4392dbb7354cc8b14f596fe Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 24 Mar 2011 15:41:02 -0700 Subject: fix typo --- nova/virt/libvirt.xml.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index bcc6b3aed..ff98275fc 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -102,7 +102,7 @@ #if $getVar('vnc_compute_host_iface', False) - + #end if -- cgit From b01742ddb5bfec7e89ccc4cee17800614a0fce3c Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 24 Mar 2011 15:55:29 -0700 Subject: incorporate feedback from termie --- bin/nova-vnc-proxy | 5 +- nova/compute/api.py | 15 ++-- nova/compute/manager.py | 2 +- nova/flags.py | 4 +- nova/virt/libvirt.xml.template | 4 +- nova/virt/libvirt_conn.py | 3 +- nova/vnc/auth.py | 22 +++--- nova/vnc/proxy.py | 15 ++-- tools/euca-get-vnc-console | 163 ----------------------------------------- 9 files changed, 36 insertions(+), 197 deletions(-) delete mode 100755 tools/euca-get-vnc-console diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy index ea2533dc3..e7b647c00 100755 --- a/bin/nova-vnc-proxy +++ b/bin/nova-vnc-proxy @@ -1,5 +1,4 @@ #!/usr/bin/env python -# pylint: disable-msg=C0103 # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the @@ -50,7 +49,7 @@ flags.DEFINE_boolean('vnc_debug', False, 'Enable debugging features, like token bypassing') flags.DEFINE_integer('vnc_proxy_port', 6080, 'Port that the VNC proxy should bind to') -flags.DEFINE_string('vnc_proxy_iface', '0.0.0.0', +flags.DEFINE_string('vnc_proxy_host', '0.0.0.0', 'Address that the VNC proxy should bind to') flags.DEFINE_integer('vnc_token_ttl', 300, 'How many seconds before deleting tokens') @@ -90,5 +89,5 @@ if __name__ == "__main__": with_auth = auth.NovaAuthMiddleware(with_logging) server = wsgi.Server() - server.start(with_auth, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_iface) + server.start(with_auth, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_host) server.wait() diff --git a/nova/compute/api.py b/nova/compute/api.py index cec978d75..cb3898f72 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -477,21 +477,22 @@ class API(base.Base): output['token'])} def get_vnc_console(self, context, instance_id): - """Get a url to an AJAX Console""" + """Get a url to a VNC Console.""" instance = self.get(context, instance_id) output = self._call_compute_message('get_vnc_console', context, instance_id) rpc.cast(context, '%s' % FLAGS.vnc_console_proxy_topic, {'method': 'authorize_vnc_console', - 'args': {'token': output['token'], 'host': output['host'], + 'args': {'token': output['token'], + 'host': output['host'], 'port': output['port']}}) - time.sleep(1) - - return {'url': '%s/vnc_auto.html?token=%s&host=%s&port=%s' % - (FLAGS.vnc_console_proxy_url, - output['token'], 'hostignore', 'portignore')} + return {'url': '%s/vnc_auto.html?token=%s&host=%s&port=%s' % ( + FLAGS.vnc_console_proxy_url, + output['token'], + 'hostignore', + 'portignore')} def get_console_output(self, context, instance_id): """Get console output for an an instance""" diff --git a/nova/compute/manager.py b/nova/compute/manager.py index e53b36b34..64982d8ff 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -559,7 +559,7 @@ class ComputeManager(manager.Manager): @exception.wrap_exception def get_vnc_console(self, context, instance_id): - """Return connection information for an vnc console""" + """Return connection information for an vnc console.""" context = context.elevated() LOG.debug(_("instance %s: getting vnc console"), instance_id) instance_ref = self.db.instance_get(context, instance_id) diff --git a/nova/flags.py b/nova/flags.py index a0ea10795..1d2469206 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -287,8 +287,8 @@ DEFINE_string('vnc_console_proxy_url', 'http://127.0.0.1:6080', 'location of vnc console proxy, \ in the form "http://127.0.0.1:6080"') -DEFINE_string('vnc_compute_host_iface', '0.0.0.0', - 'the compute host interface on which vnc server should listen') +DEFINE_string('vnc_server_host', '0.0.0.0', + 'the host interface on which vnc server should listen') DEFINE_bool('vnc_enabled', True, 'enable vnc related features') DEFINE_bool('verbose', False, 'show debug output') diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index ff98275fc..609784982 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -101,8 +101,8 @@ -#if $getVar('vnc_compute_host_iface', False) - +#if $getVar('vnc_server_host', False) + #end if diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index c3529f512..cb6384e01 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -516,6 +516,7 @@ class LibvirtConnection(object): def get_vnc_port_for_instance(instance_name): virt_dom = self._conn.lookupByName(instance_name) xml = virt_dom.XMLDesc(0) + # TODO: use etree instead of minidom dom = minidom.parseString(xml) for graphic in dom.getElementsByTagName('graphics'): @@ -735,7 +736,7 @@ class LibvirtConnection(object): 'driver_type': driver_type} if FLAGS.vnc_enabled: - xml_info['vnc_compute_host_iface'] = FLAGS.vnc_compute_host_iface + xml_info['vnc_server_host'] = FLAGS.vnc_server_host if ra_server: xml_info['ra_server'] = ra_server + "/128" if not rescue: diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index 676cb2360..1c6a638fc 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -18,11 +18,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Auth Components for VNC Console""" +"""Auth Components for VNC Console.""" import time import urlparse import webob + from webob import Request from nova import flags @@ -32,12 +33,13 @@ from nova import utils from nova import wsgi from nova import vnc + LOG = logging.getLogger('nova.vnc-proxy') FLAGS = flags.FLAGS class NovaAuthMiddleware(object): - """Implementation of Middleware to Handle Nova Auth""" + """Implementation of Middleware to Handle Nova Auth.""" def __init__(self, app): self.app = app @@ -67,13 +69,12 @@ class NovaAuthMiddleware(object): middleware = self middleware.tokens = {} - class Callback: - def __call__(self, data, message): - if data['method'] == 'authorize_vnc_console': - token = data['args']['token'] - LOG.audit(_("Received Token: %s)"), token) - middleware.tokens[token] = \ - {'args': data['args'], 'last_activity_at': time.time()} + def callback(self, data, message): + if data['method'] == 'authorize_vnc_console': + token = data['args']['token'] + LOG.audit(_("Received Token: %s)"), token) + middleware.tokens[token] = \ + {'args': data['args'], 'last_activity_at': time.time()} def delete_expired_tokens(): now = time.time() @@ -90,7 +91,7 @@ class NovaAuthMiddleware(object): consumer = rpc.TopicConsumer( connection=conn, topic=FLAGS.vnc_console_proxy_topic) - consumer.register_callback(Callback()) + consumer.register_callback(callback) utils.LoopingCall(consumer.fetch, auto_ack=True, enable_callbacks=True).start(0.1) @@ -103,7 +104,6 @@ class LoggingMiddleware(object): @webob.dec.wsgify def __call__(self, req): - if req.path == vnc.proxy.WS_ENDPOINT: LOG.info(_("Received Websocket Request: %s"), req.url) else: diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py index dea838e3d..49379d9ae 100644 --- a/nova/vnc/proxy.py +++ b/nova/vnc/proxy.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# pylint: disable-msg=C0103 # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the @@ -20,11 +19,13 @@ """Eventlet WSGI Services to proxy VNC. No nova deps.""" -from base64 import b64encode, b64decode +import base64 +import os + import eventlet from eventlet import wsgi from eventlet import websocket -import os + from webob import Request import webob @@ -32,7 +33,7 @@ WS_ENDPOINT = '/data' class WebsocketVNCProxy(object): - """Class to proxy from websocket to vnc server""" + """Class to proxy from websocket to vnc server.""" def __init__(self, wwwroot): self.wwwroot = wwwroot @@ -58,7 +59,7 @@ class WebsocketVNCProxy(object): d = source.recv(32384) if d == '': break - d = b64encode(d) + d = base64.b64encode(d) dest.send(d) except: source.close() @@ -70,7 +71,7 @@ class WebsocketVNCProxy(object): d = source.wait() if d is None: break - d = b64decode(d) + d = base64.b64decode(d) dest.sendall(d) except: source.close() @@ -118,7 +119,7 @@ class WebsocketVNCProxy(object): class DebugMiddleware(object): - """Debug middleware. Skip auth, get vnc port and host from query string""" + """Debug middleware. Skip auth, get vnc connect info from query string.""" def __init__(self, app): self.app = app diff --git a/tools/euca-get-vnc-console b/tools/euca-get-vnc-console deleted file mode 100755 index bd2788f03..000000000 --- a/tools/euca-get-vnc-console +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python -# pylint: disable-msg=C0103 -# 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. - -"""Euca add-on to use vnc console""" - -import getopt -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) - -import boto -import nova -from boto.ec2.connection import EC2Connection -from euca2ools import Euca2ool, InstanceValidationError, Util, ConnectionFailed - -usage_string = """ -Retrieves a url to an vnc console terminal - -euca-get-vnc-console [-h, --help] [--version] [--debug] instance_id - -REQUIRED PARAMETERS - -instance_id: unique identifier for the instance show the console output for. - -OPTIONAL PARAMETERS - -""" - - -# This class extends boto to add VNCConsole functionality -class NovaEC2Connection(EC2Connection): - - def get_vnc_console(self, instance_id): - """ - Retrieves a console connection for the specified instance. - - :type instance_id: string - :param instance_id: The instance ID of a running instance on the cloud. - - :rtype: :class:`VNCConsole` - """ - - class VNCConsole: - def __init__(self, parent=None): - self.parent = parent - self.instance_id = None - self.url = None - - def startElement(self, name, attrs, connection): - return None - - def endElement(self, name, value, connection): - if name == 'instanceId': - self.instance_id = value - elif name == 'url': - self.url = value - else: - setattr(self, name, value) - - params = {} - return self.get_object('GetVNCConsole', - {'InstanceId': instance_id}, VNCConsole) - - -def override_connect_ec2(aws_access_key_id=None, - aws_secret_access_key=None, **kwargs): - return NovaEC2Connection(aws_access_key_id, - aws_secret_access_key, **kwargs) - -# override boto's connect_ec2 method, so that we can use NovaEC2Connection -boto.connect_ec2 = override_connect_ec2 - - -def usage(status=1): - print usage_string - Util().usage() - sys.exit(status) - - -def version(): - print Util().version() - sys.exit() - - -def display_console_output(console_output): - print console_output.instance_id - print console_output.timestamp - print console_output.output - - -def display_vnc_console_output(console_output): - print console_output.url - - -def main(): - try: - euca = Euca2ool() - except Exception, e: - print e - usage() - - instance_id = None - - for name, value in euca.opts: - if name in ('-h', '--help'): - usage(0) - elif name == '--version': - version() - elif name == '--debug': - debug = True - - for arg in euca.args: - instance_id = arg - break - - if instance_id: - try: - euca.validate_instance_id(instance_id) - except InstanceValidationError: - print 'Invalid instance id' - sys.exit(1) - - try: - euca_conn = euca.make_connection() - except ConnectionFailed, e: - print e.message - sys.exit(1) - try: - console_output = euca_conn.get_vnc_console(instance_id) - except Exception, ex: - euca.display_error_and_exit('%s' % ex) - - display_vnc_console_output(console_output) - else: - print 'instance_id must be specified' - usage() - -if __name__ == "__main__": - main() -- cgit From f2f08a5b0309876bb312c9124e75bd89331c4816 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 24 Mar 2011 17:04:55 -0700 Subject: make everything work with trunk again --- nova/api/ec2/cloud.py | 2 +- nova/virt/libvirt_conn.py | 2 +- nova/vnc/auth.py | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 6b08f98c2..eb0428c2c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -538,7 +538,7 @@ class CloudController(object): def get_vnc_console(self, context, instance_id, **kwargs): ec2_id = instance_id - instance_id = ec2_id_to_id(ec2_id) + instance_id = ec2utils.ec2_id_to_id(ec2_id) return self.compute_api.get_vnc_console(context, instance_id=instance_id) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 9cd0e8ac9..41adbfe27 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -626,7 +626,7 @@ class LibvirtConnection(driver.ComputeDriver): return {'token': token, 'host': host, 'port': port} - _image_sems = {} # FIXME: why is this here? (anthony) + _image_sems = {} # FIXME: why is this here? (anthony) @staticmethod def _cache_image(fn, target, fname, cow=False, *args, **kwargs): diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index 1c6a638fc..4161f3666 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -69,12 +69,14 @@ class NovaAuthMiddleware(object): middleware = self middleware.tokens = {} - def callback(self, data, message): - if data['method'] == 'authorize_vnc_console': - token = data['args']['token'] + class Proxy(): + @staticmethod + def authorize_vnc_console(context, **kwargs): + data = kwargs + token = kwargs['token'] LOG.audit(_("Received Token: %s)"), token) middleware.tokens[token] = \ - {'args': data['args'], 'last_activity_at': time.time()} + {'args': kwargs, 'last_activity_at': time.time()} def delete_expired_tokens(): now = time.time() @@ -88,12 +90,12 @@ class NovaAuthMiddleware(object): del middleware.tokens[k] conn = rpc.Connection.instance(new=True) - consumer = rpc.TopicConsumer( - connection=conn, - topic=FLAGS.vnc_console_proxy_topic) - consumer.register_callback(callback) + consumer = rpc.TopicAdapterConsumer( + connection=conn, + proxy=Proxy, + topic=FLAGS.vnc_console_proxy_topic) - utils.LoopingCall(consumer.fetch, auto_ack=True, + utils.LoopingCall(consumer.fetch, enable_callbacks=True).start(0.1) utils.LoopingCall(delete_expired_tokens).start(1) -- cgit From 06c0eff8ec7eef33933da9bd8adbf7b70a977889 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 24 Mar 2011 17:44:27 -0700 Subject: add hook for osapi --- nova/api/ec2/cloud.py | 1 + nova/api/openstack/servers.py | 10 ++++++++++ nova/vnc/auth.py | 4 ++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index eb0428c2c..fa4624ff1 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -537,6 +537,7 @@ class CloudController(object): instance_id=instance_id) def get_vnc_console(self, context, instance_id, **kwargs): + """Returns vnc browser url to the dashboard.""" ec2_id = instance_id instance_id = ec2utils.ec2_id_to_id(ec2_id) return self.compute_api.get_vnc_console(context, diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 0dad46268..88cc790c1 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -481,6 +481,16 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler + def get_vnc_console(self, req, id): + """ Returns a url to an instance's ajaxterm console. """ + try: + self.compute_api.get_vnc_console(req.environ['nova.context'], + int(id)) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + @scheduler_api.redirect_handler def diagnostics(self, req, id): """Permit Admins to retrieve server diagnostics.""" diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index 4161f3666..dff9b376f 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -69,7 +69,7 @@ class NovaAuthMiddleware(object): middleware = self middleware.tokens = {} - class Proxy(): + class TopicProxy(): @staticmethod def authorize_vnc_console(context, **kwargs): data = kwargs @@ -92,7 +92,7 @@ class NovaAuthMiddleware(object): conn = rpc.Connection.instance(new=True) consumer = rpc.TopicAdapterConsumer( connection=conn, - proxy=Proxy, + proxy=TopicProxy, topic=FLAGS.vnc_console_proxy_topic) utils.LoopingCall(consumer.fetch, -- cgit From b30d5aa17c86bf1487945d8f2b2878644f79999e Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 24 Mar 2011 18:37:23 -0700 Subject: add documentation --- doc/source/runnova/vncconsole.rst | 76 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 doc/source/runnova/vncconsole.rst diff --git a/doc/source/runnova/vncconsole.rst b/doc/source/runnova/vncconsole.rst new file mode 100644 index 000000000..69f147613 --- /dev/null +++ b/doc/source/runnova/vncconsole.rst @@ -0,0 +1,76 @@ +.. + Copyright 2010-2011 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. + +Getting Started with the VNC Proxy +================================== + +The VNC Proxy is an OpenStack component that allows users of Nova to access +their instances through a websocket enabled browser (like Google Chrome). + +A VNC Connection works like so: + +* User connects over an api and gets a url like http://ip:port/?token=xyz +* User pastes url in browser +* Browser connects to VNC Proxy though a websocket enabled client like noVNC +* VNC Proxy authorizes users token, maps the token to a host and port of an + instance's VNC server +* VNC Proxy initiates connection to VNC server, and continues proxying until + the session ends + + +Configuring the VNC Proxy +------------------------- +nova-vnc-proxy requires a websocket enabled html client to work properly. At +this time, the only tested client is a slightly modified fork of noVNC, which +you can at find git://github.com/sleepsonthefloor/noVNC.git. + +.. todo:: add instruction for installing from package + +noVNC must be in the location specified by --vnc_proxy_wwwroot, which defaults +to /var/lib/nova/noVNC. nova-vnc-proxy will fail to launch until this code +is properly installed. + +By default, nova-vnc-proxy binds 0.0.0.0:6080. This can be configured with: + +* --vnc_proxy_port=[port] +* --vnc_proxy_host=[host] + + +Enabling VNC Consoles in Nova +----------------------------- +At the moment, VNC support is supported only when using libvirt. To enable VNC +Console, configure the following flags: + +* --vnc_console_proxy_url=http://[proxy_host]:[proxy_port] - proxy_port + defaults to 6080. This url must point to nova-vnc-proxy +* --vnc_enabled=[True|False] - defaults to True. If this flag is not set your + instances will launch without vnc support. + + +Getting an instance's VNC Console +--------------------------------- +You can access an instance's VNC Console url in the following methods: + +* Using the direct api: + eg: 'stack --user=admin --project=admin compute get_vnc_console instance_id=1' +* Support for Dashboard, and the Openstack API will be forthcoming + + +Accessing VNC Consoles without a web browser +-------------------------------------------- +At the moment, VNC Consoles are only supported through the web browser, but +more general VNC support is in the works. -- cgit From e722803067e6386e98f29aa867d4cf98ce6e0cc2 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 24 Mar 2011 18:38:28 -0700 Subject: clarify comment --- nova/api/ec2/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index fa4624ff1..e5a957b83 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -537,7 +537,7 @@ class CloudController(object): instance_id=instance_id) def get_vnc_console(self, context, instance_id, **kwargs): - """Returns vnc browser url to the dashboard.""" + """Returns vnc browser url. Used by OS dashboard.""" ec2_id = instance_id instance_id = ec2utils.ec2_id_to_id(ec2_id) return self.compute_api.get_vnc_console(context, -- cgit From 9632db75d229eac16970af1dfabbb047c2b71a4e Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 25 Mar 2011 08:20:56 -0400 Subject: Removed partition from setup_container --- nova/virt/disk.py | 4 ++-- nova/virt/libvirt_conn.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 0bdb04cde..bbdcc8ddf 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -115,13 +115,13 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): _unlink_device(device, nbd) -def setup_container(image, container_dir=None, partition=None): +def setup_container(image, container_dir=None): """Setup the LXC container It will mount the loopback image to the container directory in order to create the root filesystem for the container """ - nbd = False + nbd = "False" device = _link_device(image, nbd) err = utils.execute('sudo', 'mount', device, container_dir) if err: diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 9fb8ba955..bfb0a75f1 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -798,8 +798,7 @@ class LibvirtConnection(driver.ComputeDriver): if FLAGS.libvirt_type == 'lxc': disk.setup_container(basepath('disk'), - container_dir=container_dir, - partition=target_partition) + container_dir=container_dir) except Exception as e: # This could be a windows image, or a vmdk format disk LOG.warn(_('instance %(inst_name)s: ignoring error injecting' -- cgit From 47b54662c17c7af0bca9cf96dc5d4a498706fe8b Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 25 Mar 2011 08:37:47 -0400 Subject: Dont always assume qemu --- nova/virt/libvirt_conn.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index bfb0a75f1..840eaceeb 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -1053,7 +1053,8 @@ class LibvirtConnection(driver.ComputeDriver): total = 0 for dom_id in self._conn.listDomainsID(): dom = self._conn.lookupByID(dom_id) - total += len(dom.vcpus()[1]) + if dom is None: + total += len(dom.vcpus()[1]) return total def get_memory_mb_used(self): -- cgit From 3d8b55294702b531a570b279fb29db8d4ea104d3 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 25 Mar 2011 08:52:18 -0400 Subject: Fix up templating --- nova/virt/libvirt.xml.template | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index c2c5384bb..26f528cb1 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -83,24 +83,21 @@ #end if #end if #end if - -#for $nic in $nics - - + + - - - -#if $getVar('nic.extra_params', False) - ${nic.extra_params} + + + +#if $getVar('extra_params', False) + ${extra_params} #end if -#if $getVar('nic.gateway_v6', False) - +#if $getVar('gateway_v6', False) + #end if -#end for -- cgit From 6ce4f9c6ae00138184e79cdcfb6f78fc3474580e Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 25 Mar 2011 08:52:54 -0400 Subject: Fix up destroy container --- nova/virt/disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index bbdcc8ddf..b9a8fb7e7 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -140,7 +140,7 @@ def destroy_container(target, instance): container_dir = '%s/rootfs' % target utils.execute('sudo', 'umount', container_dir) finally: - out, err = utils('sudo', 'losetup', '-a') + out, err = utils.execute('sudo', 'losetup', '-a') for loop in out.splitlines(): if instance['name'] in loop: device = loop.split(loop, ':') -- cgit From c8fb0c5a16852afc98349edf89bb31afac166749 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Fri, 25 Mar 2011 09:39:20 -0400 Subject: Revert dom check --- nova/virt/libvirt_conn.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 840eaceeb..bfb0a75f1 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -1053,8 +1053,7 @@ class LibvirtConnection(driver.ComputeDriver): total = 0 for dom_id in self._conn.listDomainsID(): dom = self._conn.lookupByID(dom_id) - if dom is None: - total += len(dom.vcpus()[1]) + total += len(dom.vcpus()[1]) return total def get_memory_mb_used(self): -- cgit From cd1bac4deff367131d43f87cdfbc3b6b34bbdc1e Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Fri, 25 Mar 2011 16:39:43 -0700 Subject: Initial extensification of volumes --- nova/api/openstack/__init__.py | 10 -- nova/api/openstack/extensions.py | 151 ++++++++++++++--- nova/api/openstack/incubator/__init__.py | 20 +++ nova/api/openstack/incubator/volumes/__init__.py | 18 ++ .../incubator/volumes/volume_attachments.py | 181 +++++++++++++++++++++ nova/api/openstack/incubator/volumes/volumes.py | 160 ++++++++++++++++++ .../api/openstack/incubator/volumes/volumes_ext.py | 55 +++++++ nova/api/openstack/volume_attachments.py | 181 --------------------- nova/api/openstack/volumes.py | 160 ------------------ nova/tests/api/openstack/extensions/foxinsocks.py | 98 ----------- .../openstack/extensions/foxinsocks/__init__.py | 19 +++ .../openstack/extensions/foxinsocks/foxinsocks.py | 98 +++++++++++ 12 files changed, 682 insertions(+), 469 deletions(-) create mode 100644 nova/api/openstack/incubator/__init__.py create mode 100644 nova/api/openstack/incubator/volumes/__init__.py create mode 100644 nova/api/openstack/incubator/volumes/volume_attachments.py create mode 100644 nova/api/openstack/incubator/volumes/volumes.py create mode 100644 nova/api/openstack/incubator/volumes/volumes_ext.py delete mode 100644 nova/api/openstack/volume_attachments.py delete mode 100644 nova/api/openstack/volumes.py delete mode 100644 nova/tests/api/openstack/extensions/foxinsocks.py create mode 100644 nova/tests/api/openstack/extensions/foxinsocks/__init__.py create mode 100644 nova/tests/api/openstack/extensions/foxinsocks/foxinsocks.py diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 0e5b2a071..731e16a58 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -128,16 +128,6 @@ class APIRouter(wsgi.Router): _limits = limits.LimitsController() mapper.resource("limit", "limits", controller=_limits) - #NOTE(justinsb): volumes is not yet part of the official API - mapper.resource("volume", "volumes", - controller=volumes.Controller(), - collection={'detail': 'GET'}) - - mapper.resource("volume_attachment", "volume_attachments", - controller=volume_attachments.Controller(), - parent_resource=dict(member_name='server', - collection_name='servers')) - super(APIRouter, self).__init__(mapper) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 9d98d849a..6a8ce9669 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2011 OpenStack LLC. +# Copyright 2011 Justin Santa Barbara # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,12 +17,14 @@ # under the License. import imp +import inspect import os import sys import routes import webob.dec import webob.exc +from nova import exception from nova import flags from nova import log as logging from nova import wsgi @@ -34,6 +37,63 @@ LOG = logging.getLogger('extensions') FLAGS = flags.FLAGS +class ExtensionDescriptor(object): + """This is the base class that defines the contract for extensions""" + + def get_name(self): + """The name of the extension + + e.g. 'Fox In Socks' """ + raise NotImplementedError() + + def get_alias(self): + """The alias for the extension + + e.g. 'FOXNSOX'""" + raise NotImplementedError() + + def get_description(self): + """Friendly description for the extension + + e.g. 'The Fox In Socks Extension'""" + raise NotImplementedError() + + def get_namespace(self): + """The XML namespace for the extension + + e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'""" + raise NotImplementedError() + + def get_updated(self): + """The timestamp when the extension was last updated + + e.g. '2011-01-22T13:25:27-06:00'""" + #NOTE(justinsb): Huh? Isn't this defined by the namespace? + raise NotImplementedError() + + def get_resources(self): + """List of extensions.ResourceExtension extension objects + + Resources define new nouns, and are accessible through URLs""" + resources = [] + return resources + + def get_actions(self): + """List of extensions.ActionExtension extension objects + + Actions are verbs callable from the API""" + actions = [] + return actions + + def get_response_extensions(self): + """List of extensions.ResponseExtension extension objects + + Response extensions are used to insert information into existing + response data""" + response_exts = [] + return response_exts + + class ActionExtensionController(wsgi.Controller): def __init__(self, application): @@ -109,13 +169,10 @@ class ExtensionController(wsgi.Controller): return self._translate(ext) def delete(self, req, id): - raise faults.Fault(exc.HTTPNotFound()) + raise faults.Fault(webob.exc.HTTPNotFound()) def create(self, req): - raise faults.Fault(exc.HTTPNotFound()) - - def delete(self, req, id): - raise faults.Fault(exc.HTTPNotFound()) + raise faults.Fault(webob.exc.HTTPNotFound()) class ExtensionMiddleware(wsgi.Middleware): @@ -235,16 +292,19 @@ class ExtensionMiddleware(wsgi.Middleware): class ExtensionManager(object): """ Load extensions from the configured extension path. - See nova/tests/api/openstack/extensions/foxinsocks.py for an example - extension implementation. + + See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an + example extension implementation. """ def __init__(self, path): LOG.audit(_('Initializing extension manager.')) + self.super_verbose = False + self.path = path self.extensions = {} - self._load_extensions() + self._load_all_extensions() def get_resources(self): """ @@ -300,7 +360,7 @@ class ExtensionManager(object): except AttributeError as ex: LOG.exception(_("Exception loading extension: %s"), unicode(ex)) - def _load_extensions(self): + def _load_all_extensions(self): """ Load extensions from the configured path. The extension name is constructed from the module_name. If your extension module was named @@ -310,23 +370,74 @@ class ExtensionManager(object): See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. """ - if not os.path.exists(self.path): + self._load_extensions_under_path(self.path) + + incubator_path = os.path.join(os.path.dirname(__file__), "incubator") + self._load_extensions_under_path(incubator_path) + + def _load_extensions_under_path(self, path): + if not os.path.isdir(path): + LOG.warning(_('Extensions directory not found: %s') % path) return - for f in os.listdir(self.path): - LOG.audit(_('Loading extension file: %s'), f) + LOG.debug(_('Looking for extensions in: %s') % path) + + for child in os.listdir(path): + child_path = os.path.join(path, child) + if not os.path.isdir(child_path): + continue + self._load_extension(child_path) + + def _load_extension(self, path): + if not os.path.isdir(path): + return + + for f in os.listdir(path): mod_name, file_ext = os.path.splitext(os.path.split(f)[-1]) - ext_path = os.path.join(self.path, f) - if file_ext.lower() == '.py': - mod = imp.load_source(mod_name, ext_path) - ext_name = mod_name[0].upper() + mod_name[1:] + if file_ext.startswith('_'): + continue + if file_ext.lower() != '.py': + continue + + ext_path = os.path.join(path, f) + if self.super_verbose: + LOG.debug(_('Checking extension file: %s'), ext_path) + + mod = imp.load_source(mod_name, ext_path) + for _name, cls in inspect.getmembers(mod): try: - new_ext = getattr(mod, ext_name)() - self._check_extension(new_ext) - self.extensions[new_ext.get_alias()] = new_ext + if not inspect.isclass(cls): + continue + + #NOTE(justinsb): It seems that python modules aren't great + # If you have two identically named modules, the classes + # from both are mixed in. So name your extension based + # on the alias, not 'extension.py'! + #TODO(justinsb): Any way to work around this? + + if self.super_verbose: + LOG.debug(_('Checking class: %s'), cls) + + if not ExtensionDescriptor in cls.__bases__: + if self.super_verbose: + LOG.debug(_('Not a ExtensionDescriptor: %s'), cls) + continue + + obj = cls() + self._add_extension(obj) except AttributeError as ex: LOG.exception(_("Exception loading extension: %s"), - unicode(ex)) + unicode(ex)) + + def _add_extension(self, ext): + alias = ext.get_alias() + LOG.audit(_('Loaded extension: %s'), alias) + + self._check_extension(ext) + + if alias in self.extensions: + raise exception.Error("Found duplicate extension: %s" % alias) + self.extensions[alias] = ext class ResponseExtension(object): diff --git a/nova/api/openstack/incubator/__init__.py b/nova/api/openstack/incubator/__init__.py new file mode 100644 index 000000000..cded38174 --- /dev/null +++ b/nova/api/openstack/incubator/__init__.py @@ -0,0 +1,20 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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 datetime + +"""Incubator contains extensions that are shipped with nova. + +It can't be called 'extensions' because that causes namespacing problems.""" diff --git a/nova/api/openstack/incubator/volumes/__init__.py b/nova/api/openstack/incubator/volumes/__init__.py new file mode 100644 index 000000000..2a9c93210 --- /dev/null +++ b/nova/api/openstack/incubator/volumes/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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 datetime + +"""The volumes extension adds volumes and attachments to the API.""" diff --git a/nova/api/openstack/incubator/volumes/volume_attachments.py b/nova/api/openstack/incubator/volumes/volume_attachments.py new file mode 100644 index 000000000..58a9a727b --- /dev/null +++ b/nova/api/openstack/incubator/volumes/volume_attachments.py @@ -0,0 +1,181 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from webob import exc + +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + +FLAGS = flags.FLAGS + + +def _translate_detail_view(context, volume): + """ Maps keys for details view""" + + d = _translate_summary_view(context, volume) + + # No additional data / lookups at the moment + + return d + + +def _translate_summary_view(context, vol): + """ Maps keys for summary view""" + d = {} + + volume_id = vol['id'] + + # NOTE(justinsb): We use the volume id as the id of the attachment object + d['id'] = volume_id + + d['volumeId'] = volume_id + if vol.get('instance_id'): + d['serverId'] = vol['instance_id'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d + + +class Controller(wsgi.Controller): + """ The volume attachment API controller for the Openstack API + + A child resource of the server. Note that we use the volume id + as the ID of the attachment (though this is not guaranteed externally)""" + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'volumeAttachment': ['id', + 'serverId', + 'volumeId', + 'device']}}} + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(Controller, self).__init__() + + def index(self, req, server_id): + """ Returns the list of volume attachments for a given instance """ + return self._items(req, server_id, + entity_maker=_translate_summary_view) + + def show(self, req, server_id, id): + """Return data about the given volume""" + context = req.environ['nova.context'] + + volume_id = id + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + LOG.debug("volume_id not found") + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + return {'volumeAttachment': _translate_detail_view(context, vol)} + + def create(self, req, server_id): + """ Attach a volume to an instance """ + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + instance_id = server_id + volume_id = env['volumeAttachment']['volumeId'] + device = env['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + try: + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + # The attach is async + attachment = {} + attachment['id'] = volume_id + attachment['volumeId'] = volume_id + + # NOTE(justinsb): And now, we have a problem... + # The attach is async, so there's a window in which we don't see + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. + # For now, we'll probably have to rely on libraries being smart + + # TODO: How do I return "accepted" here?? + return {'volumeAttachment': attachment} + + def update(self, _req, _server_id, _id): + """ Update a volume attachment. We don't currently support this.""" + return faults.Fault(exc.HTTPBadRequest()) + + def delete(self, req, server_id, id): + """ Detach a volume from an instance """ + context = req.environ['nova.context'] + + volume_id = id + LOG.audit(_("Detach volume %s"), volume_id, context=context) + + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return exc.HTTPAccepted() + + def _items(self, req, server_id, entity_maker): + """Returns a list of attachments, transformed through entity_maker""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + volumes = instance['volumes'] + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumeAttachments': res} diff --git a/nova/api/openstack/incubator/volumes/volumes.py b/nova/api/openstack/incubator/volumes/volumes.py new file mode 100644 index 000000000..ec3b9a6c8 --- /dev/null +++ b/nova/api/openstack/incubator/volumes/volumes.py @@ -0,0 +1,160 @@ +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from webob import exc + +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + +FLAGS = flags.FLAGS + + +def _translate_detail_view(context, vol): + """ Maps keys for details view""" + + d = _translate_summary_view(context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_summary_view(_context, vol): + """ Maps keys for summary view""" + d = {} + + instance_id = None + # instance_data = None + attached_to = vol.get('instance') + if attached_to: + instance_id = attached_to['id'] + # instance_data = '%s[%s]' % (instance_ec2_id, + # attached_to['host']) + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] + # if context.is_admin: + # v['status'] = '%s (%s, %s, %s, %s)' % ( + # vol['status'], + # vol['user_id'], + # vol['host'], + # instance_data, + # vol['mountpoint']) + if vol['attach_status'] == 'attached': + d['attachments'] = [{'attachTime': vol['attach_time'], + 'deleteOnTermination': False, + 'mountpoint': vol['mountpoint'], + 'instanceId': instance_id, + 'status': 'attached', + 'volumeId': vol['id']}] + else: + d['attachments'] = [{}] + + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +class Controller(wsgi.Controller): + """ The Volumes API controller for the OpenStack API """ + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + ]}}} + + def __init__(self): + self.volume_api = volume.API() + super(Controller, self).__init__() + + def show(self, req, id): + """Return data about the given volume""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_detail_view(context, vol)} + + def delete(self, req, id): + """ Delete a volume """ + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + def index(self, req): + """ Returns a summary list of volumes""" + return self._items(req, entity_maker=_translate_summary_view) + + def detail(self, req): + """ Returns a detailed list of volumes """ + return self._items(req, entity_maker=_translate_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumes': res} + + def create(self, req): + """Creates a new volume""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + vol = env['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + new_volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) + + # Work around problem that instance is lazy-loaded... + new_volume['instance'] = None + + retval = _translate_detail_view(context, new_volume) + + return {'volume': retval} diff --git a/nova/api/openstack/incubator/volumes/volumes_ext.py b/nova/api/openstack/incubator/volumes/volumes_ext.py new file mode 100644 index 000000000..87a57320d --- /dev/null +++ b/nova/api/openstack/incubator/volumes/volumes_ext.py @@ -0,0 +1,55 @@ +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.openstack import extensions +from nova.api.openstack.incubator.volumes import volumes +from nova.api.openstack.incubator.volumes import volume_attachments + + +class VolumesExtension(extensions.ExtensionDescriptor): + def get_name(self): + return "Volumes" + + def get_alias(self): + return "VOLUMES" + + def get_description(self): + return "Volumes support" + + def get_namespace(self): + return "http://docs.openstack.org/ext/volumes/api/v1.1" + + def get_updated(self): + return "2011-03-25T00:00:00+00:00" + + def get_resources(self): + resources = [] + + #NOTE(justinsb): No way to provide singular name ('volume') + # Does this matter? + res = extensions.ResourceExtension('volumes', + volumes.Controller(), + collection_actions={'detail': 'GET'} + ) + resources.append(res) + + res = extensions.ResourceExtension('volume_attachments', + volume_attachments.Controller(), + parent=dict( + member_name='server', + collection_name='servers')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/volume_attachments.py b/nova/api/openstack/volume_attachments.py deleted file mode 100644 index 58a9a727b..000000000 --- a/nova/api/openstack/volume_attachments.py +++ /dev/null @@ -1,181 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from webob import exc - -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova import wsgi -from nova.api.openstack import common -from nova.api.openstack import faults - - -LOG = logging.getLogger("nova.api.volumes") - -FLAGS = flags.FLAGS - - -def _translate_detail_view(context, volume): - """ Maps keys for details view""" - - d = _translate_summary_view(context, volume) - - # No additional data / lookups at the moment - - return d - - -def _translate_summary_view(context, vol): - """ Maps keys for summary view""" - d = {} - - volume_id = vol['id'] - - # NOTE(justinsb): We use the volume id as the id of the attachment object - d['id'] = volume_id - - d['volumeId'] = volume_id - if vol.get('instance_id'): - d['serverId'] = vol['instance_id'] - if vol.get('mountpoint'): - d['device'] = vol['mountpoint'] - - return d - - -class Controller(wsgi.Controller): - """ The volume attachment API controller for the Openstack API - - A child resource of the server. Note that we use the volume id - as the ID of the attachment (though this is not guaranteed externally)""" - - _serialization_metadata = { - 'application/xml': { - 'attributes': { - 'volumeAttachment': ['id', - 'serverId', - 'volumeId', - 'device']}}} - - def __init__(self): - self.compute_api = compute.API() - self.volume_api = volume.API() - super(Controller, self).__init__() - - def index(self, req, server_id): - """ Returns the list of volume attachments for a given instance """ - return self._items(req, server_id, - entity_maker=_translate_summary_view) - - def show(self, req, server_id, id): - """Return data about the given volume""" - context = req.environ['nova.context'] - - volume_id = id - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - LOG.debug("volume_id not found") - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - return {'volumeAttachment': _translate_detail_view(context, vol)} - - def create(self, req, server_id): - """ Attach a volume to an instance """ - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - instance_id = server_id - volume_id = env['volumeAttachment']['volumeId'] - device = env['volumeAttachment']['device'] - - msg = _("Attach volume %(volume_id)s to instance %(server_id)s" - " at %(device)s") % locals() - LOG.audit(msg, context=context) - - try: - self.compute_api.attach_volume(context, - instance_id=instance_id, - volume_id=volume_id, - device=device) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - # The attach is async - attachment = {} - attachment['id'] = volume_id - attachment['volumeId'] = volume_id - - # NOTE(justinsb): And now, we have a problem... - # The attach is async, so there's a window in which we don't see - # the attachment (until the attachment completes). We could also - # get problems with concurrent requests. I think we need an - # attachment state, and to write to the DB here, but that's a bigger - # change. - # For now, we'll probably have to rely on libraries being smart - - # TODO: How do I return "accepted" here?? - return {'volumeAttachment': attachment} - - def update(self, _req, _server_id, _id): - """ Update a volume attachment. We don't currently support this.""" - return faults.Fault(exc.HTTPBadRequest()) - - def delete(self, req, server_id, id): - """ Detach a volume from an instance """ - context = req.environ['nova.context'] - - volume_id = id - LOG.audit(_("Detach volume %s"), volume_id, context=context) - - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - self.compute_api.detach_volume(context, - volume_id=volume_id) - - return exc.HTTPAccepted() - - def _items(self, req, server_id, entity_maker): - """Returns a list of attachments, transformed through entity_maker""" - context = req.environ['nova.context'] - - try: - instance = self.compute_api.get(context, server_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - volumes = instance['volumes'] - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumeAttachments': res} diff --git a/nova/api/openstack/volumes.py b/nova/api/openstack/volumes.py deleted file mode 100644 index ec3b9a6c8..000000000 --- a/nova/api/openstack/volumes.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from webob import exc - -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova import wsgi -from nova.api.openstack import common -from nova.api.openstack import faults - - -LOG = logging.getLogger("nova.api.volumes") - -FLAGS = flags.FLAGS - - -def _translate_detail_view(context, vol): - """ Maps keys for details view""" - - d = _translate_summary_view(context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_summary_view(_context, vol): - """ Maps keys for summary view""" - d = {} - - instance_id = None - # instance_data = None - attached_to = vol.get('instance') - if attached_to: - instance_id = attached_to['id'] - # instance_data = '%s[%s]' % (instance_ec2_id, - # attached_to['host']) - d['id'] = vol['id'] - d['status'] = vol['status'] - d['size'] = vol['size'] - d['availabilityZone'] = vol['availability_zone'] - d['createdAt'] = vol['created_at'] - # if context.is_admin: - # v['status'] = '%s (%s, %s, %s, %s)' % ( - # vol['status'], - # vol['user_id'], - # vol['host'], - # instance_data, - # vol['mountpoint']) - if vol['attach_status'] == 'attached': - d['attachments'] = [{'attachTime': vol['attach_time'], - 'deleteOnTermination': False, - 'mountpoint': vol['mountpoint'], - 'instanceId': instance_id, - 'status': 'attached', - 'volumeId': vol['id']}] - else: - d['attachments'] = [{}] - - d['displayName'] = vol['display_name'] - d['displayDescription'] = vol['display_description'] - return d - - -class Controller(wsgi.Controller): - """ The Volumes API controller for the OpenStack API """ - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "volume": [ - "id", - "status", - "size", - "availabilityZone", - "createdAt", - "displayName", - "displayDescription", - ]}}} - - def __init__(self): - self.volume_api = volume.API() - super(Controller, self).__init__() - - def show(self, req, id): - """Return data about the given volume""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get(context, id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - return {'volume': _translate_detail_view(context, vol)} - - def delete(self, req, id): - """ Delete a volume """ - context = req.environ['nova.context'] - - LOG.audit(_("Delete volume with id: %s"), id, context=context) - - try: - self.volume_api.delete(context, volume_id=id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() - - def index(self, req): - """ Returns a summary list of volumes""" - return self._items(req, entity_maker=_translate_summary_view) - - def detail(self, req): - """ Returns a detailed list of volumes """ - return self._items(req, entity_maker=_translate_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of volumes, transformed through entity_maker""" - context = req.environ['nova.context'] - - volumes = self.volume_api.get_all(context) - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumes': res} - - def create(self, req): - """Creates a new volume""" - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - vol = env['volume'] - size = vol['size'] - LOG.audit(_("Create volume of %s GB"), size, context=context) - new_volume = self.volume_api.create(context, size, - vol.get('display_name'), - vol.get('display_description')) - - # Work around problem that instance is lazy-loaded... - new_volume['instance'] = None - - retval = _translate_detail_view(context, new_volume) - - return {'volume': retval} diff --git a/nova/tests/api/openstack/extensions/foxinsocks.py b/nova/tests/api/openstack/extensions/foxinsocks.py deleted file mode 100644 index 0860b51ac..000000000 --- a/nova/tests/api/openstack/extensions/foxinsocks.py +++ /dev/null @@ -1,98 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json - -from nova import wsgi - -from nova.api.openstack import extensions - - -class FoxInSocksController(wsgi.Controller): - - def index(self, req): - return "Try to say this Mr. Knox, sir..." - - -class Foxinsocks(object): - - def __init__(self): - pass - - def get_name(self): - return "Fox In Socks" - - def get_alias(self): - return "FOXNSOX" - - def get_description(self): - return "The Fox In Socks Extension" - - def get_namespace(self): - return "http://www.fox.in.socks/api/ext/pie/v1.0" - - def get_updated(self): - return "2011-01-22T13:25:27-06:00" - - def get_resources(self): - resources = [] - resource = extensions.ResourceExtension('foxnsocks', - FoxInSocksController()) - resources.append(resource) - return resources - - def get_actions(self): - actions = [] - actions.append(extensions.ActionExtension('servers', 'add_tweedle', - self._add_tweedle)) - actions.append(extensions.ActionExtension('servers', 'delete_tweedle', - self._delete_tweedle)) - return actions - - def get_response_extensions(self): - response_exts = [] - - def _goose_handler(res): - #NOTE: This only handles JSON responses. - # You can use content type header to test for XML. - data = json.loads(res.body) - data['flavor']['googoose'] = "Gooey goo for chewy chewing!" - return data - - resp_ext = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)', - _goose_handler) - response_exts.append(resp_ext) - - def _bands_handler(res): - #NOTE: This only handles JSON responses. - # You can use content type header to test for XML. - data = json.loads(res.body) - data['big_bands'] = 'Pig Bands!' - return data - - resp_ext2 = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)', - _bands_handler) - response_exts.append(resp_ext2) - return response_exts - - def _add_tweedle(self, input_dict, req, id): - - return "Tweedle Beetle Added." - - def _delete_tweedle(self, input_dict, req, id): - - return "Tweedle Beetle Deleted." diff --git a/nova/tests/api/openstack/extensions/foxinsocks/__init__.py b/nova/tests/api/openstack/extensions/foxinsocks/__init__.py new file mode 100644 index 000000000..fe505359d --- /dev/null +++ b/nova/tests/api/openstack/extensions/foxinsocks/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Justin Santa Barbara +# 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 datetime + +"""Example Fox-in-Socks extension.""" diff --git a/nova/tests/api/openstack/extensions/foxinsocks/foxinsocks.py b/nova/tests/api/openstack/extensions/foxinsocks/foxinsocks.py new file mode 100644 index 000000000..442707714 --- /dev/null +++ b/nova/tests/api/openstack/extensions/foxinsocks/foxinsocks.py @@ -0,0 +1,98 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from nova import wsgi + +from nova.api.openstack import extensions + + +class FoxInSocksController(wsgi.Controller): + + def index(self, req): + return "Try to say this Mr. Knox, sir..." + + +class Foxinsocks(extensions.ExtensionDescriptor): + + def __init__(self): + pass + + def get_name(self): + return "Fox In Socks" + + def get_alias(self): + return "FOXNSOX" + + def get_description(self): + return "The Fox In Socks Extension" + + def get_namespace(self): + return "http://www.fox.in.socks/api/ext/pie/v1.0" + + def get_updated(self): + return "2011-01-22T13:25:27-06:00" + + def get_resources(self): + resources = [] + resource = extensions.ResourceExtension('foxnsocks', + FoxInSocksController()) + resources.append(resource) + return resources + + def get_actions(self): + actions = [] + actions.append(extensions.ActionExtension('servers', 'add_tweedle', + self._add_tweedle)) + actions.append(extensions.ActionExtension('servers', 'delete_tweedle', + self._delete_tweedle)) + return actions + + def get_response_extensions(self): + response_exts = [] + + def _goose_handler(res): + #NOTE: This only handles JSON responses. + # You can use content type header to test for XML. + data = json.loads(res.body) + data['flavor']['googoose'] = "Gooey goo for chewy chewing!" + return data + + resp_ext = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)', + _goose_handler) + response_exts.append(resp_ext) + + def _bands_handler(res): + #NOTE: This only handles JSON responses. + # You can use content type header to test for XML. + data = json.loads(res.body) + data['big_bands'] = 'Pig Bands!' + return data + + resp_ext2 = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)', + _bands_handler) + response_exts.append(resp_ext2) + return response_exts + + def _add_tweedle(self, input_dict, req, id): + + return "Tweedle Beetle Added." + + def _delete_tweedle(self, input_dict, req, id): + + return "Tweedle Beetle Deleted." -- cgit From 5936449d99b852897fddbbb140465db0ad9a330c Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Fri, 25 Mar 2011 17:48:59 -0700 Subject: Now that it's an extension, it has to be v1.1. Also fixed up all the things that changed in v1.1 --- nova/api/openstack/__init__.py | 2 -- nova/api/openstack/common.py | 5 ++++ nova/tests/integrated/integrated_helpers.py | 21 ++++++++++---- nova/tests/integrated/test_extensions.py | 43 +++++++++++++++++++++++++++++ nova/tests/integrated/test_servers.py | 16 ++++++----- nova/tests/integrated/test_volumes.py | 6 ++++ 6 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 nova/tests/integrated/test_extensions.py diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 731e16a58..7f2bb1155 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -39,8 +39,6 @@ from nova.api.openstack import servers from nova.api.openstack import server_metadata from nova.api.openstack import shared_ip_groups from nova.api.openstack import users -from nova.api.openstack import volumes -from nova.api.openstack import volume_attachments from nova.api.openstack import zones diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 8cad1273a..4ab6b7a81 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -21,6 +21,10 @@ import webob from nova import exception from nova import flags +from nova import log as logging + + +LOG = logging.getLogger('common') FLAGS = flags.FLAGS @@ -121,4 +125,5 @@ def get_id_from_href(href): try: return int(urlparse(href).path.split('/')[-1]) except: + LOG.debug(_("Error extracting id from href: %s") % href) raise webob.exc.HTTPBadRequest(_('could not parse id from href')) diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index 99d597f9a..c64f6945d 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -196,7 +196,7 @@ class IntegratedUnitTestContext(object): # so we treat it separately self.api_service = api_service - self.auth_url = 'http://localhost:8774/v1.0' + self.auth_url = 'http://localhost:8774/v1.1' return api_service @@ -229,18 +229,27 @@ class _IntegratedTestBase(test.TestCase): server = {} image = self.user.get_valid_image(create=True) - image_id = image['id'] + LOG.debug("Image: %s" % image) - #TODO(justinsb): This is FUBAR - image_id = abs(hash(image_id)) + if 'imageRef' in image: + image_ref = image['imageRef'] + else: + #NOTE(justinsb): The imageRef code hasn't yet landed + LOG.warning("imageRef not yet in images output") + image_ref = image['id'] + + #TODO(justinsb): This is FUBAR + image_ref = abs(hash(image_ref)) + + image_ref = 'http://fake.server/%s' % image_ref # We now have a valid imageId - server['imageId'] = image_id + server['imageRef'] = image_ref # Set a valid flavorId flavor = self.api.get_flavors()[0] LOG.debug("Using flavor: %s" % flavor) - server['flavorId'] = flavor['id'] + server['flavorRef'] = 'http://fake.server/%s' % flavor['id'] # Set a valid server name server_name = self.user.get_unused_server_name() diff --git a/nova/tests/integrated/test_extensions.py b/nova/tests/integrated/test_extensions.py new file mode 100644 index 000000000..39906e8d0 --- /dev/null +++ b/nova/tests/integrated/test_extensions.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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 os + +from nova import flags +from nova.log import logging +from nova.tests.integrated import integrated_helpers + + +LOG = logging.getLogger('nova.tests.integrated') + +FLAGS = flags.FLAGS +FLAGS.verbose = True + + +class ExtensionsTest(integrated_helpers._IntegratedTestBase): + def _get_flags(self): + f = super(ExtensionsTest, self)._get_flags() + f['osapi_extensions_path'] = os.path.join(os.path.dirname(__file__), + "../api/openstack/extensions") + return f + + def test_get_foxnsocks(self): + """Simple check that fox-n-socks works""" + response = self.api.api_request('/foxnsocks') + foxnsocks = response.read() + LOG.debug("foxnsocks: %s" % foxnsocks) + self.assertEqual('Try to say this Mr. Knox, sir...', foxnsocks) diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index c4676adc8..a0a6e333a 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -48,27 +48,29 @@ class ServersTest(integrated_helpers._IntegratedTestBase): post = {'server': server} - # Without an imageId, this throws 500. + # Without an imageRef, this throws 500. # TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) - # With an invalid imageId, this throws 500. - server['imageId'] = self.user.get_invalid_image() + # With an invalid imageRef, this throws 500. + server['imageRef'] = self.user.get_invalid_image() # TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) - # Add a valid imageId - server['imageId'] = good_server['imageId'] + # Add a valid imageId/imageRef + server['imageId'] = good_server.get('imageId') + server['imageRef'] = good_server.get('imageRef') # Without flavorId, this throws 500 # TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) - # Set a valid flavorId - server['flavorId'] = good_server['flavorId'] + # Set a valid flavorId/flavorRef + server['flavorRef'] = good_server.get('flavorRef') + server['flavorId'] = good_server.get('flavorId') # Without a name, this throws 500 # TODO(justinsb): Check whatever the spec says should be thrown here diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py index f173efea7..9ddfe85f7 100644 --- a/nova/tests/integrated/test_volumes.py +++ b/nova/tests/integrated/test_volumes.py @@ -42,6 +42,12 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): f['volume_driver'] = 'nova.volume.driver.LoggingVolumeDriver' return f + def test_get_volumes_summary(self): + """Simple check that listing volumes works""" + volumes = self.api.get_volumes(False) + for volume in volumes: + LOG.debug("volume: %s" % volume) + def test_get_volumes(self): """Simple check that listing volumes works""" volumes = self.api.get_volumes() -- cgit From cb8a13e3751cc12f7157094d094c7a26d6f583f0 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Sun, 27 Mar 2011 15:30:47 -0400 Subject: Fix utils checking --- nova/virt/disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index fd45e2c51..15cd49789 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -124,7 +124,7 @@ def setup_container(image, container_dir=None): """ nbd = "False" device = _link_device(image, nbd) - err = utils.execute('sudo', 'mount', device, container_dir) + out, err = utils.execute('sudo', 'mount', device, container_dir) if err: raise exception.Error(_('Failed to mount filesystem: %s') % err) -- cgit From b3f8e9fb546c621946563af0908e43cb01c50431 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Sun, 27 Mar 2011 18:48:32 -0700 Subject: Bunch of style fixes --- nova/api/openstack/common.py | 1 + nova/api/openstack/extensions.py | 2 +- .../incubator/volumes/volume_attachments.py | 21 +++++++++++---------- nova/api/openstack/incubator/volumes/volumes.py | 13 +++++++------ nova/compute/manager.py | 2 +- nova/image/fake.py | 1 + nova/tests/integrated/api/client.py | 4 +++- nova/tests/integrated/integrated_helpers.py | 12 +++++------- nova/tests/integrated/test_extensions.py | 1 + nova/tests/integrated/test_servers.py | 15 ++++++++------- nova/tests/integrated/test_volumes.py | 7 ++++--- nova/virt/driver.py | 13 ++++++------- 12 files changed, 49 insertions(+), 43 deletions(-) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 4ab6b7a81..75aeb0a5f 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -26,6 +26,7 @@ from nova import log as logging LOG = logging.getLogger('common') + FLAGS = flags.FLAGS diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 6a8ce9669..e81ffb3d3 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -68,7 +68,7 @@ class ExtensionDescriptor(object): """The timestamp when the extension was last updated e.g. '2011-01-22T13:25:27-06:00'""" - #NOTE(justinsb): Huh? Isn't this defined by the namespace? + #NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS raise NotImplementedError() def get_resources(self): diff --git a/nova/api/openstack/incubator/volumes/volume_attachments.py b/nova/api/openstack/incubator/volumes/volume_attachments.py index 58a9a727b..93786d1c7 100644 --- a/nova/api/openstack/incubator/volumes/volume_attachments.py +++ b/nova/api/openstack/incubator/volumes/volume_attachments.py @@ -29,11 +29,12 @@ from nova.api.openstack import faults LOG = logging.getLogger("nova.api.volumes") + FLAGS = flags.FLAGS def _translate_detail_view(context, volume): - """ Maps keys for details view""" + """Maps keys for details view""" d = _translate_summary_view(context, volume) @@ -43,12 +44,12 @@ def _translate_detail_view(context, volume): def _translate_summary_view(context, vol): - """ Maps keys for summary view""" + """Maps keys for summary view""" d = {} volume_id = vol['id'] - # NOTE(justinsb): We use the volume id as the id of the attachment object + #NOTE(justinsb): We use the volume id as the id of the attachment object d['id'] = volume_id d['volumeId'] = volume_id @@ -61,7 +62,7 @@ def _translate_summary_view(context, vol): class Controller(wsgi.Controller): - """ The volume attachment API controller for the Openstack API + """The volume attachment API controller for the Openstack API A child resource of the server. Note that we use the volume id as the ID of the attachment (though this is not guaranteed externally)""" @@ -80,7 +81,7 @@ class Controller(wsgi.Controller): super(Controller, self).__init__() def index(self, req, server_id): - """ Returns the list of volume attachments for a given instance """ + """Returns the list of volume attachments for a given instance """ return self._items(req, server_id, entity_maker=_translate_summary_view) @@ -102,7 +103,7 @@ class Controller(wsgi.Controller): return {'volumeAttachment': _translate_detail_view(context, vol)} def create(self, req, server_id): - """ Attach a volume to an instance """ + """Attach a volume to an instance """ context = req.environ['nova.context'] env = self._deserialize(req.body, req.get_content_type()) @@ -130,7 +131,7 @@ class Controller(wsgi.Controller): attachment['id'] = volume_id attachment['volumeId'] = volume_id - # NOTE(justinsb): And now, we have a problem... + #NOTE(justinsb): And now, we have a problem... # The attach is async, so there's a window in which we don't see # the attachment (until the attachment completes). We could also # get problems with concurrent requests. I think we need an @@ -138,15 +139,15 @@ class Controller(wsgi.Controller): # change. # For now, we'll probably have to rely on libraries being smart - # TODO: How do I return "accepted" here?? + #TODO(justinsb): How do I return "accepted" here? return {'volumeAttachment': attachment} def update(self, _req, _server_id, _id): - """ Update a volume attachment. We don't currently support this.""" + """Update a volume attachment. We don't currently support this.""" return faults.Fault(exc.HTTPBadRequest()) def delete(self, req, server_id, id): - """ Detach a volume from an instance """ + """Detach a volume from an instance """ context = req.environ['nova.context'] volume_id = id diff --git a/nova/api/openstack/incubator/volumes/volumes.py b/nova/api/openstack/incubator/volumes/volumes.py index ec3b9a6c8..e122bb465 100644 --- a/nova/api/openstack/incubator/volumes/volumes.py +++ b/nova/api/openstack/incubator/volumes/volumes.py @@ -26,11 +26,12 @@ from nova.api.openstack import faults LOG = logging.getLogger("nova.api.volumes") + FLAGS = flags.FLAGS def _translate_detail_view(context, vol): - """ Maps keys for details view""" + """Maps keys for details view""" d = _translate_summary_view(context, vol) @@ -40,7 +41,7 @@ def _translate_detail_view(context, vol): def _translate_summary_view(_context, vol): - """ Maps keys for summary view""" + """Maps keys for summary view""" d = {} instance_id = None @@ -78,7 +79,7 @@ def _translate_summary_view(_context, vol): class Controller(wsgi.Controller): - """ The Volumes API controller for the OpenStack API """ + """The Volumes API controller for the OpenStack API""" _serialization_metadata = { 'application/xml': { @@ -109,7 +110,7 @@ class Controller(wsgi.Controller): return {'volume': _translate_detail_view(context, vol)} def delete(self, req, id): - """ Delete a volume """ + """Delete a volume """ context = req.environ['nova.context'] LOG.audit(_("Delete volume with id: %s"), id, context=context) @@ -121,11 +122,11 @@ class Controller(wsgi.Controller): return exc.HTTPAccepted() def index(self, req): - """ Returns a summary list of volumes""" + """Returns a summary list of volumes""" return self._items(req, entity_maker=_translate_summary_view) def detail(self, req): - """ Returns a detailed list of volumes """ + """Returns a detailed list of volumes """ return self._items(req, entity_maker=_translate_detail_view) def _items(self, req, entity_maker): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 468771f46..10636f602 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1078,6 +1078,6 @@ class ComputeManager(manager.SchedulerDependentManager): # Are there VMs not in the DB? for vm_not_found_in_db in vms_not_found_in_db: name = vm_not_found_in_db - # TODO(justinsb): What to do here? Adopt it? Shut it down? + #TODO(justinsb): What to do here? Adopt it? Shut it down? LOG.warning(_("Found VM not in DB: '%(name)s'. Ignoring") % locals()) diff --git a/nova/image/fake.py b/nova/image/fake.py index 9d28e316d..29159f337 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -24,6 +24,7 @@ from nova.image import service LOG = logging.getLogger('nova.image.fake') + FLAGS = flags.FLAGS diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index 023871bda..688ba8778 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -56,7 +56,9 @@ class OpenStackApiNotFoundException(OpenStackApiException): class TestOpenStackClient(object): - """ A really basic OpenStack API client that is under our control, + """Simple OpenStack API Client. + + This is a really basic OpenStack API client that is under our control, so we can make changes / insert hooks for testing""" def __init__(self, auth_user, auth_key, auth_uri): diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index c64f6945d..7aed69099 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -85,10 +85,10 @@ class TestUser(object): def get_valid_image(self, create=False): images = self.openstack_api.get_images() if create and not images: - # TODO(justinsb): No way to create an image through API??? + #TODO(justinsb): No way currently to create an image through API #created_image = self.openstack_api.post_image(image) #images.append(created_image) - raise exception.Error("No way to create an image through API??") + raise exception.Error("No way to create an image through API") if images: return images[0] @@ -124,7 +124,7 @@ class IntegratedUnitTestContext(object): self._start_volume_service() self._start_scheduler_service() - # NOTE(justinsb): There's a bug here which is eluding me... + #NOTE(justinsb): There's a bug here which is eluding me... # If we start the network_service, all is good, but then subsequent # tests fail: CloudTestCase.test_ajax_console in particular. #self._start_network_service() @@ -192,7 +192,7 @@ class IntegratedUnitTestContext(object): if not api_service: raise Exception("API Service was None") - # NOTE(justinsb): The API service doesn't have a kill method yet, + #NOTE(justinsb): The API service doesn't have a kill method yet, # so we treat it separately self.api_service = api_service @@ -217,9 +217,7 @@ class _IntegratedTestBase(test.TestCase): super(_IntegratedTestBase, self).tearDown() def _get_flags(self): - """An opportunity to setup flags, before the services are started - - Warning - this is a bit flaky till the WSGI recycle code lands""" + """An opportunity to setup flags, before the services are started""" f = {} f['image_service'] = 'nova.image.fake.MockImageService' f['fake_network'] = True diff --git a/nova/tests/integrated/test_extensions.py b/nova/tests/integrated/test_extensions.py index 39906e8d0..1aa471f49 100644 --- a/nova/tests/integrated/test_extensions.py +++ b/nova/tests/integrated/test_extensions.py @@ -24,6 +24,7 @@ from nova.tests.integrated import integrated_helpers LOG = logging.getLogger('nova.tests.integrated') + FLAGS = flags.FLAGS FLAGS.verbose = True diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index a0a6e333a..59e5dde14 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -26,6 +26,7 @@ from nova.tests.integrated.api import client LOG = logging.getLogger('nova.tests.integrated') + FLAGS = flags.FLAGS FLAGS.verbose = True @@ -49,13 +50,13 @@ class ServersTest(integrated_helpers._IntegratedTestBase): post = {'server': server} # Without an imageRef, this throws 500. - # TODO(justinsb): Check whatever the spec says should be thrown here + #TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) # With an invalid imageRef, this throws 500. server['imageRef'] = self.user.get_invalid_image() - # TODO(justinsb): Check whatever the spec says should be thrown here + #TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) @@ -64,7 +65,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): server['imageRef'] = good_server.get('imageRef') # Without flavorId, this throws 500 - # TODO(justinsb): Check whatever the spec says should be thrown here + #TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) @@ -73,7 +74,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): server['flavorId'] = good_server.get('flavorId') # Without a name, this throws 500 - # TODO(justinsb): Check whatever the spec says should be thrown here + #TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) @@ -105,7 +106,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): break # It should be available... - # TODO(justinsb): Mock doesn't yet do this... + #TODO(justinsb): Mock doesn't yet do this... #self.assertEqual('available', found_server['status']) self._delete_server(created_server_id) @@ -125,7 +126,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): LOG.debug("Found_server=%s" % found_server) - # TODO(justinsb): Mock doesn't yet do accurate state changes + #TODO(justinsb): Mock doesn't yet do accurate state changes #if found_server['status'] != 'deleting': # break time.sleep(1) @@ -133,7 +134,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): # Should be gone self.assertFalse(found_server) -# TODO(justinsb): Enable this unit test when the metadata bug is fixed +#TODO(justinsb): Enable this unit test when the metadata bug is fixed # def test_create_server_with_metadata(self): # """Creates a server with metadata""" # diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py index 9ddfe85f7..89480618d 100644 --- a/nova/tests/integrated/test_volumes.py +++ b/nova/tests/integrated/test_volumes.py @@ -27,6 +27,7 @@ from nova.volume import driver LOG = logging.getLogger('nova.tests.integrated') + FLAGS = flags.FLAGS FLAGS.verbose = True @@ -55,7 +56,7 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): LOG.debug("volume: %s" % volume) def _poll_while(self, volume_id, continue_states, max_retries=5): - """ Poll (briefly) while the state is in continue_states""" + """Poll (briefly) while the state is in continue_states""" retries = 0 while True: try: @@ -144,7 +145,7 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): # Create server server_req = {'server': self._build_minimal_create_server_request()} - # NOTE(justinsb): Create an extra server so that server_id != volume_id + #NOTE(justinsb): Create an extra server so that server_id != volume_id self.api.post_server(server_req) created_server = self.api.post_server(server_req) LOG.debug("created_server: %s" % created_server) @@ -197,7 +198,7 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): # This is just an implementation detail, but let's check it... self.assertEquals(volume_id, attachment_id) - # NOTE(justinsb): There's an issue with the attach code, in that + #NOTE(justinsb): There's an issue with the attach code, in that # it's currently asynchronous and not recorded until the attach # completes. So the caller must be 'smart', like this... attach_done = None diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 1bc3e4dfc..eafede17a 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -66,8 +66,7 @@ class ComputeDriver(object): raise NotImplementedError() def destroy(self, instance, cleanup=True): - """ - Destroy (shutdown and delete) the specified instance. + """Destroy (shutdown and delete) the specified instance. The given parameter is an instance of nova.compute.service.Instance, and so the instance is being specified as instance.name. @@ -89,12 +88,12 @@ class ComputeDriver(object): raise NotImplementedError() def get_console_pool_info(self, console_type): - """??? + """? Returns a dict containing: - :address: ??? - :username: ??? - :password: ??? + :address: ? + :username: ? + :password: ? """ raise NotImplementedError() @@ -126,7 +125,7 @@ class ComputeDriver(object): raise NotImplementedError() def snapshot(self, instance, image_id): - """ Create snapshot from a running VM instance """ + """Create snapshot from a running VM instance """ raise NotImplementedError() def finish_resize(self, instance, disk_info): -- cgit From a1accc23427347205f7f6c49402a4f366e5256b6 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Sun, 27 Mar 2011 19:38:20 -0700 Subject: pep8 fixes --- nova/tests/integrated/api/client.py | 2 +- nova/tests/integrated/integrated_helpers.py | 85 +++++++++-------------------- 2 files changed, 26 insertions(+), 61 deletions(-) diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index 688ba8778..b9ed7b03b 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -57,7 +57,7 @@ class OpenStackApiNotFoundException(OpenStackApiException): class TestOpenStackClient(object): """Simple OpenStack API Client. - + This is a really basic OpenStack API client that is under our control, so we can make changes / insert hooks for testing""" diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index 7aed69099..bc516b4b3 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -96,12 +96,10 @@ class TestUser(object): class IntegratedUnitTestContext(object): - def __init__(self): + def __init__(self, auth_url): self.auth_manager = manager.AuthManager() - self.api_service = None - self.services = [] - self.auth_url = None + self.auth_url = auth_url self.project_name = None self.test_user = None @@ -109,7 +107,6 @@ class IntegratedUnitTestContext(object): self.setup() def setup(self): - self._start_services() self._create_test_user() def _create_test_user(self): @@ -119,47 +116,8 @@ class IntegratedUnitTestContext(object): self.project_name = 'openstack' self._configure_project(self.project_name, self.test_user) - def _start_services(self): - self._start_compute_service() - self._start_volume_service() - self._start_scheduler_service() - - #NOTE(justinsb): There's a bug here which is eluding me... - # If we start the network_service, all is good, but then subsequent - # tests fail: CloudTestCase.test_ajax_console in particular. - #self._start_network_service() - - self._start_api_service() - - def _start_compute_service(self): - compute_service = service.Service.create(binary='nova-compute') - compute_service.start() - self.services.append(compute_service) - return compute_service - - def _start_network_service(self): - network_service = service.Service.create(binary='nova-network') - network_service.start() - self.services.append(network_service) - return network_service - - def _start_volume_service(self): - volume_service = service.Service.create(binary='nova-volume') - volume_service.start() - self.services.append(volume_service) - return volume_service - - def _start_scheduler_service(self): - scheduler_service = service.Service.create(binary='nova-scheduler') - scheduler_service.start() - self.services.append(scheduler_service) - return scheduler_service - def cleanup(self): self.test_user = None - for svc in self.services: - svc.kill() - self.services = [] def _create_unittest_user(self): users = self.auth_manager.get_users() @@ -185,21 +143,6 @@ class IntegratedUnitTestContext(object): else: self.auth_manager.add_to_project(user.name, project_name) - def _start_api_service(self): - api_service = service.ApiService.create() - api_service.start() - - if not api_service: - raise Exception("API Service was None") - - #NOTE(justinsb): The API service doesn't have a kill method yet, - # so we treat it separately - self.api_service = api_service - - self.auth_url = 'http://localhost:8774/v1.1' - - return api_service - class _IntegratedTestBase(test.TestCase): def setUp(self): @@ -208,10 +151,32 @@ class _IntegratedTestBase(test.TestCase): f = self._get_flags() self.flags(**f) - self.context = IntegratedUnitTestContext() + # set up services + self.start_service('compute') + self.start_service('volume') + #NOTE(justinsb): There's a bug here which is eluding me... + # If we start the network_service, all is good, but then subsequent + # tests fail: CloudTestCase.test_ajax_console in particular. + #self.start_service('network') + self.start_service('scheduler') + + self.auth_url = self._start_api_service() + + self.context = IntegratedUnitTestContext(self.auth_url) + self.user = self.context.test_user self.api = self.user.openstack_api + def _start_api_service(self): + api_service = service.ApiService.create() + api_service.start() + + if not api_service: + raise Exception("API Service was None") + + auth_url = 'http://localhost:8774/v1.1' + return auth_url + def tearDown(self): self.context.cleanup() super(_IntegratedTestBase, self).tearDown() -- cgit From 8922630e70e97b52e363a861c76fe4a01b8418ff Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 28 Mar 2011 09:37:05 -0400 Subject: Fix typo in libvirt xml template --- nova/virt/libvirt.xml.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index 26f528cb1..a62c3b6a8 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -3,7 +3,7 @@ ${memory_kb} #if $type == 'lxc' - #set $disk prefix = '' + #set $disk_prefix = '' #set $disk_bus = '' exe /sbin/init -- cgit From 4aad5721bff628ef8b34e0c536e0e2415f2b63f4 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 28 Mar 2011 11:16:59 -0400 Subject: Removed 'is not None' to do more general truth-checking. Added rather verbose testing. --- nova/image/glance.py | 2 +- nova/tests/api/openstack/test_images.py | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/nova/image/glance.py b/nova/image/glance.py index be9805b69..f0f1ecf57 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -220,7 +220,7 @@ def _convert_timestamps_to_datetimes(image_meta): Returns image with known timestamp fields converted to datetime objects """ for attr in ['created_at', 'updated_at', 'deleted_at']: - if image_meta.get(attr) is not None: + if image_meta.get(attr): image_meta[attr] = _parse_glance_iso8601_timestamp( image_meta[attr]) return image_meta diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 1cdccadd6..88dd8f506 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -197,6 +197,73 @@ class GlanceImageServiceTest(test.TestCase, image_metas = self.service.detail(self.context) self.assertDictMatch(image_metas[0], expected) + def test_image_valid_date_format(self): + """ + Ensure 'created_at', 'updated_at', and 'deleted_at' can be valid + ISO strings. + """ + fixture = { + 'name': 'test image', + 'is_public': False, + 'properties': { + 'instance_id': '42', + 'user_id': '1', + }, + } + + valid_dates = ['2010-10-11T10:30:22', '2012-12-21T00:00:00'] + + for date in valid_dates: + fixture["created_at"] = date + fixture["updated_at"] = date + fixture["deleted_at"] = date + image_id = self.service.create(self.context, fixture)['id'] + self.assertDictMatch(self.sent_to_glance['metadata'], fixture) + + def test_image_invalid_date_format(self): + """ + Ensure `created_at`, `modified_at` can't be invalid dates. + """ + fixture = { + 'name': 'test image', + 'is_public': False, + 'properties': { + 'instance_id': '42', + 'user_id': '1', + }, + } + + invalid_dates = ['Not a date.', 4] + + for date in invalid_dates: + fixture["created_at"] = date + fixture["updated_at"] = date + fixture["deleted_at"] = date + self.assertRaises((TypeError, ValueError), self.service.create, + self.context, fixture) + + def test_image_blank_date_format(self): + """ + Ensure `created_at`, `modified_at` can be blank dates. + """ + fixture = { + 'name': 'test image', + 'is_public': False, + 'properties': { + 'instance_id': '42', + 'user_id': '1', + }, + } + + blank_dates = [None, ''] + + for date in blank_dates: + fixture["created_at"] = date + fixture["updated_at"] = date + fixture["deleted_at"] = date + image_id = self.service.create(self.context, fixture)['id'] + self.assertDictMatch(self.sent_to_glance['metadata'], fixture) + def test_create_without_instance_id(self): """ Ensure we can create an image without having to specify an @@ -235,6 +302,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): FLAGS.image_service = self.orig_image_service super(ImageControllerWithGlanceServiceTest, self).tearDown() + + def test_get_image_index(self): req = webob.Request.blank('/v1.0/images') res = req.get_response(fakes.wsgi_app()) -- cgit From 23bed216dbbd512e733ecf6065105b2d20703531 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 28 Mar 2011 13:04:02 -0400 Subject: Added MUCH more flexiable iso8601 parser dep for added stability. --- nova/image/glance.py | 8 ++-- nova/tests/api/openstack/test_images.py | 69 --------------------------------- nova/tests/image/test_glance.py | 50 ++++++++++++++++++++++++ tools/pip-requires | 1 + 4 files changed, 54 insertions(+), 74 deletions(-) diff --git a/nova/image/glance.py b/nova/image/glance.py index f0f1ecf57..c08e2e0e8 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -20,6 +20,8 @@ from __future__ import absolute_import import datetime +import iso8601 + from glance.common import exception as glance_exception from nova import exception @@ -230,8 +232,4 @@ def _parse_glance_iso8601_timestamp(timestamp): """ Parse a subset of iso8601 timestamps into datetime objects """ - GLANCE_FMT = "%Y-%m-%dT%H:%M:%S" - ISO_FMT = "%Y-%m-%dT%H:%M:%S.%f" - # FIXME(sirp): Glance is not returning in ISO format, we should fix Glance - # to do so, and then switch to parsing it here - return datetime.datetime.strptime(timestamp, GLANCE_FMT) + return iso8601.parse_date(timestamp).replace(tzinfo=None) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 88dd8f506..1cdccadd6 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -197,73 +197,6 @@ class GlanceImageServiceTest(test.TestCase, image_metas = self.service.detail(self.context) self.assertDictMatch(image_metas[0], expected) - def test_image_valid_date_format(self): - """ - Ensure 'created_at', 'updated_at', and 'deleted_at' can be valid - ISO strings. - """ - fixture = { - 'name': 'test image', - 'is_public': False, - 'properties': { - 'instance_id': '42', - 'user_id': '1', - }, - } - - valid_dates = ['2010-10-11T10:30:22', '2012-12-21T00:00:00'] - - for date in valid_dates: - fixture["created_at"] = date - fixture["updated_at"] = date - fixture["deleted_at"] = date - image_id = self.service.create(self.context, fixture)['id'] - self.assertDictMatch(self.sent_to_glance['metadata'], fixture) - - def test_image_invalid_date_format(self): - """ - Ensure `created_at`, `modified_at` can't be invalid dates. - """ - fixture = { - 'name': 'test image', - 'is_public': False, - 'properties': { - 'instance_id': '42', - 'user_id': '1', - }, - } - - invalid_dates = ['Not a date.', 4] - - for date in invalid_dates: - fixture["created_at"] = date - fixture["updated_at"] = date - fixture["deleted_at"] = date - self.assertRaises((TypeError, ValueError), self.service.create, - self.context, fixture) - - def test_image_blank_date_format(self): - """ - Ensure `created_at`, `modified_at` can be blank dates. - """ - fixture = { - 'name': 'test image', - 'is_public': False, - 'properties': { - 'instance_id': '42', - 'user_id': '1', - }, - } - - blank_dates = [None, ''] - - for date in blank_dates: - fixture["created_at"] = date - fixture["updated_at"] = date - fixture["deleted_at"] = date - image_id = self.service.create(self.context, fixture)['id'] - self.assertDictMatch(self.sent_to_glance['metadata'], fixture) - def test_create_without_instance_id(self): """ Ensure we can create an image without having to specify an @@ -302,8 +235,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): FLAGS.image_service = self.orig_image_service super(ImageControllerWithGlanceServiceTest, self).tearDown() - - def test_get_image_index(self): req = webob.Request.blank('/v1.0/images') res = req.get_response(fakes.wsgi_app()) diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py index d03aa9cc8..dfdce7584 100644 --- a/nova/tests/image/test_glance.py +++ b/nova/tests/image/test_glance.py @@ -57,6 +57,7 @@ class NullWriter(object): class BaseGlanceTest(unittest.TestCase): NOW_GLANCE_FORMAT = "2010-10-11T10:30:22" NOW_DATETIME = datetime.datetime(2010, 10, 11, 10, 30, 22) + NOW_ISO_FORMAT = "2010-10-11T10:30:22.000000" def setUp(self): # FIXME(sirp): we can probably use stubs library here rather than @@ -74,6 +75,10 @@ class BaseGlanceTest(unittest.TestCase): self.assertEqual(image_meta['updated_at'], None) self.assertEqual(image_meta['deleted_at'], None) + def assertDateTimesBlank(self, image_meta): + self.assertEqual(image_meta['updated_at'], '') + self.assertEqual(image_meta['deleted_at'], '') + class TestGlanceImageServiceProperties(BaseGlanceTest): def test_show_passes_through_to_client(self): @@ -108,33 +113,65 @@ class TestGetterDateTimeNoneTests(BaseGlanceTest): image_meta = self.service.show(self.context, 'image1') self.assertDateTimesEmpty(image_meta) + def test_show_handles_blank_datetimes(self): + self.client.images = self._make_blank_datetime_fixtures() + image_meta = self.service.show(self.context, 'image1') + self.assertDateTimesBlank(image_meta) + def test_detail_handles_none_datetimes(self): self.client.images = self._make_none_datetime_fixtures() image_meta = self.service.detail(self.context)[0] self.assertDateTimesEmpty(image_meta) + def test_detail_handles_blank_datetimes(self): + self.client.images = self._make_blank_datetime_fixtures() + image_meta = self.service.detail(self.context)[0] + self.assertDateTimesBlank(image_meta) + def test_get_handles_none_datetimes(self): self.client.images = self._make_none_datetime_fixtures() writer = NullWriter() image_meta = self.service.get(self.context, 'image1', writer) self.assertDateTimesEmpty(image_meta) + def test_get_handles_blank_datetimes(self): + self.client.images = self._make_blank_datetime_fixtures() + writer = NullWriter() + image_meta = self.service.get(self.context, 'image1', writer) + self.assertDateTimesBlank(image_meta) + def test_show_makes_datetimes(self): self.client.images = self._make_datetime_fixtures() image_meta = self.service.show(self.context, 'image1') self.assertDateTimesFilled(image_meta) + def test_show_makes_datetimes_iso(self): + self.client.images = self._make_iso_fixtures() + image_meta = self.service.show(self.context, 'image1') + self.assertDateTimesFilled(image_meta) + def test_detail_makes_datetimes(self): self.client.images = self._make_datetime_fixtures() image_meta = self.service.detail(self.context)[0] self.assertDateTimesFilled(image_meta) + def test_detail_makes_datetimes_iso(self): + self.client.images = self._make_iso_fixtures() + image_meta = self.service.detail(self.context)[0] + self.assertDateTimesFilled(image_meta) + def test_get_makes_datetimes(self): self.client.images = self._make_datetime_fixtures() writer = NullWriter() image_meta = self.service.get(self.context, 'image1', writer) self.assertDateTimesFilled(image_meta) + def test_get_makes_datetimes_iso(self): + self.client.images = self._make_iso_fixtures() + writer = NullWriter() + image_meta = self.service.get(self.context, 'image1', writer) + self.assertDateTimesFilled(image_meta) + def _make_datetime_fixtures(self): fixtures = {'image1': {'name': 'image1', 'is_public': True, 'created_at': self.NOW_GLANCE_FORMAT, @@ -142,12 +179,25 @@ class TestGetterDateTimeNoneTests(BaseGlanceTest): 'deleted_at': self.NOW_GLANCE_FORMAT}} return fixtures + def _make_iso_fixtures(self): + fixtures = {'image1': {'name': 'image1', 'is_public': True, + 'created_at': self.NOW_ISO_FORMAT, + 'updated_at': self.NOW_ISO_FORMAT, + 'deleted_at': self.NOW_ISO_FORMAT}} + return fixtures + def _make_none_datetime_fixtures(self): fixtures = {'image1': {'name': 'image1', 'is_public': True, 'updated_at': None, 'deleted_at': None}} return fixtures + def _make_blank_datetime_fixtures(self): + fixtures = {'image1': {'name': 'image1', 'is_public': True, + 'updated_at': '', + 'deleted_at': ''}} + return fixtures + class TestMutatorDateTimeTests(BaseGlanceTest): """Tests create(), update()""" diff --git a/tools/pip-requires b/tools/pip-requires index 4ab9644d8..5cd80d1f4 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -31,3 +31,4 @@ netaddr sphinx glance suds==0.4 +iso8601 -- cgit From 56b4dd3929448585c15c8d11c5fe1569ce21ea7d Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 28 Mar 2011 13:18:47 -0400 Subject: Removed extra dependency as per suggestion, although it fixes the issue much better IMO, we should be safe sticking with using the format from python's isoformat() --- nova/image/glance.py | 5 ++--- nova/tests/image/test_glance.py | 26 +------------------------- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/nova/image/glance.py b/nova/image/glance.py index c08e2e0e8..32c9fa6be 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -20,8 +20,6 @@ from __future__ import absolute_import import datetime -import iso8601 - from glance.common import exception as glance_exception from nova import exception @@ -232,4 +230,5 @@ def _parse_glance_iso8601_timestamp(timestamp): """ Parse a subset of iso8601 timestamps into datetime objects """ - return iso8601.parse_date(timestamp).replace(tzinfo=None) + ISO_FMT = "%Y-%m-%dT%H:%M:%S.%f" + return datetime.datetime.strptime(timestamp, ISO_FMT) diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py index dfdce7584..dfa754b89 100644 --- a/nova/tests/image/test_glance.py +++ b/nova/tests/image/test_glance.py @@ -55,9 +55,8 @@ class NullWriter(object): class BaseGlanceTest(unittest.TestCase): - NOW_GLANCE_FORMAT = "2010-10-11T10:30:22" + NOW_GLANCE_FORMAT = "2010-10-11T10:30:22.000000" NOW_DATETIME = datetime.datetime(2010, 10, 11, 10, 30, 22) - NOW_ISO_FORMAT = "2010-10-11T10:30:22.000000" def setUp(self): # FIXME(sirp): we can probably use stubs library here rather than @@ -145,33 +144,17 @@ class TestGetterDateTimeNoneTests(BaseGlanceTest): image_meta = self.service.show(self.context, 'image1') self.assertDateTimesFilled(image_meta) - def test_show_makes_datetimes_iso(self): - self.client.images = self._make_iso_fixtures() - image_meta = self.service.show(self.context, 'image1') - self.assertDateTimesFilled(image_meta) - def test_detail_makes_datetimes(self): self.client.images = self._make_datetime_fixtures() image_meta = self.service.detail(self.context)[0] self.assertDateTimesFilled(image_meta) - def test_detail_makes_datetimes_iso(self): - self.client.images = self._make_iso_fixtures() - image_meta = self.service.detail(self.context)[0] - self.assertDateTimesFilled(image_meta) - def test_get_makes_datetimes(self): self.client.images = self._make_datetime_fixtures() writer = NullWriter() image_meta = self.service.get(self.context, 'image1', writer) self.assertDateTimesFilled(image_meta) - def test_get_makes_datetimes_iso(self): - self.client.images = self._make_iso_fixtures() - writer = NullWriter() - image_meta = self.service.get(self.context, 'image1', writer) - self.assertDateTimesFilled(image_meta) - def _make_datetime_fixtures(self): fixtures = {'image1': {'name': 'image1', 'is_public': True, 'created_at': self.NOW_GLANCE_FORMAT, @@ -179,13 +162,6 @@ class TestGetterDateTimeNoneTests(BaseGlanceTest): 'deleted_at': self.NOW_GLANCE_FORMAT}} return fixtures - def _make_iso_fixtures(self): - fixtures = {'image1': {'name': 'image1', 'is_public': True, - 'created_at': self.NOW_ISO_FORMAT, - 'updated_at': self.NOW_ISO_FORMAT, - 'deleted_at': self.NOW_ISO_FORMAT}} - return fixtures - def _make_none_datetime_fixtures(self): fixtures = {'image1': {'name': 'image1', 'is_public': True, 'updated_at': None, -- cgit From e043561a8d5dc0c3183ec7e3a5a44f2aa306d2fd Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 28 Mar 2011 13:20:46 -0400 Subject: Removed iso8601 dep from pip-requires --- tools/pip-requires | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/pip-requires b/tools/pip-requires index 5cd80d1f4..4ab9644d8 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -31,4 +31,3 @@ netaddr sphinx glance suds==0.4 -iso8601 -- cgit From 805cb3609379827d1643785be83f75b69b602d74 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 28 Mar 2011 13:44:33 -0400 Subject: Fix libvirt merge mistake --- nova/virt/libvirt.xml.template | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index a62c3b6a8..894b216e9 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -83,22 +83,24 @@ #end if #end if #end if + +#for $nic in $nics - - + + - - - -#if $getVar('extra_params', False) - ${extra_params} + + + +#if $getVar('nic.extra_params', False) + ${nic.extra_params} #end if -#if $getVar('gateway_v6', False) - +#if $getVar('nic.gateway_v6', False) + #end if - +#end for -- cgit From de07a6409d575af5db748bdbfa2cc57881136d66 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 28 Mar 2011 13:47:18 -0400 Subject: Decided to not break old format so this should work with the way Glance used to work and the way glace works now..The best of both worlds? --- nova/image/glance.py | 12 ++++++++++-- nova/tests/image/test_glance.py | 27 +++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/nova/image/glance.py b/nova/image/glance.py index 32c9fa6be..34fc78e71 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -230,5 +230,13 @@ def _parse_glance_iso8601_timestamp(timestamp): """ Parse a subset of iso8601 timestamps into datetime objects """ - ISO_FMT = "%Y-%m-%dT%H:%M:%S.%f" - return datetime.datetime.strptime(timestamp, ISO_FMT) + ISO_FORMATS = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"] + + for iso_format in ISO_FORMATS: + try: + return datetime.datetime.strptime(timestamp, iso_format) + except ValueError: + pass + + raise ValueError(_("""%(timestamp)s does not follow any of the \ +signatures: %(ISO_FORMATS)s""") % (locals())) diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py index dfa754b89..9d0b14613 100644 --- a/nova/tests/image/test_glance.py +++ b/nova/tests/image/test_glance.py @@ -55,6 +55,7 @@ class NullWriter(object): class BaseGlanceTest(unittest.TestCase): + NOW_GLANCE_OLD_FORMAT = "2010-10-11T10:30:22" NOW_GLANCE_FORMAT = "2010-10-11T10:30:22.000000" NOW_DATETIME = datetime.datetime(2010, 10, 11, 10, 30, 22) @@ -143,23 +144,41 @@ class TestGetterDateTimeNoneTests(BaseGlanceTest): self.client.images = self._make_datetime_fixtures() image_meta = self.service.show(self.context, 'image1') self.assertDateTimesFilled(image_meta) + image_meta = self.service.show(self.context, 'image2') + self.assertDateTimesFilled(image_meta) def test_detail_makes_datetimes(self): self.client.images = self._make_datetime_fixtures() image_meta = self.service.detail(self.context)[0] self.assertDateTimesFilled(image_meta) + image_meta = self.service.detail(self.context)[1] + self.assertDateTimesFilled(image_meta) def test_get_makes_datetimes(self): self.client.images = self._make_datetime_fixtures() writer = NullWriter() image_meta = self.service.get(self.context, 'image1', writer) self.assertDateTimesFilled(image_meta) + image_meta = self.service.get(self.context, 'image2', writer) + self.assertDateTimesFilled(image_meta) def _make_datetime_fixtures(self): - fixtures = {'image1': {'name': 'image1', 'is_public': True, - 'created_at': self.NOW_GLANCE_FORMAT, - 'updated_at': self.NOW_GLANCE_FORMAT, - 'deleted_at': self.NOW_GLANCE_FORMAT}} + fixtures = { + 'image1': { + 'name': 'image1', + 'is_public': True, + 'created_at': self.NOW_GLANCE_FORMAT, + 'updated_at': self.NOW_GLANCE_FORMAT, + 'deleted_at': self.NOW_GLANCE_FORMAT, + }, + 'image2': { + 'name': 'image2', + 'is_public': True, + 'created_at': self.NOW_GLANCE_OLD_FORMAT, + 'updated_at': self.NOW_GLANCE_OLD_FORMAT, + 'deleted_at': self.NOW_GLANCE_OLD_FORMAT, + }, + } return fixtures def _make_none_datetime_fixtures(self): -- cgit From 9c61085ea9612be24e9975ac3fba456874b89f08 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 28 Mar 2011 13:49:51 -0400 Subject: Add more unit tests for lxc --- nova/tests/test_virt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index cee044279..ed7943ace 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -257,7 +257,9 @@ class LibvirtConnTestCase(test.TestCase): check = [ (lambda t: t.find('.').get('type'), 'lxc'), - (lambda t: t.find('./os/type').text, 'exe')] + (lambda t: t.find('./os/type').text, 'exe'), + (lambda t: t.find('./devices/filesystem/source').get('dir'), 'rootfs'), + (lambda t: t.find('./devices/filesystem/target').get('dir'), '/')] for i, (check, expected_result) in enumerate(check): self.assertEqual(check(tree), -- cgit From e44ce1a95baddd4f6d511d8be253167436395cb2 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 28 Mar 2011 14:07:33 -0400 Subject: More pep8 corrections --- nova/tests/test_virt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index ed7943ace..8cee55576 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -258,8 +258,8 @@ class LibvirtConnTestCase(test.TestCase): check = [ (lambda t: t.find('.').get('type'), 'lxc'), (lambda t: t.find('./os/type').text, 'exe'), - (lambda t: t.find('./devices/filesystem/source').get('dir'), 'rootfs'), - (lambda t: t.find('./devices/filesystem/target').get('dir'), '/')] + (lambda t: t.find('./devices/filesystem/source').get('dir'), 'rootfs'), + (lambda t: t.find('./devices/filesystem/target').get('dir'), '/')] for i, (check, expected_result) in enumerate(check): self.assertEqual(check(tree), -- cgit From c8e708af1789fda97674fb4c3904d86de8473a7e Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 28 Mar 2011 14:32:03 -0400 Subject: Dont make the test fail --- nova/tests/test_virt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 8cee55576..df29e69c2 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -258,7 +258,6 @@ class LibvirtConnTestCase(test.TestCase): check = [ (lambda t: t.find('.').get('type'), 'lxc'), (lambda t: t.find('./os/type').text, 'exe'), - (lambda t: t.find('./devices/filesystem/source').get('dir'), 'rootfs'), (lambda t: t.find('./devices/filesystem/target').get('dir'), '/')] for i, (check, expected_result) in enumerate(check): @@ -266,6 +265,9 @@ class LibvirtConnTestCase(test.TestCase): expected_result, '%s failed common check %d' % (xml, i)) + target = tree.find('./devices/filesystem/source').get('dir') + self.assertTrue(len(target) > 0) + def _check_xml_and_uri(self, instance, expect_ramdisk, expect_kernel, rescue=False): user_context = context.RequestContext(project=self.project, -- cgit From bb7ed6cb9cf625b675a666866a7f9fb762ca6bd2 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 28 Mar 2011 14:47:25 -0400 Subject: use self.flags in virt test --- nova/tests/test_virt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index df29e69c2..e6beb8e2e 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -246,7 +246,7 @@ class LibvirtConnTestCase(test.TestCase): {'allocated': True, 'instance_id': instance_ref['id']}) - FLAGS.libvirt_type = 'lxc' + self.flags(libvirt_type='lxc') conn = libvirt_conn.LibvirtConnection(True) uri = conn.get_uri() -- cgit From 78a9ec232cde1172fa4c639ebdcbf88967bf8e9c Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 28 Mar 2011 15:10:34 -0400 Subject: Fixed superfluous parentheses around locals(). --- nova/image/glance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/image/glance.py b/nova/image/glance.py index 34fc78e71..d8f51429e 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -230,13 +230,13 @@ def _parse_glance_iso8601_timestamp(timestamp): """ Parse a subset of iso8601 timestamps into datetime objects """ - ISO_FORMATS = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"] + iso_formats = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"] - for iso_format in ISO_FORMATS: + for iso_format in iso_formats: try: return datetime.datetime.strptime(timestamp, iso_format) except ValueError: pass raise ValueError(_("""%(timestamp)s does not follow any of the \ -signatures: %(ISO_FORMATS)s""") % (locals())) +signatures: %(ISO_FORMATS)s""") % locals()) -- cgit From dbbceaebec3ca2a729582f9851f718b2b7c3f3b9 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 28 Mar 2011 15:57:18 -0400 Subject: Fix up libvirt.xml.template --- nova/virt/libvirt.xml.template | 138 ++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 70 deletions(-) diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index 894b216e9..36d18ed95 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -3,47 +3,45 @@ ${memory_kb} #if $type == 'lxc' - #set $disk_prefix = '' - #set $disk_bus = '' - exe - /sbin/init + #set $disk_prefix = '' + #set $disk_bus = '' + exe + /sbin/init +#else if $type == 'uml' + #set $disk_prefix = 'ubd' + #set $disk_bus = 'uml' + uml + /usr/bin/linux + /dev/ubda #else - #if $type == 'uml' - #set $disk_prefix = 'ubd' - #set $disk_bus = 'uml' - uml - /usr/bin/linux - /dev/ubda - #else - #if $type == 'xen' - #set $disk_prefix = 'sd' - #set $disk_bus = 'scsi' - linux - /dev/xvda - #else - #set $disk_prefix = 'vd' - #set $disk_bus = 'virtio' - hvm - #end if - #if $getVar('rescue', False) - ${basepath}/kernel.rescue - ${basepath}/ramdisk.rescue - #else - #if $getVar('kernel', None) - ${kernel} - #if $type == 'xen' - ro - #else - root=/dev/vda console=ttyS0 - #end if - #if $getVar('ramdisk', None) - ${ramdisk} - #end if - #else - - #end if - #end if - #end if + #if $type == 'xen' + #set $disk_prefix = 'sd' + #set $disk_bus = 'scsi' + linux + /dev/xvda + #else + #set $disk_prefix = 'vd' + #set $disk_bus = 'virtio' + hvm + #end if + #if $getVar('rescue', False) + ${basepath}/kernel.rescue + ${basepath}/ramdisk.rescue + #else + #if $getVar('kernel', None) + ${kernel} + #if $type == 'xen' + ro + #else + root=/dev/vda console=ttyS0 + #end if + #if $getVar('ramdisk', None) + ${ramdisk} + #end if + #else + + #end if + #end if #end if @@ -52,36 +50,36 @@ ${vcpus} #if $type == 'lxc' - - - - + + + + #else - #if $getVar('rescue', False) - - - - - - - - - - - #else - - - - - - #if $getVar('local', False) - - - - - - #end if -#end if + #if $getVar('rescue', False) + + + + + + + + + + + #else + + + + + + #if $getVar('local', False) + + + + + + #end if + #end if #end if #for $nic in $nics @@ -91,7 +89,7 @@ - + #if $getVar('nic.extra_params', False) ${nic.extra_params} #end if -- cgit From abdd9a5078bef240fac91085f4af2da3f86e0b4e Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Mon, 28 Mar 2011 15:30:25 -0700 Subject: add period, test github --- bin/nova-vnc-proxy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy index e7b647c00..d39a9613e 100755 --- a/bin/nova-vnc-proxy +++ b/bin/nova-vnc-proxy @@ -17,7 +17,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""VNC Console Proxy Server""" +"""VNC Console Proxy Server.""" import eventlet import gettext -- cgit From 94092e3d896732fa1a97627f0fa504c3af70b3c5 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Mon, 28 Mar 2011 15:38:09 -0700 Subject: address some of termie's recommendations --- bin/nova-vnc-proxy | 3 +++ nova/api/openstack/servers.py | 4 ++-- nova/compute/api.py | 2 +- nova/tests/test_compute.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy index d39a9613e..e26bc6d8c 100755 --- a/bin/nova-vnc-proxy +++ b/bin/nova-vnc-proxy @@ -40,8 +40,10 @@ from nova import version from nova.vnc import auth from nova.vnc import proxy + LOG = logging.getLogger('nova.vnc-proxy') + FLAGS = flags.FLAGS flags.DEFINE_string('vnc_proxy_wwwroot', '/var/lib/nova/noVNC/', 'Full path to noVNC directory') @@ -57,6 +59,7 @@ flags.DEFINE_flag(flags.HelpFlag()) flags.DEFINE_flag(flags.HelpshortFlag()) flags.DEFINE_flag(flags.HelpXMLFlag()) + if __name__ == "__main__": utils.default_flagfile() FLAGS(sys.argv) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index c0fba4bb9..822342149 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -473,7 +473,7 @@ class Controller(wsgi.Controller): @scheduler_api.redirect_handler def get_ajax_console(self, req, id): - """ Returns a url to an instance's ajaxterm console. """ + """Returns a url to an instance's ajaxterm console.""" try: self.compute_api.get_ajax_console(req.environ['nova.context'], int(id)) @@ -483,7 +483,7 @@ class Controller(wsgi.Controller): @scheduler_api.redirect_handler def get_vnc_console(self, req, id): - """ Returns a url to an instance's ajaxterm console. """ + """Returns a url to an instance's ajaxterm console.""" try: self.compute_api.get_vnc_console(req.environ['nova.context'], int(id)) diff --git a/nova/compute/api.py b/nova/compute/api.py index f19c552e9..5470f40dc 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -618,7 +618,7 @@ class API(base.Base): {'method': 'authorize_vnc_console', 'args': {'token': output['token'], 'host': output['host'], - 'port': output['port']}}) + 'port': output['port']}}) return {'url': '%s/vnc_auto.html?token=%s&host=%s&port=%s' % ( FLAGS.vnc_console_proxy_url, diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 0be08a778..038824ef8 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -290,7 +290,7 @@ class ComputeTestCase(test.TestCase): self.compute.terminate_instance(self.context, instance_id) def test_vnc_console(self): - """Make sure we can a vnc console for an instance""" + """Make sure we can a vnc console for an instance.""" instance_id = self._create_instance() self.compute.run_instance(self.context, instance_id) -- cgit From 233fd092018ace16f0b9caaca55f4e2107c41fe2 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 28 Mar 2011 15:46:01 -0700 Subject: Fixes volume smoketests to work with ami-tty --- smoketests/test_sysadmin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smoketests/test_sysadmin.py b/smoketests/test_sysadmin.py index 9bed1e092..89a3e60c4 100644 --- a/smoketests/test_sysadmin.py +++ b/smoketests/test_sysadmin.py @@ -266,10 +266,10 @@ class VolumeTests(base.UserSmokeTestCase): ip = self.data['instance'].private_dns_name conn = self.connect_ssh(ip, TEST_KEY) stdin, stdout, stderr = conn.exec_command( - "blockdev --getsize64 %s" % self.device) + "cat /sys/class/block/%s/size" % self.device.rpartition('/')[2]) out = stdout.read().strip() conn.close() - expected_size = 1024 * 1024 * 1024 + expected_size = 1024 * 1024 * 1024 / 512 self.assertEquals('%s' % (expected_size,), out, 'Volume is not the right size: %s %s. Expected: %s' % (out, stderr.read(), expected_size)) -- cgit From 57a4864e30df604612a347ba069ccc8499b04f1f Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 28 Mar 2011 16:30:31 -0700 Subject: Code cleanup to keep the termie-bot happy --- nova/api/openstack/extensions.py | 131 +++++++++++++++------------------------ 1 file changed, 50 insertions(+), 81 deletions(-) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index e81ffb3d3..9566d3250 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -38,55 +38,55 @@ FLAGS = flags.FLAGS class ExtensionDescriptor(object): - """This is the base class that defines the contract for extensions""" + """This is the base class that defines the contract for extensions.""" def get_name(self): - """The name of the extension + """The name of the extension. e.g. 'Fox In Socks' """ raise NotImplementedError() def get_alias(self): - """The alias for the extension + """The alias for the extension. e.g. 'FOXNSOX'""" raise NotImplementedError() def get_description(self): - """Friendly description for the extension + """Friendly description for the extension. e.g. 'The Fox In Socks Extension'""" raise NotImplementedError() def get_namespace(self): - """The XML namespace for the extension + """The XML namespace for the extension. e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'""" raise NotImplementedError() def get_updated(self): - """The timestamp when the extension was last updated + """The timestamp when the extension was last updated. e.g. '2011-01-22T13:25:27-06:00'""" - #NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS + # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS raise NotImplementedError() def get_resources(self): - """List of extensions.ResourceExtension extension objects + """List of extensions.ResourceExtension extension objects. Resources define new nouns, and are accessible through URLs""" resources = [] return resources def get_actions(self): - """List of extensions.ActionExtension extension objects + """List of extensions.ActionExtension extension objects. Actions are verbs callable from the API""" actions = [] return actions def get_response_extensions(self): - """List of extensions.ResponseExtension extension objects + """List of extensions.ResponseExtension extension objects. Response extensions are used to insert information into existing response data""" @@ -154,17 +154,17 @@ class ExtensionController(wsgi.Controller): ext_data['description'] = ext.get_description() ext_data['namespace'] = ext.get_namespace() ext_data['updated'] = ext.get_updated() - ext_data['links'] = [] # TODO: implement extension links + ext_data['links'] = [] # TODO(dprince): implement extension links return ext_data def index(self, req): extensions = [] - for alias, ext in self.extension_manager.extensions.iteritems(): + for _alias, ext in self.extension_manager.extensions.iteritems(): extensions.append(self._translate(ext)) return dict(extensions=extensions) def show(self, req, id): - # NOTE: the extensions alias is used as the 'id' for show + # NOTE(dprince): the extensions alias is used as the 'id' for show ext = self.extension_manager.extensions[id] return self._translate(ext) @@ -176,20 +176,16 @@ class ExtensionController(wsgi.Controller): class ExtensionMiddleware(wsgi.Middleware): - """ - Extensions middleware that intercepts configured routes for extensions. - """ + """Extensions middleware for WSGI.""" @classmethod def factory(cls, global_config, **local_config): - """ paste factory """ + """Paste factory.""" def _factory(app): return cls(app, **local_config) return _factory def _action_ext_controllers(self, application, ext_mgr, mapper): - """ - Return a dict of ActionExtensionController objects by collection - """ + """Return a dict of ActionExtensionController-s by collection.""" action_controllers = {} for action in ext_mgr.get_actions(): if not action.collection in action_controllers.keys(): @@ -208,9 +204,7 @@ class ExtensionMiddleware(wsgi.Middleware): return action_controllers def _response_ext_controllers(self, application, ext_mgr, mapper): - """ - Return a dict of ResponseExtensionController objects by collection - """ + """Returns a dict of ResponseExtensionController-s by collection.""" response_ext_controllers = {} for resp_ext in ext_mgr.get_response_extensions(): if not resp_ext.key in response_ext_controllers.keys(): @@ -269,16 +263,15 @@ class ExtensionMiddleware(wsgi.Middleware): @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): - """ - Route the incoming request with router. - """ + """Route the incoming request with router.""" req.environ['extended.app'] = self.application return self._router @staticmethod @webob.dec.wsgify(RequestClass=wsgi.Request) def _dispatch(req): - """ + """Dispatch the request. + Returns the routed WSGI app's response or defers to the extended application. """ @@ -290,8 +283,7 @@ class ExtensionMiddleware(wsgi.Middleware): class ExtensionManager(object): - """ - Load extensions from the configured extension path. + """Load extensions from the configured extension path. See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an example extension implementation. @@ -307,50 +299,30 @@ class ExtensionManager(object): self._load_all_extensions() def get_resources(self): - """ - returns a list of ResourceExtension objects - """ + """Returns a list of ResourceExtension objects.""" resources = [] resources.append(ResourceExtension('extensions', ExtensionController(self))) - for alias, ext in self.extensions.iteritems(): - try: - resources.extend(ext.get_resources()) - except AttributeError: - # NOTE: Extension aren't required to have resource extensions - pass + for _alias, ext in self.extensions.iteritems(): + resources.extend(ext.get_resources()) return resources def get_actions(self): - """ - returns a list of ActionExtension objects - """ + """Returns a list of ActionExtension objects.""" actions = [] - for alias, ext in self.extensions.iteritems(): - try: - actions.extend(ext.get_actions()) - except AttributeError: - # NOTE: Extension aren't required to have action extensions - pass + for _alias, ext in self.extensions.iteritems(): + actions.extend(ext.get_actions()) return actions def get_response_extensions(self): - """ - returns a list of ResponseExtension objects - """ + """Returns a list of ResponseExtension objects.""" response_exts = [] - for alias, ext in self.extensions.iteritems(): - try: - response_exts.extend(ext.get_response_extensions()) - except AttributeError: - # NOTE: Extension aren't required to have response extensions - pass + for _alias, ext in self.extensions.iteritems(): + response_exts.extend(ext.get_response_extensions()) return response_exts def _check_extension(self, extension): - """ - Checks for required methods in extension objects. - """ + """Checks for required methods in extension objects.""" try: LOG.debug(_('Ext name: %s'), extension.get_name()) LOG.debug(_('Ext alias: %s'), extension.get_alias()) @@ -361,11 +333,17 @@ class ExtensionManager(object): LOG.exception(_("Exception loading extension: %s"), unicode(ex)) def _load_all_extensions(self): - """ - Load extensions from the configured path. The extension name is - constructed from the module_name. If your extension module was named - widgets.py the extension class within that module should be - 'Widgets'. + """Load extensions from the configured path. + + An extension consists of a directory of related files, with a class + that defines a class that inherits from ExtensionDescriptor. + + Because of some oddities involving identically named modules, it's + probably best to name your file after the name of your extension, + rather than something likely to clash like 'extension.py'. + + The name of your directory should be the same as the alias your + extension uses, for everyone's sanity. See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. @@ -409,11 +387,11 @@ class ExtensionManager(object): if not inspect.isclass(cls): continue - #NOTE(justinsb): It seems that python modules aren't great - # If you have two identically named modules, the classes - # from both are mixed in. So name your extension based - # on the alias, not 'extension.py'! - #TODO(justinsb): Any way to work around this? + # NOTE(justinsb): It seems that python modules are odd. + # If you have two identically named modules, the classes + # from both are mixed in. So name your extension based + # on the alias, not 'extension.py'! + # TODO(justinsb): Any way to work around this? if self.super_verbose: LOG.debug(_('Checking class: %s'), cls) @@ -441,10 +419,7 @@ class ExtensionManager(object): class ResponseExtension(object): - """ - ResponseExtension objects can be used to add data to responses from - core nova OpenStack API controllers. - """ + """Add data to responses from core nova OpenStack API controllers.""" def __init__(self, method, url_route, handler): self.url_route = url_route @@ -454,10 +429,7 @@ class ResponseExtension(object): class ActionExtension(object): - """ - ActionExtension objects can be used to add custom actions to core nova - nova OpenStack API controllers. - """ + """Add custom actions to core nova OpenStack API controllers.""" def __init__(self, collection, action_name, handler): self.collection = collection @@ -466,10 +438,7 @@ class ActionExtension(object): class ResourceExtension(object): - """ - ResourceExtension objects can be used to add top level resources - to the OpenStack API in nova. - """ + """Add top level resources to the OpenStack API in nova.""" def __init__(self, collection, controller, parent=None, collection_actions={}, member_actions={}): -- cgit From a6e8c83196cb4b2d8a292a99cb1feb22ed9b21db Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 28 Mar 2011 16:36:25 -0700 Subject: Cleaned up images/fake.py, including move to Duplicate exception --- .../incubator/volumes/volume_attachments.py | 30 +++++++++++----------- nova/api/openstack/incubator/volumes/volumes.py | 18 ++++++------- .../api/openstack/incubator/volumes/volumes_ext.py | 4 +-- nova/compute/manager.py | 2 +- nova/image/fake.py | 29 +++++++++------------ 5 files changed, 39 insertions(+), 44 deletions(-) diff --git a/nova/api/openstack/incubator/volumes/volume_attachments.py b/nova/api/openstack/incubator/volumes/volume_attachments.py index 93786d1c7..1e260b34d 100644 --- a/nova/api/openstack/incubator/volumes/volume_attachments.py +++ b/nova/api/openstack/incubator/volumes/volume_attachments.py @@ -34,7 +34,7 @@ FLAGS = flags.FLAGS def _translate_detail_view(context, volume): - """Maps keys for details view""" + """Maps keys for details view.""" d = _translate_summary_view(context, volume) @@ -44,12 +44,12 @@ def _translate_detail_view(context, volume): def _translate_summary_view(context, vol): - """Maps keys for summary view""" + """Maps keys for summary view.""" d = {} volume_id = vol['id'] - #NOTE(justinsb): We use the volume id as the id of the attachment object + # NOTE(justinsb): We use the volume id as the id of the attachment object d['id'] = volume_id d['volumeId'] = volume_id @@ -62,7 +62,7 @@ def _translate_summary_view(context, vol): class Controller(wsgi.Controller): - """The volume attachment API controller for the Openstack API + """The volume attachment API controller for the Openstack API. A child resource of the server. Note that we use the volume id as the ID of the attachment (though this is not guaranteed externally)""" @@ -81,12 +81,12 @@ class Controller(wsgi.Controller): super(Controller, self).__init__() def index(self, req, server_id): - """Returns the list of volume attachments for a given instance """ + """Returns the list of volume attachments for a given instance.""" return self._items(req, server_id, entity_maker=_translate_summary_view) def show(self, req, server_id, id): - """Return data about the given volume""" + """Return data about the given volume.""" context = req.environ['nova.context'] volume_id = id @@ -103,7 +103,7 @@ class Controller(wsgi.Controller): return {'volumeAttachment': _translate_detail_view(context, vol)} def create(self, req, server_id): - """Attach a volume to an instance """ + """Attach a volume to an instance.""" context = req.environ['nova.context'] env = self._deserialize(req.body, req.get_content_type()) @@ -131,15 +131,15 @@ class Controller(wsgi.Controller): attachment['id'] = volume_id attachment['volumeId'] = volume_id - #NOTE(justinsb): And now, we have a problem... + # NOTE(justinsb): And now, we have a problem... # The attach is async, so there's a window in which we don't see - # the attachment (until the attachment completes). We could also - # get problems with concurrent requests. I think we need an - # attachment state, and to write to the DB here, but that's a bigger - # change. + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. # For now, we'll probably have to rely on libraries being smart - #TODO(justinsb): How do I return "accepted" here? + # TODO(justinsb): How do I return "accepted" here? return {'volumeAttachment': attachment} def update(self, _req, _server_id, _id): @@ -147,7 +147,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPBadRequest()) def delete(self, req, server_id, id): - """Detach a volume from an instance """ + """Detach a volume from an instance.""" context = req.environ['nova.context'] volume_id = id @@ -168,7 +168,7 @@ class Controller(wsgi.Controller): return exc.HTTPAccepted() def _items(self, req, server_id, entity_maker): - """Returns a list of attachments, transformed through entity_maker""" + """Returns a list of attachments, transformed through entity_maker.""" context = req.environ['nova.context'] try: diff --git a/nova/api/openstack/incubator/volumes/volumes.py b/nova/api/openstack/incubator/volumes/volumes.py index e122bb465..a7d5fbaa6 100644 --- a/nova/api/openstack/incubator/volumes/volumes.py +++ b/nova/api/openstack/incubator/volumes/volumes.py @@ -31,7 +31,7 @@ FLAGS = flags.FLAGS def _translate_detail_view(context, vol): - """Maps keys for details view""" + """Maps keys for details view.""" d = _translate_summary_view(context, vol) @@ -41,7 +41,7 @@ def _translate_detail_view(context, vol): def _translate_summary_view(_context, vol): - """Maps keys for summary view""" + """Maps keys for summary view.""" d = {} instance_id = None @@ -79,7 +79,7 @@ def _translate_summary_view(_context, vol): class Controller(wsgi.Controller): - """The Volumes API controller for the OpenStack API""" + """The Volumes API controller for the OpenStack API.""" _serialization_metadata = { 'application/xml': { @@ -99,7 +99,7 @@ class Controller(wsgi.Controller): super(Controller, self).__init__() def show(self, req, id): - """Return data about the given volume""" + """Return data about the given volume.""" context = req.environ['nova.context'] try: @@ -110,7 +110,7 @@ class Controller(wsgi.Controller): return {'volume': _translate_detail_view(context, vol)} def delete(self, req, id): - """Delete a volume """ + """Delete a volume.""" context = req.environ['nova.context'] LOG.audit(_("Delete volume with id: %s"), id, context=context) @@ -122,15 +122,15 @@ class Controller(wsgi.Controller): return exc.HTTPAccepted() def index(self, req): - """Returns a summary list of volumes""" + """Returns a summary list of volumes.""" return self._items(req, entity_maker=_translate_summary_view) def detail(self, req): - """Returns a detailed list of volumes """ + """Returns a detailed list of volumes.""" return self._items(req, entity_maker=_translate_detail_view) def _items(self, req, entity_maker): - """Returns a list of volumes, transformed through entity_maker""" + """Returns a list of volumes, transformed through entity_maker.""" context = req.environ['nova.context'] volumes = self.volume_api.get_all(context) @@ -139,7 +139,7 @@ class Controller(wsgi.Controller): return {'volumes': res} def create(self, req): - """Creates a new volume""" + """Creates a new volume.""" context = req.environ['nova.context'] env = self._deserialize(req.body, req.get_content_type()) diff --git a/nova/api/openstack/incubator/volumes/volumes_ext.py b/nova/api/openstack/incubator/volumes/volumes_ext.py index 87a57320d..6a3bb0265 100644 --- a/nova/api/openstack/incubator/volumes/volumes_ext.py +++ b/nova/api/openstack/incubator/volumes/volumes_ext.py @@ -37,8 +37,8 @@ class VolumesExtension(extensions.ExtensionDescriptor): def get_resources(self): resources = [] - #NOTE(justinsb): No way to provide singular name ('volume') - # Does this matter? + # NOTE(justinsb): No way to provide singular name ('volume') + # Does this matter? res = extensions.ResourceExtension('volumes', volumes.Controller(), collection_actions={'detail': 'GET'} diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 10636f602..468771f46 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1078,6 +1078,6 @@ class ComputeManager(manager.SchedulerDependentManager): # Are there VMs not in the DB? for vm_not_found_in_db in vms_not_found_in_db: name = vm_not_found_in_db - #TODO(justinsb): What to do here? Adopt it? Shut it down? + # TODO(justinsb): What to do here? Adopt it? Shut it down? LOG.warning(_("Found VM not in DB: '%(name)s'. Ignoring") % locals()) diff --git a/nova/image/fake.py b/nova/image/fake.py index 29159f337..cdb650165 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -29,11 +29,11 @@ FLAGS = flags.FLAGS class MockImageService(service.BaseImageService): - """Mock (fake) image service for unit testing""" + """Mock (fake) image service for unit testing.""" def __init__(self): self.images = {} - # NOTE(justinsb): The OpenStack API can't upload an image??? + # NOTE(justinsb): The OpenStack API can't upload an image? # So, make sure we've got one.. image = {'id': '123456', 'status': 'active', @@ -46,17 +46,17 @@ class MockImageService(service.BaseImageService): super(MockImageService, self).__init__() def index(self, context): - """Returns list of images""" + """Returns list of images.""" return self.images.values() def detail(self, context): - """Return list of detailed image information""" + """Return list of detailed image information.""" return self.images.values() def show(self, context, image_id): - """ - Returns a dict containing image data for the given opaque image id. - """ + """Get data about specified image. + + Returns a dict containing image data for the given opaque image id.""" image_id = int(image_id) image = self.images.get(image_id) if image: @@ -66,16 +66,14 @@ class MockImageService(service.BaseImageService): raise exception.NotFound def create(self, context, data): - """ - Store the image data and return the new image id. + """Store the image data and return the new image id. - :raises AlreadyExists if the image already exist. + :raises Duplicate if the image already exist. """ image_id = int(data['id']) if self.images.get(image_id): - #TODO(justinsb): Where is this AlreadyExists exception?? - raise exception.Error("AlreadyExists") + raise exception.Duplicate() self.images[image_id] = data @@ -91,8 +89,7 @@ class MockImageService(service.BaseImageService): self.images[image_id] = data def delete(self, context, image_id): - """ - Delete the given image. + """Delete the given image. :raises NotFound if the image does not exist. @@ -103,7 +100,5 @@ class MockImageService(service.BaseImageService): raise exception.NotFound def delete_all(self): - """ - Clears out all images - """ + """Clears out all images.""" self.images.clear() -- cgit From 276c153f44734e78cae25deb9fc9e79a604c6219 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 28 Mar 2011 16:42:28 -0700 Subject: More fixes to keep the stylebot happy --- nova/tests/integrated/api/client.py | 2 +- nova/tests/integrated/integrated_helpers.py | 20 ++++++++++---------- nova/tests/integrated/test_extensions.py | 2 +- nova/tests/integrated/test_login.py | 8 ++++---- nova/tests/integrated/test_servers.py | 16 ++++++++-------- nova/tests/integrated/test_volumes.py | 18 +++++++++--------- nova/virt/driver.py | 2 +- nova/volume/driver.py | 2 +- 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index b9ed7b03b..b40ff58e3 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -124,7 +124,7 @@ class TestOpenStackClient(object): def api_request(self, relative_uri, check_response_status=None, **kwargs): auth_result = self._authenticate() - #NOTE(justinsb): httplib 'helpfully' converts headers to lower case + # NOTE(justinsb): httplib 'helpfully' converts headers to lower case base_uri = auth_result['x-server-management-url'] full_uri = base_uri + relative_uri diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index bc516b4b3..e0fe2d2a2 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -37,19 +37,19 @@ LOG = logging.getLogger('nova.tests.integrated') def generate_random_alphanumeric(length): - """Creates a random alphanumeric string of specified length""" + """Creates a random alphanumeric string of specified length.""" return ''.join(random.choice(string.ascii_uppercase + string.digits) for _x in range(length)) def generate_random_numeric(length): - """Creates a random numeric string of specified length""" + """Creates a random numeric string of specified length.""" return ''.join(random.choice(string.digits) for _x in range(length)) def generate_new_element(items, prefix, numeric=False): - """Creates a random string with prefix, that is not in 'items' list""" + """Creates a random string with prefix, that is not in 'items' list.""" while True: if numeric: candidate = prefix + generate_random_numeric(8) @@ -85,7 +85,7 @@ class TestUser(object): def get_valid_image(self, create=False): images = self.openstack_api.get_images() if create and not images: - #TODO(justinsb): No way currently to create an image through API + # TODO(justinsb): No way currently to create an image through API #created_image = self.openstack_api.post_image(image) #images.append(created_image) raise exception.Error("No way to create an image through API") @@ -154,9 +154,9 @@ class _IntegratedTestBase(test.TestCase): # set up services self.start_service('compute') self.start_service('volume') - #NOTE(justinsb): There's a bug here which is eluding me... - # If we start the network_service, all is good, but then subsequent - # tests fail: CloudTestCase.test_ajax_console in particular. + # NOTE(justinsb): There's a bug here which is eluding me... + # If we start the network_service, all is good, but then subsequent + # tests fail: CloudTestCase.test_ajax_console in particular. #self.start_service('network') self.start_service('scheduler') @@ -182,7 +182,7 @@ class _IntegratedTestBase(test.TestCase): super(_IntegratedTestBase, self).tearDown() def _get_flags(self): - """An opportunity to setup flags, before the services are started""" + """An opportunity to setup flags, before the services are started.""" f = {} f['image_service'] = 'nova.image.fake.MockImageService' f['fake_network'] = True @@ -197,11 +197,11 @@ class _IntegratedTestBase(test.TestCase): if 'imageRef' in image: image_ref = image['imageRef'] else: - #NOTE(justinsb): The imageRef code hasn't yet landed + # NOTE(justinsb): The imageRef code hasn't yet landed LOG.warning("imageRef not yet in images output") image_ref = image['id'] - #TODO(justinsb): This is FUBAR + # TODO(justinsb): This is FUBAR image_ref = abs(hash(image_ref)) image_ref = 'http://fake.server/%s' % image_ref diff --git a/nova/tests/integrated/test_extensions.py b/nova/tests/integrated/test_extensions.py index 1aa471f49..0d4ee8cab 100644 --- a/nova/tests/integrated/test_extensions.py +++ b/nova/tests/integrated/test_extensions.py @@ -37,7 +37,7 @@ class ExtensionsTest(integrated_helpers._IntegratedTestBase): return f def test_get_foxnsocks(self): - """Simple check that fox-n-socks works""" + """Simple check that fox-n-socks works.""" response = self.api.api_request('/foxnsocks') foxnsocks = response.read() LOG.debug("foxnsocks: %s" % foxnsocks) diff --git a/nova/tests/integrated/test_login.py b/nova/tests/integrated/test_login.py index d6e067c29..a5180b6bc 100644 --- a/nova/tests/integrated/test_login.py +++ b/nova/tests/integrated/test_login.py @@ -31,13 +31,13 @@ FLAGS.verbose = True class LoginTest(integrated_helpers._IntegratedTestBase): def test_login(self): - """Simple check - we list flavors - so we know we're logged in""" + """Simple check - we list flavors - so we know we're logged in.""" flavors = self.api.get_flavors() for flavor in flavors: LOG.debug(_("flavor: %s") % flavor) def test_bad_login_password(self): - """Test that I get a 401 with a bad username""" + """Test that I get a 401 with a bad username.""" bad_credentials_api = client.TestOpenStackClient(self.user.name, "notso_password", self.user.auth_url) @@ -46,7 +46,7 @@ class LoginTest(integrated_helpers._IntegratedTestBase): bad_credentials_api.get_flavors) def test_bad_login_username(self): - """Test that I get a 401 with a bad password""" + """Test that I get a 401 with a bad password.""" bad_credentials_api = client.TestOpenStackClient("notso_username", self.user.secret, self.user.auth_url) @@ -55,7 +55,7 @@ class LoginTest(integrated_helpers._IntegratedTestBase): bad_credentials_api.get_flavors) def test_bad_login_both_bad(self): - """Test that I get a 401 with both bad username and bad password""" + """Test that I get a 401 with both bad username and bad password.""" bad_credentials_api = client.TestOpenStackClient("notso_username", "notso_password", self.user.auth_url) diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index 59e5dde14..749ea8955 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -39,7 +39,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): LOG.debug("server: %s" % server) def test_create_and_delete_server(self): - """Creates and deletes a server""" + """Creates and deletes a server.""" # Create server @@ -50,13 +50,13 @@ class ServersTest(integrated_helpers._IntegratedTestBase): post = {'server': server} # Without an imageRef, this throws 500. - #TODO(justinsb): Check whatever the spec says should be thrown here + # TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) # With an invalid imageRef, this throws 500. server['imageRef'] = self.user.get_invalid_image() - #TODO(justinsb): Check whatever the spec says should be thrown here + # TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) @@ -65,7 +65,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): server['imageRef'] = good_server.get('imageRef') # Without flavorId, this throws 500 - #TODO(justinsb): Check whatever the spec says should be thrown here + # TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) @@ -74,7 +74,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): server['flavorId'] = good_server.get('flavorId') # Without a name, this throws 500 - #TODO(justinsb): Check whatever the spec says should be thrown here + # TODO(justinsb): Check whatever the spec says should be thrown here self.assertRaises(client.OpenStackApiException, self.api.post_server, post) @@ -106,7 +106,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): break # It should be available... - #TODO(justinsb): Mock doesn't yet do this... + # TODO(justinsb): Mock doesn't yet do this... #self.assertEqual('available', found_server['status']) self._delete_server(created_server_id) @@ -126,7 +126,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): LOG.debug("Found_server=%s" % found_server) - #TODO(justinsb): Mock doesn't yet do accurate state changes + # TODO(justinsb): Mock doesn't yet do accurate state changes #if found_server['status'] != 'deleting': # break time.sleep(1) @@ -134,7 +134,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): # Should be gone self.assertFalse(found_server) -#TODO(justinsb): Enable this unit test when the metadata bug is fixed +# TODO(justinsb): Enable this unit test when the metadata bug is fixed # def test_create_server_with_metadata(self): # """Creates a server with metadata""" # diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py index 89480618d..e9fb3c4d1 100644 --- a/nova/tests/integrated/test_volumes.py +++ b/nova/tests/integrated/test_volumes.py @@ -44,19 +44,19 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): return f def test_get_volumes_summary(self): - """Simple check that listing volumes works""" + """Simple check that listing volumes works.""" volumes = self.api.get_volumes(False) for volume in volumes: LOG.debug("volume: %s" % volume) def test_get_volumes(self): - """Simple check that listing volumes works""" + """Simple check that listing volumes works.""" volumes = self.api.get_volumes() for volume in volumes: LOG.debug("volume: %s" % volume) def _poll_while(self, volume_id, continue_states, max_retries=5): - """Poll (briefly) while the state is in continue_states""" + """Poll (briefly) while the state is in continue_states.""" retries = 0 while True: try: @@ -80,7 +80,7 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): return found_volume def test_create_and_delete_volume(self): - """Creates and deletes a volume""" + """Creates and deletes a volume.""" # Create volume created_volume = self.api.post_volume({'volume': {'size': 1}}) @@ -141,11 +141,11 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): self.assertEquals(delete_action['id'], created_volume_id) def test_attach_and_detach_volume(self): - """Creates, attaches, detaches and deletes a volume""" + """Creates, attaches, detaches and deletes a volume.""" # Create server server_req = {'server': self._build_minimal_create_server_request()} - #NOTE(justinsb): Create an extra server so that server_id != volume_id + # NOTE(justinsb): Create an extra server so that server_id != volume_id self.api.post_server(server_req) created_server = self.api.post_server(server_req) LOG.debug("created_server: %s" % created_server) @@ -198,9 +198,9 @@ class VolumesTest(integrated_helpers._IntegratedTestBase): # This is just an implementation detail, but let's check it... self.assertEquals(volume_id, attachment_id) - #NOTE(justinsb): There's an issue with the attach code, in that - # it's currently asynchronous and not recorded until the attach - # completes. So the caller must be 'smart', like this... + # NOTE(justinsb): There's an issue with the attach code, in that + # it's currently asynchronous and not recorded until the attach + # completes. So the caller must be 'smart', like this... attach_done = None retries = 0 while True: diff --git a/nova/virt/driver.py b/nova/virt/driver.py index eafede17a..f85796a97 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -125,7 +125,7 @@ class ComputeDriver(object): raise NotImplementedError() def snapshot(self, instance, image_id): - """Create snapshot from a running VM instance """ + """Create snapshot from a running VM instance.""" raise NotImplementedError() def finish_resize(self, instance, disk_info): diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 161b35d1d..850893914 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -573,7 +573,7 @@ class RBDDriver(VolumeDriver): def discover_volume(self, volume): """Discover volume on a remote host""" - #NOTE(justinsb): This is messed up... discover_volume takes 3 args + # NOTE(justinsb): This is messed up... discover_volume takes 3 args # but then that would break local_path return "rbd:%s/%s" % (FLAGS.rbd_pool, volume['name']) -- cgit From 3a39fb7b09dfee3c971ae4adaeff4717f4839f8a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 28 Mar 2011 16:50:33 -0700 Subject: add note per review --- smoketests/test_sysadmin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/smoketests/test_sysadmin.py b/smoketests/test_sysadmin.py index 89a3e60c4..268d9865b 100644 --- a/smoketests/test_sysadmin.py +++ b/smoketests/test_sysadmin.py @@ -269,6 +269,7 @@ class VolumeTests(base.UserSmokeTestCase): "cat /sys/class/block/%s/size" % self.device.rpartition('/')[2]) out = stdout.read().strip() conn.close() + # NOTE(vish): 1G bytes / 512 bytes per block expected_size = 1024 * 1024 * 1024 / 512 self.assertEquals('%s' % (expected_size,), out, 'Volume is not the right size: %s %s. Expected: %s' % -- cgit From 87bc3bca7904135656ed3a99efc19952be95dcbf Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 28 Mar 2011 16:54:17 -0700 Subject: Multi-line comments should end in a blankline --- nova/api/openstack/incubator/volumes/volume_attachments.py | 4 +++- nova/image/fake.py | 4 +++- nova/tests/integrated/api/client.py | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/incubator/volumes/volume_attachments.py b/nova/api/openstack/incubator/volumes/volume_attachments.py index 1e260b34d..aec4ea8f3 100644 --- a/nova/api/openstack/incubator/volumes/volume_attachments.py +++ b/nova/api/openstack/incubator/volumes/volume_attachments.py @@ -65,7 +65,9 @@ class Controller(wsgi.Controller): """The volume attachment API controller for the Openstack API. A child resource of the server. Note that we use the volume id - as the ID of the attachment (though this is not guaranteed externally)""" + as the ID of the attachment (though this is not guaranteed externally) + + """ _serialization_metadata = { 'application/xml': { diff --git a/nova/image/fake.py b/nova/image/fake.py index cdb650165..4caf68d63 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -56,7 +56,9 @@ class MockImageService(service.BaseImageService): def show(self, context, image_id): """Get data about specified image. - Returns a dict containing image data for the given opaque image id.""" + Returns a dict containing image data for the given opaque image id. + + """ image_id = int(image_id) image = self.images.get(image_id) if image: diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index b40ff58e3..7e20c9b00 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -59,7 +59,9 @@ class TestOpenStackClient(object): """Simple OpenStack API Client. This is a really basic OpenStack API client that is under our control, - so we can make changes / insert hooks for testing""" + so we can make changes / insert hooks for testing + + """ def __init__(self, auth_user, auth_key, auth_uri): super(TestOpenStackClient, self).__init__() -- cgit From 55b801db77b9d631e746e79bd84a7866d1877fb2 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 28 Mar 2011 17:09:39 -0700 Subject: More style changes --- nova/api/openstack/extensions.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 1c39dd0e2..d0b4d378a 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -43,45 +43,59 @@ class ExtensionDescriptor(object): def get_name(self): """The name of the extension. - e.g. 'Fox In Socks' """ + e.g. 'Fox In Socks' + + """ raise NotImplementedError() def get_alias(self): """The alias for the extension. - e.g. 'FOXNSOX'""" + e.g. 'FOXNSOX' + + """ raise NotImplementedError() def get_description(self): """Friendly description for the extension. - e.g. 'The Fox In Socks Extension'""" + e.g. 'The Fox In Socks Extension' + + """ raise NotImplementedError() def get_namespace(self): """The XML namespace for the extension. - e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'""" + e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0' + + """ raise NotImplementedError() def get_updated(self): """The timestamp when the extension was last updated. - e.g. '2011-01-22T13:25:27-06:00'""" + e.g. '2011-01-22T13:25:27-06:00' + + """ # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS raise NotImplementedError() def get_resources(self): """List of extensions.ResourceExtension extension objects. - Resources define new nouns, and are accessible through URLs""" + Resources define new nouns, and are accessible through URLs. + + """ resources = [] return resources def get_actions(self): """List of extensions.ActionExtension extension objects. - Actions are verbs callable from the API""" + Actions are verbs callable from the API. + + """ actions = [] return actions @@ -89,7 +103,9 @@ class ExtensionDescriptor(object): """List of extensions.ResponseExtension extension objects. Response extensions are used to insert information into existing - response data""" + response data. + + """ response_exts = [] return response_exts @@ -274,6 +290,7 @@ class ExtensionMiddleware(wsgi.Middleware): Returns the routed WSGI app's response or defers to the extended application. + """ match = req.environ['wsgiorg.routing_args'][1] if not match: @@ -287,6 +304,7 @@ class ExtensionManager(object): See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an example extension implementation. + """ def __init__(self, path): @@ -347,6 +365,7 @@ class ExtensionManager(object): See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. + """ self._load_extensions_under_path(self.path) -- cgit From 699f82e7e14146feb272d61a98b89ad53c93bf08 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 28 Mar 2011 17:11:16 -0700 Subject: Yet more docstring fixes --- nova/virt/driver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 55281b741..c6c28b2ba 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -77,6 +77,7 @@ class ComputeDriver(object): If the instance is not found (for example if networking failed), this function should still succeed. It's probably a good idea to log a warning in that case. + """ raise NotImplementedError() @@ -94,6 +95,7 @@ class ComputeDriver(object): :address: ? :username: ? :password: ? + """ raise NotImplementedError() -- cgit From 0e81c4175cad97359e848c993efd9a91eb981174 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Mon, 28 Mar 2011 21:22:53 -0400 Subject: Pass along the nbd flags although we dont support it just yet --- nova/virt/disk.py | 13 ++++++++----- nova/virt/libvirt_conn.py | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 15cd49789..11a7a969e 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -116,13 +116,14 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): _unlink_device(device, nbd) -def setup_container(image, container_dir=None): +def setup_container(image, container_dir=None, nbd=False): """Setup the LXC container It will mount the loopback image to the container directory in order - to create the root filesystem for the container + to create the root filesystem for the container. + + LXC does not support qcow2 images yet. """ - nbd = "False" device = _link_device(image, nbd) out, err = utils.execute('sudo', 'mount', device, container_dir) if err: @@ -131,11 +132,13 @@ def setup_container(image, container_dir=None): _unlink_device(device, nbd) -def destroy_container(target, instance): +def destroy_container(target, instance, nbd=False): """Destroy the container once it terminates It will umount the container that is mounted, try to find the loopback device associated with the container and delete it. + + LXC does not support qcow2 images yet. """ try: container_dir = '%s/rootfs' % target @@ -145,7 +148,7 @@ def destroy_container(target, instance): for loop in out.splitlines(): if instance['name'] in loop: device = loop.split(loop, ':') - utils.execute('sudo', 'losetup', '--detach', device) + _unlink_device(device, nbd) def _link_device(image, nbd): diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 66d0d195b..af2dac9e8 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -342,7 +342,7 @@ class LibvirtConnection(driver.ComputeDriver): LOG.info(_('instance %(instance_name)s: deleting instance files' ' %(target)s') % locals()) if FLAGS.libvirt_type == 'lxc': - disk.destroy_container(target, instance) + disk.destroy_container(target, instance,nbd=FLAGS.use_cow_images) if os.path.exists(target): shutil.rmtree(target) @@ -797,7 +797,8 @@ class LibvirtConnection(driver.ComputeDriver): if FLAGS.libvirt_type == 'lxc': disk.setup_container(basepath('disk'), - container_dir=container_dir) + container_dir=container_dir, + nbd=FLAGS.use_cow_images) except Exception as e: # This could be a windows image, or a vmdk format disk LOG.warn(_('instance %(inst_name)s: ignoring error injecting' -- cgit From ad6735df0de8676768353516eee4af62af2c993c Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 29 Mar 2011 08:56:42 -0400 Subject: Catch the error that mount might through a bit better --- nova/virt/disk.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 11a7a969e..8adef744f 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -124,11 +124,11 @@ def setup_container(image, container_dir=None, nbd=False): LXC does not support qcow2 images yet. """ - device = _link_device(image, nbd) - out, err = utils.execute('sudo', 'mount', device, container_dir) - if err: - raise exception.Error(_('Failed to mount filesystem: %s') - % err) + try: + device = _link_device(image, nbd) + utils.execute('sudo', 'mount', device, container_dir) + except Exception, exn: + LOG.exception(_('Failed to mount filesystem: %s'), exn) _unlink_device(device, nbd) -- cgit From beec33e8dbffbe3ea02eccec4952705698db377a Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 29 Mar 2011 10:01:54 -0400 Subject: Fix pep8 error --- nova/virt/libvirt_conn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index c4d41881d..ae3a967a4 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -347,7 +347,7 @@ class LibvirtConnection(driver.ComputeDriver): LOG.info(_('instance %(instance_name)s: deleting instance files' ' %(target)s') % locals()) if FLAGS.libvirt_type == 'lxc': - disk.destroy_container(target, instance,nbd=FLAGS.use_cow_images) + disk.destroy_container(target, instance, nbd=FLAGS.use_cow_images) if os.path.exists(target): shutil.rmtree(target) -- cgit From 07d985f8db30863bb9171e14fccbdb51d7b35f11 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Tue, 29 Mar 2011 12:06:52 -0400 Subject: Switch string concat style. --- nova/image/glance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/image/glance.py b/nova/image/glance.py index d8f51429e..fdf468594 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -238,5 +238,5 @@ def _parse_glance_iso8601_timestamp(timestamp): except ValueError: pass - raise ValueError(_("""%(timestamp)s does not follow any of the \ -signatures: %(ISO_FORMATS)s""") % locals()) + raise ValueError(_("%(timestamp)s does not follow any of the " + "signatures: %(ISO_FORMATS)s") % locals()) -- cgit From 3b8aa1fed7cd6d1deb580eec0af283947060c04d Mon Sep 17 00:00:00 2001 From: Ilya Alekseyev Date: Tue, 29 Mar 2011 21:26:17 +0400 Subject: Added checks that exists at least one network marked inhected in libvirt and xenapi --- nova/virt/libvirt_conn.py | 5 ++++- nova/virt/xenapi/vm_utils.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 80eb64f3c..7dd09813d 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -803,6 +803,7 @@ class LibvirtConnection(driver.ComputeDriver): nets = [] ifc_template = open(FLAGS.injected_network_template).read() ifc_num = -1 + have_injected_networks = False admin_context = context.get_admin_context() for (network_ref, mapping) in network_info: ifc_num += 1 @@ -810,6 +811,7 @@ class LibvirtConnection(driver.ComputeDriver): if not 'injected' in network_ref: continue + have_injected_networks = True address = mapping['ips'][0]['ip'] address_v6 = None if FLAGS.use_ipv6: @@ -825,7 +827,8 @@ class LibvirtConnection(driver.ComputeDriver): 'netmask_v6': network_ref['netmask_v6']} nets.append(net_info) - net = str(Template(ifc_template, + if have_injected_networks: + net = str(Template(ifc_template, searchList=[{'interfaces': nets, 'use_ipv6': FLAGS.use_ipv6}])) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 2288ea8a5..18c29134d 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -1108,11 +1108,13 @@ def _prepare_injectables(inst, networks_info): if networks_info: ifc_num = -1 interfaces_info = [] + have_injected_networks = False for (network_ref, info) in networks_info: ifc_num += 1 if not network_ref['injected']: continue + have_injected_networks = True ip_v4 = ip_v6 = None if 'ips' in info and len(info['ips']) > 0: ip_v4 = info['ips'][0] @@ -1131,7 +1133,9 @@ def _prepare_injectables(inst, networks_info): 'gateway_v6': ip_v6 and ip_v6['gateway'] or '', 'use_ipv6': FLAGS.use_ipv6} interfaces_info.append(interface_info) - net = str(template(template_data, + + if have_injected_networks: + net = str(template(template_data, searchList=[{'interfaces': interfaces_info, 'use_ipv6': FLAGS.use_ipv6}])) return key, net -- cgit From 8f4176f289142e48d4a2c584ad1ce270dfa53d82 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 29 Mar 2011 14:56:46 -0400 Subject: Fix up docstring --- nova/virt/disk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 8adef744f..ddea1a1f7 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -117,7 +117,7 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): def setup_container(image, container_dir=None, nbd=False): - """Setup the LXC container + """Setup the LXC container. It will mount the loopback image to the container directory in order to create the root filesystem for the container. @@ -133,7 +133,7 @@ def setup_container(image, container_dir=None, nbd=False): def destroy_container(target, instance, nbd=False): - """Destroy the container once it terminates + """Destroy the container once it terminates. It will umount the container that is mounted, try to find the loopback device associated with the container and delete it. -- cgit From b161ae983edbd9e8c302907e8951075546eafc48 Mon Sep 17 00:00:00 2001 From: Ilya Alekseyev Date: Tue, 29 Mar 2011 23:30:06 +0400 Subject: style fix --- nova/virt/libvirt_conn.py | 4 ++-- nova/virt/xenapi/vm_utils.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 7dd09813d..aaa0fe8d3 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -829,8 +829,8 @@ class LibvirtConnection(driver.ComputeDriver): if have_injected_networks: net = str(Template(ifc_template, - searchList=[{'interfaces': nets, - 'use_ipv6': FLAGS.use_ipv6}])) + searchList=[{'interfaces': nets, + 'use_ipv6': FLAGS.use_ipv6}])) if key or net: inst_name = inst['name'] diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 18c29134d..d07d60800 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -1136,6 +1136,6 @@ def _prepare_injectables(inst, networks_info): if have_injected_networks: net = str(template(template_data, - searchList=[{'interfaces': interfaces_info, - 'use_ipv6': FLAGS.use_ipv6}])) + searchList=[{'interfaces': interfaces_info, + 'use_ipv6': FLAGS.use_ipv6}])) return key, net -- cgit From 05a654211ab902cbb5b1b345dd3285efb1c6bf71 Mon Sep 17 00:00:00 2001 From: Chuck Short Date: Tue, 29 Mar 2011 15:48:02 -0400 Subject: Style fixes --- nova/tests/test_virt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index e6beb8e2e..958c8e3e2 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -237,13 +237,13 @@ class LibvirtConnTestCase(test.TestCase): network_ref = db.project_get_network(context.get_admin_context(), self.project.id) - fixed_ip = {'address': self.test_ip, - 'network_id': network_ref['id']} + fixed_ip = {'address': self.test_ip, + 'network_id': network_ref['id']} ctxt = context.get_admin_context() fixed_ip_ref = db.fixed_ip_create(ctxt, fixed_ip) db.fixed_ip_update(ctxt, self.test_ip, - {'allocated': True, + {'allocated': True, 'instance_id': instance_ref['id']}) self.flags(libvirt_type='lxc') -- cgit From 3e9b5977137c430d218ec8c00e286b691ea8367d Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 12:54:35 -0700 Subject: use manager pattern for auth token proxy --- bin/nova-vnc-proxy | 96 ----------------------------------- bin/nova-vncproxy | 101 +++++++++++++++++++++++++++++++++++++ doc/source/runnova/vncconsole.rst | 6 +-- nova/flags.py | 2 +- nova/vnc/auth.py | 103 +++++++++++++++++++++++--------------- nova/vnc/proxy.py | 3 +- 6 files changed, 168 insertions(+), 143 deletions(-) delete mode 100755 bin/nova-vnc-proxy create mode 100755 bin/nova-vncproxy diff --git a/bin/nova-vnc-proxy b/bin/nova-vnc-proxy deleted file mode 100755 index e26bc6d8c..000000000 --- a/bin/nova-vnc-proxy +++ /dev/null @@ -1,96 +0,0 @@ -#!/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. - -"""VNC Console Proxy Server.""" - -import eventlet -import gettext -import os -import sys - -possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), - os.pardir, - os.pardir)) -if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): - sys.path.insert(0, possible_topdir) - -gettext.install('nova', unicode=1) - -from nova import flags -from nova import log as logging -from nova import utils -from nova import wsgi -from nova import version -from nova.vnc import auth -from nova.vnc import proxy - - -LOG = logging.getLogger('nova.vnc-proxy') - - -FLAGS = flags.FLAGS -flags.DEFINE_string('vnc_proxy_wwwroot', '/var/lib/nova/noVNC/', - 'Full path to noVNC directory') -flags.DEFINE_boolean('vnc_debug', False, - 'Enable debugging features, like token bypassing') -flags.DEFINE_integer('vnc_proxy_port', 6080, - 'Port that the VNC proxy should bind to') -flags.DEFINE_string('vnc_proxy_host', '0.0.0.0', - 'Address that the VNC proxy should bind to') -flags.DEFINE_integer('vnc_token_ttl', 300, - 'How many seconds before deleting tokens') -flags.DEFINE_flag(flags.HelpFlag()) -flags.DEFINE_flag(flags.HelpshortFlag()) -flags.DEFINE_flag(flags.HelpXMLFlag()) - - -if __name__ == "__main__": - utils.default_flagfile() - FLAGS(sys.argv) - logging.setup() - - LOG.audit(_("Starting nova-vnc-proxy node (version %s)"), - version.version_string_with_vcs()) - - if not (os.path.exists(FLAGS.vnc_proxy_wwwroot) and - os.path.exists(FLAGS.vnc_proxy_wwwroot + '/vnc_auto.html')): - LOG.info(_("Missing vnc_proxy_wwwroot (version %s)"), - FLAGS.vnc_proxy_wwwroot) - LOG.info(_("You need a slightly modified version of noVNC " - "to work with the nova-vnc-proxy")) - LOG.info(_("Check out the most recent nova noVNC code: %s"), - "git://github.com/sleepsonthefloor/noVNC.git") - LOG.info(_("And drop it in %s"), FLAGS.vnc_proxy_wwwroot) - exit(1) - - app = proxy.WebsocketVNCProxy(FLAGS.vnc_proxy_wwwroot) - - LOG.audit(_("Allowing access to the following files: %s"), - app.get_whitelist()) - - with_logging = auth.LoggingMiddleware(app) - - if FLAGS.vnc_debug: - with_auth = proxy.DebugMiddleware(with_logging) - else: - with_auth = auth.NovaAuthMiddleware(with_logging) - - server = wsgi.Server() - server.start(with_auth, FLAGS.vnc_proxy_port, host=FLAGS.vnc_proxy_host) - server.wait() diff --git a/bin/nova-vncproxy b/bin/nova-vncproxy new file mode 100755 index 000000000..0fad8397d --- /dev/null +++ b/bin/nova-vncproxy @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 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. + +"""VNC Console Proxy Server.""" + +import eventlet +import gettext +import os +import sys + +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + +gettext.install('nova', unicode=1) + +from nova import flags +from nova import log as logging +from nova import service +from nova import utils +from nova import wsgi +from nova import version +from nova.vnc import auth +from nova.vnc import proxy + + +LOG = logging.getLogger('nova.vnc-proxy') + + +FLAGS = flags.FLAGS +flags.DEFINE_string('vncproxy_wwwroot', '/var/lib/nova/noVNC/', + 'Full path to noVNC directory') +flags.DEFINE_boolean('vnc_debug', False, + 'Enable debugging features, like token bypassing') +flags.DEFINE_integer('vncproxy_port', 6080, + 'Port that the VNC proxy should bind to') +flags.DEFINE_string('vncproxy_host', '0.0.0.0', + 'Address that the VNC proxy should bind to') +flags.DEFINE_integer('vnc_token_ttl', 300, + 'How many seconds before deleting tokens') +flags.DEFINE_string('vncproxy_manager', 'nova.vnc.auth.VNCProxyAuthManager', + 'Manager for vncproxy auth') + +flags.DEFINE_flag(flags.HelpFlag()) +flags.DEFINE_flag(flags.HelpshortFlag()) +flags.DEFINE_flag(flags.HelpXMLFlag()) + + +if __name__ == "__main__": + utils.default_flagfile() + FLAGS(sys.argv) + logging.setup() + + LOG.audit(_("Starting nova-vnc-proxy node (version %s)"), + version.version_string_with_vcs()) + + service.serve() + + if not (os.path.exists(FLAGS.vncproxy_wwwroot) and + os.path.exists(FLAGS.vncproxy_wwwroot + '/vnc_auto.html')): + LOG.info(_("Missing vncproxy_wwwroot (version %s)"), + FLAGS.vncproxy_wwwroot) + LOG.info(_("You need a slightly modified version of noVNC " + "to work with the nova-vnc-proxy")) + LOG.info(_("Check out the most recent nova noVNC code: %s"), + "git://github.com/sleepsonthefloor/noVNC.git") + LOG.info(_("And drop it in %s"), FLAGS.vncproxy_wwwroot) + exit(1) + + app = proxy.WebsocketVNCProxy(FLAGS.vncproxy_wwwroot) + + LOG.audit(_("Allowing access to the following files: %s"), + app.get_whitelist()) + + with_logging = auth.LoggingMiddleware(app) + + if FLAGS.vnc_debug: + with_auth = proxy.DebugMiddleware(with_logging) + else: + with_auth = auth.VNCNovaAuthMiddleware(with_logging) + + server = wsgi.Server() + server.start(with_auth, FLAGS.vncproxy_port, host=FLAGS.vncproxy_host) + server.wait() diff --git a/doc/source/runnova/vncconsole.rst b/doc/source/runnova/vncconsole.rst index 69f147613..6d93bad93 100644 --- a/doc/source/runnova/vncconsole.rst +++ b/doc/source/runnova/vncconsole.rst @@ -40,14 +40,14 @@ you can at find git://github.com/sleepsonthefloor/noVNC.git. .. todo:: add instruction for installing from package -noVNC must be in the location specified by --vnc_proxy_wwwroot, which defaults +noVNC must be in the location specified by --vncproxy_wwwroot, which defaults to /var/lib/nova/noVNC. nova-vnc-proxy will fail to launch until this code is properly installed. By default, nova-vnc-proxy binds 0.0.0.0:6080. This can be configured with: -* --vnc_proxy_port=[port] -* --vnc_proxy_host=[host] +* --vncproxy_port=[port] +* --vncproxy_host=[host] Enabling VNC Consoles in Nova diff --git a/nova/flags.py b/nova/flags.py index ba543f46d..b5c0cd380 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -281,7 +281,7 @@ DEFINE_string('ajax_console_proxy_url', in the form "http://127.0.0.1:8000"') DEFINE_string('ajax_console_proxy_port', 8000, 'port that ajax_console_proxy binds') -DEFINE_string('vnc_console_proxy_topic', 'vnc_proxy', +DEFINE_string('vnc_console_proxy_topic', 'vncproxy', 'the topic vnc proxy nodes listen on') DEFINE_string('vnc_console_proxy_url', 'http://127.0.0.1:6080', diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index dff9b376f..105b68fe2 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -1,9 +1,7 @@ #!/usr/bin/env python -# pylint: disable-msg=C0103 # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. +# Copyright (c) 2010 Openstack, LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,8 +24,10 @@ import webob from webob import Request +from nova import context from nova import flags from nova import log as logging +from nova import manager from nova import rpc from nova import utils from nova import wsgi @@ -38,12 +38,24 @@ LOG = logging.getLogger('nova.vnc-proxy') FLAGS = flags.FLAGS -class NovaAuthMiddleware(object): +class VNCNovaAuthMiddleware(object): """Implementation of Middleware to Handle Nova Auth.""" def __init__(self, app): self.app = app - self.register_listeners() + self.token_cache = {} + utils.LoopingCall(self._delete_expired_tokens).start(1) + + def get_token_info(self, token): + if token in self.token_cache: + return self.token_cache[token] + + rval = rpc.call(context.get_admin_context(), + FLAGS.vnc_console_proxy_topic, + {"method": "check_token", "args": {'token': token}}) + if rval: + self.token_cache[token] = rval + return rval @webob.dec.wsgify def __call__(self, req): @@ -55,49 +67,27 @@ class NovaAuthMiddleware(object): if 'token' in auth_params: token = auth_params['token'][0] - if not token in self.tokens: + connection_info = self.get_token_info(token) + if not connection_info: LOG.audit(_("Unauthorized Access: (%s)"), req.environ) return webob.exc.HTTPForbidden(detail='Unauthorized') if req.path == vnc.proxy.WS_ENDPOINT: - req.environ['vnc_host'] = self.tokens[token]['args']['host'] - req.environ['vnc_port'] = int(self.tokens[token]['args']['port']) + req.environ['vnc_host'] = connection_info['host'] + req.environ['vnc_port'] = int(connection_info['port']) return req.get_response(self.app) - def register_listeners(self): - middleware = self - middleware.tokens = {} - - class TopicProxy(): - @staticmethod - def authorize_vnc_console(context, **kwargs): - data = kwargs - token = kwargs['token'] - LOG.audit(_("Received Token: %s)"), token) - middleware.tokens[token] = \ - {'args': kwargs, 'last_activity_at': time.time()} - - def delete_expired_tokens(): - now = time.time() - to_delete = [] - for k, v in middleware.tokens.items(): - if now - v['last_activity_at'] > FLAGS.vnc_token_ttl: - to_delete.append(k) - - for k in to_delete: - LOG.audit(_("Deleting Token: %s)"), k) - del middleware.tokens[k] - - conn = rpc.Connection.instance(new=True) - consumer = rpc.TopicAdapterConsumer( - connection=conn, - proxy=TopicProxy, - topic=FLAGS.vnc_console_proxy_topic) - - utils.LoopingCall(consumer.fetch, - enable_callbacks=True).start(0.1) - utils.LoopingCall(delete_expired_tokens).start(1) + def _delete_expired_tokens(self): + now = time.time() + to_delete = [] + for k, v in self.token_cache.items(): + if now - v['last_activity_at'] > FLAGS.vnc_token_ttl: + to_delete.append(k) + + for k in to_delete: + del self.token_cache[k] + class LoggingMiddleware(object): @@ -112,3 +102,34 @@ class LoggingMiddleware(object): LOG.info(_("Received Request: %s"), req.url) return req.get_response(self.app) + + +class VNCProxyAuthManager(manager.Manager): + """Manages token based authentication.""" + + def __init__(self, scheduler_driver=None, *args, **kwargs): + super(VNCProxyAuthManager, self).__init__(*args, **kwargs) + self.tokens = {} + utils.LoopingCall(self._delete_expired_tokens).start(1) + + def authorize_vnc_console(self, context, token, host, port): + self.tokens[token] = {'host': host, + 'port': port, + 'last_activity_at': time.time()} + LOG.audit(_("Received Token: %s, %s)"), token, self.tokens[token]) + + def check_token(self, context, token): + LOG.audit(_("Checking Token: %s, %s)"), token, (token in self.tokens)) + if token in self.tokens: + return self.tokens[token] + + def _delete_expired_tokens(self): + now = time.time() + to_delete = [] + for k, v in self.tokens.items(): + if now - v['last_activity_at'] > FLAGS.vnc_token_ttl: + to_delete.append(k) + + for k in to_delete: + LOG.audit(_("Deleting Expired Token: %s)"), k) + del self.tokens[k] diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py index 49379d9ae..c6e46396b 100644 --- a/nova/vnc/proxy.py +++ b/nova/vnc/proxy.py @@ -1,8 +1,7 @@ #!/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. +# Copyright (c) 2010 Openstack, LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); -- cgit From dbef49203985b4eea82912d010df7204ec68586c Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 13:32:03 -0700 Subject: fix flag names --- nova/compute/api.py | 4 ++-- nova/flags.py | 6 +++--- nova/virt/libvirt.xml.template | 4 ++-- nova/virt/libvirt_conn.py | 2 +- nova/vnc/auth.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 5470f40dc..8f5803649 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -614,14 +614,14 @@ class API(base.Base): output = self._call_compute_message('get_vnc_console', context, instance_id) - rpc.cast(context, '%s' % FLAGS.vnc_console_proxy_topic, + rpc.cast(context, '%s' % FLAGS.vncproxy_topic, {'method': 'authorize_vnc_console', 'args': {'token': output['token'], 'host': output['host'], 'port': output['port']}}) return {'url': '%s/vnc_auto.html?token=%s&host=%s&port=%s' % ( - FLAGS.vnc_console_proxy_url, + FLAGS.vncproxy_url, output['token'], 'hostignore', 'portignore')} diff --git a/nova/flags.py b/nova/flags.py index b5c0cd380..b0c116f6b 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -281,13 +281,13 @@ DEFINE_string('ajax_console_proxy_url', in the form "http://127.0.0.1:8000"') DEFINE_string('ajax_console_proxy_port', 8000, 'port that ajax_console_proxy binds') -DEFINE_string('vnc_console_proxy_topic', 'vncproxy', +DEFINE_string('vncproxy_topic', 'vncproxy', 'the topic vnc proxy nodes listen on') -DEFINE_string('vnc_console_proxy_url', +DEFINE_string('vncproxy_url', 'http://127.0.0.1:6080', 'location of vnc console proxy, \ in the form "http://127.0.0.1:6080"') -DEFINE_string('vnc_server_host', '0.0.0.0', +DEFINE_string('vncserver_host', '0.0.0.0', 'the host interface on which vnc server should listen') DEFINE_bool('vnc_enabled', True, 'enable vnc related features') diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index c492e488c..cf6bed530 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -104,8 +104,8 @@ -#if $getVar('vnc_server_host', False) - +#if $getVar('vncserver_host', False) + #end if diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 41adbfe27..ec09aca28 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -885,7 +885,7 @@ class LibvirtConnection(driver.ComputeDriver): 'nics': nics} if FLAGS.vnc_enabled: - xml_info['vnc_server_host'] = FLAGS.vnc_server_host + xml_info['vncserver_host'] = FLAGS.vncserver_host if not rescue: if instance['kernel_id']: xml_info['kernel'] = xml_info['basepath'] + "/kernel" diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index 105b68fe2..45f77dd59 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -51,7 +51,7 @@ class VNCNovaAuthMiddleware(object): return self.token_cache[token] rval = rpc.call(context.get_admin_context(), - FLAGS.vnc_console_proxy_topic, + FLAGS.vncproxy_topic, {"method": "check_token", "args": {'token': token}}) if rval: self.token_cache[token] = rval -- cgit From 07076b1b9caf7f11c74686d546161994e2e2d691 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Tue, 29 Mar 2011 15:32:44 -0500 Subject: Make Dnsmasq_interface configurable --- bin/nova-dhcpbridge | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge index 7ef51feba..f42dfd6b5 100755 --- a/bin/nova-dhcpbridge +++ b/bin/nova-dhcpbridge @@ -48,6 +48,7 @@ flags.DECLARE('auth_driver', 'nova.auth.manager') flags.DECLARE('network_size', 'nova.network.manager') flags.DECLARE('num_networks', 'nova.network.manager') flags.DECLARE('update_dhcp_on_disassociate', 'nova.network.manager') +flags.DEFINE_string('dnsmasq_interface', 'br0', 'Default Dnsmasq interface') LOG = logging.getLogger('nova.dhcpbridge') @@ -103,7 +104,8 @@ def main(): utils.default_flagfile(flagfile) argv = FLAGS(sys.argv) logging.setup() - interface = os.environ.get('DNSMASQ_INTERFACE', 'br0') + # check ENV first so we don't break any older deploys + interface = os.environ.get('DNSMASQ_INTERFACE', FLAGS.dnsmasq_interface) if int(os.environ.get('TESTING', '0')): from nova.tests import fake_flags action = argv[1] -- cgit From 8cfc3d5e6b033a99fc47b3df8ac7e798d107240a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 29 Mar 2011 13:43:03 -0700 Subject: don't print the error message on sys.exit(0) --- bin/nova-manage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index f7308abe5..25695482f 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -1125,7 +1125,7 @@ def main(): print _("Possible wrong number of arguments supplied") print "%s %s: %s" % (category, action, fn.__doc__) raise - except: + except Exception: print _("Command failed, please check log for more info") raise -- cgit From cc7ba9a7a4ed8a38f217ad7f33fc33254f80ead7 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 13:47:47 -0700 Subject: move flags per termie's feedback --- nova/flags.py | 10 ---------- nova/virt/libvirt_conn.py | 1 + nova/vnc/__init__.py | 35 +++++++++++++++++++++++++++++++++++ nova/vnc/auth.py | 3 ++- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/nova/flags.py b/nova/flags.py index b0c116f6b..f011ab383 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -281,16 +281,6 @@ DEFINE_string('ajax_console_proxy_url', in the form "http://127.0.0.1:8000"') DEFINE_string('ajax_console_proxy_port', 8000, 'port that ajax_console_proxy binds') -DEFINE_string('vncproxy_topic', 'vncproxy', - 'the topic vnc proxy nodes listen on') -DEFINE_string('vncproxy_url', - 'http://127.0.0.1:6080', - 'location of vnc console proxy, \ - in the form "http://127.0.0.1:6080"') -DEFINE_string('vncserver_host', '0.0.0.0', - 'the host interface on which vnc server should listen') -DEFINE_bool('vnc_enabled', True, - 'enable vnc related features') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') DEFINE_bool('fake_network', False, diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index ec09aca28..8c948950b 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -57,6 +57,7 @@ from nova import flags from nova import log as logging #from nova import test from nova import utils +from nova import vnc from nova.auth import manager from nova.compute import instance_types from nova.compute import power_state diff --git a/nova/vnc/__init__.py b/nova/vnc/__init__.py index e69de29bb..1642295ec 100644 --- a/nova/vnc/__init__.py +++ b/nova/vnc/__init__.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 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. + +"""Module for VNC Proxying.""" + +from nova import flags + + +FLAGS = flags.FLAGS + +flags.DEFINE_string('vncproxy_topic', 'vncproxy', + 'the topic vnc proxy nodes listen on') +flags.DEFINE_string('vncproxy_url', + 'http://127.0.0.1:6080', + 'location of vnc console proxy, \ + in the form "http://127.0.0.1:6080"') +flags.DEFINE_string('vncserver_host', '0.0.0.0', + 'the host interface on which vnc server should listen') +flags.DEFINE_bool('vnc_enabled', True, + 'enable vnc related features') diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index 45f77dd59..fd8d351f1 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -89,8 +89,9 @@ class VNCNovaAuthMiddleware(object): del self.token_cache[k] - class LoggingMiddleware(object): + """Middleware for basic vnc-specific request logging.""" + def __init__(self, app): self.app = app -- cgit From 817572265871fd2cfd1252dd0cffb167f0e2ccdb Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 13:49:49 -0700 Subject: move functions around --- nova/vnc/auth.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index fd8d351f1..86efffbe6 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -44,18 +44,7 @@ class VNCNovaAuthMiddleware(object): def __init__(self, app): self.app = app self.token_cache = {} - utils.LoopingCall(self._delete_expired_tokens).start(1) - - def get_token_info(self, token): - if token in self.token_cache: - return self.token_cache[token] - - rval = rpc.call(context.get_admin_context(), - FLAGS.vncproxy_topic, - {"method": "check_token", "args": {'token': token}}) - if rval: - self.token_cache[token] = rval - return rval + utils.LoopingCall(self.delete_expired_tokens).start(1) @webob.dec.wsgify def __call__(self, req): @@ -78,7 +67,18 @@ class VNCNovaAuthMiddleware(object): return req.get_response(self.app) - def _delete_expired_tokens(self): + def get_token_info(self, token): + if token in self.token_cache: + return self.token_cache[token] + + rval = rpc.call(context.get_admin_context(), + FLAGS.vncproxy_topic, + {"method": "check_token", "args": {'token': token}}) + if rval: + self.token_cache[token] = rval + return rval + + def delete_expired_tokens(self): now = time.time() to_delete = [] for k, v in self.token_cache.items(): -- cgit From 2c533ca5a4cd74907b3238ec65ab29c4f686dfcc Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 13:55:01 -0700 Subject: switch cast to a call --- nova/compute/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 8f5803649..89d821d44 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -614,7 +614,7 @@ class API(base.Base): output = self._call_compute_message('get_vnc_console', context, instance_id) - rpc.cast(context, '%s' % FLAGS.vncproxy_topic, + rpc.call(context, '%s' % FLAGS.vncproxy_topic, {'method': 'authorize_vnc_console', 'args': {'token': output['token'], 'host': output['host'], -- cgit From 8cdad1ab8343eb038f119a92e28d77c731b61793 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 13:59:46 -0700 Subject: add comment --- nova/compute/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/compute/api.py b/nova/compute/api.py index 89d821d44..7977b07a2 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -620,6 +620,7 @@ class API(base.Base): 'host': output['host'], 'port': output['port']}}) + # hostignore and portignore are compatability params for noVNC return {'url': '%s/vnc_auto.html?token=%s&host=%s&port=%s' % ( FLAGS.vncproxy_url, output['token'], -- cgit From f5c072de1edddc4ddab89be8146a81d361397c45 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 14:53:38 -0700 Subject: incorporate feedback from termie --- bin/nova-vncproxy | 4 ++-- doc/source/runnova/vncconsole.rst | 2 +- nova/api/openstack/servers.py | 2 +- nova/virt/libvirt_conn.py | 2 -- nova/vnc/__init__.py | 2 -- nova/vnc/auth.py | 4 ++-- nova/vnc/proxy.py | 4 ++-- 7 files changed, 8 insertions(+), 12 deletions(-) diff --git a/bin/nova-vncproxy b/bin/nova-vncproxy index 0fad8397d..ccb97e3a3 100755 --- a/bin/nova-vncproxy +++ b/bin/nova-vncproxy @@ -71,8 +71,6 @@ if __name__ == "__main__": LOG.audit(_("Starting nova-vnc-proxy node (version %s)"), version.version_string_with_vcs()) - service.serve() - if not (os.path.exists(FLAGS.vncproxy_wwwroot) and os.path.exists(FLAGS.vncproxy_wwwroot + '/vnc_auto.html')): LOG.info(_("Missing vncproxy_wwwroot (version %s)"), @@ -96,6 +94,8 @@ if __name__ == "__main__": else: with_auth = auth.VNCNovaAuthMiddleware(with_logging) + service.serve() + server = wsgi.Server() server.start(with_auth, FLAGS.vncproxy_port, host=FLAGS.vncproxy_host) server.wait() diff --git a/doc/source/runnova/vncconsole.rst b/doc/source/runnova/vncconsole.rst index 6d93bad93..942ace611 100644 --- a/doc/source/runnova/vncconsole.rst +++ b/doc/source/runnova/vncconsole.rst @@ -36,7 +36,7 @@ Configuring the VNC Proxy ------------------------- nova-vnc-proxy requires a websocket enabled html client to work properly. At this time, the only tested client is a slightly modified fork of noVNC, which -you can at find git://github.com/sleepsonthefloor/noVNC.git. +you can at find http://github.com/openstack/noVNC.git .. todo:: add instruction for installing from package diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 822342149..8170ab4a1 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -486,7 +486,7 @@ class Controller(wsgi.Controller): """Returns a url to an instance's ajaxterm console.""" try: self.compute_api.get_vnc_console(req.environ['nova.context'], - int(id)) + int(id)) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 8c948950b..502e61395 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -627,8 +627,6 @@ class LibvirtConnection(driver.ComputeDriver): return {'token': token, 'host': host, 'port': port} - _image_sems = {} # FIXME: why is this here? (anthony) - @staticmethod def _cache_image(fn, target, fname, cow=False, *args, **kwargs): """Wrapper for a method that creates an image that caches the image. diff --git a/nova/vnc/__init__.py b/nova/vnc/__init__.py index 1642295ec..2733c81d6 100644 --- a/nova/vnc/__init__.py +++ b/nova/vnc/__init__.py @@ -20,9 +20,7 @@ from nova import flags - FLAGS = flags.FLAGS - flags.DEFINE_string('vncproxy_topic', 'vncproxy', 'the topic vnc proxy nodes listen on') flags.DEFINE_string('vncproxy_url', diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index 86efffbe6..c7df13450 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -44,7 +44,7 @@ class VNCNovaAuthMiddleware(object): def __init__(self, app): self.app = app self.token_cache = {} - utils.LoopingCall(self.delete_expired_tokens).start(1) + utils.LoopingCall(self.delete_expired_cache_items).start(1) @webob.dec.wsgify def __call__(self, req): @@ -78,7 +78,7 @@ class VNCNovaAuthMiddleware(object): self.token_cache[token] = rval return rval - def delete_expired_tokens(self): + def delete_expired_cache_items(self): now = time.time() to_delete = [] for k, v in self.token_cache.items(): diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py index c6e46396b..c4603803b 100644 --- a/nova/vnc/proxy.py +++ b/nova/vnc/proxy.py @@ -25,9 +25,9 @@ import eventlet from eventlet import wsgi from eventlet import websocket -from webob import Request import webob + WS_ENDPOINT = '/data' @@ -88,7 +88,7 @@ class WebsocketVNCProxy(object): _handle(environ, start_response) def __call__(self, environ, start_response): - req = Request(environ) + req = webob.Request(environ) if req.path == WS_ENDPOINT: return self.proxy_connection(environ, start_response) else: -- cgit From 3cdc2a90f0a7a066a231b0590aeb3d51d8ec699a Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 14:58:10 -0700 Subject: add line --- nova/vnc/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/vnc/__init__.py b/nova/vnc/__init__.py index 2733c81d6..b5b00e44e 100644 --- a/nova/vnc/__init__.py +++ b/nova/vnc/__init__.py @@ -20,6 +20,7 @@ from nova import flags + FLAGS = flags.FLAGS flags.DEFINE_string('vncproxy_topic', 'vncproxy', 'the topic vnc proxy nodes listen on') -- cgit From 93a7a7b94a0d9e4100abb3a4309a3546ab532535 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 15:22:16 -0700 Subject: clarify test --- nova/tests/test_compute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 038824ef8..1b0f426d2 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -286,7 +286,7 @@ class ComputeTestCase(test.TestCase): console = self.compute.get_ajax_console(self.context, instance_id) - self.assert_(console) + self.assert_(set(['token', 'host', 'port']).issubset(console.keys())) self.compute.terminate_instance(self.context, instance_id) def test_vnc_console(self): -- cgit From 9513458c3e50fac5f40e76757b45ab15b67e8f00 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 15:34:25 -0700 Subject: add nova-vncproxy to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 3b48990ac..20f4c1947 100644 --- a/setup.py +++ b/setup.py @@ -112,4 +112,5 @@ DistUtilsExtra.auto.setup(name='nova', 'bin/nova-spoolsentry', 'bin/stack', 'bin/nova-volume', + 'bin/nova-vncproxy', 'tools/nova-debug']) -- cgit From e86f58261ee6acb8705106d3de61be0de488d94b Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 15:37:45 -0700 Subject: Reverted extension loading tweaks --- nova/api/openstack/extensions.py | 116 +++---- nova/api/openstack/incubator/__init__.py | 4 +- nova/api/openstack/incubator/volumes.py | 355 +++++++++++++++++++++ nova/api/openstack/incubator/volumes/__init__.py | 18 -- .../incubator/volumes/volume_attachments.py | 184 ----------- nova/api/openstack/incubator/volumes/volumes.py | 161 ---------- .../api/openstack/incubator/volumes/volumes_ext.py | 55 ---- nova/tests/api/openstack/extensions/foxinsocks.py | 98 ++++++ .../openstack/extensions/foxinsocks/__init__.py | 19 -- .../openstack/extensions/foxinsocks/foxinsocks.py | 98 ------ 10 files changed, 505 insertions(+), 603 deletions(-) create mode 100644 nova/api/openstack/incubator/volumes.py delete mode 100644 nova/api/openstack/incubator/volumes/__init__.py delete mode 100644 nova/api/openstack/incubator/volumes/volume_attachments.py delete mode 100644 nova/api/openstack/incubator/volumes/volumes.py delete mode 100644 nova/api/openstack/incubator/volumes/volumes_ext.py create mode 100644 nova/tests/api/openstack/extensions/foxinsocks.py delete mode 100644 nova/tests/api/openstack/extensions/foxinsocks/__init__.py delete mode 100644 nova/tests/api/openstack/extensions/foxinsocks/foxinsocks.py diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index d0b4d378a..d1d479313 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -38,7 +38,10 @@ FLAGS = flags.FLAGS class ExtensionDescriptor(object): - """This is the base class that defines the contract for extensions.""" + """Base class that defines the contract for extensions. + + Note that you don't have to derive from this class to have a valid + extension; it is purely a convenience.""" def get_name(self): """The name of the extension. @@ -321,22 +324,37 @@ class ExtensionManager(object): resources = [] resources.append(ResourceExtension('extensions', ExtensionController(self))) - for _alias, ext in self.extensions.iteritems(): - resources.extend(ext.get_resources()) + for alias, ext in self.extensions.iteritems(): + try: + resources.extend(ext.get_resources()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have resource + # extensions + pass return resources def get_actions(self): """Returns a list of ActionExtension objects.""" actions = [] - for _alias, ext in self.extensions.iteritems(): - actions.extend(ext.get_actions()) + for alias, ext in self.extensions.iteritems(): + try: + actions.extend(ext.get_actions()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have action + # extensions + pass return actions def get_response_extensions(self): """Returns a list of ResponseExtension objects.""" response_exts = [] - for _alias, ext in self.extensions.iteritems(): - response_exts.extend(ext.get_response_extensions()) + for alias, ext in self.extensions.iteritems(): + try: + response_exts.extend(ext.get_response_extensions()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have response + # extensions + pass return response_exts def _check_extension(self, extension): @@ -353,78 +371,42 @@ class ExtensionManager(object): def _load_all_extensions(self): """Load extensions from the configured path. - An extension consists of a directory of related files, with a class - that defines a class that inherits from ExtensionDescriptor. + Load extensions from the configured path. The extension name is + constructed from the module_name. If your extension module was named + widgets.py the extension class within that module should be + 'Widgets'. - Because of some oddities involving identically named modules, it's - probably best to name your file after the name of your extension, - rather than something likely to clash like 'extension.py'. - - The name of your directory should be the same as the alias your - extension uses, for everyone's sanity. + In addition, extensions are loaded from the 'incubator' directory. See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. """ - self._load_extensions_under_path(self.path) + if os.path.exists(self.path): + self._load_all_extensions_from_path(self.path) incubator_path = os.path.join(os.path.dirname(__file__), "incubator") - self._load_extensions_under_path(incubator_path) - - def _load_extensions_under_path(self, path): - if not os.path.isdir(path): - LOG.warning(_('Extensions directory not found: %s') % path) - return - - LOG.debug(_('Looking for extensions in: %s') % path) - - for child in os.listdir(path): - child_path = os.path.join(path, child) - if not os.path.isdir(child_path): - continue - self._load_extension(child_path) - - def _load_extension(self, path): - if not os.path.isdir(path): - return + if os.path.exists(incubator_path): + self._load_all_extensions_from_path(incubator_path) + def _load_all_extensions_from_path(self, path): for f in os.listdir(path): + LOG.audit(_('Loading extension file: %s'), f) mod_name, file_ext = os.path.splitext(os.path.split(f)[-1]) - if mod_name.startswith('_'): - continue - if file_ext.lower() != '.py': - continue - ext_path = os.path.join(path, f) - if self.super_verbose: - LOG.debug(_('Checking extension file: %s'), ext_path) - - mod = imp.load_source(mod_name, ext_path) - for _name, cls in inspect.getmembers(mod): - try: - if not inspect.isclass(cls): - continue - - # NOTE(justinsb): It seems that python modules are odd. - # If you have two identically named modules, the classes - # from both are mixed in. So name your extension based - # on the alias, not 'extension.py'! - # TODO(justinsb): Any way to work around this? - - if self.super_verbose: - LOG.debug(_('Checking class: %s'), cls) - - if not ExtensionDescriptor in cls.__bases__: - if self.super_verbose: - LOG.debug(_('Not a ExtensionDescriptor: %s'), cls) - continue - - obj = cls() - self._add_extension(obj) - except AttributeError as ex: - LOG.exception(_("Exception loading extension: %s"), - unicode(ex)) + if file_ext.lower() == '.py' and not mod_name.startswith('_'): + mod = imp.load_source(mod_name, ext_path) + ext_name = mod_name[0].upper() + mod_name[1:] + new_ext_class = getattr(mod, ext_name, None) + if not new_ext_class: + LOG.warn(_('Did not find expected name ' + '"%(ext_name)s" in %(file)s'), + {'ext_name': ext_name, + 'file': ext_path}) + continue + new_ext = new_ext_class() + self._check_extension(new_ext) + self._add_extension(new_ext) def _add_extension(self, ext): alias = ext.get_alias() diff --git a/nova/api/openstack/incubator/__init__.py b/nova/api/openstack/incubator/__init__.py index cded38174..e115f1ab9 100644 --- a/nova/api/openstack/incubator/__init__.py +++ b/nova/api/openstack/incubator/__init__.py @@ -17,4 +17,6 @@ """Incubator contains extensions that are shipped with nova. -It can't be called 'extensions' because that causes namespacing problems.""" +It can't be called 'extensions' because that causes namespacing problems. + +""" diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py new file mode 100644 index 000000000..7a5b2c1d0 --- /dev/null +++ b/nova/api/openstack/incubator/volumes.py @@ -0,0 +1,355 @@ +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The volumes extension.""" + +from webob import exc + +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + + +FLAGS = flags.FLAGS + + +def _translate_volume_detail_view(context, vol): + """Maps keys for volumes details view.""" + + d = _translate_volume_summary_view(context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_volume_summary_view(_context, vol): + """Maps keys for volumes summary view.""" + d = {} + + instance_id = None + # instance_data = None + attached_to = vol.get('instance') + if attached_to: + instance_id = attached_to['id'] + # instance_data = '%s[%s]' % (instance_ec2_id, + # attached_to['host']) + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] + # if context.is_admin: + # v['status'] = '%s (%s, %s, %s, %s)' % ( + # vol['status'], + # vol['user_id'], + # vol['host'], + # instance_data, + # vol['mountpoint']) + if vol['attach_status'] == 'attached': + d['attachments'] = [{'attachTime': vol['attach_time'], + 'deleteOnTermination': False, + 'mountpoint': vol['mountpoint'], + 'instanceId': instance_id, + 'status': 'attached', + 'volumeId': vol['id']}] + else: + d['attachments'] = [{}] + + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +class VolumeController(wsgi.Controller): + """The Volumes API controller for the OpenStack API.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + ]}}} + + def __init__(self): + self.volume_api = volume.API() + super(VolumeController, self).__init__() + + def show(self, req, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_volume_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + def index(self, req): + """Returns a summary list of volumes.""" + return self._items(req, entity_maker=_translate_volume_summary_view) + + def detail(self, req): + """Returns a detailed list of volumes.""" + return self._items(req, entity_maker=_translate_volume_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker.""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumes': res} + + def create(self, req): + """Creates a new volume.""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + vol = env['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + new_volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) + + # Work around problem that instance is lazy-loaded... + new_volume['instance'] = None + + retval = _translate_volume_detail_view(context, new_volume) + + return {'volume': retval} + + +def _translate_attachment_detail_view(_context, vol): + """Maps keys for attachment details view.""" + + d = _translate_attachment_summary_view(_context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_attachment_summary_view(_context, vol): + """Maps keys for attachment summary view.""" + d = {} + + volume_id = vol['id'] + + # NOTE(justinsb): We use the volume id as the id of the attachment object + d['id'] = volume_id + + d['volumeId'] = volume_id + if vol.get('instance_id'): + d['serverId'] = vol['instance_id'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d + + +class VolumeAttachmentController(wsgi.Controller): + """The volume attachment API controller for the Openstack API. + + A child resource of the server. Note that we use the volume id + as the ID of the attachment (though this is not guaranteed externally) + + """ + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'volumeAttachment': ['id', + 'serverId', + 'volumeId', + 'device']}}} + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(VolumeAttachmentController, self).__init__() + + def index(self, req, server_id): + """Returns the list of volume attachments for a given instance.""" + return self._items(req, server_id, + entity_maker=_translate_attachment_summary_view) + + def show(self, req, server_id, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + volume_id = id + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + LOG.debug("volume_id not found") + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + return {'volumeAttachment': _translate_attachment_detail_view(context, + vol)} + + def create(self, req, server_id): + """Attach a volume to an instance.""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + instance_id = server_id + volume_id = env['volumeAttachment']['volumeId'] + device = env['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + try: + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + # The attach is async + attachment = {} + attachment['id'] = volume_id + attachment['volumeId'] = volume_id + + # NOTE(justinsb): And now, we have a problem... + # The attach is async, so there's a window in which we don't see + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. + # For now, we'll probably have to rely on libraries being smart + + # TODO(justinsb): How do I return "accepted" here? + return {'volumeAttachment': attachment} + + def update(self, _req, _server_id, _id): + """Update a volume attachment. We don't currently support this.""" + return faults.Fault(exc.HTTPBadRequest()) + + def delete(self, req, server_id, id): + """Detach a volume from an instance.""" + context = req.environ['nova.context'] + + volume_id = id + LOG.audit(_("Detach volume %s"), volume_id, context=context) + + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return exc.HTTPAccepted() + + def _items(self, req, server_id, entity_maker): + """Returns a list of attachments, transformed through entity_maker.""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + volumes = instance['volumes'] + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumeAttachments': res} + + +class Volumes(extensions.ExtensionDescriptor): + def get_name(self): + return "Volumes" + + def get_alias(self): + return "VOLUMES" + + def get_description(self): + return "Volumes support" + + def get_namespace(self): + return "http://docs.openstack.org/ext/volumes/api/v1.1" + + def get_updated(self): + return "2011-03-25T00:00:00+00:00" + + def get_resources(self): + resources = [] + + # NOTE(justinsb): No way to provide singular name ('volume') + # Does this matter? + res = extensions.ResourceExtension('volumes', + VolumeController(), + collection_actions= + {'detail': 'GET'} + ) + resources.append(res) + + res = extensions.ResourceExtension('volume_attachments', + VolumeAttachmentController(), + parent=dict( + member_name='server', + collection_name='servers')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/incubator/volumes/__init__.py b/nova/api/openstack/incubator/volumes/__init__.py deleted file mode 100644 index 2a9c93210..000000000 --- a/nova/api/openstack/incubator/volumes/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# 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 datetime - -"""The volumes extension adds volumes and attachments to the API.""" diff --git a/nova/api/openstack/incubator/volumes/volume_attachments.py b/nova/api/openstack/incubator/volumes/volume_attachments.py deleted file mode 100644 index aec4ea8f3..000000000 --- a/nova/api/openstack/incubator/volumes/volume_attachments.py +++ /dev/null @@ -1,184 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from webob import exc - -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova import wsgi -from nova.api.openstack import common -from nova.api.openstack import faults - - -LOG = logging.getLogger("nova.api.volumes") - - -FLAGS = flags.FLAGS - - -def _translate_detail_view(context, volume): - """Maps keys for details view.""" - - d = _translate_summary_view(context, volume) - - # No additional data / lookups at the moment - - return d - - -def _translate_summary_view(context, vol): - """Maps keys for summary view.""" - d = {} - - volume_id = vol['id'] - - # NOTE(justinsb): We use the volume id as the id of the attachment object - d['id'] = volume_id - - d['volumeId'] = volume_id - if vol.get('instance_id'): - d['serverId'] = vol['instance_id'] - if vol.get('mountpoint'): - d['device'] = vol['mountpoint'] - - return d - - -class Controller(wsgi.Controller): - """The volume attachment API controller for the Openstack API. - - A child resource of the server. Note that we use the volume id - as the ID of the attachment (though this is not guaranteed externally) - - """ - - _serialization_metadata = { - 'application/xml': { - 'attributes': { - 'volumeAttachment': ['id', - 'serverId', - 'volumeId', - 'device']}}} - - def __init__(self): - self.compute_api = compute.API() - self.volume_api = volume.API() - super(Controller, self).__init__() - - def index(self, req, server_id): - """Returns the list of volume attachments for a given instance.""" - return self._items(req, server_id, - entity_maker=_translate_summary_view) - - def show(self, req, server_id, id): - """Return data about the given volume.""" - context = req.environ['nova.context'] - - volume_id = id - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - LOG.debug("volume_id not found") - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - return {'volumeAttachment': _translate_detail_view(context, vol)} - - def create(self, req, server_id): - """Attach a volume to an instance.""" - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - instance_id = server_id - volume_id = env['volumeAttachment']['volumeId'] - device = env['volumeAttachment']['device'] - - msg = _("Attach volume %(volume_id)s to instance %(server_id)s" - " at %(device)s") % locals() - LOG.audit(msg, context=context) - - try: - self.compute_api.attach_volume(context, - instance_id=instance_id, - volume_id=volume_id, - device=device) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - # The attach is async - attachment = {} - attachment['id'] = volume_id - attachment['volumeId'] = volume_id - - # NOTE(justinsb): And now, we have a problem... - # The attach is async, so there's a window in which we don't see - # the attachment (until the attachment completes). We could also - # get problems with concurrent requests. I think we need an - # attachment state, and to write to the DB here, but that's a bigger - # change. - # For now, we'll probably have to rely on libraries being smart - - # TODO(justinsb): How do I return "accepted" here? - return {'volumeAttachment': attachment} - - def update(self, _req, _server_id, _id): - """Update a volume attachment. We don't currently support this.""" - return faults.Fault(exc.HTTPBadRequest()) - - def delete(self, req, server_id, id): - """Detach a volume from an instance.""" - context = req.environ['nova.context'] - - volume_id = id - LOG.audit(_("Detach volume %s"), volume_id, context=context) - - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - self.compute_api.detach_volume(context, - volume_id=volume_id) - - return exc.HTTPAccepted() - - def _items(self, req, server_id, entity_maker): - """Returns a list of attachments, transformed through entity_maker.""" - context = req.environ['nova.context'] - - try: - instance = self.compute_api.get(context, server_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - volumes = instance['volumes'] - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumeAttachments': res} diff --git a/nova/api/openstack/incubator/volumes/volumes.py b/nova/api/openstack/incubator/volumes/volumes.py deleted file mode 100644 index a7d5fbaa6..000000000 --- a/nova/api/openstack/incubator/volumes/volumes.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from webob import exc - -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova import wsgi -from nova.api.openstack import common -from nova.api.openstack import faults - - -LOG = logging.getLogger("nova.api.volumes") - - -FLAGS = flags.FLAGS - - -def _translate_detail_view(context, vol): - """Maps keys for details view.""" - - d = _translate_summary_view(context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_summary_view(_context, vol): - """Maps keys for summary view.""" - d = {} - - instance_id = None - # instance_data = None - attached_to = vol.get('instance') - if attached_to: - instance_id = attached_to['id'] - # instance_data = '%s[%s]' % (instance_ec2_id, - # attached_to['host']) - d['id'] = vol['id'] - d['status'] = vol['status'] - d['size'] = vol['size'] - d['availabilityZone'] = vol['availability_zone'] - d['createdAt'] = vol['created_at'] - # if context.is_admin: - # v['status'] = '%s (%s, %s, %s, %s)' % ( - # vol['status'], - # vol['user_id'], - # vol['host'], - # instance_data, - # vol['mountpoint']) - if vol['attach_status'] == 'attached': - d['attachments'] = [{'attachTime': vol['attach_time'], - 'deleteOnTermination': False, - 'mountpoint': vol['mountpoint'], - 'instanceId': instance_id, - 'status': 'attached', - 'volumeId': vol['id']}] - else: - d['attachments'] = [{}] - - d['displayName'] = vol['display_name'] - d['displayDescription'] = vol['display_description'] - return d - - -class Controller(wsgi.Controller): - """The Volumes API controller for the OpenStack API.""" - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "volume": [ - "id", - "status", - "size", - "availabilityZone", - "createdAt", - "displayName", - "displayDescription", - ]}}} - - def __init__(self): - self.volume_api = volume.API() - super(Controller, self).__init__() - - def show(self, req, id): - """Return data about the given volume.""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get(context, id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - return {'volume': _translate_detail_view(context, vol)} - - def delete(self, req, id): - """Delete a volume.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete volume with id: %s"), id, context=context) - - try: - self.volume_api.delete(context, volume_id=id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() - - def index(self, req): - """Returns a summary list of volumes.""" - return self._items(req, entity_maker=_translate_summary_view) - - def detail(self, req): - """Returns a detailed list of volumes.""" - return self._items(req, entity_maker=_translate_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of volumes, transformed through entity_maker.""" - context = req.environ['nova.context'] - - volumes = self.volume_api.get_all(context) - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumes': res} - - def create(self, req): - """Creates a new volume.""" - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - vol = env['volume'] - size = vol['size'] - LOG.audit(_("Create volume of %s GB"), size, context=context) - new_volume = self.volume_api.create(context, size, - vol.get('display_name'), - vol.get('display_description')) - - # Work around problem that instance is lazy-loaded... - new_volume['instance'] = None - - retval = _translate_detail_view(context, new_volume) - - return {'volume': retval} diff --git a/nova/api/openstack/incubator/volumes/volumes_ext.py b/nova/api/openstack/incubator/volumes/volumes_ext.py deleted file mode 100644 index 6a3bb0265..000000000 --- a/nova/api/openstack/incubator/volumes/volumes_ext.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from nova.api.openstack import extensions -from nova.api.openstack.incubator.volumes import volumes -from nova.api.openstack.incubator.volumes import volume_attachments - - -class VolumesExtension(extensions.ExtensionDescriptor): - def get_name(self): - return "Volumes" - - def get_alias(self): - return "VOLUMES" - - def get_description(self): - return "Volumes support" - - def get_namespace(self): - return "http://docs.openstack.org/ext/volumes/api/v1.1" - - def get_updated(self): - return "2011-03-25T00:00:00+00:00" - - def get_resources(self): - resources = [] - - # NOTE(justinsb): No way to provide singular name ('volume') - # Does this matter? - res = extensions.ResourceExtension('volumes', - volumes.Controller(), - collection_actions={'detail': 'GET'} - ) - resources.append(res) - - res = extensions.ResourceExtension('volume_attachments', - volume_attachments.Controller(), - parent=dict( - member_name='server', - collection_name='servers')) - resources.append(res) - - return resources diff --git a/nova/tests/api/openstack/extensions/foxinsocks.py b/nova/tests/api/openstack/extensions/foxinsocks.py new file mode 100644 index 000000000..0860b51ac --- /dev/null +++ b/nova/tests/api/openstack/extensions/foxinsocks.py @@ -0,0 +1,98 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from nova import wsgi + +from nova.api.openstack import extensions + + +class FoxInSocksController(wsgi.Controller): + + def index(self, req): + return "Try to say this Mr. Knox, sir..." + + +class Foxinsocks(object): + + def __init__(self): + pass + + def get_name(self): + return "Fox In Socks" + + def get_alias(self): + return "FOXNSOX" + + def get_description(self): + return "The Fox In Socks Extension" + + def get_namespace(self): + return "http://www.fox.in.socks/api/ext/pie/v1.0" + + def get_updated(self): + return "2011-01-22T13:25:27-06:00" + + def get_resources(self): + resources = [] + resource = extensions.ResourceExtension('foxnsocks', + FoxInSocksController()) + resources.append(resource) + return resources + + def get_actions(self): + actions = [] + actions.append(extensions.ActionExtension('servers', 'add_tweedle', + self._add_tweedle)) + actions.append(extensions.ActionExtension('servers', 'delete_tweedle', + self._delete_tweedle)) + return actions + + def get_response_extensions(self): + response_exts = [] + + def _goose_handler(res): + #NOTE: This only handles JSON responses. + # You can use content type header to test for XML. + data = json.loads(res.body) + data['flavor']['googoose'] = "Gooey goo for chewy chewing!" + return data + + resp_ext = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)', + _goose_handler) + response_exts.append(resp_ext) + + def _bands_handler(res): + #NOTE: This only handles JSON responses. + # You can use content type header to test for XML. + data = json.loads(res.body) + data['big_bands'] = 'Pig Bands!' + return data + + resp_ext2 = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)', + _bands_handler) + response_exts.append(resp_ext2) + return response_exts + + def _add_tweedle(self, input_dict, req, id): + + return "Tweedle Beetle Added." + + def _delete_tweedle(self, input_dict, req, id): + + return "Tweedle Beetle Deleted." diff --git a/nova/tests/api/openstack/extensions/foxinsocks/__init__.py b/nova/tests/api/openstack/extensions/foxinsocks/__init__.py deleted file mode 100644 index fe505359d..000000000 --- a/nova/tests/api/openstack/extensions/foxinsocks/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# Copyright 2011 Justin Santa Barbara -# 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 datetime - -"""Example Fox-in-Socks extension.""" diff --git a/nova/tests/api/openstack/extensions/foxinsocks/foxinsocks.py b/nova/tests/api/openstack/extensions/foxinsocks/foxinsocks.py deleted file mode 100644 index 442707714..000000000 --- a/nova/tests/api/openstack/extensions/foxinsocks/foxinsocks.py +++ /dev/null @@ -1,98 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json - -from nova import wsgi - -from nova.api.openstack import extensions - - -class FoxInSocksController(wsgi.Controller): - - def index(self, req): - return "Try to say this Mr. Knox, sir..." - - -class Foxinsocks(extensions.ExtensionDescriptor): - - def __init__(self): - pass - - def get_name(self): - return "Fox In Socks" - - def get_alias(self): - return "FOXNSOX" - - def get_description(self): - return "The Fox In Socks Extension" - - def get_namespace(self): - return "http://www.fox.in.socks/api/ext/pie/v1.0" - - def get_updated(self): - return "2011-01-22T13:25:27-06:00" - - def get_resources(self): - resources = [] - resource = extensions.ResourceExtension('foxnsocks', - FoxInSocksController()) - resources.append(resource) - return resources - - def get_actions(self): - actions = [] - actions.append(extensions.ActionExtension('servers', 'add_tweedle', - self._add_tweedle)) - actions.append(extensions.ActionExtension('servers', 'delete_tweedle', - self._delete_tweedle)) - return actions - - def get_response_extensions(self): - response_exts = [] - - def _goose_handler(res): - #NOTE: This only handles JSON responses. - # You can use content type header to test for XML. - data = json.loads(res.body) - data['flavor']['googoose'] = "Gooey goo for chewy chewing!" - return data - - resp_ext = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)', - _goose_handler) - response_exts.append(resp_ext) - - def _bands_handler(res): - #NOTE: This only handles JSON responses. - # You can use content type header to test for XML. - data = json.loads(res.body) - data['big_bands'] = 'Pig Bands!' - return data - - resp_ext2 = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)', - _bands_handler) - response_exts.append(resp_ext2) - return response_exts - - def _add_tweedle(self, input_dict, req, id): - - return "Tweedle Beetle Added." - - def _delete_tweedle(self, input_dict, req, id): - - return "Tweedle Beetle Deleted." -- cgit From 034a841cbac8e73c55e9525df7360a068fe9d892 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 15:43:38 -0700 Subject: pep8 fixes --- nova/api/openstack/incubator/volumes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py index 7a5b2c1d0..47b86216e 100644 --- a/nova/api/openstack/incubator/volumes.py +++ b/nova/api/openstack/incubator/volumes.py @@ -339,9 +339,8 @@ class Volumes(extensions.ExtensionDescriptor): # NOTE(justinsb): No way to provide singular name ('volume') # Does this matter? res = extensions.ResourceExtension('volumes', - VolumeController(), - collection_actions= - {'detail': 'GET'} + VolumeController(), + collection_actions={'detail': 'GET'} ) resources.append(res) -- cgit From 11d258e1d8a4a78a699aa564b5f8139bf0b73db2 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 15:52:04 -0700 Subject: Added missing blank line at end of multiline docstring --- nova/api/openstack/extensions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index d1d479313..631275235 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -41,7 +41,9 @@ class ExtensionDescriptor(object): """Base class that defines the contract for extensions. Note that you don't have to derive from this class to have a valid - extension; it is purely a convenience.""" + extension; it is purely a convenience. + + """ def get_name(self): """The name of the extension. -- cgit From 620c2dabfa8d92dbf250c078dda71d3ec11c6d8c Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 16:13:09 -0700 Subject: fix for lp742650 --- bin/nova-ajax-console-proxy | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/bin/nova-ajax-console-proxy b/bin/nova-ajax-console-proxy index 0342c620a..d88f59e40 100755 --- a/bin/nova-ajax-console-proxy +++ b/bin/nova-ajax-console-proxy @@ -108,17 +108,17 @@ class AjaxConsoleProxy(object): return "Server Error" def register_listeners(self): - class Callback: - def __call__(self, data, message): - if data['method'] == 'authorize_ajax_console': - AjaxConsoleProxy.tokens[data['args']['token']] = \ - {'args': data['args'], 'last_activity': time.time()} + class TopicProxy(): + @staticmethod + def authorize_ajax_console(context, **kwargs): + AjaxConsoleProxy.tokens[kwargs['token']] = \ + {'args': kwargs, 'last_activity': time.time()} conn = rpc.Connection.instance(new=True) consumer = rpc.TopicAdapterConsumer( - connection=conn, - topic=FLAGS.ajax_console_proxy_topic) - consumer.register_callback(Callback()) + connection=conn, + proxy=TopicProxy, + topic=FLAGS.ajax_console_proxy_topic) def delete_expired_tokens(): now = time.time() @@ -130,8 +130,7 @@ class AjaxConsoleProxy(object): for k in to_delete: del AjaxConsoleProxy.tokens[k] - utils.LoopingCall(consumer.fetch, auto_ack=True, - enable_callbacks=True).start(0.1) + utils.LoopingCall(consumer.fetch, enable_callbacks=True).start(0.1) utils.LoopingCall(delete_expired_tokens).start(1) if __name__ == '__main__': -- cgit From ded3416d48980c32eb20f95665f281ffc2927517 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 17:10:40 -0700 Subject: Removed commented-out EC2 code from volumes.py --- nova/api/openstack/incubator/volumes.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py index 47b86216e..0e6a1d0e9 100644 --- a/nova/api/openstack/incubator/volumes.py +++ b/nova/api/openstack/incubator/volumes.py @@ -49,24 +49,16 @@ def _translate_volume_summary_view(_context, vol): d = {} instance_id = None - # instance_data = None attached_to = vol.get('instance') if attached_to: instance_id = attached_to['id'] - # instance_data = '%s[%s]' % (instance_ec2_id, - # attached_to['host']) + d['id'] = vol['id'] d['status'] = vol['status'] d['size'] = vol['size'] d['availabilityZone'] = vol['availability_zone'] d['createdAt'] = vol['created_at'] - # if context.is_admin: - # v['status'] = '%s (%s, %s, %s, %s)' % ( - # vol['status'], - # vol['user_id'], - # vol['host'], - # instance_data, - # vol['mountpoint']) + if vol['attach_status'] == 'attached': d['attachments'] = [{'attachTime': vol['attach_time'], 'deleteOnTermination': False, -- cgit From 27b92e509c71a8b79dc6240aecdf598bf9d608f1 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 17:13:40 -0700 Subject: Change volume so that it returns attachments in the same format as is used for the attachment object --- nova/api/openstack/incubator/volumes.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py index 0e6a1d0e9..f96e20ed4 100644 --- a/nova/api/openstack/incubator/volumes.py +++ b/nova/api/openstack/incubator/volumes.py @@ -44,15 +44,10 @@ def _translate_volume_detail_view(context, vol): return d -def _translate_volume_summary_view(_context, vol): +def _translate_volume_summary_view(context, vol): """Maps keys for volumes summary view.""" d = {} - instance_id = None - attached_to = vol.get('instance') - if attached_to: - instance_id = attached_to['id'] - d['id'] = vol['id'] d['status'] = vol['status'] d['size'] = vol['size'] @@ -60,12 +55,7 @@ def _translate_volume_summary_view(_context, vol): d['createdAt'] = vol['created_at'] if vol['attach_status'] == 'attached': - d['attachments'] = [{'attachTime': vol['attach_time'], - 'deleteOnTermination': False, - 'mountpoint': vol['mountpoint'], - 'instanceId': instance_id, - 'status': 'attached', - 'volumeId': vol['id']}] + d['attachments'] = [_translate_attachment_detail_view(context, vol)] else: d['attachments'] = [{}] -- cgit From 6d3c31df757f65da7b29aaed1fb4d6e2b29126a0 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 17:17:20 -0700 Subject: Found a better (?) docstring from get_console_pool_info --- nova/virt/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index c6c28b2ba..2f52f9cf1 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -89,7 +89,7 @@ class ComputeDriver(object): raise NotImplementedError() def get_console_pool_info(self, console_type): - """? + """Get info about the host on which the VM resides. Returns a dict containing: :address: ? -- cgit From 86914566436d778cdae2244cb9b277e25e21cb21 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 17:22:20 -0700 Subject: Fix a docstring --- nova/api/openstack/incubator/volumes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py index f96e20ed4..6efacce52 100644 --- a/nova/api/openstack/incubator/volumes.py +++ b/nova/api/openstack/incubator/volumes.py @@ -202,7 +202,7 @@ class VolumeAttachmentController(wsgi.Controller): entity_maker=_translate_attachment_summary_view) def show(self, req, server_id, id): - """Return data about the given volume.""" + """Return data about the given volume attachment.""" context = req.environ['nova.context'] volume_id = id -- cgit From f0cd8a993d235fddbfb9478b69a77f4ed32f6dff Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 17:35:24 -0700 Subject: Wipe out the bad docstring on get_console_pool_info --- nova/virt/driver.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 2f52f9cf1..eb9626d08 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -89,14 +89,6 @@ class ComputeDriver(object): raise NotImplementedError() def get_console_pool_info(self, console_type): - """Get info about the host on which the VM resides. - - Returns a dict containing: - :address: ? - :username: ? - :password: ? - - """ raise NotImplementedError() def get_console_output(self, instance): -- cgit From 9c8300c98239c181cc66740bf18717f0488a0743 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 18:08:17 -0700 Subject: Renamed incubator => contrib --- nova/api/openstack/contrib/__init__.py | 22 ++ nova/api/openstack/contrib/volumes.py | 336 +++++++++++++++++++++++++++++++ nova/api/openstack/extensions.py | 4 +- nova/api/openstack/incubator/__init__.py | 22 -- nova/api/openstack/incubator/volumes.py | 336 ------------------------------- 5 files changed, 360 insertions(+), 360 deletions(-) create mode 100644 nova/api/openstack/contrib/__init__.py create mode 100644 nova/api/openstack/contrib/volumes.py delete mode 100644 nova/api/openstack/incubator/__init__.py delete mode 100644 nova/api/openstack/incubator/volumes.py diff --git a/nova/api/openstack/contrib/__init__.py b/nova/api/openstack/contrib/__init__.py new file mode 100644 index 000000000..e115f1ab9 --- /dev/null +++ b/nova/api/openstack/contrib/__init__.py @@ -0,0 +1,22 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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 datetime + +"""Incubator contains extensions that are shipped with nova. + +It can't be called 'extensions' because that causes namespacing problems. + +""" diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py new file mode 100644 index 000000000..6efacce52 --- /dev/null +++ b/nova/api/openstack/contrib/volumes.py @@ -0,0 +1,336 @@ +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The volumes extension.""" + +from webob import exc + +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + + +FLAGS = flags.FLAGS + + +def _translate_volume_detail_view(context, vol): + """Maps keys for volumes details view.""" + + d = _translate_volume_summary_view(context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_volume_summary_view(context, vol): + """Maps keys for volumes summary view.""" + d = {} + + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] + + if vol['attach_status'] == 'attached': + d['attachments'] = [_translate_attachment_detail_view(context, vol)] + else: + d['attachments'] = [{}] + + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +class VolumeController(wsgi.Controller): + """The Volumes API controller for the OpenStack API.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + ]}}} + + def __init__(self): + self.volume_api = volume.API() + super(VolumeController, self).__init__() + + def show(self, req, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_volume_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + def index(self, req): + """Returns a summary list of volumes.""" + return self._items(req, entity_maker=_translate_volume_summary_view) + + def detail(self, req): + """Returns a detailed list of volumes.""" + return self._items(req, entity_maker=_translate_volume_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker.""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumes': res} + + def create(self, req): + """Creates a new volume.""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + vol = env['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + new_volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) + + # Work around problem that instance is lazy-loaded... + new_volume['instance'] = None + + retval = _translate_volume_detail_view(context, new_volume) + + return {'volume': retval} + + +def _translate_attachment_detail_view(_context, vol): + """Maps keys for attachment details view.""" + + d = _translate_attachment_summary_view(_context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_attachment_summary_view(_context, vol): + """Maps keys for attachment summary view.""" + d = {} + + volume_id = vol['id'] + + # NOTE(justinsb): We use the volume id as the id of the attachment object + d['id'] = volume_id + + d['volumeId'] = volume_id + if vol.get('instance_id'): + d['serverId'] = vol['instance_id'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d + + +class VolumeAttachmentController(wsgi.Controller): + """The volume attachment API controller for the Openstack API. + + A child resource of the server. Note that we use the volume id + as the ID of the attachment (though this is not guaranteed externally) + + """ + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'volumeAttachment': ['id', + 'serverId', + 'volumeId', + 'device']}}} + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(VolumeAttachmentController, self).__init__() + + def index(self, req, server_id): + """Returns the list of volume attachments for a given instance.""" + return self._items(req, server_id, + entity_maker=_translate_attachment_summary_view) + + def show(self, req, server_id, id): + """Return data about the given volume attachment.""" + context = req.environ['nova.context'] + + volume_id = id + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + LOG.debug("volume_id not found") + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + return {'volumeAttachment': _translate_attachment_detail_view(context, + vol)} + + def create(self, req, server_id): + """Attach a volume to an instance.""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + instance_id = server_id + volume_id = env['volumeAttachment']['volumeId'] + device = env['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + try: + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + # The attach is async + attachment = {} + attachment['id'] = volume_id + attachment['volumeId'] = volume_id + + # NOTE(justinsb): And now, we have a problem... + # The attach is async, so there's a window in which we don't see + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. + # For now, we'll probably have to rely on libraries being smart + + # TODO(justinsb): How do I return "accepted" here? + return {'volumeAttachment': attachment} + + def update(self, _req, _server_id, _id): + """Update a volume attachment. We don't currently support this.""" + return faults.Fault(exc.HTTPBadRequest()) + + def delete(self, req, server_id, id): + """Detach a volume from an instance.""" + context = req.environ['nova.context'] + + volume_id = id + LOG.audit(_("Detach volume %s"), volume_id, context=context) + + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return exc.HTTPAccepted() + + def _items(self, req, server_id, entity_maker): + """Returns a list of attachments, transformed through entity_maker.""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + volumes = instance['volumes'] + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumeAttachments': res} + + +class Volumes(extensions.ExtensionDescriptor): + def get_name(self): + return "Volumes" + + def get_alias(self): + return "VOLUMES" + + def get_description(self): + return "Volumes support" + + def get_namespace(self): + return "http://docs.openstack.org/ext/volumes/api/v1.1" + + def get_updated(self): + return "2011-03-25T00:00:00+00:00" + + def get_resources(self): + resources = [] + + # NOTE(justinsb): No way to provide singular name ('volume') + # Does this matter? + res = extensions.ResourceExtension('volumes', + VolumeController(), + collection_actions={'detail': 'GET'} + ) + resources.append(res) + + res = extensions.ResourceExtension('volume_attachments', + VolumeAttachmentController(), + parent=dict( + member_name='server', + collection_name='servers')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 631275235..cba151fb6 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -378,7 +378,7 @@ class ExtensionManager(object): widgets.py the extension class within that module should be 'Widgets'. - In addition, extensions are loaded from the 'incubator' directory. + In addition, extensions are loaded from the 'contrib' directory. See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. @@ -387,7 +387,7 @@ class ExtensionManager(object): if os.path.exists(self.path): self._load_all_extensions_from_path(self.path) - incubator_path = os.path.join(os.path.dirname(__file__), "incubator") + incubator_path = os.path.join(os.path.dirname(__file__), "contrib") if os.path.exists(incubator_path): self._load_all_extensions_from_path(incubator_path) diff --git a/nova/api/openstack/incubator/__init__.py b/nova/api/openstack/incubator/__init__.py deleted file mode 100644 index e115f1ab9..000000000 --- a/nova/api/openstack/incubator/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# 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 datetime - -"""Incubator contains extensions that are shipped with nova. - -It can't be called 'extensions' because that causes namespacing problems. - -""" diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py deleted file mode 100644 index 6efacce52..000000000 --- a/nova/api/openstack/incubator/volumes.py +++ /dev/null @@ -1,336 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""The volumes extension.""" - -from webob import exc - -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova import wsgi -from nova.api.openstack import common -from nova.api.openstack import extensions -from nova.api.openstack import faults - - -LOG = logging.getLogger("nova.api.volumes") - - -FLAGS = flags.FLAGS - - -def _translate_volume_detail_view(context, vol): - """Maps keys for volumes details view.""" - - d = _translate_volume_summary_view(context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_volume_summary_view(context, vol): - """Maps keys for volumes summary view.""" - d = {} - - d['id'] = vol['id'] - d['status'] = vol['status'] - d['size'] = vol['size'] - d['availabilityZone'] = vol['availability_zone'] - d['createdAt'] = vol['created_at'] - - if vol['attach_status'] == 'attached': - d['attachments'] = [_translate_attachment_detail_view(context, vol)] - else: - d['attachments'] = [{}] - - d['displayName'] = vol['display_name'] - d['displayDescription'] = vol['display_description'] - return d - - -class VolumeController(wsgi.Controller): - """The Volumes API controller for the OpenStack API.""" - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "volume": [ - "id", - "status", - "size", - "availabilityZone", - "createdAt", - "displayName", - "displayDescription", - ]}}} - - def __init__(self): - self.volume_api = volume.API() - super(VolumeController, self).__init__() - - def show(self, req, id): - """Return data about the given volume.""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get(context, id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - return {'volume': _translate_volume_detail_view(context, vol)} - - def delete(self, req, id): - """Delete a volume.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete volume with id: %s"), id, context=context) - - try: - self.volume_api.delete(context, volume_id=id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() - - def index(self, req): - """Returns a summary list of volumes.""" - return self._items(req, entity_maker=_translate_volume_summary_view) - - def detail(self, req): - """Returns a detailed list of volumes.""" - return self._items(req, entity_maker=_translate_volume_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of volumes, transformed through entity_maker.""" - context = req.environ['nova.context'] - - volumes = self.volume_api.get_all(context) - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumes': res} - - def create(self, req): - """Creates a new volume.""" - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - vol = env['volume'] - size = vol['size'] - LOG.audit(_("Create volume of %s GB"), size, context=context) - new_volume = self.volume_api.create(context, size, - vol.get('display_name'), - vol.get('display_description')) - - # Work around problem that instance is lazy-loaded... - new_volume['instance'] = None - - retval = _translate_volume_detail_view(context, new_volume) - - return {'volume': retval} - - -def _translate_attachment_detail_view(_context, vol): - """Maps keys for attachment details view.""" - - d = _translate_attachment_summary_view(_context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_attachment_summary_view(_context, vol): - """Maps keys for attachment summary view.""" - d = {} - - volume_id = vol['id'] - - # NOTE(justinsb): We use the volume id as the id of the attachment object - d['id'] = volume_id - - d['volumeId'] = volume_id - if vol.get('instance_id'): - d['serverId'] = vol['instance_id'] - if vol.get('mountpoint'): - d['device'] = vol['mountpoint'] - - return d - - -class VolumeAttachmentController(wsgi.Controller): - """The volume attachment API controller for the Openstack API. - - A child resource of the server. Note that we use the volume id - as the ID of the attachment (though this is not guaranteed externally) - - """ - - _serialization_metadata = { - 'application/xml': { - 'attributes': { - 'volumeAttachment': ['id', - 'serverId', - 'volumeId', - 'device']}}} - - def __init__(self): - self.compute_api = compute.API() - self.volume_api = volume.API() - super(VolumeAttachmentController, self).__init__() - - def index(self, req, server_id): - """Returns the list of volume attachments for a given instance.""" - return self._items(req, server_id, - entity_maker=_translate_attachment_summary_view) - - def show(self, req, server_id, id): - """Return data about the given volume attachment.""" - context = req.environ['nova.context'] - - volume_id = id - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - LOG.debug("volume_id not found") - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - return {'volumeAttachment': _translate_attachment_detail_view(context, - vol)} - - def create(self, req, server_id): - """Attach a volume to an instance.""" - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - instance_id = server_id - volume_id = env['volumeAttachment']['volumeId'] - device = env['volumeAttachment']['device'] - - msg = _("Attach volume %(volume_id)s to instance %(server_id)s" - " at %(device)s") % locals() - LOG.audit(msg, context=context) - - try: - self.compute_api.attach_volume(context, - instance_id=instance_id, - volume_id=volume_id, - device=device) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - # The attach is async - attachment = {} - attachment['id'] = volume_id - attachment['volumeId'] = volume_id - - # NOTE(justinsb): And now, we have a problem... - # The attach is async, so there's a window in which we don't see - # the attachment (until the attachment completes). We could also - # get problems with concurrent requests. I think we need an - # attachment state, and to write to the DB here, but that's a bigger - # change. - # For now, we'll probably have to rely on libraries being smart - - # TODO(justinsb): How do I return "accepted" here? - return {'volumeAttachment': attachment} - - def update(self, _req, _server_id, _id): - """Update a volume attachment. We don't currently support this.""" - return faults.Fault(exc.HTTPBadRequest()) - - def delete(self, req, server_id, id): - """Detach a volume from an instance.""" - context = req.environ['nova.context'] - - volume_id = id - LOG.audit(_("Detach volume %s"), volume_id, context=context) - - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - self.compute_api.detach_volume(context, - volume_id=volume_id) - - return exc.HTTPAccepted() - - def _items(self, req, server_id, entity_maker): - """Returns a list of attachments, transformed through entity_maker.""" - context = req.environ['nova.context'] - - try: - instance = self.compute_api.get(context, server_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - volumes = instance['volumes'] - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumeAttachments': res} - - -class Volumes(extensions.ExtensionDescriptor): - def get_name(self): - return "Volumes" - - def get_alias(self): - return "VOLUMES" - - def get_description(self): - return "Volumes support" - - def get_namespace(self): - return "http://docs.openstack.org/ext/volumes/api/v1.1" - - def get_updated(self): - return "2011-03-25T00:00:00+00:00" - - def get_resources(self): - resources = [] - - # NOTE(justinsb): No way to provide singular name ('volume') - # Does this matter? - res = extensions.ResourceExtension('volumes', - VolumeController(), - collection_actions={'detail': 'GET'} - ) - resources.append(res) - - res = extensions.ResourceExtension('volume_attachments', - VolumeAttachmentController(), - parent=dict( - member_name='server', - collection_name='servers')) - resources.append(res) - - return resources -- cgit From be8bf22f90e322823cb3cf4963f5c7313ef727ec Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 18:08:36 -0700 Subject: Removed unused super_verbose argument left over from previous code --- nova/api/openstack/extensions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index cba151fb6..6813d85c9 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -315,8 +315,6 @@ class ExtensionManager(object): def __init__(self, path): LOG.audit(_('Initializing extension manager.')) - self.super_verbose = False - self.path = path self.extensions = {} self._load_all_extensions() -- cgit From 4c16db6ad330b0c3a1bdde098bbdcf958fc23bdf Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 18:13:04 -0700 Subject: Rename MockImageService -> FakeImageService --- nova/image/fake.py | 4 ++-- nova/tests/integrated/integrated_helpers.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/image/fake.py b/nova/image/fake.py index 4caf68d63..c84f7ec02 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -28,7 +28,7 @@ LOG = logging.getLogger('nova.image.fake') FLAGS = flags.FLAGS -class MockImageService(service.BaseImageService): +class FakeImageService(service.BaseImageService): """Mock (fake) image service for unit testing.""" def __init__(self): @@ -43,7 +43,7 @@ class MockImageService(service.BaseImageService): 'disk_format': 'ami'} } self.create(None, image) - super(MockImageService, self).__init__() + super(FakeImageService, self).__init__() def index(self, context): """Returns list of images.""" diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index e0fe2d2a2..2e5d67017 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -184,7 +184,7 @@ class _IntegratedTestBase(test.TestCase): def _get_flags(self): """An opportunity to setup flags, before the services are started.""" f = {} - f['image_service'] = 'nova.image.fake.MockImageService' + f['image_service'] = 'nova.image.fake.FakeImageService' f['fake_network'] = True return f -- cgit From 2315682856f420ff0b781bead142e1aff82071a4 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 18:16:09 -0700 Subject: "Incubator" is no more. Long live "contrib" --- nova/api/openstack/contrib/__init__.py | 2 +- nova/api/openstack/extensions.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/contrib/__init__.py b/nova/api/openstack/contrib/__init__.py index e115f1ab9..b42a1d89d 100644 --- a/nova/api/openstack/contrib/__init__.py +++ b/nova/api/openstack/contrib/__init__.py @@ -15,7 +15,7 @@ # License for the specific language governing permissions and limitations # under the License.import datetime -"""Incubator contains extensions that are shipped with nova. +"""Contrib contains extensions that are shipped with nova. It can't be called 'extensions' because that causes namespacing problems. diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 6813d85c9..fb1dccb28 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -385,9 +385,9 @@ class ExtensionManager(object): if os.path.exists(self.path): self._load_all_extensions_from_path(self.path) - incubator_path = os.path.join(os.path.dirname(__file__), "contrib") - if os.path.exists(incubator_path): - self._load_all_extensions_from_path(incubator_path) + contrib_path = os.path.join(os.path.dirname(__file__), "contrib") + if os.path.exists(contrib_path): + self._load_all_extensions_from_path(contrib_path) def _load_all_extensions_from_path(self, path): for f in os.listdir(path): -- cgit From f4b9e95bd143d46cd4939a3ea31de10a3e66fd90 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 19:10:23 -0700 Subject: name, created_at, updated_at are required. I think some of the other image services might also break because of this, but that's a different issue... --- nova/image/fake.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nova/image/fake.py b/nova/image/fake.py index c84f7ec02..16c628091 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -16,6 +16,8 @@ # under the License. """Implementation of an fake image service""" +import datetime + from nova import exception from nova import flags from nova import log as logging @@ -35,7 +37,11 @@ class FakeImageService(service.BaseImageService): self.images = {} # NOTE(justinsb): The OpenStack API can't upload an image? # So, make sure we've got one.. + timestamp = datetime.datetime(2011, 01, 01, 01, 02, 03) image = {'id': '123456', + 'name': 'fakeimage123456', + 'created_at': timestamp, + 'updated_at': timestamp, 'status': 'active', 'type': 'machine', 'properties': {'kernel_id': FLAGS.null_kernel, -- cgit From 9397766990b00167071bca6392096abfb93af982 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 19:11:12 -0700 Subject: Deepcopy the images, because the string formatting transforms them in-place --- nova/image/fake.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/nova/image/fake.py b/nova/image/fake.py index 16c628091..08302d6eb 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -16,6 +16,7 @@ # under the License. """Implementation of an fake image service""" +import copy import datetime from nova import exception @@ -53,11 +54,11 @@ class FakeImageService(service.BaseImageService): def index(self, context): """Returns list of images.""" - return self.images.values() + return copy.deepcopy(self.images.values()) def detail(self, context): """Return list of detailed image information.""" - return self.images.values() + return copy.deepcopy(self.images.values()) def show(self, context, image_id): """Get data about specified image. @@ -68,7 +69,7 @@ class FakeImageService(service.BaseImageService): image_id = int(image_id) image = self.images.get(image_id) if image: - return image + return copy.deepcopy(image) LOG.warn("Unable to find image id %s. Have images: %s", image_id, self.images) raise exception.NotFound @@ -83,7 +84,7 @@ class FakeImageService(service.BaseImageService): if self.images.get(image_id): raise exception.Duplicate() - self.images[image_id] = data + self.images[image_id] = copy.deepcopy(data) def update(self, context, image_id, data): """Replace the contents of the given image with the new data. @@ -94,7 +95,7 @@ class FakeImageService(service.BaseImageService): image_id = int(image_id) if not self.images.get(image_id): raise exception.NotFound - self.images[image_id] = data + self.images[image_id] = copy.deepcopy(data) def delete(self, context, image_id): """Delete the given image. -- cgit From 3e9bafd4f05a4bda29c30460bf3e3428a03f8218 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 29 Mar 2011 22:37:19 -0700 Subject: fix doc to refer to nova-vncproxy --- doc/source/runnova/vncconsole.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/source/runnova/vncconsole.rst b/doc/source/runnova/vncconsole.rst index 942ace611..c1fe9be39 100644 --- a/doc/source/runnova/vncconsole.rst +++ b/doc/source/runnova/vncconsole.rst @@ -26,7 +26,7 @@ A VNC Connection works like so: * User connects over an api and gets a url like http://ip:port/?token=xyz * User pastes url in browser * Browser connects to VNC Proxy though a websocket enabled client like noVNC -* VNC Proxy authorizes users token, maps the token to a host and port of an +* VNC Proxy authorizes users token, maps the token to a host and port of an instance's VNC server * VNC Proxy initiates connection to VNC server, and continues proxying until the session ends @@ -34,17 +34,17 @@ A VNC Connection works like so: Configuring the VNC Proxy ------------------------- -nova-vnc-proxy requires a websocket enabled html client to work properly. At -this time, the only tested client is a slightly modified fork of noVNC, which +nova-vncproxy requires a websocket enabled html client to work properly. At +this time, the only tested client is a slightly modified fork of noVNC, which you can at find http://github.com/openstack/noVNC.git .. todo:: add instruction for installing from package noVNC must be in the location specified by --vncproxy_wwwroot, which defaults -to /var/lib/nova/noVNC. nova-vnc-proxy will fail to launch until this code -is properly installed. +to /var/lib/nova/noVNC. nova-vncproxy will fail to launch until this code +is properly installed. -By default, nova-vnc-proxy binds 0.0.0.0:6080. This can be configured with: +By default, nova-vncproxy binds 0.0.0.0:6080. This can be configured with: * --vncproxy_port=[port] * --vncproxy_host=[host] @@ -55,17 +55,17 @@ Enabling VNC Consoles in Nova At the moment, VNC support is supported only when using libvirt. To enable VNC Console, configure the following flags: -* --vnc_console_proxy_url=http://[proxy_host]:[proxy_port] - proxy_port - defaults to 6080. This url must point to nova-vnc-proxy +* --vnc_console_proxy_url=http://[proxy_host]:[proxy_port] - proxy_port + defaults to 6080. This url must point to nova-vncproxy * --vnc_enabled=[True|False] - defaults to True. If this flag is not set your - instances will launch without vnc support. + instances will launch without vnc support. Getting an instance's VNC Console --------------------------------- You can access an instance's VNC Console url in the following methods: -* Using the direct api: +* Using the direct api: eg: 'stack --user=admin --project=admin compute get_vnc_console instance_id=1' * Support for Dashboard, and the Openstack API will be forthcoming -- cgit From 7856df88b22e6ff3bd0f124e3d71f130e3e9c205 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 29 Mar 2011 22:41:15 -0700 Subject: fix localization for multiple replacement strings --- nova/vnc/auth.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py index c7df13450..ce5e10388 100644 --- a/nova/vnc/auth.py +++ b/nova/vnc/auth.py @@ -117,11 +117,13 @@ class VNCProxyAuthManager(manager.Manager): self.tokens[token] = {'host': host, 'port': port, 'last_activity_at': time.time()} - LOG.audit(_("Received Token: %s, %s)"), token, self.tokens[token]) + token_dict = self.tokens[token] + LOG.audit(_("Received Token: %(token)s, %(token_dict)s)"), locals()) def check_token(self, context, token): - LOG.audit(_("Checking Token: %s, %s)"), token, (token in self.tokens)) - if token in self.tokens: + token_valid = token in self.tokens + LOG.audit(_("Checking Token: %(token)s, %(token_valid)s)"), locals()) + if token_valid: return self.tokens[token] def _delete_expired_tokens(self): -- cgit