Files
os-brick/os_brick/initiator/connectors/lightos.py
Takashi Kajinami 5832f18d25 Use oslo.utils to strip scope from an IPv6 address
oslo.utils provides the utility method to remove scope from an IPv6
address since 7.3.0[1].

[1] 076aaac87f6478a95bfb156bd674dfd0558f6782

Change-Id: I9870590fdc62c48a3de4baae38dadf8ba1090769
2024-12-19 00:26:54 +00:00

376 lines
14 KiB
Python

# Copyright (C) 2016-2022 Lightbits Labs Ltd.
# Copyright (C) 2020 Intel Corporation
# 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 glob
import http.client
import os
import re
import tempfile
import time
import traceback
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
from oslo_utils import netutils
import psutil
from os_brick import exception
from os_brick.i18n import _
from os_brick.initiator.connectors import base
from os_brick.privileged import lightos as priv_lightos
from os_brick import utils
DEVICE_SCAN_ATTEMPTS_DEFAULT = 5
DISCOVERY_CLIENT_PORT = 6060
LOG = logging.getLogger(__name__)
nvmec_pattern = ".*nvme[0-9]+[cp][0-9]+.*"
nvmec_match = re.compile(nvmec_pattern)
class LightOSConnector(base.BaseLinuxConnector):
"""Connector class to attach/detach LightOS volumes using NVMe/TCP."""
WAIT_DEVICE_TIMEOUT = 60
def __init__(self,
root_helper,
driver=None,
execute=None,
device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
message_queue=None,
*args,
**kwargs):
super(LightOSConnector, self).__init__(
root_helper,
driver=driver,
execute=execute,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
self.message_queue = message_queue
self.DISCOVERY_DIR_PATH = '/etc/discovery-client/discovery.d/'
@staticmethod
def get_ip_addresses():
"""Find all IPs for the host machine, return list of IP addresses."""
def get_ip_addresses_psutil():
ip_addresses = []
for interface, snics in psutil.net_if_addrs().items():
for snic in snics:
# Collect each (interface, address) tuple
ip_addresses.append((interface, snic.address))
return ip_addresses
loop_back = ['lo']
ips = []
is_ipv6_enabled = netutils.is_ipv6_enabled()
iface_with_ips = get_ip_addresses_psutil()
for iface_ip_tuple in iface_with_ips:
iface = iface_ip_tuple[0]
ip = iface_ip_tuple[1]
if iface in loop_back:
continue
if ip == "":
continue
if is_ipv6_enabled and netutils.is_valid_ipv6(ip):
ips.append(netutils.get_noscope_ipv6(ip))
elif netutils.is_valid_ipv4(ip):
ips.append(ip)
return ips
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The LightOS connector properties."""
props = {}
lightos_connector = LightOSConnector(root_helper=root_helper,
message_queue=None,
execute=kwargs.get('execute'))
hostnqn = utils.get_host_nqn()
found_dsc = lightos_connector.find_dsc()
host_ips = lightos_connector.get_ip_addresses()
LOG.info('Current host hostNQN %s and IP(s) are %s ', hostnqn,
host_ips)
if not found_dsc:
LOG.debug('LIGHTOS: did not find dsc, continuing anyway.')
if hostnqn:
LOG.debug("LIGHTOS: finally hostnqn: %s dsc: %s",
hostnqn, found_dsc)
props['nqn'] = hostnqn
props['found_dsc'] = found_dsc
props['host_ips'] = host_ips
else:
LOG.debug('LIGHTOS: no hostnqn found.')
return props
def dsc_file_name(self, uuid):
return os.path.join(self.DISCOVERY_DIR_PATH, "%s.conf" % uuid)
def find_dsc(self):
conn = http.client.HTTPConnection("localhost", DISCOVERY_CLIENT_PORT)
try:
conn.request("HEAD", "/metrics")
resp = conn.getresponse()
return 'found' if resp.status == http.client.OK else ''
except Exception as e:
LOG.debug('LIGHTOS: %s', e)
out = ''
return out
def dsc_need_connect(self, connection_info):
return not os.path.isfile(self.dsc_file_name(connection_info['uuid']))
def dsc_connect_volume(self, connection_info):
if not self.dsc_need_connect(connection_info):
return
subsysnqn = connection_info['subsysnqn']
uuid = connection_info['uuid']
hostnqn = utils.get_host_nqn()
with tempfile.NamedTemporaryFile(mode='w', delete=False) as dscfile:
dscfile.write('# os_brick connector dsc file for LightOS'
' volume: {}\n'.format(uuid))
for (ip, node) in connection_info['lightos_nodes'].items():
transport = node['transport_type']
host = node['target_portal']
port = node['target_port']
dscfile.write('-t {} -a {} -s {} -q {} -n {}\n'.format(
transport, host, port, hostnqn, subsysnqn))
dscfile.flush()
try:
dest_name = self.dsc_file_name(uuid)
priv_lightos.move_dsc_file(dscfile.name, dest_name)
except Exception:
LOG.warning(
"LIGHTOS: Failed to create dsc file for connection with "
"uuid:%s", uuid)
raise
def dsc_disconnect_volume(self, connection_info):
uuid = connection_info['uuid']
try:
priv_lightos.delete_dsc_file(self.dsc_file_name(uuid))
except Exception:
LOG.warning("LIGHTOS: Failed delete dsc file uuid:%s", uuid)
raise
def monitor_db(self, lightos_db):
for connection_info in lightos_db.values():
self.dsc_connect_volume(connection_info)
def monitor_message_queue(self, message_queue, lightos_db):
while not message_queue.empty():
msg = message_queue.get()
op, connection = msg
LOG.debug("LIGHTOS: queue got op: %s, connection: %s",
op, connection)
if op == 'delete':
LOG.info("LIGHTOS: Removing volume: %s from db",
connection['uuid'])
if connection['uuid'] in lightos_db:
del lightos_db[connection['uuid']]
else:
LOG.warning("LIGHTOS: No volume: %s found in db",
connection['uuid'])
elif op == 'add':
LOG.info("LIGHTOS: Adding volume: %s to db",
connection['uuid'])
lightos_db[connection['uuid']] = connection
def lightos_monitor(self, lightos_db, message_queue):
'''Bookkeeping lightos connections.
This is useful when the connector is comming up to a running node with
connected volumes already exists.
This is used in the Nova driver to restore connections after reboot
'''
first_time = True
while True:
self.monitor_db(lightos_db)
# give us some time before trying to access the MQ
# for the first time
if first_time:
time.sleep(5)
first_time = False
else:
time.sleep(1)
self.monitor_message_queue(message_queue, lightos_db)
# This is part of our abstract interface
def get_search_path(self):
return '/dev'
# This is part of our abstract interface
def get_volume_paths(self, connection_properties):
path = connection_properties['device_path']
return [path]
def _check_device_exists_using_dev_lnk(self, uuid):
lnk_path = f"/dev/disk/by-id/nvme-uuid.{uuid}"
if os.path.exists(lnk_path):
devname = os.path.realpath(lnk_path)
if devname.startswith("/dev/nvme"):
LOG.info("LIGHTOS: devpath %s detected for uuid %s",
devname, uuid)
return devname
return None
def _check_device_exists_reading_block_class(self, uuid):
file_path = "/sys/class/block/*/wwid"
wwid = "uuid." + uuid
for match_path in glob.glob(file_path):
try:
with open(match_path, "r") as f:
match_wwid = f.readline()
except Exception:
LOG.warning("LIGHTOS: Failed to read file %s",
match_path)
continue
if wwid != match_wwid.strip():
continue
# skip slave nvme devices, for example: nvme0c0n1
if nvmec_match.match(match_path.split("/")[-2]):
continue
LOG.info("LIGHTOS: matching uuid %s was found"
" for device path %s", uuid, match_path)
return os.path.join("/dev", match_path.split("/")[-2])
return None
@utils.trace
def _get_device_by_uuid(self, uuid):
endtime = time.time() + self.WAIT_DEVICE_TIMEOUT
while time.time() < endtime:
try:
device = self._check_device_exists_using_dev_lnk(uuid)
if device:
return device
except Exception as e:
LOG.debug('LIGHTOS: %s', e)
device = self._check_device_exists_reading_block_class(uuid)
if device:
return device
time.sleep(1)
return None
def _get_size_by_uuid(self, uuid):
devpath = self._get_device_by_uuid(uuid)
devname = devpath.split("/")[-1]
try:
size_path_name = os.path.join("/sys/class/block/", devname, "size")
with open(size_path_name, "r") as f:
size_blks = f.read().strip()
bytesize = int(size_blks) * 512
return bytesize
except Exception:
LOG.warning("LIGHTOS: Could not find the size at for"
" uuid %s in %s", uuid, devpath)
return None
@utils.trace
@utils.connect_volume_prepare_result
@base.synchronized('volume_op')
def connect_volume(self, connection_properties):
"""Discover and attach the volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
connection_properties must include:
nqn - NVMe subsystem name to the volume to be connected
target_port - NVMe target port that hosts the nqn sybsystem
target_portal - NVMe target ip that hosts the nqn sybsystem
:type connection_properties: dict
:returns: dict
"""
device_info = {'type': 'block'}
uuid = connection_properties['uuid']
LOG.info("LIGHTOS: connect_volume called for volume %s, connection"
" properties: %s",
uuid, connection_properties)
self.dsc_connect_volume(connection_properties)
device_path = self._get_device_by_uuid(uuid)
if not device_path:
msg = _("Device with uuid %s did not show up" % uuid)
priv_lightos.delete_dsc_file(self.dsc_file_name(uuid))
raise exception.BrickException(message=msg)
device_info['path'] = device_path
# bookkeeping lightos connections - add connection
if self.message_queue:
self.message_queue.put(('add', connection_properties))
return device_info
@utils.trace
@base.synchronized('volume_op')
@utils.connect_volume_undo_prepare_result(unlink_after=True)
def disconnect_volume(self, connection_properties, device_info,
force=False, ignore_errors=False):
"""Disconnect a volume from the local host.
The connection_properties are the same as from connect_volume.
The device_info is returned from connect_volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
:param force: Whether to forcefully disconnect even if flush fails.
:type force: bool
:param ignore_errors: When force is True, this will decide whether to
ignore errors or raise an exception once finished
the operation. Default is False.
:type ignore_errors: bool
"""
# bookkeeping lightos connections - delete connection
if self.message_queue:
self.message_queue.put(('delete', connection_properties))
uuid = connection_properties['uuid']
LOG.debug('LIGHTOS: disconnect_volume called for volume %s', uuid)
device_path = self._get_device_by_uuid(uuid)
exc = exception.ExceptionChainer()
try:
if device_path:
self._linuxscsi.flush_device_io(device_path)
except putils.ProcessExecutionError as e:
exc.add_exception(type(e), e, traceback.format_exc())
if not (force or ignore_errors):
raise
try:
self.dsc_disconnect_volume(connection_properties)
except Exception as e:
exc.add_exception(type(e), e, traceback.format_exc())
if exc:
if not ignore_errors:
raise exc
@utils.trace
@base.synchronized('volume_op')
@utils.connect_volume_undo_prepare_result
def extend_volume(self, connection_properties):
uuid = connection_properties['uuid']
new_size = self._get_size_by_uuid(uuid)
return new_size