diff --git a/Makefile b/Makefile index f975c6ee799aca2f853cedead2f3f469cd8e8919..f72f324a458405df8af8688d0f3a46c65be5957a 100644 --- a/Makefile +++ b/Makefile @@ -22,8 +22,6 @@ VERSION = $$(basename $$(git describe --tags | tr - . | grep -o '[0-9].*$$')) all: @echo "Usage:" @echo - @echo " make deb - Builds debian packages." - @echo " make rpm - Builds rpm packages." @echo " make release - Generates the release tarball." @echo @echo " make clean - Cleanup the local repository build files." @@ -32,15 +30,7 @@ all: clean: @rm -fv ${NAME}/*.pyc ${NAME}/*.html @rm -frv ${NAME}.egg-info MANIFEST build - @rm -frv debian/tmp - @rm -fv build-stamp - @rm -fv dpkg-buildpackage.log dpkg-buildpackage.version - @rm -frv *.rpm - @rm -fv debian/files debian/*.log debian/*.substvars - @rm -frv debian/${PKGNAME}-doc/ debian/python2.5-${PKGNAME}/ - @rm -frv debian/python2.6-${PKGNAME}/ debian/python-${PKGNAME}/ @rm -frv results - @rm -fv rpm/*.spec *.spec rpm/sed* sed* @rm -frv ${PKGNAME}-* @echo "Finished cleanup." @@ -59,41 +49,6 @@ build/release-stamp: @echo "Fixing version string..." @sed -i "s/__version__ = .*/__version__ = '${VERSION}'/g" \ build/${PKGNAME}-${VERSION}/${NAME}/__init__.py - @echo "Generating rpm specfile from template..." - @cd build/${PKGNAME}-${VERSION}; \ - for spectmpl in rpm/*.spec.tmpl; do \ - sed -i "s/Version:\( *\).*/Version:\1${VERSION}/g" $${spectmpl}; \ - mv $${spectmpl} $$(basename $${spectmpl} .tmpl); \ - done; \ - rm -r rpm - @echo "Generating rpm changelog..." - @( \ - version=$$(basename $$(git describe HEAD --tags | tr - .)); \ - author=$$(git show HEAD --format="format:%an <%ae>" -s); \ - date=$$(git show HEAD --format="format:%ad" -s \ - | awk '{print $$1,$$2,$$3,$$5}'); \ - hash=$$(git show HEAD --format="format:%H" -s); \ - echo '* '"$${date} $${author} $${version}-1"; \ - echo " - Generated from git commit $${hash}."; \ - ) >> $$(ls build/${PKGNAME}-${VERSION}/*.spec) - @echo "Generating debian changelog..." - @( \ - version=$$(basename $$(git describe HEAD --tags | tr - . | grep -o '[0-9].*$$')); \ - author=$$(git show HEAD --format="format:%an <%ae>" -s); \ - date=$$(git show HEAD --format="format:%aD" -s); \ - day=$$(git show HEAD --format='format:%ai' -s \ - | awk '{print $$1}' \ - | awk -F '-' '{print $$3}' | sed 's/^0/ /g'); \ - date=$$(echo $${date} \ - | awk '{print $$1, "'"$${day}"'", $$3, $$4, $$5, $$6}'); \ - hash=$$(git show HEAD --format="format:%H" -s); \ - echo "${PKGNAME} ($${version}) unstable; urgency=low"; \ - echo; \ - echo " * Generated from git commit $${hash}."; \ - echo; \ - echo " -- $${author} $${date}"; \ - echo; \ - ) > build/${PKGNAME}-${VERSION}/debian/changelog @find build/${PKGNAME}-${VERSION}/ -exec \ touch -t $$(date -d @$$(git show -s --format="format:%at") \ +"%Y%m%d%H%M.%S") {} \; @@ -101,32 +56,9 @@ build/release-stamp: @cd build; tar -c --owner=0 --group=0 --numeric-owner \ --format=gnu -b20 --quoting-style=escape \ -f ../dist/${PKGNAME}-${VERSION}.tar \ - $$(find ${PKGNAME}-${VERSION} -type f | sort) + $$(find ${PKGNAME}-${VERSION} -type f | sort) \ + $$(find ${PKGNAME}-${VERSION} -type l | sort) @gzip -6 -n dist/${PKGNAME}-${VERSION}.tar @echo "Generated release tarball:" @echo " $$(ls dist/${PKGNAME}-${VERSION}.tar.gz)" @touch build/release-stamp - -deb: release build/deb-stamp -build/deb-stamp: - @echo "Building debian packages..." - @cd build/${PKGNAME}-${VERSION}; \ - dpkg-buildpackage -rfakeroot -us -uc - @mv build/*_${VERSION}_*.deb dist/ - @echo "Generated debian packages:" - @for pkg in $$(ls dist/*_${VERSION}_*.deb); do echo " $${pkg}"; done - @touch build/deb-stamp - -rpm: release build/rpm-stamp -build/rpm-stamp: - @echo "Building rpm packages..." - @mkdir -p build/rpm - @build=$$(pwd)/build/rpm; dist=$$(pwd)/dist/; rpmbuild \ - --define "_topdir $${build}" --define "_sourcedir $${dist}" \ - --define "_rpmdir $${build}" --define "_buildir $${build}" \ - --define "_srcrpmdir $${build}" -ba build/${PKGNAME}-${VERSION}/*.spec - @mv build/rpm/*-${VERSION}*.src.rpm dist/ - @mv build/rpm/*/*-${VERSION}*.rpm dist/ - @echo "Generated rpm packages:" - @for pkg in $$(ls dist/*-${VERSION}*.rpm); do echo " $${pkg}"; done - @touch build/rpm-stamp diff --git a/README.md b/README.md index 0b40a431f60c2aa5da49357ddd565a8b21514d41..89f69787c5ff55260e1a147e7c27a319fbb574f5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A Python object API for managing the Linux LIO kernel target rtslib-fb is an object-based Python library for configuring the LIO generic SCSI target, present in 3.x Linux kernel versions. -This runs with Python 2 and 2to3 is run by setup.py to run on Python 3. +It supports both Python 2 and Python 3, thanks to the python-six library. rtslib-fb development --------------------- @@ -16,19 +16,18 @@ Since rtslib-fb is used most often with targetcli-fb, the targetcli-fb mailing should be used for rtslib-fb discussion. * Mailing list: [targetcli-fb-devel](https://lists.fedorahosted.org/mailman/listinfo/targetcli-fb-devel) - * Source repo: [GitHub](https://github.com/agrover/rtslib-fb) - * Bugs: [GitHub](https://github.com/agrover/rtslib-fb/issues) or [Trac](https://fedorahosted.org/targetcli-fb/) + * Source repo: [GitHub](https://github.com/open-iscsi/rtslib-fb) + * Bugs: [GitHub](https://github.com/open-iscsi/rtslib-fb/issues) or [Trac](https://fedorahosted.org/targetcli-fb/) * Tarballs: [fedorahosted](https://fedorahosted.org/releases/t/a/targetcli-fb/) -In-repo packaging ------------------ -Packaging scripts for RPM and DEB are included, but these are to make end-user -custom packaging easier -- distributions tend to maintain their own packaging -scripts separately. If you run into issues with packaging, start with opening -a bug on your distro's bug reporting system. - -Some people do use these scripts, so we want to keep them around. Fixes for -any breakage you encounter are welcome. +Packages +-------- +rtslib-fb is packaged for a number of Linux distributions including +RHEL, +[Fedora](https://apps.fedoraproject.org/packages/python-rtslib), +openSUSE, Arch Linux, +[Gentoo](https://packages.gentoo.org/packages/dev-python/rtslib-fb), and +[Debian](https://tracker.debian.org/pkg/python-rtslib-fb). "fb" -- "free branch" --------------------- diff --git a/debian/changelog b/debian/changelog index 20250726789b9d9dce7dace7611eef8d59835db3..d6d70784dba436be92045e2b378a74257df25267 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -python-rtslib-fb (2.1.57+debian-4) UNRELEASED; urgency=medium +python-rtslib-fb (2.1.67-1) UNRELEASED; urgency=medium [ Ondřej Nový ] * Fixed VCS URLs (https). @@ -20,7 +20,10 @@ python-rtslib-fb (2.1.57+debian-4) UNRELEASED; urgency=medium [ Ondřej Nový ] * d/control: Set Vcs-* to salsa.debian.org - -- Ondřej Nový Sun, 28 Feb 2016 15:49:37 +0100 + [ Christophe Vu-Brugier ] + * New upstream version. + + -- Christophe Vu-Brugier Sat, 17 Feb 2018 21:43:08 +0100 python-rtslib-fb (2.1.57+debian-3) unstable; urgency=medium diff --git a/debian/control b/debian/control index 1dd66e77034a0083296958bf6115dfe902f64fb3..2db164a54f1c5f169097147b7167d7c3235f9199 100644 --- a/debian/control +++ b/debian/control @@ -19,11 +19,12 @@ Build-Depends-Indep: Standards-Version: 4.1.0 Vcs-Browser: https://salsa.debian.org/openstack-team/python/python-rtslib-fb Vcs-Git: https://salsa.debian.org/openstack-team/python/python-rtslib-fb.git -Homepage: https://github.com/agrover/rtslib-fb +Homepage: https://github.com/open-iscsi/rtslib-fb Package: python-rtslib-fb Architecture: all Depends: + python-pyudev, python-six, ${misc:Depends}, ${python:Depends}, @@ -67,6 +68,7 @@ Package: python3-rtslib-fb Architecture: all Depends: python-rtslib-fb, + python3-pyudev, python3-six, ${misc:Depends}, ${python3:Depends}, diff --git a/debian/copyright b/debian/copyright index 15dcdd9cdbe10a0043509f5dbc2f38fc8921f12a..cba7065c6e828ff5b17c52f0e5f22b027df8dcd9 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,6 +1,6 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: rtslib-fb -Source: https://github.com/agrover/rtslib-fb +Source: https://github.com/open-iscsi/rtslib-fb Files: debian/* Copyright: 2009-2013, Jerome Martin diff --git a/debian/patches/fix-path-of-etc-saveconfig.json.patch b/debian/patches/fix-path-of-etc-saveconfig.json.patch index ccf00922cc30d87f59cc577e1d077340c30a155e..2f4884d42e8f19279669cb227d49f0efdc075d97 100644 --- a/debian/patches/fix-path-of-etc-saveconfig.json.patch +++ b/debian/patches/fix-path-of-etc-saveconfig.json.patch @@ -22,12 +22,21 @@ Index: python-rtslib-fb/rtslib/root.py =================================================================== --- python-rtslib-fb.orig/rtslib/root.py +++ python-rtslib-fb/rtslib/root.py -@@ -29,7 +29,7 @@ from .tcm import so_mapping, StorageObje - from .utils import RTSLibError, modprobe, mount_configfs - from .utils import dict_remove, set_attributes +@@ -32,7 +32,7 @@ from .utils import dict_remove, set_attr + from .utils import fread, fwrite + from .alua import ALUATargetPortGroup -default_save_file = "/etc/target/saveconfig.json" +default_save_file = "/etc/rtslib-fb-target/saveconfig.json" class RTSRoot(CFSNode): ''' +@@ -63,7 +63,7 @@ class RTSRoot(CFSNode): + # this should match the kernel target driver default db dir + _default_dbroot = "/var/target" + # this is where the target DB is to be located (instead of the default) +- _preferred_dbroot = "/etc/target" ++ _preferred_dbroot = "/etc/rtslib-fb-target" + + def __init__(self): + ''' diff --git a/debian/rules b/debian/rules index 6125919cfd5b5c77692bd1b791b67d3f5f9be807..d3bebae9c5beb3f9b9d02e22f71d61de8baf20ab 100755 --- a/debian/rules +++ b/debian/rules @@ -6,7 +6,7 @@ PYTHON3S:=$(shell py3versions -vr) #prevent internet access to use PyPi export http_proxy = http://127.0.0.1:9 -UPSTREAM_GIT := https://github.com/agrover/rtslib-fb.git +UPSTREAM_GIT := https://github.com/open-iscsi/rtslib-fb.git -include /usr/share/openstack-pkg-tools/pkgos.make %: diff --git a/debian/watch b/debian/watch index 10dd34fb1c9ea6bb2f6144092ad013a3b22c48d3..886293cce83331d73a13561d5c68f325ce708ed6 100644 --- a/debian/watch +++ b/debian/watch @@ -1,4 +1,3 @@ -version=3 -opts="uversionmangle=s/v//" \ -opts="uversionmangle=s/fb//" \ -https://github.com/agrover/rtslib-fb/tags .*/(\d[\d\.]+)\.tar\.gz +version=4 +opts=uversionmangle=s/fb//,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/rtslib-fb-$1\.tar\.gz/ \ + https://github.com/open-iscsi/rtslib-fb/releases .*/v?(\d\S+)\.tar\.gz diff --git a/doc/saveconfig.json.5 b/doc/saveconfig.json.5 index b22efb1594a530252c2bb17c3d093c4cfe446112..42c63177cbf4c93abc8c353ca82a1020e8299931 100644 --- a/doc/saveconfig.json.5 +++ b/doc/saveconfig.json.5 @@ -195,6 +195,8 @@ values are (number). .I iser (boolean) is an optional value to enable iSER. +.I offload +(boolean) is an optional value to enable hardware offload. .SS node_acls This contains information about explicit initiator LUN mappings. @@ -243,4 +245,4 @@ Man page written by Andy Grover . .SH REPORTING BUGS Report bugs via .br -or +or diff --git a/doc/targetctl.8 b/doc/targetctl.8 index f87aaaf29121ddaf6cb9de4670f8d8afaa2d8865..923383c2fffdb1dcdb09c702b01ea6d71483ad2d 100644 --- a/doc/targetctl.8 +++ b/doc/targetctl.8 @@ -25,7 +25,7 @@ permissions will be set to only allow root access. If .B config-file is not supplied, .B targetctl -will use the default file location, +will use the default file location, .BR /etc/target/saveconfig.json. .P .B targetctl restore [config-file] @@ -60,4 +60,4 @@ Man page written by Andy Grover . .SH REPORTING BUGS Report bugs via .br -or +or diff --git a/example-debian/README.Debian b/example-debian/README.Debian deleted file mode 100644 index 78a1f14d6cdc0566e952bfeaff0517f8a603fb10..0000000000000000000000000000000000000000 --- a/example-debian/README.Debian +++ /dev/null @@ -1,13 +0,0 @@ -Copyright (c) 2011-2013 by Datera, Inc - -Licensed under the Apache License, Version 2.0 (the "License"); you may -not use this file except in compliance with the License. You may obtain -a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations -under the License. diff --git a/example-debian/compat b/example-debian/compat deleted file mode 100644 index 7f8f011eb73d6043d2e6db9d2c101195ae2801f2..0000000000000000000000000000000000000000 --- a/example-debian/compat +++ /dev/null @@ -1 +0,0 @@ -7 diff --git a/example-debian/control b/example-debian/control deleted file mode 100644 index 86ddfc2fec306121cfb8e8c65b96006237de584f..0000000000000000000000000000000000000000 --- a/example-debian/control +++ /dev/null @@ -1,29 +0,0 @@ -Source: rtslib-fb -Section: python -Priority: optional -Maintainer: Andy Grover -Build-Depends: debhelper(>= 8), python, python3 , python-epydoc, python-setuptools, python3-setuptools -Standards-Version: 3.9.4 -X-Python-Version: >= 2.6 -X-Python3-Version: >= 3.1 - -Package: python-rtslib-fb-docs -Architecture: all -Description: RisingTide Systems LIO target Python API (Documentation). - -Package: python-rtslib-fb -Architecture: all -Depends: ${python:Depends}, ${misc:Depends} -Provides: ${python:Provides} -Suggests: python-rtslib-fb-doc -Conflicts: python-rtslib, rtsadmin-frozen -Description: RisingTide Systems LIO target Python 2 API (free branch). - -Package: python3-rtslib-fb -Architecture: all -Depends: ${python3:Depends}, ${misc:Depends} -Suggests: python-rtslib-fb-doc -Conflicts: lio-utils -Replaces: targetcli-fb (<< 2.1.fb32) -Description: RisingTide Systems LIO target Python 3 API (free branch). - This includes the LIO rc scripts to start and stop the target. diff --git a/example-debian/copyright b/example-debian/copyright deleted file mode 100644 index 638eeb93e132b0b711de90e0ed725d3e8733ee3f..0000000000000000000000000000000000000000 --- a/example-debian/copyright +++ /dev/null @@ -1,24 +0,0 @@ -This package was originally debianized by Jerome Martin -on Fri Nov 18 12:00:01 UTC 2009. It is currently maintained by Andy Grover -. - -Upstream Author: Jerome Martin - -Copyright: - -This file is part of RTSLib. -Copyright (c) 2011-2013 by Datera, Inc - -Licensed under the Apache License, Version 2.0 (the "License"); you may -not use this file except in compliance with the License. You may obtain -a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations -under the License. - - diff --git a/example-debian/python-rtslib-fb-docs.doc-base b/example-debian/python-rtslib-fb-docs.doc-base deleted file mode 100644 index 9d070d33640ee700d77e3d87cbad5471b5e812e1..0000000000000000000000000000000000000000 --- a/example-debian/python-rtslib-fb-docs.doc-base +++ /dev/null @@ -1,9 +0,0 @@ -Document: python-rtslib-fb -Title: python-rtslib documentation -Author: Jerome Martin -Abstract: Python library for configuring the Linux kernel-based multiprotocol SCSI target (LIO) -Section: Programming/Python - -Format: HTML -Index: /usr/share/doc/python-rtslib-fb-docs/html/index.html -Files: /usr/share/doc/python-rtslib-fb-docs/html/*.html diff --git a/example-debian/python-rtslib-fb.install b/example-debian/python-rtslib-fb.install deleted file mode 100644 index 05ca243b38effd1920d10a6f8d46d31a1c61dba2..0000000000000000000000000000000000000000 --- a/example-debian/python-rtslib-fb.install +++ /dev/null @@ -1,2 +0,0 @@ -usr/lib/python2* -usr/bin diff --git a/example-debian/python-rtslib-fb.manpages b/example-debian/python-rtslib-fb.manpages deleted file mode 100644 index f620da6365cd9eeaeed63a2105ec7401302f4e06..0000000000000000000000000000000000000000 --- a/example-debian/python-rtslib-fb.manpages +++ /dev/null @@ -1,2 +0,0 @@ -doc/saveconfig.json.5 -doc/targetctl.8 diff --git a/example-debian/python3-rtslib-fb.install b/example-debian/python3-rtslib-fb.install deleted file mode 100644 index bf7cd62f19ae9af97c89d65b7f54fc0b78f2eb34..0000000000000000000000000000000000000000 --- a/example-debian/python3-rtslib-fb.install +++ /dev/null @@ -1,2 +0,0 @@ -usr/lib/python3* -usr/bin diff --git a/example-debian/python3-rtslib-fb.manpages b/example-debian/python3-rtslib-fb.manpages deleted file mode 100644 index f620da6365cd9eeaeed63a2105ec7401302f4e06..0000000000000000000000000000000000000000 --- a/example-debian/python3-rtslib-fb.manpages +++ /dev/null @@ -1,2 +0,0 @@ -doc/saveconfig.json.5 -doc/targetctl.8 diff --git a/example-debian/python3-rtslib-fb.target.init b/example-debian/python3-rtslib-fb.target.init deleted file mode 100644 index b0110133944014b5e6e196d8ec892198731c53c8..0000000000000000000000000000000000000000 --- a/example-debian/python3-rtslib-fb.target.init +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -### BEGIN INIT INFO -# Provides: target -# Default-Start: 3 4 5 -# Default-Stop: 0 1 2 6 -# Required-Start: $local_fs $network -# Required-Stop: $local_fs $network -# Short-Description: Start LIO targets -# Description: Loads configfs and restores LIO config with targetctl -### END INIT INFO - - -case "$1" in - start) - echo "Loading lio configuration" - /usr/bin/targetctl restore - if [[ $? -gt 0 ]]; then - exit 1 - fi - ;; - - stop) - echo "Unloading lio configuration" - /usr/bin/targetctl clear - if [[ $? -gt 0 ]]; then - exit 1 - fi - ;; - - restart|force-reload) - $0 stop - sleep 3 - $0 start - ;; - - *) - echo "usage: $0 {start|stop|restart|force-reload}" -esac - -exit 0 diff --git a/example-debian/rules b/example-debian/rules deleted file mode 100755 index 6250cebeececd3da20fbe46447d8a99afe06a40b..0000000000000000000000000000000000000000 --- a/example-debian/rules +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/make -f - -build_dir = build -install_dir = $(CURDIR)/debian/tmp -pkgname = rtslib-fb -name = rtslib - -#export DH_VERBOSE=1 - -PYTHON2=$(shell pyversions -vr) -PYTHON3=$(shell py3versions -vr) - -#prevent internet access to use PyPi -export http_proxy = http://127.0.0.1:9 - - -%: - dh $@ --with python2,python3 - - -build-python%: - python$* setup.py build - -override_dh_auto_build: $(PYTHON3:%=build-python%) - dh_auto_build - - -install-python%: - python$* setup.py install --root=$(install_dir) --install-layout=deb - - -override_dh_auto_install: $(PYTHON3:%=install-python%) - dh_auto_install - # for using python 3 for targetctl - # even though it is overwritten by python2 setup - sed -i '1s|/usr/bin/python$$|/usr/bin/python3|' $(install_dir)/usr/bin/targetctl - - -override_dh_installdocs: - cd $(build_dir); epydoc --no-sourcecode --html -n $(pkgname) \ - --exclude configobj ../$(name)/*.py - dh_installdocs $(build_dir)/html - -override_dh_installinit: - dh_installinit --name target - - -override_dh_auto_clean: - dh_auto_clean - rm -rf build - rm -rf *.egg-info diff --git a/example-debian/source/format b/example-debian/source/format deleted file mode 100644 index 89ae9db8f88b823b6a7eabf55e203658739da122..0000000000000000000000000000000000000000 --- a/example-debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) diff --git a/example-rpm/python-rtslib.spec.tmpl b/example-rpm/python-rtslib.spec.tmpl deleted file mode 100644 index ae566b5caea418e8c3a92067f8e1f6f09c54bc21..0000000000000000000000000000000000000000 --- a/example-rpm/python-rtslib.spec.tmpl +++ /dev/null @@ -1,43 +0,0 @@ -%define oname rtslib - -Name: python-rtslib -License: Apache License 2.0 -Group: System Environment/Libraries -Summary: A framework to implement simple but nice CLIs. -Version: VERSION -Release: 1%{?dist} -URL: http://www.risingtidesystems.com/git/ -Source: %{oname}-%{version}.tar.gz -BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-rpmroot -BuildArch: noarch -BuildRequires: python-devel, epydoc -Vendor: Datera, Inc. - -%description -API for RisingTide Systems generic SCSI target. - -%prep -%setup -q -n %{oname}-%{version} - -%build -%{__python} setup.py build -mkdir -p doc -epydoc --no-sourcecode --html -n %{oname} --exclude configobj %{oname}/*.py -mv html doc/ - -%install -rm -rf %{buildroot} -%{__python} setup.py install --skip-build --root %{buildroot} --prefix usr -mkdir -p %{buildroot}/usr/share/doc/python-rtslib-doc-%{version} -cp -r doc/* %{buildroot}/usr/share/doc/python-rtslib-doc-%{version}/ - -%clean -rm -rf %{buildroot} - -%files -%defattr(-,root,root,-) -%{python_sitelib} -/usr/share/doc/python-rtslib-doc-%{version} -%doc COPYING README.md - -%changelog diff --git a/rtslib/__init__.py b/rtslib/__init__.py index c4b3cb6d2559e5198cea2916f2689fc2b751d7fa..568619e85f25cc9d2bef1b330aab39f0b45d07f0 100644 --- a/rtslib/__init__.py +++ b/rtslib/__init__.py @@ -23,6 +23,7 @@ if __name__ == "rtslib": from .root import RTSRoot from .utils import RTSLibError, RTSLibBrokenLink, RTSLibNotInCFS +from .utils import RTSLibALUANotSupported from .target import LUN, MappedLUN from .target import NodeACL, NetworkPortal, TPG, Target @@ -33,6 +34,8 @@ from .tcm import FileIOStorageObject, BlockStorageObject from .tcm import PSCSIStorageObject, RDMCPStorageObject, UserBackedStorageObject from .tcm import StorageObjectFactory +from .alua import ALUATargetPortGroup + __version__ = 'GIT_VERSION' __author__ = "Jerome Martin " __url__ = "http://www.risingtidesystems.com" diff --git a/rtslib/alua.py b/rtslib/alua.py new file mode 100644 index 0000000000000000000000000000000000000000..8a4b30db7e195aadcca837eb424205f0a677f82b --- /dev/null +++ b/rtslib/alua.py @@ -0,0 +1,397 @@ +''' +Implements the RTS ALUA Target Port Group class. + +This file is part of RTSLib. +Copyright (c) 2016 by Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. +''' + +from .node import CFSNode +from .utils import RTSLibError, RTSLibALUANotSupported, fread, fwrite + +alua_rw_params = ['alua_access_state', 'alua_access_status', + 'alua_write_metadata', 'alua_access_type', 'preferred', + 'nonop_delay_msecs', 'trans_delay_msecs', + 'implicit_trans_secs', 'alua_support_offline', + 'alua_support_standby', 'alua_support_transitioning', + 'alua_support_active_nonoptimized', + 'alua_support_unavailable', 'alua_support_active_optimized'] +alua_ro_params = ['tg_pt_gp_id', 'members', 'alua_support_lba_dependent'] +alua_types = ['None', 'Implicit', 'Explicit', 'Implicit and Explicit'] +alua_statuses = ['None', 'Altered by Explicit STPG', 'Altered by Implicit ALUA'] + +class ALUATargetPortGroup(CFSNode): + """ + ALUA Target Port Group interface + """ + + def __repr__(self): + return "" % self.name + + def __init__(self, storage_object, name, tag=None): + """ + @param storage_object: backstore storage object to create ALUA group for + @param name: name of ALUA group + @param tag: target port group id. If not passed in, try to look + up existing ALUA TPG with the same name + """ + if storage_object.alua_supported is False: + raise RTSLibALUANotSupported("Backend does not support ALUA setup") + + # default_tg_pt_gp takes tag 1 + if tag is not None and (tag > 65535 or tag < 1): + raise RTSLibError("The TPG Tag must be between 1 and 65535") + + super(ALUATargetPortGroup, self).__init__() + self.name = name + self.storage_object = storage_object + + self._path = "%s/alua/%s" % (storage_object.path, name) + + if tag is not None: + try: + self._create_in_cfs_ine('create') + except OSError as msg: + raise RTSLibError(msg) + + try: + fwrite("%s/tg_pt_gp_id" % self._path, tag) + except IOError as msg: + self.delete() + raise RTSLibError("Cannot set id to %d: %s" % (tag, str(msg))) + else: + try: + self._create_in_cfs_ine('lookup') + except OSError as msg: + raise RTSLibError(msg) + + # Public + + def delete(self): + """ + Delete ALUA TPG and unmap from LUNs + """ + self._check_self() + + # default_tg_pt_gp created by the kernel and cannot be deleted + if self.name == "default_tg_pt_gp": + raise RTSLibError("Can not delete default_tg_pt_gp") + + # This will reset the ALUA tpg to default_tg_pt_gp + super(ALUATargetPortGroup, self).delete() + + def _get_alua_access_state(self): + self._check_self() + path = "%s/alua_access_state" % self.path + return int(fread(path)) + + def _set_alua_access_state(self, newstate): + self._check_self() + path = "%s/alua_access_state" % self.path + try: + fwrite(path, str(int(newstate))) + except IOError as e: + raise RTSLibError("Cannot change ALUA state: %s" % e) + + def _get_alua_access_status(self): + self._check_self() + path = "%s/alua_access_status" % self.path + status = fread(path) + return alua_statuses.index(status) + + def _set_alua_access_status(self, newstatus): + self._check_self() + path = "%s/alua_access_status" % self.path + try: + fwrite(path, str(int(newstatus))) + except IOError as e: + raise RTSLibError("Cannot change ALUA status: %s" % e) + + def _get_alua_access_type(self): + self._check_self() + path = "%s/alua_access_type" % self.path + alua_type = fread(path) + return alua_types.index(alua_type) + + def _set_alua_access_type(self, access_type): + self._check_self() + path = "%s/alua_access_type" % self.path + try: + fwrite(path, str(int(access_type))) + except IOError as e: + raise RTSLibError("Cannot change ALUA access type: %s" % e) + + def _get_preferred(self): + self._check_self() + path = "%s/preferred" % self.path + return int(fread(path)) + + def _set_preferred(self, pref): + self._check_self() + path = "%s/preferred" % self.path + try: + fwrite(path, str(int(pref))) + except IOError as e: + raise RTSLibError("Cannot set preferred: %s" % e) + + def _get_alua_write_metadata(self): + self._check_self() + path = "%s/alua_write_metadata" % self.path + return int(fread(path)) + + def _set_alua_write_metadata(self, pref): + self._check_self() + path = "%s/alua_write_metadata" % self.path + try: + fwrite(path, str(int(pref))) + except IOError as e: + raise RTSLibError("Cannot set alua_write_metadata: %s" % e) + + def _get_alua_support_active_nonoptimized(self): + self._check_self() + path = "%s/alua_support_active_nonoptimized" % self.path + return int(fread(path)) + + def _set_alua_support_active_nonoptimized(self, enabled): + self._check_self() + path = "%s/alua_support_active_nonoptimized" % self.path + try: + fwrite(path, str(int(enabled))) + except IOError as e: + raise RTSLibError("Cannot set alua_support_active_nonoptimized: %s" % e) + + def _get_alua_support_active_optimized(self): + self._check_self() + path = "%s/alua_support_active_optimized" % self.path + return int(fread(path)) + + def _set_alua_support_active_optimized(self, enabled): + self._check_self() + path = "%s/alua_support_active_optimized" % self.path + try: + fwrite(path, str(int(enabled))) + except IOError as e: + raise RTSLibError("Cannot set alua_support_active_optimized: %s" % e) + + def _get_alua_support_offline(self): + self._check_self() + path = "%s/alua_support_offline" % self.path + return int(fread(path)) + + def _set_alua_support_offline(self, enabled): + self._check_self() + path = "%s/alua_support_offline" % self.path + try: + fwrite(path, str(int(enabled))) + except IOError as e: + raise RTSLibError("Cannot set alua_support_offline: %s" % e) + + def _get_alua_support_unavailable(self): + self._check_self() + path = "%s/alua_support_unavailable" % self.path + return int(fread(path)) + + def _set_alua_support_unavailable(self, enabled): + self._check_self() + path = "%s/alua_support_unavailable" % self.path + try: + fwrite(path, str(int(enabled))) + except IOError as e: + raise RTSLibError("Cannot set alua_support_unavailable: %s" % e) + + def _get_alua_support_standby(self): + self._check_self() + path = "%s/alua_support_standby" % self.path + return int(fread(path)) + + def _set_alua_support_standby(self, enabled): + self._check_self() + path = "%s/alua_support_standby" % self.path + try: + fwrite(path, str(int(enabled))) + except IOError as e: + raise RTSLibError("Cannot set alua_support_standby: %s" % e) + + def _get_alua_support_transitioning(self): + self._check_self() + path = "%s/alua_support_transitioning" % self.path + return int(fread(path)) + + def _set_alua_support_transitioning(self, enabled): + self._check_self() + path = "%s/alua_support_transitioning" % self.path + try: + fwrite(path, str(int(enabled))) + except IOError as e: + raise RTSLibError("Cannot set alua_support_transitioning: %s" % e) + + def _get_alua_support_lba_dependent(self): + self._check_self() + path = "%s/alua_support_lba_dependent" % self.path + return int(fread(path)) + + def _get_members(self): + self._check_self() + path = "%s/members" % self.path + + member_list = [] + + for member in fread(path).splitlines(): + lun_path = member.split("/") + if len(lun_path) != 4: + continue + member_list.append({ 'driver': lun_path[0], 'target': lun_path[1], + 'tpgt': int(lun_path[2].split("_", 1)[1]), + 'lun': int(lun_path[3].split("_", 1)[1]) }) + return member_list + + def _get_tg_pt_gp_id(self): + self._check_self() + path = "%s/tg_pt_gp_id" % self.path + return int(fread(path)) + + def _get_trans_delay_msecs(self): + self._check_self() + path = "%s/trans_delay_msecs" % self.path + return int(fread(path)) + + def _set_trans_delay_msecs(self, secs): + self._check_self() + path = "%s/trans_delay_msecs" % self.path + try: + fwrite(path, str(int(secs))) + except IOError as e: + raise RTSLibError("Cannot set trans_delay_msecs: %s" % e) + + def _get_implicit_trans_secs(self): + self._check_self() + path = "%s/implicit_trans_secs" % self.path + return int(fread(path)) + + def _set_implicit_trans_secs(self, secs): + self._check_self() + path = "%s/implicit_trans_secs" % self.path + try: + fwrite(path, str(int(secs))) + except IOError as e: + raise RTSLibError("Cannot set implicit_trans_secs: %s" % e) + + def _get_nonop_delay_msecs(self): + self._check_self() + path = "%s/nonop_delay_msecs" % self.path + return int(fread(path)) + + def _set_nonop_delay_msecs(self, delay): + self._check_self() + path = "%s/nonop_delay_msecs" % self.path + try: + fwrite(path, str(int(delay))) + except IOError as e: + raise RTSLibError("Cannot set nonop_delay_msecs: %s" % e) + + def dump(self): + d = super(ALUATargetPortGroup, self).dump() + d['name'] = self.name + d['tg_pt_gp_id'] = self.tg_pt_gp_id + for param in alua_rw_params: + d[param] = getattr(self, param, None) + return d + + alua_access_state = property(_get_alua_access_state, _set_alua_access_state, + doc="Get or set ALUA state. " + "0 = Active/optimized, " + "1 = Active/non-optimized, " + "2 = Standby, " + "3 = Unavailable, " + "4 = LBA Dependent, " + "14 = Offline, " + "15 = Transitioning") + + alua_access_type = property(_get_alua_access_type, _set_alua_access_type, + doc="Get or set ALUA access type. " + "1 = Implicit, 2 = Explicit, 3 = Both") + + alua_access_status = property(_get_alua_access_status, + _set_alua_access_status, + doc="Get or set ALUA access status. " + "0 = None, " + "1 = Altered by Explicit STPG, " + "2 = Altered by Implicit ALUA") + + preferred = property(_get_preferred, _set_preferred, + doc="Get or set preferred bit. 1 = Pref, 0 Not-Pre") + + alua_write_metadata = property(_get_alua_write_metadata, + _set_alua_write_metadata, + doc="Get or set alua_write_metadata flag. " + "enable (1) or disable (0)") + + tg_pt_gp_id = property(_get_tg_pt_gp_id, doc="Get ALUA Target Port Group ID") + + members = property(_get_members, doc="Get LUNs in Target Port Group") + + alua_support_active_nonoptimized = property(_get_alua_support_active_nonoptimized, + _set_alua_support_active_nonoptimized, + doc="Enable (1) or disable (0) " + "Active/non-optimized support") + + alua_support_active_optimized = property(_get_alua_support_active_optimized, + _set_alua_support_active_optimized, + doc="Enable (1) or disable (0) " + "Active/optimized support") + + alua_support_offline = property(_get_alua_support_offline, + _set_alua_support_offline, + doc="Enable (1) or disable (0) " + "offline support") + + alua_support_unavailable = property(_get_alua_support_unavailable, + _set_alua_support_unavailable, + doc="enable (1) or disable (0) " + "unavailable support") + + alua_support_standby = property(_get_alua_support_standby, + _set_alua_support_standby, + doc="enable (1) or disable (0) " + "standby support") + + alua_support_lba_dependent = property(_get_alua_support_lba_dependent, + doc="show lba_dependent support " + "enabled (1) or disabled (0)") + + alua_support_transitioning = property(_get_alua_support_transitioning, + _set_alua_support_transitioning, + doc="enable (1) or disable (0) " + "transitioning support") + + trans_delay_msecs = property(_get_trans_delay_msecs, + _set_trans_delay_msecs, + doc="msecs to delay state transition") + + implicit_trans_secs = property(_get_implicit_trans_secs, + _set_implicit_trans_secs, + doc="implicit transition time limit") + + nonop_delay_msecs = property(_get_nonop_delay_msecs, _set_nonop_delay_msecs, + doc="msecs to delay IO when non-optimized") + + @classmethod + def setup(cls, storage_obj, alua_tpg, err_func): + name = alua_tpg['name'] + if name == 'default_tg_pt_gp': + return + + alua_tpg_obj = cls(storage_obj, name, alua_tpg['tg_pt_gp_id']) + for param in alua_rw_params: + setattr(alua_tpg_obj, param, alua_tpg[param]) diff --git a/rtslib/fabric.py b/rtslib/fabric.py index eae6325615b4e498055d28f0769844ac9c5e4ec0..b529f14815ccfee50be3e9b830a0426449347f7b 100644 --- a/rtslib/fabric.py +++ b/rtslib/fabric.py @@ -118,8 +118,8 @@ from .utils import RTSLibError, modprobe, ignored from .target import Target from .utils import _get_auth_attr, _set_auth_attr -version_attributes = {"lio_version", "version"} -discovery_auth_attributes = {"discovery_auth"} +version_attributes = set(["lio_version", "version"]) +discovery_auth_attributes = set(["discovery_auth"]) target_names_excludes = version_attributes | discovery_auth_attributes @@ -137,12 +137,11 @@ class _BaseFabricModule(CFSNode): def __init__(self, name): ''' Instantiate a FabricModule object, according to the provided name. - @param name: the name of the FabricModule object. It must match an - existing target fabric module specfile (name.spec). + @param name: the name of the FabricModule object. @type name: str ''' super(_BaseFabricModule, self).__init__() - self.name = str(name) + self.name = name self.spec_file = "N/A" self._path = "%s/%s" % (self.configfs_dir, self.name) self.features = ('discovery_auth', 'acls', 'auth', 'nps', 'tpgts') @@ -153,8 +152,11 @@ class _BaseFabricModule(CFSNode): def _check_self(self): if not self.exists: - modprobe(self.kernel_module) - self._create_in_cfs_ine('any') + try: + self._create_in_cfs_ine('any') + except RTSLibError: + modprobe(self.kernel_module) + self._create_in_cfs_ine('any') super(_BaseFabricModule, self)._check_self() def has_feature(self, feature): @@ -436,6 +438,27 @@ class VhostFabricModule(_BaseFabricModule): self.wwn_types = ('naa',) self.kernel_module = "tcm_vhost" +class XenPvScsiFabricModule(_BaseFabricModule): + def __init__(self): + super(XenPvScsiFabricModule, self).__init__('xen-pvscsi') + self._path = "%s/%s" % (self.configfs_dir, 'xen-pvscsi') + self.features = ("nexus", "tpgts") + self.wwn_types = ('naa',) + self.kernel_module = "xen-scsiback" + + +class IbmvscsisFabricModule(_BaseFabricModule): + def __init__(self): + super(IbmvscsisFabricModule, self).__init__('ibmvscsis') + self.features = () + self.kernel_module = "ibmvscsis" + + @property + def wwns(self): + for wwn_file in glob("/sys/module/ibmvscsis/drivers/vio:ibmvscsis/*/devspec"): + name = fread(wwn_file) + yield name[name.find("@") + 1:] + fabric_modules = { "srpt": SRPTFabricModule, @@ -446,6 +469,8 @@ fabric_modules = { "tcm_fc": FCoEFabricModule, # "usb_gadget": USBGadgetFabricModule, # very rare, don't show "vhost": VhostFabricModule, + "xen-pvscsi": XenPvScsiFabricModule, + "ibmvscsis": IbmvscsisFabricModule, } # diff --git a/rtslib/node.py b/rtslib/node.py index c319a5bcd062b6e1651e3ca44ba713645e31a64e..1d77cd1b81ba333d6c81239fa13c0c927d6722ac 100644 --- a/rtslib/node.py +++ b/rtslib/node.py @@ -52,9 +52,16 @@ class CFSNode(object): ''' if mode not in ['any', 'lookup', 'create']: raise RTSLibError("Invalid mode: %s" % mode) + if self.exists and mode == 'create': - raise RTSLibError("This %s already exists in configFS" - % self.__class__.__name__) + # ensure that self.path is not stale hba-only dir + if os.path.samefile(os.path.dirname(self.path), self.configfs_dir+'/core') \ + and not next(os.walk(self.path))[1]: + os.rmdir(self.path) + else: + raise RTSLibError("This %s already exists in configFS" + % self.__class__.__name__) + elif not self.exists and mode == 'lookup': raise RTSLibNotInCFS("No such %s in configfs: %s" % (self.__class__.__name__, self.path)) @@ -214,7 +221,10 @@ class CFSNode(object): attrs = {} params = {} for item in self.list_attributes(writable=True): - attrs[item] = int(self.get_attribute(item)) + try: + attrs[item] = int(self.get_attribute(item)) + except ValueError: + attrs[item] = self.get_attribute(item) if attrs: d['attributes'] = attrs for item in self.list_parameters(writable=True): diff --git a/rtslib/root.py b/rtslib/root.py index a4fbb2ee7154c22b0868e6bba272bc939497a8f5..72c02993f73fcdd777217adedf5aa65b204e3d6c 100644 --- a/rtslib/root.py +++ b/rtslib/root.py @@ -21,13 +21,16 @@ under the License. import os import stat import json +import glob from .node import CFSNode from .target import Target from .fabric import FabricModule -from .tcm import so_mapping, StorageObject -from .utils import RTSLibError, modprobe, mount_configfs +from .tcm import so_mapping, bs_cache, StorageObject +from .utils import RTSLibError, RTSLibALUANotSupported, modprobe, mount_configfs from .utils import dict_remove, set_attributes +from .utils import fread, fwrite +from .alua import ALUATargetPortGroup default_save_file = "/etc/target/saveconfig.json" @@ -56,16 +59,31 @@ class RTSRoot(CFSNode): ''' # RTSRoot private stuff + + # this should match the kernel target driver default db dir + _default_dbroot = "/var/target" + # this is where the target DB is to be located (instead of the default) + _preferred_dbroot = "/etc/target" + def __init__(self): ''' Instantiate an RTSRoot object. Basically checks for configfs setup and base kernel modules (tcm) ''' super(RTSRoot, self).__init__() - modprobe('configfs') - mount_configfs() - modprobe('target_core_mod') - self._create_in_cfs_ine('any') + try: + mount_configfs() + except RTSLibError: + modprobe('configfs') + mount_configfs() + + try: + self._create_in_cfs_ine('any') + except RTSLibError: + modprobe('target_core_mod') + self._create_in_cfs_ine('any') + + self._set_dbroot_if_needed() def _list_targets(self): self._check_self() @@ -78,6 +96,12 @@ class RTSRoot(CFSNode): for so in StorageObject.all(): yield so + def _list_alua_tpgs(self): + self._check_self() + for so in self.storage_objects: + for a in so.alua_tpgs: + yield a + def _list_tpgs(self): self._check_self() for t in self.targets: @@ -134,6 +158,24 @@ class RTSRoot(CFSNode): def __str__(self): return "rtslib" + def _set_dbroot_if_needed(self): + dbroot_path = self.path + "/dbroot" + if not os.path.exists(dbroot_path): + self._dbroot = self._default_dbroot + return + self._dbroot = fread(dbroot_path) + if self._dbroot != self._preferred_dbroot: + try: + fwrite(dbroot_path, self._preferred_dbroot+"\n") + except: + if not os.path.isdir(self._preferred_dbroot): + raise RTSLibError("Cannot set dbroot to {}. Please check if this directory exists." + .format(self._preferred_dbroot)) + self._dbroot = fread(dbroot_path) + + def _get_dbroot(self): + return self._dbroot + # RTSRoot public stuff def dump(self): @@ -165,6 +207,11 @@ class RTSRoot(CFSNode): for so in self.storage_objects: so.delete() + # If somehow some hbas still exist (no storage object within?) clean + # them up too. + for hba_dir in glob.glob("%s/core/*_*" % self.configfs_dir): + os.rmdir(hba_dir) + def restore(self, config, clear_existing=False, abort_on_error=False): ''' Takes a dict generated by dump() and reconfigures the target to match. @@ -196,7 +243,7 @@ class RTSRoot(CFSNode): err_func("'plugin' not defined or invalid in storageobject %s" % so['name']) continue kwargs = so.copy() - dict_remove(kwargs, ('exists', 'attributes', 'plugin', 'buffered_mode')) + dict_remove(kwargs, ('exists', 'attributes', 'plugin', 'buffered_mode', 'alua_tpgs')) try: so_obj = so_cls(**kwargs) except Exception as e: @@ -209,6 +256,12 @@ class RTSRoot(CFSNode): set_attributes(so_obj, so.get('attributes', {}), so_err_func) + for alua_tpg in so.get('alua_tpgs', {}): + try: + ALUATargetPortGroup.setup(so_obj, alua_tpg, err_func) + except RTSLibALUANotSupported: + pass + # Don't need to create fabric modules for index, fm in enumerate(config.get('fabric_modules', [])): if 'name' not in fm: @@ -249,7 +302,9 @@ class RTSRoot(CFSNode): os.fchmod(f.fileno(), stat.S_IRUSR | stat.S_IWUSR) f.write(json.dumps(self.dump(), sort_keys=True, indent=2)) f.write("\n") + f.flush() os.fsync(f.fileno()) + f.close() os.rename(save_file+".temp", save_file) @@ -268,6 +323,12 @@ class RTSRoot(CFSNode): return self.restore(config, clear_existing=clear_existing, abort_on_error=abort_on_error) + def invalidate_caches(self): + ''' + Invalidate any caches used throughout the hierarchy + ''' + bs_cache.clear() + targets = property(_list_targets, doc="Get the list of Target objects.") tpgs = property(_list_tpgs, @@ -290,6 +351,10 @@ class RTSRoot(CFSNode): doc="Get the list of all existing LUN objects.") fabric_modules = property(_list_fabric_modules, doc="Get the list of all FabricModule objects.") + alua_tpgs = property(_list_alua_tpgs, + doc="Get the list of all ALUA TPG objects.") + dbroot = property(_get_dbroot, + doc="Get the target database root") def _test(): '''Run the doctests.''' diff --git a/rtslib/target.py b/rtslib/target.py index 206ef12b1142f53e4490ec5f3270d56a6fb36ff5..d3cdab7158915b8959e2df6bfa13207df7b76d6a 100644 --- a/rtslib/target.py +++ b/rtslib/target.py @@ -582,6 +582,44 @@ class LUN(CFSNode): if os.path.realpath("%s/%s" % (mlun.path, mlun.alias)) == self.path: yield mlun + + # pass through backends will not have setup all the default + # ALUA structs in the kernel. If the kernel has been setup, + # a user created group or default_tg_pt_gp will be returned. + # If the kernel was not properly setup an empty string is + # return in alua_tg_pt_gp. Writing to alua_tg_pt_gp will crash + # older kernels and will return a -Exyz code in newer ones. + def _get_alua_tg_pt_gp_name(self): + self._check_self() + + storage_object = self._get_storage_object() + if storage_object.alua_supported is False: + return None + + path = "%s/alua_tg_pt_gp" % self.path + try: + info = fread(path) + if not info: + return None + group_line = info.splitlines()[0] + return group_line.split(':')[1].strip() + except IOError as e: + return None + + def _set_alua_tg_pt_gp_name(self, group_name): + self._check_self() + + if not self._get_alua_tg_pt_gp_name(): + return -1 + + path = "%s/alua_tg_pt_gp" % self.path + try: + fwrite(path, group_name) + except IOError as e: + return -1 + + return 0 + # LUN public stuff def delete(self): @@ -615,6 +653,8 @@ class LUN(CFSNode): doc="Get the LUN alias.") mapped_luns = property(_list_mapped_luns, doc="List all MappedLUN objects referencing this LUN.") + alua_tg_pt_gp_name = property(_get_alua_tg_pt_gp_name, _set_alua_tg_pt_gp_name, + doc="Get and Set the LUN's ALUA Target Port Group") @classmethod def setup(cls, tpg_obj, lun, err_func): @@ -637,16 +677,24 @@ class LUN(CFSNode): return try: - cls(tpg_obj, lun['index'], storage_object=match_so) + lun_obj = cls(tpg_obj, lun['index'], storage_object=match_so, alias=lun.get('alias')) except (RTSLibError, KeyError): err_func("Creating TPG %d LUN index %d failed" % (tpg_obj.tag, lun['index'])) + try: + lun_obj.alua_tg_pt_gp_name = lun['alua_tg_pt_gp_name'] + except KeyError: + # alua_tg_pt_gp support not present in older versions + pass + def dump(self): d = super(LUN, self).dump() d['storage_object'] = "/backstores/%s/%s" % \ (self.storage_object.plugin, self.storage_object.name) d['index'] = self.lun + d['alias'] = self.alias + d['alua_tg_pt_gp_name'] = self.alua_tg_pt_gp_name return d @@ -727,10 +775,27 @@ class NetworkPortal(CFSNode): if os.path.isfile(path): raise RTSLibError("Cannot change iser") + def _get_offload(self): + try: + # only offload at the moment is cxgbit + return bool(int(fread("%s/cxgbit" % self.path))) + except IOError: + return False + + def _set_offload(self, boolean): + path = "%s/cxgbit" % self.path + try: + fwrite(path, str(int(boolean))) + except IOError: + # b/w compat: don't complain if cxgbit entry is missing + if os.path.isfile(path): + raise RTSLibError("Cannot change offload") + # NetworkPortal public stuff def delete(self): self.iser = False + self.offload = False super(NetworkPortal, self).delete() parent_tpg = property(_get_parent_tpg, @@ -742,6 +807,9 @@ class NetworkPortal(CFSNode): iser = property(_get_iser, _set_iser, doc="Get or set a boolean value representing if this " \ + "NetworkPortal supports iSER.") + offload = property(_get_offload, _set_offload, + doc="Get or set a boolean value representing if this " \ + + "NetworkPortal supports offload.") @classmethod def setup(cls, tpg_obj, p, err_func): @@ -755,6 +823,7 @@ class NetworkPortal(CFSNode): try: np = cls(tpg_obj, p['ip_address'], p['port']) np.iser = p.get('iser', False) + np.offload = p.get('offload', False) except (RTSLibError, KeyError) as e: err_func("Creating NetworkPortal object %s:%s failed: %s" % (p['ip_address'], p['port'], e)) @@ -764,6 +833,7 @@ class NetworkPortal(CFSNode): d['port'] = self.port d['ip_address'] = self.ip_address d['iser'] = self.iser + d['offload'] = self.offload return d @@ -998,7 +1068,7 @@ class MappedLUN(CFSNode): self.parent_nodeacl.parent_tpg.tag, self.tpg_lun.lun) def __init__(self, parent_nodeacl, mapped_lun, - tpg_lun=None, write_protect=None): + tpg_lun=None, write_protect=None, alias=None): ''' A MappedLUN object can be instanciated in two ways: - B{Creation mode}: If I{tpg_lun} is specified, the underlying @@ -1046,14 +1116,14 @@ class MappedLUN(CFSNode): if tpg_lun is not None: self._create_in_cfs_ine('create') try: - self._configure(tpg_lun, write_protect) + self._configure(tpg_lun, write_protect, alias) except: self.delete() raise else: self._create_in_cfs_ine('lookup') - def _configure(self, tpg_lun, write_protect): + def _configure(self, tpg_lun, write_protect, alias): self._check_self() if isinstance(tpg_lun, LUN): tpg_lun = tpg_lun.lun @@ -1071,8 +1141,10 @@ class MappedLUN(CFSNode): if not (isinstance(tpg_lun, LUN) and tpg_lun): raise RTSLibError("LUN %s does not exist in this TPG" % str(tpg_lun)) - os.symlink(tpg_lun.path, "%s/%s" - % (self.path, str(uuid.uuid4())[-10:])) + + if not alias: + alias = str(uuid.uuid4())[-10:] + os.symlink(tpg_lun.path, "%s/%s" % (self.path, alias)) try: self.write_protect = int(write_protect) > 0 @@ -1170,7 +1242,8 @@ class MappedLUN(CFSNode): try: mlun_obj = cls(acl_obj, mlun['index'], - tpg_lun_obj, mlun.get('write_protect')) + tpg_lun_obj, mlun.get('write_protect'), + mlun.get('alias')) mlun_obj.tag = mlun.get("tag", None) except (RTSLibError, KeyError): err_func("Creating MappedLUN object %d failed" % mlun['index']) @@ -1180,6 +1253,7 @@ class MappedLUN(CFSNode): d['write_protect'] = self.write_protect d['index'] = self.mapped_lun d['tpg_lun'] = self.tpg_lun.lun + d['alias'] = self.alias return d diff --git a/rtslib/tcm.py b/rtslib/tcm.py index a7688c646a7d51a40fef8ec8f382f05fb42d4f38..1aeea23c0f97eb2f938564d192a17696f35f137a 100644 --- a/rtslib/tcm.py +++ b/rtslib/tcm.py @@ -25,12 +25,26 @@ import glob import resource from six.moves import range +from .alua import ALUATargetPortGroup from .node import CFSNode from .utils import fread, fwrite, generate_wwn, RTSLibError, RTSLibNotInCFS from .utils import convert_scsi_path_to_hctl, convert_scsi_hctl_to_path from .utils import is_dev_in_use, get_blockdev_type from .utils import get_size_for_blk_dev, get_size_for_disk_name +def storage_object_get_alua_support_attr(so): + ''' + Helper function that can be called by passthrough type of backends. + ''' + try: + if int(so.get_attribute("alua_support")) == 1: + return True + except RTSLibError: + pass + # Default to false because older kernels will crash when + # reading/writing to some ALUA files when ALUA was not + # fully supported by pscsi and tcmu. + return False class StorageObject(CFSNode): ''' @@ -77,7 +91,8 @@ class StorageObject(CFSNode): need to read it in and squirt it back into configfs when we configure the storage object. BLEH. """ - aptpl_dir = "/var/target/pr" + from .root import RTSRoot + aptpl_dir = "%s/pr" % RTSRoot().dbroot try: lines = fread("%s/aptpl_%s" % (aptpl_dir, self.wwn)).split() @@ -215,6 +230,22 @@ class StorageObject(CFSNode): for lun in self._gen_attached_luns(): yield lun + def _list_alua_tpgs(self): + ''' + Generate all ALUA groups attach to a storage object. + ''' + self._check_self() + for tpg in os.listdir("%s/alua" % self.path): + if self.alua_supported: + yield ALUATargetPortGroup(self, tpg) + + def _get_alua_supported(self): + ''' + Children should override if the backend did not always support ALUA + ''' + self._check_self() + return True + # StorageObject public stuff def delete(self): @@ -226,6 +257,10 @@ class StorageObject(CFSNode): ''' self._check_self() + for alua_tpg in self._list_alua_tpgs(): + if alua_tpg.name != 'default_tg_pt_gp': + alua_tpg.delete() + # If we are called after a configure error, we can skip this if self.is_configured(): for lun in self._gen_attached_luns(): @@ -264,11 +299,16 @@ class StorageObject(CFSNode): + "is used by any LUN") attached_luns = property(_list_attached_luns, doc="Get the list of all LUN objects attached.") + alua_tpgs = property(_list_alua_tpgs, + doc="Get list of ALUA Target Port Groups attached.") + alua_supported = property(_get_alua_supported, + doc="Returns true if ALUA can be setup. False if not supported.") def dump(self): d = super(StorageObject, self).dump() d['name'] = self.name d['plugin'] = self.plugin + d['alua_tpgs'] = [tpg.dump() for tpg in self.alua_tpgs] return d @@ -313,11 +353,14 @@ class PSCSIStorageObject(StorageObject): def _configure(self, dev): self._check_self() - # Use H:C:T:L format or preserve the path given by the user. + # Use H:C:T:L format or use the path given by the user. try: + # assume 'dev' is the path, try to get h:c:t:l values (hostid, channelid, targetid, lunid) = \ convert_scsi_path_to_hctl(dev) - except TypeError: + udev_path = dev.strip() + except: + # Oops, maybe 'dev' is in h:c:t:l format, try to get udev_path try: (hostid, channelid, targetid, lunid) = dev.split(':') hostid = int(hostid) @@ -329,15 +372,13 @@ class PSCSIStorageObject(StorageObject): + "path, and dev " + "parameter not in H:C:T:L " + "format: %s" % dev) - else: - udev_path = convert_scsi_hctl_to_path(hostid, - channelid, - targetid, - lunid) - if not udev_path: - raise RTSLibError("SCSI device does not exist") - else: - udev_path = dev.strip() + + udev_path = convert_scsi_hctl_to_path(hostid, + channelid, + targetid, + lunid) + + # -- now have all 5 values or have errored out -- if is_dev_in_use(udev_path): raise RTSLibError("Cannot configure StorageObject because " @@ -391,6 +432,10 @@ class PSCSIStorageObject(StorageObject): self._check_self() return int(self._parse_info('Host ID')) + def _get_alua_supported(self): + self._check_self() + return storage_object_get_alua_support_attr(self) + # PSCSIStorageObject public stuff wwn = property(StorageObject._get_wwn, _set_wwn, @@ -410,6 +455,8 @@ class PSCSIStorageObject(StorageObject): doc="Get the SCSI device target id") lun = property(_get_lun, doc="Get the SCSI device LUN") + alua_supported = property(_get_alua_supported, + doc="Returns true if ALUA can be setup. False if not supported.") def dump(self): d = super(PSCSIStorageObject, self).dump() @@ -565,8 +612,6 @@ class FileIOStorageObject(StorageObject): def _configure(self, dev, size, wwn, write_back): self._check_self() - - dev = os.path.realpath(dev) block_type = get_blockdev_type(dev) if block_type is None: # a file if os.path.exists(dev) and not os.path.isfile(dev): @@ -741,52 +786,48 @@ class UserBackedStorageObject(StorageObject): An interface to configFS storage objects for userspace-backed backstore. ''' - def __init__(self, name, config=None, level=None, size=None, wwn=None): + def __init__(self, name, config=None, size=None, wwn=None, + hw_max_sectors=None): ''' @param name: The name of the UserBackedStorageObject. @type name: string - @param dev: The path to the backend block device to be used. - - Example: I{dev="/dev/sda"}. - - The only device type that is accepted I{TYPE_DISK}. - For other device types, use pscsi. - @type dev: string - @param size: The size of the device to create, in bytes. - @type size: int @param config: user-handler-specific config string. - e.g. "rbd/machine1@snap4" @type config: string - @param level: TCMU emulation level, 0 or 1. Level 0 will pass all SCSI - commands, 1 will just pass I/O commands, READ, WRITE, etc. - @type level: int + @param size: The size of the device to create, in bytes. + @type size: int + @param wwn: T10 WWN Unit Serial, will generate if None + @type wwn: string + @hw_max_sectors: Max sectors per command limit to export to initiators. + @type hw_max_sectors: int @return: A UserBackedStorageObject object. ''' if size is not None: - if level is None or config is None: - raise RTSLibError("'size', 'level', and 'config' must be set when " + if config is None: + raise RTSLibError("'size' and 'config' must be set when " "creating a new UserBackedStorageObject") if '/' not in config: raise RTSLibError("'config' must contain a '/' separating subtype " "from its configuration string") super(UserBackedStorageObject, self).__init__(name, 'create') try: - self._configure(config, level, size, wwn) + self._configure(config, size, wwn, hw_max_sectors) except: self.delete() raise else: super(UserBackedStorageObject, self).__init__(name, 'lookup') - def _configure(self, config, level, size, wwn): + def _configure(self, config, size, wwn, hw_max_sectors): self._check_self() if ':' in config: raise RTSLibError("':' not allowed in config string") - if level not in (0, 1): - raise RTSLibError("Current allowable levels are 0 or 1") self._control("dev_config=%s" % config) - self._control("pass_level=%d" % level) self._control("dev_size=%d" % size) + if hw_max_sectors is not None: + self._control("hw_max_sectors=%s" % hw_max_sectors) self._enable() super(UserBackedStorageObject, self)._configure(wwn) @@ -795,9 +836,9 @@ class UserBackedStorageObject(StorageObject): self._check_self() return int(self._parse_info('Size')) - def _get_level(self): + def _get_hw_max_sectors(self): self._check_self() - return int(self._parse_info('PassLevel')) + return int(self._parse_info('HwMaxSectors')) def _get_config(self): self._check_self() @@ -806,19 +847,26 @@ class UserBackedStorageObject(StorageObject): return None return val + def _get_alua_supported(self): + self._check_self() + return storage_object_get_alua_support_attr(self) + + hw_max_sectors = property(_get_hw_max_sectors, + doc="Get the max sectors per command.") size = property(_get_size, doc="Get the size in bytes.") - level = property(_get_level, - doc="Get the command emulation level.") config = property(_get_config, doc="Get the TCMU config.") + alua_supported = property(_get_alua_supported, + doc="Returns true if ALUA can be setup. False if not supported.") def dump(self): d = super(UserBackedStorageObject, self).dump() d['wwn'] = self.wwn d['size'] = self.size - d['level'] = self.level d['config'] = self.config + d['hw_max_sectors'] = self.hw_max_sectors + return d @@ -829,7 +877,6 @@ class StorageObjectFactory(object): """ def __new__(cls, path): - path = os.path.realpath(path) name = path.strip("/").replace("/", "-") if os.path.exists(path): s = os.stat(path) diff --git a/rtslib/utils.py b/rtslib/utils.py index 5531e8fdf62bed6bd1e40bf1ea5abb3a3c962b53..315661014a45dd796a2c93d02f0e986b6ad7b610 100644 --- a/rtslib/utils.py +++ b/rtslib/utils.py @@ -27,12 +27,22 @@ import subprocess import uuid from contextlib import contextmanager +import pyudev + +_CONTEXT = pyudev.Context() + class RTSLibError(Exception): ''' Generic rtslib error. ''' pass +class RTSLibALUANotSupported(RTSLibError): + ''' + Backend does not support ALUA. + ''' + pass + class RTSLibBrokenLink(RTSLibError): ''' Broken link in configfs, i.e. missing LUN storage object. @@ -111,21 +121,36 @@ def is_dev_in_use(path): os.close(file_fd) return False +def _get_size_for_dev(device): + ''' + @param device: the device + @type device: pyudev.Device + @return: the size in logical blocks, 0 if none found + @rtype: int + ''' + attributes = device.attributes + try: + sect_size = attributes.asint('size') + except (KeyError, UnicodeDecodeError, ValueError): + return 0 + + try: + logical_block_size = attributes.asint('queue/logical_block_size') + except (KeyError, UnicodeDecodeError, ValueError): + return 0 + + return (sect_size * 512) // logical_block_size + def get_size_for_blk_dev(path): ''' @param path: The path to a block device @type path: string @return: The size in logical blocks of the device + @raises: DeviceNotFoundError if corresponding device not found + @raises: EnvironmentError, ValueError in some situations ''' - rdev = os.lstat(path).st_rdev - maj, min = os.major(rdev), os.minor(rdev) - - for line in list(open("/proc/partitions"))[2:]: - xmaj, xmin, size, name = line.split() - if (maj, min) == (int(xmaj), int(xmin)): - return get_size_for_disk_name(name) - else: - return 0 + device = pyudev.Device.from_device_file(_CONTEXT, os.path.realpath(str(path))) + return _get_size_for_dev(device) get_block_size = get_size_for_blk_dev @@ -134,32 +159,35 @@ def get_size_for_disk_name(name): @param name: a kernel disk name, as found in /proc/partitions @type name: string @return: The size in logical blocks of a disk-type block device. + @raises: DeviceNotFoundError ''' # size is in 512-byte sectors, we want to return number of logical blocks - def get_size(path, is_partition=False): - sect_size = int(fread("%s/size" % path)) - if is_partition: - path = os.path.split(path)[0] - logical_block_size = int(fread("%s/queue/logical_block_size" % path)) - return sect_size / (logical_block_size / 512) + def get_size(name): + """ + :param str name: name of block device + :raises DeviceNotFoundError: if device not found + """ + device = pyudev.Device.from_name(_CONTEXT, 'block', name) + return _get_size_for_dev(device) # Disk names can include '/' (e.g. 'cciss/c0d0') but these are changed to # '!' when listed in /sys/block. + # in pyudev 0.19 it should no longer be necessary to swap '/'s in name name = name.replace("/", "!") try: - return get_size("/sys/block/%s" % name) - except IOError: + return get_size(name) + except pyudev.DeviceNotFoundError: # Maybe it's a partition? - m = re.search(r'^([a-z0-9_\-!]+)(\d+)$', name) + m = re.search(r'^([a-z0-9_\-!]+?)(\d+)$', name) if m: # If disk name ends with a digit, Linux sticks a 'p' between it and # the partition number in the blockdev name. disk = m.groups()[0] if disk[-1] == 'p' and disk[-2].isdigit(): disk = disk[:-1] - return get_size("/sys/block/%s/%s" % (disk, m.group()), True) + return get_size(m.group()) else: raise @@ -183,22 +211,21 @@ def get_blockdev_type(path): @type path: string @return: An int for the block device type, or None if not a block device. ''' - dev = os.path.realpath(path) - - # is dev a block device? try: - mode = os.stat(dev) - except OSError: + device = pyudev.Device.from_device_file(_CONTEXT, path) + except (pyudev.DeviceNotFoundError, EnvironmentError, ValueError): return None - if not stat.S_ISBLK(mode[stat.ST_MODE]): + if device.subsystem != u'block': return None - # assume disk if device/type is missing - disk_type = 0 - with ignored(IOError): - disk_type = int(fread("/sys/block/%s/device/type" % os.path.basename(dev))) + attributes = device.attributes + disk_type = 0 + try: + disk_type = attributes.asint('device/type') + except (KeyError, UnicodeDecodeError, ValueError): + pass return disk_type get_block_type = get_blockdev_type @@ -225,17 +252,15 @@ def convert_scsi_path_to_hctl(path): @param path: The udev path to the SCSI block device. @type path: string @return: An (host, controller, target, lun) tuple of integer - values representing the SCSI ID of the device, or None if no - match is found. + values representing the SCSI ID of the device, or raise RTSLibError. ''' - devname = os.path.basename(os.path.realpath(path)) try: - hctl = os.listdir("/sys/block/%s/device/scsi_device" - % devname)[0].split(':') + path = os.path.realpath(path) + device = pyudev.Device.from_device_file(_CONTEXT, path) + parent = device.find_parent(subsystem='scsi') + return [int(data) for data in parent.sys_name.split(':')] except: - return None - - return [int(data) for data in hctl] + raise RTSLibError("Could not convert scsi path to hctl") def convert_scsi_hctl_to_path(host, controller, target, lun): ''' @@ -258,7 +283,7 @@ def convert_scsi_hctl_to_path(host, controller, target, lun): @type target: int @param lun: The SCSI Logical Unit Number. @type lun: int - @return: A string for the canonical path to the device, or empty string. + @return: A string for the canonical path to the device, or raise RTSLibError. ''' try: host = int(host) @@ -269,12 +294,21 @@ def convert_scsi_hctl_to_path(host, controller, target, lun): raise RTSLibError( "The host, controller, target and lun parameter must be integers") - for devname in os.listdir("/sys/block"): - path = "/dev/%s" % devname - hctl = [host, controller, target, lun] - if convert_scsi_path_to_hctl(path) == hctl: - return os.path.realpath(path) - return '' + hctl = [str(host), str(controller), str(target), str(lun)] + try: + scsi_device = pyudev.Device.from_name(_CONTEXT, 'scsi', ':'.join(hctl)) + except pyudev.DeviceNotFoundError: + raise RTSLibError("Could not find path for SCSI hctl") + + devices = _CONTEXT.list_devices( + subsystem='block', + parent=scsi_device + ) + + path = next((dev.device_node for dev in devices), '') + if path == None: + raise RTSLibError("Could not find path for SCSI hctl") + return path def generate_wwn(wwn_type): ''' @@ -344,12 +378,12 @@ def normalize_wwn(wwn_types, wwn): wwn_test = { 'free': lambda wwn: True, 'iqn': lambda wwn: \ - re.match("iqn\.[0-9]{4}-[0-1][0-9]\..*\..*", wwn) \ + re.match(r"iqn\.[0-9]{4}-[0-1][0-9]\..*\..*", wwn) \ and not re.search(' ', wwn) \ and not re.search('_', wwn), - 'naa': lambda wwn: re.match("naa\.[125][0-9a-fA-F]{15}$", wwn), - 'eui': lambda wwn: re.match("eui\.[0-9a-f]{16}$", wwn), - 'ib': lambda wwn: re.match("ib\.[0-9a-f]{32}$", wwn), + 'naa': lambda wwn: re.match(r"naa\.[125][0-9a-fA-F]{15}$", wwn), + 'eui': lambda wwn: re.match(r"eui\.[0-9a-f]{16}$", wwn), + 'ib': lambda wwn: re.match(r"ib\.[0-9a-f]{32}$", wwn), 'unit_serial': lambda wwn: \ re.match("[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$", wwn), } @@ -382,7 +416,6 @@ def modprobe(module): try: import kmod - kmod.Kmod().modprobe(module) except ImportError: process = subprocess.Popen(("modprobe", module), stdout=subprocess.PIPE, @@ -390,6 +423,12 @@ def modprobe(module): (stdoutdata, stderrdata) = process.communicate() if process.returncode != 0: raise RTSLibError(stderrdata) + return + + try: + kmod.Kmod().modprobe(module) + except kmod.error.KmodError: + raise RTSLibError("Could not load module: %s" % module) def mount_configfs(): if not os.path.ismount("/sys/kernel/config"): @@ -398,7 +437,8 @@ def mount_configfs(): stdout=subprocess.PIPE, stderr=subprocess.PIPE) (stdoutdata, stderrdata) = process.communicate() - if process.returncode != 0: + if process.returncode != 0 and not os.path.ismount( + "/sys/kernel/config"): raise RTSLibError("Cannot mount configfs") def dict_remove(d, items): @@ -446,7 +486,9 @@ def _set_auth_attr(self, value, attribute, ignore=False): path = "%s/%s" % (self.path, attribute) value = value.strip() if value == "NULL": - raise ValueError("'NULL' is not a permitted value") + raise RTSLibError("'NULL' is not a permitted value") + if len(value) > 255: + raise RTSLibError("Value longer than maximum length of 255") if value == '': value = "NULL" try: diff --git a/scripts/convert-to-json b/scripts/convert-to-json new file mode 100755 index 0000000000000000000000000000000000000000..3483e0a661cc608e4f76212e72e47fa9e019895c --- /dev/null +++ b/scripts/convert-to-json @@ -0,0 +1,327 @@ +#!/usr/bin/python3 +''' +convert-to-json + +This file is part of RTSLib-fb. +Copyright (c) 2013-2016 by Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +''' + +# +# A script to convert .lio format save files to json format. +# + +import json +import re + +def human_to_bytes(hsize, kilo=1024): + ''' + This function converts human-readable amounts of bytes to bytes. + It understands the following units : + - I{B} or no unit present for Bytes + - I{k}, I{K}, I{kB}, I{KB} for kB (kilobytes) + - I{m}, I{M}, I{mB}, I{MB} for MB (megabytes) + - I{g}, I{G}, I{gB}, I{GB} for GB (gigabytes) + - I{t}, I{T}, I{tB}, I{TB} for TB (terabytes) + + Note: The definition of I{kilo} defaults to 1kB = 1024Bytes. + Strictly speaking, those should not be called I{kB} but I{kiB}. + You can override that with the optional kilo parameter. + + @param hsize: The human-readable version of the Bytes amount to convert + @type hsize: string or int + @param kilo: Optional base for the kilo prefix + @type kilo: int + @return: An int representing the human-readable string converted to bytes + ''' + size = hsize.replace('i', '') + size = size.lower() + if not re.match("^[0-9\.]+[k|m|g|t]?[b]?$", size): + raise Exception("Cannot interpret size, wrong format: %s" % hsize) + + size = size.rstrip('ib') + + units = ['k', 'm', 'g', 't'] + try: + power = units.index(size[-1]) + 1 + except ValueError: + power = 0 + size = int(size) + else: + try: + size = int(size[:-1]) + except ValueError: + size = int(float(size[:-1])) + + return size * (int(kilo) ** power) + +def parse_yesno(val): + if val == "yes": + return 1 + elif val == "no": + return 0 + else: + try: + return int(val) + except: + return val + +def parse_attributes(txt, cur): + attribs = {} + while txt[cur] != "}": + name = txt[cur] + val = txt[cur+1] + attribs[name] = parse_yesno(val) + cur += 2 + return (cur+1, attribs) + +def parse_fileio(txt, cur): + so = dict(plugin="fileio") + while txt[cur] != "}": + if txt[cur] == "path": + so["dev"] = txt[cur+1] + cur += 2 + continue + if txt[cur] == "size": + so["size"] = human_to_bytes(txt[cur+1]) + cur += 2 + continue + if txt[cur] == "buffered": + # skip, recent LIO doesn't use for fileio + cur += 2 + continue + if txt[cur] == "attribute": + cur, so["attributes"] = parse_attributes(txt, cur+2) + continue + return (cur+1, so) + +def parse_block(txt, cur): + so = dict(plugin="block") + while txt[cur] != "}": + if txt[cur] == "path": + so["dev"] = txt[cur+1] + cur += 2 + continue + if txt[cur] == "attribute": + cur, so["attributes"] = parse_attributes(txt, cur+2) + continue + return (cur+1, so) + +def parse_ramdisk(txt, cur): + so = dict(plugin="ramdisk") + while txt[cur] != "}": + if txt[cur] == "nullio": + so["nullio"] = parse_yesno(txt[cur+1]) + cur += 2 + continue + if txt[cur] == "size": + so["size"] = human_to_bytes(txt[cur+1]) + cur += 2 + continue + if txt[cur] == "attribute": + cur, so["attributes"] = parse_attributes(txt, cur+2) + continue + return (cur+1, so) + +so_types = { + "fileio": parse_fileio, + "rd_mcp": parse_ramdisk, + "iblock": parse_block, +} + +def parse_storage(txt, cur): + name = txt[cur+3] + ty = txt[cur+1] + cur += 5 + (cur, d) = so_types[ty](txt, cur) + d["name"] = name + return (cur, d) + +def parse_lun(txt, cur): + index = int(txt[cur+1]) + plugin, name = txt[cur+3].split(":") + return cur+4, dict(index=index, plugin=plugin, name=name) + +def parse_mapped_lun(txt, cur): + mlun = dict(index=txt[cur+1]) + cur += 3 + while txt[cur] != "}": + if txt[cur] == "target_lun": + mlun["tpg_lun"] = parse_yesno(txt[cur+1]) + cur += 2 + continue + if txt[cur] == "write_protect": + mlun["write_protect"] = bool(parse_yesno(txt[cur+1])) + cur += 2 + continue + return cur+1, mlun + +def parse_acl(txt, cur): + acl = dict(node_wwn=txt[cur+1]) + mapped_luns = [] + cur += 3 + while txt[cur] != "}": + if txt[cur] == "attribute": + cur, acl["attributes"] = parse_attributes(txt, cur+2) + continue + if txt[cur] == "auth": + cur, auth = parse_attributes(txt, cur+2) + if len(auth): + acl["auth"] = auth + continue + if txt[cur] == "mapped_lun": + cur, mlun = parse_mapped_lun(txt, cur) + mapped_luns.append(mlun) + acl["mapped_luns"] = mapped_luns + return cur+1, acl + +def parse_tpg(tag, txt, cur): + if tag is None: + tag = int(txt[cur+1]) + cur += 2 + tpg = dict(tag=tag) + luns = [] + acls = [] + portals = [] + cur += 3 + while txt[cur] != "}": + if txt[cur] == "enable": + tpg["enable"] = parse_yesno(txt[cur+1]) + cur += 2 + continue + if txt[cur] == "attribute": + cur, tpg["attributes"] = parse_attributes(txt, cur+2) + continue + if txt[cur] == "parameter": + cur, tpg["parameters"] = parse_attributes(txt, cur+2) + continue + if txt[cur] == "auth": + cur, auth = parse_attributes(txt, cur+2) + if len(auth): + tpg["auth"] = auth + continue + if txt[cur] == "lun": + cur, l = parse_lun(txt, cur) + luns.append(l) + continue + if txt[cur] == "acl": + cur, acl = parse_acl(txt, cur) + acls.append(acl) + continue + if txt[cur] == "portal": + ip, port = txt[cur+1].split(":") + portal = dict(ip_address=ip, port=port) + portals.append(portal) + cur += 2 + continue + if len(luns): + tpg["luns"] = luns + if len(acls): + tpg["node_acls"] = acls + if len(portals): + tpg["portals"] = portals + return cur+1, tpg + + +def parse_target(fabric, txt, cur): + target = dict(wwn=txt[cur+1], fabric=fabric) + tpgs = [] + tpgt = None + # handle multiple tpgts + if txt[cur+2] == "{": + extra = 1 + else: + extra = 0 + tpgt = int(txt[cur+3]) + cur += 2 + extra + while txt[cur] != "}": + cur, tpg = parse_tpg(tpgt, txt, cur) + tpgs.append(tpg) + target["tpgs"] = tpgs + return cur+extra, target + +def parse_fabric(txt, cur): + fabric = txt[cur+1] + cur += 3 + while txt[cur] != "}": + if txt[cur] == "discovery_auth": + cur, disco = parse_attributes(txt, cur+2) + new_disco = {} + if disco.get("enable"): + new_disco["discovery_enable_auth"] = disco.get("enable") + if disco.get("userid"): + new_disco["discovery_userid"] = disco.get("userid") + if disco.get("password"): + new_disco["discovery_password"] = disco.get("password") + if disco.get("mutual_userid"): + new_disco["discovery_mutual_userid"] = disco.get("mutual_userid") + if disco.get("mutual_password"): + new_disco["discovery_mutual_password"] = disco.get("mutual_password") + new_disco["name"] = "iscsi" + fabs.append(new_disco) + continue + if txt[cur] == "target": + cur, t = parse_target(fabric, txt, cur) + targs.append(t) + continue + return cur + +sos = [] +fabs = [] +targs = [] + +# a basic tokenizer that splits on whitespace and handles double quotes +def split(s): + new_lst = [] + in_quotes = False + new_str = [] + for c in s: + if c not in " \n\t\"": + new_str.append(c) + elif c == '\"' and in_quotes == False: + in_quotes = True + elif c == '\"' and in_quotes == True: + in_quotes = False + if len(new_str) == 0: + # don't include things that are set to '""' + del new_lst[-1] + elif in_quotes == True: # append ws if in quotes + new_str.append(c) + elif len(new_str): # not in quotes, break on ws if anything in new_str + new_lst.append("".join(new_str)) + new_str = [] + else: + pass # drop ws + + return new_lst + +def parse(txt, cur): + cur = 0 + end = len(txt) - 1 + while cur != end: + if txt[cur] == "storage": + cur, d = parse_storage(txt, cur) + sos.append(d) + elif txt[cur] == "fabric": + cur = parse_fabric(txt, cur) + +with open("/etc/target/scsi_target.lio") as f: + txt = f.read() + txt = split(txt) + cur = parse(txt, 0) + +output = dict(storage_objects=sos, fabric_modules=fabs, targets=targs) + +print(json.dumps(output, indent=2, sort_keys=True)) + diff --git a/setup.py b/setup.py index ac759d539e11f17f358f38e1ee23de673e63e9c2..dd26dc4011fd502b6f3b6ceb1eb2ed9d6fa9de9a 100755 --- a/setup.py +++ b/setup.py @@ -20,14 +20,18 @@ from setuptools import setup setup ( name = 'rtslib-fb', - version = '2.1.57', + version = '2.1.67', description = 'API for Linux kernel SCSI target (aka LIO)', license = 'Apache 2.0', maintainer = 'Andy Grover', maintainer_email = 'agrover@redhat.com', - url = 'http://github.com/agrover/rtslib-fb', + url = 'http://github.com/open-iscsi/rtslib-fb', packages = ['rtslib_fb', 'rtslib'], scripts = ['scripts/targetctl'], + install_requires = [ + 'pyudev >= 0.16.1', + 'six', + ], classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3",