initial commit of openvz driver broken out from nova code base

This commit is contained in:
Daniel Salinas 2013-04-22 13:25:36 -05:00
parent 7480cf1365
commit 9e67e5a0fc
23 changed files with 4494 additions and 0 deletions

3
.gitignore vendored
View File

@ -33,3 +33,6 @@ nosetests.xml
.mr.developer.cfg
.project
.pydevproject
# Pycharm
.idea

View File

@ -0,0 +1,261 @@
# nova-rootwrap command filters for compute nodes
# This file should be owned by (and only-writeable by) the root user
[Filters]
# nova/virt/disk/mount/api.py: 'kpartx', '-a', device
# nova/virt/disk/mount/api.py: 'kpartx', '-d', device
kpartx: CommandFilter, /sbin/kpartx, root
# nova/virt/xenapi/vm_utils.py: tune2fs, -O ^has_journal, part_path
# nova/virt/xenapi/vm_utils.py: tune2fs, -j, partition_path
tune2fs: CommandFilter, /sbin/tune2fs, root
# nova/virt/disk/mount/api.py: 'mount', mapped_device
# nova/virt/disk/api.py: 'mount', '-o', 'bind', src, target
# nova/virt/xenapi/vm_utils.py: 'mount', '-t', 'ext2,ext3,ext4,reiserfs'..
# nova/virt/configdrive.py: 'mount', device, mountdir
# nova/virt/libvirt/volume.py: 'mount', '-t', 'sofs' ...
# nova/virt/openvz/utils.py: 'mount', '-o', 'defaults' ...
mount: CommandFilter, /bin/mount, root
# nova/virt/disk/mount/api.py: 'umount', mapped_device
# nova/virt/disk/api.py: 'umount' target
# nova/virt/xenapi/vm_utils.py: 'umount', dev_path
# nova/virt/configdrive.py: 'umount', mountdir
# nova/virt/openvz/utils.py: 'umount'
umount: CommandFilter, /bin/umount, root
# nova/virt/disk/mount/nbd.py: 'qemu-nbd', '-c', device, image
# nova/virt/disk/mount/nbd.py: 'qemu-nbd', '-d', device
qemu-nbd: CommandFilter, /usr/bin/qemu-nbd, root
# nova/virt/disk/mount/loop.py: 'losetup', '--find', '--show', image
# nova/virt/disk/mount/loop.py: 'losetup', '--detach', device
losetup: CommandFilter, /sbin/losetup, root
# nova/virt/disk/vfs/localfs.py: 'tee', canonpath
tee: CommandFilter, /usr/bin/tee, root
# nova/virt/disk/vfs/localfs.py: 'mkdir', canonpath
mkdir: CommandFilter, /bin/mkdir, root
# nova/virt/disk/vfs/localfs.py: 'chown'
# nova/virt/libvirt/connection.py: 'chown', os.getuid( console_log
# nova/virt/libvirt/connection.py: 'chown', os.getuid( console_log
# nova/virt/libvirt/connection.py: 'chown', 'root', basepath('disk')
# nova/utils.py: 'chown', owner_uid, path
chown: CommandFilter, /bin/chown, root
# nova/virt/disk/vfs/localfs.py: 'chmod'
chmod: CommandFilter, /bin/chmod, root
# nova/virt/libvirt/vif.py: 'ip', 'tuntap', 'add', dev, 'mode', 'tap'
# nova/virt/libvirt/vif.py: 'ip', 'link', 'set', dev, 'up'
# nova/virt/libvirt/vif.py: 'ip', 'link', 'delete', dev
# nova/network/linux_net.py: 'ip', 'addr', 'add', str(floating_ip)+'/32'i..
# nova/network/linux_net.py: 'ip', 'addr', 'del', str(floating_ip)+'/32'..
# nova/network/linux_net.py: 'ip', 'addr', 'add', '169.254.169.254/32',..
# nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', dev, 'scope',..
# nova/network/linux_net.py: 'ip', 'addr', 'del/add', ip_params, dev)
# nova/network/linux_net.py: 'ip', 'addr', 'del', params, fields[-1]
# nova/network/linux_net.py: 'ip', 'addr', 'add', params, bridge
# nova/network/linux_net.py: 'ip', '-f', 'inet6', 'addr', 'change', ..
# nova/network/linux_net.py: 'ip', 'link', 'set', 'dev', dev, 'promisc',..
# nova/network/linux_net.py: 'ip', 'link', 'add', 'link', bridge_if ...
# nova/network/linux_net.py: 'ip', 'link', 'set', interface, address,..
# nova/network/linux_net.py: 'ip', 'link', 'set', interface, 'up'
# nova/network/linux_net.py: 'ip', 'link', 'set', bridge, 'up'
# nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', interface, ..
# nova/network/linux_net.py: 'ip', 'link', 'set', dev, address, ..
# nova/network/linux_net.py: 'ip', 'link', 'set', dev, 'up'
ip: CommandFilter, /sbin/ip, root
# nova/virt/libvirt/vif.py: 'tunctl', '-b', '-t', dev
# nova/network/linux_net.py: 'tunctl', '-b', '-t', dev
tunctl: CommandFilter, tunctl, root
# nova/virt/libvirt/vif.py: 'ovs-vsctl', ...
# nova/virt/libvirt/vif.py: 'ovs-vsctl', 'del-port', ...
# nova/network/linux_net.py: 'ovs-vsctl', ....
ovs-vsctl: CommandFilter, /usr/bin/ovs-vsctl, root
# nova/network/linux_net.py: 'ovs-ofctl', ....
ovs-ofctl: CommandFilter, /usr/bin/ovs-ofctl, root
# nova/virt/libvirt/connection.py: 'dd', if=%s % virsh_output, ...
dd: CommandFilter, /bin/dd, root
# nova/virt/xenapi/volume_utils.py: 'iscsiadm', '-m', ...
iscsiadm: CommandFilter, iscsiadm, root
# nova/virt/libvirt/volume.py: 'aoe-revalidate', aoedev
# nova/virt/libvirt/volume.py: 'aoe-discover'
aoe-revalidate: CommandFilter, /usr/sbin/aoe-revalidate, root
aoe-discover: CommandFilter, /usr/sbin/aoe-discover, root
# nova/virt/xenapi/vm_utils.py: parted, --script, ...
# nova/virt/xenapi/vm_utils.py: 'parted', '--script', dev_path, ..*.
parted: CommandFilter, parted, root
# nova/virt/xenapi/vm_utils.py: 'pygrub', '-qn', dev_path
pygrub: CommandFilter, /usr/bin/pygrub, root
# nova/virt/xenapi/vm_utils.py: fdisk %(dev_path)s
# nova/virt/openvz/volume.py: fdisk %(dev_path)s
fdisk: CommandFilter, /sbin/fdisk, root
# nova/virt/xenapi/vm_utils.py: e2fsck, -f, -p, partition_path
# nova/virt/disk/api.py: e2fsck, -f, -p, image
e2fsck: CommandFilter, /sbin/e2fsck, root
# nova/virt/xenapi/vm_utils.py: resize2fs, partition_path
# nova/virt/disk/api.py: resize2fs, image
resize2fs: CommandFilter, /sbin/resize2fs, root
# nova/network/linux_net.py: 'ip[6]tables-save' % (cmd, '-t', ...
iptables-save: CommandFilter, iptables-save, root
ip6tables-save: CommandFilter, ip6tables-save, root
# nova/network/linux_net.py: 'ip[6]tables-restore' % (cmd,)
iptables-restore: CommandFilter, iptables-restore, root
ip6tables-restore: CommandFilter, ip6tables-restore, root
# nova/network/linux_net.py: 'arping', '-U', floating_ip, '-A', '-I', ...
# nova/network/linux_net.py: 'arping', '-U', network_ref['dhcp_server'],..
arping: CommandFilter, arping, root
# nova/network/linux_net.py: 'route', '-n'
# nova/network/linux_net.py: 'route', 'del', 'default', 'gw'
# nova/network/linux_net.py: 'route', 'add', 'default', 'gw'
# nova/network/linux_net.py: 'route', '-n'
# nova/network/linux_net.py: 'route', 'del', 'default', 'gw', old_gw, ..
# nova/network/linux_net.py: 'route', 'add', 'default', 'gw', old_gateway
route: CommandFilter, /sbin/route, root
# nova/network/linux_net.py: 'dhcp_release', dev, address, mac_address
dhcp_release: CommandFilter, /usr/bin/dhcp_release, root
# nova/network/linux_net.py: 'kill', '-9', pid
# nova/network/linux_net.py: 'kill', '-HUP', pid
kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP
# nova/network/linux_net.py: 'kill', pid
kill_radvd: KillFilter, root, /usr/sbin/radvd
# nova/network/linux_net.py: dnsmasq call
dnsmasq: DnsmasqFilter, /usr/sbin/dnsmasq, root
dnsmasq_deprecated: DeprecatedDnsmasqFilter, /usr/sbin/dnsmasq, root
# nova/network/linux_net.py: 'radvd', '-C', '%s' % _ra_file(dev, 'conf'..
radvd: CommandFilter, /usr/sbin/radvd, root
# nova/network/linux_net.py: 'brctl', 'addbr', bridge
# nova/network/linux_net.py: 'brctl', 'setfd', bridge, 0
# nova/network/linux_net.py: 'brctl', 'stp', bridge, 'off'
# nova/network/linux_net.py: 'brctl', 'addif', bridge, interface
brctl: CommandFilter, brctl, root
# nova/virt/libvirt/utils.py: 'mkswap'
# nova/virt/xenapi/vm_utils.py: 'mkswap'
mkswap: CommandFilter, /sbin/mkswap, root
# nova/virt/xenapi/vm_utils.py: 'mkfs'
mkfs: CommandFilter, /sbin/mkfs, root
# nova/virt/libvirt/utils.py: 'qemu-img'
qemu-img: CommandFilter, /usr/bin/qemu-img, root
# nova/virt/disk/vfs/localfs.py: 'readlink', '-e'
readlink: CommandFilter, readlink, root
# nova/virt/disk/api.py: 'touch', target
touch: CommandFilter, /usr/bin/touch, root
# nova/virt/disk/api.py:
mkfs.ext3: CommandFilter, /sbin/mkfs.ext3, root
mkfs.ntfs: CommandFilter, /sbin/mkfs.ntfs, root
# nova/virt/libvirt/connection.py:
read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi
# nova/virt/libvirt/connection.py:
lvremove: CommandFilter, /sbin/lvremove, root
# nova/virt/libvirt/utils.py:
lvcreate: CommandFilter, /sbin/lvcreate, root
# nova/virt/libvirt/utils.py:
lvs: CommandFilter, /sbin/lvs, root
# nova/virt/libvirt/utils.py:
vgs: CommandFilter, /sbin/vgs, root
# nova/virt/baremetal/volume_driver.py: 'tgtadm', '--lld', 'iscsi', ...
tgtadm: CommandFilter, /usr/sbin/tgtadm, root
# nova/utils.py:read_file_as_root: 'cat', file_path
# (called from nova/virt/disk/vfs/localfs.py:VFSLocalFS.read_file)
read_passwd: RegExpFilter, cat, root, cat, (/var|/usr)?/tmp/openstack-vfs-localfs[^/]+/etc/passwd
read_shadow: RegExpFilter, cat, root, cat, (/var|/usr)?/tmp/openstack-vfs-localfs[^/]+/etc/shadow
# nova/virt/libvirt/volume.py: 'multipath' '-R'
multipath: CommandFilter, /sbin/multipath, root
# nova/virt/libvirt/utils.py:
systool: CommandFilter, /usr/bin/systool, root
# nova/virt/libvirt/volume.py:
sginfo: CommandFilter, /usr/bin/sginfo, root
sg_scan: CommandFilter, /usr/bin/sg_scan, root
# nova/virt/xenapi/vm_utils.py:
xenstore-read: CommandFilter, /usr/bin/xenstore-read, root
# nova/virt/baremetal/tilera.py: '/usr/sbin/rpc.mountd'
rpc.mountd: CommandFilter, /usr/sbin/rpc.mountd, root
# Rackspace Openvz starts here
# nova/compute/manager.py: 'blockdev', '--getsize64', host_device
blockdev: CommandFilter, /sbin/blockdev, root
# nova/virt/openvz_conn.py: '/usr/sbin/vzlist'
vzlist: CommandFilter, /usr/sbin/vzlist, root
# nova/virt/openvz_conn.py: '/usr/sbin/vzctl'
vzctl: CommandFilter, /usr/sbin/vzctl, root
# nova/virt/openvz_conn.py: '/usr/sbin/arping'
arping_usrsbin: CommandFilter, /usr/sbin/arping, root
# nova/virt/openvz_conn.py: '/bin/rm'
rm: CommandFilter, /bin/rm, root
# nova/virt/openvz_conn.py:
cpuinfo: ReadFileFilter, /proc/cpuinfo
# nova/virt/openvz_conn.py:
meminfo: ReadFileFilter, /proc/meminfo
# nova/virt/openvz_conn.py: '/usr/sbin/vzcpucheck'
vzcpucheck: CommandFilter, /usr/sbin/vzcpucheck, root
# nova/virt/openvz_conn.py: '/bin/rmdir'
rmdir: CommandFilter, /bin/rmdir, root
# nova/virt/openvz/volume_drivers/iscsi.py: '/usr/bin/iscsiadm'
iscsiadm_usrsbin: CommandFilter, /usr/bin/iscsiadm, root
# nova/virt/openvz/utils.py: '/sbin/blkid'
blkid: CommandFilter, /sbin/blkid, root
# nova/virt/openvz/network_plugins/tc.py
tc: CommandFilter, /sbin/tc, root
# nova/virt/openvz/volume.py
ls: CommandFilter, /bin/ls, root
# nova/virt/openvz/volume.py
sfdisk: CommandFilter, /sbin/sfdisk, root
# nova/virt/openvz/volume.py
mknod: CommandFilter, /bin/mknod, root

