diff -Nru ceph-iscsi-3.4/ceph_iscsi_config/client.py ceph-iscsi-3.5/ceph_iscsi_config/client.py --- ceph-iscsi-3.4/ceph_iscsi_config/client.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/ceph_iscsi_config/client.py 2021-04-12 09:24:20.000000000 +0000 @@ -404,8 +404,7 @@ except RTSLibError as err: self.error = True self.error_msg = ("Unable to configure authentication " - "for {} - ".format(self.iqn, - err)) + "for {} - {}".format(self.iqn, err)) self.logger.error("(Client.configure_auth) failed to set " "credentials for {}".format(self.iqn)) else: diff -Nru ceph-iscsi-3.4/ceph_iscsi_config/common.py ceph-iscsi-3.5/ceph_iscsi_config/common.py --- ceph-iscsi-3.4/ceph_iscsi_config/common.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/ceph_iscsi_config/common.py 2021-04-12 09:24:20.000000000 +0000 @@ -75,9 +75,11 @@ lock_time_limit = 30 - def __init__(self, logger, cfg_name='gateway.conf', pool=None): + def __init__(self, logger, cfg_name=None, pool=None): self.logger = logger self.config_name = cfg_name + if self.config_name is None: + self.config_name = settings.config.gateway_conf if pool is None: pool = settings.config.pool self.pool = pool @@ -617,7 +619,7 @@ del current_config[txn.type] else: self.error = True - self.error_msg = "Unknown transaction type ({}} encountered in " \ + self.error_msg = "Unknown transaction type ({}) encountered in " \ "_commit_rbd".format(txn.action) if not self.error: diff -Nru ceph-iscsi-3.4/ceph_iscsi_config/device_status.py ceph-iscsi-3.5/ceph_iscsi_config/device_status.py --- ceph-iscsi-3.4/ceph_iscsi_config/device_status.py 1970-01-01 00:00:00.000000000 +0000 +++ ceph-iscsi-3.5/ceph_iscsi_config/device_status.py 2021-04-12 09:24:20.000000000 +0000 @@ -0,0 +1,162 @@ +import json +import rados +import threading +import time +from datetime import datetime + +import ceph_iscsi_config.settings as settings + + +class StatusCounter(object): + def __init__(self, name, cnt): + self.name = name + self.cnt = cnt + self.last_cnt = cnt + + +class TcmuDevStatusTracker(object): + def __init__(self, image_name): + self.image_name = image_name + self.gw_counter_lookup = {} + self.lock_owner = "" + self.lock_owner_timestamp = None + self.state = "Online" + self.changed_state = False + self.stable_cnt = 0 + + def get_status_dict(self): + status = {} + + status['state'] = self.state + status['lock_owner'] = self.lock_owner + status['gateways'] = {} + + for gw, stat_cnt_dict in self.gw_counter_lookup.items(): + status['gateways'][gw] = {} + + for name, stat_cnt in stat_cnt_dict.items(): + status['gateways'][gw][name] = stat_cnt.cnt + + return status + + def check_for_degraded_state(self, stat_cnt): + if stat_cnt.name in ["cmd_timed_out_cnt", "conn_lost_cnt"]: + if abs(stat_cnt.cnt - stat_cnt.last_cnt) >= 1: + self.state = "Degraded - cluster access failure" + self.changed_state = True + self.stable_cnt = 0 + return + + if stat_cnt.name == "lock_lost_cnt" and \ + abs(stat_cnt.cnt - stat_cnt.last_cnt) >= \ + settings.config.lock_lost_cnt_threshhold: + self.state = "Degraded - excessive failovers" + self.changed_state = True + self.stable_cnt = 0 + return + + def update_status(self, gw, status, status_stamp): + if status is None: + # Sometimes status calls will return empty statuses even though + # there is valid data. We might not see it until the Nth call. + return + + counter_dict = self.gw_counter_lookup.get(gw) + if counter_dict is None: + counter_dict = {} + + for name, val in status.items(): + if name == "lock_owner" and val == "true": + dt = datetime.strptime(status_stamp, "%Y-%m-%dT%H:%M:%S.%f%z") + if self.lock_owner_timestamp is None or dt > self.lock_owner_timestamp: + self.lock_owner_timestamp = dt + self.lock_owner = gw + self.stable_cnt = 0 + continue + + if name not in ["cmd_timed_out_cnt", "conn_lost_cnt", "lock_lost_cnt"]: + continue + + stat_cnt = counter_dict.get(name) + if stat_cnt is None: + stat_cnt = StatusCounter(name, int(val)) + + stat_cnt.cnt = int(val) + # TODO: + # If we detect a degraded state, we can throttle the path here. + self.check_for_degraded_state(stat_cnt) + stat_cnt.last_cnt = stat_cnt.cnt + + counter_dict[name] = stat_cnt + + self.gw_counter_lookup[gw] = counter_dict + + +class DeviceStatusWatcher(threading.Thread): + def __init__(self, logger): + threading.Thread.__init__(self) + self.logger = logger + self.daemon = True + self.cluster = None + self.status_lookup = {} + + def get_dev_status(self, image_name): + return self.status_lookup.get(image_name) + + def exit(self): + if self.cluster: + self.cluster.shutdown() + + def run(self): + self.cluster = rados.Rados(conffile=settings.config.cephconf, + name=settings.config.cluster_client_name) + self.cluster.connect() + + while True: + time.sleep(settings.config.status_check_interval) + + cmd = json.dumps({"prefix": "service status", "format": "json"}) + + ret, outb, outs = self.cluster.mgr_command(cmd, b'') + if ret != 0: + self.logger.error("mgr command failed {}".format(ret)) + continue + + svc = json.loads(outb).get('tcmu-runner') + if svc is None: + self.logger.warning("there is no tcmu-runner data avaliable") + self.state = "Unknown" + continue + + image_names_dict = {} + for daemon, daemon_info in svc.items(): + gw, image_name = daemon.split(":", 1) + image_names_dict[image_name] = image_name + + dev_status = self.get_dev_status(image_name) + if dev_status is None: + dev_status = TcmuDevStatusTracker(image_name) + self.status_lookup[image_name] = dev_status + + dev_status.update_status(gw, daemon_info.get('status'), + daemon_info.get('status_stamp')) + + # cleanup stale entries and try to move to online if a dev + # didn't not see any errors on every gateway for a while + for image_name, dev_status in self.status_lookup.items(): + if image_names_dict.get(image_name) is None: + del self.status_lookup[image_name] + else: + if dev_status.changed_state is False: + dev_status.stable_cnt += 1 + + if dev_status.stable_cnt > settings.config.stable_state_reset_count: + dev_status.stable_cnt = 0 + dev_status.state = "Online" + else: + dev_status.changed_state = False + + # debugging info + dev_status = self.get_dev_status(image_name) + stats_dict = dev_status.get_status_dict() + self.logger.debug(stats_dict) diff -Nru ceph-iscsi-3.4/ceph_iscsi_config/gateway.py ceph-iscsi-3.5/ceph_iscsi_config/gateway.py --- ceph-iscsi-3.4/ceph_iscsi_config/gateway.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/ceph_iscsi_config/gateway.py 2021-04-12 09:24:20.000000000 +0000 @@ -26,68 +26,88 @@ else: self.hostname = this_host() - def ceph_rm_blacklist(self, blacklisted_ip): + def _run_ceph_cmd(self, cmd, stderr=None, shell=True): + if not stderr: + stderr = subprocess.STDOUT + try: + result = subprocess.check_output(cmd, stderr=stderr, shell=shell) + except subprocess.CalledProcessError as err: + return None, err + return result, None + + def ceph_rm_blocklist(self, blocklisted_ip): """ - Issue a ceph osd blacklist rm command for a given IP on this host - :param blacklisted_ip: IP address (str - dotted quad) + Issue a ceph osd blocklist rm command for a given IP on this host + :param blocklisted_ip: IP address (str - dotted quad) :return: boolean for success of the rm operation """ - self.logger.info("Removing blacklisted entry for this host : " - "{}".format(blacklisted_ip)) + self.logger.info("Removing blocklisted entry for this host : " + "{}".format(blocklisted_ip)) conf = settings.config - result = subprocess.check_output("ceph -n {client_name} --conf {cephconf} " - "osd blacklist rm {blacklisted_ip}". - format(blacklisted_ip=blacklisted_ip, - client_name=conf.cluster_client_name, - cephconf=conf.cephconf), - stderr=subprocess.STDOUT, shell=True) - if ("un-blacklisting" in result) or ("isn't blacklisted" in result): - self.logger.info("Successfully removed blacklist entry") - return True - else: - self.logger.critical("blacklist removal failed. Run" + result, err = self._run_ceph_cmd( + "ceph -n {client_name} --conf {cephconf} osd blocklist rm " + "{blocklisted_ip}".format(blocklisted_ip=blocklisted_ip, + client_name=conf.cluster_client_name, + cephconf=conf.cephconf)) + if err: + result, err = self._run_ceph_cmd( + "ceph -n {client_name} --conf {cephconf} osd blacklist rm " + "{blocklisted_ip}".format(blocklisted_ip=blocklisted_ip, + client_name=conf.cluster_client_name, + cephconf=conf.cephconf)) + + if err: + self.logger.critical("blocklist removal failed: {}. Run" " 'ceph -n {client_name} --conf {cephconf} " - "osd blacklist rm {blacklisted_ip}'". - format(blacklisted_ip=blacklisted_ip, + "osd blocklist rm {blocklisted_ip}'". + format(err.output.decode('utf-8').strip(), + blocklisted_ip=blocklisted_ip, client_name=conf.cluster_client_name, cephconf=conf.cephconf)) return False - def osd_blacklist_cleanup(self): + self.logger.info("Successfully removed blocklist entry") + return True + + def osd_blocklist_cleanup(self): """ - Process the osd's to see if there are any blacklist entries for this + Process the osd's to see if there are any blocklist entries for this node - :return: True, blacklist entries removed OK, False - problems removing - a blacklist + :return: True, blocklist entries removed OK, False - problems removing + a blocklist """ - self.logger.info("Processing osd blacklist entries for this node") + self.logger.info("Processing osd blocklist entries for this node") cleanup_state = True conf = settings.config - try: - - # NB. Need to use the stderr override to catch the output from - # the command - blacklist = subprocess.check_output("ceph -n {client_name} --conf {cephconf} " - "osd blacklist ls" - .format(client_name=conf.cluster_client_name, - cephconf=conf.cephconf), - shell=True, - stderr=subprocess.STDOUT) - except subprocess.CalledProcessError: - self.logger.critical("Failed to run 'ceph -n {client_name} --conf {cephconf} " - "osd blacklist ls'. Please resolve manually..." - .format(client_name=conf.cluster_client_name, - cephconf=conf.cephconf)) + # NB. Need to use the stderr override to catch the output from + # the command + blocklist, err = self._run_ceph_cmd( + "ceph -n {client_name} --conf {cephconf} osd blocklist ls". + format(client_name=conf.cluster_client_name, + cephconf=conf.cephconf)) + + if err: + blocklist, err = self._run_ceph_cmd( + "ceph -n {client_name} --conf {cephconf} osd blacklist ls". + format(client_name=conf.cluster_client_name, + cephconf=conf.cephconf)) + + if err: + self.logger.critical( + "Failed to run 'ceph -n {client_name} --conf {cephconf} " + "osd blocklist ls'. Please resolve manually..." + .format(client_name=conf.cluster_client_name, + cephconf=conf.cephconf)) cleanup_state = False else: - blacklist_output = blacklist.decode('utf-8').split('\n')[:-1] - if len(blacklist_output) > 1: + blocklist_output = blocklist.decode('utf-8').split('\n')[:-1] + if len(blocklist_output) > 1: # We have entries to look for, so first build a list of ipv4 # addresses on this node @@ -96,23 +116,28 @@ dev_info = netifaces.ifaddresses(iface).get(netifaces.AF_INET, []) ipv4_list += [dev['addr'] for dev in dev_info] - # process the entries (first entry just says "Listed X entries, - # last entry is just null) - for blacklist_entry in blacklist_output[1:]: + # process the entries. last entry is just null) + for blocklist_entry in blocklist_output: + + # blocklist_output is not gauranteed to be in order returned + # from the ceph command. 'listed N entries' line could be + # at any index. + if "listed" in blocklist_entry: + continue # valid entries to process look like - # 192.168.122.101:0/3258528596 2016-09-28 18:23:15.307227 - blacklisted_ip = blacklist_entry.split(':')[0] - # Look for this hosts ipv4 address in the blacklist + blocklisted_ip = blocklist_entry.split(':')[0] + # Look for this hosts ipv4 address in the blocklist - if blacklisted_ip in ipv4_list: + if blocklisted_ip in ipv4_list: # pass in the ip:port/nonce - rm_ok = self.ceph_rm_blacklist(blacklist_entry.split(' ')[0]) + rm_ok = self.ceph_rm_blocklist(blocklist_entry.split(' ')[0]) if not rm_ok: cleanup_state = False break else: - self.logger.info("No OSD blacklist entries found") + self.logger.info("No OSD blocklist entries found") return cleanup_state diff -Nru ceph-iscsi-3.4/ceph_iscsi_config/gateway_setting.py ceph-iscsi-3.5/ceph_iscsi_config/gateway_setting.py --- ceph-iscsi-3.4/ceph_iscsi_config/gateway_setting.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/ceph_iscsi_config/gateway_setting.py 2021-04-12 09:24:20.000000000 +0000 @@ -71,7 +71,7 @@ return str(norm_val) def normalize(self, raw_val): - return raw_val.split(',') if raw_val else [] + return [r.strip() for r in raw_val.split(',')] if raw_val else [] class StrSetting(Setting): @@ -183,6 +183,7 @@ "debug": BoolSetting("debug", False), "minimum_gateways": IntSetting("minimum_gateways", 1, 9999, 2), "ceph_config_dir": StrSetting("ceph_config_dir", '/etc/ceph'), + "gateway_conf": StrSetting("gateway_conf", 'gateway.conf'), "priv_key": StrSetting("priv_key", 'iscsi-gateway.key'), "pub_key": StrSetting("pub_key", 'iscsi-gateway-pub.key'), "prometheus_exporter": BoolSetting("prometheus_exporter", True), @@ -190,6 +191,9 @@ "prometheus_host": StrSetting("prometheus_host", "::"), "logger_level": IntSetting("logger_level", logging.DEBUG, logging.CRITICAL, logging.DEBUG), + "log_to_stderr": BoolSetting("log_to_stderr", False), + "log_to_stderr_prefix": StrSetting("log_to_stderr_prefix", ""), + "log_to_file": BoolSetting("log_to_file", True), # TODO: This is under sys for compat. It is not settable per device/backend # type yet. "alua_failover_type": EnumSetting("alua_failover_type", @@ -200,3 +204,8 @@ "qfull_timeout": IntSetting("qfull_timeout", 0, 600, 5), "osd_op_timeout": IntSetting("osd_op_timeout", 0, 600, 30), "hw_max_sectors": IntSetting("hw_max_sectors", 1, 8192, 1024)} + +TCMU_DEV_STATUS_SETTINGS = { + "lock_lost_cnt_threshhold": IntSetting("lock_lost_cnt_threshhold", 1, 1000000, 12), + "status_check_interval": IntSetting("status_check_interval", 1, 600, 10), + "stable_state_reset_count": IntSetting("stable_state_reset_count", 1, 600, 3)} diff -Nru ceph-iscsi-3.4/ceph_iscsi_config/lun.py ceph-iscsi-3.5/ceph_iscsi_config/lun.py --- ceph-iscsi-3.4/ceph_iscsi_config/lun.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/ceph_iscsi_config/lun.py 2021-04-12 09:24:20.000000000 +0000 @@ -406,17 +406,17 @@ self.config.update_item("targets", target_iqn, target_config) # determine which host was the path owner - disk_owner = self.config.config['disks'][self.config_key]['owner'] - - # update the active_luns count for gateway that owned this - # lun - gw_metadata = self.config.config['gateways'][disk_owner] - if gw_metadata['active_luns'] > 0: - gw_metadata['active_luns'] -= 1 - - self.config.update_item('gateways', - disk_owner, - gw_metadata) + disk_owner = self.config.config['disks'][self.config_key].get('owner') + if disk_owner: + # update the active_luns count for gateway that owned this + # lun + gw_metadata = self.config.config['gateways'][disk_owner] + if gw_metadata['active_luns'] > 0: + gw_metadata['active_luns'] -= 1 + + self.config.update_item('gateways', + disk_owner, + gw_metadata) disk_metadata = self.config.config['disks'][self.config_key] if 'owner' in disk_metadata: @@ -986,8 +986,8 @@ if mode in mode_vars: if not all(x in kwargs for x in mode_vars[mode]): return ("{} request must contain the following " - "variables: ".format(mode, - ','.join(mode_vars[mode]))) + "variables: {}".format(mode, + ','.join(mode_vars[mode]))) else: return "disk operation mode '{}' is invalid".format(mode) diff -Nru ceph-iscsi-3.4/ceph_iscsi_config/metrics.py ceph-iscsi-3.5/ceph_iscsi_config/metrics.py --- ceph-iscsi-3.4/ceph_iscsi_config/metrics.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/ceph_iscsi_config/metrics.py 2021-04-12 09:24:20.000000000 +0000 @@ -6,7 +6,7 @@ from rtslib_fb.root import RTSRoot from rtslib_fb.utils import fread -from .utils import this_host +from ceph_iscsi_config.utils import this_host, CephiSCSIInval class Metric(object): @@ -98,7 +98,11 @@ def _get_tpg(self): stat = Metric("target portal groups defined within gateway group", "gauge") - labels = {"gw_iqn": next(self._root.targets).wwn} + tgt = next(self._root.targets, None) + if tgt is None: + raise CephiSCSIInval("No targets setup.") + + labels = {"gw_iqn": tgt.wwn} v = len([tpg for tpg in self._root.tpgs]) stat.add(labels, v) @@ -107,8 +111,8 @@ def _get_mapping(self): mapping = Metric("LUN mapping state 0=unmapped, 1=mapped", "gauge") - mapped_devices = [l.tpg_lun.storage_object.name - for l in self._root.mapped_luns] + mapped_devices = [lun.tpg_lun.storage_object.name + for lun in self._root.mapped_luns] tpg_mappers = [] for tpg in self._root.tpgs: @@ -119,6 +123,9 @@ for mapper in tpg_mappers: mapper.join() + if not tpg_mappers: + raise CephiSCSIInval("Target not mapped to gateway.") + # merge the tpg lun maps all_devs = tpg_mappers[0].owned_luns.copy() for mapper in tpg_mappers[1:]: diff -Nru ceph-iscsi-3.4/ceph_iscsi_config/settings.py ceph-iscsi-3.5/ceph_iscsi_config/settings.py --- ceph-iscsi-3.4/ceph_iscsi_config/settings.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/ceph_iscsi_config/settings.py 2021-04-12 09:24:20.000000000 +0000 @@ -8,10 +8,12 @@ import hashlib import json +import rados import re from ceph_iscsi_config.gateway_setting import (TGT_SETTINGS, SYS_SETTINGS, - TCMU_SETTINGS) + TCMU_SETTINGS, + TCMU_DEV_STATUS_SETTINGS) # this module when imported preserves the global values @@ -22,6 +24,9 @@ config = Settings() +MON_CONFIG_PREFIX = 'config://' + + class Settings(object): _float_regex = re.compile(r"^[0-9]*\.{1}[0-9]$") _int_regex = re.compile(r"^[0-9]+$") @@ -63,6 +68,7 @@ self._add_attrs_from_defs(SYS_SETTINGS) self._add_attrs_from_defs(TGT_SETTINGS) self._add_attrs_from_defs(TCMU_SETTINGS) + self._add_attrs_from_defs(TCMU_DEV_STATUS_SETTINGS) if len(dataset) != 0: # If we have a file use it to override the defaults @@ -70,6 +76,10 @@ self._override_attrs_from_conf(config.items("config"), SYS_SETTINGS) + if config.has_section("device_status"): + self._override_attrs_from_conf(config.items("device_status"), + TCMU_DEV_STATUS_SETTINGS) + if config.has_section("target"): all_settings = TGT_SETTINGS.copy() all_settings.update(TCMU_SETTINGS) @@ -77,10 +87,13 @@ self._override_attrs_from_conf(config.items("target"), all_settings) - self.cephconf = '{}/{}.conf'.format(self.ceph_config_dir, self.cluster_name) if self.api_secure: self.api_ssl_verify = False if self.api_secure else None + @property + def cephconf(self): + return '{}/{}.conf'.format(self.ceph_config_dir, self.cluster_name) + def __repr__(self): s = '' for k in self.__dict__: @@ -97,14 +110,24 @@ for k, setting in def_settings.items(): self.__setattr__(k, setting.def_val) - def _override_attrs_from_conf(self, config, def_settings): - """ - receive a settings dict and apply those key/value to the - current instance, settings that look like numbers are converted - :param settings: dict of settings - :return: None - """ - for k, v in config: + def pull_from_mon_config(self, v): + if not self.cluster_client_name or not self.cephconf: + return '' + + with rados.Rados(conffile=self.cephconf, + name=self.cluster_client_name) as cluster: + if v.startswith(MON_CONFIG_PREFIX): + v = v[len(MON_CONFIG_PREFIX):] + + cmd = {"prefix": "config-key get", + "key": "{}".format(v)} + ret, v_data, outs = cluster.mon_command(json.dumps(cmd), b'') + if ret: + return '' + return v_data.decode('utf-8') + + def _override_attrs(self, override_attrs, def_settings): + for k, v in override_attrs.items(): if hasattr(self, k): setting = def_settings[k] try: @@ -114,6 +137,29 @@ # so the deamons can still start pass + def _override_attrs_from_conf(self, config, def_settings): + """ + receive a settings dict and apply those key/value to the + current instance, settings that look like numbers are converted + :param settings: dict of settings + :return: None + """ + mon_config_items = { + k: v for k, v in config + if isinstance(v, str) and v.startswith(MON_CONFIG_PREFIX)} + config_items = {k: v for k, v in config if k not in mon_config_items} + + # first process non mon config items because we need the + # cluster_client_name and ceph_conf in order to talk to the mon config + # store + self._override_attrs(config_items, def_settings) + + if mon_config_items: + # Now let's attempt to pull these from the config store + for k, v in mon_config_items.items(): + mon_config_items[k] = self.pull_from_mon_config(v) + self._override_attrs(mon_config_items, def_settings) + def _hash_settings(self, def_settings, sync_settings): for setting in def_settings: if setting not in Settings.exclude_from_hash: @@ -129,6 +175,7 @@ self._hash_settings(SYS_SETTINGS.keys(), sync_settings) self._hash_settings(TGT_SETTINGS.keys(), sync_settings) self._hash_settings(TCMU_SETTINGS.keys(), sync_settings) + self._hash_settings(TCMU_DEV_STATUS_SETTINGS.keys(), sync_settings) h = hashlib.sha256() h.update(json.dumps(sync_settings).encode('utf-8')) diff -Nru ceph-iscsi-3.4/ceph_iscsi_config/target.py ceph-iscsi-3.5/ceph_iscsi_config/target.py --- ceph-iscsi-3.4/ceph_iscsi_config/target.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/ceph_iscsi_config/target.py 2021-04-12 09:24:20.000000000 +0000 @@ -385,10 +385,15 @@ # there could/should be multiple tpg's for the target for tpg in self.target.tpgs: self.tpg_list.append(tpg) - ip_address = list(tpg.network_portals)[0].ip_address - gateway_name = self._get_gateway_name(ip_address) - if gateway_name: - self.tpg_tag_by_gateway_name[gateway_name] = tpg.tag + network_portals = list(tpg.network_portals) + if network_portals: + ip_address = network_portals[0].ip_address + gateway_name = self._get_gateway_name(ip_address) + if gateway_name: + self.tpg_tag_by_gateway_name[gateway_name] = tpg.tag + else: + self.logger.info("No available network portal for target " + "with iqn of '{}'".format(self.iqn)) # self.portal = self.tpg.network_portals.next() @@ -681,6 +686,9 @@ else: # create the target self.create_target() + # if error happens, we should never store this target to config + if self.error: + return seed_target = { 'disks': {}, 'clients': {}, diff -Nru ceph-iscsi-3.4/ceph-iscsi.spec ceph-iscsi-3.5/ceph-iscsi.spec --- ceph-iscsi-3.4/ceph-iscsi.spec 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/ceph-iscsi.spec 2021-04-12 09:24:20.000000000 +0000 @@ -20,7 +20,7 @@ Name: ceph-iscsi -Version: 3.4 +Version: 3.5 Release: 1%{?dist} Group: System/Filesystems Summary: Python modules for Ceph iSCSI gateway configuration management @@ -38,6 +38,7 @@ Obsoletes: ceph-iscsi-cli Requires: tcmu-runner >= 1.4.0 +Requires: ceph-common >= 10.2.2 %if 0%{?with_python2} BuildRequires: python2-devel BuildRequires: python2-setuptools diff -Nru ceph-iscsi-3.4/debian/ceph-iscsi.install ceph-iscsi-3.5/debian/ceph-iscsi.install --- ceph-iscsi-3.4/debian/ceph-iscsi.install 2021-01-20 15:21:38.000000000 +0000 +++ ceph-iscsi-3.5/debian/ceph-iscsi.install 2021-09-24 12:45:45.000000000 +0000 @@ -1 +1 @@ -usr/lib/systemd/system/*.service lib/systemd/system/ +usr/lib/systemd/system/*.service diff -Nru ceph-iscsi-3.4/debian/changelog ceph-iscsi-3.5/debian/changelog --- ceph-iscsi-3.4/debian/changelog 2021-06-24 05:33:10.000000000 +0000 +++ ceph-iscsi-3.5/debian/changelog 2021-09-24 12:45:45.000000000 +0000 @@ -1,13 +1,18 @@ -ceph-iscsi (3.4-1ubuntu1) impish; urgency=low +ceph-iscsi (3.5-2) unstable; urgency=medium - * Merge from Debian unstable. Remaining changes: - - Ensure tests run as part of package build. - - d/source/options: Ignore any changes to .egg-info files. - - d/control: Drop dependency on python3-rpm as its not required. - - d/p/ubuntu-support.patch: Ensure Ubuntu >= 18.04 is in the list of - supported distributions. + * QA upload. + * Apply patch to Test enablement, misc tidy actions - thanks to + James Page (Closes: #945777) + * Bump standards version - -- James Page Thu, 24 Jun 2021 06:33:10 +0100 + -- Neil Williams Fri, 24 Sep 2021 13:45:45 +0100 + +ceph-iscsi (3.5-1) experimental; urgency=medium + + [ Sébastien Delafond ] + * New upstream version 3.5 + + -- Sebastien Delafond Mon, 12 Apr 2021 11:29:26 +0200 ceph-iscsi (3.4-1) unstable; urgency=medium @@ -17,32 +22,6 @@ -- Sebastien Delafond Wed, 20 Jan 2021 10:34:01 +0100 -ceph-iscsi (3.4-0ubuntu2) focal; urgency=medium - - * d/p/ubuntu-support.patch: Ensure Ubuntu >= 18.04 is in the list of - supported distributions (LP: #1864838). - - -- James Page Wed, 26 Feb 2020 12:23:17 +0000 - -ceph-iscsi (3.4-0ubuntu1) focal; urgency=medium - - * New upstream release. - - -- James Page Tue, 25 Feb 2020 09:23:38 +0000 - -ceph-iscsi (3.3-1ubuntu1) focal; urgency=medium - - * Ensure tests run as part of package build: - - d/control: Add runtime dependencies to BD's alongside mock - and pytest. - - d/rules: Set PYBUILD_TEST_ARGS inline with upstream - configuration of pytest. - - d/rules: Ensure .pytest_cache is tidied on clean. - * d/source/options: Ignore any changes to .egg-info files. - * d/control: Drop dependency on python3-rpm as its not required. - - -- James Page Thu, 28 Nov 2019 13:51:45 +0000 - ceph-iscsi (3.3-1) unstable; urgency=medium * Team upload @@ -71,4 +50,3 @@ * First release (Closes: #933350) -- Sebastien Delafond Mon, 23 Sep 2019 08:46:44 +0200 - diff -Nru ceph-iscsi-3.4/debian/control ceph-iscsi-3.5/debian/control --- ceph-iscsi-3.4/debian/control 2021-06-24 04:39:17.000000000 +0000 +++ ceph-iscsi-3.5/debian/control 2021-09-24 09:59:58.000000000 +0000 @@ -1,26 +1,25 @@ Source: ceph-iscsi Section: python Priority: optional -Maintainer: Ubuntu Developers -XSBC-Original-Maintainer: Freexian Packaging Team -Uploaders: Sebastien Delafond -Build-Depends: debhelper-compat (= 12), - dh-python, - python3-all, - python3-configshell-fb, - python3-cryptography, - python3-distutils, - python3-flask, - python3-mock, - python3-netifaces, - python3-openssl, - python3-pytest, - python3-rados, - python3-rbd, - python3-requests, - python3-rtslib-fb, - python3-setuptools -Standards-Version: 4.5.1 +Maintainer: Debian QA Group +Build-Depends: debhelper-compat (= 12), + dh-python, + python3-setuptools, + python3-all, + python3-configshell-fb, + python3-cryptography, + python3-distutils, + python3-flask, + python3-mock, + python3-netifaces, + python3-openssl, + python3-pytest, + python3-rados, + python3-rbd, + python3-requests, + python3-rtslib-fb, + python3-setuptools +Standards-Version: 4.6.0 Homepage: https://github.com/ceph/ceph-iscsi Vcs-Browser: https://salsa.debian.org/debian/ceph-iscsi Vcs-Git: https://salsa.debian.org/debian/ceph-iscsi.git diff -Nru ceph-iscsi-3.4/debian/patches/series ceph-iscsi-3.5/debian/patches/series --- ceph-iscsi-3.4/debian/patches/series 2021-01-20 15:21:38.000000000 +0000 +++ ceph-iscsi-3.5/debian/patches/series 2021-04-12 09:29:26.000000000 +0000 @@ -1,2 +1 @@ 0001-Replace-etc-sysconfig-ceph-by-etc-default-ceph-in-se.patch -ubuntu-support.patch diff -Nru ceph-iscsi-3.4/debian/patches/ubuntu-support.patch ceph-iscsi-3.5/debian/patches/ubuntu-support.patch --- ceph-iscsi-3.4/debian/patches/ubuntu-support.patch 2020-02-26 12:17:31.000000000 +0000 +++ ceph-iscsi-3.5/debian/patches/ubuntu-support.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -Description: Add Ubuntu 18.04 or later to supported platforms - Thus avoiding the need to pass skipchecks to all cli commands -Author: James Page -Forwarded: no - ---- a/rbd-target-api.py -+++ b/rbd-target-api.py -@@ -2736,7 +2736,8 @@ def pre_reqs_errors(): - valid_dists = { - "rhel": 7.4, - "suse": 15.1, -- "debian": 10} -+ "debian": 10, -+ "ubuntu": 18.04} - - errors_found = [] - diff -Nru ceph-iscsi-3.4/gwcli/gateway.py ceph-iscsi-3.5/gwcli/gateway.py --- ceph-iscsi-3.4/gwcli/gateway.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/gwcli/gateway.py 2021-04-12 09:24:20.000000000 +0000 @@ -33,7 +33,6 @@ UIRoot.__init__(self, shell) self.error = False - self.error_msg = '' self.interactive = True # default interactive mode self.scan_threads = scan_threads @@ -59,10 +58,9 @@ self.target = ISCSITargets(self) def refresh(self): - self.config = self._get_config() + self.config = self._get_config() if not self.error: - if 'disks' in self.config: self.disks.refresh(self.config['disks']) else: @@ -74,14 +72,10 @@ self.target.gateway_group = {} self.target.refresh(self.config['targets'], self.config['discovery_auth']) - self.ceph.refresh() - else: # Unable to get the config, tell the user and exit the cli - self.logger.critical("Unable to access the configuration " - "object : {}".format(self.error_msg)) - raise GatewayError + self.logger.critical("Unable to access the configuration object") def _get_config(self, endpoint=None): @@ -96,12 +90,14 @@ return api.response.json() except Exception: self.error = True - self.error_msg = "Malformed REST API response" + self.logger.error("Malformed REST API response") return {} else: + # 403 maybe due to the ip address is not in the iscsi + # gateway trusted ip list self.error = True - self.error_msg = "REST API failure, code : " \ - "{}".format(api.response.status_code) + self.logger.error("REST API failure, code : " + "{}".format(api.response.status_code)) return {} def export_copy(self, config): @@ -130,6 +126,9 @@ return current_config = self._get_config() + if not current_config: + self.logger.error("Unable to refresh local config.") + return if mode == 'copy': self.export_copy(current_config) @@ -200,11 +199,9 @@ if not target_exists: Target(target_iqn, self) else: - self.logger.error("Failed to create the target on the local node") - - raise GatewayAPIError("iSCSI target creation failed - " - "{}".format(response_message(api.response, - self.logger))) + self.logger.error("Failed to create the target on the local node " + "{}".format(response_message(api.response, + self.logger))) def ui_command_delete(self, target_iqn): """ @@ -297,7 +294,7 @@ msg = response_message(api.response, self.logger) self.logger.error("Delete of {} failed : {}".format(gw_name, msg)) - raise GatewayAPIError + return else: self.logger.debug("- deleted {}".format(gw_name)) @@ -363,7 +360,6 @@ self.logger.info('ok') else: self.logger.error("Error: {}".format(response_message(api.response, self.logger))) - return def _set_auth(self, username, password, mutual_username, mutual_password): if mutual_username != '' and mutual_password != '': @@ -417,6 +413,7 @@ config = self.parent.parent._get_config() if not config: self.logger.error("Unable to refresh local config") + raise GatewayError self.auth = config['targets'][target_iqn]['auth'] # decode the chap password if necessary @@ -516,14 +513,13 @@ self.logger.error("Failed to reconfigure : " "{}".format(response_message(api.response, self.logger))) - return config = self.parent.parent._get_config() if not config: - self.logger.error("Unable to refresh local config") - self.controls = config['targets'][self.target_iqn]['controls'] - - self.logger.info('ok') + self.logger.error("Unable to refresh local config.") + else: + self.controls = config['targets'][self.target_iqn]['controls'] + self.logger.info('ok') def ui_command_auth(self, username=None, password=None, mutual_username=None, mutual_password=None): @@ -591,7 +587,6 @@ self.logger.error("Failed to update target auth: " "{}".format(response_message(api.response, self.logger))) - return class GatewayGroup(UIGroup): @@ -705,7 +700,7 @@ config = self.parent.parent.parent._get_config() if not config: self.logger.error("Unable to refresh local config over API - sync " - "aborted, restart rbd-target-api on {} to " + "aborted, restart rbd-target-api on {0} to " "sync".format(gateway_name)) return @@ -757,6 +752,7 @@ config = self.parent.parent.parent._get_config() if not config: self.logger.error("Could not refresh display. Restart gwcli.") + return elif not config['targets'][target_iqn]['portals']: # no more gws so everything but the target is dropped. disks_object = self.parent.get_child("disks") @@ -820,7 +816,8 @@ if not config: self.logger.error("Unable to refresh local config" " over API - sync aborted, restart rbd-target-api" - " on {} to sync".format(gateway_name)) + " on {0} to sync".format(gateway_name)) + return target_iqn = self.parent.name target_config = config['targets'][target_iqn] @@ -833,6 +830,25 @@ if skipchecks == 'true': self.logger.warning("OS version/package checks have been bypassed") + # Check if we can get hostname from + # the new gw endpoint + new_gw_endpoint = ('{}://{}:{}/' + 'api'.format(self.http_mode, + gateway_name, + settings.config.api_port)) + api = APIRequest('{}/sysinfo/hostname'.format(new_gw_endpoint)) + api.get() + if api.response.status_code != 200: + msg = response_message(api.response, self.logger) + self.logger.error("Get gateway hostname failed : {}\n" + "Please check api_host setting and make sure " + "host {} IP is listening on port {}" + "".format(msg, gateway_name, + settings.config.api_port)) + return + + gateway_hostname = api.response.json()['data'] + self.logger.info("Adding gateway, {}".format(sync_text)) gw_api = '{}://{}:{}/api'.format(self.http_mode, @@ -858,15 +874,12 @@ # add to the UI. We have to use the new gateway to ensure what # we get back is current (the other gateways will lag until they see # epoch xattr change on the config object) - new_gw_endpoint = ('{}://{}:{}/' - 'api'.format(self.http_mode, - gateway_name, - settings.config.api_port)) - - api = APIRequest('{}/sysinfo/hostname'.format(new_gw_endpoint)) - api.get() - gateway_hostname = api.response.json()['data'] config = self.parent.parent.parent._get_config(endpoint=new_gw_endpoint) + if not config: + self.logger.error("Unable to refresh local config" + " over API - sync aborted, restart rbd-target-api" + " on {0} to sync".format(gateway_name)) + return target_config = config['targets'][target_iqn] portal_config = target_config['portals'][gateway_hostname] Gateway(self, gateway_hostname, portal_config) diff -Nru ceph-iscsi-3.4/gwcli/storage.py ceph-iscsi-3.5/gwcli/storage.py --- ceph-iscsi-3.4/gwcli/storage.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/gwcli/storage.py 2021-04-12 09:24:20.000000000 +0000 @@ -627,8 +627,9 @@ class Disk(UINode): display_attributes = ["image", "ceph_cluster", "pool", "wwn", "size_h", - "feature_list", "snapshots", "owner", "backstore", - "backstore_object_name", "control_values"] + "feature_list", "snapshots", "owner", "lock_owner", + "state", "backstore", "backstore_object_name", + "control_values"] def __init__(self, parent, image_id, image_config, size=None, features=None, snapshots=None): @@ -664,6 +665,7 @@ self.parent.parent.disk_lookup[image_id] = self self._apply_config(image_config) + self._apply_status() if not size: # Size/features are not stored in the config, since it can be changed @@ -684,6 +686,34 @@ disk_map[self.image_id]['size'] = self.size disk_map[self.image_id]['size_h'] = self.size_h + def _apply_status(self): + disk_api = ('{}://localhost:{}/api/' + 'disk/{}'.format(self.http_mode, + settings.config.api_port, self.image_id)) + self.logger.debug("disk GET status for {}".format(self.image_id)) + api = APIRequest(disk_api) + api.get() + + # set both the 'lock_owner' and 'state' to Unknown as default in + # case if the api response fails the gwcli command will fail too + self.__setattr__('lock_owner', 'Unknown') + self.__setattr__('state', 'Unknown') + + if api.response.status_code == 200: + info = api.response.json() + status = info.get("status") + + if status is None: + return + + state = status.get('state') + if (state): + self.__setattr__('state', state) + + owner = status.get('lock_owner') + if (owner): + self.__setattr__('lock_owner', owner) + def _apply_config(self, image_config): # set the remaining attributes based on the fields in the dict disk_map = self.parent.parent.disk_info @@ -700,9 +730,28 @@ if not self.exists: return 'NOT FOUND', False - msg = [self.image_id, "({})".format(self.size_h)] + status = True + + disk_api = ('{}://localhost:{}/api/' + 'disk/{}'.format(self.http_mode, settings.config.api_port, + self.image_id)) + + self.logger.debug("disk GET status for {}".format(self.image_id)) + api = APIRequest(disk_api) + api.get() + + state = "State unknown" + if api.response.status_code == 200: + info = api.response.json() + disk_status = info.get("status") + if disk_status: + state = disk_status.get("state") + if state != "Online": + status = False + + msg = [self.image_id, "({}, {})".format(state, self.size_h)] - return " ".join(msg), True + return " ".join(msg), status def _parse_snapshots(self, snapshots): self.snapshots = ["{name} ({size})".format(name=s['name'], @@ -741,6 +790,7 @@ if not config: return self._apply_config(config['disks'][self.image_id]) + self._apply_status() def _get_meta_data_tcmu(self): """ diff -Nru ceph-iscsi-3.4/gwcli/utils.py ceph-iscsi-3.5/gwcli/utils.py --- ceph-iscsi-3.4/gwcli/utils.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/gwcli/utils.py 2021-04-12 09:24:20.000000000 +0000 @@ -113,8 +113,7 @@ api.get() if api.response.status_code != 200: return ("checkconf API call to {} failed with " - "code".format(gw_name, - api.response.status_code)) + "code {}".format(gw_name, api.response.status_code)) # compare the hash of the new gateways conf file with the local one local_hash = settings.config.hash() @@ -441,7 +440,7 @@ "{}".format(self.args[0])) self.data = Response() self.data.status_code = 500 - self.data._content = '{{"message": "{}" }}'.format(msg) + self.data._content = '{{"message": "{}" }}'.format(msg).encode('utf-8') return self._get_response except Exception: raise GatewayAPIError("Unknown error connecting to " diff -Nru ceph-iscsi-3.4/iscsi-gateway.cfg_sample ceph-iscsi-3.5/iscsi-gateway.cfg_sample --- ceph-iscsi-3.4/iscsi-gateway.cfg_sample 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/iscsi-gateway.cfg_sample 2021-04-12 09:24:20.000000000 +0000 @@ -8,6 +8,7 @@ # CephX client name # cluster_client_name = client. # E.g.: client.admin +# gateway_conf = gateway.conf # API settings. # The api supports a number of options that allow you to tailor it to your diff -Nru ceph-iscsi-3.4/rbd-target-api.py ceph-iscsi-3.5/rbd-target-api.py --- ceph-iscsi-3.4/rbd-target-api.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/rbd-target-api.py 2021-04-12 09:24:20.000000000 +0000 @@ -10,6 +10,7 @@ import ssl import operator import OpenSSL +import tempfile import threading import time import inspect @@ -35,6 +36,7 @@ from ceph_iscsi_config.utils import (normalize_ip_literal, resolve_ip_addresses, ip_addresses, read_os_release, encryption_available, CephiSCSIError, this_host) +from ceph_iscsi_config.device_status import DeviceStatusWatcher from gwcli.utils import (APIRequest, valid_gateway, valid_client, valid_credentials, get_remote_gateways, valid_snapshot_name, @@ -120,7 +122,7 @@ Display all the available API endpoints **UNRESTRICTED** Examples: - curl --insecure --user admin:admin -X GET http://192.168.122.69:5000/api + curl --user admin:admin -X GET http://192.168.122.69:5000/api """ links = [] @@ -168,7 +170,7 @@ Valid query types are: ip_addresses, checkconf and checkversions **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET http://192.168.122.69:5000/api/sysinfo/ip_addresses + curl --user admin:admin -X GET http://192.168.122.69:5000/api/sysinfo/ip_addresses """ if query_type == 'ip_addresses': @@ -227,10 +229,10 @@ List targets defined in the configuration. **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET https://192.168.122.69:5000/api/targets + curl --user admin:admin -X GET http://192.168.122.69:5000/api/targets """ - return jsonify({'targets': config.config['targets'].keys()}), 200 + return jsonify({'targets': list(config.config['targets'].keys())}), 200 @app.route('/api/target/', methods=['PUT', 'DELETE']) @@ -245,9 +247,9 @@ :param controls: (JSON dict) valid control overrides **RESTRICTED** Examples: - curl --insecure --user admin:admin + curl --user admin:admin -X PUT http://192.168.122.69:5000/api/target/iqn.2003-01.com.redhat.iscsi-gw0 - curl --insecure --user admin:admin -d mode=reconfigure -d controls='{cmdsn_depth=128}' + curl --user admin:admin -d mode=reconfigure -d controls='{cmdsn_depth=128}' -X PUT http://192.168.122.69:5000/api/target/iqn.2003-01.com.redhat.iscsi-gw0 """ @@ -277,10 +279,6 @@ return jsonify(message="Target: {} is not defined." "".format(target_iqn)), 400 - if client_controls and not target_config['clients']: - return jsonify(message="No clients found. Create clients then " - "rerun reconfigure command."), 400 - gateway_ip_list = [] target = GWTarget(logger, str(target_iqn), @@ -501,7 +499,7 @@ :param decrypt_passwords: (bool) if true, passwords will be decrypted **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET http://192.168.122.69:5000/api/config + curl --user admin:admin -X GET http://192.168.122.69:5000/api/config """ if request.method == 'GET': @@ -550,8 +548,8 @@ Return the gateway subsection of the config object to the caller **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET - http://192.168.122.69:5000/api/gateways/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw + curl --user admin:admin -X GET + http://192.168.122.69:5000/api/gateways/iqn.2003-01.com.redhat.iscsi-gw """ try: @@ -583,8 +581,10 @@ :param force: (bool) if True will force removal of gateway. **RESTRICTED** Examples: - curl --insecure --user admin:admin -d ip_address=192.168.122.69 - -X PUT http://192.168.122.69:5000/api/gateway/iscsi-gw0 + curl --user admin:admin -d ip_address=192.168.122.69 + -X PUT http://192.168.122.69:5000/api/gateway/iqn.2003-01.com.redhat.iscsi-gw/gateway1 + curl --user admin:admin + -X DELETE http://192.168.122.69:5000/api/gateway/iqn.2003-01.com.redhat.iscsi-gw/gateway1 """ # the definition of a gateway into an existing configuration can apply the @@ -660,7 +660,7 @@ elif request.method == 'DELETE': gateways.remove(gateway_name) - if request.form.get('force', 'false').lower() == 'true': + if gateway_name != this_host() and request.form.get('force', 'false').lower() == 'true': # The gw we want to delete is down and the user has decided to # force the deletion, so we do the config modification locally # then only tell the other gws to update their state. @@ -752,8 +752,10 @@ :param disk: (str) rbd image name on the format pool/image **RESTRICTED** Examples: - curl --insecure --user admin:admin -d disk=rbd.new2_1 - -X PUT https://192.168.122.69:5000/api/targetlun/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw + curl --user admin:admin -d disk=rbd/new2_1 + -X PUT http://192.168.122.69:5000/api/targetlun/iqn.2003-01.com.redhat.iscsi-gw + curl --user admin:admin -d disk=rbd/new2_1 + -X DELETE http://192.168.122.69:5000/api/targetlun/iqn.2003-01.com.redhat.iscsi-gw """ try: @@ -954,7 +956,7 @@ :param config: (str) 'yes' to list the config info of all disks, default is 'no' **RESTRICTED** Examples: - curl --insecure --user admin:admin -d config=yes -X GET https://192.168.122.69:5000/api/disks + curl --user admin:admin -d config=yes -X GET http://192.168.122.69:5000/api/disks """ conf = request.form.get('config', 'no') @@ -962,7 +964,7 @@ disk_names = config.config['disks'] response = {"disks": disk_names} else: - disk_names = config.config['disks'].keys() + disk_names = list(config.config['disks'].keys()) response = {"disks": disk_names} return jsonify(response), 200 @@ -987,18 +989,18 @@ :param count: (str) the number of images will be created :param owner: (str) the owner of the rbd image :param controls: (JSON dict) valid control overrides - :param preserve_image: (bool) do NOT delete RBD image - :param create_image: (bool) create RBD image if not exists + :param preserve_image: (bool, 'true/false') do NOT delete RBD image + :param create_image: (bool, 'true/false') create RBD image if not exists, true as default :param backstore: (str) lio backstore :param wwn: (str) unit serial number **RESTRICTED** Examples: - curl --insecure --user admin:admin -d mode=create -d size=1g -d pool=rbd -d count=5 - -X PUT https://192.168.122.69:5000/api/disk/rbd.new2_ - curl --insecure --user admin:admin -d mode=create -d size=10g -d pool=rbd - -X PUT https://192.168.122.69:5000/api/disk/rbd.new3_ - curl --insecure --user admin:admin -X GET https://192.168.122.69:5000/api/disk/rbd.new2_1 - curl --insecure --user admin:admin -X DELETE https://192.168.122.69:5000/api/disk/rbd.new2_1 + curl --user admin:admin -d mode=create -d size=1g -d pool=rbd -d count=5 + -X PUT http://192.168.122.69:5000/api/disk/rbd/new0_ + curl --user admin:admin -d mode=create -d size=10g -d pool=rbd -d create_image=false + -X PUT http://192.168.122.69:5000/api/disk/rbd/new1 + curl --user admin:admin -X GET http://192.168.122.69:5000/api/disk/rbd/new2 + curl --user admin:admin -X DELETE http://192.168.122.69:5000/api/disk/rbd/new3 """ local_gw = this_host() @@ -1011,7 +1013,15 @@ if request.method == 'GET': if image_id in config.config['disks']: - return jsonify(config.config["disks"][image_id]), 200 + disk_dict = config.config["disks"][image_id] + global dev_status_watcher + disk_status = dev_status_watcher.get_dev_status(image_id) + if disk_status: + disk_dict['status'] = disk_status.get_status_dict() + else: + disk_dict['status'] = {'state': 'Unknown'} + + return jsonify(disk_dict), 200 else: return jsonify(message="rbd image {} not " @@ -1059,17 +1069,26 @@ if disk_usable != 'ok': return jsonify(message=disk_usable), 400 - create_image = request.form.get('create_image') == 'true' - if mode == 'create' and (not create_image or not size): + create_image = request.form.get('create_image', 'true') + if create_image not in ['true', 'false']: + logger.error("Invalid 'create_image' value {}".format(create_image)) + return jsonify(message="Invalid 'create_image' value {}".format(create_image)), 400 + + if mode == 'create' and (create_image == 'false' or not size): try: + # no size implies not intention to create an image, try to + # check whether it exists rbd_image = RBDDev(image, 0, backstore, pool) size = rbd_image.current_size except rbd.ImageNotFound: - if not create_image: - return jsonify(message="Image {} does not exist".format(image_id)), 400 - else: + # the create_image=true will be implied if size is specified + # by default + if create_image == 'true': + # the size must be specified when creating an image return jsonify(message="Size parameter is required when creating a new " "image"), 400 + elif create_image == 'false': + return jsonify(message="Image {} does not exist".format(image_id)), 400 if mode == 'reconfigure': resp_text, resp_code = lun_reconfigure(image_id, controls, backstore) @@ -1119,7 +1138,7 @@ api_vars = { 'purge_host': local_gw, - 'preserve_image': request.form['preserve_image'], + 'preserve_image': request.form.get('preserve_image'), 'backstore': backstore } @@ -1262,7 +1281,7 @@ # if valid_request(request.remote_addr): purge_host = request.form['purge_host'] - preserve_image = request.form['preserve_image'] == 'true' + preserve_image = request.form.get('preserve_image') == 'true' logger.debug("delete request for disk image '{}'".format(image_id)) pool, image = image_id.split('/', 1) disk_config = config.config['disks'][image_id] @@ -1384,10 +1403,10 @@ :param mode: (str) 'create' or 'rollback' the rbd snapshot **RESTRICTED** Examples: - curl --insecure --user admin:admin -d mode=create - -X PUT https://192.168.122.69:5000/api/disksnap/rbd.image/new1 - curl --insecure --user admin:admin - -X DELETE https://192.168.122.69:5000/api/disksnap/rbd.image/new1 + curl --user admin:admin -d mode=create + -X PUT http://192.168.122.69:5000/api/disksnap/rbd.image/new1 + curl --user admin:admin + -X DELETE http://192.168.122.69:5000/api/disksnap/rbd.image/new1 """ if not valid_snapshot_name(name): @@ -1538,9 +1557,9 @@ [0-9a-zA-Z] and '@' '-' '_' '/' **RESTRICTED** Example: - curl --insecure --user admin:admin -d username=myiscsiusername -d password=myiscsipassword + curl --user admin:admin -d username=myiscsiusername -d password=myiscsipassword -d mutual_username=myiscsiusername -d mutual_password=myiscsipassword - -X PUT https://192.168.122.69:5000/api/discoveryauth + -X PUT http://192.168.122.69:5000/api/discoveryauth """ username = request.form.get('username', '') @@ -1613,8 +1632,8 @@ [0-9a-zA-Z] and '@' '-' '_' '/' **RESTRICTED** Examples: - curl --insecure --user admin:admin -d auth='disable_acl' - -X PUT https://192.168.122.69:5000/api/targetauth/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw + curl --user admin:admin -d auth='disable_acl' + -X PUT http://192.168.122.69:5000/api/targetauth/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw """ action = request.form.get('action') @@ -1744,7 +1763,7 @@ Returns the total number of active sessions for **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET + curl --user admin:admin -X GET http://192.168.122.69:5000/api/targetinfo/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw """ if target_iqn not in config.config['targets']: @@ -1774,7 +1793,7 @@ Returns the number of active sessions for on local gateway **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET + curl --user admin:admin -X GET http://192.168.122.69:5000/api/_targetinfo/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw """ if target_iqn not in config.config['targets']: @@ -1792,7 +1811,7 @@ Returns the number of active sessions on local gateway **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET + curl --user admin:admin -X GET http://192.168.122.69:5000/api/gatewayinfo """ local_gw = this_host() @@ -1816,8 +1835,8 @@ restricted_auth wrapper **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET - https://192.168.122.69:5000/api/clients/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw + curl --user admin:admin -X GET + http://192.168.122.69:5000/api/clients/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw """ try: @@ -1827,7 +1846,7 @@ return jsonify(message=err_str), 500 target_config = config.config['targets'][target_iqn] - client_list = target_config['clients'].keys() + client_list = list(target_config['clients'].keys()) response = {"clients": client_list} return jsonify(response), 200 @@ -1888,9 +1907,9 @@ [0-9a-zA-Z] and '@' '-' '_' '/' **RESTRICTED** Example: - curl --insecure --user admin:admin -d username=myiscsiusername -d password=myiscsipassword + curl --user admin:admin -d username=myiscsiusername -d password=myiscsipassword -d mutual_username=myiscsiusername -d mutual_password=myiscsipassword - -X PUT https://192.168.122.69:5000/api/clientauth/iqn.2017-08.org.ceph:iscsi-gw0 + -X PUT http://192.168.122.69:5000/api/clientauth/iqn.2017-08.org.ceph:iscsi-gw0 """ try: @@ -1988,8 +2007,12 @@ :param disk: (str) rbd image name of the format pool/image **RESTRICTED** Examples: - curl --insecure --user admin:admin -d disk=rbd.new2_1 - -X PUT https://192.168.122.69:5000/api/clientlun/iqn.2017-08.org.ceph:iscsi-gw0 + TARGET_IQN = iqn.2017-08.org.ceph:iscsi-gw + CLIENT_IQN = iqn.1994-05.com.redhat:myhost4 + curl --user admin:admin -d disk=rbd/new2_1 + -X PUT http://192.168.122.69:5000/api/clientlun/$TARGET_IQN/$CLIENT_IQN + curl --user admin:admin -d disk=rbd/new2_1 + -X DELETE http://192.168.122.69:5000/api/clientlun/$TARGET_IQN/$CLIENT_IQN """ try: @@ -2109,10 +2132,12 @@ :param client_iqn: (str) IQN of the client to create or delete **RESTRICTED** Examples: - curl --insecure --user admin:admin - -X PUT https://192.168.122.69:5000/api/client/iqn.1994-05.com.redhat:myhost4 - curl --insecure --user admin:admin - -X DELETE https://192.168.122.69:5000/api/client/iqn.1994-05.com.redhat:myhost4 + TARGET_IQN = iqn.2017-08.org.ceph:iscsi-gw + CLIENT_IQN = iqn.1994-05.com.redhat:myhost4 + curl --user admin:admin + -X PUT http://192.168.122.69:5000/api/client/$TARGET_IQN/$CLIENT_IQN + curl --user admin:admin + -X DELETE http://192.168.122.69:5000/api/client/$TARGET_IQN/$CLIENT_IQN """ method = {"PUT": 'create', @@ -2255,7 +2280,7 @@ Returns client alias, ip_address and state for each connected portal **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET + curl --user admin:admin -X GET http://192.168.122.69:5000/api/clientinfo/ iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw-client """ @@ -2298,7 +2323,7 @@ Returns client alias, ip_address and state for local gateway **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET + curl --user admin:admin -X GET http://192.168.122.69:5000/api/_clientinfo/ iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw-client """ @@ -2319,7 +2344,7 @@ Return the hostgroup names defined to the configuration **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET + curl --user admin:admin -X GET http://192.168.122.69:5000/api/hostgroups/iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw """ @@ -2331,7 +2356,7 @@ target_config = config.config['targets'][target_iqn] if request.method == 'GET': - return jsonify({"groups": target_config['groups'].keys()}), 200 + return jsonify({"groups": list(target_config['groups'].keys())}), 200 @app.route('/api/hostgroup//', methods=['GET', 'PUT', 'DELETE']) @@ -2346,12 +2371,12 @@ :param: action (str) 'add'/'remove' group's client members/disks, default is 'add' :return: Examples: - curl --insecure --user admin:admin -X GET http://192.168.122.69:5000/api/hostgroup/group_name - curl --insecure --user admin:admin -d members=iqn.1994-05.com.redhat:myhost4 + curl --user admin:admin -X GET http://192.168.122.69:5000/api/hostgroup/group_name + curl --user admin:admin -d members=iqn.1994-05.com.redhat:myhost4 -d disks=rbd.disk1 -X PUT http://192.168.122.69:5000/api/hostgroup/group_name - curl --insecure --user admin:admin -d action=remove -d disks=rbd.disk1 + curl --user admin:admin -d action=remove -d disks=rbd.disk1 -X PUT http://192.168.122.69:5000/api/hostgroup/group_name - curl --insecure --user admin:admin + curl --user admin:admin -X DELETE http://192.168.122.69:5000/api/hostgroup/group_name """ http_mode = 'https' if settings.config.api_secure else 'http' @@ -2484,7 +2509,7 @@ List settings. **RESTRICTED** Examples: - curl --insecure --user admin:admin -X GET https://192.168.122.69:5000/api/settings + curl --user admin:admin -X GET http://192.168.122.69:5000/api/settings """ target_default_controls, target_controls_limits = fill_settings_dict(GWTarget.SETTINGS) @@ -2511,7 +2536,7 @@ 'config': { 'minimum_gateways': settings.config.minimum_gateways }, - 'api_version': 1 + 'api_version': 2 }), 200 @@ -2736,7 +2761,8 @@ valid_dists = { "rhel": 7.4, "suse": 15.1, - "debian": 10} + "debian": 10, + "ubuntu": 18.04} errors_found = [] @@ -2800,11 +2826,7 @@ try: obj_epoch = int(ioctx.get_xattr('gateway.conf', 'epoch')) except rados.ObjectNotFound: - # daemon is running prior to any config being created or it has - # skip the error, and - logger.warning("config object missing, recreating") - config.refresh() - + raise else: # if it's changed, refresh the local config to ensure a query # to this node will return current state @@ -2815,13 +2837,46 @@ config.refresh() +def get_ssl_files_from_mon(): + client_name = settings.config.cluster_client_name + temp_files = [] + crt_data = settings.config.pull_from_mon_config( + "iscsi/{}/iscsi-gateway.crt".format(client_name)) + if not crt_data: + return temp_files + + key_data = settings.config.pull_from_mon_config( + "iscsi/{}/iscsi-gateway.key".format(client_name)) + if not key_data: + return temp_files + + for data in crt_data, key_data: + # NOTE: Annoyingly SSLContext.load_cert_chain can only take + # paths to files and not file like objects.. yet. So we need to + # create tempfiles for the SSL context to read. Once + # https://bugs.python.org/issue16487 is resolved, we should be able + # to simply use file-like objects and makes this much nicer. + + tmp_f = tempfile.NamedTemporaryFile(mode='w') + tmp_f.write(data) + tmp_f.flush() + temp_files.append(tmp_f) + return temp_files + + def get_ssl_context(): # Use these self-signed crt and key files cert_files = ['/etc/ceph/iscsi-gateway.crt', '/etc/ceph/iscsi-gateway.key'] + temp_files = [] if not all([os.path.exists(crt_file) for crt_file in cert_files]): - return None + # attempt to pull out the crt and key data from global mon config-key + # storage, we need to return the tempfiles so they're not gc'ed. + temp_files = get_ssl_files_from_mon() + cert_files = [f.name for f in temp_files] + if not cert_files or not all([os.path.exists(crt_file) for crt_file in cert_files]): + return None ver, rel, mod = werkzeug.__version__.split('.') if int(rel) > 9: @@ -2842,6 +2897,10 @@ logger.critical("SSL Error : {}".format(err)) return None + # If we have loaded the certs into tempfiles we can clean them up now. + # This should happen when we return, but let's be explicit. + for f in temp_files: + f.close() return context @@ -2853,12 +2912,17 @@ log.setLevel(logging.DEBUG) # Attach the werkzeug log to the handlers defined in the outer scope - log.addHandler(file_handler) + if settings.config.log_to_file: + log.addHandler(file_handler) log.addHandler(syslog_handler) + global dev_status_watcher + dev_status_watcher = DeviceStatusWatcher(logger) + dev_status_watcher.start() + ceph_gw = CephiSCSIGateway(logger, config) - osd_state_ok = ceph_gw.osd_blacklist_cleanup() + osd_state_ok = ceph_gw.osd_blocklist_cleanup() if not osd_state_ok: sys.exit(16) @@ -2923,23 +2987,30 @@ logger.setLevel(logging.DEBUG) # syslog (systemctl/journalctl messages) - syslog_handler = logging.handlers.SysLogHandler(address='/dev/log') - syslog_handler.setLevel(logging.INFO) syslog_format = logging.Formatter("%(message)s") + if settings.config.log_to_stderr: + syslog_handler = logging.StreamHandler(sys.stderr) + if settings.config.log_to_stderr_prefix: + syslog_format = \ + logging.Formatter("{} %(message)s".format( + settings.config.log_to_stderr_prefix)) + else: + syslog_handler = logging.handlers.SysLogHandler(address='/dev/log') + syslog_handler.setLevel(logging.INFO) syslog_handler.setFormatter(syslog_format) - - # file target - more verbose logging for diagnostics - file_handler = RotatingFileHandler('/var/log/rbd-target-api/rbd-target-api.log', - maxBytes=5242880, - backupCount=7) - file_handler.setLevel(logger_level) - file_format = logging.Formatter( - "%(asctime)s %(levelname)8s [%(filename)s:%(lineno)s:%(funcName)s()] " - "- %(message)s") - file_handler.setFormatter(file_format) - logger.addHandler(syslog_handler) - logger.addHandler(file_handler) + + if settings.config.log_to_file: + # file target - more verbose logging for diagnostics + file_handler = RotatingFileHandler('/var/log/rbd-target-api/rbd-target-api.log', + maxBytes=5242880, + backupCount=7) + file_handler.setLevel(logger_level) + file_format = logging.Formatter( + "%(asctime)s %(levelname)8s [%(filename)s:%(lineno)s:%(funcName)s()] " + "- %(message)s") + file_handler.setFormatter(file_format) + logger.addHandler(file_handler) # config is set in the outer scope, so it's easily accessible to all # api functions diff -Nru ceph-iscsi-3.4/rbd-target-gw.py ceph-iscsi-3.5/rbd-target-gw.py --- ceph-iscsi-3.4/rbd-target-gw.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/rbd-target-gw.py 2021-04-12 09:24:20.000000000 +0000 @@ -4,11 +4,12 @@ import logging.handlers from logging.handlers import RotatingFileHandler -from flask import Flask, Response +from flask import Flask, Response, jsonify from ceph_iscsi_config.metrics import GatewayStats import ceph_iscsi_config.settings as settings +from ceph_iscsi_config.utils import CephiSCSIInval # Create a flask instance app = Flask(__name__) @@ -32,7 +33,10 @@ """ Collect the stats and send back to the caller""" stats = GatewayStats() - stats.collect() + try: + stats.collect() + except CephiSCSIInval as err: + return jsonify(message="Could not get metrics: {}".format(err)), 404 return Response(stats.formatted(), content_type="text/plain") diff -Nru ceph-iscsi-3.4/README.md ceph-iscsi-3.5/README.md --- ceph-iscsi-3.4/README.md 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/README.md 2021-04-12 09:24:20.000000000 +0000 @@ -75,14 +75,14 @@ | **utils** | common code called by multiple modules | The rbd-target-api daemon performs the following tasks; - 1. At start up remove any osd blacklist entry that may apply to the running host - 2. Read the configuration object from Rados - 3. Process the configuration - 3.1 map rbd's to the host - 3.2 add rbd's to LIO - 3.3 Create the iscsi target, TPG's and port IP's - 3.4 Define clients (NodeACL's) - 3.5 add the required rbd images to clients + 1. At start up remove any osd blocklist entry that may apply to the running host + 2. Read the configuration object from Rados + 3. Process the configuration + 3.1 map rbd's to the host + 3.2 add rbd's to LIO + 3.3 Create the iscsi target, TPG's and port IP's + 3.4 Define clients (NodeACL's) + 3.5 add the required rbd images to clients 4. Export a REST API for system configuration. diff -Nru ceph-iscsi-3.4/setup.py ceph-iscsi-3.5/setup.py --- ceph-iscsi-3.4/setup.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/setup.py 2021-04-12 09:24:20.000000000 +0000 @@ -28,7 +28,7 @@ setup( name="ceph_iscsi", - version="3.4", + version="3.5", description="Common classes/functions and CLI tools used to configure iSCSI " "gateways backed by Ceph RBD", long_description=long_description, diff -Nru ceph-iscsi-3.4/test/test_settings.py ceph-iscsi-3.5/test/test_settings.py --- ceph-iscsi-3.4/test/test_settings.py 2021-01-20 09:32:42.000000000 +0000 +++ ceph-iscsi-3.5/test/test_settings.py 2021-04-12 09:24:20.000000000 +0000 @@ -5,13 +5,16 @@ from ceph_iscsi_config.settings import Settings from ceph_iscsi_config.target import GWTarget +from ceph_iscsi_config.gateway_setting import SYS_SETTINGS class SettingsTest(unittest.TestCase): @staticmethod - def _normalize(controls): - return Settings.normalize_controls(controls, GWTarget.SETTINGS) + def _normalize(controls, settings=None): + if not settings: + settings = GWTarget.SETTINGS + return Settings.normalize_controls(controls, settings) def test_normalize_controls_int(self): self.assertEqual( @@ -69,3 +72,16 @@ with self.assertRaises(ValueError) as cm: SettingsTest._normalize({'immediate_data': 123}) self.assertEqual('expected yes or no for immediate_data', str(cm.exception)) + + def test_normalise_list(self): + self.assertDictEqual( + SettingsTest._normalize( + {'trusted_ip_list': '10.1.1.1,10.1.1.2,10.1.1.3'}, + SYS_SETTINGS), + {'trusted_ip_list': ['10.1.1.1', '10.1.1.2', '10.1.1.3']}) + + self.assertDictEqual( + SettingsTest._normalize( + {'trusted_ip_list': '10.1.1.1, 10.1.1.2 , 10.1.1.3'}, + SYS_SETTINGS), + {'trusted_ip_list': ['10.1.1.1', '10.1.1.2', '10.1.1.3']})