ironic/nova/virt/baremetal/volume_driver.py

269 lines
9.4 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# coding=utf-8
# Copyright (c) 2012 NTT DOCOMO, INC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import re
from oslo.config import cfg
from nova import context as nova_context
from nova import exception
from nova.openstack.common import importutils
from nova.openstack.common import log as logging
from nova import utils
from nova.virt.baremetal import db as bmdb
from nova.virt.libvirt import utils as libvirt_utils
opts = [
cfg.BoolOpt('use_unsafe_iscsi',
default=False,
help='Do not set this out of dev/test environments. '
'If a node does not have a fixed PXE IP address, '
'volumes are exported with globally opened ACL'),
cfg.StrOpt('iscsi_iqn_prefix',
default='iqn.2010-10.org.openstack.baremetal',
help='iSCSI IQN prefix used in baremetal volume connections.'),
]
baremetal_group = cfg.OptGroup(name='baremetal',
title='Baremetal Options')
CONF = cfg.CONF
CONF.register_group(baremetal_group)
CONF.register_opts(opts, baremetal_group)
CONF.import_opt('host', 'nova.netconf')
CONF.import_opt('use_ipv6', 'nova.netconf')
CONF.import_opt('libvirt_volume_drivers', 'nova.virt.libvirt.driver')
LOG = logging.getLogger(__name__)
def _get_baremetal_node_by_instance_uuid(instance_uuid):
context = nova_context.get_admin_context()
return bmdb.bm_node_get_by_instance_uuid(context, instance_uuid)
def _create_iscsi_export_tgtadm(path, tid, iqn):
utils.execute('tgtadm', '--lld', 'iscsi',
'--mode', 'target',
'--op', 'new',
'--tid', tid,
'--targetname', iqn,
run_as_root=True)
utils.execute('tgtadm', '--lld', 'iscsi',
'--mode', 'logicalunit',
'--op', 'new',
'--tid', tid,
'--lun', '1',
'--backing-store', path,
run_as_root=True)
def _allow_iscsi_tgtadm(tid, address):
utils.execute('tgtadm', '--lld', 'iscsi',
'--mode', 'target',
'--op', 'bind',
'--tid', tid,
'--initiator-address', address,
run_as_root=True)
def _delete_iscsi_export_tgtadm(tid):
try:
utils.execute('tgtadm', '--lld', 'iscsi',
'--mode', 'logicalunit',
'--op', 'delete',
'--tid', tid,
'--lun', '1',
run_as_root=True)
except exception.ProcessExecutionError:
pass
try:
utils.execute('tgtadm', '--lld', 'iscsi',
'--mode', 'target',
'--op', 'delete',
'--tid', tid,
run_as_root=True)
except exception.ProcessExecutionError:
pass
# Check if the tid is deleted, that is, check the tid no longer exists.
# If the tid dose not exist, tgtadm returns with exit_code 22.
# utils.execute() can check the exit_code if check_exit_code parameter is
# passed. But, regardless of whether check_exit_code contains 0 or not,
# if the exit_code is 0, the function dose not report errors. So we have to
# catch a ProcessExecutionError and test its exit_code is 22.
try:
utils.execute('tgtadm', '--lld', 'iscsi',
'--mode', 'target',
'--op', 'show',
'--tid', tid,
run_as_root=True)
except exception.ProcessExecutionError as e:
if e.exit_code == 22:
# OK, the tid is deleted
return
raise
raise exception.NovaException(_(
'baremetal driver was unable to delete tid %s') % tid)
def _show_tgtadm():
out, _ = utils.execute('tgtadm', '--lld', 'iscsi',
'--mode', 'target',
'--op', 'show',
run_as_root=True)
return out
def _list_backingstore_path():
out = _show_tgtadm()
l = []
for line in out.split('\n'):
m = re.search(r'Backing store path: (.*)$', line)
if m:
if '/' in m.group(1):
l.append(m.group(1))
return l
def _get_next_tid():
out = _show_tgtadm()
last_tid = 0
for line in out.split('\n'):
m = re.search(r'^Target (\d+):', line)
if m:
tid = int(m.group(1))
if last_tid < tid:
last_tid = tid
return last_tid + 1
def _find_tid(iqn):
out = _show_tgtadm()
pattern = r'^Target (\d+): *' + re.escape(iqn)
for line in out.split('\n'):
m = re.search(pattern, line)
if m:
return int(m.group(1))
return None
def _get_iqn(instance_name, mountpoint):
mp = mountpoint.replace('/', '-').strip('-')
iqn = '%s:%s-%s' % (CONF.baremetal.iscsi_iqn_prefix,
instance_name,
mp)
return iqn
class VolumeDriver(object):
def __init__(self, virtapi):
super(VolumeDriver, self).__init__()
self.virtapi = virtapi
self._initiator = None
def get_volume_connector(self, instance):
if not self._initiator:
self._initiator = libvirt_utils.get_iscsi_initiator()
if not self._initiator:
LOG.warn(_('Could not determine iscsi initiator name '
'for instance %s') % instance)
return {
'ip': CONF.my_ip,
'initiator': self._initiator,
'host': CONF.host,
}
def attach_volume(self, connection_info, instance, mountpoint):
raise NotImplementedError()
def detach_volume(self, connection_info, instance, mountpoint):
raise NotImplementedError()
class LibvirtVolumeDriver(VolumeDriver):
"""The VolumeDriver deligates to nova.virt.libvirt.volume."""
def __init__(self, virtapi):
super(LibvirtVolumeDriver, self).__init__(virtapi)
self.volume_drivers = {}
for driver_str in CONF.libvirt_volume_drivers:
driver_type, _sep, driver = driver_str.partition('=')
driver_class = importutils.import_class(driver)
self.volume_drivers[driver_type] = driver_class(self)
def _volume_driver_method(self, method_name, connection_info,
*args, **kwargs):
driver_type = connection_info.get('driver_volume_type')
if driver_type not in self.volume_drivers:
raise exception.VolumeDriverNotFound(driver_type=driver_type)
driver = self.volume_drivers[driver_type]
method = getattr(driver, method_name)
return method(connection_info, *args, **kwargs)
def attach_volume(self, connection_info, instance, mountpoint):
node = _get_baremetal_node_by_instance_uuid(instance['uuid'])
ctx = nova_context.get_admin_context()
pxe_ip = bmdb.bm_pxe_ip_get_by_bm_node_id(ctx, node['id'])
if not pxe_ip:
if not CONF.baremetal.use_unsafe_iscsi:
raise exception.NovaException(_(
'No fixed PXE IP is associated to %s') % instance['uuid'])
mount_device = mountpoint.rpartition("/")[2]
self._volume_driver_method('connect_volume',
connection_info,
mount_device)
device_path = connection_info['data']['device_path']
iqn = _get_iqn(instance['name'], mountpoint)
tid = _get_next_tid()
_create_iscsi_export_tgtadm(device_path, tid, iqn)
if pxe_ip:
_allow_iscsi_tgtadm(tid, pxe_ip['address'])
else:
# NOTE(NTTdocomo): Since nova-compute does not know the
# instance's initiator ip, it allows any initiators
# to connect to the volume. This means other bare-metal
# instances that are not attached the volume can connect
# to the volume. Do not set CONF.baremetal.use_unsafe_iscsi
# out of dev/test environments.
# TODO(NTTdocomo): support CHAP
_allow_iscsi_tgtadm(tid, 'ALL')
def detach_volume(self, connection_info, instance, mountpoint):
mount_device = mountpoint.rpartition("/")[2]
try:
iqn = _get_iqn(instance['name'], mountpoint)
tid = _find_tid(iqn)
if tid is not None:
_delete_iscsi_export_tgtadm(tid)
else:
LOG.warn(_('detach volume could not find tid for %s') % iqn)
finally:
self._volume_driver_method('disconnect_volume',
connection_info,
mount_device)
def get_all_block_devices(self):
"""
Return all block devices in use on this node.
"""
return _list_backingstore_path()