View File

@ -0,0 +1,20 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from nova.virt.openvz import driver
OpenVzDriver = driver.OpenVzDriver

1915
nova/virt/openvz/driver.py Normal file

File diff suppressed because it is too large Load Diff

221
nova/virt/openvz/file.py Normal file
View File

@ -0,0 +1,221 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
A driver specific to OpenVz as the support for Ovz in libvirt
is sketchy at best.
"""
from nova import exception
from nova.openstack.common import log as logging
from nova.virt.openvz import utils as ovz_utils
import os
from oslo.config import cfg
CONF = cfg.CONF
LOG = logging.getLogger('nova.virt.openvz.file')
class OVZFile(object):
"""
This is a generic file class for wrapping up standard file operations that
may need to run on files associated with OpenVz
"""
def __init__(self, filename, permissions):
self.filename = os.path.abspath(filename)
self.contents = []
self.permissions = permissions
def __enter__(self):
"""
This may feel dirty but we need to be able to read and write files
as a non priviledged user so we need to change the permissions to
be more open for our file operations and then secure them once we
are done.
"""
if not self.exists():
self.make_path()
self.touch()
self.set_permissions(666)
def __exit__(self, _type, value, tb):
if self.exists():
self.set_permissions(self.permissions)
def read(self):
"""
Open the file for reading only and read it's contents into the instance
attribute self.contents
"""
try:
with open(self.filename, 'r') as fh:
self.contents = fh.read().split('\n')
except Exception as err:
LOG.error(_('Output from open: %s') % err)
raise exception.FileNotFound(
_('Failed to read %s') % self.filename)
def write(self):
"""
Because self.contents may or may not get manipulated throughout the
process, this is a method used to dump the contents of self.contents
back into the file that this object represents.
"""
LOG.debug(_('File contents before write: %s') %
'\n'.join(self.contents))
try:
with open(self.filename, 'w') as fh:
fh.writelines('\n'.join(self.contents) + '\n')
except Exception as err:
LOG.error(_('Output from open: %s') % err)
raise exception.FileNotFound(
_('Failed to write %s') % self.filename)
def touch(self):
"""
There are certain conditions where we create an OVZFile object but that
file may or may not exist and this provides us with a way to create
that file if it doesn't exist.
Run the command:
touch <filename>
If this does not run an exception is raised as a failure to touch a
file when you intend to do so would cause a serious failure of
procedure.
"""
self.make_path()
ovz_utils.execute('touch', self.filename, run_as_root=True)
def make_proper_script(self):
"""
OpenVz mount and umount files must be properly formatted scripts.
Prepend the proper shell script header to the files
"""
if not self.contents[:1] == ['#!/bin/sh']:
self.prepend('#!/bin/sh')
def append(self, contents):
"""
Add the argument contents to the end of self.contents
"""
if not isinstance(contents, list):
contents = [str(contents)]
self.contents += contents
LOG.debug(_('File contents: %s') % self.contents)
def prepend(self, contents):
"""
Add the argument contents to the beginning of self.contents
"""
if not isinstance(contents, list):
contents = [str(contents)]
self.contents = contents + self.contents
LOG.debug(_('File contents: %s') % self.contents)
def delete(self, contents):
"""
Delete the argument contents from self.contents if they exist.
"""
if isinstance(contents, list):
LOG.debug(_('A list was passed to delete: %s') % contents)
for line in contents:
LOG.debug(_('Line: %(line)s \tFound in: %(contents)s') %
locals())
self.remove_line(line)
else:
LOG.debug(_('A string was passed to delete: %s') % contents)
self.remove_line(contents)
def remove_line(self, line):
"""
Simple helper method to actually do the removal of a line from an array
"""
LOG.debug(_('Removing line if present: %s') % line)
if line in self.contents:
LOG.debug(_('Line found: %s') % line)
self.contents.remove(line)
LOG.debug(_('Line removed: %(line)s \t%(contents)s') %
{'line': line, 'contents': self.contents})
def set_permissions(self, permissions):
"""
Because nova runs as an unprivileged user we need a way to mangle
permissions on files that may be owned by root for manipulation
Run the command:
chmod <permissions> <filename>
If this doesn't run an exception is raised because the permissions not
being set to what the application believes they are set to can cause
a failure of epic proportions.
"""
ovz_utils.execute('chmod', permissions, self.filename,
run_as_root=True)
def make_path(self, path=None):
"""
Helper method for an OVZFile object to be able to create the path for
self.filename if it doesn't exist before running touch()
"""
if not path:
path = self.filename
basedir = os.path.dirname(path)
ovz_utils.make_dir(basedir)
def set_contents(self, new_contents):
"""
We need the ability to overwrite all contents and this is the cleanest
way while allowing some validation.
"""
if isinstance(new_contents, str):
LOG.debug(_('Type of new_contents is: str'))
self.contents = new_contents.split('\n')
LOG.debug(_('Contents of file: %s') % self.contents)
elif isinstance(new_contents, list):
LOG.debug(_('Type of new_contents is: list'))
self.contents = new_contents
else:
raise TypeError(_("I don't know what to do with this type: %s") %
type(new_contents))
def run_contents(self):
# Because only approved commands in rootwrap can be executed we
# don't need to worry about unwanted command injection
for line in self.contents:
if len(line) > 0:
if line[0] != '#' and line != '\n':
line = line.strip()
try:
LOG.debug(
_('Running line from %(filename)s: %(line)s') %
{'filename': self.filename, 'line': line})
line = line.split()
ovz_utils.execute(*line, run_as_root=True)
except exception.InstanceUnacceptable as err:
LOG.error(_('Cannot execute: %s') % line)
LOG.error(err)
def exists(self):
"""
Simple wrapper for os.path.exists
"""
return os.path.exists(self.filename)

View File

View File

@ -0,0 +1,43 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
OpenVZ doesn't allow for a script to be run after a container has started up
using the host node's context so we are implementing one here
"""
from nova.openstack.common import log as logging
from nova.virt.openvz import file as ovzfile
import os
from oslo.config import cfg
CONF = cfg.CONF
LOG = logging.getLogger('nova.virt.openvz.file_ext.boot')
class OVZBootFile(ovzfile.OVZFile):
def __init__(self, instance_id, permissions):
"""
Child class of OVZFile used to manage the CTID.boot file. This file
will be executed line by line after the container has started but
using the host's context.
:param instance_id: Instance used for the file
"""
filename = "%s/%s.boot" % (CONF.ovz_config_dir, instance_id)
filename = os.path.abspath(filename)
super(OVZBootFile, self).__init__(filename, permissions)

