summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Authors7
-rwxr-xr-xbin/nova-api1
-rwxr-xr-xbin/nova-combined1
-rwxr-xr-xbin/nova-manage12
-rw-r--r--contrib/boto_v6/__init__.py37
-rw-r--r--contrib/boto_v6/ec2/__init__.py0
-rw-r--r--contrib/boto_v6/ec2/connection.py41
-rw-r--r--contrib/boto_v6/ec2/instance.py37
-rwxr-xr-xcontrib/nova.sh14
-rw-r--r--nova/api/ec2/cloud.py47
-rw-r--r--nova/api/openstack/servers.py9
-rw-r--r--nova/auth/manager.py4
-rw-r--r--nova/compute/api.py115
-rw-r--r--nova/compute/disk.py205
-rw-r--r--nova/compute/manager.py33
-rw-r--r--nova/db/api.py16
-rw-r--r--nova/db/sqlalchemy/api.py29
-rw-r--r--nova/db/sqlalchemy/models.py8
-rw-r--r--nova/exception.py4
-rw-r--r--nova/flags.py1
-rw-r--r--nova/network/linux_net.py83
-rw-r--r--nova/network/manager.py32
-rw-r--r--nova/test.py3
-rw-r--r--nova/tests/test_api.py66
-rw-r--r--nova/tests/test_cloud.py15
-rw-r--r--nova/tests/test_compute.py7
-rw-r--r--nova/tests/test_network.py22
-rw-r--r--nova/tests/test_xenapi.py27
-rw-r--r--nova/utils.py36
-rw-r--r--nova/virt/disk.py186
-rw-r--r--nova/virt/fake.py21
-rw-r--r--nova/virt/libvirt.xml.template17
-rw-r--r--nova/virt/libvirt_conn.py400
-rw-r--r--nova/virt/xenapi/vmops.py203
-rw-r--r--nova/virt/xenapi_conn.py7
-rw-r--r--nova/volume/driver.py106
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/agent126
-rw-r--r--smoketests/admin_smoketests.py3
-rw-r--r--smoketests/base.py17
-rw-r--r--smoketests/flags.py3
-rw-r--r--smoketests/public_network_smoketests.py180
-rw-r--r--smoketests/user_smoketests.py39
-rw-r--r--tools/pip-requires1
43 files changed, 1732 insertions, 489 deletions
diff --git a/Authors b/Authors
index 56344957e..bcb2cd0fb 100644
--- a/Authors
+++ b/Authors
@@ -4,8 +4,8 @@ Anthony Young <sleepsonthefloor@gmail.com>
Antony Messerli <ant@openstack.org>
Armando Migliaccio <Armando.Migliaccio@eu.citrix.com>
Chiradeep Vittal <chiradeep@cloud.com>
-Chris Behrens <cbehrens@codestud.com>
Chmouel Boudjnah <chmouel@chmouel.com>
+Chris Behrens <cbehrens@codestud.com>
Cory Wright <corywright@gmail.com>
David Pravec <David.Pravec@danix.org>
Dean Troyer <dtroyer@gmail.com>
@@ -14,6 +14,7 @@ Ed Leafe <ed@leafe.com>
Eldar Nugaev <enugaev@griddynamics.com>
Eric Day <eday@oddments.org>
Ewan Mellor <ewan.mellor@citrix.com>
+Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>
Hisaki Ohara <hisaki.ohara@intel.com>
Ilya Alekseyev <ialekseev@griddynamics.com>
Jay Pipes <jaypipes@gmail.com>
@@ -21,15 +22,19 @@ Jesse Andrews <anotherjesse@gmail.com>
Joe Heck <heckj@mac.com>
Joel Moore <joelbm24@gmail.com>
Jonathan Bryce <jbryce@jbryce.com>
+Josh Durgin <joshd@hq.newdream.net>
Josh Kearney <josh.kearney@rackspace.com>
Joshua McKenty <jmckenty@gmail.com>
Justin Santa Barbara <justin@fathomdb.com>
Ken Pepple <ken.pepple@gmail.com>
+Koji Iida <iida.koji@lab.ntt.co.jp>
Lorin Hochstein <lorin@isi.edu>
Matt Dietz <matt.dietz@rackspace.com>
Michael Gundlach <michael.gundlach@rackspace.com>
Monsyne Dragon <mdragon@rackspace.com>
Monty Taylor <mordred@inaugust.com>
+MORITA Kazutaka <morita.kazutaka@gmail.com>
+Nachi Ueno <ueno.nachi@lab.ntt.co.jp> <openstack@lab.ntt.co.jp> <nati.ueno@gmail.com> <nova@u4>
Paul Voccio <paul@openstack.org>
Rick Clark <rick@openstack.org>
Rick Harris <rconradharris@gmail.com>
diff --git a/bin/nova-api b/bin/nova-api
index 1c671201e..c7cbb3ab4 100755
--- a/bin/nova-api
+++ b/bin/nova-api
@@ -41,7 +41,6 @@ from nova import wsgi
FLAGS = flags.FLAGS
-flags.DEFINE_integer('osapi_port', 8774, 'OpenStack API port')
flags.DEFINE_string('osapi_host', '0.0.0.0', 'OpenStack API host')
flags.DEFINE_integer('ec2api_port', 8773, 'EC2 API port')
flags.DEFINE_string('ec2api_host', '0.0.0.0', 'EC2 API host')
diff --git a/bin/nova-combined b/bin/nova-combined
index 53322f1a0..f932fdfd5 100755
--- a/bin/nova-combined
+++ b/bin/nova-combined
@@ -44,7 +44,6 @@ from nova import wsgi
FLAGS = flags.FLAGS
-flags.DEFINE_integer('osapi_port', 8774, 'OpenStack API port')
flags.DEFINE_string('osapi_host', '0.0.0.0', 'OpenStack API host')
flags.DEFINE_integer('ec2api_port', 8773, 'EC2 API port')
flags.DEFINE_string('ec2api_host', '0.0.0.0', 'EC2 API host')
diff --git a/bin/nova-manage b/bin/nova-manage
index 3f5957190..b5842b595 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -77,18 +77,21 @@ from nova import crypto
from nova import db
from nova import exception
from nova import flags
+from nova import log as logging
from nova import quota
from nova import utils
from nova.auth import manager
from nova.cloudpipe import pipelib
+logging.basicConfig()
FLAGS = flags.FLAGS
flags.DECLARE('fixed_range', 'nova.network.manager')
flags.DECLARE('num_networks', 'nova.network.manager')
flags.DECLARE('network_size', 'nova.network.manager')
flags.DECLARE('vlan_start', 'nova.network.manager')
flags.DECLARE('vpn_start', 'nova.network.manager')
+flags.DECLARE('fixed_range_v6', 'nova.network.manager')
class VpnCommands(object):
@@ -437,11 +440,12 @@ class NetworkCommands(object):
"""Class for managing networks."""
def create(self, fixed_range=None, num_networks=None,
- network_size=None, vlan_start=None, vpn_start=None):
+ network_size=None, vlan_start=None, vpn_start=None,
+ fixed_range_v6=None):
"""Creates fixed ips for host by range
arguments: [fixed_range=FLAG], [num_networks=FLAG],
[network_size=FLAG], [vlan_start=FLAG],
- [vpn_start=FLAG]"""
+ [vpn_start=FLAG], [fixed_range_v6=FLAG]"""
if not fixed_range:
fixed_range = FLAGS.fixed_range
if not num_networks:
@@ -452,11 +456,13 @@ class NetworkCommands(object):
vlan_start = FLAGS.vlan_start
if not vpn_start:
vpn_start = FLAGS.vpn_start
+ if not fixed_range_v6:
+ fixed_range_v6 = FLAGS.fixed_range_v6
net_manager = utils.import_object(FLAGS.network_manager)
net_manager.create_networks(context.get_admin_context(),
fixed_range, int(num_networks),
int(network_size), int(vlan_start),
- int(vpn_start))
+ int(vpn_start), fixed_range_v6)
class ServiceCommands(object):
diff --git a/contrib/boto_v6/__init__.py b/contrib/boto_v6/__init__.py
new file mode 100644
index 000000000..9fec157f1
--- /dev/null
+++ b/contrib/boto_v6/__init__.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+# All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+
+def connect_ec2(aws_access_key_id=None, aws_secret_access_key=None, **kwargs):
+ """
+ :type aws_access_key_id: string
+ :param aws_access_key_id: Your AWS Access Key ID
+
+ :type aws_secret_access_key: string
+ :param aws_secret_access_key: Your AWS Secret Access Key
+
+ :rtype: :class:`boto.ec2.connection.EC2Connection`
+ :return: A connection to Amazon's EC2
+ """
+ from boto_v6.ec2.connection import EC2ConnectionV6
+ return EC2ConnectionV6(aws_access_key_id, aws_secret_access_key, **kwargs)
diff --git a/contrib/boto_v6/ec2/__init__.py b/contrib/boto_v6/ec2/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/contrib/boto_v6/ec2/__init__.py
diff --git a/contrib/boto_v6/ec2/connection.py b/contrib/boto_v6/ec2/connection.py
new file mode 100644
index 000000000..23466e5d7
--- /dev/null
+++ b/contrib/boto_v6/ec2/connection.py
@@ -0,0 +1,41 @@
+'''
+Created on 2010/12/20
+
+@author: Nachi Ueno <ueno.nachi@lab.ntt.co.jp>
+'''
+import boto
+import boto.ec2
+from boto_v6.ec2.instance import ReservationV6
+
+
+class EC2ConnectionV6(boto.ec2.EC2Connection):
+ '''
+ EC2Connection for OpenStack IPV6 mode
+ '''
+ def get_all_instances(self, instance_ids=None, filters=None):
+ """
+ Retrieve all the instances associated with your account.
+
+ :type instance_ids: list
+ :param instance_ids: A list of strings of instance IDs
+
+ :type filters: dict
+ :param filters: Optional filters that can be used to limit
+ the results returned. Filters are provided
+ in the form of a dictionary consisting of
+ filter names as the key and filter values
+ as the value. The set of allowable filter
+ names/values is dependent on the request
+ being performed. Check the EC2 API guide
+ for details.
+
+ :rtype: list
+ :return: A list of :class:`boto.ec2.instance.Reservation`
+ """
+ params = {}
+ if instance_ids:
+ self.build_list_params(params, instance_ids, 'InstanceId')
+ if filters:
+ self.build_filter_params(params, filters)
+ return self.get_list('DescribeInstancesV6', params,
+ [('item', ReservationV6)])
diff --git a/contrib/boto_v6/ec2/instance.py b/contrib/boto_v6/ec2/instance.py
new file mode 100644
index 000000000..18b5cd33a
--- /dev/null
+++ b/contrib/boto_v6/ec2/instance.py
@@ -0,0 +1,37 @@
+'''
+Created on 2010/12/20
+
+@author: Nachi Ueno <ueno.nachi@lab.ntt.co.jp>
+'''
+import boto
+from boto.resultset import ResultSet
+from boto.ec2.instance import Reservation
+from boto.ec2.instance import Group
+from boto.ec2.instance import Instance
+
+
+class ReservationV6(Reservation):
+ def startElement(self, name, attrs, connection):
+ if name == 'instancesSet':
+ self.instances = ResultSet([('item', InstanceV6)])
+ return self.instances
+ elif name == 'groupSet':
+ self.groups = ResultSet([('item', Group)])
+ return self.groups
+ else:
+ return None
+
+
+class InstanceV6(Instance):
+ def __init__(self, connection=None):
+ Instance.__init__(self, connection)
+ self.dns_name_v6 = None
+
+ def endElement(self, name, value, connection):
+ Instance.endElement(self, name, value, connection)
+ if name == 'dnsNameV6':
+ self.dns_name_v6 = value
+
+ def _update(self, updated):
+ self.__dict__.update(updated.__dict__)
+ self.dns_name_v6 = updated.dns_name_v6
diff --git a/contrib/nova.sh b/contrib/nova.sh
index e06706295..a0e8e642c 100755
--- a/contrib/nova.sh
+++ b/contrib/nova.sh
@@ -83,9 +83,17 @@ if [ "$CMD" == "install" ]; then
sudo /etc/init.d/iscsitarget restart
sudo modprobe kvm
sudo /etc/init.d/libvirt-bin restart
+ sudo modprobe nbd
sudo apt-get install -y python-twisted python-sqlalchemy python-mox python-greenlet python-carrot
- sudo apt-get install -y python-daemon python-eventlet python-gflags python-tornado python-ipy
- sudo apt-get install -y python-libvirt python-libxml2 python-routes
+ sudo apt-get install -y python-daemon python-eventlet python-gflags python-ipy
+ sudo apt-get install -y python-libvirt python-libxml2 python-routes python-cheetah
+#For IPV6
+ sudo apt-get install -y python-netaddr
+ sudo apt-get install -y radvd
+#(Nati) Note that this configuration is only needed for nova-network node.
+ sudo bash -c "echo 1 > /proc/sys/net/ipv6/conf/all/forwarding"
+ sudo bash -c "echo 0 > /proc/sys/net/ipv6/conf/all/accept_ra"
+
if [ "$USE_MYSQL" == 1 ]; then
cat <<MYSQL_PRESEED | debconf-set-selections
mysql-server-5.1 mysql-server/root_password password $MYSQL_PASS
@@ -107,6 +115,8 @@ function screen_it {
if [ "$CMD" == "run" ]; then
killall dnsmasq
+ #For IPv6
+ killall radvd
screen -d -m -S nova -t nova
sleep 1
if [ "$USE_MYSQL" == 1 ]; then
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 832426b94..630aaeaf2 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -26,9 +26,11 @@ import base64
import datetime
import IPy
import os
+import urllib
from nova import compute
from nova import context
+
from nova import crypto
from nova import db
from nova import exception
@@ -73,17 +75,13 @@ def _gen_key(context, user_id, key_name):
def ec2_id_to_id(ec2_id):
- """Convert an ec2 ID (i-[base 36 number]) to an instance id (int)"""
- return int(ec2_id[2:], 36)
+ """Convert an ec2 ID (i-[base 16 number]) to an instance id (int)"""
+ return int(ec2_id.split('-')[-1], 16)
-def id_to_ec2_id(instance_id):
- """Convert an instance ID (int) to an ec2 ID (i-[base 36 number])"""
- digits = []
- while instance_id != 0:
- instance_id, remainder = divmod(instance_id, 36)
- digits.append('0123456789abcdefghijklmnopqrstuvwxyz'[remainder])
- return "i-%s" % ''.join(reversed(digits))
+def id_to_ec2_id(instance_id, template='i-%08x'):
+ """Convert an instance ID (int) to an ec2 ID (i-[base 16 number])"""
+ return template % instance_id
class CloudController(object):
@@ -378,6 +376,7 @@ class CloudController(object):
values['group_id'] = source_security_group['id']
elif cidr_ip:
# If this fails, it throws an exception. This is what we want.
+ cidr_ip = urllib.unquote(cidr_ip).decode()
IPy.IP(cidr_ip)
values['cidr'] = cidr_ip
else:
@@ -541,6 +540,8 @@ class CloudController(object):
return self.compute_api.get_ajax_console(context, internal_id)
def describe_volumes(self, context, volume_id=None, **kwargs):
+ if volume_id:
+ volume_id = [ec2_id_to_id(x) for x in volume_id]
volumes = self.volume_api.get_all(context)
# NOTE(vish): volume_id is an optional list of volume ids to filter by.
volumes = [self._format_volume(context, v) for v in volumes
@@ -556,7 +557,7 @@ class CloudController(object):
instance_data = '%s[%s]' % (instance_ec2_id,
volume['instance']['host'])
v = {}
- v['volumeId'] = volume['id']
+ v['volumeId'] = id_to_ec2_id(volume['id'], 'vol-%08x')
v['status'] = volume['status']
v['size'] = volume['size']
v['availabilityZone'] = volume['availability_zone']
@@ -574,7 +575,8 @@ class CloudController(object):
'device': volume['mountpoint'],
'instanceId': instance_ec2_id,
'status': 'attached',
- 'volume_id': volume['ec2_id']}]
+ 'volumeId': id_to_ec2_id(volume['id'],
+ 'vol-%08x')}]
else:
v['attachmentSet'] = [{}]
@@ -590,13 +592,15 @@ class CloudController(object):
# TODO(vish): Instance should be None at db layer instead of
# trying to lazy load, but for now we turn it into
# a dict to avoid an error.
- return {'volumeSet': [self._format_volume(context, dict(volume_ref))]}
+ return {'volumeSet': [self._format_volume(context, dict(volume))]}
def delete_volume(self, context, volume_id, **kwargs):
+ volume_id = ec2_id_to_id(volume_id)
self.volume_api.delete(context, volume_id)
return True
def update_volume(self, context, volume_id, **kwargs):
+ volume_id = ec2_id_to_id(volume_id)
updatable_fields = ['display_name', 'display_description']
changes = {}
for field in updatable_fields:
@@ -607,18 +611,21 @@ class CloudController(object):
return True
def attach_volume(self, context, volume_id, instance_id, device, **kwargs):
+ volume_id = ec2_id_to_id(volume_id)
+ instance_id = ec2_id_to_id(instance_id)
LOG.audit(_("Attach volume %s to instacne %s at %s"), volume_id,
instance_id, device, context=context)
self.compute_api.attach_volume(context, instance_id, volume_id, device)
volume = self.volume_api.get(context, volume_id)
return {'attachTime': volume['attach_time'],
'device': volume['mountpoint'],
- 'instanceId': instance_id,
+ 'instanceId': id_to_ec2_id(instance_id),
'requestId': context.request_id,
'status': volume['attach_status'],
- 'volumeId': volume_id}
+ 'volumeId': id_to_ec2_id(volume_id, 'vol-%08x')}
def detach_volume(self, context, volume_id, **kwargs):
+ volume_id = ec2_id_to_id(volume_id)
LOG.audit(_("Detach volume %s"), volume_id, context=context)
volume = self.volume_api.get(context, volume_id)
instance = self.compute_api.detach_volume(context, volume_id)
@@ -627,7 +634,7 @@ class CloudController(object):
'instanceId': id_to_ec2_id(instance['id']),
'requestId': context.request_id,
'status': volume['attach_status'],
- 'volumeId': volume_id}
+ 'volumeId': id_to_ec2_id(volume_id, 'vol-%08x')}
def _convert_to_set(self, lst, label):
if lst == None or lst == []:
@@ -639,6 +646,10 @@ class CloudController(object):
def describe_instances(self, context, **kwargs):
return self._format_describe_instances(context, **kwargs)
+ def describe_instances_v6(self, context, **kwargs):
+ kwargs['use_v6'] = True
+ return self._format_describe_instances(context, **kwargs)
+
def _format_describe_instances(self, context, **kwargs):
return {'reservationSet': self._format_instances(context, **kwargs)}
@@ -674,10 +685,16 @@ class CloudController(object):
if instance['fixed_ip']['floating_ips']:
fixed = instance['fixed_ip']
floating_addr = fixed['floating_ips'][0]['address']
+ if instance['fixed_ip']['network'] and 'use_v6' in kwargs:
+ i['dnsNameV6'] = utils.to_global_ipv6(
+ instance['fixed_ip']['network']['cidr_v6'],
+ instance['mac_address'])
+
i['privateDnsName'] = fixed_addr
i['publicDnsName'] = floating_addr
i['dnsName'] = i['publicDnsName'] or i['privateDnsName']
i['keyName'] = instance['key_name']
+
if context.user.is_admin():
i['keyName'] = '%s (%s, %s)' % (i['keyName'],
instance['project_id'],
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 29af82533..8cbcebed2 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -165,15 +165,18 @@ class Controller(wsgi.Controller):
if not inst_dict:
return faults.Fault(exc.HTTPUnprocessableEntity())
+ ctxt = req.environ['nova.context']
update_dict = {}
if 'adminPass' in inst_dict['server']:
update_dict['admin_pass'] = inst_dict['server']['adminPass']
+ try:
+ self.compute_api.set_admin_password(ctxt, id)
+ except exception.TimeoutException, e:
+ return exc.HTTPRequestTimeout()
if 'name' in inst_dict['server']:
update_dict['display_name'] = inst_dict['server']['name']
-
try:
- self.compute_api.update(req.environ['nova.context'], id,
- **update_dict)
+ self.compute_api.update(ctxt, id, **update_dict)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPNoContent()
diff --git a/nova/auth/manager.py b/nova/auth/manager.py
index 89f02998d..de18a3838 100644
--- a/nova/auth/manager.py
+++ b/nova/auth/manager.py
@@ -721,7 +721,7 @@ class AuthManager(object):
if project is None:
project = user.id
pid = Project.safe_id(project)
- return self.__generate_rc(user.access, user.secret, pid, use_dmz)
+ return self.__generate_rc(user, pid, use_dmz)
@staticmethod
def __generate_rc(user, pid, use_dmz=True, host=None):
@@ -747,7 +747,7 @@ class AuthManager(object):
's3': 'http://%s:%s' % (s3_host, FLAGS.s3_port),
'os': '%s://%s:%s%s' % (FLAGS.os_prefix,
cc_host,
- FLAGS.cc_port,
+ FLAGS.osapi_port,
FLAGS.os_suffix),
'user': user.name,
'nova': FLAGS.ca_file,
diff --git a/nova/compute/api.py b/nova/compute/api.py
index bf921aa00..90273da36 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -280,7 +280,7 @@ class API(base.Base):
return self.db.instance_update(context, instance_id, kwargs)
def delete(self, context, instance_id):
- LOG.debug(_("Going to try and terminate %s"), instance_id)
+ LOG.debug(_("Going to try to terminate %s"), instance_id)
try:
instance = self.get(context, instance_id)
except exception.NotFound as e:
@@ -301,10 +301,8 @@ class API(base.Base):
host = instance['host']
if host:
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "terminate_instance",
- "args": {"instance_id": instance_id}})
+ self._cast_compute_message('terminate_instance', context,
+ instance_id, host)
else:
self.db.instance_destroy(context, instance_id)
@@ -332,50 +330,34 @@ class API(base.Base):
project_id)
return self.db.instance_get_all(context)
+ def _cast_compute_message(self, method, context, instance_id, host=None):
+ """Generic handler for RPC calls to compute."""
+ if not host:
+ instance = self.get(context, instance_id)
+ host = instance['host']
+ queue = self.db.queue_get_for(context, FLAGS.compute_topic, host)
+ kwargs = {'method': method, 'args': {'instance_id': instance_id}}
+ rpc.cast(context, queue, kwargs)
+
def snapshot(self, context, instance_id, name):
"""Snapshot the given instance."""
- instance = self.get(context, instance_id)
- host = instance['host']
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "snapshot_instance",
- "args": {"instance_id": instance_id, "name": name}})
+ self._cast_compute_message('snapshot_instance', context, instance_id)
def reboot(self, context, instance_id):
"""Reboot the given instance."""
- instance = self.get(context, instance_id)
- host = instance['host']
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "reboot_instance",
- "args": {"instance_id": instance_id}})
+ self._cast_compute_message('reboot_instance', context, instance_id)
def pause(self, context, instance_id):
"""Pause the given instance."""
- instance = self.get(context, instance_id)
- host = instance['host']
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "pause_instance",
- "args": {"instance_id": instance_id}})
+ self._cast_compute_message('pause_instance', context, instance_id)
def unpause(self, context, instance_id):
"""Unpause the given instance."""
- instance = self.get(context, instance_id)
- host = instance['host']
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "unpause_instance",
- "args": {"instance_id": instance_id}})
+ self._cast_compute_message('unpause_instance', context, instance_id)
def get_diagnostics(self, context, instance_id):
"""Retrieve diagnostics for the given instance."""
- instance = self.get(context, instance_id)
- host = instance["host"]
- return rpc.call(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "get_diagnostics",
- "args": {"instance_id": instance_id}})
+ self._cast_compute_message('get_diagnostics', context, instance_id)
def get_actions(self, context, instance_id):
"""Retrieve actions for the given instance."""
@@ -383,39 +365,23 @@ class API(base.Base):
def suspend(self, context, instance_id):
"""suspend the instance with instance_id"""
- instance = self.get(context, instance_id)
- host = instance['host']
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "suspend_instance",
- "args": {"instance_id": instance_id}})
+ self._cast_compute_message('suspend_instance', context, instance_id)
def resume(self, context, instance_id):
"""resume the instance with instance_id"""
- instance = self.get(context, instance_id)
- host = instance['host']
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "resume_instance",
- "args": {"instance_id": instance_id}})
+ self._cast_compute_message('resume_instance', context, instance_id)
def rescue(self, context, instance_id):
"""Rescue the given instance."""
- instance = self.get(context, instance_id)
- host = instance['host']
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "rescue_instance",
- "args": {"instance_id": instance_id}})
+ self._cast_compute_message('rescue_instance', context, instance_id)
def unrescue(self, context, instance_id):
"""Unrescue the given instance."""
- instance = self.get(context, instance_id)
- host = instance['host']
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "unrescue_instance",
- "args": {"instance_id": instance['id']}})
+ self._cast_compute_message('unrescue_instance', context, instance_id)
+
+ def set_admin_password(self, context, instance_id):
+ """Set the root/admin password for the given instance."""
+ self._cast_compute_message('set_admin_password', context, instance_id)
def get_ajax_console(self, context, instance_id):
"""Get a url to an AJAX Console"""
@@ -437,35 +403,16 @@ class API(base.Base):
output['token'])}
def lock(self, context, instance_id):
- """
- lock the instance with instance_id
-
- """
- instance = self.get_instance(context, instance_id)
- host = instance['host']
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "lock_instance",
- "args": {"instance_id": instance['id']}})
+ """lock the instance with instance_id"""
+ self._cast_compute_message('lock_instance', context, instance_id)
def unlock(self, context, instance_id):
- """
- unlock the instance with instance_id
-
- """
- instance = self.get_instance(context, instance_id)
- host = instance['host']
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "unlock_instance",
- "args": {"instance_id": instance['id']}})
+ """unlock the instance with instance_id"""
+ self._cast_compute_message('unlock_instance', context, instance_id)
def get_lock(self, context, instance_id):
- """
- return the boolean state of (instance with instance_id)'s lock
-
- """
- instance = self.get_instance(context, instance_id)
+ """return the boolean state of (instance with instance_id)'s lock"""
+ instance = self.get(context, instance_id)
return instance['locked']
def attach_volume(self, context, instance_id, volume_id, device):
diff --git a/nova/compute/disk.py b/nova/compute/disk.py
deleted file mode 100644
index 741499294..000000000
--- a/nova/compute/disk.py
+++ /dev/null
@@ -1,205 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-"""
-Utility methods to resize, repartition, and modify disk images.
-
-Includes injection of SSH PGP keys into authorized_keys file.
-
-"""
-
-import os
-import tempfile
-
-from nova import exception
-from nova import flags
-from nova import log as logging
-
-
-LOG = logging.getLogger('nova.compute.disk')
-FLAGS = flags.FLAGS
-flags.DEFINE_integer('minimum_root_size', 1024 * 1024 * 1024 * 10,
- 'minimum size in bytes of root partition')
-flags.DEFINE_integer('block_size', 1024 * 1024 * 256,
- 'block_size to use for dd')
-
-
-def partition(infile, outfile, local_bytes=0, resize=True,
- local_type='ext2', execute=None):
- """
- Turns a partition (infile) into a bootable drive image (outfile).
-
- The first 63 sectors (0-62) of the resulting image is a master boot record.
- Infile becomes the first primary partition.
- If local bytes is specified, a second primary partition is created and
- formatted as ext2.
-
- ::
-
- In the diagram below, dashes represent drive sectors.
- +-----+------. . .-------+------. . .------+
- | 0 a| b c|d e|
- +-----+------. . .-------+------. . .------+
- | mbr | primary partiton | local partition |
- +-----+------. . .-------+------. . .------+
-
- """
- sector_size = 512
- file_size = os.path.getsize(infile)
- if resize and file_size < FLAGS.minimum_root_size:
- last_sector = FLAGS.minimum_root_size / sector_size - 1
- execute('dd if=/dev/zero of=%s count=1 seek=%d bs=%d'
- % (infile, last_sector, sector_size))
- execute('e2fsck -fp %s' % infile, check_exit_code=False)
- execute('resize2fs %s' % infile)
- file_size = FLAGS.minimum_root_size
- elif file_size % sector_size != 0:
- LOG.warn(_("Input partition size not evenly divisible by"
- " sector size: %d / %d"), file_size, sector_size)
- primary_sectors = file_size / sector_size
- if local_bytes % sector_size != 0:
- LOG.warn(_("Bytes for local storage not evenly divisible"
- " by sector size: %d / %d"), local_bytes, sector_size)
- local_sectors = local_bytes / sector_size
-
- mbr_last = 62 # a
- primary_first = mbr_last + 1 # b
- primary_last = primary_first + primary_sectors - 1 # c
- local_first = primary_last + 1 # d
- local_last = local_first + local_sectors - 1 # e
- last_sector = local_last # e
-
- # create an empty file
- execute('dd if=/dev/zero of=%s count=1 seek=%d bs=%d'
- % (outfile, mbr_last, sector_size))
-
- # make mbr partition
- execute('parted --script %s mklabel msdos' % outfile)
-
- # append primary file
- execute('dd if=%s of=%s bs=%s conv=notrunc,fsync oflag=append'
- % (infile, outfile, FLAGS.block_size))
-
- # make primary partition
- execute('parted --script %s mkpart primary %ds %ds'
- % (outfile, primary_first, primary_last))
-
- if local_bytes > 0:
- # make the file bigger
- execute('dd if=/dev/zero of=%s count=1 seek=%d bs=%d'
- % (outfile, last_sector, sector_size))
- # make and format local partition
- execute('parted --script %s mkpartfs primary %s %ds %ds'
- % (outfile, local_type, local_first, local_last))
-
-
-def extend(image, size, execute):
- file_size = os.path.getsize(image)
- if file_size >= size:
- return
- return execute('truncate -s size %s' % (image,))
-
-
-def inject_data(image, key=None, net=None, partition=None, execute=None):
- """Injects a ssh key and optionally net data into a disk image.
-
- it will mount the image as a fully partitioned disk and attempt to inject
- into the specified partition number.
-
- If partition is not specified it mounts the image as a single partition.
-
- """
- out, err = execute('sudo losetup --find --show %s' % image)
- if err:
- raise exception.Error(_('Could not attach image to loopback: %s')
- % err)
- device = out.strip()
- try:
- if not partition is None:
- # create partition
- out, err = execute('sudo kpartx -a %s' % device)
- if err:
- raise exception.Error(_('Failed to load partition: %s') % err)
- mapped_device = '/dev/mapper/%sp%s' % (device.split('/')[-1],
- partition)
- else:
- mapped_device = device
-
- # We can only loopback mount raw images. If the device isn't there,
- # it's normally because it's a .vmdk or a .vdi etc
- if not os.path.exists(mapped_device):
- raise exception.Error('Mapped device was not found (we can'
- ' only inject raw disk images): %s' %
- mapped_device)
-
- # Configure ext2fs so that it doesn't auto-check every N boots
- out, err = execute('sudo tune2fs -c 0 -i 0 %s' % mapped_device)
-
- tmpdir = tempfile.mkdtemp()
- try:
- # mount loopback to dir
- out, err = execute(
- 'sudo mount %s %s' % (mapped_device, tmpdir))
- if err:
- raise exception.Error(_('Failed to mount filesystem: %s')
- % err)
-
- try:
- if key:
- # inject key file
- _inject_key_into_fs(key, tmpdir, execute=execute)
- if net:
- _inject_net_into_fs(net, tmpdir, execute=execute)
- finally:
- # unmount device
- execute('sudo umount %s' % mapped_device)
- finally:
- # remove temporary directory
- execute('rmdir %s' % tmpdir)
- if not partition is None:
- # remove partitions
- execute('sudo kpartx -d %s' % device)
- finally:
- # remove loopback
- execute('sudo losetup --detach %s' % device)
-
-
-def _inject_key_into_fs(key, fs, execute=None):
- """Add the given public ssh key to root's authorized_keys.
-
- key is an ssh key string.
- fs is the path to the base of the filesystem into which to inject the key.
- """
- sshdir = os.path.join(fs, 'root', '.ssh')
- execute('sudo mkdir -p %s' % sshdir) # existing dir doesn't matter
- execute('sudo chown root %s' % sshdir)
- execute('sudo chmod 700 %s' % sshdir)
- keyfile = os.path.join(sshdir, 'authorized_keys')
- execute('sudo tee -a %s' % keyfile, '\n' + key.strip() + '\n')
-
-
-def _inject_net_into_fs(net, fs, execute=None):
- """Inject /etc/network/interfaces into the filesystem rooted at fs.
-
- net is the contents of /etc/network/interfaces.
- """
- netdir = os.path.join(os.path.join(fs, 'etc'), 'network')
- execute('sudo mkdir -p %s' % netdir) # existing dir doesn't matter
- execute('sudo chown root:root %s' % netdir)
- execute('sudo chmod 755 %s' % netdir)
- netfile = os.path.join(netdir, 'interfaces')
- execute('sudo tee %s' % netfile, net)
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 6b2fc4adb..613ee45f6 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -35,6 +35,8 @@ terminating it.
"""
import datetime
+import random
+import string
import logging
import socket
import functools
@@ -54,6 +56,8 @@ flags.DEFINE_string('compute_driver', 'nova.virt.connection.get_connection',
'Driver to use for controlling virtualization')
flags.DEFINE_string('stub_network', False,
'Stub network related code')
+flags.DEFINE_integer('password_length', 12,
+ 'Length of generated admin passwords')
flags.DEFINE_string('console_host', socket.gethostname(),
'Console proxy host to use to connect to instances on'
'this host.')
@@ -311,6 +315,35 @@ class ComputeManager(manager.Manager):
@exception.wrap_exception
@checks_instance_lock
+ def set_admin_password(self, context, instance_id, new_pass=None):
+ """Set the root/admin password for an instance on this server."""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+ if instance_ref['state'] != power_state.RUNNING:
+ logging.warn('trying to reset the password on a non-running '
+ 'instance: %s (state: %s expected: %s)',
+ instance_ref['id'],
+ instance_ref['state'],
+ power_state.RUNNING)
+
+ logging.debug('instance %s: setting admin password',
+ instance_ref['name'])
+ if new_pass is None:
+ # Generate a random password
+ new_pass = self._generate_password(FLAGS.password_length)
+
+ self.driver.set_admin_password(instance_ref, new_pass)
+ self._update_state(context, instance_id)
+
+ def _generate_password(self, length=20):
+ """Generate a random sequence of letters and digits
+ to be used as a password.
+ """
+ chrs = string.letters + string.digits
+ return "".join([random.choice(chrs) for i in xrange(length)])
+
+ @exception.wrap_exception
+ @checks_instance_lock
def rescue_instance(self, context, instance_id):
"""Rescue an instance on this server."""
context = context.elevated()
diff --git a/nova/db/api.py b/nova/db/api.py
index 1f81ef145..f9d561587 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -42,6 +42,10 @@ flags.DEFINE_string('db_backend', 'sqlalchemy',
'The backend to use for db')
flags.DEFINE_boolean('enable_new_services', True,
'Services to be added to the available pool on create')
+flags.DEFINE_string('instance_name_template', 'instance-%08x',
+ 'Template string to be used to generate instance names')
+flags.DEFINE_string('volume_name_template', 'volume-%08x',
+ 'Template string to be used to generate instance names')
IMPL = utils.LazyPluggable(FLAGS['db_backend'],
@@ -295,6 +299,10 @@ def fixed_ip_get_instance(context, address):
return IMPL.fixed_ip_get_instance(context, address)
+def fixed_ip_get_instance_v6(context, address):
+ return IMPL.fixed_ip_get_instance_v6(context, address)
+
+
def fixed_ip_get_network(context, address):
"""Get a network for a fixed ip by address."""
return IMPL.fixed_ip_get_network(context, address)
@@ -353,6 +361,10 @@ def instance_get_fixed_address(context, instance_id):
return IMPL.instance_get_fixed_address(context, instance_id)
+def instance_get_fixed_address_v6(context, instance_id):
+ return IMPL.instance_get_fixed_address_v6(context, instance_id)
+
+
def instance_get_floating_address(context, instance_id):
"""Get the first floating ip address of an instance."""
return IMPL.instance_get_floating_address(context, instance_id)
@@ -548,6 +560,10 @@ def project_get_network(context, project_id, associate=True):
return IMPL.project_get_network(context, project_id)
+def project_get_network_v6(context, project_id):
+ return IMPL.project_get_network_v6(context, project_id)
+
+
###################
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 2e4f8fc39..6c9989b7b 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -606,6 +606,17 @@ def fixed_ip_get_instance(context, address):
return fixed_ip_ref.instance
+@require_context
+def fixed_ip_get_instance_v6(context, address):
+ session = get_session()
+ mac = utils.to_mac(address)
+
+ result = session.query(models.Instance
+ ).filter_by(mac_address=mac
+ ).first()
+ return result
+
+
@require_admin_context
def fixed_ip_get_network(context, address):
fixed_ip_ref = fixed_ip_get_by_address(context, address)
@@ -765,12 +776,14 @@ def instance_get_by_id(context, instance_id):
if is_admin_context(context):
result = session.query(models.Instance).\
options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ip.floating_ips')).\
filter_by(id=instance_id).\
filter_by(deleted=can_read_deleted(context)).\
first()
elif is_user_context(context):
result = session.query(models.Instance).\
options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ip.floating_ips')).\
filter_by(project_id=context.project_id).\
filter_by(id=instance_id).\
filter_by(deleted=False).\
@@ -792,6 +805,17 @@ def instance_get_fixed_address(context, instance_id):
@require_context
+def instance_get_fixed_address_v6(context, instance_id):
+ session = get_session()
+ with session.begin():
+ instance_ref = instance_get(context, instance_id, session=session)
+ network_ref = network_get_by_instance(context, instance_id)
+ prefix = network_ref.cidr_v6
+ mac = instance_ref.mac_address
+ return utils.to_global_ipv6(prefix, mac)
+
+
+@require_context
def instance_get_floating_address(context, instance_id):
session = get_session()
with session.begin():
@@ -1128,6 +1152,11 @@ def project_get_network(context, project_id, associate=True):
return result
+@require_context
+def project_get_network_v6(context, project_id):
+ return project_get_network(context, project_id)
+
+
###################
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 1dc46fe78..af9584f81 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -169,7 +169,7 @@ class Instance(BASE, NovaBase):
@property
def name(self):
- return "instance-%08x" % self.id
+ return FLAGS.instance_name_template % self.id
admin_pass = Column(String(255))
user_id = Column(String(255))
@@ -256,7 +256,7 @@ class Volume(BASE, NovaBase):
@property
def name(self):
- return "volume-%08x" % self.id
+ return FLAGS.volume_name_template % self.id
user_id = Column(String(255))
project_id = Column(String(255))
@@ -411,6 +411,10 @@ class Network(BASE, NovaBase):
injected = Column(Boolean, default=False)
cidr = Column(String(255), unique=True)
+ cidr_v6 = Column(String(255), unique=True)
+
+ ra_server = Column(String(255))
+
netmask = Column(String(255))
bridge = Column(String(255))
gateway = Column(String(255))
diff --git a/nova/exception.py b/nova/exception.py
index 7680e534a..ecd814e5d 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -76,6 +76,10 @@ class InvalidInputException(Error):
pass
+class TimeoutException(Error):
+ pass
+
+
def wrap_exception(f):
def _wrap(*args, **kw):
try:
diff --git a/nova/flags.py b/nova/flags.py
index fdcba6c72..ef66c3f3a 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -259,6 +259,7 @@ DEFINE_string('os_prefix', 'http', 'prefix for openstack')
DEFINE_string('cc_host', '$my_ip', 'ip of api server')
DEFINE_string('cc_dmz', '$my_ip', 'internal ip of api server')
DEFINE_integer('cc_port', 8773, 'cloud controller port')
+DEFINE_integer('osapi_port', 8774, 'OpenStack API port')
DEFINE_string('ec2_suffix', '/services/Cloud', 'suffix for ec2')
DEFINE_string('os_suffix', '/v1.0/', 'suffix for openstack')
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index 3743fc7e8..891e9bc1d 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -50,6 +50,7 @@ flags.DEFINE_string('routing_source_ip', '$my_ip',
'Public IP of network host')
flags.DEFINE_bool('use_nova_chains', False,
'use the nova_ routing chains instead of default')
+
flags.DEFINE_string('dns_server', None,
'if set, uses specific dns server for dnsmasq')
flags.DEFINE_string('dmz_cidr', '10.128.0.0/24',
@@ -196,6 +197,10 @@ def ensure_bridge(bridge, interface, net_attrs=None):
net_attrs['gateway'],
net_attrs['broadcast'],
net_attrs['netmask']))
+ if(FLAGS.use_ipv6):
+ _execute("sudo ifconfig %s add %s up" % \
+ (bridge,
+ net_attrs['cidr_v6']))
else:
_execute("sudo ifconfig %s up" % bridge)
if FLAGS.use_nova_chains:
@@ -262,6 +267,50 @@ def update_dhcp(context, network_id):
_execute(command, addl_env=env)
+def update_ra(context, network_id):
+ network_ref = db.network_get(context, network_id)
+
+ conffile = _ra_file(network_ref['bridge'], 'conf')
+ with open(conffile, 'w') as f:
+ conf_str = """
+interface %s
+{
+ AdvSendAdvert on;
+ MinRtrAdvInterval 3;
+ MaxRtrAdvInterval 10;
+ prefix %s
+ {
+ AdvOnLink on;
+ AdvAutonomous on;
+ };
+};
+""" % (network_ref['bridge'], network_ref['cidr_v6'])
+ f.write(conf_str)
+
+ # Make sure radvd can actually read it (it setuid()s to "nobody")
+ os.chmod(conffile, 0644)
+
+ pid = _ra_pid_for(network_ref['bridge'])
+
+ # if radvd is already running, then tell it to reload
+ if pid:
+ out, _err = _execute('cat /proc/%d/cmdline'
+ % pid, check_exit_code=False)
+ if conffile in out:
+ try:
+ _execute('sudo kill -HUP %d' % pid)
+ return
+ except Exception as exc: # pylint: disable-msg=W0703
+ LOG.debug(_("Hupping radvd threw %s"), exc)
+ else:
+ LOG.debug(_("Pid %d is stale, relaunching radvd"), pid)
+ command = _ra_cmd(network_ref)
+ _execute(command)
+ db.network_update(context, network_id,
+ {"ra_server":
+ utils.get_my_linklocal(network_ref['bridge'])})
+
+
def _host_dhcp(fixed_ip_ref):
"""Return a host string for an address"""
instance_ref = fixed_ip_ref['instance']
@@ -323,6 +372,15 @@ def _dnsmasq_cmd(net):
return ''.join(cmd)
+def _ra_cmd(net):
+ """Builds radvd command"""
+ cmd = ['sudo -E radvd',
+# ' -u nobody',
+ ' -C %s' % _ra_file(net['bridge'], 'conf'),
+ ' -p %s' % _ra_file(net['bridge'], 'pid')]
+ return ''.join(cmd)
+
+
def _stop_dnsmasq(network):
"""Stops the dnsmasq instance for a given network"""
pid = _dnsmasq_pid_for(network)
@@ -344,6 +402,16 @@ def _dhcp_file(bridge, kind):
kind))
+def _ra_file(bridge, kind):
+ """Return path to a pid or conf file for a bridge"""
+
+ if not os.path.exists(FLAGS.networks_path):
+ os.makedirs(FLAGS.networks_path)
+ return os.path.abspath("%s/nova-ra-%s.%s" % (FLAGS.networks_path,
+ bridge,
+ kind))
+
+
def _dnsmasq_pid_for(bridge):
"""Returns the pid for prior dnsmasq instance for a bridge
@@ -357,3 +425,18 @@ def _dnsmasq_pid_for(bridge):
if os.path.exists(pid_file):
with open(pid_file, 'r') as f:
return int(f.read())
+
+
+def _ra_pid_for(bridge):
+ """Returns the pid for prior radvd instance for a bridge
+
+ Returns None if no pid file exists
+
+ If machine has rebooted pid might be incorrect (caller should check)
+ """
+
+ pid_file = _ra_file(bridge, 'pid')
+
+ if os.path.exists(pid_file):
+ with open(pid_file, 'r') as f:
+ return int(f.read())
diff --git a/nova/network/manager.py b/nova/network/manager.py
index c75ecc671..4d553f074 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -82,6 +82,7 @@ flags.DEFINE_integer('network_size', 256,
flags.DEFINE_string('floating_range', '4.4.4.0/24',
'Floating IP address block')
flags.DEFINE_string('fixed_range', '10.0.0.0/8', 'Fixed IP address block')
+flags.DEFINE_string('fixed_range_v6', 'fd00::/48', 'Fixed IPv6 address block')
flags.DEFINE_integer('cnt_vpn_clients', 5,
'Number of addresses reserved for vpn clients')
flags.DEFINE_string('network_driver', 'nova.network.linux_net',
@@ -90,6 +91,9 @@ flags.DEFINE_bool('update_dhcp_on_disassociate', False,
'Whether to update dhcp when fixed_ip is disassociated')
flags.DEFINE_integer('fixed_ip_disassociate_timeout', 600,
'Seconds after which a deallocated ip is disassociated')
+
+flags.DEFINE_bool('use_ipv6', True,
+ 'use the ipv6')
flags.DEFINE_string('network_host', socket.gethostname(),
'Network host to use for ip allocation in flat modes')
flags.DEFINE_bool('fake_call', False,
@@ -235,7 +239,7 @@ class NetworkManager(manager.Manager):
"""Get the network host for the current context."""
raise NotImplementedError()
- def create_networks(self, context, num_networks, network_size,
+ def create_networks(self, context, num_networks, network_size, cidr_v6,
*args, **kwargs):
"""Create networks based on parameters."""
raise NotImplementedError()
@@ -321,9 +325,11 @@ class FlatManager(NetworkManager):
pass
def create_networks(self, context, cidr, num_networks, network_size,
- *args, **kwargs):
+ cidr_v6, *args, **kwargs):
"""Create networks based on parameters."""
fixed_net = IPy.IP(cidr)
+ fixed_net_v6 = IPy.IP(cidr_v6)
+ significant_bits_v6 = 64
for index in range(num_networks):
start = index * network_size
significant_bits = 32 - int(math.log(network_size, 2))
@@ -336,7 +342,13 @@ class FlatManager(NetworkManager):
net['gateway'] = str(project_net[1])
net['broadcast'] = str(project_net.broadcast())
net['dhcp_start'] = str(project_net[2])
+
+ if(FLAGS.use_ipv6):
+ cidr_v6 = "%s/%s" % (fixed_net_v6[0], significant_bits_v6)
+ net['cidr_v6'] = cidr_v6
+
network_ref = self.db.network_create_safe(context, net)
+
if network_ref:
self._create_fixed_ips(context, network_ref['id'])
@@ -482,12 +494,16 @@ class VlanManager(NetworkManager):
network_ref['bridge'])
def create_networks(self, context, cidr, num_networks, network_size,
- vlan_start, vpn_start):
+ vlan_start, vpn_start, cidr_v6):
"""Create networks based on parameters."""
fixed_net = IPy.IP(cidr)
+ fixed_net_v6 = IPy.IP(cidr_v6)
+ network_size_v6 = 1 << 64
+ significant_bits_v6 = 64
for index in range(num_networks):
vlan = vlan_start + index
start = index * network_size
+ start_v6 = index * network_size_v6
significant_bits = 32 - int(math.log(network_size, 2))
cidr = "%s/%s" % (fixed_net[start], significant_bits)
project_net = IPy.IP(cidr)
@@ -500,6 +516,13 @@ class VlanManager(NetworkManager):
net['dhcp_start'] = str(project_net[3])
net['vlan'] = vlan
net['bridge'] = 'br%s' % vlan
+ if(FLAGS.use_ipv6):
+ cidr_v6 = "%s/%s" % (
+ fixed_net_v6[start_v6],
+ significant_bits_v6
+ )
+ net['cidr_v6'] = cidr_v6
+
# NOTE(vish): This makes ports unique accross the cloud, a more
# robust solution would be to make them unique per ip
net['vpn_public_port'] = vpn_start + index
@@ -538,6 +561,7 @@ class VlanManager(NetworkManager):
self.driver.ensure_vlan_bridge(network_ref['vlan'],
network_ref['bridge'],
network_ref)
+
# NOTE(vish): only ensure this forward if the address hasn't been set
# manually.
if address == FLAGS.vpn_ip:
@@ -546,6 +570,8 @@ class VlanManager(NetworkManager):
network_ref['vpn_private_address'])
if not FLAGS.fake_network:
self.driver.update_dhcp(context, network_id)
+ if(FLAGS.use_ipv6):
+ self.driver.update_ra(context, network_id)
@property
def _bottom_reserved_ips(self):
diff --git a/nova/test.py b/nova/test.py
index db5826c04..5922e4b1c 100644
--- a/nova/test.py
+++ b/nova/test.py
@@ -156,7 +156,8 @@ class TrialTestCase(trial_unittest.TestCase):
FLAGS.fixed_range,
5, 16,
FLAGS.vlan_start,
- FLAGS.vpn_start)
+ FLAGS.vpn_start,
+ FLAGS.fixed_range_v6)
# emulate some of the mox stuff, we can't use the metaclass
# because it screws with our generators
diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py
index 33d4cb294..d22d7beb1 100644
--- a/nova/tests/test_api.py
+++ b/nova/tests/test_api.py
@@ -265,6 +265,72 @@ class ApiEc2TestCase(test.TrialTestCase):
return
+ def test_authorize_revoke_security_group_cidr_v6(self):
+ """
+ Test that we can add and remove CIDR based rules
+ to a security group for IPv6
+ """
+ self.expect_http()
+ self.mox.ReplayAll()
+ user = self.manager.create_user('fake', 'fake', 'fake')
+ project = self.manager.create_project('fake', 'fake', 'fake')
+
+ # At the moment, you need both of these to actually be netadmin
+ self.manager.add_role('fake', 'netadmin')
+ project.add_role('fake', 'netadmin')
+
+ security_group_name = "".join(random.choice("sdiuisudfsdcnpaqwertasd")
+ for x in range(random.randint(4, 8)))
+
+ group = self.ec2.create_security_group(security_group_name,
+ 'test group')
+
+ self.expect_http()
+ self.mox.ReplayAll()
+ group.connection = self.ec2
+
+ group.authorize('tcp', 80, 81, '::/0')
+
+ self.expect_http()
+ self.mox.ReplayAll()
+
+ rv = self.ec2.get_all_security_groups()
+ # I don't bother checkng that we actually find it here,
+ # because the create/delete unit test further up should
+ # be good enough for that.
+ for group in rv:
+ if group.name == security_group_name:
+ self.assertEquals(len(group.rules), 1)
+ self.assertEquals(int(group.rules[0].from_port), 80)
+ self.assertEquals(int(group.rules[0].to_port), 81)
+ self.assertEquals(len(group.rules[0].grants), 1)
+ self.assertEquals(str(group.rules[0].grants[0]), '::/0')
+
+ self.expect_http()
+ self.mox.ReplayAll()
+ group.connection = self.ec2
+
+ group.revoke('tcp', 80, 81, '::/0')
+
+ self.expect_http()
+ self.mox.ReplayAll()
+
+ self.ec2.delete_security_group(security_group_name)
+
+ self.expect_http()
+ self.mox.ReplayAll()
+ group.connection = self.ec2
+
+ rv = self.ec2.get_all_security_groups()
+
+ self.assertEqual(len(rv), 1)
+ self.assertEqual(rv[0].name, 'default')
+
+ self.manager.delete_project(project)
+ self.manager.delete_user(user)
+
+ return
+
def test_authorize_revoke_security_group_foreign_group(self):
"""
Test that we can grant and revoke another security group access
diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py
index fdacb04f6..2e350cd5a 100644
--- a/nova/tests/test_cloud.py
+++ b/nova/tests/test_cloud.py
@@ -126,10 +126,13 @@ class CloudTestCase(test.TestCase):
vol2 = db.volume_create(self.context, {})
result = self.cloud.describe_volumes(self.context)
self.assertEqual(len(result['volumeSet']), 2)
+ volume_id = cloud.id_to_ec2_id(vol2['id'], 'vol-%08x')
result = self.cloud.describe_volumes(self.context,
- volume_id=[vol2['id']])
+ volume_id=[volume_id])
self.assertEqual(len(result['volumeSet']), 1)
- self.assertEqual(result['volumeSet'][0]['volumeId'], vol2['id'])
+ self.assertEqual(
+ cloud.ec2_id_to_id(result['volumeSet'][0]['volumeId']),
+ vol2['id'])
db.volume_destroy(self.context, vol1['id'])
db.volume_destroy(self.context, vol2['id'])
@@ -385,7 +388,8 @@ class CloudTestCase(test.TestCase):
def test_update_of_volume_display_fields(self):
vol = db.volume_create(self.context, {})
- self.cloud.update_volume(self.context, vol['id'],
+ self.cloud.update_volume(self.context,
+ cloud.id_to_ec2_id(vol['id'], 'vol-%08x'),
display_name='c00l v0lum3')
vol = db.volume_get(self.context, vol['id'])
self.assertEqual('c00l v0lum3', vol['display_name'])
@@ -393,8 +397,9 @@ class CloudTestCase(test.TestCase):
def test_update_of_volume_wont_update_private_fields(self):
vol = db.volume_create(self.context, {})
- self.cloud.update_volume(self.context, vol['id'],
- mountpoint='/not/here')
+ self.cloud.update_volume(self.context,
+ cloud.id_to_ec2_id(vol['id'], 'vol-%08x'),
+ mountpoint='/not/here')
vol = db.volume_get(self.context, vol['id'])
self.assertEqual(None, vol['mountpoint'])
db.volume_destroy(self.context, vol['id'])
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 52660ee74..a7d47961c 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -151,6 +151,13 @@ class ComputeTestCase(test.TestCase):
self.compute.reboot_instance(self.context, instance_id)
self.compute.terminate_instance(self.context, instance_id)
+ def test_set_admin_password(self):
+ """Ensure instance can have its admin password set"""
+ instance_id = self._create_instance()
+ self.compute.run_instance(self.context, instance_id)
+ self.compute.set_admin_password(self.context, instance_id)
+ self.compute.terminate_instance(self.context, instance_id)
+
def test_snapshot(self):
"""Ensure instance can be snapshotted"""
instance_id = self._create_instance()
diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py
index 349e20f84..00f9323f3 100644
--- a/nova/tests/test_network.py
+++ b/nova/tests/test_network.py
@@ -96,6 +96,28 @@ class NetworkTestCase(test.TestCase):
self.context.project_id = self.projects[project_num].id
self.network.deallocate_fixed_ip(self.context, address)
+ def test_private_ipv6(self):
+ """Make sure ipv6 is OK"""
+ if FLAGS.use_ipv6:
+ instance_ref = self._create_instance(0)
+ address = self._create_address(0, instance_ref['id'])
+ network_ref = db.project_get_network(
+ context.get_admin_context(),
+ self.context.project_id)
+ address_v6 = db.instance_get_fixed_address_v6(
+ context.get_admin_context(),
+ instance_ref['id'])
+ self.assertEqual(instance_ref['mac_address'],
+ utils.to_mac(address_v6))
+ instance_ref2 = db.fixed_ip_get_instance_v6(
+ context.get_admin_context(),
+ address_v6)
+ self.assertEqual(instance_ref['id'], instance_ref2['id'])
+ self.assertEqual(address_v6,
+ utils.to_global_ipv6(
+ network_ref['cidr_v6'],
+ instance_ref['mac_address']))
+
def test_public_network_association(self):
"""Makes sure that we can allocaate a public ip"""
# TODO(vish): better way of adding floating ips
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index ce0bc002a..9f5b266f3 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -31,6 +31,7 @@ from nova.compute import power_state
from nova.virt import xenapi_conn
from nova.virt.xenapi import fake as xenapi_fake
from nova.virt.xenapi import volume_utils
+from nova.virt.xenapi.vmops import SimpleDH
from nova.tests.db import fakes as db_fakes
from nova.tests.xenapi import stubs
from nova.tests.glance import stubs as glance_stubs
@@ -296,3 +297,29 @@ class XenAPIVMTestCase(test.TestCase):
instance = db.instance_create(values)
self.conn.spawn(instance)
return instance
+
+
+class XenAPIDiffieHellmanTestCase(test.TestCase):
+ """
+ Unit tests for Diffie-Hellman code
+ """
+ def setUp(self):
+ super(XenAPIDiffieHellmanTestCase, self).setUp()
+ self.alice = SimpleDH()
+ self.bob = SimpleDH()
+
+ def test_shared(self):
+ alice_pub = self.alice.get_public()
+ bob_pub = self.bob.get_public()
+ alice_shared = self.alice.compute_shared(bob_pub)
+ bob_shared = self.bob.compute_shared(alice_pub)
+ self.assertEquals(alice_shared, bob_shared)
+
+ def test_encryption(self):
+ msg = "This is a top-secret message"
+ enc = self.alice.encrypt(msg)
+ dec = self.bob.decrypt(enc)
+ self.assertEquals(dec, msg)
+
+ def tearDown(self):
+ super(XenAPIDiffieHellmanTestCase, self).tearDown()
diff --git a/nova/utils.py b/nova/utils.py
index 45adb7b38..27589c30c 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -30,6 +30,8 @@ import struct
import sys
import time
from xml.sax import saxutils
+import re
+import netaddr
from eventlet import event
from eventlet import greenthread
@@ -200,6 +202,40 @@ def last_octet(address):
return int(address.split(".")[-1])
+def get_my_linklocal(interface):
+ try:
+ if_str = execute("ip -f inet6 -o addr show %s" % interface)
+ condition = "\s+inet6\s+([0-9a-f:]+/\d+)\s+scope\s+link"
+ links = [re.search(condition, x) for x in if_str[0].split('\n')]
+ address = [w.group(1) for w in links if w is not None]
+ if address[0] is not None:
+ return address[0]
+ else:
+ return 'fe00::'
+ except IndexError as ex:
+ LOG.warn(_("Couldn't get Link Local IP of %s :%s"), interface, ex)
+ except ProcessExecutionError as ex:
+ LOG.warn(_("Couldn't get Link Local IP of %s :%s"), interface, ex)
+ except:
+ return 'fe00::'
+
+
+def to_global_ipv6(prefix, mac):
+ mac64 = netaddr.EUI(mac).eui64().words
+ int_addr = int(''.join(['%02x' % i for i in mac64]), 16)
+ mac64_addr = netaddr.IPAddress(int_addr)
+ maskIP = netaddr.IPNetwork(prefix).ip
+ return (mac64_addr ^ netaddr.IPAddress('::0200:0:0:0') | maskIP).format()
+
+
+def to_mac(ipv6_address):
+ address = netaddr.IPAddress(ipv6_address)
+ mask1 = netaddr.IPAddress("::ffff:ffff:ffff:ffff")
+ mask2 = netaddr.IPAddress("::0200:0:0:0")
+ mac64 = netaddr.EUI(int(address & mask1 ^ mask2)).words
+ return ":".join(["%02x" % i for i in mac64[0:3] + mac64[5:8]])
+
+
def utcnow():
"""Overridable version of datetime.datetime.utcnow."""
if utcnow.override_time:
diff --git a/nova/virt/disk.py b/nova/virt/disk.py
new file mode 100644
index 000000000..c5565abfa
--- /dev/null
+++ b/nova/virt/disk.py
@@ -0,0 +1,186 @@
+# 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.
+"""
+Utility methods to resize, repartition, and modify disk images.
+
+Includes injection of SSH PGP keys into authorized_keys file.
+
+"""
+
+import os
+import tempfile
+import time
+
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova import utils
+
+
+LOG = logging.getLogger('nova.compute.disk')
+FLAGS = flags.FLAGS
+flags.DEFINE_integer('minimum_root_size', 1024 * 1024 * 1024 * 10,
+ 'minimum size in bytes of root partition')
+flags.DEFINE_integer('block_size', 1024 * 1024 * 256,
+ 'block_size to use for dd')
+
+
+def extend(image, size):
+ """Increase image to size"""
+ file_size = os.path.getsize(image)
+ if file_size >= size:
+ return
+ utils.execute('truncate -s %s %s' % (size, image))
+ # NOTE(vish): attempts to resize filesystem
+ utils.execute('e2fsck -fp %s' % image, check_exit_code=False)
+ utils.execute('resize2fs %s' % image, check_exit_code=False)
+
+
+def inject_data(image, key=None, net=None, partition=None, nbd=False):
+ """Injects a ssh key and optionally net data into a disk image.
+
+ it will mount the image as a fully partitioned disk and attempt to inject
+ into the specified partition number.
+
+ If partition is not specified it mounts the image as a single partition.
+
+ """
+ device = _link_device(image, nbd)
+ try:
+ if not partition is None:
+ # create partition
+ out, err = utils.execute('sudo kpartx -a %s' % device)
+ if err:
+ raise exception.Error(_('Failed to load partition: %s') % err)
+ mapped_device = '/dev/mapper/%sp%s' % (device.split('/')[-1],
+ partition)
+ else:
+ mapped_device = device
+
+ # We can only loopback mount raw images. If the device isn't there,
+ # it's normally because it's a .vmdk or a .vdi etc
+ if not os.path.exists(mapped_device):
+ raise exception.Error('Mapped device was not found (we can'
+ ' only inject raw disk images): %s' %
+ mapped_device)
+
+ # Configure ext2fs so that it doesn't auto-check every N boots
+ out, err = utils.execute('sudo tune2fs -c 0 -i 0 %s' % mapped_device)
+
+ tmpdir = tempfile.mkdtemp()
+ try:
+ # mount loopback to dir
+ out, err = utils.execute(
+ 'sudo mount %s %s' % (mapped_device, tmpdir))
+ if err:
+ raise exception.Error(_('Failed to mount filesystem: %s')
+ % err)
+
+ try:
+ if key:
+ # inject key file
+ _inject_key_into_fs(key, tmpdir)
+ if net:
+ _inject_net_into_fs(net, tmpdir)
+ finally:
+ # unmount device
+ utils.execute('sudo umount %s' % mapped_device)
+ finally:
+ # remove temporary directory
+ utils.execute('rmdir %s' % tmpdir)
+ if not partition is None:
+ # remove partitions
+ utils.execute('sudo kpartx -d %s' % device)
+ finally:
+ _unlink_device(device, nbd)
+
+
+def _link_device(image, nbd):
+ """Link image to device using loopback or nbd"""
+ if nbd:
+ device = _allocate_device()
+ utils.execute('sudo qemu-nbd -c %s %s' % (device, image))
+ # NOTE(vish): this forks into another process, so give it a chance
+ # to set up before continuuing
+ for i in xrange(10):
+ if os.path.exists("/sys/block/%s/pid" % os.path.basename(device)):
+ return device
+ time.sleep(1)
+ raise exception.Error(_('nbd device %s did not show up') % device)
+ else:
+ out, err = utils.execute('sudo losetup --find --show %s' % image)
+ if err:
+ raise exception.Error(_('Could not attach image to loopback: %s')
+ % err)
+ return out.strip()
+
+
+def _unlink_device(device, nbd):
+ """Unlink image from device using loopback or nbd"""
+ if nbd:
+ utils.execute('sudo qemu-nbd -d %s' % device)
+ _free_device(device)
+ else:
+ utils.execute('sudo losetup --detach %s' % device)
+
+
+_DEVICES = ['/dev/nbd%s' % i for i in xrange(16)]
+
+
+def _allocate_device():
+ # NOTE(vish): This assumes no other processes are allocating nbd devices.
+ # It may race cause a race condition if multiple
+ # workers are running on a given machine.
+ while True:
+ if not _DEVICES:
+ raise exception.Error(_('No free nbd devices'))
+ device = _DEVICES.pop()
+ if not os.path.exists("/sys/block/%s/pid" % os.path.basename(device)):
+ break
+ return device
+
+
+def _free_device(device):
+ _DEVICES.append(device)
+
+
+def _inject_key_into_fs(key, fs):
+ """Add the given public ssh key to root's authorized_keys.
+
+ key is an ssh key string.
+ fs is the path to the base of the filesystem into which to inject the key.
+ """
+ sshdir = os.path.join(fs, 'root', '.ssh')
+ utils.execute('sudo mkdir -p %s' % sshdir) # existing dir doesn't matter
+ utils.execute('sudo chown root %s' % sshdir)
+ utils.execute('sudo chmod 700 %s' % sshdir)
+ keyfile = os.path.join(sshdir, 'authorized_keys')
+ utils.execute('sudo tee -a %s' % keyfile, '\n' + key.strip() + '\n')
+
+
+def _inject_net_into_fs(net, fs):
+ """Inject /etc/network/interfaces into the filesystem rooted at fs.
+
+ net is the contents of /etc/network/interfaces.
+ """
+ netdir = os.path.join(os.path.join(fs, 'etc'), 'network')
+ utils.execute('sudo mkdir -p %s' % netdir) # existing dir doesn't matter
+ utils.execute('sudo chown root:root %s' % netdir)
+ utils.execute('sudo chmod 755 %s' % netdir)
+ netfile = os.path.join(netdir, 'interfaces')
+ utils.execute('sudo tee %s' % netfile, net)
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 9186d885e..a57a8f43b 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -98,7 +98,7 @@ class FakeConnection(object):
the new instance.
The work will be done asynchronously. This function returns a
- Deferred that allows the caller to detect when it is complete.
+ task that allows the caller to detect when it is complete.
Once this successfully completes, the instance should be
running (power_state.RUNNING).
@@ -122,7 +122,7 @@ class FakeConnection(object):
The second parameter is the name of the snapshot.
The work will be done asynchronously. This function returns a
- Deferred that allows the caller to detect when it is complete.
+ task that allows the caller to detect when it is complete.
"""
pass
@@ -134,7 +134,20 @@ class FakeConnection(object):
and so the instance is being specified as instance.name.
The work will be done asynchronously. This function returns a
- Deferred that allows the caller to detect when it is complete.
+ task that allows the caller to detect when it is complete.
+ """
+ pass
+
+ def set_admin_password(self, instance, new_pass):
+ """
+ Set the root password on the specified instance.
+
+ The first parameter is an instance of nova.compute.service.Instance,
+ and so the instance is being specified as instance.name. The second
+ parameter is the value of the new password.
+
+ The work will be done asynchronously. This function returns a
+ task that allows the caller to detect when it is complete.
"""
pass
@@ -182,7 +195,7 @@ class FakeConnection(object):
and so the instance is being specified as instance.name.
The work will be done asynchronously. This function returns a
- Deferred that allows the caller to detect when it is complete.
+ task that allows the caller to detect when it is complete.
"""
del self.instances[instance.name]
diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template
index 2eb7d9488..de06a1eb0 100644
--- a/nova/virt/libvirt.xml.template
+++ b/nova/virt/libvirt.xml.template
@@ -7,13 +7,13 @@
#set $disk_bus = 'uml'
<type>uml</type>
<kernel>/usr/bin/linux</kernel>
- <root>/dev/ubda1</root>
+ <root>/dev/ubda</root>
#else
#if $type == 'xen'
#set $disk_prefix = 'sd'
#set $disk_bus = 'scsi'
<type>linux</type>
- <root>/dev/xvda1</root>
+ <root>/dev/xvda</root>
#else
#set $disk_prefix = 'vd'
#set $disk_bus = 'virtio'
@@ -28,7 +28,7 @@
#if $type == 'xen'
<cmdline>ro</cmdline>
#else
- <cmdline>root=/dev/vda1 console=ttyS0</cmdline>
+ <cmdline>root=/dev/vda console=ttyS0</cmdline>
#end if
#if $getVar('ramdisk', None)
<initrd>${ramdisk}</initrd>
@@ -46,18 +46,28 @@
<devices>
#if $getVar('rescue', False)
<disk type='file'>
+ <driver type='${driver_type}'/>
<source file='${basepath}/rescue-disk'/>
<target dev='${disk_prefix}a' bus='${disk_bus}'/>
</disk>
<disk type='file'>
+ <driver type='${driver_type}'/>
<source file='${basepath}/disk'/>
<target dev='${disk_prefix}b' bus='${disk_bus}'/>
</disk>
#else
<disk type='file'>
+ <driver type='${driver_type}'/>
<source file='${basepath}/disk'/>
<target dev='${disk_prefix}a' bus='${disk_bus}'/>
</disk>
+ #if $getVar('local', False)
+ <disk type='file'>
+ <driver type='${driver_type}'/>
+ <source file='${basepath}/local'/>
+ <target dev='${disk_prefix}b' bus='${disk_bus}'/>
+ </disk>
+ #end if
#end if
<interface type='bridge'>
<source bridge='${bridge_name}'/>
@@ -66,6 +76,7 @@
<filterref filter="nova-instance-${name}">
<parameter name="IP" value="${ip_address}" />
<parameter name="DHCPSERVER" value="${dhcp_server}" />
+ <parameter name="RASERVER" value="${ra_server}" />
#if $getVar('extra_params', False)
${extra_params}
#end if
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py
index 655c55fa1..073a8e5bb 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -58,9 +58,9 @@ from nova import log as logging
from nova import utils
#from nova.api import context
from nova.auth import manager
-from nova.compute import disk
from nova.compute import instance_types
from nova.compute import power_state
+from nova.virt import disk
from nova.virt import images
libvirt = None
@@ -91,6 +91,9 @@ flags.DEFINE_string('libvirt_uri',
flags.DEFINE_bool('allow_project_net_traffic',
True,
'Whether to allow in project network traffic')
+flags.DEFINE_bool('use_cow_images',
+ True,
+ 'Whether to use cow images')
flags.DEFINE_string('ajaxterm_portrange',
'10000-12000',
'Range of ports that ajaxterm should randomly try to bind')
@@ -127,6 +130,16 @@ def _get_net_and_mask(cidr):
return str(net.net()), str(net.netmask())
+def _get_net_and_prefixlen(cidr):
+ net = IPy.IP(cidr)
+ return str(net.net()), str(net.prefixlen())
+
+
+def _get_ip_version(cidr):
+ net = IPy.IP(cidr)
+ return int(net.version())
+
+
class LibvirtConnection(object):
def __init__(self, read_only):
@@ -197,40 +210,29 @@ class LibvirtConnection(object):
pass
# If the instance is already terminated, we're still happy
- done = event.Event()
-
# We'll save this for when we do shutdown,
# instead of destroy - but destroy returns immediately
timer = utils.LoopingCall(f=None)
- def _wait_for_shutdown():
+ while True:
try:
state = self.get_info(instance['name'])['state']
db.instance_set_state(context.get_admin_context(),
instance['id'], state)
if state == power_state.SHUTDOWN:
- timer.stop()
+ break
except Exception:
db.instance_set_state(context.get_admin_context(),
instance['id'],
power_state.SHUTDOWN)
- timer.stop()
+ break
- timer.f = _wait_for_shutdown
- timer_done = timer.start(interval=0.5, now=True)
+ self.firewall_driver.unfilter_instance(instance)
- # NOTE(termie): this is strictly superfluous (we could put the
- # cleanup code in the timer), but this emulates the
- # previous model so I am keeping it around until
- # everything has been vetted a bit
- def _wait_for_timer():
- timer_done.wait()
- if cleanup:
- self._cleanup(instance)
- done.send()
+ if cleanup:
+ self._cleanup(instance)
- greenthread.spawn(_wait_for_timer)
- return done
+ return True
def _cleanup(self, instance):
target = os.path.join(FLAGS.instances_path, instance['name'])
@@ -243,11 +245,24 @@ class LibvirtConnection(object):
def attach_volume(self, instance_name, device_path, mountpoint):
virt_dom = self._conn.lookupByName(instance_name)
mount_device = mountpoint.rpartition("/")[2]
- xml = """<disk type='block'>
- <driver name='qemu' type='raw'/>
- <source dev='%s'/>
- <target dev='%s' bus='virtio'/>
- </disk>""" % (device_path, mount_device)
+ if device_path.startswith('/dev/'):
+ xml = """<disk type='block'>
+ <driver name='qemu' type='raw'/>
+ <source dev='%s'/>
+ <target dev='%s' bus='virtio'/>
+ </disk>""" % (device_path, mount_device)
+ elif ':' in device_path:
+ (protocol, name) = device_path.split(':')
+ xml = """<disk type='network'>
+ <driver name='qemu' type='raw'/>
+ <source protocol='%s' name='%s'/>
+ <target dev='%s' bus='virtio'/>
+ </disk>""" % (protocol,
+ name,
+ mount_device)
+ else:
+ raise exception.Invalid(_("Invalid device path %s") % device_path)
+
virt_dom.attachDevice(xml)
def _get_disk_xml(self, xml, device):
@@ -370,7 +385,6 @@ class LibvirtConnection(object):
instance['id'],
power_state.NOSTATE,
'launching')
-
self.nwfilter.setup_basic_filtering(instance)
self.firewall_driver.prepare_instance_filter(instance)
self._create_image(instance, xml)
@@ -478,19 +492,57 @@ class LibvirtConnection(object):
subprocess.Popen(cmd, shell=True)
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.
+
+ This wrapper will save the image into a common store and create a
+ copy for use by the hypervisor.
+
+ The underlying method should specify a kwarg of target representing
+ where the image will be saved.
+
+ fname is used as the filename of the base image. The filename needs
+ to be unique to a given image.
+
+ If cow is True, it will make a CoW image instead of a copy.
+ """
+ if not os.path.exists(target):
+ base_dir = os.path.join(FLAGS.instances_path, '_base')
+ if not os.path.exists(base_dir):
+ os.mkdir(base_dir)
+ os.chmod(base_dir, 0777)
+ base = os.path.join(base_dir, fname)
+ if not os.path.exists(base):
+ fn(target=base, *args, **kwargs)
+ if cow:
+ utils.execute('qemu-img create -f qcow2 -o '
+ 'cluster_size=2M,backing_file=%s %s'
+ % (base, target))
+ else:
+ utils.execute('cp %s %s' % (base, target))
+
+ def _fetch_image(self, target, image_id, user, project, size=None):
+ """Grab image and optionally attempt to resize it"""
+ images.fetch(image_id, target, user, project)
+ if size:
+ disk.extend(target, size)
+
+ def _create_local(self, target, local_gb):
+ """Create a blank image of specified size"""
+ utils.execute('truncate %s -s %dG' % (target, local_gb))
+ # TODO(vish): should we format disk by default?
+
def _create_image(self, inst, libvirt_xml, prefix='', disk_images=None):
# syntactic nicety
- basepath = lambda fname = '', prefix = prefix: os.path.join(
- FLAGS.instances_path,
- inst['name'],
- prefix + fname)
+ def basepath(fname='', prefix=prefix):
+ return os.path.join(FLAGS.instances_path,
+ inst['name'],
+ prefix + fname)
# ensure directories exist and are writable
utils.execute('mkdir -p %s' % basepath(prefix=''))
utils.execute('chmod 0777 %s' % basepath(prefix=''))
- # TODO(termie): these are blocking calls, it would be great
- # if they weren't.
LOG.info(_('instance %s: Creating image'), inst['name'])
f = open(basepath('libvirt.xml'), 'w')
f.write(libvirt_xml)
@@ -507,23 +559,44 @@ class LibvirtConnection(object):
disk_images = {'image_id': inst['image_id'],
'kernel_id': inst['kernel_id'],
'ramdisk_id': inst['ramdisk_id']}
- if not os.path.exists(basepath('disk')):
- images.fetch(inst.image_id, basepath('disk-raw'), user,
- project)
-
- if inst['kernel_id']:
- if not os.path.exists(basepath('kernel')):
- images.fetch(inst['kernel_id'], basepath('kernel'),
- user, project)
- if inst['ramdisk_id']:
- if not os.path.exists(basepath('ramdisk')):
- images.fetch(inst['ramdisk_id'], basepath('ramdisk'),
- user, project)
-
- def execute(cmd, process_input=None, check_exit_code=True):
- return utils.execute(cmd=cmd,
- process_input=process_input,
- check_exit_code=check_exit_code)
+
+ if disk_images['kernel_id']:
+ self._cache_image(fn=self._fetch_image,
+ target=basepath('kernel'),
+ fname=disk_images['kernel_id'],
+ image_id=disk_images['kernel_id'],
+ user=user,
+ project=project)
+ if disk_images['ramdisk_id']:
+ self._cache_image(fn=self._fetch_image,
+ target=basepath('ramdisk'),
+ fname=disk_images['ramdisk_id'],
+ image_id=disk_images['ramdisk_id'],
+ user=user,
+ project=project)
+
+ root_fname = disk_images['image_id']
+ size = FLAGS.minimum_root_size
+ if inst['instance_type'] == 'm1.tiny' or prefix == 'rescue-':
+ size = None
+ root_fname += "_sm"
+
+ self._cache_image(fn=self._fetch_image,
+ target=basepath('disk'),
+ fname=root_fname,
+ cow=FLAGS.use_cow_images,
+ image_id=disk_images['image_id'],
+ user=user,
+ project=project,
+ size=size)
+ type_data = instance_types.INSTANCE_TYPES[inst['instance_type']]
+
+ if type_data['local_gb']:
+ self._cache_image(fn=self._create_local,
+ target=basepath('local'),
+ fname="local_%s" % type_data['local_gb'],
+ cow=FLAGS.use_cow_images,
+ local_gb=type_data['local_gb'])
# For now, we assume that if we're not using a kernel, we're using a
# partitioned disk image where the target partition is the first
@@ -539,12 +612,16 @@ class LibvirtConnection(object):
if network_ref['injected']:
admin_context = context.get_admin_context()
address = db.instance_get_fixed_address(admin_context, inst['id'])
+ ra_server = network_ref['ra_server']
+ if not ra_server:
+ ra_server = "fd00::"
with open(FLAGS.injected_network_template) as f:
net = f.read() % {'address': address,
'netmask': network_ref['netmask'],
'gateway': network_ref['gateway'],
'broadcast': network_ref['broadcast'],
- 'dns': network_ref['dns']}
+ 'dns': network_ref['dns'],
+ 'ra_server': ra_server}
if key or net:
if key:
LOG.info(_('instance %s: injecting key into image %s'),
@@ -553,34 +630,15 @@ class LibvirtConnection(object):
LOG.info(_('instance %s: injecting net into image %s'),
inst['name'], inst.image_id)
try:
- disk.inject_data(basepath('disk-raw'), key, net,
+ disk.inject_data(basepath('disk'), key, net,
partition=target_partition,
- execute=execute)
+ nbd=FLAGS.use_cow_images)
except Exception as e:
# This could be a windows image, or a vmdk format disk
LOG.warn(_('instance %s: ignoring error injecting data'
' into image %s (%s)'),
inst['name'], inst.image_id, e)
- if inst['kernel_id']:
- if os.path.exists(basepath('disk')):
- utils.execute('rm -f %s' % basepath('disk'))
-
- local_bytes = (instance_types.INSTANCE_TYPES[inst.instance_type]
- ['local_gb']
- * 1024 * 1024 * 1024)
-
- resize = True
- if inst['instance_type'] == 'm1.tiny' or prefix == 'rescue-':
- resize = False
-
- if inst['kernel_id']:
- disk.partition(basepath('disk-raw'), basepath('disk'),
- local_bytes, resize, execute=execute)
- else:
- os.rename(basepath('disk-raw'), basepath('disk'))
- disk.extend(basepath('disk'), local_bytes, execute=execute)
-
if FLAGS.libvirt_type == 'uml':
utils.execute('sudo chown root %s' % basepath('disk'))
@@ -599,15 +657,36 @@ class LibvirtConnection(object):
instance['id'])
# Assume that the gateway also acts as the dhcp server.
dhcp_server = network['gateway']
-
+ ra_server = network['ra_server']
+ if not ra_server:
+ ra_server = 'fd00::'
if FLAGS.allow_project_net_traffic:
- net, mask = _get_net_and_mask(network['cidr'])
- extra_params = ("<parameter name=\"PROJNET\" "
+ if FLAGS.use_ipv6:
+ net, mask = _get_net_and_mask(network['cidr'])
+ net_v6, prefixlen_v6 = _get_net_and_prefixlen(
+ network['cidr_v6'])
+ extra_params = ("<parameter name=\"PROJNET\" "
+ "value=\"%s\" />\n"
+ "<parameter name=\"PROJMASK\" "
+ "value=\"%s\" />\n"
+ "<parameter name=\"PROJNETV6\" "
+ "value=\"%s\" />\n"
+ "<parameter name=\"PROJMASKV6\" "
+ "value=\"%s\" />\n") % \
+ (net, mask, net_v6, prefixlen_v6)
+ else:
+ net, mask = _get_net_and_mask(network['cidr'])
+ extra_params = ("<parameter name=\"PROJNET\" "
"value=\"%s\" />\n"
"<parameter name=\"PROJMASK\" "
- "value=\"%s\" />\n") % (net, mask)
+ "value=\"%s\" />\n") % \
+ (net, mask)
else:
extra_params = "\n"
+ if FLAGS.use_cow_images:
+ driver_type = 'qcow2'
+ else:
+ driver_type = 'raw'
xml_info = {'type': FLAGS.libvirt_type,
'name': instance['name'],
@@ -619,8 +698,11 @@ class LibvirtConnection(object):
'mac_address': instance['mac_address'],
'ip_address': ip_address,
'dhcp_server': dhcp_server,
+ 'ra_server': ra_server,
'extra_params': extra_params,
- 'rescue': rescue}
+ 'rescue': rescue,
+ 'local': instance_type['local_gb'],
+ 'driver_type': driver_type}
if not rescue:
if instance['kernel_id']:
xml_info['kernel'] = xml_info['basepath'] + "/kernel"
@@ -774,6 +856,10 @@ class FirewallDriver(object):
At this point, the instance isn't running yet."""
raise NotImplementedError()
+ def unfilter_instance(self, instance):
+ """Stop filtering instance"""
+ raise NotImplementedError()
+
def apply_instance_filter(self, instance):
"""Apply instance filter.
@@ -876,6 +962,15 @@ class NWFilterFirewall(FirewallDriver):
</rule>
</filter>'''
+ def nova_ra_filter(self):
+ return '''<filter name='nova-allow-ra-server' chain='root'>
+ <uuid>d707fa71-4fb5-4b27-9ab7-ba5ca19c8804</uuid>
+ <rule action='accept' direction='inout'
+ priority='100'>
+ <icmpv6 srcipaddr='$RASERVER'/>
+ </rule>
+ </filter>'''
+
def setup_basic_filtering(self, instance):
"""Set up basic filtering (MAC, IP, and ARP spoofing protection)"""
logging.info('called setup_basic_filtering in nwfilter')
@@ -900,13 +995,17 @@ class NWFilterFirewall(FirewallDriver):
['no-mac-spoofing',
'no-ip-spoofing',
'no-arp-spoofing',
- 'allow-dhcp-server']))
+ 'allow-dhcp-server'
+ ]))
self._define_filter(self.nova_base_ipv4_filter)
self._define_filter(self.nova_base_ipv6_filter)
self._define_filter(self.nova_dhcp_filter)
+ self._define_filter(self.nova_ra_filter)
self._define_filter(self.nova_vpn_filter)
if FLAGS.allow_project_net_traffic:
self._define_filter(self.nova_project_filter)
+ if FLAGS.use_ipv6:
+ self._define_filter(self.nova_project_filter_v6)
self.static_filters_configured = True
@@ -938,13 +1037,13 @@ class NWFilterFirewall(FirewallDriver):
def nova_base_ipv6_filter(self):
retval = "<filter name='nova-base-ipv6' chain='ipv6'>"
- for protocol in ['tcp', 'udp', 'icmp']:
+ for protocol in ['tcp-ipv6', 'udp-ipv6', 'icmpv6']:
for direction, action, priority in [('out', 'accept', 399),
('in', 'drop', 400)]:
retval += """<rule action='%s' direction='%s' priority='%d'>
- <%s-ipv6 />
+ <%s />
</rule>""" % (action, direction,
- priority, protocol)
+ priority, protocol)
retval += '</filter>'
return retval
@@ -957,20 +1056,33 @@ class NWFilterFirewall(FirewallDriver):
retval += '</filter>'
return retval
+ def nova_project_filter_v6(self):
+ retval = "<filter name='nova-project-v6' chain='ipv6'>"
+ for protocol in ['tcp-ipv6', 'udp-ipv6', 'icmpv6']:
+ retval += """<rule action='accept' direction='inout'
+ priority='200'>
+ <%s srcipaddr='$PROJNETV6'
+ srcipmask='$PROJMASKV6' />
+ </rule>""" % (protocol)
+ retval += '</filter>'
+ return retval
+
def _define_filter(self, xml):
if callable(xml):
xml = xml()
-
# execute in a native thread and block current greenthread until done
tpool.execute(self._conn.nwfilterDefineXML, xml)
+ def unfilter_instance(self, instance):
+ # Nothing to do
+ pass
+
def prepare_instance_filter(self, instance):
"""
Creates an NWFilter for the given instance. In the process,
it makes sure the filters for the security groups as well as
the base filter are all in place.
"""
-
if instance['image_id'] == FLAGS.vpn_image_id:
base_filter = 'nova-vpn'
else:
@@ -982,11 +1094,15 @@ class NWFilterFirewall(FirewallDriver):
instance_secgroup_filter_children = ['nova-base-ipv4',
'nova-base-ipv6',
'nova-allow-dhcp-server']
+ if FLAGS.use_ipv6:
+ instance_secgroup_filter_children += ['nova-allow-ra-server']
ctxt = context.get_admin_context()
if FLAGS.allow_project_net_traffic:
instance_filter_children += ['nova-project']
+ if FLAGS.use_ipv6:
+ instance_filter_children += ['nova-project-v6']
for security_group in db.security_group_get_by_instance(ctxt,
instance['id']):
@@ -1014,12 +1130,19 @@ class NWFilterFirewall(FirewallDriver):
security_group = db.security_group_get(context.get_admin_context(),
security_group_id)
rule_xml = ""
+ v6protocol = {'tcp': 'tcp-ipv6', 'udp': 'udp-ipv6', 'icmp': 'icmpv6'}
for rule in security_group.rules:
rule_xml += "<rule action='accept' direction='in' priority='300'>"
if rule.cidr:
- net, mask = _get_net_and_mask(rule.cidr)
- rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
- (rule.protocol, net, mask)
+ version = _get_ip_version(rule.cidr)
+ if(FLAGS.use_ipv6 and version == 6):
+ net, prefixlen = _get_net_and_prefixlen(rule.cidr)
+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
+ (v6protocol[rule.protocol], net, prefixlen)
+ else:
+ net, mask = _get_net_and_mask(rule.cidr)
+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
+ (rule.protocol, net, mask)
if rule.protocol in ['tcp', 'udp']:
rule_xml += "dstportstart='%s' dstportend='%s' " % \
(rule.from_port, rule.to_port)
@@ -1034,8 +1157,11 @@ class NWFilterFirewall(FirewallDriver):
rule_xml += '/>\n'
rule_xml += "</rule>\n"
- xml = "<filter name='nova-secgroup-%s' chain='ipv4'>%s</filter>" % \
- (security_group_id, rule_xml,)
+ xml = "<filter name='nova-secgroup-%s' " % security_group_id
+ if(FLAGS.use_ipv6):
+ xml += "chain='root'>%s</filter>" % rule_xml
+ else:
+ xml += "chain='ipv4'>%s</filter>" % rule_xml
return xml
def _instance_filter_name(self, instance):
@@ -1045,17 +1171,25 @@ class NWFilterFirewall(FirewallDriver):
class IptablesFirewallDriver(FirewallDriver):
def __init__(self, execute=None):
self.execute = execute or utils.execute
- self.instances = set()
+ self.instances = {}
def apply_instance_filter(self, instance):
"""No-op. Everything is done in prepare_instance_filter"""
pass
def remove_instance(self, instance):
- self.instances.remove(instance)
+ if instance['id'] in self.instances:
+ del self.instances[instance['id']]
+ else:
+ LOG.info(_('Attempted to unfilter instance %s which is not '
+ 'filtered'), instance['id'])
def add_instance(self, instance):
- self.instances.add(instance)
+ self.instances[instance['id']] = instance
+
+ def unfilter_instance(self, instance):
+ self.remove_instance(instance)
+ self.apply_ruleset()
def prepare_instance_filter(self, instance):
self.add_instance(instance)
@@ -1064,11 +1198,17 @@ class IptablesFirewallDriver(FirewallDriver):
def apply_ruleset(self):
current_filter, _ = self.execute('sudo iptables-save -t filter')
current_lines = current_filter.split('\n')
- new_filter = self.modify_rules(current_lines)
+ new_filter = self.modify_rules(current_lines, 4)
self.execute('sudo iptables-restore',
process_input='\n'.join(new_filter))
-
- def modify_rules(self, current_lines):
+ if(FLAGS.use_ipv6):
+ current_filter, _ = self.execute('sudo ip6tables-save -t filter')
+ current_lines = current_filter.split('\n')
+ new_filter = self.modify_rules(current_lines, 6)
+ self.execute('sudo ip6tables-restore',
+ process_input='\n'.join(new_filter))
+
+ def modify_rules(self, current_lines, ip_version=4):
ctxt = context.get_admin_context()
# Remove any trace of nova rules.
new_filter = filter(lambda l: 'nova-' not in l, current_lines)
@@ -1082,18 +1222,22 @@ class IptablesFirewallDriver(FirewallDriver):
if not new_filter[rules_index].startswith(':'):
break
- our_chains = [':nova-ipv4-fallback - [0:0]']
- our_rules = ['-A nova-ipv4-fallback -j DROP']
+ our_chains = [':nova-fallback - [0:0]']
+ our_rules = ['-A nova-fallback -j DROP']
our_chains += [':nova-local - [0:0]']
our_rules += ['-A FORWARD -j nova-local']
- security_groups = set()
+ security_groups = {}
# Add our chains
# First, we add instance chains and rules
- for instance in self.instances:
+ for instance_id in self.instances:
+ instance = self.instances[instance_id]
chain_name = self._instance_chain_name(instance)
- ip_address = self._ip_for_instance(instance)
+ if(ip_version == 4):
+ ip_address = self._ip_for_instance(instance)
+ elif(ip_version == 6):
+ ip_address = self._ip_for_instance_v6(instance)
our_chains += [':%s - [0:0]' % chain_name]
@@ -1113,40 +1257,54 @@ class IptablesFirewallDriver(FirewallDriver):
for security_group in \
db.security_group_get_by_instance(ctxt,
instance['id']):
- security_groups.add(security_group)
+ security_groups[security_group['id']] = security_group
- sg_chain_name = self._security_group_chain_name(security_group)
+ sg_chain_name = self._security_group_chain_name(
+ security_group['id'])
our_rules += ['-A %s -j %s' % (chain_name, sg_chain_name)]
- # Allow DHCP responses
- dhcp_server = self._dhcp_server_for_instance(instance)
- our_rules += ['-A %s -s %s -p udp --sport 67 --dport 68' %
- (chain_name, dhcp_server)]
+ if(ip_version == 4):
+ # Allow DHCP responses
+ dhcp_server = self._dhcp_server_for_instance(instance)
+ our_rules += ['-A %s -s %s -p udp --sport 67 --dport 68' %
+ (chain_name, dhcp_server)]
+ elif(ip_version == 6):
+ # Allow RA responses
+ ra_server = self._ra_server_for_instance(instance)
+ our_rules += ['-A %s -s %s -p icmpv6' %
+ (chain_name, ra_server)]
# If nothing matches, jump to the fallback chain
- our_rules += ['-A %s -j nova-ipv4-fallback' % (chain_name,)]
+ our_rules += ['-A %s -j nova-fallback' % (chain_name,)]
# then, security group chains and rules
- for security_group in security_groups:
- chain_name = self._security_group_chain_name(security_group)
+ for security_group_id in security_groups:
+ chain_name = self._security_group_chain_name(security_group_id)
our_chains += [':%s - [0:0]' % chain_name]
rules = \
db.security_group_rule_get_by_security_group(ctxt,
- security_group['id'])
+ security_group_id)
for rule in rules:
logging.info('%r', rule)
- args = ['-A', chain_name, '-p', rule.protocol]
- if rule.cidr:
- args += ['-s', rule.cidr]
- else:
+ if not rule.cidr:
# Eventually, a mechanism to grant access for security
# groups will turn up here. It'll use ipsets.
continue
+ version = _get_ip_version(rule.cidr)
+ if version != ip_version:
+ continue
+
+ protocol = rule.protocol
+ if version == 6 and rule.protocol == 'icmp':
+ protocol = 'icmpv6'
+
+ args = ['-A', chain_name, '-p', protocol, '-s', rule.cidr]
+
if rule.protocol in ['udp', 'tcp']:
if rule.from_port == rule.to_port:
args += ['--dport', '%s' % (rule.from_port,)]
@@ -1166,7 +1324,12 @@ class IptablesFirewallDriver(FirewallDriver):
icmp_type_arg += '/%s' % icmp_code
if icmp_type_arg:
- args += ['-m', 'icmp', '--icmp-type', icmp_type_arg]
+ if(ip_version == 4):
+ args += ['-m', 'icmp', '--icmp-type',
+ icmp_type_arg]
+ elif(ip_version == 6):
+ args += ['-m', 'icmp6', '--icmpv6-type',
+ icmp_type_arg]
args += ['-j ACCEPT']
our_rules += [' '.join(args)]
@@ -1182,8 +1345,8 @@ class IptablesFirewallDriver(FirewallDriver):
def refresh_security_group_rules(self, security_group):
self.apply_ruleset()
- def _security_group_chain_name(self, security_group):
- return 'nova-sg-%s' % (security_group['id'],)
+ def _security_group_chain_name(self, security_group_id):
+ return 'nova-sg-%s' % (security_group_id,)
def _instance_chain_name(self, instance):
return 'nova-inst-%s' % (instance['id'],)
@@ -1192,7 +1355,16 @@ class IptablesFirewallDriver(FirewallDriver):
return db.instance_get_fixed_address(context.get_admin_context(),
instance['id'])
+ def _ip_for_instance_v6(self, instance):
+ return db.instance_get_fixed_address_v6(context.get_admin_context(),
+ instance['id'])
+
def _dhcp_server_for_instance(self, instance):
network = db.project_get_network(context.get_admin_context(),
instance['project_id'])
return network['gateway']
+
+ def _ra_server_for_instance(self, instance):
+ network = db.project_get_network(context.get_admin_context(),
+ instance['project_id'])
+ return network['ra_server']
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 9ed8896b6..37e623ed3 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -20,6 +20,11 @@ Management class for VM-related functions (spawn, reboot, etc).
"""
import json
+import M2Crypto
+import os
+import subprocess
+import tempfile
+import uuid
from nova import db
from nova import context
@@ -128,12 +133,31 @@ class VMOps(object):
"""Refactored out the common code of many methods that receive either
a vm name or a vm instance, and want a vm instance in return.
"""
+ vm = None
try:
- instance_name = instance_or_vm.name
- vm = VMHelper.lookup(self._session, instance_name)
- except AttributeError:
- # A vm opaque ref was passed
- vm = instance_or_vm
+ if instance_or_vm.startswith("OpaqueRef:"):
+ # Got passed an opaque ref; return it
+ return instance_or_vm
+ else:
+ # Must be the instance name
+ instance_name = instance_or_vm
+ except (AttributeError, KeyError):
+ # Note the the KeyError will only happen with fakes.py
+ # Not a string; must be an ID or a vm instance
+ if isinstance(instance_or_vm, (int, long)):
+ ctx = context.get_admin_context()
+ try:
+ instance_obj = db.instance_get_by_id(ctx, instance_or_vm)
+ instance_name = instance_obj.name
+ except exception.NotFound:
+ # The unit tests screw this up, as they use an integer for
+ # the vm name. I'd fix that up, but that's a matter for
+ # another bug report. So for now, just try with the passed
+ # value
+ instance_name = instance_or_vm
+ else:
+ instance_name = instance_or_vm.name
+ vm = VMHelper.lookup(self._session, instance_name)
if vm is None:
raise Exception(_('Instance not present %s') % instance_name)
return vm
@@ -190,6 +214,44 @@ class VMOps(object):
task = self._session.call_xenapi('Async.VM.clean_reboot', vm)
self._session.wait_for_task(instance.id, task)
+ def set_admin_password(self, instance, new_pass):
+ """Set the root/admin password on the VM instance. This is done via
+ an agent running on the VM. Communication between nova and the agent
+ is done via writing xenstore records. Since communication is done over
+ the XenAPI RPC calls, we need to encrypt the password. We're using a
+ simple Diffie-Hellman class instead of the more advanced one in
+ M2Crypto for compatibility with the agent code.
+ """
+ # Need to uniquely identify this request.
+ transaction_id = str(uuid.uuid4())
+ # The simple Diffie-Hellman class is used to manage key exchange.
+ dh = SimpleDH()
+ args = {'id': transaction_id, 'pub': str(dh.get_public())}
+ resp = self._make_agent_call('key_init', instance, '', args)
+ if resp is None:
+ # No response from the agent
+ return
+ resp_dict = json.loads(resp)
+ # Successful return code from key_init is 'D0'
+ if resp_dict['returncode'] != 'D0':
+ # There was some sort of error; the message will contain
+ # a description of the error.
+ raise RuntimeError(resp_dict['message'])
+ agent_pub = int(resp_dict['message'])
+ dh.compute_shared(agent_pub)
+ enc_pass = dh.encrypt(new_pass)
+ # Send the encrypted password
+ args['enc_pass'] = enc_pass
+ resp = self._make_agent_call('password', instance, '', args)
+ if resp is None:
+ # No response from the agent
+ return
+ resp_dict = json.loads(resp)
+ # Successful return code from password is '0'
+ if resp_dict['returncode'] != '0':
+ raise RuntimeError(resp_dict['message'])
+ return resp_dict['message']
+
def destroy(self, instance):
"""Destroy VM instance"""
vm = VMHelper.lookup(self._session, instance.name)
@@ -247,30 +309,19 @@ class VMOps(object):
def suspend(self, instance, callback):
"""suspend the specified instance"""
- instance_name = instance.name
- vm = VMHelper.lookup(self._session, instance_name)
- if vm is None:
- raise Exception(_("suspend: instance not present %s") %
- instance_name)
+ vm = self._get_vm_opaque_ref(instance)
task = self._session.call_xenapi('Async.VM.suspend', vm)
- self._wait_with_callback(task, callback)
+ self._wait_with_callback(instance.id, task, callback)
def resume(self, instance, callback):
"""resume the specified instance"""
- instance_name = instance.name
- vm = VMHelper.lookup(self._session, instance_name)
- if vm is None:
- raise Exception(_("resume: instance not present %s") %
- instance_name)
+ vm = self._get_vm_opaque_ref(instance)
task = self._session.call_xenapi('Async.VM.resume', vm, False, True)
- self._wait_with_callback(task, callback)
+ self._wait_with_callback(instance.id, task, callback)
- def get_info(self, instance_id):
+ def get_info(self, instance):
"""Return data about VM instance"""
- vm = VMHelper.lookup(self._session, instance_id)
- if vm is None:
- raise exception.NotFound(_('Instance not'
- ' found %s') % instance_id)
+ vm = self._get_vm_opaque_ref(instance)
rec = self._session.get_xenapi().VM.get_record(vm)
return VMHelper.compile_info(rec)
@@ -334,22 +385,34 @@ class VMOps(object):
return self._make_plugin_call('xenstore.py', method=method, vm=vm,
path=path, addl_args=addl_args)
+ def _make_agent_call(self, method, vm, path, addl_args={}):
+ """Abstracts out the interaction with the agent xenapi plugin."""
+ return self._make_plugin_call('agent', method=method, vm=vm,
+ path=path, addl_args=addl_args)
+
def _make_plugin_call(self, plugin, method, vm, path, addl_args={}):
"""Abstracts out the process of calling a method of a xenapi plugin.
Any errors raised by the plugin will in turn raise a RuntimeError here.
"""
+ instance_id = vm.id
vm = self._get_vm_opaque_ref(vm)
rec = self._session.get_xenapi().VM.get_record(vm)
args = {'dom_id': rec['domid'], 'path': path}
args.update(addl_args)
- # If the 'testing_mode' attribute is set, add that to the args.
- if getattr(self, 'testing_mode', False):
- args['testing_mode'] = 'true'
try:
task = self._session.async_call_plugin(plugin, method, args)
- ret = self._session.wait_for_task(0, task)
+ ret = self._session.wait_for_task(instance_id, task)
except self.XenAPI.Failure, e:
- raise RuntimeError("%s" % e.details[-1])
+ ret = None
+ err_trace = e.details[-1]
+ err_msg = err_trace.splitlines()[-1]
+ strargs = str(args)
+ if 'TIMEOUT:' in err_msg:
+ LOG.error(_('TIMEOUT: The call to %(method)s timed out. '
+ 'VM id=%(instance_id)s; args=%(strargs)s') % locals())
+ else:
+ LOG.error(_('The call to %(method)s returned an error: %(e)s. '
+ 'VM id=%(instance_id)s; args=%(strargs)s') % locals())
return ret
def add_to_xenstore(self, vm, path, key, value):
@@ -461,3 +524,89 @@ class VMOps(object):
"""Removes all data from the xenstore parameter record for this VM."""
self.write_to_param_xenstore(instance_or_vm, {})
########################################################################
+
+
+def _runproc(cmd):
+ pipe = subprocess.PIPE
+ return subprocess.Popen([cmd], shell=True, stdin=pipe, stdout=pipe,
+ stderr=pipe, close_fds=True)
+
+
+class SimpleDH(object):
+ """This class wraps all the functionality needed to implement
+ basic Diffie-Hellman-Merkle key exchange in Python. It features
+ intelligent defaults for the prime and base numbers needed for the
+ calculation, while allowing you to supply your own. It requires that
+ the openssl binary be installed on the system on which this is run,
+ as it uses that to handle the encryption and decryption. If openssl
+ is not available, a RuntimeError will be raised.
+ """
+ def __init__(self, prime=None, base=None, secret=None):
+ """You can specify the values for prime and base if you wish;
+ otherwise, reasonable default values will be used.
+ """
+ if prime is None:
+ self._prime = 162259276829213363391578010288127
+ else:
+ self._prime = prime
+ if base is None:
+ self._base = 5
+ else:
+ self._base = base
+ self._shared = self._public = None
+
+ self._dh = M2Crypto.DH.set_params(
+ self.dec_to_mpi(self._prime),
+ self.dec_to_mpi(self._base))
+ self._dh.gen_key()
+ self._public = self.mpi_to_dec(self._dh.pub)
+
+ def get_public(self):
+ return self._public
+
+ def compute_shared(self, other):
+ self._shared = self.bin_to_dec(
+ self._dh.compute_key(self.dec_to_mpi(other)))
+ return self._shared
+
+ def mpi_to_dec(self, mpi):
+ bn = M2Crypto.m2.mpi_to_bn(mpi)
+ hexval = M2Crypto.m2.bn_to_hex(bn)
+ dec = int(hexval, 16)
+ return dec
+
+ def bin_to_dec(self, binval):
+ bn = M2Crypto.m2.bin_to_bn(binval)
+ hexval = M2Crypto.m2.bn_to_hex(bn)
+ dec = int(hexval, 16)
+ return dec
+
+ def dec_to_mpi(self, dec):
+ bn = M2Crypto.m2.dec_to_bn('%s' % dec)
+ mpi = M2Crypto.m2.bn_to_mpi(bn)
+ return mpi
+
+ def _run_ssl(self, text, which):
+ base_cmd = ('cat %(tmpfile)s | openssl enc -aes-128-cbc '
+ '-a -pass pass:%(shared)s -nosalt %(dec_flag)s')
+ if which.lower()[0] == 'd':
+ dec_flag = ' -d'
+ else:
+ dec_flag = ''
+ fd, tmpfile = tempfile.mkstemp()
+ os.close(fd)
+ file(tmpfile, 'w').write(text)
+ shared = self._shared
+ cmd = base_cmd % locals()
+ proc = _runproc(cmd)
+ proc.wait()
+ err = proc.stderr.read()
+ if err:
+ raise RuntimeError(_('OpenSSL error: %s') % err)
+ return proc.stdout.read()
+
+ def encrypt(self, text):
+ return self._run_ssl(text, 'enc')
+
+ def decrypt(self, text):
+ return self._run_ssl(text, 'dec')
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 3b961bb77..492e2314b 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -152,6 +152,10 @@ class XenAPIConnection(object):
"""Reboot VM instance"""
self._vmops.reboot(instance)
+ def set_admin_password(self, instance, new_pass):
+ """Set the root/admin password on the VM instance"""
+ self._vmops.set_admin_password(instance, new_pass)
+
def destroy(self, instance):
"""Destroy VM instance"""
self._vmops.destroy(instance)
@@ -269,7 +273,8 @@ class XenAPISession(object):
def _poll_task(self, id, task, done):
"""Poll the given XenAPI task, and fire the given action if we
- get a result."""
+ get a result.
+ """
try:
name = self._session.xenapi.task.get_name_label(task)
status = self._session.xenapi.task.get_status(task)
diff --git a/nova/volume/driver.py b/nova/volume/driver.py
index 6bc925f3e..71fe18a40 100644
--- a/nova/volume/driver.py
+++ b/nova/volume/driver.py
@@ -49,6 +49,8 @@ flags.DEFINE_string('iscsi_target_prefix', 'iqn.2010-10.org.openstack:',
'prefix for iscsi volumes')
flags.DEFINE_string('iscsi_ip_prefix', '127.0',
'discover volumes on the ip that starts with this prefix')
+flags.DEFINE_string('rbd_pool', 'rbd',
+ 'the rbd pool in which volumes are stored')
class VolumeDriver(object):
@@ -314,3 +316,107 @@ class FakeISCSIDriver(ISCSIDriver):
"""Execute that simply logs the command."""
LOG.debug(_("FAKE ISCSI: %s"), cmd)
return (None, None)
+
+
+class RBDDriver(VolumeDriver):
+ """Implements RADOS block device (RBD) volume commands"""
+
+ def check_for_setup_error(self):
+ """Returns an error if prerequisites aren't met"""
+ (stdout, stderr) = self._execute("rados lspools")
+ pools = stdout.split("\n")
+ if not FLAGS.rbd_pool in pools:
+ raise exception.Error(_("rbd has no pool %s") %
+ FLAGS.rbd_pool)
+
+ def create_volume(self, volume):
+ """Creates a logical volume."""
+ if int(volume['size']) == 0:
+ size = 100
+ else:
+ size = int(volume['size']) * 1024
+ self._try_execute("rbd --pool %s --size %d create %s" %
+ (FLAGS.rbd_pool,
+ size,
+ volume['name']))
+
+ def delete_volume(self, volume):
+ """Deletes a logical volume."""
+ self._try_execute("rbd --pool %s rm %s" %
+ (FLAGS.rbd_pool,
+ volume['name']))
+
+ def local_path(self, volume):
+ """Returns the path of the rbd volume."""
+ # This is the same as the remote path
+ # since qemu accesses it directly.
+ return self.discover_volume(volume)
+
+ def ensure_export(self, context, volume):
+ """Synchronously recreates an export for a logical volume."""
+ pass
+
+ def create_export(self, context, volume):
+ """Exports the volume"""
+ pass
+
+ def remove_export(self, context, volume):
+ """Removes an export for a logical volume"""
+ pass
+
+ def discover_volume(self, volume):
+ """Discover volume on a remote host"""
+ return "rbd:%s/%s" % (FLAGS.rbd_pool, volume['name'])
+
+ def undiscover_volume(self, volume):
+ """Undiscover volume on a remote host"""
+ pass
+
+
+class SheepdogDriver(VolumeDriver):
+ """Executes commands relating to Sheepdog Volumes"""
+
+ def check_for_setup_error(self):
+ """Returns an error if prerequisites aren't met"""
+ try:
+ (out, err) = self._execute("collie cluster info")
+ if not out.startswith('running'):
+ raise exception.Error(_("Sheepdog is not working: %s") % out)
+ except exception.ProcessExecutionError:
+ raise exception.Error(_("Sheepdog is not working"))
+
+ def create_volume(self, volume):
+ """Creates a sheepdog volume"""
+ if int(volume['size']) == 0:
+ sizestr = '100M'
+ else:
+ sizestr = '%sG' % volume['size']
+ self._try_execute("qemu-img create sheepdog:%s %s" %
+ (volume['name'], sizestr))
+
+ def delete_volume(self, volume):
+ """Deletes a logical volume"""
+ self._try_execute("collie vdi delete %s" % volume['name'])
+
+ def local_path(self, volume):
+ return "sheepdog:%s" % volume['name']
+
+ def ensure_export(self, context, volume):
+ """Safely and synchronously recreates an export for a logical volume"""
+ pass
+
+ def create_export(self, context, volume):
+ """Exports the volume"""
+ pass
+
+ def remove_export(self, context, volume):
+ """Removes an export for a logical volume"""
+ pass
+
+ def discover_volume(self, volume):
+ """Discover volume on a remote host"""
+ return "sheepdog:%s" % volume['name']
+
+ def undiscover_volume(self, volume):
+ """Undiscover volume on a remote host"""
+ pass
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent
new file mode 100755
index 000000000..12c3a19c8
--- /dev/null
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2011 Citrix Systems, Inc.
+# Copyright 2011 OpenStack LLC.
+# Copyright 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.
+
+#
+# XenAPI plugin for reading/writing information to xenstore
+#
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+import os
+import random
+import subprocess
+import tempfile
+import time
+
+import XenAPIPlugin
+
+from pluginlib_nova import *
+configure_logging("xenstore")
+import xenstore
+
+AGENT_TIMEOUT = 30
+
+
+def jsonify(fnc):
+ def wrapper(*args, **kwargs):
+ return json.dumps(fnc(*args, **kwargs))
+ return wrapper
+
+
+class TimeoutError(StandardError):
+ pass
+
+
+@jsonify
+def key_init(self, arg_dict):
+ """Handles the Diffie-Hellman key exchange with the agent to
+ establish the shared secret key used to encrypt/decrypt sensitive
+ info to be passed, such as passwords. Returns the shared
+ secret key value.
+ """
+ pub = int(arg_dict["pub"])
+ arg_dict["value"] = json.dumps({"name": "keyinit", "value": pub})
+ request_id = arg_dict["id"]
+ arg_dict["path"] = "data/host/%s" % request_id
+ xenstore.write_record(self, arg_dict)
+ try:
+ resp = _wait_for_agent(self, request_id, arg_dict)
+ except TimeoutError, e:
+ raise PluginError("%s" % e)
+ return resp
+
+
+@jsonify
+def password(self, arg_dict):
+ """Writes a request to xenstore that tells the agent to set
+ the root password for the given VM. The password should be
+ encrypted using the shared secret key that was returned by a
+ previous call to key_init. The encrypted password value should
+ be passed as the value for the 'enc_pass' key in arg_dict.
+ """
+ pub = int(arg_dict["pub"])
+ enc_pass = arg_dict["enc_pass"]
+ arg_dict["value"] = json.dumps({"name": "password", "value": enc_pass})
+ request_id = arg_dict["id"]
+ arg_dict["path"] = "data/host/%s" % request_id
+ xenstore.write_record(self, arg_dict)
+ try:
+ resp = _wait_for_agent(self, request_id, arg_dict)
+ except TimeoutError, e:
+ raise PluginError("%s" % e)
+ return resp
+
+
+def _wait_for_agent(self, request_id, arg_dict):
+ """Periodically checks xenstore for a response from the agent.
+ The request is always written to 'data/host/{id}', and
+ the agent's response for that request will be in 'data/guest/{id}'.
+ If no value appears from the agent within the time specified by
+ AGENT_TIMEOUT, the original request is deleted and a TimeoutError
+ is returned.
+ """
+ arg_dict["path"] = "data/guest/%s" % request_id
+ arg_dict["ignore_missing_path"] = True
+ start = time.time()
+ while True:
+ if time.time() - start > AGENT_TIMEOUT:
+ # No response within the timeout period; bail out
+ # First, delete the request record
+ arg_dict["path"] = "data/host/%s" % request_id
+ xenstore.delete_record(self, arg_dict)
+ raise TimeoutError("TIMEOUT: No response from agent within %s seconds." %
+ AGENT_TIMEOUT)
+ ret = xenstore.read_record(self, arg_dict)
+ # Note: the response for None with be a string that includes
+ # double quotes.
+ if ret != '"None"':
+ # The agent responded
+ return ret
+ else:
+ time.sleep(3)
+
+
+if __name__ == "__main__":
+ XenAPIPlugin.dispatch(
+ {"key_init": key_init,
+ "password": password})
diff --git a/smoketests/admin_smoketests.py b/smoketests/admin_smoketests.py
index 1ef1c1425..86a7f600d 100644
--- a/smoketests/admin_smoketests.py
+++ b/smoketests/admin_smoketests.py
@@ -43,7 +43,7 @@ flags.DEFINE_string('suite', None, 'Specific test suite to run ' + SUITE_NAMES)
# TODO(devamcar): Use random tempfile
ZIP_FILENAME = '/tmp/nova-me-x509.zip'
-TEST_PREFIX = 'test%s' % int(random.random()*1000000)
+TEST_PREFIX = 'test%s' % int(random.random() * 1000000)
TEST_USERNAME = '%suser' % TEST_PREFIX
TEST_PROJECTNAME = '%sproject' % TEST_PREFIX
@@ -96,4 +96,3 @@ class UserTests(AdminSmokeTestCase):
if __name__ == "__main__":
suites = {'user': unittest.makeSuite(UserTests)}
sys.exit(base.run_tests(suites))
-
diff --git a/smoketests/base.py b/smoketests/base.py
index 5a14d3e09..610270c5c 100644
--- a/smoketests/base.py
+++ b/smoketests/base.py
@@ -17,6 +17,7 @@
# under the License.
import boto
+import boto_v6
import commands
import httplib
import os
@@ -69,6 +70,17 @@ class SmokeTestCase(unittest.TestCase):
'test.')
parts = self.split_clc_url(clc_url)
+ if FLAGS.use_ipv6:
+ return boto_v6.connect_ec2(aws_access_key_id=access_key,
+ aws_secret_access_key=secret_key,
+ is_secure=parts['is_secure'],
+ region=RegionInfo(None,
+ 'nova',
+ parts['ip']),
+ port=parts['port'],
+ path='/services/Cloud',
+ **kwargs)
+
return boto.connect_ec2(aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
is_secure=parts['is_secure'],
@@ -115,7 +127,8 @@ class SmokeTestCase(unittest.TestCase):
return True
def upload_image(self, bucket_name, image):
- cmd = 'euca-upload-bundle -b %s -m /tmp/%s.manifest.xml' % (bucket_name, image)
+ cmd = 'euca-upload-bundle -b '
+ cmd += '%s -m /tmp/%s.manifest.xml' % (bucket_name, image)
status, output = commands.getstatusoutput(cmd)
if status != 0:
print '%s -> \n %s' % (cmd, output)
@@ -130,6 +143,7 @@ class SmokeTestCase(unittest.TestCase):
raise Exception(output)
return True
+
def run_tests(suites):
argv = FLAGS(sys.argv)
@@ -151,4 +165,3 @@ def run_tests(suites):
else:
for suite in suites.itervalues():
unittest.TextTestRunner(verbosity=2).run(suite)
-
diff --git a/smoketests/flags.py b/smoketests/flags.py
index ae4d09508..35f432a77 100644
--- a/smoketests/flags.py
+++ b/smoketests/flags.py
@@ -33,6 +33,7 @@ DEFINE_bool = DEFINE_bool
# __GLOBAL FLAGS ONLY__
# Define any app-specific flags in their own files, docs at:
# http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#39
+
DEFINE_string('region', 'nova', 'Region to use')
DEFINE_string('test_image', 'ami-tiny', 'Image to use for launch tests')
-
+DEFINE_string('use_ipv6', True, 'use the ipv6 or not')
diff --git a/smoketests/public_network_smoketests.py b/smoketests/public_network_smoketests.py
new file mode 100644
index 000000000..bfc2b20ba
--- /dev/null
+++ b/smoketests/public_network_smoketests.py
@@ -0,0 +1,180 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import commands
+import os
+import random
+import socket
+import sys
+import time
+import unittest
+
+from smoketests import flags
+from smoketests import base
+from smoketests import user_smoketests
+
+#Note that this test should run from
+#public network (outside of private network segments)
+#Please set EC2_URL correctly
+#You should use admin account in this test
+
+FLAGS = flags.FLAGS
+
+TEST_PREFIX = 'test%s' % int(random.random() * 1000000)
+TEST_BUCKET = '%s_bucket' % TEST_PREFIX
+TEST_KEY = '%s_key' % TEST_PREFIX
+TEST_KEY2 = '%s_key2' % TEST_PREFIX
+TEST_DATA = {}
+
+
+class InstanceTestsFromPublic(user_smoketests.UserSmokeTestCase):
+ def test_001_can_create_keypair(self):
+ key = self.create_key_pair(self.conn, TEST_KEY)
+ self.assertEqual(key.name, TEST_KEY)
+
+ def test_002_security_group(self):
+ security_group_name = "".join(random.choice("sdiuisudfsdcnpaqwertasd")
+ for x in range(random.randint(4, 8)))
+ group = self.conn.create_security_group(security_group_name,
+ 'test group')
+ group.connection = self.conn
+ group.authorize('tcp', 22, 22, '0.0.0.0/0')
+ if FLAGS.use_ipv6:
+ group.authorize('tcp', 22, 22, '::/0')
+
+ reservation = self.conn.run_instances(FLAGS.test_image,
+ key_name=TEST_KEY,
+ security_groups=[security_group_name],
+ instance_type='m1.tiny')
+ self.data['security_group_name'] = security_group_name
+ self.data['group'] = group
+ self.data['instance_id'] = reservation.instances[0].id
+
+ def test_003_instance_with_group_runs_within_60_seconds(self):
+ reservations = self.conn.get_all_instances([self.data['instance_id']])
+ instance = reservations[0].instances[0]
+ # allow 60 seconds to exit pending with IP
+ for x in xrange(60):
+ instance.update()
+ if instance.state == u'running':
+ break
+ time.sleep(1)
+ else:
+ self.fail('instance failed to start')
+ ip = reservations[0].instances[0].private_dns_name
+ self.failIf(ip == '0.0.0.0')
+ self.data['private_ip'] = ip
+ if FLAGS.use_ipv6:
+ ipv6 = reservations[0].instances[0].dns_name_v6
+ self.failIf(ipv6 is None)
+ self.data['ip_v6'] = ipv6
+
+ def test_004_can_ssh_to_ipv6(self):
+ if FLAGS.use_ipv6:
+ for x in xrange(20):
+ try:
+ conn = self.connect_ssh(
+ self.data['ip_v6'], TEST_KEY)
+ conn.close()
+ except Exception as ex:
+ print ex
+ time.sleep(1)
+ else:
+ break
+ else:
+ self.fail('could not ssh to instance')
+
+ def test_012_can_create_instance_with_keypair(self):
+ if 'instance_id' in self.data:
+ self.conn.terminate_instances([self.data['instance_id']])
+ reservation = self.conn.run_instances(FLAGS.test_image,
+ key_name=TEST_KEY,
+ instance_type='m1.tiny')
+ self.assertEqual(len(reservation.instances), 1)
+ self.data['instance_id'] = reservation.instances[0].id
+
+ def test_013_instance_runs_within_60_seconds(self):
+ reservations = self.conn.get_all_instances([self.data['instance_id']])
+ instance = reservations[0].instances[0]
+ # allow 60 seconds to exit pending with IP
+ for x in xrange(60):
+ instance.update()
+ if instance.state == u'running':
+ break
+ time.sleep(1)
+ else:
+ self.fail('instance failed to start')
+ ip = reservations[0].instances[0].private_dns_name
+ self.failIf(ip == '0.0.0.0')
+ self.data['private_ip'] = ip
+ if FLAGS.use_ipv6:
+ ipv6 = reservations[0].instances[0].dns_name_v6
+ self.failIf(ipv6 is None)
+ self.data['ip_v6'] = ipv6
+
+ def test_014_can_not_ping_private_ip(self):
+ for x in xrange(4):
+ # ping waits for 1 second
+ status, output = commands.getstatusoutput(
+ 'ping -c1 %s' % self.data['private_ip'])
+ if status == 0:
+ self.fail('can ping private ip from public network')
+ if FLAGS.use_ipv6:
+ status, output = commands.getstatusoutput(
+ 'ping6 -c1 %s' % self.data['ip_v6'])
+ if status == 0:
+ self.fail('can ping ipv6 from public network')
+ else:
+ pass
+
+ def test_015_can_not_ssh_to_private_ip(self):
+ for x in xrange(1):
+ try:
+ conn = self.connect_ssh(self.data['private_ip'], TEST_KEY)
+ conn.close()
+ except Exception:
+ time.sleep(1)
+ else:
+ self.fail('can ssh for ipv4 address from public network')
+
+ if FLAGS.use_ipv6:
+ for x in xrange(1):
+ try:
+ conn = self.connect_ssh(
+ self.data['ip_v6'], TEST_KEY)
+ conn.close()
+ except Exception:
+ time.sleep(1)
+ else:
+ self.fail('can ssh for ipv6 address from public network')
+
+ def test_999_tearDown(self):
+ self.delete_key_pair(self.conn, TEST_KEY)
+ security_group_name = self.data['security_group_name']
+ group = self.data['group']
+ if group:
+ group.revoke('tcp', 22, 22, '0.0.0.0/0')
+ if FLAGS.use_ipv6:
+ group.revoke('tcp', 22, 22, '::/0')
+ self.conn.delete_security_group(security_group_name)
+ if 'instance_id' in self.data:
+ self.conn.terminate_instances([self.data['instance_id']])
+
+if __name__ == "__main__":
+ suites = {'instance': unittest.makeSuite(InstanceTestsFromPublic)}
+ sys.exit(base.run_tests(suites))
diff --git a/smoketests/user_smoketests.py b/smoketests/user_smoketests.py
index 578c0722e..d5a3a7556 100644
--- a/smoketests/user_smoketests.py
+++ b/smoketests/user_smoketests.py
@@ -45,7 +45,7 @@ flags.DEFINE_string('bundle_kernel', 'openwrt-x86-vmlinuz',
flags.DEFINE_string('bundle_image', 'openwrt-x86-ext2.image',
'Local image file to use for bundling tests')
-TEST_PREFIX = 'test%s' % int (random.random()*1000000)
+TEST_PREFIX = 'test%s' % int(random.random() * 1000000)
TEST_BUCKET = '%s_bucket' % TEST_PREFIX
TEST_KEY = '%s_key' % TEST_PREFIX
TEST_GROUP = '%s_group' % TEST_PREFIX
@@ -80,7 +80,7 @@ class ImageTests(UserSmokeTestCase):
def test_006_can_register_kernel(self):
kernel_id = self.conn.register_image('%s/%s.manifest.xml' %
- (TEST_BUCKET, FLAGS.bundle_kernel))
+ (TEST_BUCKET, FLAGS.bundle_kernel))
self.assert_(kernel_id is not None)
self.data['kernel_id'] = kernel_id
@@ -92,7 +92,7 @@ class ImageTests(UserSmokeTestCase):
time.sleep(1)
else:
print image.state
- self.assert_(False) # wasn't available within 10 seconds
+ self.assert_(False) # wasn't available within 10 seconds
self.assert_(image.type == 'machine')
for i in xrange(10):
@@ -101,7 +101,7 @@ class ImageTests(UserSmokeTestCase):
break
time.sleep(1)
else:
- self.assert_(False) # wasn't available within 10 seconds
+ self.assert_(False) # wasn't available within 10 seconds
self.assert_(kernel.type == 'kernel')
def test_008_can_describe_image_attribute(self):
@@ -152,14 +152,17 @@ class InstanceTests(UserSmokeTestCase):
for x in xrange(60):
instance.update()
if instance.state == u'running':
- break
+ break
time.sleep(1)
else:
self.fail('instance failed to start')
ip = reservations[0].instances[0].private_dns_name
self.failIf(ip == '0.0.0.0')
self.data['private_ip'] = ip
- print self.data['private_ip']
+ if FLAGS.use_ipv6:
+ ipv6 = reservations[0].instances[0].dns_name_v6
+ self.failIf(ipv6 is None)
+ self.data['ip_v6'] = ipv6
def test_004_can_ping_private_ip(self):
for x in xrange(120):
@@ -171,6 +174,16 @@ class InstanceTests(UserSmokeTestCase):
else:
self.fail('could not ping instance')
+ if FLAGS.use_ipv6:
+ for x in xrange(120):
+ # ping waits for 1 second
+ status, output = commands.getstatusoutput(
+ 'ping6 -c1 %s' % self.data['ip_v6'])
+ if status == 0:
+ break
+ else:
+ self.fail('could not ping instance')
+
def test_005_can_ssh_to_private_ip(self):
for x in xrange(30):
try:
@@ -183,6 +196,19 @@ class InstanceTests(UserSmokeTestCase):
else:
self.fail('could not ssh to instance')
+ if FLAGS.use_ipv6:
+ for x in xrange(30):
+ try:
+ conn = self.connect_ssh(
+ self.data['ip_v6'], TEST_KEY)
+ conn.close()
+ except Exception:
+ time.sleep(1)
+ else:
+ break
+ else:
+ self.fail('could not ssh to instance v6')
+
def test_006_can_allocate_elastic_ip(self):
result = self.conn.allocate_address()
self.assertTrue(hasattr(result, 'public_ip'))
@@ -388,7 +414,6 @@ class SecurityGroupTests(UserSmokeTestCase):
raise Exception("Timeout")
time.sleep(1)
-
def test_999_tearDown(self):
self.conn.delete_key_pair(TEST_KEY)
self.conn.delete_security_group(TEST_GROUP)
diff --git a/tools/pip-requires b/tools/pip-requires
index c9ce5c31c..a6676a5e9 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -25,4 +25,5 @@ bzr
Twisted>=10.1.0
PasteDeploy
paste
+netaddr
glance