Hello community, here is the log from the commit of package ceph-iscsi for openSUSE:Factory checked in at 2019-09-11 10:33:40 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/ceph-iscsi (Old) and /work/SRC/openSUSE:Factory/.ceph-iscsi.new.7948 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ceph-iscsi" Wed Sep 11 10:33:40 2019 rev:17 rq:729677 version:3.2+1568099844.g09c5205 Changes: -------- --- /work/SRC/openSUSE:Factory/ceph-iscsi/ceph-iscsi.changes 2019-08-15 12:30:19.590440858 +0200 +++ /work/SRC/openSUSE:Factory/.ceph-iscsi.new.7948/ceph-iscsi.changes 2019-09-11 10:33:50.643323217 +0200 @@ -1,0 +2,14 @@ +Tue Sep 10 06:54:56 UTC 2019 - Nathan Cutler <ncut...@suse.com> + +- Update to 3.2+1568098374.g09c5205: + + upstream 3.2 release + * Always use host FQDN instead of shortname + * Validate target controls min/max value + * Validate rbd:user/tcmu-runner image controls min/max value + + checkin.sh: + * add "sed" statements to reproduce Dominique Leuenberger's + downstream-only mod from July 29, 2019 (see previous entry), so it + doesn't get clobbered every time we run the script + * add "sed" statement to collapse multiple newlines down to one + +------------------------------------------------------------------- Old: ---- ceph-iscsi-3.0+1560249372.g70ec7a9.tar.gz New: ---- ceph-iscsi-3.2+1568099844.g09c5205.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ ceph-iscsi.spec ++++++ --- /var/tmp/diff_new_pack.mUUA5H/_old 2019-09-11 10:33:51.807322879 +0200 +++ /var/tmp/diff_new_pack.mUUA5H/_new 2019-09-11 10:33:51.811322879 +0200 @@ -18,9 +18,8 @@ %global with_python2 1 %endif - Name: ceph-iscsi -Version: 3.0+1560249372.g70ec7a9 +Version: 3.2+1568099844.g09c5205 Release: 1%{?dist} Group: System/Filesystems Summary: Python modules for Ceph iSCSI gateway configuration management @@ -34,32 +33,37 @@ %if 0%{?suse_version} Source98: checkin.sh Source99: README-checkin.txt -%endif ExclusiveArch: x86_64 aarch64 ppc64le s390x +%endif Obsoletes: ceph-iscsi-config Obsoletes: ceph-iscsi-cli +Requires: tcmu-runner >= 1.4.0 %if 0%{?with_python2} BuildRequires: python2-devel BuildRequires: python2-setuptools Requires: python-rados >= 10.2.2 Requires: python-rbd >= 10.2.2 Requires: python-netifaces >= 0.10.4 -Requires: python-rtslib >= 2.1.fb67 -Requires: rpm-python >= 4.11 +Requires: python-rtslib >= 2.1.fb68 Requires: python-cryptography Requires: python-flask >= 0.10.1 Requires: python-configshell +%if 0%{?rhel} == 7 +Requires: pyOpenSSL +%else +Requires: python-pyOpenSSL +%endif %else BuildRequires: python3-devel BuildRequires: python3-setuptools Requires: python3-rados >= 10.2.2 Requires: python3-rbd >= 10.2.2 Requires: python3-netifaces >= 0.10.4 -Requires: python3-rtslib >= 2.1.fb67 +Requires: python3-rtslib >= 2.1.fb68 Requires: python3-cryptography -Requires: python3-rpm >= 4.11 +Requires: python3-pyOpenSSL %if 0%{?suse_version} BuildRequires: python-rpm-macros BuildRequires: fdupes @@ -71,7 +75,7 @@ %endif %endif -%if 0%{?rhel} == 7 +%if 0%{?rhel} BuildRequires: systemd %else BuildRequires: systemd-rpm-macros @@ -130,7 +134,6 @@ install -m 0644 gwcli.8 %{buildroot}%{_mandir}/man8/ gzip %{buildroot}%{_mandir}/man8/gwcli.8 mkdir -p %{buildroot}%{_unitdir}/rbd-target-gw.service.d -install -m 0644 .%{_sysconfdir}/systemd/system/rbd-target-gw.service.d/dependencies.conf %{buildroot}%{_unitdir}/rbd-target-gw.service.d/ %if 0%{?suse_version} mkdir -p %{buildroot}%{_sbindir} ln -s service %{buildroot}%{_sbindir}/rcrbd-target-gw @@ -177,7 +180,6 @@ %service_del_postun rbd-target-gw.service rbd-target-api.service %endif - %files %license LICENSE %license COPYING @@ -197,7 +199,6 @@ %attr(0770,root,root) %dir %{_localstatedir}/log/rbd-target-gw %attr(0770,root,root) %dir %{_localstatedir}/log/rbd-target-api %dir %{_unitdir}/rbd-target-gw.service.d -%{_unitdir}/rbd-target-gw.service.d/dependencies.conf %if 0%{?suse_version} %{_sbindir}/rcrbd-target-gw %{_sbindir}/rcrbd-target-api ++++++ ceph-iscsi-3.0+1560249372.g70ec7a9.tar.gz -> ceph-iscsi-3.2+1568099844.g09c5205.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/README.md new/ceph-iscsi-3.2+1568099844.g09c5205/README.md --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/README.md 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/README.md 2019-09-10 09:17:24.087333799 +0200 @@ -38,18 +38,16 @@ python-cryptography python-flask -To install the python package that provides the application logic, run the -provided setup.py script i.e. ```> python setup.py install``` +To install the python package that provides the CLI tool, daemons and +application logic, run the provided setup.py script i.e. +```> python setup.py install``` -For the daemons (```rbd-target-gw``` and ```rbd-target-api```), simply copy the -following files into their equivalent places on each gateway: +If using systemd, copy the following unit files into their equivalent places +on each gateway: - <archive_root>/usr/lib/systemd/system/rbd-target-gw.service --> /lib/systemd/system - <archive_root>/usr/lib/systemd/system/rbd-target-api.service --> /lib/systemd/system -- <archive_root>/usr/bin/rbd-target-gw --> /usr/bin -- <archive_root>/usr/bin/rbd-target-api --> /usr/bin -- <archive_root>/usr/bin/gwcli --> /usr/bin -Once the daemon is in place, reload the configuration with +Once the unit files are in place, reload the configuration with ``` systemctl daemon-reload systemctl enable rbd-target-api diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph-iscsi.spec new/ceph-iscsi-3.2+1568099844.g09c5205/ceph-iscsi.spec --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph-iscsi.spec 2019-06-11 12:36:12.911206015 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/ceph-iscsi.spec 2019-09-10 09:17:24.303334792 +0200 @@ -18,9 +18,8 @@ %global with_python2 1 %endif - Name: ceph-iscsi -Version: 3.0+1560249372.g70ec7a9 +Version: 3.2+1568099844.g09c5205 Release: 1%{?dist} Group: System/Filesystems Summary: Python modules for Ceph iSCSI gateway configuration management @@ -34,33 +33,37 @@ %if 0%{?suse_version} Source98: checkin.sh Source99: README-checkin.txt +ExclusiveArch: x86_64 aarch64 ppc64le s390x %endif -BuildArch: noarch - Obsoletes: ceph-iscsi-config Obsoletes: ceph-iscsi-cli +Requires: tcmu-runner >= 1.4.0 %if 0%{?with_python2} BuildRequires: python2-devel BuildRequires: python2-setuptools Requires: python-rados >= 10.2.2 Requires: python-rbd >= 10.2.2 Requires: python-netifaces >= 0.10.4 -Requires: python-rtslib >= 2.1.fb67 -Requires: rpm-python >= 4.11 +Requires: python-rtslib >= 2.1.fb68 Requires: python-cryptography Requires: python-flask >= 0.10.1 Requires: python-configshell +%if 0%{?rhel} == 7 +Requires: pyOpenSSL +%else +Requires: python-pyOpenSSL +%endif %else BuildRequires: python3-devel BuildRequires: python3-setuptools Requires: python3-rados >= 10.2.2 Requires: python3-rbd >= 10.2.2 Requires: python3-netifaces >= 0.10.4 -Requires: python3-rtslib >= 2.1.fb67 +Requires: python3-rtslib >= 2.1.fb68 Requires: python3-cryptography -Requires: python3-rpm >= 4.11 +Requires: python3-pyOpenSSL %if 0%{?suse_version} BuildRequires: python-rpm-macros BuildRequires: fdupes @@ -72,7 +75,7 @@ %endif %endif -%if 0%{?rhel} == 7 +%if 0%{?rhel} BuildRequires: systemd %else BuildRequires: systemd-rpm-macros @@ -131,7 +134,6 @@ install -m 0644 gwcli.8 %{buildroot}%{_mandir}/man8/ gzip %{buildroot}%{_mandir}/man8/gwcli.8 mkdir -p %{buildroot}%{_unitdir}/rbd-target-gw.service.d -install -m 0644 .%{_sysconfdir}/systemd/system/rbd-target-gw.service.d/dependencies.conf %{buildroot}%{_unitdir}/rbd-target-gw.service.d/ %if 0%{?suse_version} mkdir -p %{buildroot}%{_sbindir} ln -s service %{buildroot}%{_sbindir}/rcrbd-target-gw @@ -178,7 +180,6 @@ %service_del_postun rbd-target-gw.service rbd-target-api.service %endif - %files %license LICENSE %license COPYING @@ -198,7 +199,6 @@ %attr(0770,root,root) %dir %{_localstatedir}/log/rbd-target-gw %attr(0770,root,root) %dir %{_localstatedir}/log/rbd-target-api %dir %{_unitdir}/rbd-target-gw.service.d -%{_unitdir}/rbd-target-gw.service.d/dependencies.conf %if 0%{?suse_version} %{_sbindir}/rcrbd-target-gw %{_sbindir}/rcrbd-target-api diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/client.py new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/client.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/client.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/client.py 2019-09-10 09:17:24.087333799 +0200 @@ -339,8 +339,8 @@ try: self.logger.debug("Updating the ACL") - if username != self.acl.chap_userid or \ - password != self.acl.chap_password: + if username != acl_chap_userid or \ + password != acl_chap_password: self.acl.chap_userid = username self.acl.chap_password = password @@ -353,8 +353,8 @@ self.error_msg = new_chap.error_msg return - if mutual_username != self.acl.chap_mutual_userid or \ - mutual_password != self.acl.chap_mutual_password: + if mutual_username != acl_chap_mutual_userid or \ + mutual_password != acl_chap_mutual_password: self.acl.chap_mutual_userid = mutual_username self.acl.chap_mutual_password = mutual_password diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/common.py new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/common.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/common.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/common.py 2019-09-10 09:17:24.087333799 +0200 @@ -1,4 +1,5 @@ import rados +import socket import time import json import traceback @@ -55,7 +56,7 @@ 'mutual_username': '', 'mutual_password': '', 'mutual_password_encryption_enabled': False}, - "version": 9, + "version": 10, "epoch": 0, "created": '', "updated": '' @@ -315,6 +316,44 @@ self.update_item("targets", target_iqn, target) self.update_item("version", None, 9) + if self.config['version'] == 9 or self.config['version'] == 9.5: + # temporary field to store the gateways already upgraded from v9 to v10 + gateways_upgraded = self.config.get('gateways_upgraded') + if not gateways_upgraded: + gateways_upgraded = [] + self.add_item('gateways_upgraded', None, gateways_upgraded) + this_shortname = socket.gethostname().split('.')[0] + this_fqdn = socket.getfqdn() + if this_fqdn not in gateways_upgraded: + gateways_config = self.config['gateways'] + gateway_config = gateways_config.get(this_shortname) + if gateway_config: + gateways_config.pop(this_shortname) + gateways_config[this_fqdn] = gateway_config + self.update_item("gateways", None, gateways_config) + for target_iqn, target in self.config['targets'].items(): + portals_config = target['portals'] + portal_config = portals_config.get(this_shortname) + if portal_config: + portals_config.pop(this_shortname) + portals_config[this_fqdn] = portal_config + self.update_item("targets", target_iqn, target) + for disk_id, disk in self.config['disks'].items(): + if disk.get('allocating_host') == this_shortname: + disk['allocating_host'] = this_fqdn + if disk.get('owner') == this_shortname: + disk['owner'] = this_fqdn + self.update_item("disks", disk_id, disk) + gateways_upgraded.append(this_fqdn) + self.update_item("gateways_upgraded", None, gateways_upgraded) + if any(gateway_name not in gateways_upgraded + for gateway_name in self.config['gateways'].keys()): + # upgrade from v9 to v10 is still in progress, some gateways are not upgraded yet + self.update_item("version", None, 9.5) + else: + self.del_item("gateways_upgraded", None) + self.update_item("version", None, 10) + self.commit("retain") def init_config(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/gateway.py new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/gateway.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/gateway.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/gateway.py 2019-09-10 09:17:24.087333799 +0200 @@ -40,7 +40,7 @@ client_name=conf.cluster_client_name, cephconf=conf.cephconf), stderr=subprocess.STDOUT, shell=True) - if "un-blacklisting" in result: + if ("un-blacklisting" in result) or ("isn't blacklisted" in result): self.logger.info("Successfully removed blacklist entry") return True else: @@ -160,7 +160,7 @@ # 0, this is a boot time request self.logger.info("Setting up {}".format(target_iqn)) - target = GWTarget(self.logger, self.config, target_iqn, gw_ip_list, + target = GWTarget(self.logger, target_iqn, gw_ip_list, enable_portal=self.portals_active(target_iqn)) if target.error: raise CephiSCSIError("Error initializing iSCSI target: " @@ -241,7 +241,7 @@ def delete_target(self, target_iqn): - target = GWTarget(self.logger, self.config, target_iqn, {}) + target = GWTarget(self.logger, target_iqn, {}) if target.error: raise CephiSCSIError("Could not initialize target: {}". format(target.error_msg)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/lun.py new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/lun.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/lun.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/lun.py 2019-09-10 09:17:24.087333799 +0200 @@ -347,9 +347,7 @@ self.size_bytes = convert_2_bytes(size) self.config_key = '{}/{}'.format(self.pool, self.image) - # the allocating host could be fqdn or shortname - fqdn_enabled = settings.config.fqdn_enabled - self.allocating_host = allocating_host if fqdn_enabled else allocating_host.split('.')[0] + self.allocating_host = allocating_host self.backstore = backstore self.backstore_object_name = backstore_object_name @@ -555,7 +553,7 @@ # Add the mapping for the lun to ensure the block device is # present on all TPG's - gateway = GWTarget(self.logger, self.config, target_iqn, ip_list) + gateway = GWTarget(self.logger, target_iqn, ip_list) gateway.map_lun(self.config, so) if gateway.error: raise CephiSCSIError("LUN mapping failed - {}".format(gateway.error_msg)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/settings.py new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/settings.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/settings.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/settings.py 2019-09-10 09:17:24.087333799 +0200 @@ -24,6 +24,36 @@ class Settings(object): LIO_YES_NO_SETTINGS = ["immediate_data", "initial_r2t"] + LIO_INT_SETTINGS_LIMITS = { + "cmdsn_depth": { + "min": 1, "max": 512}, + "dataout_timeout": { + "min": 2, "max": 60}, + "nopin_response_timeout": { + "min": 3, "max": 60}, + "nopin_timeout": { + "min": 3, "max": 60}, + "first_burst_length": { + "min": 512, "max": 16777215}, + "max_burst_length": { + "min": 512, "max": 16777215}, + "max_outstanding_r2t": { + "min": 1, "max": 65535}, + "max_recv_data_segment_length": { + "min": 512, "max": 16777215}, + "max_xmit_data_segment_length": { + "min": 512, "max": 16777215}, + + "qfull_timeout": { + "min": 0, "max": 600}, + "hw_max_sectors": { + "min": 1, "max": 8192}, + "max_data_area_mb": { + "min": 1, "max": 2048}, + "osd_op_timeout": { + "min": 0, "max": 600} + } + _float_regex = re.compile(r"^[0-9]*\.{1}[0-9]$") _int_regex = re.compile(r"^[0-9]+$") @@ -56,7 +86,14 @@ value = int(raw_value) except ValueError: raise ValueError("expected integer for {}".format(key)) - + limits = Settings.LIO_INT_SETTINGS_LIMITS.get(key) + if limits is not None: + min = limits.get('min') + if min is not None and value < min: + raise ValueError("expected integer >= {} for {}".format(min, key)) + max = limits.get('max') + if max is not None and value > max: + raise ValueError("expected integer <= {} for {}".format(max, key)) controls[key] = value return controls @@ -117,17 +154,12 @@ "prometheus_exporter": "true", "prometheus_port": 9287, "prometheus_host": "::", - "logger_level": logging.DEBUG, - "fqdn_enabled": "false" + "logger_level": logging.DEBUG } - sync_required = ["cluster_name", - "pool", - "api_port", - "api_secure", - "minimum_gateways", - "prometheus_port" - ] + exclude_from_hash = ["cluster_client_name", + "logger_level" + ] target_defaults = {"osd_op_timeout": 30, "dataout_timeout": 20, @@ -186,6 +218,7 @@ self.error = False self.error_msg = '' + self._defined_settings = [] config = ConfigParser() dataset = config.read(conffile) @@ -230,6 +263,7 @@ v = settings[k] v = self.normalize(k, settings[k]) + self._defined_settings.append(k) self.__setattr__(k, v) def hash(self): @@ -239,8 +273,9 @@ """ sync_settings = {} - for setting in Settings.sync_required: - sync_settings[setting] = getattr(self, setting) + for setting in self._defined_settings: + if setting not in Settings.exclude_from_hash: + sync_settings[setting] = getattr(self, setting) h = hashlib.sha256() h.update(json.dumps(sync_settings).encode('utf-8')) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/target.py new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/target.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/target.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/target.py 2019-09-10 09:17:24.087333799 +0200 @@ -48,7 +48,7 @@ # gwcli to get/set all tpgs/clients under the target instead of per obj. SETTINGS = TPG_SETTINGS + TPG_KERNEL_SETTINGS + GWClient.SETTINGS - def __init__(self, logger, config, iqn, gateway_ip_list, enable_portal=True): + def __init__(self, logger, iqn, gateway_ip_list, enable_portal=True): """ Instantiate the class :param iqn: iscsi iqn name for the gateway @@ -62,7 +62,6 @@ self.enable_portal = enable_portal # boolean to trigger portal IP creation self.logger = logger # logger object - self.config = config try: iqn, iqn_type = normalize_wwn(['iqn'], iqn) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/utils.py new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/utils.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/ceph_iscsi_config/utils.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/ceph_iscsi_config/utils.py 2019-09-10 09:17:24.087333799 +0200 @@ -6,7 +6,6 @@ import re import datetime import os -import rpm import ceph_iscsi_config.settings as settings @@ -83,13 +82,13 @@ pass addrs = set() - for family in families: - try: - infos = socket.getaddrinfo(addr, 0, family) - for info in infos: + try: + infos = socket.getaddrinfo(addr, 0) + for info in infos: + if info[0] in families: addrs.add(info[4][0]) - except Exception: - pass + except Exception: + pass return list(addrs) @@ -279,35 +278,9 @@ def this_host(): """ - return the local machine's fqdn or shortname + return the local machine's fqdn """ - fqdn_enabled = settings.config.fqdn_enabled - return socket.getfqdn() if fqdn_enabled else socket.gethostname().split('.')[0] - - -def valid_rpm(in_rpm): - """ - check a given rpm matches the current installed rpm - :param in_rpm: a dict of name, version and release to check against - :return: bool representing whether the rpm is valid or not - """ - ts = rpm.TransactionSet() - mi = ts.dbMatch('name', in_rpm['name']) - if mi: - # check the version is OK - rpm_hdr = mi.next() - rc = rpm.labelCompare(('1', rpm_hdr['version'], rpm_hdr['release']), - ('1', in_rpm['version'], in_rpm['release'])) - - if rc < 0: - # -1 version old - return False - else: - # 0 = version match, 1 = version exceeds min requirement - return True - else: - # rpm not installed - return False + return socket.getfqdn() def encryption_available(): @@ -326,6 +299,20 @@ return all([os.path.exists(key) for key in keys]) +def read_os_release(): + os_release_file = '/etc/os-release' + d = {} + if not os.path.exists(os_release_file): + return d + with open(os_release_file) as f: + for line in f: + rs = line.rstrip() + if rs: + k, v = rs.split("=") + d[k] = v.strip('"') + return d + + def gen_control_string(controls): """ Generate a kernel control string from a given dictionary diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/etc/systemd/system/rbd-target-gw.service.d/dependencies.conf new/ceph-iscsi-3.2+1568099844.g09c5205/etc/systemd/system/rbd-target-gw.service.d/dependencies.conf --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/etc/systemd/system/rbd-target-gw.service.d/dependencies.conf 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/etc/systemd/system/rbd-target-gw.service.d/dependencies.conf 1970-01-01 01:00:00.000000000 +0100 @@ -1,4 +0,0 @@ -[Unit] -Wants=rbd-target-api.service -Requires=sys-kernel-config.mount -Before=rbd-target-api.service diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/gwcli/client.py new/ceph-iscsi-3.2+1568099844.g09c5205/gwcli/client.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/gwcli/client.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/gwcli/client.py 2019-09-10 09:17:24.087333799 +0200 @@ -1,10 +1,10 @@ from gwcli.node import UIGroup, UINode -from gwcli.utils import response_message, APIRequest, get_config, this_host +from gwcli.utils import response_message, APIRequest, get_config from ceph_iscsi_config.client import CHAP, GWClient import ceph_iscsi_config.settings as settings -from ceph_iscsi_config.utils import human_size +from ceph_iscsi_config.utils import human_size, this_host from rtslib_fb.utils import normalize_wwn, RTSLibError diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/gwcli/gateway.py new/ceph-iscsi-3.2+1568099844.g09c5205/gwcli/gateway.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/gwcli/gateway.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/gwcli/gateway.py 2019-09-10 09:17:24.091333818 +0200 @@ -5,11 +5,11 @@ from gwcli.hostgroup import HostGroups from gwcli.storage import Disks, TargetDisks from gwcli.client import Clients -from gwcli.utils import (this_host, response_message, GatewayAPIError, +from gwcli.utils import (response_message, GatewayAPIError, GatewayError, APIRequest, console_message, get_config) import ceph_iscsi_config.settings as settings -from ceph_iscsi_config.utils import (normalize_ip_address, format_lio_yes_no) +from ceph_iscsi_config.utils import (normalize_ip_address, format_lio_yes_no, this_host) from ceph_iscsi_config.target import GWTarget from gwcli.ceph import CephGroup diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/gwcli/storage.py new/ceph-iscsi-3.2+1568099844.g09c5205/gwcli/storage.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/gwcli/storage.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/gwcli/storage.py 2019-09-10 09:17:24.091333818 +0200 @@ -13,9 +13,9 @@ from gwcli.client import Clients from gwcli.utils import (console_message, response_message, GatewayAPIError, - this_host, APIRequest, valid_snapshot_name, get_config) + APIRequest, valid_snapshot_name, get_config) -from ceph_iscsi_config.utils import valid_size, convert_2_bytes, human_size +from ceph_iscsi_config.utils import valid_size, convert_2_bytes, human_size, this_host from ceph_iscsi_config.lun import LUN import ceph_iscsi_config.settings as settings @@ -154,8 +154,7 @@ def ui_command_attach(self, pool=None, image=None, backstore=None): """ - Create a LUN and assign to the gateway(s) - (RBD image must exist). + Assign a previously created RBD image to the gateway(s) The attach command supports two request formats; @@ -191,7 +190,7 @@ def ui_command_create(self, pool=None, image=None, size=None, backstore=None, count=1): """ - Create a LUN and assign to the gateway(s). + Create a RBD image and assign to the gateway(s). The create command supports two request formats; @@ -199,7 +198,7 @@ Short format : create pool/image <size> e.g. - create pool=rbd image=testimage size=100g max_data_area_mb=16 + create pool=rbd image=testimage size=100g create rbd.testimage 100g The syntax of each parameter is as follows; @@ -210,16 +209,16 @@ backstore : lio backstore count : integer (default is 1)[2]. If the request provides a count=<n> parameter the image name will be used as a prefix, and the count - used as a suffix to create multiple LUNs from the same request. + used as a suffix to create multiple images from the same request. e.g. create rbd.test 1g count=5 - -> create 5 LUNs called test1..test5 each of 1GB in size + -> create 5 images called test1..test5 each of 1GB in size from the rbd pool Notes. 1) size does not support decimal representations - 2) Using a count to create multiple LUNs will lock the CLI until all - LUNs have been created + 2) Using a count to create multiple images will lock the CLI until all + images have been created """ # NB the text above is shown on a help create request in the CLI diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/gwcli/utils.py new/ceph-iscsi-3.2+1568099844.g09c5205/gwcli/utils.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/gwcli/utils.py 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/gwcli/utils.py 2019-09-10 09:17:24.091333818 +0200 @@ -1,4 +1,3 @@ -import socket import requests from requests import Response import sys @@ -12,7 +11,7 @@ from ceph_iscsi_config.client import GWClient import ceph_iscsi_config.settings as settings from ceph_iscsi_config.utils import (resolve_ip_addresses, - CephiSCSIError) + CephiSCSIError, this_host) __author__ = 'Paul Cuzner' @@ -31,14 +30,6 @@ return content -def this_host(): - """ - return the local machine's fqdn or shortname - """ - fqdn_enabled = settings.config.fqdn_enabled - return socket.getfqdn() if fqdn_enabled else socket.gethostname().split('.')[0] - - def get_config(): """ use the /config api to return the current gateway configuration diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/gwcli.8 new/ceph-iscsi-3.2+1568099844.g09c5205/gwcli.8 --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/gwcli.8 2019-06-11 12:36:12.615206521 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/gwcli.8 2019-09-10 09:17:24.087333799 +0200 @@ -39,7 +39,7 @@ .PP \fBNB.\fR An example iscsi-gateway.cfg file is provided under /usr/share/doc/ceph-iscsi-config* .PP -Access to the API is normally restricted to the IP's of the gateway nodes, but you may also define other IP addresses that should be granted access to the API by adding the follwing entry to the configuration file; +Access to the API is normally restricted to the IP's of the gateway nodes, but you may also define other IP addresses that should be granted access to the API by adding the following entry to the configuration file; .PP .RS 3 \fBtrusted_ip_list = <ip_address,ip_address...>\fR diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/rbd-target-api.py new/ceph-iscsi-3.2+1568099844.g09c5205/rbd-target-api.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/rbd-target-api.py 2019-06-11 12:36:12.619206514 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/rbd-target-api.py 2019-09-10 09:17:24.091333818 +0200 @@ -13,11 +13,9 @@ import threading import time import inspect -import platform import copy from functools import (reduce, wraps) -from rpm import labelCompare import rados import rbd @@ -34,10 +32,10 @@ from ceph_iscsi_config.client import GWClient, CHAP from ceph_iscsi_config.common import Config from ceph_iscsi_config.utils import (normalize_ip_literal, resolve_ip_addresses, - ip_addresses, valid_rpm, - format_lio_yes_no, CephiSCSIError) + ip_addresses, read_os_release, + format_lio_yes_no, CephiSCSIError, this_host) -from gwcli.utils import (this_host, APIRequest, valid_gateway, valid_client, +from gwcli.utils import (APIRequest, valid_gateway, valid_client, valid_credentials, get_remote_gateways, valid_snapshot_name, GatewayAPIError) @@ -187,7 +185,11 @@ elif query_type == 'checkversions': - return jsonify(data='checks passed'), 200 + config_errors = pre_reqs_errors() + if config_errors: + return jsonify(data=config_errors), 500 + else: + return jsonify(data='checks passed'), 200 else: # Request Unknown @@ -280,7 +282,6 @@ gateway_ip_list = [] target = GWTarget(logger, - config, str(target_iqn), gateway_ip_list) @@ -369,7 +370,7 @@ def local_target_reconfigure(target_iqn, tpg_controls, client_controls): config.refresh() - target = GWTarget(logger, config, str(target_iqn), []) + target = GWTarget(logger, str(target_iqn), []) if target.error: logger.error("Unable to create an instance of the GWTarget class") return target.error_msg @@ -474,7 +475,7 @@ else: # DELETE target request - target = GWTarget(logger, config, target_iqn, '') + target = GWTarget(logger, target_iqn, '') if target.error: return jsonify(message="Failed to access target"), 500 @@ -847,7 +848,6 @@ target_config = config.config['targets'][target_iqn] ip_list = target_config.get('ip_list', []) gateway = GWTarget(logger, - config, target_iqn, ip_list) @@ -971,6 +971,8 @@ image_id = '{}/{}'.format(pool, image) + config.refresh() + if request.method == 'GET': if image_id in config.config['disks']: @@ -1110,6 +1112,8 @@ image_id = '{}/{}'.format(pool, image) + config.refresh() + if request.method == 'GET': if image_id in config.config['disks']: return jsonify(config.config["disks"][image_id]), 200 @@ -1177,7 +1181,6 @@ return jsonify(message="LUN resized"), 200 elif mode in ['activate', 'deactivate']: - config.refresh() disk = config.config['disks'].get(image_id, None) if not disk: return jsonify(message="rbd image {} not " @@ -1611,7 +1614,7 @@ committing_host = request.form['committing_host'] action = request.form['action'] - target = GWTarget(logger, config, target_iqn, []) + target = GWTarget(logger, target_iqn, []) acl_enabled = (action == 'enable_acl') @@ -2360,27 +2363,36 @@ """ target_default_controls = {} + target_controls_limits = {} settings_list = GWTarget.SETTINGS for k in settings_list: default_val = getattr(settings.config, k, None) if k in settings.Settings.LIO_YES_NO_SETTINGS: default_val = format_lio_yes_no(default_val) + elif k in settings.Settings.LIO_INT_SETTINGS_LIMITS: + target_controls_limits[k] = settings.Settings.LIO_INT_SETTINGS_LIMITS[k] target_default_controls[k] = default_val disk_default_controls = {} + disk_controls_limits = {} required_rbd_features = {} unsupported_rbd_features = {} for backstore, ks in LUN.SETTINGS.items(): disk_default_controls[backstore] = {} + disk_controls_limits[backstore] = {} for k in ks: default_val = getattr(settings.config, k, None) disk_default_controls[backstore][k] = default_val + if k in settings.Settings.LIO_INT_SETTINGS_LIMITS: + disk_controls_limits[backstore][k] = settings.Settings.LIO_INT_SETTINGS_LIMITS[k] required_rbd_features[backstore] = RBDDev.required_features(backstore) unsupported_rbd_features[backstore] = RBDDev.unsupported_features(backstore) return jsonify({ 'target_default_controls': target_default_controls, + 'target_controls_limits': target_controls_limits, 'disk_default_controls': disk_default_controls, + 'disk_controls_limits': disk_controls_limits, 'unsupported_rbd_features': unsupported_rbd_features, 'required_rbd_features': required_rbd_features, 'backstores': LUN.BACKSTORES, @@ -2601,64 +2613,39 @@ def pre_reqs_errors(): """ - function to check pre-req rpms are installed and at the relevant versions + function to check pre-reqs are installed and at the relevant versions :return: list of configuration errors detected """ dist_translations = { - "centos": "redhat"} + "centos": "redhat", + "opensuse-leap": "suse"} valid_dists = { - "redhat": 7.4} - - required_rpms = [ - {"name": "python-rtslib", - "version": "2.1.fb64", - "release": "0.1"}, - {"name": "tcmu-runner", - "version": "1.3.0", - "release": "0.2.3"} - ] - - k_vers = '3.10.0' - k_rel = '823.el7' + "redhat": 7.4, + "suse": 15.1, + "debian": 10} errors_found = [] - dist, rel, dist_id = platform.linux_distribution(full_distribution_name=0) + os_release = read_os_release() + dist = os_release.get('ID', '') + rel = os_release.get('VERSION_ID') dist = dist.lower() dist = dist_translations.get(dist, dist) if dist in valid_dists: + if dist == 'redhat': + import platform + _, rel, _ = platform.linux_distribution(full_distribution_name=0) # CentOS formats a release similar 7.4.1708 rel = float(".".join(rel.split('.')[:2])) if rel < valid_dists[dist]: errors_found.append("OS version is unsupported") - # check rpm versions are OK - for rpm in required_rpms: - if not valid_rpm(rpm): - logger.error("RPM check for {} failed".format(rpm['name'])) - errors_found.append("{} rpm must be installed at >= " - "{}-{}".format(rpm['name'], - rpm['version'], - rpm['release'])) else: errors_found.append("OS is unsupported") - # check the running kernel is OK (required kernel has patches to rbd.ko) - os_info = os.uname() - this_arch = os_info[-1] - this_kernel = os_info[2].replace(".{}".format(this_arch), '') - this_ver, this_rel = this_kernel.split('-', 1) - - # use labelCompare from the rpm module to handle the comparison - if labelCompare(('1', this_ver, this_rel), ('1', k_vers, k_rel)) < 0: - logger.error("Kernel version check failed") - errors_found.append("Kernel version too old - {}-{} " - "or above needed".format(k_vers, - k_rel)) - return errors_found diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/setup.py new/ceph-iscsi-3.2+1568099844.g09c5205/setup.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/setup.py 2019-06-11 12:36:12.619206514 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/setup.py 2019-09-10 09:17:24.091333818 +0200 @@ -28,7 +28,7 @@ setup( name="ceph_iscsi", - version="3.0", + version="3.2", description="Common classes/functions and CLI tools used to configure iSCSI " "gateways backed by Ceph RBD", long_description=long_description, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/test/test_common.py new/ceph-iscsi-3.2+1568099844.g09c5205/test/test_common.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/test/test_common.py 2019-06-11 12:36:12.619206514 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/test/test_common.py 2019-09-10 09:17:24.091333818 +0200 @@ -25,11 +25,26 @@ def test_upgrade_config(self): gateway_conf_initial = json.dumps(self.gateway_conf_initial) + + # First, the upgrade is executed on node1 with mock.patch.object(Config, 'init_config', return_value=True), \ mock.patch.object(Config, '_read_config_object', return_value=gateway_conf_initial), \ - mock.patch.object(Config, 'commit'): + mock.patch.object(Config, 'commit'), \ + mock.patch("socket.gethostname", return_value='node1'), \ + mock.patch("socket.getfqdn", return_value='node1.ceph.local'): + config = Config(self.logger) + + # And then, the upgrade is executed on node2 + current_config = json.dumps(config.config) + with mock.patch.object(Config, 'init_config', return_value=True), \ + mock.patch.object(Config, '_read_config_object', + return_value=current_config), \ + mock.patch.object(Config, 'commit'), \ + mock.patch("socket.gethostname", return_value='node2'),\ + mock.patch("socket.getfqdn", return_value='node2.ceph.local'): config = Config(self.logger) + self.maxDiff = None iqn = 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw' @@ -148,7 +163,7 @@ }, "created": "2018/12/07 09:18:04", "image": "disk_1", - "owner": "node1", + "owner": "node1.ceph.local", "backstore": "user:rbd", "backstore_object_name": "rbd.disk_1", "pool": "rbd", @@ -167,12 +182,12 @@ }, "epoch": 19, "gateways": { - "node1": { + "node1.ceph.local": { "active_luns": 1, "created": "2018/12/07 09:18:07", "updated": "2018/12/07 09:18:08" }, - "node2": { + "node2.ceph.local": { "active_luns": 0, "created": "2018/12/07 09:18:09", "updated": "2018/12/07 09:18:10" @@ -224,7 +239,7 @@ "192.168.100.202" ], "portals": { - "node1": { + "node1.ceph.local": { "gateway_ip_list": [ "192.168.100.201", "192.168.100.202" @@ -235,7 +250,7 @@ "portal_ip_addresses": ["192.168.100.201"], "tpgs": 2 }, - "node2": { + "node2.ceph.local": { "gateway_ip_list": [ "192.168.100.201", "192.168.100.202" @@ -251,5 +266,5 @@ } }, "updated": "2018/12/07 09:18:13", - "version": 9 + "version": 10 } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/test/test_settings.py new/ceph-iscsi-3.2+1568099844.g09c5205/test/test_settings.py --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/test/test_settings.py 1970-01-01 01:00:00.000000000 +0100 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/test/test_settings.py 2019-09-10 09:17:24.091333818 +0200 @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import unittest + +from ceph_iscsi_config.settings import Settings +from ceph_iscsi_config.target import GWTarget + + +class SettingsTest(unittest.TestCase): + + @staticmethod + def _normalize(controls): + return Settings.normalize_controls(controls, GWTarget.SETTINGS) + + def test_normalize_controls_int(self): + self.assertEqual( + SettingsTest._normalize({'dataout_timeout': 3}), {'dataout_timeout': 3}) + self.assertEqual( + SettingsTest._normalize({'dataout_timeout': '3'}), {'dataout_timeout': 3}) + + with self.assertRaises(ValueError) as cm: + SettingsTest._normalize({'dataout_timeout': 1}) + self.assertEqual('expected integer >= 2 for dataout_timeout', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + SettingsTest._normalize({'dataout_timeout': 64}) + self.assertEqual('expected integer <= 60 for dataout_timeout', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + SettingsTest._normalize({'dataout_timeout': '64'}) + self.assertEqual('expected integer <= 60 for dataout_timeout', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + SettingsTest._normalize({'dataout_timeout': 'abc'}) + self.assertEqual('expected integer for dataout_timeout', str(cm.exception)) + + def test_normalize_controls_yes_no(self): + self.assertEqual( + SettingsTest._normalize({'immediate_data': 'Yes'}), {'immediate_data': True}) + self.assertEqual( + SettingsTest._normalize({'immediate_data': 'yes'}), {'immediate_data': True}) + self.assertEqual( + SettingsTest._normalize({'immediate_data': True}), {'immediate_data': True}) + self.assertEqual( + SettingsTest._normalize({'immediate_data': 'True'}), {'immediate_data': True}) + self.assertEqual( + SettingsTest._normalize({'immediate_data': 'true'}), {'immediate_data': True}) + self.assertEqual( + SettingsTest._normalize({'immediate_data': '1'}), {'immediate_data': True}) + + self.assertEqual( + SettingsTest._normalize({'immediate_data': 'No'}), {'immediate_data': False}) + self.assertEqual( + SettingsTest._normalize({'immediate_data': 'no'}), {'immediate_data': False}) + self.assertEqual( + SettingsTest._normalize({'immediate_data': False}), {'immediate_data': False}) + self.assertEqual( + SettingsTest._normalize({'immediate_data': 'False'}), {'immediate_data': False}) + self.assertEqual( + SettingsTest._normalize({'immediate_data': 'false'}), {'immediate_data': False}) + self.assertEqual( + SettingsTest._normalize({'immediate_data': '0'}), {'immediate_data': False}) + + with self.assertRaises(ValueError) as cm: + SettingsTest._normalize({'immediate_data': 'abc'}) + self.assertEqual('expected yes or no for immediate_data', str(cm.exception)) + + with self.assertRaises(ValueError) as cm: + SettingsTest._normalize({'immediate_data': 123}) + self.assertEqual('expected yes or no for immediate_data', str(cm.exception)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ceph-iscsi-3.0+1560249372.g70ec7a9/tox.ini new/ceph-iscsi-3.2+1568099844.g09c5205/tox.ini --- old/ceph-iscsi-3.0+1560249372.g70ec7a9/tox.ini 2019-06-11 12:36:12.619206514 +0200 +++ new/ceph-iscsi-3.2+1568099844.g09c5205/tox.ini 2019-09-10 09:17:24.091333818 +0200 @@ -22,6 +22,5 @@ cryptography rtslib_fb netifaces - rpm commands= - {envbindir}/py.test --ignore=test/test_group.py test/ \ No newline at end of file + {envbindir}/py.test --ignore=test/test_group.py test/ ++++++ checkin.sh ++++++ --- /var/tmp/diff_new_pack.mUUA5H/_old 2019-09-11 10:33:51.879322858 +0200 +++ /var/tmp/diff_new_pack.mUUA5H/_new 2019-09-11 10:33:51.879322858 +0200 @@ -98,7 +98,9 @@ VERSION="${VERSION}+$(date +%s).${GIT_SHA1}" sed -i -e 's/^Version:.*/Version: '$VERSION'/' $PROJECT.spec sed -i -e 's#^Source0:.*#Source0: %{name}-%{version}.tar.gz#' $PROJECT.spec -sed -i -e '/Source0/a %if 0%{?suse_version}\nSource98: checkin.sh\nSource99: README-checkin.txt\n%endif' $PROJECT.spec +sed -i -e '/Source0/a %if 0%{?suse_version}\nSource98: checkin.sh\nSource99: README-checkin.txt\nExclusiveArch: x86_64 aarch64 ppc64le s390x\n%endif' $PROJECT.spec +sed -i -e '/BuildArch:\s\+noarch/d' $PROJECT.spec +sed -i -e 'N;/^\n$/D;P;D;' $PROJECT.spec # collapse multiple adjacent newlines down to a single newline cp $PROJECT.spec $THIS_DIR echo "Version number is ->$VERSION<-" cd ..