View File

@ -0,0 +1,114 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
A driver specific to OpenVz as the support for Ovz in libvirt
is sketchy at best.
"""
import json
from nova.openstack.common import log as logging
from nova.virt.openvz import file as ovzfile
import os
from oslo.config import cfg
CONF = cfg.CONF
LOG = logging.getLogger('nova.virt.openvz.file_ext.ext_storage')
class OVZExtStorage(object):
"""
Local store for volume information needing to be persisted to the local
host.
"""
def __init__(self, instance_id):
"""
:param instance_id: CTID of the container
:return: None
"""
filename = '%s/%s.ext_storage' % (CONF.ovz_config_dir, instance_id)
filename = os.path.abspath(filename)
self.instance_id = instance_id
self.local_store = ovzfile.OVZFile(filename, 600)
# read the local store for the CTID or create it if it doesn't already
# exist.
with self.local_store:
self.local_store.read()
# preload volume info
self.load_volumes()
def load_volumes(self):
"""
return the contents of self.contents *after* converting it from json
to python objects.
:return: list
"""
try:
self.volumes = json.loads(self.local_store.contents[0])
except (ValueError, IndexError):
self.volumes = dict()
def add_volume(self, device, volume_info, overwrite=True):
"""
Add a volume to local storage so volumes can be reconnected to in
emergencies without nova services if need be.
:param device: Device name
:param volume_info: Nova volume information from block_device_map
:return: None
"""
if device in self.volumes and not overwrite:
msg = (_('You told me not to overwrite volume info for device: '
'%(device)s on instance: %(instnace_id)s') %
{'device': device, 'instance_id': self.instance_id})
LOG.error(msg)
raise KeyError(msg)
self.volumes[device] = volume_info
def remove_volume(self, device):
"""
removes a volume from the volume store.
:param device:
:return: None
"""
try:
self.volumes.pop(device)
LOG.debug(
_('Removed volume %(device)s from instance %(instance_id)s') %
{'device': device, 'instance_id': self.instance_id})
except KeyError:
LOG.error(
_('Volume %(device)s was not in local store for '
'instance %(instance_id)s') %
{'device': device, 'instance_id': self.instance_id})
def save(self):
"""
Flushes contents of self.volumes to disk for persistance
:return: None
"""
self.local_store.set_contents(json.dumps(self.volumes))
with self.local_store:
self.local_store.write()

View File

@ -0,0 +1,44 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
OpenVz doesn't have provision for a script to be run before a container
is stopped but with the host node's context so we are implementing one here.
"""
from nova.openstack.common import log as logging
from nova.virt.openvz import file as ovzfile
import os
from oslo.config import cfg
CONF = cfg.CONF
LOG = logging.getLogger('nova.virt.openvz.file_ext.shutdown')
class OVZShutdownFile(ovzfile.OVZFile):
def __init__(self, instance_id, permissions):
"""
Child class of OVZFile used to manage the CTID.boot file. This file
will be executed line by line after the container has started but
using the host's context.
:param instance_id: Instance used for the file
"""
LOG.debug(_('Beginning OVZShutdownFile'))
filename = "%s/%s.shutdown" % (CONF.ovz_config_dir, instance_id)
filename = os.path.abspath(filename)
LOG.debug(_('OVZShutdownFile: %s') % filename)
super(OVZShutdownFile, self).__init__(filename, permissions)

View File

@ -0,0 +1,33 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
A driver specific to OpenVz as the support for Ovz in libvirt
is sketchy at best.
"""
from nova.virt.openvz import file as ovzfile
import os
from oslo.config import cfg
CONF = cfg.CONF
class OVZContainerStartScript(ovzfile.OVZFile):
def __init__(self, instance_id):
filename = "%s/%s.start" % (CONF.ovz_config_dir, instance_id)
filename = os.path.abspath(filename)
super(OVZContainerStartScript, self).__init__(filename)

View File

@ -0,0 +1,33 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
A driver specific to OpenVz as the support for Ovz in libvirt
is sketchy at best.
"""
from nova.virt.openvz import file as ovzfile
import os
from oslo.config import cfg
CONF = cfg.CONF
class OVZContainerStopScript(ovzfile.OVZFile):
def __init__(self, instance_id):
filename = "%s/%s.stop" % (CONF.ovz_config_dir, instance_id)
filename = os.path.abspath(filename)
super(OVZContainerStopScript, self).__init__(filename)

209
nova/virt/openvz/network.py Normal file
View File

@ -0,0 +1,209 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
A driver specific to OpenVz as the support for Ovz in libvirt
is sketchy at best.
"""
from Cheetah import Template
from nova import exception
from nova.openstack.common import log as logging
from nova.virt.openvz import file as ovzfile
from nova.virt.openvz.file_ext import boot as ovzboot
from nova.virt.openvz.file_ext import shutdown as ovzshutdown
from nova.virt.openvz.network_drivers import tc as ovztc
from nova.virt.openvz import utils as ovz_utils
import os
from oslo.config import cfg
CONF = cfg.CONF
LOG = logging.getLogger('nova.virt.openvz.network')
class OVZNetworkInterfaces(object):
"""
Helper class for managing interfaces in OpenVz
"""
#TODO(imsplitbit): fix this to work with redhat based distros
def __init__(self, interface_info):
"""
Manage the network interfaces for your OpenVz containers.
"""
self.interface_info = interface_info
LOG.debug(_('Interface info: %s') % self.interface_info)
self.boot_file = ovzboot.OVZBootFile(self.interface_info[0]['id'], 755)
with self.boot_file:
self.boot_file.set_contents(list())
self.shutdown_file = ovzshutdown.OVZShutdownFile(
self.interface_info[0]['id'], 755)
with self.shutdown_file:
self.shutdown_file.set_contents(list())
def add(self):
"""
Add all interfaces and addresses to the container.
"""
if CONF.ovz_use_veth_devs:
for net_dev in self.interface_info:
self._add_netif(net_dev['id'], net_dev['name'],
net_dev['bridge'], net_dev['mac'])
tc_rules = ovztc.OVZTcRules()
tc_rules.instance_info(net_dev['id'], net_dev['address'],
net_dev['vz_host_if'])
with self.boot_file:
self.boot_file.append(tc_rules.container_start())
with self.shutdown_file:
self.shutdown_file.append(tc_rules.container_stop())
self._load_template()
self._fill_templates()
else:
for net_dev in self.interface_info:
self._add_ip(net_dev['id'], net_dev['address'])
with self.boot_file:
self.boot_file.write()
with self.shutdown_file:
self.shutdown_file.write()
self._set_nameserver(self.interface_info[0]['id'],
self.interface_info[0]['dns'])
def _load_template(self):
"""
Load templates needed for network interfaces.
"""
if CONF.ovz_use_veth_devs:
# TODO(imsplitbit): make a cheatah template for dhcp networking
self.template = open(CONF.injected_network_template).read()
else:
self.template = None
def _fill_templates(self):
"""
Iterate through each file necessary for creating interfaces on a
given platform, open the file and write the contents of the template
to the file.
"""
for filename in self._filename_factory():
network_file = OVZNetworkFile(filename)
self.iface_file = str(
Template.Template(self.template,
searchList=[
{'interfaces': self.interface_info,
'use_ipv6': CONF.use_ipv6}]))
with network_file:
network_file.append(self.iface_file.split('\n'))
network_file.write()
def _filename_factory(self, variant='debian'):
"""
Generate a path for the file needed to implement an interface
"""
#TODO(imsplitbit): Figure out how to introduce OS hints into nova
# so we can generate support for redhat based distros. This will
# require an os hint to be placed in glance to use for making
# decisions. Then we can create a generator that will generate
# redhat style interface paths like:
#
# /etc/sysconfig/network-scripts/ifcfg-eth0
#
# for now, we just return the debian path.
redhat_path = '/etc/sysconfig/network-scripts/'
debian_path = '/etc/network/interfaces'
prefix = '%(private_dir)s/%(instance_id)s' %\
{'private_dir': CONF.ovz_ve_private_dir,
'instance_id': self.interface_info[0]['id']}
prefix = os.path.abspath(prefix)
#TODO(imsplitbit): fix this placeholder for RedHat compatibility.
if variant == 'redhat':
for net_dev in self.interface_info:
path = prefix + redhat_path + ('ifcfg-%s' % net_dev['name'])
path = os.path.abspath(path)
LOG.debug(_('Generated filename %(path)s') % locals())
yield path
elif variant == 'debian':
path = prefix + debian_path
path = os.path.abspath(path)
LOG.debug(_('Generated filename %(path)s') % locals())
yield path
else:
raise exception.InvalidMetadata(
_('Variant %(variant)s is not known') % locals())
def _add_netif(self, instance_id, netif, bridge, host_mac):
"""
This is a work around to add the eth devices the way OpenVZ
wants them.
When this works, it runs a command similar to this:
vzctl set 1 --save --netif_add \
eth0,,veth1.eth0,11:11:11:11:11:11,br100
"""
host_if = 'veth%s.%s' % (instance_id, netif)
ovz_utils.execute('vzctl', 'set', instance_id, '--save', '--netif_add',
'%s,,%s,%s,%s' % (netif, host_if, host_mac, bridge),
run_as_root=True)
def _add_ip(self, instance_id, ip):
"""
Add an IP address to a container if you are not using veth devices.
Run the command:
vzctl set <ctid> --save --ipadd <ip>
If this fails to run an exception is raised as this indicates a failure
to create a network available connection within the container thus
making it unusable to all but local users and therefore unusable to
nova.
"""
ovz_utils.execute('vzctl', 'set', instance_id, '--save', '--ipadd', ip,
run_as_root=True)
def _set_nameserver(self, instance_id, dns):
"""
Get the nameserver for the assigned network and set it using
OpenVz's tools.
Run the command:
vzctl set <ctid> --save --nameserver <nameserver>
If this fails to run an exception is raised as this will indicate
the container's inability to do name resolution.
"""
ovz_utils.execute('vzctl', 'set', instance_id, '--save',
'--nameserver', dns, run_as_root=True)
class OVZNetworkFile(ovzfile.OVZFile):
"""
An abstraction for network files. This is necessary for multi-platform
support. OpenVz runs on all linux distros and can host all linux distros
but they don't all create interfaces the same way. This should make it
easy to add interface files to all flavors of linux.
"""
def __init__(self, filename):
super(OVZNetworkFile, self).__init__(filename, 644)

View File

@ -0,0 +1,58 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
A driver specific to OpenVz as the support for Ovz in libvirt
is sketchy at best.
"""
from nova.network import linux_net
from nova.openstack.common import log as logging
LOG = logging.getLogger('nova.virt.openvz.network_drivers.network_bridge')
class OVZNetworkBridgeDriver(object):
"""
VIF driver for a Linux Bridge
"""
def plug(self, instance, network, mapping):
"""
Ensure that the bridge exists and add a vif to it.
"""
if (not network.get('should_create_bridge') and
mapping.get('should_create_vlan')):
if mapping.get('should_create_vlan'):
LOG.debug(_('Ensuring bridge %(bridge)s and vlan %(vlan)s') %
{'bridge': network['bridge'],
'vlan': network['vlan']})
linux_net.LinuxBridgeInterfaceDriver.ensure_vlan_bridge(
network['vlan'],
network['bridge'],
network['bridge_interface'])
else:
LOG.debug(_('Ensuring bridge %s') % network['bridge'])
linux_net.LinuxBridgeInterfaceDriver.ensure_bridge(
network['bridge'],
network['bridge_interface'])
def unplug(self, instance, network, mapping):
"""
No manual unplugging required
"""
pass

