summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorJosh Kearney <josh.kearney@rackspace.com>2011-01-14 10:47:38 -0600
committerJosh Kearney <josh.kearney@rackspace.com>2011-01-14 10:47:38 -0600
commit8040d0de838ab475b5f54dad85307df579eb0935 (patch)
treead9c44dc298de6498a911e6e022d15bb1d81ccb9 /nova
parented0b531c57ebbf5a3e28b8cd0a1d02bad712705b (diff)
parent9fb1e7b1f627b10fda1249754e4bc612d697110c (diff)
Merged trunk
Diffstat (limited to 'nova')
-rw-r--r--nova/api/ec2/cloud.py13
-rw-r--r--nova/api/openstack/servers.py9
-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.py12
-rw-r--r--nova/db/sqlalchemy/api.py27
-rw-r--r--nova/db/sqlalchemy/models.py4
-rw-r--r--nova/exception.py4
-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_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.py354
-rw-r--r--nova/virt/xenapi/vmops.py199
-rw-r--r--nova/virt/xenapi_conn.py7
-rw-r--r--nova/volume/driver.py49
24 files changed, 1111 insertions, 420 deletions
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 3abb6e3f6..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
@@ -374,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:
@@ -643,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)}
@@ -678,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 3a6c61a3a..6c84d15fd 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/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 f1fdd64e6..d5f8f338f 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 e57766b5c..f9d561587 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -299,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)
@@ -357,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)
@@ -552,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 39df21e30..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)
@@ -794,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():
@@ -1130,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 bbc89e573..af9584f81 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -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/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_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 ec9462ada..261ee0fde 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
@@ -262,3 +263,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 797ac1b60..f227266c8 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):
@@ -214,6 +227,8 @@ class LibvirtConnection(object):
power_state.SHUTDOWN)
break
+ self.firewall_driver.unfilter_instance(instance)
+
if cleanup:
self._cleanup(instance)
@@ -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") % (net, mask)
+ "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)
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 9da50377f..96be17d8c 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
@@ -127,12 +132,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
@@ -189,6 +213,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)
@@ -246,21 +308,13 @@ 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(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(instance.id, task, callback)
@@ -272,12 +326,9 @@ class VMOps(object):
"""Unrescue the specified instance"""
return True
- 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)
@@ -341,22 +392,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):
@@ -468,3 +531,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 c24ec972b..b2e209f63 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -149,6 +149,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)
@@ -274,7 +278,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 44bfeaf0c..71fe18a40 100644
--- a/nova/volume/driver.py
+++ b/nova/volume/driver.py
@@ -371,3 +371,52 @@ class RBDDriver(VolumeDriver):
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