View File

@ -0,0 +1,267 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from Cheetah import Template
from nova.conductor import api
from nova import context
from nova import exception
from nova.openstack.common import lockutils
from nova.openstack.common import log as logging
from nova.virt.openvz import utils as ovz_utils
import os
from oslo.config import cfg
import random
CONF = cfg.CONF
LOG = logging.getLogger('nova.virt.openvz.network_drivers.tc')
global _ovz_tc_available_ids
global _ovz_tc_inflight_ids
_ovz_tc_available_ids = None
_ovz_tc_inflight_ids = None
class OVZTcRules(object):
# Set our class variables if they don't exist
# TODO(imsplitbit): figure out if there is a more pythonic way to do this
if not _ovz_tc_available_ids:
_ovz_tc_available_ids = list()
if not _ovz_tc_inflight_ids:
_ovz_tc_inflight_ids = list()
def __init__(self):
"""
This class is used to return TC rulesets for both a host and guest for
use with host and guest startup and shutdown.
"""
self.conductor = api.API()
if not len(OVZTcRules._ovz_tc_available_ids):
LOG.debug(_('Available_ids is empty, filling it with numbers'))
OVZTcRules._ovz_tc_available_ids = [
i for i in range(1, CONF.ovz_tc_id_max)
]
# do an initial clean of the available ids
self._remove_used_ids()
LOG.debug(
_('OVZTcRules thinks ovz_tc_host_slave_device is set to %s')
% CONF.ovz_tc_host_slave_device)
def instance_info(self, instance_id, address, vz_iface):
"""
Use this method when creating or resizing an instance. It will
generate a new tc ruleset
:param instance_id: Instance to generate the rules for
:param address: IP address for the instance
:param vz_iface: interface on the hosts bridge that is associated with
the instance
"""
if not instance_id:
self.instance_type = dict()
self.instance_type['memory_mb'] = 2048
else:
admin_context = context.get_admin_context()
self.instance = self.conductor.instance_get(
admin_context, instance_id)
self.instance_type = self.conductor.instance_type_get(
admin_context, self.instance['instance_type_id'])
LOG.debug(_('CT TC address: %s') % address)
self.address = address
self.vz_iface = vz_iface
# Calculate the bandwidth total by figuring out how many units we have
self.bandwidth = int(
round(self.instance_type['memory_mb'] /
CONF.ovz_memory_unit_size)) * CONF.ovz_tc_mbit_per_unit
LOG.debug(_('Allotted bandwidth: %s') % self.bandwidth)
self.tc_id = self._get_instance_tc_id()
if not self.tc_id:
LOG.debug(_('No preassigned tc_id for %s, getting a new one') %
instance_id)
self.tc_id = self.get_id()
self._save_instance_tc_id()
LOG.debug(_('Saved the tc_id in the database for the instance'))
@lockutils.synchronized('get_id_lock', 'openvz-')
def get_id(self):
"""
Uses nova utils decorator @lockutils.synchronized to make sure that we
do not return duplicate available ids. This will return a random id
number between 1 and 9999 which are the limits of TC.
"""
self._remove_used_ids()
LOG.debug(_('Pulling new TC id'))
tc_id = self._pull_id()
LOG.debug(_('TC id %s pulled, testing for dupe') % tc_id)
while tc_id in OVZTcRules._ovz_tc_inflight_ids:
LOG.debug(_('TC id %s inflight already, pulling another') % tc_id)
tc_id = self._pull_id()
LOG.debug(_('TC id %s pulled, testing for dupe') % tc_id)
LOG.debug(_('TC id %s pulled, verified unique'))
self._reserve_id(tc_id)
return tc_id
def container_start(self):
template = self._load_template('tc_container_start.template')
search_list = {
'prio': self.tc_id,
'host_iface': CONF.ovz_tc_host_slave_device,
'vz_iface': self.vz_iface,
'bandwidth': self.bandwidth,
'vz_address': self.address,
'line_speed': CONF.ovz_tc_max_line_speed
}
ovz_utils.save_instance_metadata(self.instance['id'],
'tc_id', self.tc_id)
return self._fill_template(template, search_list).splitlines()
def container_stop(self):
template = self._load_template('tc_container_stop.template')
search_list = {
'prio': self.tc_id,
'host_iface': CONF.ovz_tc_host_slave_device,
'vz_iface': self.vz_iface,
'bandwidth': self.bandwidth,
'vz_address': self.address
}
ovz_utils.save_instance_metadata(self.instance['id'],
'tc_id', self.tc_id)
return self._fill_template(template, search_list).splitlines()
def host_start(self):
template = self._load_template('tc_host_start.template')
search_list = {
'host_iface': CONF.ovz_tc_host_slave_device,
'line_speed': CONF.ovz_tc_max_line_speed
}
return self._fill_template(template, search_list).splitlines()
def host_stop(self):
template = self._load_template('tc_host_stop.template')
search_list = {
'host_iface': CONF.ovz_tc_host_slave_device
}
return self._fill_template(template, search_list).splitlines()
def _load_template(self, template_name):
"""
read and return the template file
"""
full_template_path = '%s/%s' % (
CONF.ovz_tc_template_dir, template_name)
full_template_path = os.path.abspath(full_template_path)
try:
LOG.debug(_('Opening template file: %s') % full_template_path)
template_file = open(full_template_path).read()
LOG.debug(_('Template file opened successfully'))
return template_file
except Exception as err:
LOG.error(_('Unable to open template: %s') % full_template_path)
raise exception.FileNotFound(err)
def _fill_template(self, template, search_list):
"""
Take the vars in search_list and fill out template
"""
return str(Template.Template(template, searchList=[search_list]))
def _pull_id(self):
"""
Pop a random id from the list of available ids for tc rules
"""
return OVZTcRules._ovz_tc_available_ids[random.randint(
0, len(OVZTcRules._ovz_tc_available_ids) - 1)]
def _list_existing_ids(self):
LOG.debug(_('Attempting to list existing IDs'))
out = ovz_utils.execute('tc', 'filter', 'show', 'dev',
CONF.ovz_tc_host_slave_device,
run_as_root=True)
ids = list()
for line in out.splitlines():
line = line.split()
if line[0] == 'filter':
tc_id = int(line[6])
if tc_id not in ids:
ids.append(int(tc_id))
# get the metadata for instances from the database
# this will provide us with any ids of instances that aren't
# currently running so that we can remove active and provisioned
# but inactive ids from the available list
instances_metadata = ovz_utils.read_all_instance_metadata()
LOG.debug(_('Instances metadata: %s') % instances_metadata)
if instances_metadata:
for instance_id, meta in instances_metadata.iteritems():
tc_id = meta.get('tc_id')
if tc_id:
if tc_id not in ids:
LOG.debug(
_('TC id "%(tc_id)s" for instance '
'"%(instance_id)s" found') % locals())
ids.append(int(tc_id))
return ids
def _reserve_id(self, tc_id):
"""
This removes the id from the _ovz_tc_available_ids and adds it to the
_ovz_tc_inflight_ids
"""
LOG.debug(_('Beginning reservation process'))
OVZTcRules._ovz_tc_inflight_ids.append(tc_id)
LOG.debug(_('Added id "%s" to _ovz_tc_inflight_ids') % tc_id)
OVZTcRules._ovz_tc_available_ids.remove(tc_id)
LOG.debug(_('Removed id "%s" from _ovz_tc_available_ids') % tc_id)
def _remove_used_ids(self):
"""
Clean ids that are currently provisioned on the host from the
list of _ovz_tc_available_ids
"""
LOG.debug(_('Beginning cleanup of used ids'))
used_ids = self._list_existing_ids()
LOG.debug(_('Used ids found, removing from _ovz_tc_available_ids'))
for tc_id in used_ids:
if tc_id in OVZTcRules._ovz_tc_available_ids:
OVZTcRules._ovz_tc_available_ids.remove(tc_id)
LOG.debug(_('Removed all ids in use'))
def _save_instance_tc_id(self):
"""
Save the tc id in the instance metadata in the database
"""
ovz_utils.save_instance_metadata(self.instance['id'], 'tc_id',
self.tc_id, fail_on_dupe_key=True)
def _get_instance_tc_id(self):
"""
Look up instance metadata in the db and see if there is already
a tc_id for the instance
"""
instance_metadata = ovz_utils.read_instance_metadata(
self.instance['id'])
LOG.debug(_('Instances metadata: %s') % instance_metadata)
if instance_metadata:
tc_id = instance_metadata.get('tc_id')
LOG.debug(
_('TC id for instance %(instance_id)s is %(tc_id)s') %
{'instance_id': self.instance['id'], 'tc_id': tc_id})
return tc_id

View File

@ -0,0 +1,8 @@
tc qdisc add dev ${vz_iface} root handle 1: htb default 10
tc class add dev ${vz_iface} parent 1: classid 1:1 htb rate ${line_speed}mbit ceil ${line_speed}mbit burst 15k
tc class add dev ${vz_iface} parent 1:1 classid 1:${prio} htb rate ${bandwidth}mbit ceil ${bandwidth}mbit burst 15k
tc qdisc add dev ${vz_iface} parent 1:${prio} handle ${prio}: sfq perturb 10
tc filter replace dev ${vz_iface} protocol ip parent 1: prio ${prio} u32 match ip dst ${vz_address} flowid 1:${prio}
tc class add dev ${host_iface} parent 1:1 classid 1:${prio} htb rate ${bandwidth}mbit ceil ${bandwidth}mbit burst 15k
tc qdisc add dev ${host_iface} parent 1:${prio} handle ${prio}: sfq perturb 10
tc filter replace dev ${host_iface} protocol ip parent 1: prio ${prio} u32 match ip src ${vz_address} flowid 1:${prio}

View File

@ -0,0 +1,6 @@
tc filter del dev ${host_iface} protocol ip parent 1:0 prio ${prio}
tc class del dev ${host_iface} parent 1:1 classid 1:${prio} htb rate ${bandwidth}mbit
tc qdisc delete dev ${host_iface} parent 1:${prio} handle ${prio}
tc filter del dev ${vz_iface} protocol ip parent 1:0 prio ${prio}
tc class del dev ${vz_iface} parent 1:1 classid 1:${prio} htb rate ${bandwidth}mbit
tc qdisc delete dev ${vz_iface} parent 1:${prio} handle ${prio}

View File

@ -0,0 +1,5 @@
# Outgoing traffic control
# Should only be run once at setup or all rules are wiped
tc qdisc del dev ${host_iface} root
tc qdisc add dev ${host_iface} root handle 1: htb default 10
tc class add dev ${host_iface} parent 1: classid 1:1 htb rate ${line_speed}mbit ceil ${line_speed}mbit burst 15k

View File

@ -0,0 +1 @@
tc qdisc del dev ${host_iface} root

616
nova/virt/openvz/utils.py Normal file
View File

@ -0,0 +1,616 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
A driver specific to OpenVz as the support for Ovz in libvirt
is sketchy at best.
"""
import multiprocessing
from nova.conductor import api
from nova import context
from nova import exception
from nova.openstack.common import log as logging
from nova import utils
import os
from oslo.config import cfg
import platform
import re
import sys
import uuid
CONF = cfg.CONF
LOG = logging.getLogger('nova.virt.openvz.utils')
conductor = api.API()
def execute(*cmd, **kwargs):
"""
This is a wrapper for utils.execute so as to reduce the amount of
duplicated code in the driver.
"""
if 'raise_on_error' in kwargs:
raise_on_error = kwargs.pop('raise_on_error')
else:
raise_on_error = True
try:
out, err = utils.execute(*cmd, **kwargs)
if out:
LOG.debug(_('Stdout from %(command)s: %(out)s') %
{'command': cmd[0], 'out': out})
if err:
LOG.debug(_('Stderr from %(command)s: %(out)s') %
{'command': cmd[0], 'out': err})
return out
except exception.ProcessExecutionError as err:
msg = (_('Stderr from %(command)s: %(out)s') %
{'command': cmd[0], 'out': err})
if raise_on_error:
LOG.error(msg)
raise exception.InstanceUnacceptable(
_('Error running %(command)s: %(out)s') %
{'command': cmd[0], 'out': err})
else:
LOG.warn(msg)
return None
def mkfs(path, fs, fs_uuid=None, fs_label=None):
"""Format a file or block device
:param fs: Filesystem type (examples include 'ext3', 'ext4'
'btrfs', etc.)
:param path: Path to file or block device to format
:param fs_uuid: Volume uuid to use
:param fs_label: Volume label to use
"""
if not fs_uuid:
fs_uuid = str(uuid.uuid4())
args = ['mkfs', '-F', '-t', fs]
if fs_uuid:
args.extend(['-U', fs_uuid])
if fs_label:
args.extend(['-L', fs_label])
args.append(path)
LOG.debug(_('Mkfs command: %s') % args)
execute(*args, run_as_root=True)
def get_fs_uuid(device):
"""
Because we may need to operate on a volume by it's uuid
we need a way to see if a filesystem has a uuid on it
"""
LOG.debug(_('Getting FS UUID for: %s') % device)
out = execute('blkid', '-o', 'value', '-s', 'UUID', device,
run_as_root=True, raise_on_error=False)
if out:
LOG.debug(_('Found something in get_fs_uuid'))
for line in out.split('\n'):
line = line.strip()
LOG.debug(_('Examining line: %s') % line)
result = re.search(
'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}',
line)
if result:
LOG.debug(
_('Found the FS UUID for: %(device)s It is: %(uuid)s') %
{'device': device, 'uuid': result.group(0)})
return result.group(0)
return None
def get_vcpu_total():
"""Get vcpu number of physical computer.
:returns: the number of cpu core.
"""
LOG.debug(_('Getting cpu count'))
# On certain platforms, this will raise a NotImplementedError.
try:
cpu_count = multiprocessing.cpu_count()
LOG.debug(_('Got cpu count: %s') % cpu_count)
return cpu_count
except NotImplementedError:
LOG.warn(_("Cannot get the number of cpu, because this "
"function is not implemented for this platform. "
"This error can be safely ignored for now."))
return 0
def get_cpuinfo():
"""
Gather detailed statistics on the host's cpus
"""
if sys.platform.upper() not in ['LINUX2', 'LINUX3']:
return 0
cpuinfo = dict()
cpuinfo['features'] = list()
# Gather the per core/hyperthread info for accumulation
for line in open('/proc/cpuinfo').readlines():
# Do some cleaning up to make searching for keys easier
line = line.split(':')
line[0] = line[0].strip().replace(' ', '_')
if len(line) > 1:
line[1] = line[1].strip()
if line[0] == 'vendor_id':
cpuinfo['vendor'] = line[1]
if line[0] == 'model_name':
cpuinfo['model'] = line[1]
if line[0] == 'flags':
cpuinfo['features'] += line[1].split()
cpuinfo['arch'] = platform.uname()[4]
# dedupe the features
cpuinfo['features'] = list(set(cpuinfo['features']))
return cpuinfo
def get_iscsi_initiator():
"""
This is a basic implementation of the iscsi stuff needed for external
volumes. This should live in a utility module when the openvz driver is
broken up.
"""
# code borrowed from libvirt utils
contents = utils.read_file_as_root('/etc/iscsi/initiatorname.iscsi')
for l in contents.split('\n'):
if l.startswith('InitiatorName='):
return l[l.index('=') + 1:].strip()
def get_cpuunits_capability():
"""
Use openvz tools to discover the total processing capability of the
host node. This is done using the vzcpucheck utility.
Run the command:
vzcpucheck
If this fails to run an exception is raised because the output of this
method is required to calculate the overall bean count available on the
host to be carved up for guests to use.
"""
result = {}
out = execute('vzcpucheck', run_as_root=True)
for line in out.splitlines():
line = line.split()
if len(line) > 0:
if line[0] == 'Power':
LOG.debug(_('Power of host: %s') % line[4])
result['total'] = int(line[4])
elif line[0] == 'Current':
LOG.debug(_('Current cpuunits subscribed: %s') % line[3])
result['subscribed'] = int(line[3])
if len(result.keys()) == 2:
return result
else:
raise exception.InvalidCPUInfo(
_("Cannot determine the CPUUNITS for host"))
def get_vcpu_used():
"""
OpenVz doesn't have the concept of VCPUs but we can approximate
what they would be by looking at the percentage of subscribed
cpuunits.
:returns: The total number of vcpu that currently used.
"""
cpuunits = get_cpuunits_capability()
cpus = get_vcpu_total()
pct_cpuunits_used = float(cpuunits['subscribed']) / cpuunits['total']
return int(cpus * pct_cpuunits_used)
def get_memory_mb_total():
"""Get the total memory size(MB) of physical computer.
:returns: the total amount of memory(MB).
"""
if sys.platform.upper() not in ['LINUX2', 'LINUX3']:
return 1
meminfo = open('/proc/meminfo').read().split()
idx = meminfo.index(u'MemTotal:')
# transforming kb to mb.
return int(meminfo[idx + 1]) / 1024
def get_memory_mb_used(instance_id=None, block_size=4096):
"""
Get the free memory size(MB) of physical computer.
:returns: the total committed of memory(MB).
"""
if instance_id:
cmd = "vzlist -H -o ctid,privvmpages.l %s" % instance_id
else:
cmd = "vzlist --all -H -o ctid,privvmpages.l"
cmd = cmd.split()
total_used_mb = 0
out = execute(*cmd, run_as_root=True, raise_on_error=False)
if out:
for line in out.splitlines():
line = line.split()
total_used_mb += ((int(line[1]) * block_size) / 1024 ** 2)
return total_used_mb
def get_local_gb(path):
"""
Get the total hard drive space at <path>
"""
hddinfo = os.statvfs(path)
total = hddinfo.f_frsize * hddinfo.f_blocks
free = hddinfo.f_frsize * hddinfo.f_bavail
used = hddinfo.f_frsize * (hddinfo.f_blocks - hddinfo.f_bfree)
return {'total': total, 'free': free, 'used': used}
def get_local_gb_total():
"""
Get the total hdd size(GB) of physical computer.
:returns:
The total amount of HDD(GB).
Note that this value shows a partition where
OVZ_VE_PRIVATE_DIR is.
"""
return get_local_gb(CONF.ovz_ve_private_dir)['total'] / (1024 ** 3)
def get_local_gb_used():
"""
Get the total used disk space on the host computer.
:returns:
The total amount of HDD(GB)
Note that this value show a partition where
OVZ_VE_PRIVATE_DIR is.
"""
return get_local_gb(CONF.ovz_ve_private_dir)['used'] / (1024 ** 3)
def get_hypervisor_type():
"""
Just return the type of hypervisor we are using. This will always
be 'openvz'
"""
return 'openvz'
def get_hypervisor_version():
"""
Since the version of the hypervisor is really determined by the kernel
it is running lets just return that for version. I don't think it
matters at this point and this is the most accurate representation of
what *version* we are running.
"""
return platform.uname()[2]
def make_dir(path):
"""
This is the method that actually creates directories. This is used by
make_path and can be called directly as a utility to create
directories.
Run the command:
mkdir -p <path>
If this doesn't run an exception is raised as this path creation is
required to ensure that other file operations are successful. Such
as creating the path for a file yet to be created.
"""
if not os.path.exists(path):
execute('mkdir', '-p', path, run_as_root=True)
def delete_path(path):
"""
After a volume has been detached and the mount statements have been
removed from the mount configuration for a container we want to remove
the paths created on the host system so as not to leave orphaned files
and directories on the system.
This runs a command like:
sudo rmdir /mnt/100/var/lib/mysql
"""
try:
execute('rmdir', path, run_as_root=True)
return True
except exception.InstanceUnacceptable:
return False
def set_permissions(filename, permissions):
"""
Because nova runs as an unprivileged user we need a way to mangle
permissions on files that may be owned by root for manipulation
Run the command:
chmod <permissions> <filename>
If this doesn't run an exception is raised because the permissions not
being set to what the application believes they are set to can cause
a failure of epic proportions.
"""
execute('chmod', permissions, filename, run_as_root=True)
def format_system_metadata(metadata):
"""
System metadata returned by conductor contains data that is irrelevant to
OpenVz and the nova driver. This method sanitizes this information down
to key / value pairs in a list
:param metadata: instance['system_metadata']
:return: dict
"""
fmt_meta = dict()
if metadata:
for item in metadata:
meta_item = dict()
for key, value in item.iteritems():
if key in ['key', 'value']:
meta_item[key] = value
fmt_meta[meta_item['key']] = meta_item['value']
return fmt_meta
def save_instance_metadata(instance_id, key, value, fail_on_dupe_key=False):
"""
Simple wrapper for saving data to the db as metadata for an instance
:param instance_id: Instance id to add metadata to
:param key: Key to save to db
:param value: Value of key to save to db
"""
LOG.debug(
_('Beginning saving instance metadata for '
'%(instance_id)s: {%(key)s: %(value)s}') % locals())
admin_context = context.get_admin_context()
LOG.debug(_('Got admin context'))
try:
instance = conductor.instance_get(admin_context, instance_id)
LOG.debug(_('Fetched instance: %s') % instance)
LOG.debug(_('Fixing system_metadata'))
sysmeta = format_system_metadata(instance['system_metadata'])
if key in sysmeta:
if fail_on_dupe_key:
raise KeyError(_('Key %(key)s is set already') % locals())
sysmeta[key] = value
conductor.instance_update(
admin_context, instance['uuid'], system_metadata=sysmeta)
LOG.debug(_('Updated instance metadata'))
except exception.InstanceNotFound as err:
LOG.error(_('Instance not in the database: %s') % instance_id)
LOG.error(_('Consistency check is needed, orphaned instance: %s') %
instance_id)
def read_instance_metadata(instance_id):
"""
Read the metadata in the database for a given instance
:param instance_id: Instance to read all metadata for from the db
:returns: dict()
"""
LOG.debug(_('Beginning read_instance_metadata: %s') % instance_id)
admin_context = context.get_admin_context()
LOG.debug(_('Got admin context for db access'))
try:
instance = conductor.instance_get(admin_context, instance_id)
LOG.debug(_('Fetched instance'))
LOG.debug(
_('Results from read_instance_metadata: %s') %
instance['system_metadata'])
return format_system_metadata(instance['system_metadata'])
except exception.InstanceNotFound:
LOG.error(_('Instance not in the database: %s') % instance_id)
LOG.error(_('Consistency check is needed, orphaned instance: %s') %
instance_id)
return dict()
def read_all_instance_metadata():
"""
Fetch a list of all instance ids on the local host and for each get
the associated metadata and return it in a dictionary
:returns dict():
"""
instances_metadata = {}
out = execute(
'vzlist', '-H', '-o', 'ctid', '--all', raise_on_error=False,
run_as_root=True)
if out:
for line in out.splitlines():
instance_id = line.strip()
instances_metadata[instance_id] = read_instance_metadata(
instance_id)
return instances_metadata
def remove_instance_metadata_key(instance_id, key):
"""
Remove a specific key from an instance's metadata
"""
LOG.debug(_('Getting an admin context for remove_instance_metadata_key'))
admin_context = context.get_admin_context()
LOG.debug(_('Got admin context, fetching instance ref'))
instance = conductor.instance_get(admin_context, instance_id)
LOG.debug(_('Got instance ref, beginning deletion of metadata'))
sysmeta = format_system_metadata(instance['system_metadata'])
if key in sysmeta:
LOG.debug(_('Key %s exists in system_metadata') % key)
sysmeta.pop(key)
conductor.instance_update(
admin_context, instance['uuid'], system_metadata=sysmeta)
else:
LOG.debug(_('Key %s does not exist in system_metadata') % key)
def remove_instance_metadata(instance_id):
"""
Clean up instance metadata for an instance
"""
openvz_metadata_keys = ('tc_id',)
try:
for key in openvz_metadata_keys:
LOG.debug(_('Removing metadata key: %s') % key)
remove_instance_metadata_key(instance_id, key)
LOG.debug(_('Deleted metadata for %(instance_id)s') % locals())
return True
except exception.InstanceNotFound as err:
LOG.error(_('Instance not in the database: %s') % instance_id)
LOG.error(_('Consistency check is needed, orphaned instance: %s') %
instance_id)
return False
def generate_network_dict(instance_id, network_info):
interfaces = list()
interface_num = -1
for (network, mapping) in network_info:
if mapping['ips']:
interface_num += 1
#TODO(imsplitbit): make this work for ipv6
address_v6 = None
gateway_v6 = None
netmask_v6 = None
if CONF.use_ipv6:
address_v6 = mapping['ip6s'][0]['ip']
netmask_v6 = mapping['ip6s'][0]['netmask']
gateway_v6 = mapping['gateway6']
interface_info = {
'id': instance_id,
'interface_number': interface_num,
'bridge': network['bridge'],
'name': 'eth%d' % interface_num,
'mac': mapping['mac'],
'address': mapping['ips'][0]['ip'],
'netmask': mapping['ips'][0]['netmask'],
'gateway': mapping['gateway'],
'broadcast': mapping['broadcast'],
'dns': ' '.join(mapping['dns']),
'address_v6': address_v6,
'gateway_v6': gateway_v6,
'netmask_v6': netmask_v6
}
interface_info['vz_host_if'] = ('veth%s.%s' %
(interface_info['id'],
interface_info['name']))
interfaces.append(interface_info)
return interfaces
def move(src, dest):
"""
utility method to wrap up moving a file or directory from one place to
another.
"""
try:
execute('mv', src, dest, run_as_root=True)
return True
except exception.InstanceUnacceptable as err:
LOG.error(_('Error moving file %s') % src)
LOG.error(err)
return False
def copy(src, dest):
"""
utility method wrap up copying files from one directory to another
"""
try:
execute('cp', src, dest, run_as_root=True)
return True
except exception.InstanceUnacceptable as err:
LOG.error(_('Error copying file %s') % src)
LOG.error(err)
return False
def tar(source, target_file, working_dir=None, skip_list=[]):
"""
wrapper for tar for making backup archives
"""
cmd = ['tar', '-cf', target_file]
if working_dir:
cmd += ['-C', working_dir]
if skip_list:
for exclude_path in skip_list:
cmd += ['--exclude', '%s/*' % exclude_path]
cmd.append(source)
try:
execute(*cmd, run_as_root=True)
return True
except exception.InstanceUnacceptable as err:
LOG.error(_('Error tarring: %s') % source)
LOG.error(err)
return False
def untar(target_file, destination):
"""
wrapper for untarring files into a destination directory
"""
try:
execute('tar', '-xf', target_file, '-C', destination, run_as_root=True)
return True
except exception.InstanceUnacceptable as err:
LOG.error(_('Error untarring: %s') % target_file)
LOG.error(err)
return False

473
nova/virt/openvz/volume.py Normal file
View File

@ -0,0 +1,473 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
A driver specific to OpenVz as the support for Ovz in libvirt
is sketchy at best.
"""
import glob
from nova import exception
from nova.openstack.common import log as logging
from nova.virt.openvz import file as ovzfile
from nova.virt.openvz import utils as ovz_utils
import os
from oslo.config import cfg
import re
import time
CONF = cfg.CONF
CONF.register_opt(
cfg.IntOpt('ovz_volume_settle_num_tries',
default=10,
help='Number of attempts before we determine that a device '
'has had time to settle and we are beyond a reasonable '
'wait time'))
LOG = logging.getLogger('nova.virt.openvz.volume')
class OVZVolume(object):
"""
This Class is a helper class to manage the mount and umount files
for a given container.
"""
def __init__(self, instance_id, mountpoint, dev):
"""
Setup critical information for attaching/detaching volumes from
instance_id.
:param instance_id:
:param mountpoint: connection_info['data']['mount_device']
:param dev:
"""
#TODO(imsplitbit): Need to make this happen while supporting block
# devices and externally mounted filesystems at the same time.
# Currently it does not.
self.instance_id = instance_id
self.mountpoint = mountpoint
self.device = dev
def attach(self):
"""
Public method for attaching volumes either by creating bind mounts or
attaching a raw device to the instance.
"""
self._attach_raw_devices()
def detach(self):
"""
Public method for detaching volumes from an instance.
"""
self._detach_raw_devices()
def device_name(self):
"""
Generates an device name. If no volume driver is used this will
assist in generating an error should things get this far.
"""
return
def _check_device_exists(self, device_path):
"""Check that the device path exists.
Verify that the device path has actually been created and can report
it's size, only then can it be available for formatting, retry
num_tries to account for the time lag.
"""
try:
ovz_utils.execute('blockdev', '--getsize64', device_path,
attempts=CONF.ovz_system_num_tries,
run_as_root=True)
except exception.ProcessExecutionError:
raise exception.InvalidDevicePath(path=device_path)
def _find_device(self):
try:
return os.path.realpath(self.device_name())
except (OSError, AttributeError) as err:
raise exception.VolumeNotFound(
_('Device cannot be found %s') % err)
def _list_host_devices(self):
dev = self._find_device()
LOG.debug(_('Device name: %(dev)s') % locals())
devs = list()
listing = glob.glob('/dev/sd*')
for device in listing:
LOG.debug(_('Trying to match against %(device)s') % locals())
m = re.match('^(?P<device>%s)(?P<part>\\d*)$' % dev, device)
if m:
LOG.debug(_('Matched device %(device)s') % locals())
devs.append(m.string)
LOG.debug(_('Devices: %(devs)s') % locals())
return devs
def _get_major_minor(self, device):
try:
dev = os.stat(device)
maj_min = dict()
maj_min['major'] = os.major(dev.st_rdev)
maj_min['minor'] = os.minor(dev.st_rdev)
LOG.debug(
_('Major Minor numbers for %(dev)s: %(maj_min)s') % locals())
return maj_min
except OSError as err:
LOG.error(_('Device error in OpenVz volumes: %(err)s') % locals())
raise exception.VolumeNotFound(
_('Unable to find device: %(device)s') % locals())
def _let_disk_settle(self):
"""
Check to see if the device_name is available in /dev/disk/by-path. If
it isn't then we'll wait a bit and retry. Disks don't just instantly
show up in /dev so this gives a little bit of wait time for things
to show up.
:return:
"""
ovz_utils.execute(
'ls', self.device_name(), run_as_root=True,
attempts=CONF.ovz_volume_settle_num_tries, delay_on_retry=True)
def _list_partition_table(self):
"""
There is a bit of work done here to make a nice partition table
structure for later use. Currently the only thing we care about is
if there is already a partition table but we may need this info soon
so the legwork is done.
:return: dict
"""
# Now we should be able to move on to goodness.
dev = self._find_device()
LOG.debug(_('Device name: %(dev)s') % locals())
part_table = dict()
try:
out = ovz_utils.execute('sfdisk', '-d', dev, run_as_root=True)
for line in out.splitlines():
line = line.split(':')
dev = line[0].strip()
LOG.debug(_('Device from part table: %(dev)s') % locals())
if dev.startswith('/dev'):
line = line[1].split(',')
LOG.debug(_('Line from part table: %(line)s') % locals())
part_table[dev] = {}
part_table[dev]['start'] = line[0].split('=')[1]
part_table[dev]['size'] = line[1].split('=')[1]
part_table[dev]['id'] = line[2].split('=')[1]
LOG.debug(
_('Device info from part table: %s') % part_table[dev])
if len(line) > 3:
part_table[dev]['flags'] = line[3]
LOG.debug(_('Disk flags exist for %(dev)s') % locals())
LOG.debug(_('Partition Table: %(part_table)s') % locals())
return part_table
except exception.InstanceUnacceptable as err:
LOG.warn(_('No partition table on %(dev)s') % locals())
return part_table
def _partition_disk(self):
"""
Openvz requires that we pass in not only the root device but if the
user is to access partitions on that disk we need also to pass in
the major/minor numbers for each partition. Given that requirement
we're making the assumption for now that volumes are going to be
presented as one partition. This will partition the root device so
we can later get the information on both the major and minor numbers.
If the disk contains a valid partition table then we just return
:return:
"""
# We need potentially need to give the device time to settle
self._let_disk_settle()
dev = self._find_device()
part_table = self._list_partition_table()
LOG.debug(_('Partition table for %(dev)s: %(part_table)s') % locals())
# If the volume is partitioned we're assuming this is a reconnect of
# a volume and we won't partition anything.
if not part_table:
commands = ['o,w', 'n,p,1,,,t,1,83,w']
for command in commands:
command = command.replace(',', '\n')
ovz_utils.execute(
'fdisk', dev, process_input=command, run_as_root=True)
time.sleep(3)
def _get_vz_device_list(self):
"""
Grab existing device information from vz config and create new
device string for device connections.
:return:
"""
devices = list()
config = os.path.normpath(
'%s/%s.conf' % (CONF.ovz_config_dir, self.instance_id))
config = ovzfile.OVZFile(config, 644)
config.read()
for line in config.contents:
if '=' in line:
line = line.split('=')
if line[0] == 'DEVICES':
devices = line[1].replace('\"', '').strip().split()
return devices
def _ovz_block_dev_name_generator(self, major, minor, privs):
"""
OpenVz requires a specific format for granting or revoking block
device privileges. This tool will create those names.
:param major:
:param minor:
:param privs:
:return:
"""
return 'b:%(major)s:%(minor)s:%(privs)s' % locals()
def _attach_raw_devices(self):
"""
If there are no partitions on the disk already, partition it with 1
partition. Then use openvz tools and mknod to attach these devices
to the container.
:return:
"""
LOG.debug(_('Partitioning disk in _attach_raw_devices'))
self._partition_disk()
# storage for block device major/minor pairs for setting in
# the container config file.
bdevs = []
for dev in self._list_host_devices():
maj_min = self._get_major_minor(dev)
bdevs.append(
self._ovz_block_dev_name_generator(
maj_min['major'], maj_min['minor'], 'rw'))
m = re.match('^(?P<device>/[a-z]+/[a-z]+)(?P<part>\\d*)$', dev)
device_name = self.mountpoint
if m and m.group('part'):
device_name = '%s%s' % (self.mountpoint, m.group('part'))
# Remove the device if it happens to exist. This prevents stale
# devices with old major/minor numbers existing.
self._delete_device(device_name)
self._mknod(maj_min['major'], maj_min['minor'], device_name)
if bdevs:
self._add_block_devices(bdevs)
def _attach_raw_device(self, major, minor):
ovz_utils.execute(
'vzctl', 'set', self.instance_id,
'--devices', 'b:%s:%s:rw' % (major, minor),
run_as_root=True)
def _add_block_devices(self, new_devices):
"""
New devices should be a list that looks like this:
['b:8:32:rw', 'b:8:33:rw']
:param new_devices:
:return:
"""
devices = self._get_vz_device_list()
for dev in new_devices:
if dev not in devices:
devices.append(dev)
self._set_block_devices(devices)
def _del_block_devices(self, new_devices):
"""
New devices should be a list that looks like this:
['b:8:32:rw', 'b:8:33:rw']
:param new_devices:
:return:
"""
devices = self._get_vz_device_list()
for dev in new_devices:
if dev in devices:
devices.remove(dev)
self._set_block_devices(devices)
def _detach_remaining_block_devices(self):
"""
If the final ephemeral volume is detached from the container we issue
a privs :none for the remaining devices in the conf file for the
container before we remove those entries from the conf file. This is
necessary to ensure the removal of the devices from the in memory
device list for openvz.
:return:
"""
devices = self._get_vz_device_list()
cmd = ['vzctl', 'set', self.instance_id]
for dev in devices:
dev = dev.replace(':rw', ':none')
cmd.append('--devices')
cmd.append(dev)
ovz_utils.execute(*cmd, run_as_root=True)
def _set_block_devices(self, devices):
"""
Run the command necessary to save the block device permissions to
the container config file so that they persist on reboot.
:param devices:
:return:
"""
cmd = ['vzctl', 'set', self.instance_id, '--save']
if devices:
for dev in devices:
cmd.append('--devices')
cmd.append(dev)
ovz_utils.execute(*cmd, run_as_root=True)
else:
# if the device list is empty this means that it's the last device
# attached to the container and we need to run some special case
# code to remove that device.
self._detach_remaining_block_devices()
self._remove_all_device_entries()
def _remove_all_device_entries(self):
"""
When there are no devices left to remove we need to remove the
DEVICES line from the CTID.conf file. This cannot be achieved by
a vzctl command so the file has to be manipulated and saved.
:return: None
"""
filename = '%s/%s.conf' % (CONF.ovz_config_dir, self.instance_id)
filename = os.path.normpath(filename)
ct_conf = ovzfile.OVZFile(filename, 644)
ct_conf.read()
with ct_conf:
for line in ct_conf.contents:
if "DEVICES" in line:
ct_conf.contents.remove(line)
ct_conf.write()
def _detach_raw_devices(self):
"""
Remove the associated devices from the container.
:return:
"""
LOG.debug(_('Detaching raw devices'))
# storage for block device major/minor pairs for setting in
# the container config file.
bdevs_conf = list()
for dev in self._list_host_devices():
maj_min = self._get_major_minor(dev)
bdevs_conf.append(
self._ovz_block_dev_name_generator(
maj_min['major'], maj_min['minor'], 'rw'))
# TODO(imsplitbit): research to see if running this remove command
# adhoc is necessary or if just removing the device from the config
# file is adequate.
self._detach_raw_device(maj_min['major'], maj_min['minor'])
m = re.match('^(?P<device>/[a-z]+/[a-z]+)(?P<part>\\d*)$', dev)
device_name = self.mountpoint
if m and m.group('part'):
device_name = '%s%s' % (self.mountpoint, m.group('part'))
self._delete_device(device_name)
if bdevs_conf:
self._del_block_devices(bdevs_conf)
def _detach_raw_device(self, major, minor):
"""
Remove perms from the container for a device based on major and minor
numbers.
:param major:
:param minor:
:return:
"""
ovz_utils.execute(
'vzctl', 'set', self.instance_id, '--devices',
'b:%s:%s:none' % (major, minor), run_as_root=True)
def _mknod(self, major, minor, device_name, dev_type='b'):
"""
Because openvz doesn't support device aliases we manually create
a block device sharing the same major and minor numbers as the device
on the host. This device should be created in the openvz chrooted
/dev directory so as to allow it to be cleaned up upon deletion.
:param major:
:param minor:
:param dev_type:
:return: None
"""
good_types = ['b']
if dev_type not in good_types:
raise TypeError(
_('Cannot make device of type "%(dev_type)s"') % locals())
device_path = '%s/%s/%s' % (
CONF.ovz_ve_private_dir, self.instance_id, device_name)
device_path = os.path.normpath(device_path)
ovz_utils.execute(
'mknod', device_path, dev_type, major, minor, run_as_root=True)
def _delete_device(self, device):
"""
Utility method for removing a device from inside the container. This
is to happen before you create a device because the existing device
may or may not have the proper major/minor numbers. This is obviously
done on volume detach as well.
:param device:
:return:
"""
device = '%s/%s/%s' % (
CONF.ovz_ve_private_dir, self.instance_id, device)
device = os.path.normpath(device)
ovz_utils.execute('rm', '-f', device, run_as_root=True)

View File

@ -0,0 +1,164 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Rackspace
# 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.
"""
A driver specific to OpenVz as the support for Ovz in libvirt
is sketchy at best.
"""
from nova import exception
from nova.openstack.common import lockutils
from nova.openstack.common import log as logging
from nova.virt.openvz import utils as ovz_utils
from nova.virt.openvz import volume as ovzvolume
from oslo.config import cfg
CONF = cfg.CONF
CONF.register_opt(
cfg.IntOpt('ovz_iscsiadm_num_tries',
default=1,
help='Number of attempts to make an iscsi connection'))
LOG = logging.getLogger('nova.virt.openvz.volume_drivers.iscsi')
class OVZISCSIStorageDriver(ovzvolume.OVZVolume):
def __init__(self, instance_id, mountpoint, connection_data):
"""
Driver for openvz to connect iscsi volumes to instances
:param connection_data: Data from the connection_info given to the
ovz driver
:param instance_id: Instance ID for the volume to be attached to
:param mountpoint: place to mount the volume within the container
"""
self.iscsi_properties = connection_data['data']
super(OVZISCSIStorageDriver, self).__init__(
instance_id, mountpoint, self.device_name())
self.init_iscsi_device()
def device_name(self):
"""
Generates an device name based on the iSCSI parameters in the
block device mapping
"""
return "/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" % \
(self.iscsi_properties['target_portal'],
self.iscsi_properties['target_iqn'],
self.iscsi_properties['target_lun'])
def init_iscsi_device(self):
"""
Log into the device and setup files
"""
self.discover_volume()
def _run_iscsiadm(self, iscsi_command, raise_on_error=True):
out = ovz_utils.execute('iscsiadm', '-m', 'node', '-T',
self.iscsi_properties['target_iqn'],
'-p', self.iscsi_properties['target_portal'],
*iscsi_command, run_as_root=True,
attempts=CONF.ovz_iscsiadm_num_tries,
raise_on_error=raise_on_error)
LOG.debug("iscsiadm %s: stdout=%s" %
(iscsi_command, out))
return out
def _iscsiadm_update(self, property_key, property_value,
raise_on_error=True):
iscsi_command = ('--op', 'update', '-n', property_key,
'-v', property_value)
return self._run_iscsiadm(iscsi_command, raise_on_error=raise_on_error)
def get_iscsi_properties_for_volume(self):
if not self.iscsi_properties['target_discovered']:
self._run_iscsiadm(('--op', 'new'))
def session_currently_connected(self):
out = ovz_utils.execute(
'iscsiadm', '-m', 'session', run_as_root=True,
raise_on_error=False)
if out:
for line in out.splitlines():
if self.iscsi_properties['target_iqn'] in line:
return True
else:
return False
def set_iscsi_auth(self):
if self.iscsi_properties.get('auth_method', None):
self._iscsiadm_update("node.session.auth.authmethod",
self.iscsi_properties['auth_method'])
self._iscsiadm_update("node.session.auth.username",
self.iscsi_properties['auth_username'])
self._iscsiadm_update("node.session.auth.password",
self.iscsi_properties['auth_password'])
@lockutils.synchronized('iscsiadm_lock', 'openvz-')
def discover_volume(self):
"""Discover volume on a remote host."""
self.get_iscsi_properties_for_volume()
self.set_iscsi_auth()
try:
if not self.session_currently_connected():
LOG.debug(_('iSCSI session for %s not connected, '
'connecting now') %
self.iscsi_properties['target_iqn'])
self._run_iscsiadm(("--login",))
LOG.debug(_('iSCSI session for %s connected') %
self.iscsi_properties['target_iqn'])
except exception.ProcessExecutionError as err:
if "15 - already exists" in err.message:
raise exception.VolumeUnattached()
LOG.error(err)
raise exception.VolumeNotFound(_("iSCSI device %s not found") %
self.iscsi_properties['target_iqn'])
def detach(self, raise_on_error=True):
"""Detach the volume from instance_name."""
super(OVZISCSIStorageDriver, self).detach()
self._detach_iscsi()
@lockutils.synchronized('iscsiadm_lock', 'openvz-')
def _detach_iscsi(self):
"""
Detach all iscsi sessions for this volume
:return:
"""
self._iscsiadm_update("node.startup", "manual")
self._run_iscsiadm(("--logout",), raise_on_error=False)
self._run_iscsiadm(('--op', 'delete'), raise_on_error=False)
@lockutils.synchronized('iscsiadm_lock', 'openvz-')
def rescan(self):
"""Rescan the client storage connection."""
self.get_iscsi_properties_for_volume()
try:
LOG.debug("ISCSI Properties: %s" % self.iscsi_properties)
self._run_iscsiadm(("--rescan",))
return ("/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" %
(self.iscsi_properties['target_portal'],
self.iscsi_properties['target_iqn'],
self.iscsi_properties['target_lun']))
except exception.ProcessExecutionError as err:
LOG.error(err)
raise exception.ISCSITargetNotFoundForVolume(
_("Error rescanning iscsi device: %s") %
self.iscsi_properties['target_iqn'])