├── .fmf └── version ├── ci-tests └── sts.fmf ├── systemd ├── targetclid.socket └── targetclid.service ├── .gitignore ├── THANKS ├── src └── targetcli │ ├── __init__.py │ ├── ui_node.py │ ├── targetclid.py │ ├── targetcli_shell.py │ ├── ui_root.py │ ├── ui_backstore.py │ └── ui_target.py ├── .github └── workflows │ ├── codeql.yml │ └── publish.yml ├── .pre-commit-config.yaml ├── .packit.yaml ├── python-targetcli-fb.spec ├── README.md ├── targetclid.8 ├── pyproject.toml ├── COPYING └── targetcli.8 /.fmf/version: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /ci-tests/sts.fmf: -------------------------------------------------------------------------------- 1 | plan: 2 | import: 3 | url: https://gitlab.com/rh-kernel-stqe/sts 4 | name: /plans/lio/core 5 | -------------------------------------------------------------------------------- /systemd/targetclid.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=targetclid socket 3 | Documentation=man:targetclid(8) 4 | 5 | [Socket] 6 | ListenStream=/var/run/targetclid.sock 7 | SocketMode=0600 8 | 9 | [Install] 10 | WantedBy=sockets.target 11 | -------------------------------------------------------------------------------- /systemd/targetclid.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Targetcli daemon 3 | Documentation=man:targetclid(8) 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/usr/bin/targetclid 9 | Restart=on-failure 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | Also=targetclid.socket 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | debian/changelog 2 | dpkg-buildpackage.log 3 | dpkg-buildpackage.version 4 | build-stamp 5 | build/ 6 | debian/files 7 | debian/rtsadmin-doc.debhelper.log 8 | debian/rtsadmin-doc.substvars 9 | debian/rtsadmin-doc/ 10 | debian/rtsadmin.debhelper.log 11 | debian/rtsadmin.substvars 12 | debian/rtsadmin/ 13 | debian/tmp/ 14 | dist/ 15 | doc/ 16 | *.pyc 17 | *.swp 18 | ./*.spec 19 | redhat/*.spec 20 | ./rtsadmin-* 21 | log/ 22 | venv/ 23 | .idea 24 | *egg-info/ 25 | targetcli/version.py 26 | -------------------------------------------------------------------------------- /THANKS: -------------------------------------------------------------------------------- 1 | contribution: Johannes Dewender 2 | contribution: Jerome Martin 3 | contribution: Nicholas Bellinger 4 | contribution: Rafiu Fakunle 5 | contribution: António Meireles 6 | contribution: Andy Grover 7 | contribution: Tregaron Bayly 8 | contribution: Christophe Vu-Brugier 9 | testing: Dax Kelson 10 | testing: Gris Ge 11 | -------------------------------------------------------------------------------- /src/targetcli/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of targetcli. 3 | Copyright (c) 2011-2013 by Datera, Inc 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | not use this file except in compliance with the License. You may obtain 7 | a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations 15 | under the License. 16 | ''' 17 | 18 | 19 | import importlib.metadata 20 | 21 | __version__ = importlib.metadata.version('targetcli') 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | language: [ 'python' ] 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v3 29 | with: 30 | languages: ${{ matrix.language }} 31 | queries: security-and-quality 32 | 33 | - name: Autobuild 34 | uses: github/codeql-action/autobuild@v3 35 | 36 | - name: Perform CodeQL Analysis 37 | uses: github/codeql-action/analyze@v3 38 | with: 39 | category: "/language:${{matrix.language}}" 40 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.14.7 4 | hooks: 5 | - id: ruff 6 | args: [--fix] 7 | 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v6.0.0 10 | hooks: 11 | - id: check-case-conflict 12 | - id: check-ast 13 | - id: check-docstring-first 14 | - id: check-case-conflict 15 | - id: check-merge-conflict 16 | - id: check-builtin-literals 17 | - id: check-docstring-first 18 | - id: check-merge-conflict 19 | - id: check-toml 20 | - id: debug-statements 21 | - id: end-of-file-fixer 22 | - id: trailing-whitespace 23 | args: [--markdown-linebreak-ext=md] 24 | 25 | - repo: https://github.com/packit/pre-commit-hooks 26 | rev: v1.3.0 27 | hooks: 28 | - id: validate-config 29 | 30 | ci: 31 | autofix_commit_msg: | 32 | Auto fixes from pre-commit.com hooks 33 | autofix_prs: false 34 | autoupdate_schedule: monthly 35 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: pypi-release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | workflow_dispatch: 8 | inputs: 9 | ref: 10 | description: 'Branch, tag or SHA to checkout' 11 | required: true 12 | default: 'master' 13 | 14 | jobs: 15 | pypi-publish: 16 | runs-on: ubuntu-latest 17 | 18 | permissions: 19 | id-token: write # Needed for trusted publishing 20 | 21 | environment: 22 | name: pypi 23 | url: https://pypi.org/p/targetcli 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | ref: ${{ github.event.inputs.ref || github.ref }} 29 | 30 | - uses: actions/setup-python@v5 31 | with: 32 | python-version: "3.x" 33 | 34 | - name: Build a binary wheel and a source tarball 35 | run: | 36 | python -m pip install hatch 37 | hatch build 38 | 39 | - name: Publish to PyPI 40 | uses: pypa/gh-action-pypi-publish@release/v1 41 | -------------------------------------------------------------------------------- /.packit.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Basic configuration for Packit 3 | # See documentation: https://packit.dev/docs/configuration/ 4 | 5 | specfile_path: python-targetcli-fb.spec 6 | 7 | upstream_package_name: targetcli-fb 8 | # In Fedora, Python packages are prefixed with 'python-' 9 | downstream_package_name: python3-targetcli 10 | 11 | # Use hatch to create the source archive 12 | actions: 13 | create-archive: 14 | - hatch build -t sdist 15 | - bash -c "ls -1t ./dist/*.tar.gz | head -n 1" 16 | 17 | srpm_build_deps: 18 | - python3-devel 19 | - python3-pip 20 | - hatch 21 | - python3-hatchling 22 | - python3-hatch-vcs 23 | 24 | jobs: 25 | # Test PRs in COPR 26 | - job: copr_build 27 | trigger: pull_request 28 | targets: 29 | - fedora-latest-stable 30 | - epel-9 31 | 32 | # Run tests 33 | - job: tests 34 | trigger: pull_request 35 | targets: 36 | - fedora-latest-stable 37 | - epel-9 38 | 39 | # Sync with Fedora when a new release is created 40 | - job: propose_downstream 41 | trigger: release 42 | dist_git_branches: 43 | - fedora-all 44 | - epel-9 45 | -------------------------------------------------------------------------------- /python-targetcli-fb.spec: -------------------------------------------------------------------------------- 1 | Name: python-targetcli-fb 2 | Version: 2.1.58 3 | Release: %autorelease 4 | Summary: Command shell for managing the Linux LIO kernel target 5 | 6 | License: Apache-2.0 7 | URL: https://github.com/open-iscsi/targetcli-fb 8 | Source: %{pypi_source targetcli} 9 | 10 | BuildArch: noarch 11 | BuildRequires: python3-devel 12 | BuildRequires: python3-pip 13 | BuildRequires: hatch 14 | BuildRequires: python3-hatch-vcs 15 | BuildRequires: python3-hatchling 16 | BuildRequires: systemd-rpm-macros 17 | 18 | %global _description %{expand: 19 | An administration shell for configuring iSCSI, FCoE, and other SCSI targets, 20 | using the TCM/LIO kernel target subsystem. 21 | } 22 | 23 | %description 24 | %{_description} 25 | 26 | %package -n targetcli 27 | Summary: %{summary} 28 | Requires: python3-configshell 29 | Requires: python3-rtslib 30 | Requires: target-restore 31 | Requires: python3-six 32 | Requires: python3-dbus 33 | Requires: python3-gobject-base 34 | 35 | %description -n targetcli 36 | %{_description} 37 | 38 | %prep 39 | %autosetup -n targetcli-%{version} 40 | 41 | %build 42 | %pyproject_wheel 43 | 44 | %install 45 | %pyproject_install 46 | %pyproject_save_files targetcli 47 | 48 | mkdir -p %{buildroot}%{_sysconfdir}/target/backup 49 | install -d %{buildroot}%{_unitdir} 50 | install -p -m 644 systemd/targetclid.service %{buildroot}%{_unitdir}/ 51 | install -p -m 644 systemd/targetclid.socket %{buildroot}%{_unitdir}/ 52 | install -d %{buildroot}%{_mandir}/man8 53 | install -p -m 644 targetcli.8 %{buildroot}%{_mandir}/man8/ 54 | install -p -m 644 targetclid.8 %{buildroot}%{_mandir}/man8/ 55 | 56 | %files -n targetcli -f %{pyproject_files} 57 | %license COPYING 58 | %doc README.md THANKS 59 | %{_bindir}/targetcli 60 | %{_bindir}/targetclid 61 | %{_mandir}/man8/targetcli.8* 62 | %{_mandir}/man8/targetclid.8* 63 | %{_unitdir}/targetclid.service 64 | %{_unitdir}/targetclid.socket 65 | %dir %{_sysconfdir}/target 66 | %dir %{_sysconfdir}/target/backup 67 | 68 | %changelog 69 | %autochangelog 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | targetcli-fb 2 | ============ 3 | 4 | A command shell for managing the Linux LIO kernel target 5 | -------------------------------------------------------- 6 | An administration shell for configuring iSCSI, FCoE, and other 7 | SCSI targets, using the TCM/LIO kernel target subsystem. FCoE 8 | users will also need to install and use fcoe-utils. 9 | 10 | 11 | targetcli-fb development 12 | ------------------------ 13 | targetcli-fb is licensed under the Apache 2.0 license. Contributions are welcome. 14 | 15 | * Mailing list: [targetcli-fb-devel](https://lists.fedorahosted.org/mailman/listinfo/targetcli-fb-devel) 16 | * Source repo: [GitHub](https://github.com/open-iscsi/targetcli-fb) 17 | * Bugs: [GitHub](https://github.com/open-iscsi/targetcli-fb/issues) or [Trac](https://fedorahosted.org/targetcli-fb/) 18 | * Tarballs: [fedorahosted](https://fedorahosted.org/releases/t/a/targetcli-fb/) 19 | * Playlist of instructional screencast videos: [YouTube](https://www.youtube.com/playlist?list=PLC2C75481A3ABB067) 20 | 21 | Packages 22 | -------- 23 | targetcli-fb is packaged for a number of Linux distributions including 24 | RHEL, 25 | [Fedora](https://apps.fedoraproject.org/packages/targetcli), 26 | openSUSE, Arch Linux, 27 | [Gentoo](https://packages.gentoo.org/packages/sys-block/targetcli-fb), and 28 | [Debian](https://tracker.debian.org/pkg/targetcli-fb). 29 | 30 | Contribute 31 | ---------- 32 | targetcli complies with PEP 621 and as such can be built and installed with tools like `build` and `pip`. 33 | 34 | For development, consider using [Hatch](https://hatch.pypa.io): 35 | `hatch shell` to create and enter a Python virtualenv with the project installed in editable mode 36 | `pre-commit install` to enable pre-commit hooks 37 | `hatch build` to create tarball and wheel 38 | 39 | "fb" -- "free branch" 40 | --------------------- 41 | 42 | targetcli-fb is a fork of the "targetcli" code written by RisingTide Systems. 43 | The "-fb" differentiates between the original and this version. 44 | Please ensure to use either all "fb" versions of the targetcli components -- 45 | targetcli, rtslib, and configshell, or stick with all non-fb versions, since 46 | they are no longer strictly compatible. 47 | -------------------------------------------------------------------------------- /targetclid.8: -------------------------------------------------------------------------------- 1 | .TH targetclid 8 2 | .SH NAME 3 | .B targetclid 4 | \- daemon component for targetcli 5 | .SH DESCRIPTION 6 | .B targetclid 7 | is the daemon component of targetcli, which will help retain state of various 8 | configfs object in memory, hence any new request/command can directly use the 9 | in memory objects instead of reconstructing them by parsing through the entire 10 | configfs files again and again for each and every single command. This will 11 | greatly improve the overall execution time taken by targetcli commands at scale. 12 | 13 | .SH USAGE 14 | .B targetclid [cmd] 15 | .br 16 | .B "--help" 17 | for additional usage information. 18 | .br 19 | .B "--version" 20 | for version information. 21 | .SH QUICKSTART & EXAMPLES 22 | .TP 23 | To start using the daemon, one need to enable targetclid socket, 24 | .br 25 | $ systemctl enable targetclid.socket 26 | .TP 27 | If you would like to use the daemonized approach as default method then, 28 | .br 29 | $ targetcli set global auto_use_daemon=true 30 | .br 31 | $ targetcli ls 32 | .TP 33 | You can use interactive mode, 34 | .br 35 | $ targetcli 36 | .br 37 | targetcli shell version 2.1.51 38 | .br 39 | Entering targetcli interactive mode for daemonized approach. 40 | .br 41 | Type 'exit' to quit. 42 | .br 43 | /> pwd 44 | .br 45 | / 46 | .br 47 | /> cd /iscsi 48 | .br 49 | /> pwd 50 | .br 51 | /iscsi 52 | .br 53 | /> exit 54 | .br 55 | .TP 56 | You can also use batch mode for sending multiple commands in one go, 57 | .br 58 | $ targetcli set global daemon_use_batch_mode=true 59 | .br 60 | Parameter daemon_use_batch_mode is now 'true'. 61 | .br 62 | $ targetcli 63 | .br 64 | targetcli shell version 2.1.51 65 | .br 66 | Entering targetcli batch mode for daemonized approach. 67 | .br 68 | Enter multiple commands separated by newline and type 'exit' to run them all in one go. 69 | .br 70 | /> ls 71 | .br 72 | /> pwd 73 | .br 74 | /> get global loglevel_file 75 | .br 76 | /> exit 77 | .br 78 | .TP 79 | You can set preference to stop using daemonized mode even when the daemon is not running, 80 | .br 81 | $ targetcli --disable-daemon 82 | .SH FILES 83 | .B /etc/target/saveconfig.json 84 | .br 85 | .B /etc/target/backup/* 86 | .br 87 | .B /var/run/targetclid.sock 88 | .br 89 | .B /var/run/targetclid.pid 90 | .SH ENVIRONMENT 91 | .SS TARGETCLI_HOME 92 | If set, this variable points to a directory that should be used instead of ~/.targetcli 93 | .SH SEE ALSO 94 | .BR targetcli (8), 95 | .BR targetctl (8), 96 | .BR tcmu-runner (8) 97 | .SH AUTHOR 98 | Written by Prasanna Kumar Kalever 99 | .br 100 | Man page written by Prasanna Kumar Kalever 101 | .SH REPORTING BUGS 102 | Report bugs via 103 | .br 104 | or 105 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling", "hatch-vcs"] 4 | 5 | [project] 6 | name = "targetcli" 7 | description = "A command shell for managing the Linux LIO kernel target" 8 | readme = "README.md" 9 | license = "Apache-2.0" 10 | requires-python = ">=3.9" 11 | authors = [{email = "agrover@redhat.com", name = "Andy Grover"}] 12 | maintainers = [{email = "mlombard@redhat.com", name = "Maurizio Lombardi"}] 13 | classifiers = [ 14 | "License :: OSI Approved :: Apache Software License", 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.9", 17 | "Programming Language :: Python :: 3.12", 18 | ] 19 | dependencies = [ 20 | "PyGObject", 21 | "configshell-fb", 22 | "rtslib-fb", 23 | ] 24 | dynamic = ["version"] 25 | 26 | [project.license-files] 27 | paths = ["COPYING"] 28 | 29 | [project.scripts] 30 | targetcli = "targetcli.targetcli_shell:main" 31 | targetclid = "targetcli.targetclid:main" 32 | 33 | [project.urls] 34 | Homepage = "http://github.com/open-iscsi/targetcli-fb" 35 | 36 | [tool.hatch.build.targets.sdist] 37 | include = [ 38 | ".pre-commit-config.yaml", 39 | "THANKS", 40 | "src", 41 | "systemd", 42 | "targetcli*", 43 | ] 44 | 45 | [tool.hatch.version] 46 | source = "vcs" 47 | 48 | [tool.hatch.envs.default] 49 | dependencies = [ 50 | "ruff", 51 | "pre-commit", 52 | ] 53 | 54 | [tool.ruff] 55 | line-length = 120 56 | 57 | [tool.ruff.lint] 58 | select = [ 59 | "F", # Pyflakes 60 | "E", # pycodestyle error 61 | "W", # pycodestyle warning 62 | "I", # isort 63 | "N", # pep8-naming 64 | "UP", # pyupgrade 65 | "YTT", # flake8-2020 66 | "S", # flake8-bandit 67 | "B", # flake8-bugbear 68 | "A", # flake8-builtins 69 | "COM", # flake8-commas 70 | "C4", # flake8-comprehensions 71 | "EXE", # flake8-executable 72 | "FA", # flake8-future-annotations 73 | "ISC", # flake8-implicit-str-concat 74 | "ICN", # flake8-import-conventions 75 | "PIE", # flake8-pie 76 | "Q003", # flake8-quotes avoidable-escaped-quote 77 | "Q004", # flake8-quotes unnecessary-escaped-quote 78 | "RSE", # flake8-raise 79 | "RET", # flake8-return 80 | "SIM", # flake8-simplify 81 | "TID", # flake8-tidy-imports 82 | "INT", # flake8-gettext 83 | "ARG", # flake8-unused-argument 84 | #"PTH", # flake8-use-pathlib TODO 85 | "PL", # Pylint 86 | "FLY", # flynt 87 | "PERF", # Perflint 88 | "FURB", # refurb 89 | "RUF", # Ruff 90 | ] 91 | ignore = [ 92 | "E722", # TODO do not use bare 'except' 93 | "S104", # Possible binding to all interfaces (0.0.0.0) 94 | "RUF012", # TODO Mutable class attributes should be annotated with `typing.ClassVar` 95 | "B904", # raise-without-from-inside-except 96 | "ARG002", "PLR6301", # TODO Unused self, parameter in methods definitions 97 | "PLR09", # Too many branches/statements/arguments 98 | "PLW1514", # TODO `open` in text mode without explicit `encoding` argument 99 | "UP031", # Use format specifiers instead of percent format 100 | ] 101 | [tool.ruff.lint.flake8-quotes] 102 | # Single quotes are currently prevalent in the codebase. Not being checked. 103 | inline-quotes = "single" 104 | 105 | [tool.ruff.lint.per-file-ignores] 106 | # Preserving long lines for readability 107 | "src/targetcli/ui_target.py" = ["E501"] 108 | -------------------------------------------------------------------------------- /src/targetcli/ui_node.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implements the targetcli base UI node. 3 | 4 | This file is part of targetcli. 5 | Copyright (c) 2011-2013 by Datera, Inc 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | 20 | 21 | from configshell import ConfigNode, ExecutionError 22 | 23 | 24 | class UINode(ConfigNode): 25 | ''' 26 | Our targetcli basic UI node. 27 | ''' 28 | def __init__(self, name, parent=None, shell=None): 29 | ConfigNode.__init__(self, name, parent, shell) 30 | self.define_config_group_param( 31 | 'global', 'export_backstore_name_as_model', 'bool', 32 | 'If true, the backstore name is used for the scsi inquiry model name.') 33 | self.define_config_group_param( 34 | 'global', 'auto_enable_tpgt', 'bool', 35 | 'If true, automatically enables TPGTs upon creation.') 36 | self.define_config_group_param( 37 | 'global', 'auto_add_mapped_luns', 'bool', 38 | 'If true, automatically create node ACLs mapped LUNs after creating a new target LUN or a new node ACL') 39 | self.define_config_group_param( 40 | 'global', 'auto_cd_after_create', 'bool', 41 | 'If true, changes current path to newly created objects.') 42 | self.define_config_group_param( 43 | 'global', 'auto_save_on_exit', 'bool', 44 | 'If true, saves configuration on exit.') 45 | self.define_config_group_param( 46 | 'global', 'auto_add_default_portal', 'bool', 47 | 'If true, adds a portal listening on all IPs to new targets.') 48 | self.define_config_group_param( 49 | 'global', 'max_backup_files', 'string', 50 | 'Max no. of configurations to be backed up in /etc/target/backup/ directory.') 51 | self.define_config_group_param( 52 | 'global', 'auto_use_daemon', 'bool', 53 | 'If true, commands will be sent to targetclid.') 54 | self.define_config_group_param( 55 | 'global', 'daemon_use_batch_mode', 'bool', 56 | 'If true, use batch mode for daemonized approach.') 57 | 58 | def assert_root(self): 59 | ''' 60 | For commands requiring root privileges, disable command if not the root 61 | node's as_root attribute is False. 62 | ''' 63 | root_node = self.get_root() 64 | if hasattr(root_node, 'as_root') and not self.get_root().as_root: 65 | raise ExecutionError("This privileged command is disabled: you are not root.") 66 | 67 | def new_node(self, new_node): 68 | ''' 69 | Used to honor global 'auto_cd_after_create'. 70 | Either returns None if the global is False, or the new_node if the 71 | global is True. In both cases, set the @last bookmark to last_node. 72 | ''' 73 | self.shell.prefs['bookmarks']['last'] = new_node.path 74 | self.shell.prefs.save() 75 | if self.shell.prefs['auto_cd_after_create']: 76 | self.shell.log.info(f"Entering new node {new_node.path}") 77 | # Piggy backs on cd instead of just returning new_node, 78 | # so we update navigation history. 79 | return self.ui_command_cd(new_node.path) 80 | return None 81 | 82 | def refresh(self): 83 | ''' 84 | Refreshes and updates the objects tree from the current path. 85 | ''' 86 | for child in self.children: 87 | child.refresh() 88 | 89 | def ui_command_refresh(self): 90 | ''' 91 | Refreshes and updates the objects tree from the current path. 92 | ''' 93 | self.refresh() 94 | 95 | def ui_command_status(self): 96 | ''' 97 | Displays the current node's status summary. 98 | 99 | SEE ALSO 100 | ======== 101 | ls 102 | ''' 103 | description, _is_healthy = self.summary() 104 | self.shell.log.info(f"Status for {self.path}: {description}") 105 | 106 | def ui_setgroup_global(self, parameter, value): 107 | ConfigNode.ui_setgroup_global(self, parameter, value) 108 | self.get_root().refresh() 109 | 110 | def ui_type_yesno(self, value=None, enum=False, reverse=False): 111 | ''' 112 | UI parameter type helper for "Yes" and "No" boolean values. 113 | "Yes" and "No" are used for boolean iSCSI session parameters. 114 | ''' 115 | if reverse: 116 | if value is not None: 117 | return value 118 | return 'n/a' 119 | type_enum = ('Yes', 'No') 120 | syntax = '|'.join(type_enum) 121 | if value is None: 122 | if enum: 123 | return type_enum 124 | return syntax 125 | if value in type_enum: 126 | return value 127 | raise ValueError(f"Syntax error, '{value}' is not {syntax}.") 128 | 129 | 130 | class UIRTSLibNode(UINode): 131 | ''' 132 | A subclass of UINode for nodes with an underlying RTSLib object. 133 | ''' 134 | def __init__(self, name, rtslib_object, parent, late_params=False): 135 | ''' 136 | Call from the class that inherits this, with the rtslib object that 137 | should be checked upon. 138 | ''' 139 | UINode.__init__(self, name, parent) 140 | self.rtsnode = rtslib_object 141 | 142 | if late_params: 143 | return 144 | 145 | # If the rtsnode has parameters, use them 146 | parameters = self.rtsnode.list_parameters() 147 | parameters_ro = self.rtsnode.list_parameters(writable=False) 148 | for parameter in parameters: 149 | writable = parameter not in parameters_ro 150 | param_type, desc = getattr(self.__class__, 'ui_desc_parameters', {}).get(parameter, ('string', '')) 151 | self.define_config_group_param( 152 | 'parameter', parameter, param_type, desc, writable) 153 | 154 | # If the rtsnode has attributes, enable them 155 | attributes = self.rtsnode.list_attributes() 156 | attributes_ro = self.rtsnode.list_attributes(writable=False) 157 | for attribute in attributes: 158 | writable = attribute not in attributes_ro 159 | param_type, desc = getattr(self.__class__, 'ui_desc_attributes', {}).get(attribute, ('string', '')) 160 | self.define_config_group_param( 161 | 'attribute', attribute, param_type, desc, writable) 162 | 163 | def ui_getgroup_attribute(self, attribute): 164 | ''' 165 | This is the backend method for getting attributes. 166 | @param attribute: The attribute to get the value of. 167 | @type attribute: str 168 | @return: The attribute's value 169 | @rtype: arbitrary 170 | ''' 171 | return self.rtsnode.get_attribute(attribute) 172 | 173 | def ui_setgroup_attribute(self, attribute, value): 174 | ''' 175 | This is the backend method for setting attributes. 176 | @param attribute: The attribute to set the value of. 177 | @type attribute: str 178 | @param value: The attribute's value 179 | @type value: arbitrary 180 | ''' 181 | self.assert_root() 182 | self.rtsnode.set_attribute(attribute, value) 183 | 184 | def ui_getgroup_parameter(self, parameter): 185 | ''' 186 | This is the backend method for getting parameters. 187 | @param parameter: The parameter to get the value of. 188 | @type parameter: str 189 | @return: The parameter's value 190 | @rtype: arbitrary 191 | ''' 192 | return self.rtsnode.get_parameter(parameter) 193 | 194 | def ui_setgroup_parameter(self, parameter, value): 195 | ''' 196 | This is the backend method for setting parameters. 197 | @param parameter: The parameter to set the value of. 198 | @type parameter: str 199 | @param value: The parameter's value 200 | @type value: arbitrary 201 | ''' 202 | self.assert_root() 203 | self.rtsnode.set_parameter(parameter, value) 204 | 205 | def ui_command_info(self): 206 | info = self.rtsnode.dump() 207 | for item in ('attributes', 'parameters'): 208 | if item in info: 209 | del info[item] 210 | for name, value in sorted(info.items()): 211 | if not isinstance(value, (dict, list)): 212 | self.shell.log.info(f"{name}: {value}") 213 | -------------------------------------------------------------------------------- /src/targetcli/targetclid.py: -------------------------------------------------------------------------------- 1 | ''' 2 | targetclid 3 | 4 | This file is part of targetcli-fb. 5 | Copyright (c) 2019 by Red Hat, Inc. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | 20 | import contextlib 21 | import errno 22 | import fcntl 23 | import os 24 | import signal 25 | import socket 26 | import stat 27 | import struct 28 | import sys 29 | import tempfile 30 | from os import getenv, getuid 31 | from pathlib import Path 32 | from threading import Thread 33 | 34 | from configshell import ConfigShell 35 | 36 | from targetcli import __version__ as targetcli_version 37 | from targetcli.ui_root import UIRoot 38 | 39 | err = sys.stderr 40 | 41 | class TargetCLI: 42 | def __init__(self): 43 | ''' 44 | initializer 45 | ''' 46 | # socket for unix communication 47 | self.socket_path = '/var/run/targetclid.sock' 48 | # pid file for defending on multiple daemon runs 49 | self.pid_file = '/var/run/targetclid.pid' 50 | 51 | self.NoSignal = True 52 | self.sock = None 53 | 54 | # shell console methods 55 | self.shell = ConfigShell(getenv("TARGETCLI_HOME", '~/.targetcli')) 56 | self.con = self.shell.con 57 | self.display = self.shell.con.display 58 | self.render = self.shell.con.render_text 59 | 60 | # Handle SIGINT SIGTERM SIGHUP gracefully 61 | signal.signal(signal.SIGINT, self.signal_handler) 62 | signal.signal(signal.SIGTERM, self.signal_handler) 63 | signal.signal(signal.SIGHUP, self.signal_handler) 64 | 65 | try: 66 | self.pfd = open(self.pid_file, 'w+') # noqa: SIM115 67 | except OSError as e: 68 | self.display( 69 | self.render(f"opening pidfile failed: {e!s}", 'red'), 70 | ) 71 | sys.exit(1) 72 | 73 | self.try_pidfile_lock() 74 | 75 | is_root = False 76 | if getuid() == 0: 77 | is_root = True 78 | 79 | try: 80 | root_node = UIRoot(self.shell, as_root=is_root) 81 | root_node.refresh() 82 | except Exception as error: 83 | self.display(self.render(str(error), 'red')) 84 | if not is_root: 85 | self.display(self.render("Retry as root.", 'red')) 86 | self.pfd.close() 87 | sys.exit(1) 88 | 89 | # Keep track, for later use 90 | self.con_stdout_ = self.con._stdout 91 | self.con_stderr_ = self.con._stderr 92 | 93 | 94 | def __del__(self): 95 | ''' 96 | destructor 97 | ''' 98 | if hasattr(self, 'pfd'): 99 | self.pfd.close() 100 | 101 | 102 | def signal_handler(self): 103 | ''' 104 | signal handler 105 | ''' 106 | self.NoSignal = False 107 | if self.sock: 108 | self.sock.close() 109 | 110 | 111 | def try_pidfile_lock(self): 112 | ''' 113 | get lock on pidfile, which is to check if targetclid is running 114 | ''' 115 | # check if targetclid is already running 116 | lock = struct.pack('hhllhh', fcntl.F_WRLCK, 0, 0, 0, 0, 0) 117 | try: 118 | fcntl.fcntl(self.pfd, fcntl.F_SETLK, lock) 119 | except Exception: 120 | self.display(self.render("targetclid is already running...", 'red')) 121 | self.pfd.close() 122 | sys.exit(1) 123 | 124 | 125 | def release_pidfile_lock(self): 126 | ''' 127 | release lock on pidfile 128 | ''' 129 | lock = struct.pack('hhllhh', fcntl.F_UNLCK, 0, 0, 0, 0, 0) 130 | try: 131 | fcntl.fcntl(self.pfd, fcntl.F_SETLK, lock) 132 | except Exception as e: 133 | self.display( 134 | self.render(f"fcntl(UNLCK) on pidfile failed: {e!s}", 'red'), 135 | ) 136 | self.pfd.close() 137 | sys.exit(1) 138 | self.pfd.close() 139 | 140 | 141 | def client_thread(self, connection): 142 | ''' 143 | Handle commands from client 144 | ''' 145 | # load the prefs 146 | self.shell.prefs.load() 147 | 148 | still_listen = True 149 | # Receive the data in small chunks and retransmit it 150 | while still_listen: 151 | data = connection.recv(65535) 152 | if b'-END@OF@DATA-' in data: 153 | connection.close() 154 | still_listen = False 155 | else: 156 | self.con._stdout = self.con._stderr = f = tempfile.NamedTemporaryFile(mode='w', delete=False) # noqa: SIM115 157 | try: 158 | # extract multiple commands delimited with '%' 159 | list_data = data.decode().split('%') 160 | for cmd in list_data: 161 | self.shell.run_cmdline(cmd) 162 | except Exception as e: 163 | print(str(e), file=f) # push error to stream 164 | 165 | # Restore 166 | self.con._stdout = self.con_stdout_ 167 | self.con._stderr = self.con_stderr_ 168 | f.close() 169 | 170 | with open(f.name) as f: 171 | output = f.read() 172 | var = struct.pack('i', len(output)) 173 | connection.sendall(var) # length of string 174 | if len(output): 175 | connection.sendall(output.encode()) # actual string 176 | 177 | Path(f.name).unlink() 178 | 179 | 180 | def usage(): 181 | print(f"Usage: {sys.argv[0]} [--version|--help]", file=err) 182 | print(" --version\t\tPrint version", file=err) 183 | print(" --help\t\tPrint this information", file=err) 184 | sys.exit(0) 185 | 186 | 187 | def version(): 188 | print(f"{sys.argv[0]} version {targetcli_version}", file=err) 189 | sys.exit(0) 190 | 191 | 192 | def usage_version(cmd): 193 | if cmd in {"help", "--help", "-h"}: 194 | usage() 195 | 196 | if cmd in {"version", "--version", "-v"}: 197 | version() 198 | 199 | 200 | def main(): 201 | ''' 202 | start targetclid 203 | ''' 204 | if len(sys.argv) > 1: 205 | usage_version(sys.argv[1]) 206 | print(f"unrecognized option: {sys.argv[1]}") 207 | sys.exit(-1) 208 | 209 | to = TargetCLI() 210 | 211 | if getenv('LISTEN_PID'): 212 | # the systemd-activation path, using next available FD 213 | fn = sys.stderr.fileno() + 1 214 | try: 215 | sock = socket.fromfd(fn, socket.AF_UNIX, socket.SOCK_STREAM) 216 | except OSError as err: 217 | to.display(to.render(err.strerror, 'red')) 218 | sys.exit(1) 219 | 220 | # save socket so a signal can clea it up 221 | to.sock = sock 222 | else: 223 | # Make sure file doesn't exist already 224 | with contextlib.suppress(FileNotFoundError): 225 | Path(to.socket_path).unlink() 226 | 227 | # Create a TCP/IP socket 228 | try: 229 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 230 | except OSError as err: 231 | to.display(to.render(err.strerror, 'red')) 232 | sys.exit(1) 233 | 234 | # save socket so a signal can clea it up 235 | to.sock = sock 236 | 237 | mode = stat.S_IRUSR | stat.S_IWUSR # 0o600 238 | umask = 0o777 ^ mode # Prevents always downgrading umask to 0 239 | umask_original = os.umask(umask) 240 | # Bind the socket path 241 | try: 242 | sock.bind(to.socket_path) 243 | except OSError as err: 244 | to.display(to.render(err.strerror, 'red')) 245 | sys.exit(1) 246 | finally: 247 | os.umask(umask_original) 248 | 249 | # Listen for incoming connections 250 | try: 251 | sock.listen(1) 252 | except OSError as err: 253 | to.display(to.render(err.strerror, 'red')) 254 | sys.exit(1) 255 | 256 | while to.NoSignal: 257 | try: 258 | # Wait for a connection 259 | connection, _client_address = sock.accept() 260 | except OSError as err: 261 | if err.errno != errno.EBADF or to.NoSignal: 262 | to.display(to.render(err.strerror, 'red')) 263 | break 264 | 265 | thread = Thread(target=to.client_thread, args=(connection,)) 266 | thread.start() 267 | try: 268 | thread.join() 269 | except Exception as error: 270 | to.display(to.render(str(error), 'red')) 271 | 272 | to.release_pidfile_lock() 273 | 274 | if not to.NoSignal: 275 | to.display(to.render("Signal received, quiting gracefully!", 'green')) 276 | sys.exit(0) 277 | sys.exit(1) 278 | 279 | 280 | if __name__ == "__main__": 281 | main() 282 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /src/targetcli/targetcli_shell.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Starts the targetcli CLI shell. 3 | 4 | This file is part of targetcli. 5 | Copyright (c) 2011-2013 by Datera, Inc 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | 20 | 21 | import contextlib 22 | import fcntl 23 | import readline 24 | import socket 25 | import struct 26 | import sys 27 | from os import getenv, getuid 28 | 29 | from configshell import ConfigShell, ExecutionError 30 | from rtslib import RTSLibError 31 | 32 | from targetcli import __version__ as targetcli_version 33 | from targetcli.ui_root import UIRoot 34 | 35 | err = sys.stderr 36 | # lockfile for serializing multiple targetcli requests 37 | lock_file = '/var/run/targetcli.lock' 38 | socket_path = '/var/run/targetclid.sock' 39 | hints = ['/', 'backstores/', 'iscsi/', 'loopback/', 'vhost/', 'xen-pvscsi/', 40 | 'cd', 'pwd', 'ls', 'set', 'get', 'help', 'refresh', 'status', 41 | 'clearconfig', 'restoreconfig', 'saveconfig', 'exit'] 42 | 43 | class TargetCLI(ConfigShell): 44 | default_prefs = {'color_path': 'magenta', 45 | 'color_command': 'cyan', 46 | 'color_parameter': 'magenta', 47 | 'color_keyword': 'cyan', 48 | 'completions_in_columns': True, 49 | 'logfile': None, 50 | 'loglevel_console': 'info', 51 | 'loglevel_file': 'debug9', 52 | 'color_mode': True, 53 | 'prompt_length': 30, 54 | 'tree_max_depth': 0, 55 | 'tree_status_mode': True, 56 | 'tree_round_nodes': True, 57 | 'tree_show_root': True, 58 | 'export_backstore_name_as_model': True, 59 | 'auto_enable_tpgt': True, 60 | 'auto_add_mapped_luns': True, 61 | 'auto_cd_after_create': False, 62 | 'auto_save_on_exit': True, 63 | 'max_backup_files': '10', 64 | 'auto_add_default_portal': True, 65 | 'auto_use_daemon': False, 66 | 'daemon_use_batch_mode': False, 67 | } 68 | 69 | def usage(): 70 | print(f"Usage: {sys.argv[0]} [--version|--help|CMD|--disable-daemon]", file=err) 71 | print(" --version\t\tPrint version", file=err) 72 | print(" --help\t\tPrint this information", file=err) 73 | print(" CMD\t\t\tRun targetcli shell command and exit", file=err) 74 | print(" \t\tEnter configuration shell", file=err) 75 | print(" --disable-daemon\tTurn-off the global auto use daemon flag", file=err) 76 | print("See man page for more information.", file=err) 77 | sys.exit(-1) 78 | 79 | def version(): 80 | print(f"{sys.argv[0]} version {targetcli_version}", file=err) 81 | sys.exit(0) 82 | 83 | def usage_version(cmd): 84 | if cmd in {"help", "--help", "-h"}: 85 | usage() 86 | 87 | if cmd in {"version", "--version", "-v"}: 88 | version() 89 | 90 | def try_op_lock(shell, lkfd): 91 | ''' 92 | acquire a blocking lock on lockfile, to serialize multiple requests 93 | ''' 94 | try: 95 | fcntl.flock(lkfd, fcntl.LOCK_EX) # wait here until ongoing request is finished 96 | except Exception as e: 97 | shell.con.display( 98 | shell.con.render_text( 99 | f"taking lock on lockfile failed: {e!s}", 100 | 'red')) 101 | sys.exit(1) 102 | 103 | def release_op_lock(shell, lkfd): 104 | ''' 105 | release blocking lock on lockfile, which can allow other requests process 106 | ''' 107 | try: 108 | fcntl.flock(lkfd, fcntl.LOCK_UN) # allow other requests now 109 | except Exception as e: 110 | shell.con.display( 111 | shell.con.render_text( 112 | f"unlock on lockfile failed: {e!s}", 113 | 'red')) 114 | sys.exit(1) 115 | lkfd.close() 116 | 117 | def completer(text, state): 118 | options = [x for x in hints if x.startswith(text)] 119 | try: 120 | return options[state] 121 | except IndexError: 122 | return None 123 | 124 | def call_daemon(shell, req, interactive): 125 | try: 126 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 127 | except OSError as err: 128 | shell.con.display(shell.con.render_text(err, 'red')) 129 | sys.exit(1) 130 | 131 | try: 132 | sock.connect(socket_path) 133 | except OSError as err: 134 | shell.con.display(shell.con.render_text(err, 'red')) 135 | shell.con.display( 136 | shell.con.render_text("Currently auto_use_daemon is true, " 137 | "hence please make sure targetclid daemon is running ...\n" 138 | "(or)\nIncase if you wish to turn auto_use_daemon to false " 139 | "then run '#targetcli --disable-daemon'", 'red')) 140 | sys.exit(1) 141 | 142 | # Two cases where we want to get pwd: 143 | # 1. Before starting shell in interactive mode, needed for setting terminal 144 | # 2. And only in Interactive mode, having command 'cd' 145 | get_pwd = False 146 | if interactive: 147 | if not req: 148 | req = "pwd" 149 | get_pwd = True 150 | elif "cd " in req: 151 | req += "%pwd" 152 | get_pwd = True 153 | else: 154 | req = "cd /%" + req # Non-interactive modes always consider start at '/' 155 | 156 | try: 157 | # send request 158 | sock.sendall(req.encode()) 159 | except OSError as err: 160 | shell.con.display(shell.con.render_text(err, 'red')) 161 | sys.exit(1) 162 | 163 | var = sock.recv(4) # get length of data 164 | sending = struct.unpack('i', var) 165 | amount_expected = sending[0] 166 | amount_received = 0 167 | 168 | # get the actual data in chunks 169 | output = "" 170 | path = "" 171 | while amount_received < amount_expected: 172 | data = sock.recv(1024) 173 | data = data.decode() 174 | amount_received += len(data) 175 | output += data 176 | 177 | if get_pwd: 178 | output_split = output.splitlines() 179 | lines = len(output_split) 180 | for i in range(lines): 181 | if i == lines - 1: 182 | path = str(output_split[i]) 183 | else: 184 | print(str(output_split[i]), end="\n") 185 | else: 186 | print(output, end="") 187 | 188 | sock.send(b'-END@OF@DATA-') 189 | sock.close() 190 | 191 | return path 192 | 193 | def switch_to_daemon(shell, interactive): 194 | readline.set_completer(completer) 195 | readline.set_completer_delims('') 196 | 197 | if 'libedit' in readline.__doc__: 198 | readline.parse_and_bind("bind ^I rl_complete") 199 | else: 200 | readline.parse_and_bind("tab: complete") 201 | 202 | if len(sys.argv) > 1: 203 | command = " ".join(sys.argv[1:]) 204 | call_daemon(shell, command, False) 205 | sys.exit(0) 206 | 207 | if interactive: 208 | shell.con.display(f"targetcli shell version {targetcli_version}\n" 209 | "Entering targetcli interactive mode for daemonized approach.\n" 210 | "Type 'exit' to quit.\n") 211 | else: 212 | shell.con.display(f"targetcli shell version {targetcli_version}\n" 213 | "Entering targetcli batch mode for daemonized approach.\n" 214 | "Enter multiple commands separated by newline and " 215 | "type 'exit' to run them all in one go.\n") 216 | 217 | prompt_path = "/" 218 | if interactive: 219 | prompt_path = call_daemon(shell, None, interactive) # get the initial path 220 | 221 | inputs = [] 222 | real_exit = False 223 | while True: 224 | command = input(f"{prompt_path}> ") 225 | if command.lower() == "exit": 226 | real_exit = True 227 | elif not command: 228 | continue 229 | if not interactive: 230 | inputs.append(command) 231 | if real_exit: 232 | command = '%'.join(inputs) # delimit multiple commands with '%' 233 | call_daemon(shell, command, interactive) 234 | break 235 | else: 236 | if real_exit: 237 | break 238 | path = call_daemon(shell, command, interactive) 239 | if path: 240 | if path[0] == "/": 241 | prompt_path = path 242 | else: 243 | print(path) # Error No Path ... 244 | 245 | sys.exit(0) 246 | 247 | def main(): 248 | ''' 249 | Start the targetcli shell. 250 | ''' 251 | shell = TargetCLI(getenv("TARGETCLI_HOME", '~/.targetcli')) 252 | 253 | is_root = False 254 | if getuid() == 0: 255 | is_root = True 256 | 257 | try: 258 | lkfd = open(lock_file, 'w+') # noqa: SIM115 259 | except OSError as e: 260 | shell.con.display( 261 | shell.con.render_text(f"opening lockfile failed: {e!s}", 262 | 'red')) 263 | sys.exit(1) 264 | 265 | try_op_lock(shell, lkfd) 266 | 267 | use_daemon = False 268 | if shell.prefs['auto_use_daemon']: 269 | use_daemon = True 270 | 271 | disable_daemon = False 272 | if len(sys.argv) > 1: 273 | usage_version(sys.argv[1]) 274 | if sys.argv[1] in {"disable-daemon", "--disable-daemon"}: 275 | disable_daemon = True 276 | 277 | interactive_mode = True 278 | if shell.prefs['daemon_use_batch_mode']: 279 | interactive_mode = False 280 | 281 | if use_daemon and not disable_daemon: 282 | switch_to_daemon(shell, interactive_mode) 283 | # does not return 284 | 285 | try: 286 | root_node = UIRoot(shell, as_root=is_root) 287 | root_node.refresh() 288 | except Exception as error: 289 | shell.con.display(shell.con.render_text(str(error), 'red')) 290 | if not is_root: 291 | shell.con.display(shell.con.render_text("Retry as root.", 'red')) 292 | sys.exit(-1) 293 | 294 | if len(sys.argv) > 1: 295 | try: 296 | if disable_daemon: 297 | shell.run_cmdline('set global auto_use_daemon=false') 298 | else: 299 | shell.run_cmdline(" ".join(sys.argv[1:])) 300 | except Exception as e: 301 | print(str(e), file=sys.stderr) 302 | sys.exit(1) 303 | sys.exit(0) 304 | 305 | shell.con.display(f"targetcli shell version {targetcli_version}\n" 306 | "Copyright 2011-2013 by Datera, Inc and others.\n" 307 | "For help on commands, type 'help'.\n") 308 | if not is_root: 309 | shell.con.display("You are not root, disabling privileged commands.\n") 310 | 311 | while not shell._exit: 312 | try: 313 | shell.run_interactive() 314 | except (RTSLibError, ExecutionError) as msg: # noqa: PERF203 - would otherwise exit shell 315 | shell.log.error(str(msg)) 316 | 317 | if shell.prefs['auto_save_on_exit'] and is_root: 318 | shell.log.info("Global pref auto_save_on_exit=true") 319 | root_node.ui_command_saveconfig() 320 | 321 | release_op_lock(shell, lkfd) 322 | 323 | 324 | if __name__ == "__main__": 325 | with contextlib.suppress(KeyboardInterrupt): 326 | main() 327 | -------------------------------------------------------------------------------- /src/targetcli/ui_root.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implements the targetcli root UI. 3 | 4 | This file is part of targetcli. 5 | Copyright (c) 2011-2013 by Datera, Inc 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | 20 | import gzip 21 | import os 22 | import re 23 | import shutil 24 | import stat 25 | from datetime import datetime 26 | from glob import glob 27 | from pathlib import Path, PurePosixPath 28 | 29 | from configshell import ExecutionError 30 | from rtslib import RTSRoot 31 | from rtslib.utils import ignored 32 | 33 | from targetcli import __version__ 34 | 35 | from .ui_backstore import UIBackstores, complete_path 36 | from .ui_node import UINode 37 | from .ui_target import UIFabricModule 38 | 39 | default_target_dir = "/etc/target" 40 | default_save_file = os.path.join(default_target_dir, "saveconfig.json") 41 | universal_prefs_file = os.path.join(default_target_dir, "targetcli.conf") 42 | 43 | class UIRoot(UINode): 44 | ''' 45 | The targetcli hierarchy root node. 46 | ''' 47 | def __init__(self, shell, as_root=False): 48 | UINode.__init__(self, '/', shell=shell) 49 | self.as_root = as_root 50 | self.rtsroot = RTSRoot() 51 | 52 | def refresh(self): 53 | ''' 54 | Refreshes the tree of target fabric modules. 55 | ''' 56 | self._children = set() 57 | 58 | # Invalidate any rtslib caches 59 | if 'invalidate_caches' in dir(RTSRoot): 60 | self.rtsroot.invalidate_caches() 61 | 62 | UIBackstores(self) 63 | 64 | # only show fabrics present in the system 65 | for fm in self.rtsroot.fabric_modules: 66 | if fm.wwns is None or any(fm.wwns): 67 | UIFabricModule(fm, self) 68 | 69 | def _compare_files(self, backupfile, savefile): 70 | ''' 71 | Compare backfile and saveconfig file 72 | ''' 73 | backupfilepath = Path(backupfile) 74 | if PurePosixPath(backupfile).suffix == '.gz': 75 | try: 76 | with gzip.open(backupfilepath, 'rb') as fbkp: 77 | fdata_bkp = fbkp.read() 78 | except OSError as e: 79 | self.shell.log.warning(f"Could not gzip open backupfile {backupfile}: {e.strerror}") 80 | 81 | else: 82 | try: 83 | fdata_bkp = backupfilepath.read_bytes() 84 | except OSError as e: 85 | self.shell.log.warning(f"Could not open backupfile {backupfile}: {e.strerror}") 86 | 87 | try: 88 | fdata = Path(savefile).read_bytes() 89 | except OSError as e: 90 | self.shell.log.warning(f"Could not open saveconfig file {savefile}: {e.strerror}") 91 | 92 | return fdata_bkp == fdata 93 | 94 | def _create_dir(self, dirname): 95 | ''' 96 | create directory with permissions 0o600 set 97 | if directory already exists, set right perms 98 | ''' 99 | mode = stat.S_IRUSR | stat.S_IWUSR # 0o600 100 | dir_path = Path(dirname) 101 | if not dir_path.exists(): 102 | umask = 0o777 ^ mode # Prevents always downgrading umask to 0 103 | umask_original = os.umask(umask) 104 | try: 105 | dir_path.mkdir(mode=mode) 106 | except OSError as exe: 107 | raise ExecutionError(f"Cannot create directory [{dirname}] {exe.strerror}.") 108 | finally: 109 | os.umask(umask_original) 110 | elif dirname == default_target_dir and (os.stat(dirname).st_mode & 0o777) != mode: 111 | os.chmod(dirname, mode) 112 | 113 | def _save_backups(self, savefile): 114 | ''' 115 | Take backup of config-file if needed. 116 | ''' 117 | # Only save backups if saving to default location 118 | if savefile != default_save_file: 119 | return 120 | 121 | backup_dir = os.path.dirname(savefile) + "/backup/" 122 | backup_name = "saveconfig-" + \ 123 | datetime.now().strftime("%Y%m%d-%H:%M:%S") + "-json.gz" 124 | backupfile = backup_dir + backup_name 125 | backup_error = None 126 | 127 | self._create_dir(backup_dir) 128 | 129 | # Only save backups if savefile exits 130 | if not Path(savefile).exists(): 131 | return 132 | 133 | backed_files_list = sorted(glob(os.path.dirname(savefile) + \ 134 | "/backup/saveconfig-*json*")) 135 | 136 | # Save backup if backup dir is empty, or savefile is differnt from recent backup copy 137 | if not backed_files_list or not self._compare_files(backed_files_list[-1], savefile): 138 | mode = stat.S_IRUSR | stat.S_IWUSR # 0o600 139 | umask = 0o777 ^ mode # Prevents always downgrading umask to 0 140 | umask_original = os.umask(umask) 141 | try: 142 | with open(savefile, 'rb') as f_in, gzip.open(backupfile, 'wb') as f_out: 143 | shutil.copyfileobj(f_in, f_out) 144 | f_out.flush() 145 | except OSError as ioe: 146 | backup_error = ioe.strerror or "Unknown error" 147 | finally: 148 | os.umask(umask_original) 149 | 150 | if backup_error is None: 151 | # remove excess backups 152 | max_backup_files = int(self.shell.prefs['max_backup_files']) 153 | 154 | try: 155 | prefs = Path(universal_prefs_file).read_text() 156 | backups = [line for line in prefs.splitlines() if re.match( 157 | r'^max_backup_files\s*=', line)] 158 | max_backup_files = max(max_backup_files, int(backups[0].split('=')[1].strip())) 159 | except: 160 | self.shell.log.debug(f"No universal prefs file '{universal_prefs_file}'.") 161 | 162 | files_to_unlink = list(reversed(backed_files_list))[max_backup_files - 1:] 163 | for f in files_to_unlink: 164 | with ignored(IOError): 165 | Path(f).unlink() 166 | 167 | self.shell.log.info("Last %d configs saved in %s." 168 | % (max_backup_files, backup_dir)) 169 | else: 170 | self.shell.log.warning(f"Could not create backup file {backupfile}: {backup_error}.") 171 | 172 | def ui_command_saveconfig(self, savefile=default_save_file): 173 | ''' 174 | Saves the current configuration to a file so that it can be restored 175 | on next boot. 176 | ''' 177 | self.assert_root() 178 | 179 | if not savefile: 180 | savefile = default_save_file 181 | 182 | savefile = os.path.expanduser(savefile) 183 | 184 | save_dir = os.path.dirname(savefile) 185 | self._create_dir(save_dir) 186 | self._save_backups(savefile) 187 | 188 | self.rtsroot.save_to_file(savefile) 189 | 190 | self.shell.log.info(f"Configuration saved to {savefile}") 191 | 192 | def ui_command_restoreconfig(self, savefile=default_save_file, clear_existing=False, 193 | target=None, storage_object=None): 194 | ''' 195 | Restores configuration from a file. 196 | ''' 197 | self.assert_root() 198 | 199 | savefile = os.path.expanduser(savefile) 200 | 201 | if not os.path.isfile(savefile): 202 | raise ExecutionError(f"Restore file {savefile} not found") 203 | 204 | target = self.ui_eval_param(target, 'string', None) 205 | storage_object = self.ui_eval_param(storage_object, 'string', None) 206 | errors = self.rtsroot.restore_from_file(savefile, clear_existing, 207 | target, storage_object) 208 | 209 | self.refresh() 210 | 211 | if errors: 212 | raise ExecutionError("Configuration restored, %d recoverable errors:\n%s" % \ 213 | (len(errors), "\n".join(errors))) 214 | 215 | self.shell.log.info(f"Configuration restored from {savefile}") 216 | 217 | def ui_complete_saveconfig(self, parameters, text, current_param): 218 | ''' 219 | Auto-completes the file name 220 | ''' 221 | if current_param != 'savefile': 222 | return [] 223 | completions = complete_path(text, stat.S_ISREG) 224 | if len(completions) == 1 and not completions[0].endswith('/'): 225 | completions = [completions[0] + ' '] 226 | return completions 227 | 228 | ui_complete_restoreconfig = ui_complete_saveconfig 229 | 230 | def ui_command_clearconfig(self, confirm=None): 231 | ''' 232 | Removes entire configuration of backstores and targets 233 | ''' 234 | self.assert_root() 235 | 236 | confirm = self.ui_eval_param(confirm, 'bool', False) 237 | 238 | self.rtsroot.clear_existing(confirm=confirm) 239 | 240 | self.shell.log.info("All configuration cleared") 241 | 242 | self.refresh() 243 | 244 | def ui_command_version(self): 245 | ''' 246 | Displays the targetcli and support libraries versions. 247 | ''' 248 | self.shell.log.info(f"targetcli version {__version__}") 249 | 250 | def ui_command_sessions(self, action="list", sid=None): 251 | ''' 252 | Displays a detailed list of all open sessions. 253 | 254 | PARAMETERS 255 | ========== 256 | 257 | action 258 | ------ 259 | The action is one of: 260 | - `list`` gives a short session list 261 | - `detail` gives a detailed list 262 | 263 | sid 264 | --- 265 | You can specify an "sid" to only list this one, 266 | with or without details. 267 | 268 | SEE ALSO 269 | ======== 270 | status 271 | ''' 272 | 273 | indent_step = 4 274 | base_steps = 0 275 | action_list = ("list", "detail") 276 | 277 | if action not in action_list: 278 | raise ExecutionError(f"action must be one of: {', '.join(action_list)}") 279 | if sid is not None: 280 | try: 281 | int(sid) 282 | except ValueError: 283 | raise ExecutionError(f"sid must be a number, '{sid}' given") 284 | 285 | def indent_print(text, steps): 286 | console = self.shell.con 287 | console.display(console.indent(text, indent_step * steps), 288 | no_lf=True) 289 | 290 | def print_session(session): 291 | acl = session['parent_nodeacl'] 292 | indent_print("alias: %(alias)s\tsid: %(id)i type: %(type)s session-state: %(state)s" % session, 293 | base_steps) 294 | 295 | if action == 'detail': 296 | if self.as_root: 297 | auth = " (authenticated)" if acl.authenticate_target else " (NOT AUTHENTICATED)" 298 | else: 299 | auth = "" 300 | 301 | indent_print(f"name: {acl.node_wwn}{auth}", 302 | base_steps + 1) 303 | 304 | for mlun in acl.mapped_luns: 305 | plugin = mlun.tpg_lun.storage_object.plugin 306 | name = mlun.tpg_lun.storage_object.name 307 | mode = "r" if mlun.write_protect else "rw" 308 | indent_print("mapped-lun: %d backstore: %s/%s mode: %s" % 309 | (mlun.mapped_lun, plugin, name, mode), 310 | base_steps + 1) 311 | 312 | for connection in session['connections']: 313 | indent_print("address: %(address)s (%(transport)s) cid: %(cid)i connection-state: %(cstate)s" 314 | % connection, base_steps + 1) 315 | 316 | if sid: 317 | printed_sessions = [x for x in self.rtsroot.sessions if x['id'] == int(sid)] 318 | else: 319 | printed_sessions = list(self.rtsroot.sessions) 320 | 321 | if len(printed_sessions): 322 | for session in printed_sessions: 323 | print_session(session) 324 | elif sid is None: 325 | indent_print("(no open sessions)", base_steps) 326 | else: 327 | raise ExecutionError("no session found with sid %i" % int(sid)) 328 | -------------------------------------------------------------------------------- /targetcli.8: -------------------------------------------------------------------------------- 1 | .TH targetcli 8 2 | .SH NAME 3 | .B targetcli 4 | \- administration shell for storage targets 5 | .SH DESCRIPTION 6 | .B targetcli 7 | is a shell for viewing, editing, and saving the configuration of the 8 | kernel's target subsystem, also known as LIO. It enables the 9 | administrator to assign local storage resources backed by either 10 | files, volumes, local SCSI devices, or ramdisk, and export them to 11 | remote systems via network fabrics, such as iSCSI or FCoE. 12 | .P 13 | There is a daemon component for targetcli, which will greatly improve 14 | the overall execution time taken by targetcli commands at scale. For 15 | more details about switching to daemonized mode refer to targetclid(8) 16 | man page. 17 | .P 18 | The configuration layout is tree-based, similar to a filesystem, and 19 | is navigated in a similar manner. 20 | .SH USAGE 21 | .B targetcli 22 | .P 23 | .B targetcli [cmd] 24 | .P 25 | Invoke 26 | .B targetcli 27 | as root to enter the configuration shell, or follow with a command to 28 | execute but do not enter the shell. Use 29 | .B ls 30 | to list nodes below the current path. Moving around the tree is 31 | accomplished by the 32 | .B cd 33 | command, or by entering the new location directly. Objects are created 34 | using 35 | .BR create , 36 | removed using 37 | .BR delete . 38 | Use 39 | .B "help " 40 | for additional usage information. Tab-completion is available for 41 | commands and command arguments. 42 | .P 43 | Configuration changes in targetcli are made immediately to the 44 | underlying kernel target configuration. Settings will not be retained 45 | across reboot unless 46 | .B saveconfig 47 | is either explicitly called, or implicitly by exiting the shell with 48 | the global preference 49 | .B auto_save_on_exit 50 | set to 51 | .BR true , 52 | the default. 53 | .P 54 | .SH QUICKSTART 55 | To create an iSCSI target and share a file-backed LUN without any auth checks: 56 | .P 57 | $ sudo targetcli 58 | .br 59 | /> backstores/fileio create test /tmp/test.img 100m 60 | .br 61 | /> iscsi/ create iqn.2006-04.com.example:test-target 62 | .br 63 | /> cd iscsi/iqn.2006-04.com.example:test-target/tpg1/ 64 | .br 65 | tpg1/> luns/ create /backstores/fileio/test 66 | .br 67 | tpg1/> set attribute generate_node_acls=1 68 | .br 69 | tpg1/> exit 70 | .P 71 | Although by default targetcli saves the running configuration upon 72 | exit, a distribution-specific service must be enabled to restore the 73 | saved configuration on reboot. See distribution documentation for 74 | specifics, but for example: 75 | .P 76 | $ sudo systemctl enable target.service 77 | .P 78 | See 79 | .B EXAMPLES 80 | below for more detailed information on commands and using the shell. 81 | .SH BACKSTORES 82 | .B Backstores 83 | are different kinds of local storage resources that the kernel target 84 | uses to "back" the SCSI devices it exports. The mappings to local 85 | storage resources that each backstore creates are called 86 | .BR "storage objects" . 87 | .SS FILEIO 88 | Allows files to be treated as disk images. When storage objects of 89 | this type are created, they can support either write-back or 90 | write-thru operation. Using write-back enables the local filesystem 91 | cache, which will improve performance but increase the risk of data 92 | loss. It is also possible to use fileio with local block device files, 93 | if buffered operation is needed. 94 | .P 95 | Fileio also supports using an existing file, or creating a new 96 | file. New files are sparsely allocated by default. 97 | .SS BLOCK 98 | Allows a local disk block device to be shared. 99 | .SS PSCSI 100 | Allows a local SCSI device of any type to be shared. It is generally 101 | advised to prefer the block backstore if sharing a block SCSI device 102 | is desired. 103 | .SS RAMDISK 104 | Allows kernel memory to be shared as a block SCSI device. Since memory 105 | is volatile, the contents of the ramdisk will be lost if the system 106 | restarts, and this backstore is best used for testing only. 107 | .P 108 | It also supports "nullio" mode, which is not backed by any storage. It 109 | discards all writes, and returns all-zeroes for reads. 110 | .SS USERSPACE-BACKED 111 | Backstores starting with "user:" are not supported in the kernel, but 112 | rely on a userspace process to handle requests. See 113 | .BR tcmu-runner (8) 114 | for more information on creating backstores of this type. 115 | .SH TARGETS 116 | .B Targets 117 | are instances of a 118 | .BR fabric , 119 | which adapts the kernel target to a specific transport protocol such 120 | as iSCSI, Fibre Channel, or SBP-2. Creating a target in targetcli 121 | enables that target to be configured. The name of the target, its WWN 122 | (world wide name), may link the configuration to a specific hardware 123 | endpoint, like SRP for example, or it may not, like iSCSI. 124 | .P 125 | Aside from "backstores", all other top-level configuration nodes in 126 | targetcli are fabrics that may have targets created for them. Fabrics 127 | that require hardware are only listed if the hardware is present and 128 | configured properly. 129 | .SS CREATING A TARGET 130 | Use the 131 | .B create 132 | command within a fabric's node to create a target. If the fabric's 133 | targets are tied to hardware then targetcli will constrain the WWN to 134 | available hardware WWNs. These can be shown via tab-completion. If the 135 | fabric is not tied to hardware, such as iSCSI, then targetcli will 136 | either auto-generate a WWN if none is given, or check that the given 137 | WWN has the correct format. All WWNs are prefaced by their type, such 138 | as "iqn", "naa", or "ib", and may be given with or without colons. 139 | .P 140 | iSCSI supports multiple WWN formats: iqn, naa, and eui. Other fabrics 141 | support single formats only. 142 | .SH CONFIGURING A TARGET 143 | Not all fabrics have the same capabilities. Targets on different 144 | fabrics will have different configuration node layouts. iSCSI has the 145 | most to configure; other fabrics present subsets of iSCSI's feature 146 | set. 147 | .SH CONFIGURING ISCSI 148 | iSCSI has the most options for configuration. 149 | .SS TPGS 150 | TPGs (Target Portal Groups) allow the iSCSI to support multiple 151 | complete configurations within one target. This is useful for complex 152 | quality-of-service configurations. targetcli will automatically create 153 | one TPG when the target is created, and almost all setups only need 154 | one. 155 | .SS PORTALS 156 | An iSCSI target may be reached via multiple IP addresses and 157 | ports. These addr:port pairs are called 158 | .BR portals . 159 | Both IPv4 and IPv6 addresses are supported. 160 | .P 161 | When a target is created, targetcli automatically creates a default 162 | portal listening on all IPv4 and IPv6 addresses (shown as ::0) on port 163 | 3260. If IPV6_V6ONLY is set, the default will only listen on all IPv6 164 | addresses. If a different configuration is needed, the default portal can 165 | be removed and portals configured as desired. 166 | .P 167 | If the hardware supports it, 168 | .B iSER 169 | (iSCSI Extensions for RDMA) may be enabled via the 170 | .B enable_iser 171 | command within each portal's node. Or, if the hardware supports it, 172 | hardware offload may be enabled via the 173 | .B enable_offload 174 | command within each portal's node. 175 | .SS LUNS 176 | The kernel target exports SCSI Logical Units, also called 177 | .BR LUNs . 178 | This section links the previously-defined storage objects with the 179 | target, and defines which number (the Logical Unit Number) the device 180 | will use. Note that if ACLs are being used, a 181 | .B "lun mapping" 182 | must be created under the ACL that refers back to the TPG LUN. 183 | .SS ACLS 184 | ACLs (Access Control Lists) allow different configuration, depending 185 | on the initiator that is connecting to the target. This includes both 186 | per-initiator authentication settings as well as per-initiator LUN 187 | mappings. 188 | .P 189 | .B "create " 190 | in the 191 | .B acls 192 | node creates an ACL for an initiator, and 193 | .B create 194 | within the ACL creates a LUN mapping. (This can either refer to the 195 | TPG LUN, or to the storage object, in which case the TPG LUN will be 196 | configured as well.) Global setting 197 | .B auto_add_mapped_luns 198 | affects this, see below. 199 | .SS AUTHENTICATION 200 | iSCSI supports authentication via the CHAP protocol, which uses a 201 | username and password. The initiator may be required to supply valid 202 | credentials to the target, and the target may also be required to 203 | supply credentials back to the initiator. The latter is referred to as 204 | .BR "mutual authentication" . 205 | .P 206 | Furthermore, authentication credentials may be different for each 207 | session phase (Discovery or Normal), and authentication in a Normal 208 | session may be set at the TPG level, or per-ACL. 209 | .P 210 | .B Discovery Authentication 211 | .br 212 | iSCSI Discovery sessions allow the initiator to connect to a portal 213 | and discover targets with the SendTargets command, but not access 214 | them. The four parameters 215 | .BR userid , 216 | .BR password , 217 | .BR mutual_userid ", and" 218 | .B mutual_password 219 | are configured via 220 | .B "set discovery_auth" 221 | command within the top-level iscsi configuration node. 1-way 222 | authentication is enabled if userid and password are both set, and 223 | mutual authentication is enabled if all four are set. Authentication 224 | is disabled by unsetting the parameters. 225 | .P 226 | .B Normal Authentication 227 | .br 228 | Similarly, the four parameters 229 | .BR userid , 230 | .BR password , 231 | .BR mutual_userid ", and" 232 | .B mutual_password 233 | are configured via 234 | .B "set auth" 235 | command within the TPG node and ACL nodes. However, LIO only uses one 236 | or the other, depending on the TPG's 237 | .B generate_node_acls 238 | attribute setting. If generate_node_acls is 1, the TPG-wide settings 239 | will be used. If generate_node_acls is 0, then the user-created ACLs' 240 | settings will be used. 241 | .P 242 | Enable generate_node_acls with 243 | .B set attribute generate_node_acls=1 244 | within the TPG node. This can be thought of as "ignore ACLs mode" -- 245 | both authentication and LUN mapping will then use the TPG settings. 246 | .P 247 | .B No Authentication 248 | .br 249 | Authentication is disabled by clearing the TPG "authentication" 250 | attribute: 251 | .BR "set attribute authentication=0" . 252 | Although initiator names are trivially forgeable, generate_node_acls 253 | still works here to either ignore user-defined ACLs and allow all, or 254 | check that an ACL exists for the connecting initiator. 255 | .SH CONFIGURING FIBRE CHANNEL (QLA2XXX) 256 | Operation as a target requires that 257 | .B /sys/module/qla2xxx/parameters/qlini_mode 258 | report "disabled". This may require passing the 259 | .B qlini_mode=disabled 260 | parameter to the qla2xxx module when it loads. 261 | .SH CONFIGURING FIBRE CHANNEL OVER ETHERNET (TCM_FC) 262 | Ensure 263 | .B "fcoeadm -i" 264 | shows a working endpoint. 265 | .SH CONFIGURING SRP 266 | SRP (SCSI RDMA Protocol) requires that RDMA-capable hardware is 267 | present. It uses "ib" WWNs. 268 | .SH CONFIGURING LOOPBACK 269 | Storage objects may be re-exported as local SCSI devices with this 270 | fabric. 271 | .SH CONFIGURING OTHER FABRICS 272 | Other fabrics may be present. They are for specialized uses. Use at 273 | your own risk. 274 | .SH EXAMPLES 275 | .SS DEFINING A STORAGE OBJECT WITHIN A BACKSTORE 276 | .B backstores/fileio create disk1 /disks/disk1.img 140M 277 | .br 278 | Creates a storage object named 279 | .I disk1 280 | with the given path and size. 281 | .B targetcli 282 | supports common size abbreviations like 'M', 'G', and 'T'. 283 | .P 284 | .SS EXPORTING A STORAGE OBJECT VIA ISCSI 285 | .B iscsi/ create 286 | .br 287 | Creates an iSCSI target with a default WWN. It will also create an 288 | initial target portal group called 289 | .IR tpg1 . 290 | .P 291 | .B iqn.2003-01.org.linux-iscsi.test2.x8664:sn123456789012/tpg1/ 292 | .br 293 | An example of changing to the configuration node for the given 294 | target's first target portal group (TPG). This is equivalent to giving 295 | the command prefixed by "cd". (Although more can be useful for certain 296 | setups, most configurations have a single TPG per target. In this 297 | case, configuring the TPG is equivalent to configuring the overall 298 | target.) 299 | .P 300 | .B portals/ create 301 | .br 302 | Add a portal, i.e. an IP address and TCP port via which the target can 303 | be contacted by initiators. Only required if the default [::0]:3260 304 | portal is not present. 305 | .P 306 | .B luns/ create /backstores/fileio/disk1 307 | .br 308 | Create a new LUN in the TPG, attached to the storage object that has 309 | previously been defined. The storage object now shows up under the 310 | /backstores configuration node as activated. 311 | .P 312 | .B acls/ create iqn.1994-05.com.redhat:4321576890 313 | .br 314 | Creates an ACL (access control list) entry for the given iSCSI 315 | initiator. 316 | .P 317 | .B acls/iqn.1994-05.com.redhat:4321576890 create 2 0 318 | .br 319 | Gives the initiator access to the first exported LUN (lun0), which the 320 | initiator will see as lun2. The default is to give the initiator 321 | read/write access; if read-only access was desired, an additional "1" 322 | argument would be added to enable write-protect. (Note: if global 323 | setting 324 | .B auto_add_mapped_luns 325 | is true, this step is not necessary.) 326 | .SS EXPORTING A STORAGE OBJECT VIA FCOE 327 | .B tcm_fc/ create 20:00:00:19:99:a8:34:bc 328 | .br 329 | Create an FCoE target with the given WWN. 330 | .B targetcli 331 | can tab-complete the WWN based on registered FCoE interfaces. If none 332 | are found, verify that they are properly configured and are shown in 333 | the output of 334 | .BR "fcoeadm -i" . 335 | .P 336 | .B tcm_fc/20:00:00:19:99:a8:34:bc/ 337 | .br 338 | If 339 | .B auto_cd_after_create 340 | is set to false, change to the configuration node for the given 341 | target, equivalent to giving the command prefixed by 342 | .BR cd . 343 | .P 344 | .B luns/ create /backstores/fileio/disk1 345 | .br 346 | Create a new LUN for the interface, attached to a previously defined 347 | storage object. The storage object now shows up under the /backstores 348 | configuration node as 349 | .BR activated . 350 | .P 351 | .B acls/ create 00:99:88:77:66:55:44:33 352 | .br 353 | Create an ACL (access control list), for defining the resources each 354 | initiator may access. The default behavior is to auto-map existing 355 | LUNs to the ACL; see help for more information. 356 | .P 357 | The LUN should now be accessible via FCoE. 358 | .SH OTHER COMMANDS 359 | .B saveconfig 360 | .br 361 | Save the current configuration settings to a file, from which settings 362 | will be restored if the system is rebooted. By default, this will save 363 | the configuration to 364 | .IR /etc/target/saveconfig.json . 365 | .P 366 | This command is executed from the configuration root node. 367 | .P 368 | .B restoreconfig 369 | .br 370 | Restore target configuration from a file, the default is the file 371 | listed under 372 | .BR saveconfig . 373 | This will fail if there is already an established config, unless the 374 | .I clear_existing 375 | option is set to 376 | .IR true . 377 | .P 378 | This command is executed from the configuration root node. 379 | .P 380 | .B clearconfig 381 | .br 382 | Clears the entire current local configuration. The parameter 383 | .I confirm=true 384 | must also be given, as a precaution. 385 | .P 386 | This command is executed from the configuration root node. 387 | .P 388 | .B sessions [ list | detail ] [sid] 389 | .br 390 | Lists the current open sessions or a specific session, with or without 391 | details. 392 | .P 393 | This command is executed from the configuration root node. 394 | .P 395 | .B exit 396 | .br 397 | Leave the configuration shell. 398 | .SH SETTINGS GROUPS 399 | Settings are broken into groups. Individual settings are accessed by 400 | .B "get " 401 | and 402 | .BR "set =" , 403 | and the settings of an entire group may be displayed by 404 | .BR "get " . 405 | All except for 406 | .I global 407 | are associated with a particular configuration node. 408 | .SS GLOBAL 409 | Shell-related user-specific settings are in 410 | .IR global , 411 | and are visible from all configuration nodes. They are mostly shell 412 | display options, but some starting with 413 | .B auto_ 414 | affect shell behavior and may merit customization. These include 415 | .BR auto_save_on_exit , 416 | which controls if exiting targetcli saves the configuration; 417 | .BR auto_add_mapped_luns , 418 | to automatically add existing LUNs to new ACLs, and new LUNS to 419 | existing ACLs; and 420 | .BR auto_cd_after_create , 421 | to change working path to newly-created nodes. Global settings 422 | are user-specific and are saved to ~/.targetcli/ upon exit, unlike 423 | other groups, which are system-wide and kept in 424 | .BR /etc/target/saveconfig.json . 425 | .SS BACKSTORE-SPECIFIC 426 | .B attribute 427 | .br 428 | /backstore// configuration node. Contains values relating 429 | to the backstore and storage object. 430 | .P 431 | .SS ISCSI-SPECIFIC 432 | .B discovery_auth 433 | .br 434 | /iscsi configuration node. Set the normal and mutual authentication 435 | userid and password for discovery sessions, as well as enabling or 436 | disabling it. By default it is disabled -- no authentication is 437 | required for discovery. 438 | .P 439 | .B parameter 440 | .br 441 | /iscsi//tpgX configuration node. ISCSI-specific parameters 442 | such as 443 | .IR AuthMethod , 444 | .IR MaxBurstLength , 445 | .IR IFMarker , 446 | .IR DataDigest , 447 | and similar. 448 | .P 449 | .B attribute 450 | .br 451 | /iscsi//tpgX configuration node. Contains 452 | implementation-specific settings for the TPG, such as 453 | .BR authentication , 454 | to enforce or disable authentication for the full-feature phase 455 | (i.e. non-discovery). 456 | .P 457 | .B auth 458 | .br 459 | /iscsi//tpgX/acls/ configuration node. Set 460 | the userid and password for full-feature phase for this ACL. 461 | .SH FILES 462 | .B /etc/target/saveconfig.json 463 | .br 464 | .B /etc/target/backup/* 465 | .SH ENVIRONMENT 466 | .SS TARGETCLI_HOME 467 | If set, this variable points to a directory that should be used instead of ~/.targetcli 468 | .SH SEE ALSO 469 | .BR targetclid (8), 470 | .BR targetctl (8), 471 | .BR tcmu-runner (8) 472 | .SH AUTHOR 473 | Written by Jerome Martin . 474 | .br 475 | Man page written by Andy Grover . 476 | .SH REPORTING BUGS 477 | Report bugs via 478 | .br 479 | or 480 | -------------------------------------------------------------------------------- /src/targetcli/ui_backstore.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implements the targetcli backstores related UI. 3 | 4 | This file is part of targetcli. 5 | Copyright (c) 2011-2013 by Datera, Inc 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | 20 | import array 21 | import fcntl 22 | import glob 23 | import os 24 | import re 25 | import stat 26 | import struct 27 | from pathlib import Path 28 | 29 | from configshell import ExecutionError 30 | from gi.repository import Gio 31 | from rtslib import ( 32 | ALUATargetPortGroup, 33 | BlockStorageObject, 34 | FileIOStorageObject, 35 | PSCSIStorageObject, 36 | RDMCPStorageObject, 37 | RTSLibError, 38 | RTSRoot, 39 | UserBackedStorageObject, 40 | ) 41 | from rtslib.utils import get_block_type 42 | 43 | from .ui_node import UINode, UIRTSLibNode 44 | 45 | default_save_file = "/etc/target/saveconfig.json" 46 | 47 | alua_rw_params = ['alua_access_state', 'alua_access_status', 48 | 'alua_write_metadata', 'alua_access_type', 'preferred', 49 | 'nonop_delay_msecs', 'trans_delay_msecs', 50 | 'implicit_trans_secs', 'alua_support_offline', 51 | 'alua_support_standby', 'alua_support_transitioning', 52 | 'alua_support_active_nonoptimized', 53 | 'alua_support_unavailable', 'alua_support_active_optimized'] 54 | alua_ro_params = ['tg_pt_gp_id', 'members', 'alua_support_lba_dependent'] 55 | 56 | alua_state_names = {0: 'Active/optimized', 57 | 1: 'Active/non-optimized', 58 | 2: 'Standby', 59 | 3: 'Unavailable', 60 | 4: 'LBA Dependent', 61 | 14: 'Offline', 62 | 15: 'Transitioning'} 63 | 64 | def human_to_bytes(hsize, kilo=1024): 65 | ''' 66 | This function converts human-readable amounts of bytes to bytes. 67 | It understands the following units : 68 | - B or no unit present for Bytes 69 | - k, K, kB, KB for kB (kilobytes) 70 | - m, M, mB, MB for MB (megabytes) 71 | - g, G, gB, GB for GB (gigabytes) 72 | - t, T, tB, TB for TB (terabytes) 73 | 74 | Note: The definition of kilo defaults to 1kB = 1024Bytes. 75 | Strictly speaking, those should not be called "kB" but "kiB". 76 | You can override that with the optional kilo parameter. 77 | 78 | @param hsize: The human-readable version of the Bytes amount to convert 79 | @type hsize: string or int 80 | @param kilo: Optional base for the kilo prefix 81 | @type kilo: int 82 | @return: An int representing the human-readable string converted to bytes 83 | ''' 84 | size = hsize.replace('i', '').lower() 85 | if not re.match("^[0-9]+[k|m|g|t]?[b]?$", size): 86 | raise RTSLibError(f"Cannot interpret size, wrong format: {hsize}") 87 | 88 | size = size.rstrip('ib') 89 | 90 | units = ['k', 'm', 'g', 't'] 91 | try: 92 | power = units.index(size[-1]) + 1 93 | except ValueError: 94 | power = 0 95 | size = int(size) 96 | else: 97 | size = int(size[:-1]) 98 | 99 | return size * (int(kilo) ** power) 100 | 101 | def bytes_to_human(size): 102 | kilo = 1024.0 103 | 104 | # don't use decimal for bytes 105 | if size < kilo: 106 | return "%d bytes" % size 107 | size /= kilo 108 | 109 | for x in ('KiB', 'MiB', 'GiB', 'TiB', 'PiB'): 110 | if size < kilo: 111 | return f"{size:3.1f}{x}" 112 | size /= kilo 113 | return None 114 | 115 | def complete_path(path, stat_fn): 116 | filtered = [] 117 | for entry in glob.glob(path + '*'): 118 | st = os.stat(entry) 119 | if stat.S_ISDIR(st.st_mode): 120 | filtered.append(entry + '/') 121 | elif stat_fn(st.st_mode): 122 | filtered.append(entry) 123 | 124 | # Put directories at the end 125 | return sorted(filtered, 126 | key=lambda s: '~' + s if s.endswith('/') else s) 127 | 128 | 129 | class UIALUATargetPortGroup(UIRTSLibNode): 130 | ''' 131 | A generic UI for ALUATargetPortGroup objects. 132 | ''' 133 | def __init__(self, alua_tpg, parent): 134 | name = alua_tpg.name 135 | super().__init__(name, alua_tpg, parent) 136 | self.refresh() 137 | 138 | for param in alua_rw_params: 139 | self.define_config_group_param("alua", param, 'string') 140 | 141 | for param in alua_ro_params: 142 | self.define_config_group_param("alua", param, 'string', writable=False) 143 | 144 | def ui_getgroup_alua(self, alua_attr): 145 | return getattr(self.rtsnode, alua_attr) 146 | 147 | def ui_setgroup_alua(self, alua_attr, value): 148 | self.assert_root() 149 | 150 | if value is None: 151 | return 152 | 153 | setattr(self.rtsnode, alua_attr, value) 154 | 155 | def summary(self): 156 | return (f"ALUA state: {alua_state_names[self.rtsnode.alua_access_state]}", True) 157 | 158 | class UIALUATargetPortGroups(UINode): 159 | ''' 160 | ALUA Target Port Group UI 161 | ''' 162 | def __init__(self, parent): 163 | super().__init__("alua", parent) 164 | self.refresh() 165 | 166 | def summary(self): 167 | return (f"ALUA Groups: {len(self.children)}", None) 168 | 169 | def refresh(self): 170 | self._children = set() 171 | 172 | so = self.parent.rtsnode 173 | for tpg in so.alua_tpgs: 174 | UIALUATargetPortGroup(tpg, self) 175 | 176 | def ui_command_create(self, name, tag): 177 | ''' 178 | Create a new ALUA Target Port Group attached to a storage object. 179 | ''' 180 | self.assert_root() 181 | 182 | so = self.parent.rtsnode 183 | alua_tpg_object = ALUATargetPortGroup(so, name, int(tag)) 184 | self.shell.log.info(f"Created ALUA TPG {alua_tpg_object.name}.") 185 | ui_alua_tpg = UIALUATargetPortGroup(alua_tpg_object, self) 186 | return self.new_node(ui_alua_tpg) 187 | 188 | def ui_command_delete(self, name): 189 | ''' 190 | Delete the ALUA Target Por Group and unmap it from a LUN if needed. 191 | ''' 192 | self.assert_root() 193 | 194 | so = self.parent.rtsnode 195 | try: 196 | alua_tpg_object = ALUATargetPortGroup(so, name) 197 | except: 198 | raise RTSLibError("Invalid ALUA group name") 199 | 200 | alua_tpg_object.delete() 201 | self.refresh() 202 | 203 | def ui_complete_delete(self, parameters, text, current_param): 204 | ''' 205 | Parameter auto-completion method for user command delete. 206 | @param parameters: Parameters on the command line. 207 | @type parameters: dict 208 | @param text: Current text of parameter being typed by the user. 209 | @type text: str 210 | @param current_param: Name of parameter to complete. 211 | @type current_param: str 212 | @return: Possible completions 213 | @rtype: list of str 214 | ''' 215 | if current_param == 'name': 216 | so = self.parent.rtsnode 217 | 218 | tpgs = [tpg.name for tpg in so.alua_tpgs] 219 | completions = [tpg for tpg in tpgs if tpg.startswith(text)] 220 | else: 221 | completions = [] 222 | 223 | if len(completions) == 1: 224 | return [completions[0] + ' '] 225 | return completions 226 | 227 | class UIBackstores(UINode): 228 | ''' 229 | The backstores container UI. 230 | ''' 231 | def __init__(self, parent): 232 | UINode.__init__(self, 'backstores', parent) 233 | self.refresh() 234 | 235 | def _user_backstores(self): 236 | ''' 237 | tcmu-runner (or other daemon providing the same service) exposes a 238 | DBus ObjectManager-based iface to find handlers it supports. 239 | ''' 240 | bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) 241 | try: 242 | mgr_iface = Gio.DBusProxy.new_sync(bus, 243 | Gio.DBusProxyFlags.NONE, 244 | None, 245 | 'org.kernel.TCMUService1', 246 | '/org/kernel/TCMUService1', 247 | 'org.freedesktop.DBus.ObjectManager', 248 | None) 249 | 250 | for k, v in mgr_iface.GetManagedObjects().items(): 251 | tcmu_iface = Gio.DBusProxy.new_sync(bus, 252 | Gio.DBusProxyFlags.NONE, 253 | None, 254 | 'org.kernel.TCMUService1', 255 | k, 256 | 'org.kernel.TCMUService1', 257 | None) 258 | yield (k[k.rfind("/") + 1:], tcmu_iface, v) 259 | except Exception: 260 | return 261 | 262 | def refresh(self): 263 | self._children = set() 264 | UIPSCSIBackstore(self) 265 | UIRDMCPBackstore(self) 266 | UIFileIOBackstore(self) 267 | UIBlockBackstore(self) 268 | 269 | for name, iface, prop_dict in self._user_backstores(): 270 | UIUserBackedBackstore(self, name, iface, prop_dict) 271 | 272 | class UIBackstore(UINode): 273 | ''' 274 | A backstore UI. 275 | Abstract Base Class, do not instantiate. 276 | ''' 277 | def __init__(self, plugin, parent): 278 | UINode.__init__(self, plugin, parent) 279 | self.refresh() 280 | 281 | def refresh(self): 282 | self._children = set() 283 | for so in RTSRoot().storage_objects: 284 | if so.plugin == self.name: 285 | self.so_cls(so, self) 286 | 287 | def summary(self): 288 | return (f"Storage Objects: {len(self._children)}", None) 289 | 290 | def ui_command_delete(self, name, save=None): 291 | ''' 292 | Recursively deletes the storage object having the specified name. If 293 | there are LUNs using this storage object, they will be deleted too. 294 | 295 | EXAMPLE 296 | ======= 297 | delete mystorage 298 | ---------------- 299 | Deletes the storage object named mystorage, and all associated LUNs. 300 | ''' 301 | self.assert_root() 302 | try: 303 | child = self.get_child(name) 304 | except ValueError: 305 | raise ExecutionError(f"No storage object named {name}.") 306 | 307 | save = self.ui_eval_param(save, 'bool', False) 308 | if save: 309 | rn = self.get_root() 310 | rn._save_backups(default_save_file) 311 | 312 | child.rtsnode.delete(save=save) 313 | self.remove_child(child) 314 | self.shell.log.info(f"Deleted storage object {name}.") 315 | 316 | def ui_complete_delete(self, parameters, text, current_param): 317 | ''' 318 | Parameter auto-completion method for user command delete. 319 | @param parameters: Parameters on the command line. 320 | @type parameters: dict 321 | @param text: Current text of parameter being typed by the user. 322 | @type text: str 323 | @param current_param: Name of parameter to complete. 324 | @type current_param: str 325 | @return: Possible completions 326 | @rtype: list of str 327 | ''' 328 | if current_param == 'name': 329 | names = [child.name for child in self.children] 330 | completions = [name for name in names 331 | if name.startswith(text)] 332 | else: 333 | completions = [] 334 | 335 | if len(completions) == 1: 336 | return [completions[0] + ' '] 337 | return completions 338 | 339 | def setup_model_alias(self, storageobject): 340 | if self.shell.prefs['export_backstore_name_as_model']: 341 | try: 342 | storageobject.set_attribute("emulate_model_alias", 1) 343 | except RTSLibError: 344 | raise ExecutionError("'export_backstore_name_as_model' is set but" 345 | " emulate_model_alias\n not supported by kernel.") 346 | 347 | 348 | class UIPSCSIBackstore(UIBackstore): 349 | ''' 350 | PSCSI backstore UI. 351 | ''' 352 | def __init__(self, parent): 353 | self.so_cls = UIPSCSIStorageObject 354 | UIBackstore.__init__(self, 'pscsi', parent) 355 | 356 | def ui_command_create(self, name, dev): 357 | ''' 358 | Creates a PSCSI storage object, with supplied name and SCSI device. The 359 | SCSI device "dev" can either be a path name to the device, in which 360 | case it is recommended to use the /dev/disk/by-id hierarchy to have 361 | consistent naming should your physical SCSI system be modified, or an 362 | SCSI device ID in the H:C:T:L format, which is not recommended as SCSI 363 | IDs may vary in time. 364 | ''' 365 | self.assert_root() 366 | 367 | if get_block_type(dev) is not None: 368 | self.shell.log.info("Note: block backstore recommended for " 369 | "SCSI block devices") 370 | 371 | so = PSCSIStorageObject(name, dev) 372 | ui_so = UIPSCSIStorageObject(so, self) 373 | self.shell.log.info(f"Created pscsi storage object {name} using {dev}") 374 | return self.new_node(ui_so) 375 | 376 | 377 | class UIRDMCPBackstore(UIBackstore): 378 | ''' 379 | RDMCP backstore UI. 380 | ''' 381 | def __init__(self, parent): 382 | self.so_cls = UIRamdiskStorageObject 383 | UIBackstore.__init__(self, 'ramdisk', parent) 384 | 385 | def ui_command_create(self, name, size, nullio=None, wwn=None): 386 | ''' 387 | Creates an RDMCP storage object. "size" is the size of the ramdisk. 388 | 389 | SIZE SYNTAX 390 | =========== 391 | - If size is an int, it represents a number of bytes. 392 | - If size is a string, the following units can be used: 393 | - B or no unit present for bytes 394 | - k, K, kB, KB for kB (kilobytes) 395 | - m, M, mB, MB for MB (megabytes) 396 | - g, G, gB, GB for GB (gigabytes) 397 | - t, T, tB, TB for TB (terabytes) 398 | ''' 399 | self.assert_root() 400 | 401 | nullio = self.ui_eval_param(nullio, 'bool', False) 402 | wwn = self.ui_eval_param(wwn, 'string', None) 403 | 404 | so = RDMCPStorageObject(name, human_to_bytes(size), nullio=nullio, wwn=wwn) 405 | ui_so = UIRamdiskStorageObject(so, self) 406 | self.setup_model_alias(so) 407 | self.shell.log.info(f"Created ramdisk {name} with size {size}.") 408 | return self.new_node(ui_so) 409 | 410 | 411 | class UIFileIOBackstore(UIBackstore): 412 | ''' 413 | FileIO backstore UI. 414 | ''' 415 | def __init__(self, parent): 416 | self.so_cls = UIFileioStorageObject 417 | UIBackstore.__init__(self, 'fileio', parent) 418 | 419 | def _create_file(self, filename, size, sparse=True): 420 | try: 421 | f = open(filename, "w+") # noqa: SIM115 422 | except OSError: 423 | raise ExecutionError(f"Could not open {filename}") 424 | try: 425 | if sparse: 426 | os.ftruncate(f.fileno(), size) 427 | else: 428 | self.shell.log.info("Writing %d bytes" % size) 429 | try: 430 | # Prior to version 3.3, Python does not provide fallocate 431 | os.posix_fallocate(f.fileno(), 0, size) 432 | except AttributeError: 433 | while size > 0: 434 | write_size = min(size, 1024) 435 | f.write("\0" * write_size) 436 | size -= write_size 437 | except OSError: 438 | Path(filename).unlink() 439 | raise ExecutionError("Could not expand file to %d bytes" % size) 440 | except OverflowError: 441 | raise ExecutionError("The file size is too large (%d bytes)" % size) 442 | finally: 443 | f.close() 444 | 445 | def ui_command_create(self, name, file_or_dev, size=None, write_back=None, 446 | sparse=None, wwn=None): 447 | ''' 448 | Creates a FileIO storage object. If "file_or_dev" is a path 449 | to a regular file to be used as backend, then the "size" 450 | parameter is mandatory. Else, if "file_or_dev" is a path to a 451 | block device, the size parameter must be omitted. If 452 | present, "size" is the size of the file to be used, "file" 453 | the path to the file or "dev" the path to a block device. The 454 | "write_back" parameter is a boolean controlling write 455 | caching. It is enabled by default. The "sparse" parameter is 456 | only applicable when creating a new backing file. It is a 457 | boolean stating if the created file should be created as a 458 | sparse file (the default), or fully initialized. 459 | 460 | SIZE SYNTAX 461 | =========== 462 | - If size is an int, it represents a number of bytes. 463 | - If size is a string, the following units can be used: 464 | - B or no unit present for bytes 465 | - k, K, kB, KB for kB (kilobytes) 466 | - m, M, mB, MB for MB (megabytes) 467 | - g, G, gB, GB for GB (gigabytes) 468 | - t, T, tB, TB for TB (terabytes) 469 | ''' 470 | self.assert_root() 471 | 472 | sparse = self.ui_eval_param(sparse, 'bool', True) 473 | write_back = self.ui_eval_param(write_back, 'bool', True) 474 | wwn = self.ui_eval_param(wwn, 'string', None) 475 | 476 | self.shell.log.debug(f"Using params size={size} write_back={write_back} sparse={sparse}") 477 | 478 | file_or_dev = os.path.expanduser(file_or_dev) 479 | # can't use is_dev_in_use() on files so just check against other 480 | # storage object paths 481 | file_or_dev_path = Path(file_or_dev) 482 | if file_or_dev_path.exists(): 483 | for so in RTSRoot().storage_objects: 484 | if so.udev_path and file_or_dev_path.samefile(so.udev_path): 485 | raise ExecutionError(f"storage object for {file_or_dev} already exists: {so.name}") 486 | 487 | if get_block_type(file_or_dev) is not None: 488 | if size: 489 | self.shell.log.info("Block device, size parameter ignored") 490 | size = None 491 | self.shell.log.info("Note: block backstore preferred for best results") 492 | elif Path(file_or_dev).is_file(): 493 | new_size = os.path.getsize(file_or_dev) 494 | if size: 495 | self.shell.log.info(f"{file_or_dev} exists, using its size ({new_size} bytes) instead") 496 | size = new_size 497 | elif Path(file_or_dev).exists(): 498 | raise ExecutionError(f"Path {file_or_dev} exists but is not a file") 499 | else: 500 | # create file and extend to given file size 501 | if not size: 502 | raise ExecutionError("Attempting to create file for new fileio backstore, need a size") 503 | size = human_to_bytes(size) 504 | self._create_file(file_or_dev, size, sparse) 505 | 506 | so = FileIOStorageObject(name, file_or_dev, size, 507 | write_back=write_back, wwn=wwn) 508 | ui_so = UIFileioStorageObject(so, self) 509 | self.setup_model_alias(so) 510 | self.shell.log.info(f"Created fileio {name} with size {so.size}") 511 | return self.new_node(ui_so) 512 | 513 | def ui_complete_create(self, parameters, text, current_param): 514 | ''' 515 | Auto-completes the file name 516 | ''' 517 | if current_param != 'file_or_dev': 518 | return [] 519 | completions = complete_path( 520 | text, lambda x: stat.S_ISREG(x) or stat.S_ISBLK(x), 521 | ) if text else [] 522 | if len(completions) == 1 and not completions[0].endswith('/'): 523 | completions = [completions[0] + ' '] 524 | return completions 525 | 526 | 527 | class UIBlockBackstore(UIBackstore): 528 | ''' 529 | Block backstore UI. 530 | ''' 531 | def __init__(self, parent): 532 | self.so_cls = UIBlockStorageObject 533 | UIBackstore.__init__(self, 'block', parent) 534 | 535 | def _ui_block_ro_check(self, dev): 536 | BLKROGET = 0x0000125E # noqa: N806 537 | try: 538 | f = os.open(dev, os.O_RDONLY) 539 | except OSError: 540 | raise ExecutionError(f"Could not open {dev}") 541 | # ioctl returns an int. Provision a buffer for it 542 | buf = array.array('b', [0] * 4) 543 | try: 544 | fcntl.ioctl(f, BLKROGET, buf) 545 | except OSError: 546 | os.close(f) 547 | return False 548 | 549 | os.close(f) 550 | return struct.unpack('I', buf)[0] != 0 551 | 552 | def ui_command_create(self, name, dev, readonly=None, wwn=None, 553 | exclusive=None): 554 | ''' 555 | Creates an Block Storage object. "dev" is the path to the TYPE_DISK 556 | block device to use. 557 | ''' 558 | self.assert_root() 559 | 560 | ro_string = self.ui_eval_param(readonly, 'string', None) 561 | readonly = self._ui_block_ro_check(dev) if ro_string is None else self.ui_eval_param(readonly, "bool", False) 562 | 563 | wwn = self.ui_eval_param(wwn, 'string', None) 564 | 565 | excl_string = self.ui_eval_param(exclusive, 'string', None) 566 | exclusive = True if excl_string is None else self.ui_eval_param(exclusive, "bool", True) 567 | 568 | so = BlockStorageObject(name, dev, readonly=readonly, wwn=wwn, 569 | exclusive=exclusive) 570 | ui_so = UIBlockStorageObject(so, self) 571 | self.setup_model_alias(so) 572 | self.shell.log.info(f"Created block storage object {name} using {dev}.") 573 | return self.new_node(ui_so) 574 | 575 | def ui_complete_create(self, parameters, text, current_param): 576 | ''' 577 | Auto-completes the device name 578 | ''' 579 | if current_param != 'dev': 580 | return [] 581 | completions = complete_path(text, stat.S_ISBLK) if text else [] 582 | if len(completions) == 1 and not completions[0].endswith('/'): 583 | completions = [completions[0] + ' '] 584 | return completions 585 | 586 | 587 | class UIUserBackedBackstore(UIBackstore): 588 | ''' 589 | User backstore UI. 590 | ''' 591 | def __init__(self, parent, name, iface, prop_dict): 592 | self.so_cls = UIUserBackedStorageObject 593 | self.handler = name 594 | self.iface = iface 595 | self.prop_dict = prop_dict 596 | super().__init__("user:" + name, parent) 597 | 598 | def refresh(self): 599 | self._children = set() 600 | for so in RTSRoot().storage_objects: 601 | if so.plugin == 'user' and so.config: 602 | idx = so.config.find("/") 603 | handler = so.config[:idx] 604 | if handler == self.handler: 605 | self.so_cls(so, self) 606 | 607 | def ui_command_help(self, topic=None): 608 | super().ui_command_help(topic) 609 | if topic == "create": 610 | print("CFGSTRING FORMAT") 611 | print("=================") 612 | x = self.prop_dict.get("org.kernel.TCMUService1", {}) 613 | print(x.get("ConfigDesc", "No description.")) 614 | print() 615 | 616 | def ui_command_create(self, name, size, cfgstring, wwn=None, 617 | hw_max_sectors=None, control=None): 618 | ''' 619 | Creates a User-backed storage object. 620 | 621 | SIZE SYNTAX 622 | =========== 623 | - If size is an int, it represents a number of bytes. 624 | - If size is a string, the following units can be used: 625 | - B or no unit present for bytes 626 | - k, K, kB, KB for kB (kilobytes) 627 | - m, M, mB, MB for MB (megabytes) 628 | - g, G, gB, GB for GB (gigabytes) 629 | - t, T, tB, TB for TB (terabytes) 630 | ''' 631 | 632 | size = human_to_bytes(size) 633 | wwn = self.ui_eval_param(wwn, 'string', None) 634 | 635 | config = self.handler + "/" + cfgstring 636 | 637 | ok, errmsg = self.iface.CheckConfig('(s)', config) 638 | if not ok: 639 | raise ExecutionError(f"cfgstring invalid: {errmsg}") 640 | 641 | try: 642 | so = UserBackedStorageObject(name, size=size, config=config, 643 | wwn=wwn, hw_max_sectors=hw_max_sectors, 644 | control=control) 645 | except: 646 | raise ExecutionError("UserBackedStorageObject creation failed.") 647 | 648 | ui_so = UIUserBackedStorageObject(so, self) 649 | self.shell.log.info("Created user-backed storage object %s size %d." 650 | % (name, size)) 651 | return self.new_node(ui_so) 652 | 653 | def ui_command_changemedium(self, name, size, cfgstring): 654 | size = human_to_bytes(size) 655 | config = self.handler + "/" + cfgstring 656 | 657 | try: 658 | rc, errmsg = self.iface.ChangeMedium('(sts)', name, size, config) 659 | except Exception as e: 660 | raise ExecutionError(f"ChangeMedium failed: {e}") 661 | else: 662 | if rc == 0: 663 | self.shell.log.info("Medium Changed.") 664 | else: 665 | raise ExecutionError(f"ChangeMedium failed: {errmsg}") 666 | 667 | class UIStorageObject(UIRTSLibNode): 668 | ''' 669 | A storage object UI. 670 | Abstract Base Class, do not instantiate. 671 | ''' 672 | ui_desc_attributes = { 673 | 'block_size': ('number', 'Block size of the underlying device.'), 674 | 'emulate_3pc': ('number', 'If set to 1, enable Third Party Copy.'), 675 | 'emulate_caw': ('number', 'If set to 1, enable Compare and Write.'), 676 | 'emulate_dpo': ('number', 'If set to 1, turn on Disable Page Out.'), 677 | 'emulate_fua_read': ('number', 'If set to 1, enable Force Unit Access read.'), 678 | 'emulate_fua_write': ('number', 'If set to 1, enable Force Unit Access write.'), 679 | 'emulate_model_alias': ('number', 'If set to 1, use the backend device name for the model alias.'), 680 | 'emulate_rest_reord': ('number', 'If set to 0, the Queue Algorithm Modifier is Restricted Reordering.'), 681 | 'emulate_tas': ('number', 'If set to 1, enable Task Aborted Status.'), 682 | 'emulate_tpu': ('number', 'If set to 1, enable Thin Provisioning Unmap.'), 683 | 'emulate_tpws': ('number', 'If set to 1, enable Thin Provisioning Write Same.'), 684 | 'emulate_ua_intlck_ctrl': ('number', 'If set to 1, enable Unit Attention Interlock.'), 685 | 'emulate_write_cache': ('number', 'If set to 1, turn on Write Cache Enable.'), 686 | 'emulate_pr': ('number', 'If set to 1, enable SCSI Reservations.'), 687 | 'enforce_pr_isids': ('number', 'If set to 1, enforce persistent reservation ISIDs.'), 688 | 'force_pr_aptpl': ('number', 689 | 'If set to 1, force SPC-3 PR Activate Persistence across Target Power Loss operation.'), 690 | 'fabric_max_sectors': ('number', 'Maximum number of sectors the fabric can transfer at once.'), 691 | 'hw_block_size': ('number', 'Hardware block size in bytes.'), 692 | 'hw_max_sectors': ('number', 'Maximum number of sectors the hardware can transfer at once.'), 693 | 'control': ('string', 694 | 'Comma separated string of control=value tuples that will be passed to kernel control file.'), 695 | 'hw_pi_prot_type': ('number', 'If non-zero, DIF protection is enabled on the underlying hardware.'), 696 | 'hw_queue_depth': ('number', 'Hardware queue depth.'), 697 | 'is_nonrot': ('number', 'If set to 1, the backstore is a non rotational device.'), 698 | 'max_unmap_block_desc_count': ('number', 'Maximum number of block descriptors for UNMAP.'), 699 | 'max_unmap_lba_count': ('number', 'Maximum number of LBA for UNMAP.'), 700 | 'max_write_same_len': ('number', 'Maximum length for WRITE_SAME.'), 701 | 'optimal_sectors': ('number', 'Optimal request size in sectors.'), 702 | 'pi_prot_format': ('number', 'DIF protection format.'), 703 | 'pi_prot_type': ('number', 'DIF protection type.'), 704 | 'queue_depth': ('number', 'Queue depth.'), 705 | 'unmap_granularity': ('number', 'UNMAP granularity.'), 706 | 'unmap_granularity_alignment': ('number', 'UNMAP granularity alignment.'), 707 | 'unmap_zeroes_data': ('number', 'If set to 1, zeroes are read back after an UNMAP.'), 708 | } 709 | 710 | def __init__(self, storage_object, parent): 711 | name = storage_object.name 712 | UIRTSLibNode.__init__(self, name, storage_object, parent) 713 | self.refresh() 714 | 715 | UIALUATargetPortGroups(self) 716 | 717 | def ui_command_version(self): 718 | ''' 719 | Displays the version of the current backstore's plugin. 720 | ''' 721 | self.shell.con.display(f"Backstore plugin {self.rtsnode.plugin} {self.rtsnode.version}") 722 | 723 | def ui_command_saveconfig(self, savefile=None): 724 | ''' 725 | Save configuration of this StorageObject. 726 | ''' 727 | so = self.rtsnode 728 | rn = self.get_root() 729 | 730 | if not savefile: 731 | savefile = default_save_file 732 | 733 | savefile = os.path.expanduser(savefile) 734 | 735 | rn._save_backups(savefile) 736 | 737 | rn.rtsroot.save_to_file(savefile, 738 | '/backstores/' + so.plugin + '/' + so.name) 739 | 740 | self.shell.log.info(f"Storage Object '{so.plugin}:{so.name}' config saved to {savefile}.") 741 | 742 | 743 | class UIPSCSIStorageObject(UIStorageObject): 744 | def summary(self): 745 | so = self.rtsnode 746 | return (f"{so.udev_path} {so.status}", True) 747 | 748 | 749 | class UIRamdiskStorageObject(UIStorageObject): 750 | def summary(self): 751 | so = self.rtsnode 752 | 753 | nullio_str = "" 754 | if so.nullio: 755 | nullio_str = "nullio " 756 | 757 | return (f"{nullio_str}({bytes_to_human(so.size)}) {so.status}", True) 758 | 759 | 760 | class UIFileioStorageObject(UIStorageObject): 761 | def summary(self): 762 | so = self.rtsnode 763 | 764 | wb_str = "write-back" if so.write_back else "write-thru" 765 | 766 | return (f"{so.udev_path} ({bytes_to_human(so.size)}) {wb_str} {so.status}", True) 767 | 768 | 769 | class UIBlockStorageObject(UIStorageObject): 770 | def summary(self): 771 | so = self.rtsnode 772 | 773 | wb_str = "write-back" if so.write_back else "write-thru" 774 | 775 | ro_str = "" 776 | if so.readonly: 777 | ro_str = "ro " 778 | 779 | return f"{so.udev_path} ({bytes_to_human(so.size)}) {ro_str}{wb_str} {so.status}", True 780 | 781 | 782 | class UIUserBackedStorageObject(UIStorageObject): 783 | def summary(self): 784 | so = self.rtsnode 785 | 786 | if not so.config: 787 | config_str = "(no config)" 788 | else: 789 | idx = so.config.find("/") 790 | config_str = so.config[idx + 1:] 791 | 792 | return (f"{config_str} ({bytes_to_human(so.size)}) {so.status}", True) 793 | -------------------------------------------------------------------------------- /src/targetcli/ui_target.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implements the targetcli target related UI. 3 | 4 | This file is part of targetcli. 5 | Copyright (c) 2011-2013 by Datera, Inc 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | from pathlib import Path 20 | 21 | try: 22 | import ethtool 23 | except ImportError: 24 | ethtool = None 25 | import stat 26 | 27 | from configshell import ExecutionError 28 | from rtslib import ( 29 | LUN, 30 | TPG, 31 | MappedLUN, 32 | NetworkPortal, 33 | NodeACL, 34 | RTSLibBrokenLink, 35 | RTSLibError, 36 | StorageObjectFactory, 37 | Target, 38 | ) 39 | 40 | from .ui_backstore import complete_path 41 | from .ui_node import UINode, UIRTSLibNode 42 | 43 | auth_params = ('userid', 'password', 'mutual_userid', 'mutual_password') 44 | int_params = ('enable',) 45 | discovery_params = auth_params + int_params 46 | default_portal_listen = "::0" 47 | 48 | class UIFabricModule(UIRTSLibNode): 49 | ''' 50 | A fabric module UI. 51 | ''' 52 | def __init__(self, fabric_module, parent): 53 | super().__init__(fabric_module.name, 54 | fabric_module, parent, 55 | late_params=True) 56 | self.refresh() 57 | if self.rtsnode.has_feature('discovery_auth'): 58 | for param in discovery_params: 59 | if param in int_params: 60 | self.define_config_group_param('discovery_auth', 61 | param, 'number') 62 | else: 63 | self.define_config_group_param('discovery_auth', 64 | param, 'string') 65 | self.refresh() 66 | 67 | # Support late params 68 | # 69 | # By default the base class will call list_parameters and list_attributes 70 | # in init. This stops us from being able to lazy-load fabric modules. 71 | # We declare we support "late_params" to stop this, and then 72 | # this code overrides the base class methods that involve enumerating 73 | # this stuff, so we don't need to call list_parameters/attrs (which 74 | # would cause the module to load) until the ui is actually asking for 75 | # them from us. 76 | # Currently fabricmodules don't have these anyways, this is all a CYA thing. 77 | def list_config_groups(self): 78 | groups = super().list_config_groups() 79 | if len(self.rtsnode.list_parameters()): 80 | groups.append('parameter') 81 | if len(self.rtsnode.list_attributes()): 82 | groups.append('attribute') 83 | return groups 84 | 85 | # Support late params (see above) 86 | def list_group_params(self, group, writable=None): 87 | if group not in {"parameter", "attribute"}: 88 | return super().list_group_params(group, 89 | writable) 90 | 91 | params_func = getattr(self.rtsnode, f"list_{group}s") 92 | params = params_func() 93 | params_ro = params_func(writable=False) 94 | 95 | ret_list = [] 96 | for param in params: 97 | p_writable = param not in params_ro 98 | if writable is not None and p_writable != writable: 99 | continue 100 | ret_list.append(param) 101 | 102 | ret_list.sort() 103 | return ret_list 104 | 105 | # Support late params (see above) 106 | def get_group_param(self, group, param): 107 | if group not in {"parameter", "attribute"}: 108 | return super().get_group_param(group, param) 109 | 110 | if param not in self.list_group_params(group): 111 | raise ValueError(f"Not such parameter {param} in configuration group {group}") 112 | 113 | description = f"The {param} {group}." 114 | writable = param in self.list_group_params(group, writable=True) 115 | 116 | return {'name': param, 'group': group, 'type': "string", 117 | 'description': description, 'writable': writable} 118 | 119 | def ui_getgroup_discovery_auth(self, auth_attr): 120 | ''' 121 | This is the backend method for getting discovery_auth attributes. 122 | @param auth_attr: The auth attribute to get the value of. 123 | @type auth_attr: str 124 | @return: The auth attribute's value 125 | @rtype: str 126 | ''' 127 | if auth_attr == 'enable': 128 | return self.rtsnode.discovery_enable_auth 129 | return getattr(self.rtsnode, "discovery_" + auth_attr) 130 | 131 | def ui_setgroup_discovery_auth(self, auth_attr, value): 132 | ''' 133 | This is the backend method for setting discovery auth attributes. 134 | @param auth_attr: The auth attribute to set the value of. 135 | @type auth_attr: str 136 | @param value: The auth's value 137 | @type value: str 138 | ''' 139 | self.assert_root() 140 | 141 | if value is None: 142 | value = '' 143 | 144 | if auth_attr == 'enable': 145 | self.rtsnode.discovery_enable_auth = value 146 | else: 147 | setattr(self.rtsnode, "discovery_" + auth_attr, value) 148 | 149 | def refresh(self): 150 | self._children = set() 151 | for target in self.rtsnode.targets: 152 | self.shell.log.debug(f"Found target {target.wwn} under fabric module {target.fabric_module}.") 153 | if target.has_feature('tpgts'): 154 | UIMultiTPGTarget(target, self) 155 | else: 156 | UITarget(target, self) 157 | 158 | def summary(self): 159 | status = None 160 | msg = [] 161 | 162 | fm = self.rtsnode 163 | if fm.has_feature('discovery_auth') and fm.discovery_enable_auth: 164 | status = bool(fm.discovery_password and fm.discovery_userid) 165 | 166 | if fm.discovery_authenticate_target: 167 | msg.append("mutual disc auth") 168 | else: 169 | msg.append("1-way disc auth") 170 | 171 | msg.append(f"Targets: {len(self._children)}") 172 | 173 | return (", ".join(msg), status) 174 | 175 | def ui_command_create(self, wwn=None): 176 | ''' 177 | Creates a new target. The "wwn" format depends on the transport(s) 178 | supported by the fabric module. If "wwn" is omitted, then a 179 | target will be created using either a randomly generated WWN of the 180 | proper type, or the first unused WWN in the list of possible WWNs if 181 | one is available. If WWNs are constrained to a list (i.e. for hardware 182 | targets addresses) and all WWNs are in use, the target creation will 183 | fail. Use the `info` command to get more information abour WWN type 184 | and possible values. 185 | 186 | SEE ALSO 187 | ======== 188 | info 189 | ''' 190 | self.assert_root() 191 | 192 | target = Target(self.rtsnode, wwn, mode='create') 193 | wwn = target.wwn 194 | if self.rtsnode.wwns is not None and wwn not in self.rtsnode.wwns: 195 | self.shell.log.warning("Hardware missing for this WWN") 196 | 197 | if target.has_feature('tpgts'): 198 | ui_target = UIMultiTPGTarget(target, self) 199 | self.shell.log.info(f"Created target {wwn}.") 200 | return ui_target.ui_command_create() 201 | 202 | ui_target = UITarget(target, self) 203 | self.shell.log.info(f"Created target {wwn}.") 204 | return self.new_node(ui_target) 205 | 206 | def ui_complete_create(self, parameters, text, current_param): 207 | ''' 208 | Parameter auto-completion method for user command create. 209 | @param parameters: Parameters on the command line. 210 | @type parameters: dict 211 | @param text: Current text of parameter being typed by the user. 212 | @type text: str 213 | @param current_param: Name of parameter to complete. 214 | @type current_param: str 215 | @return: Possible completions 216 | @rtype: list of str 217 | ''' 218 | if current_param == 'wwn' and self.rtsnode.wwns is not None: 219 | existing_wwns = [child.wwn for child in self.rtsnode.targets] 220 | completions = [wwn for wwn in self.rtsnode.wwns 221 | if wwn.startswith(text) 222 | if wwn not in existing_wwns] 223 | else: 224 | completions = [] 225 | 226 | if len(completions) == 1: 227 | return [completions[0] + ' '] 228 | return completions 229 | 230 | def ui_command_delete(self, wwn): 231 | ''' 232 | Recursively deletes the target with the specified wwn, and all 233 | objects hanging under it. 234 | 235 | SEE ALSO 236 | ======== 237 | create 238 | ''' 239 | self.assert_root() 240 | target = Target(self.rtsnode, wwn, mode='lookup') 241 | target.delete() 242 | self.shell.log.info(f"Deleted Target {wwn}.") 243 | self.refresh() 244 | 245 | def ui_complete_delete(self, parameters, text, current_param): 246 | ''' 247 | Parameter auto-completion method for user command delete. 248 | @param parameters: Parameters on the command line. 249 | @type parameters: dict 250 | @param text: Current text of parameter being typed by the user. 251 | @type text: str 252 | @param current_param: Name of parameter to complete. 253 | @type current_param: str 254 | @return: Possible completions 255 | @rtype: list of str 256 | ''' 257 | if current_param == 'wwn': 258 | wwns = [child.name for child in self.children] 259 | completions = [wwn for wwn in wwns if wwn.startswith(text)] 260 | else: 261 | completions = [] 262 | 263 | if len(completions) == 1: 264 | return [completions[0] + ' '] 265 | return completions 266 | 267 | def ui_command_info(self): 268 | ''' 269 | Displays information about the fabric module, notably the supported 270 | transports(s) and accepted wwn format(s), along with supported 271 | features. 272 | ''' 273 | fabric = self.rtsnode 274 | self.shell.log.info(f"Fabric module name: {self.name}") 275 | self.shell.log.info(f"ConfigFS path: {self.rtsnode.path}") 276 | self.shell.log.info(f"Allowed WWN types: {', '.join(fabric.wwn_types)}") 277 | if fabric.wwns is not None: 278 | self.shell.log.info(f"Allowed WWNs list: {', '.join(fabric.wwns)}") 279 | self.shell.log.info(f"Fabric module features: {', '.join(fabric.features)}") 280 | self.shell.log.info(f"Corresponding kernel module: {fabric.kernel_module}") 281 | 282 | def ui_command_version(self): 283 | ''' 284 | Displays the target fabric module version. 285 | ''' 286 | version = f"Target fabric module {self.rtsnode.name}: {self.rtsnode.version}" 287 | self.shell.con.display(version.strip()) 288 | 289 | 290 | class UIMultiTPGTarget(UIRTSLibNode): 291 | ''' 292 | A generic target UI that has multiple TPGs. 293 | ''' 294 | def __init__(self, target, parent): 295 | super().__init__(target.wwn, target, parent) 296 | self.refresh() 297 | 298 | def refresh(self): 299 | self._children = set() 300 | for tpg in self.rtsnode.tpgs: 301 | UITPG(tpg, self) 302 | 303 | def summary(self): 304 | try: 305 | self.rtsnode.fabric_module.to_normalized_wwn(self.rtsnode.wwn) 306 | except: 307 | return ("INVALID WWN", False) 308 | 309 | return (f"TPGs: {len(self._children)}", None) 310 | 311 | def ui_command_create(self, tag=None): 312 | ''' 313 | Creates a new Target Portal Group within the target. The 314 | tag must be a positive integer value, optionally prefaced 315 | by 'tpg'. If omitted, the next available Target Portal Group 316 | Tag (TPGT) will be used. 317 | 318 | SEE ALSO 319 | ======== 320 | delete 321 | ''' 322 | self.assert_root() 323 | 324 | if tag: 325 | if tag.startswith("tpg"): 326 | tag = tag.removeprefix("tpg") 327 | 328 | try: 329 | tag = int(tag) 330 | except ValueError: 331 | raise ExecutionError("Tag argument must be a number.") 332 | 333 | tpg = TPG(self.rtsnode, tag, mode='create') 334 | if self.shell.prefs['auto_enable_tpgt']: 335 | tpg.enable = True 336 | 337 | if tpg.has_feature("auth"): 338 | tpg.set_attribute("authentication", 0) 339 | 340 | self.shell.log.info(f"Created TPG {tpg.tag}.") 341 | 342 | if tpg.has_feature("nps") and self.shell.prefs['auto_add_default_portal']: 343 | try: 344 | NetworkPortal(tpg, f"[{default_portal_listen}]") 345 | self.shell.log.info("Global pref auto_add_default_portal=true") 346 | self.shell.log.info("Created default portal listening on all IPs" 347 | f" ({default_portal_listen}), port 3260.") 348 | except RTSLibError: 349 | self.shell.log.info("Default portal not created, TPGs within a target cannot share ip:port.") 350 | 351 | ui_tpg = UITPG(tpg, self) 352 | return self.new_node(ui_tpg) 353 | 354 | def ui_command_delete(self, tag): 355 | ''' 356 | Deletes the Target Portal Group with TPGT "tag" from the target. The 357 | tag must be a positive integer matching an existing TPGT. 358 | 359 | SEE ALSO 360 | ======== 361 | create 362 | ''' 363 | self.assert_root() 364 | if tag.startswith("tpg"): 365 | tag = tag.removeprefix("tpg") 366 | try: 367 | tag = int(tag) 368 | except ValueError: 369 | raise ExecutionError("Tag argument must be a number.") 370 | 371 | tpg = TPG(self.rtsnode, tag, mode='lookup') 372 | tpg.delete() 373 | self.shell.log.info(f"Deleted TPGT {tag}.") 374 | self.refresh() 375 | 376 | def ui_complete_delete(self, parameters, text, current_param): 377 | ''' 378 | Parameter auto-completion method for user command delete. 379 | @param parameters: Parameters on the command line. 380 | @type parameters: dict 381 | @param text: Current text of parameter being typed by the user. 382 | @type text: str 383 | @param current_param: Name of parameter to complete. 384 | @type current_param: str 385 | @return: Possible completions 386 | @rtype: list of str 387 | ''' 388 | if current_param == 'tag': 389 | tags = [child.name[3:] for child in self.children] 390 | completions = [tag for tag in tags if tag.startswith(text)] 391 | else: 392 | completions = [] 393 | 394 | if len(completions) == 1: 395 | return [completions[0] + ' '] 396 | return completions 397 | 398 | 399 | class UITPG(UIRTSLibNode): 400 | ui_desc_attributes = { 401 | 'authentication': ('number', 'If set to 1, enforce authentication for this TPG.'), 402 | 'cache_dynamic_acls': ('number', 'If set to 1 in demo mode, cache dynamically generated ACLs.'), 403 | 'default_cmdsn_depth': ('number', 'Default CmdSN (Command Sequence Number) depth.'), 404 | 'default_erl': ('number', 'Default Error Recovery Level.'), 405 | 'demo_mode_discovery': ('number', 'If set to 1 in demo mode, enable discovery.'), 406 | 'demo_mode_write_protect': ('number', 'If set to 1 in demo mode, prevent writes to LUNs.'), 407 | 'fabric_prot_type': ('number', 'Fabric DIF protection type.'), 408 | 'generate_node_acls': ('number', 'If set to 1, allow all initiators to login (i.e. demo mode).'), 409 | 'login_timeout': ('number', 'Login timeout value in seconds.'), 410 | 'netif_timeout': ('number', 'NIC failure timeout in seconds.'), 411 | 'prod_mode_write_protect': ('number', 'If set to 1, prevent writes to LUNs.'), 412 | 't10_pi': ('number', 'If set to 1, enable T10 Protection Information.'), 413 | 'tpg_enabled_sendtargets': ('number', 'If set to 1, the SendTargets discovery response advertises the TPG only if the TPG is enabled.'), 414 | } 415 | 416 | ui_desc_parameters = { 417 | 'AuthMethod': ('string', 'Authentication method used by the TPG.'), 418 | 'DataDigest': ('string', 'If set to CRC32C, the integrity of the PDU data part is verified.'), 419 | 'DataPDUInOrder': ('yesno', 'If set to Yes, the data PDUs within sequences must be in order.'), 420 | 'DataSequenceInOrder': ('yesno', 'If set to Yes, the data sequences must be in order.'), 421 | 'DefaultTime2Retain': ('number', 'Maximum time, in seconds, after an initial wait, before which an active task reassignment is still possible after an unexpected connection termination or a connection reset.'), 422 | 'DefaultTime2Wait': ('number', 'Minimum time, in seconds, to wait before attempting an explicit/implicit logout or an active task reassignment after an unexpected connection termination or a connection reset.'), 423 | 'ErrorRecoveryLevel': ('number', 'Recovery levels represent a combination of recovery capabilities.'), 424 | 'FirstBurstLength': ('number', 'Maximum amount in bytes of unsolicited data an initiator may send.'), 425 | 'HeaderDigest': ('string', 'If set to CRC32C, the integrity of the PDU header part is verified.'), 426 | 'IFMarker': ('yesno', 'Deprecated according to RFC 7143.'), 427 | 'IFMarkInt': ('string', 'Deprecated according to RFC 7143.'), 428 | 'ImmediateData': ('string', 'Immediate data support.'), 429 | 'InitialR2T': ('yesno', 'If set to No, the default use of R2T (Ready To Transfer) is disabled.'), 430 | 'MaxBurstLength': ('number', 'Maximum SCSI data payload in bytes in a Data-In or a solicited Data-Out iSCSI sequence.'), 431 | 'MaxConnections': ('number', 'Maximum number of connections acceptable.'), 432 | 'MaxOutstandingR2T': ('number', 'Maximum number of outstanding R2Ts per task.'), 433 | 'MaxRecvDataSegmentLength': ('number', 'Maximum data segment length in bytes the target can receive in an iSCSI PDU.'), 434 | 'MaxXmitDataSegmentLength': ('number', 'Outgoing MaxRecvDataSegmentLength sent over the wire during iSCSI login response.'), 435 | 'OFMarker': ('yesno', 'Deprecated according to RFC 7143.'), 436 | 'OFMarkInt': ('string', 'Deprecated according to RFC 7143.'), 437 | 'TargetAlias': ('string', 'Human-readable target name or description.'), 438 | } 439 | 440 | ''' 441 | A generic TPG UI. 442 | ''' 443 | def __init__(self, tpg, parent): 444 | name = "tpg%d" % tpg.tag 445 | super().__init__(name, tpg, parent) 446 | self.refresh() 447 | 448 | UILUNs(tpg, self) 449 | 450 | if tpg.has_feature('acls'): 451 | UINodeACLs(self.rtsnode, self) 452 | if tpg.has_feature('nps'): 453 | UIPortals(self.rtsnode, self) 454 | 455 | if self.rtsnode.has_feature('auth') and Path(self.rtsnode.path + "/auth").exists: 456 | for param in auth_params: 457 | self.define_config_group_param('auth', param, 'string') 458 | 459 | def summary(self): 460 | tpg = self.rtsnode 461 | status = None 462 | 463 | msg = [] 464 | if tpg.has_feature('nexus'): 465 | msg.append(str(self.rtsnode.nexus)) 466 | 467 | if not tpg.enable: 468 | return ("disabled", False) 469 | 470 | if tpg.has_feature("acls"): 471 | if "generate_node_acls" in tpg.list_attributes() and \ 472 | int(tpg.get_attribute("generate_node_acls")): 473 | msg.append("gen-acls") 474 | else: 475 | msg.append("no-gen-acls") 476 | 477 | # 'auth' feature requires 'acls' 478 | if tpg.has_feature("auth"): 479 | if not int(tpg.get_attribute("authentication")): 480 | msg.append("no-auth") 481 | if int(tpg.get_attribute("generate_node_acls")): 482 | # if auth=0, g_n_a=1 is recommended 483 | status = True 484 | elif not int(tpg.get_attribute("generate_node_acls")): 485 | msg.append("auth per-acl") 486 | else: 487 | msg.append("tpg-auth") 488 | 489 | status = True 490 | if not (tpg.chap_password and tpg.chap_userid): 491 | status = False 492 | 493 | if tpg.authenticate_target: 494 | msg.append("mutual auth") 495 | else: 496 | msg.append("1-way auth") 497 | 498 | return (", ".join(msg), status) 499 | 500 | def ui_getgroup_auth(self, auth_attr): 501 | return getattr(self.rtsnode, "chap_" + auth_attr) 502 | 503 | def ui_setgroup_auth(self, auth_attr, value): 504 | self.assert_root() 505 | 506 | if value is None: 507 | value = '' 508 | 509 | setattr(self.rtsnode, "chap_" + auth_attr, value) 510 | 511 | def ui_command_enable(self): 512 | ''' 513 | Enables the TPG. 514 | 515 | SEE ALSO 516 | ======== 517 | disable status 518 | ''' 519 | self.assert_root() 520 | if self.rtsnode.enable: 521 | self.shell.log.info("The TPGT is already enabled.") 522 | else: 523 | try: 524 | self.rtsnode.enable = True 525 | self.shell.log.info("The TPGT has been enabled.") 526 | except RTSLibError: 527 | raise ExecutionError("The TPGT could not be enabled.") 528 | 529 | def ui_command_disable(self): 530 | ''' 531 | Disables the TPG. 532 | 533 | SEE ALSO 534 | ======== 535 | enable status 536 | ''' 537 | self.assert_root() 538 | if self.rtsnode.enable: 539 | self.rtsnode.enable = False 540 | self.shell.log.info("The TPGT has been disabled.") 541 | else: 542 | self.shell.log.info("The TPGT is already disabled.") 543 | 544 | 545 | class UITarget(UITPG): 546 | ''' 547 | A generic target UI merged with its only TPG. 548 | ''' 549 | def __init__(self, target, parent): 550 | super().__init__(TPG(target, 1), parent) 551 | self._name = target.wwn 552 | self.target = target 553 | if self.parent.name != "sbp": 554 | self.rtsnode.enable = True 555 | 556 | def summary(self): 557 | try: 558 | self.target.fabric_module.to_normalized_wwn(self.target.wwn) 559 | except: 560 | return ("INVALID WWN", False) 561 | 562 | return super().summary() 563 | 564 | 565 | class UINodeACLs(UINode): 566 | ''' 567 | A generic UI for node ACLs. 568 | ''' 569 | def __init__(self, tpg, parent): 570 | super().__init__("acls", parent) 571 | self.tpg = tpg 572 | self.refresh() 573 | 574 | def refresh(self): 575 | self._children = set() 576 | for name in self.all_names(): 577 | UINodeACL(name, self) 578 | 579 | def summary(self): 580 | return (f"ACLs: {len(self._children)}", None) 581 | 582 | def ui_command_create(self, wwn, add_mapped_luns=None): 583 | ''' 584 | Creates a Node ACL for the initiator node with the specified wwn. 585 | The node's wwn must match the expected WWN Type of the target's 586 | fabric module. 587 | 588 | "add_mapped_luns" can be "true" of "false". If true, then 589 | after creating the ACL, mapped LUNs will be automatically 590 | created for all existing LUNs. If the parameter is omitted, 591 | the global parameter "auto_add_mapped_luns" is used. 592 | 593 | SEE ALSO 594 | ======== 595 | delete 596 | 597 | ''' 598 | self.assert_root() 599 | 600 | add_mapped_luns = self.ui_eval_param(add_mapped_luns, 'bool', 601 | self.shell.prefs['auto_add_mapped_luns']) 602 | 603 | node_acl = NodeACL(self.tpg, wwn, mode="create") 604 | ui_node_acl = UINodeACL(node_acl.node_wwn, self) 605 | self.shell.log.info(f"Created Node ACL for {node_acl.node_wwn}") 606 | 607 | if add_mapped_luns: 608 | for lun in self.tpg.luns: 609 | MappedLUN(node_acl, lun.lun, lun.lun, write_protect=False) 610 | self.shell.log.info("Created mapped LUN %d." % lun.lun) 611 | self.refresh() 612 | 613 | return self.new_node(ui_node_acl) 614 | 615 | def ui_command_delete(self, wwn): 616 | ''' 617 | Deletes the Node ACL with the specified wwn. 618 | 619 | SEE ALSO 620 | ======== 621 | create 622 | ''' 623 | self.assert_root() 624 | node_acl = NodeACL(self.tpg, wwn, mode='lookup') 625 | node_acl.delete() 626 | self.shell.log.info(f"Deleted Node ACL {wwn}.") 627 | self.refresh() 628 | 629 | def ui_complete_delete(self, parameters, text, current_param): 630 | ''' 631 | Parameter auto-completion method for user command delete. 632 | @param parameters: Parameters on the command line. 633 | @type parameters: dict 634 | @param text: Current text of parameter being typed by the user. 635 | @type text: str 636 | @param current_param: Name of parameter to complete. 637 | @type current_param: str 638 | @return: Possible completions 639 | @rtype: list of str 640 | ''' 641 | if current_param == 'wwn': 642 | wwns = [acl.node_wwn for acl in self.tpg.node_acls] 643 | completions = [wwn for wwn in wwns if wwn.startswith(text)] 644 | else: 645 | completions = [] 646 | 647 | if len(completions) == 1: 648 | return [completions[0] + ' '] 649 | return completions 650 | 651 | def find_tagged(self, name): 652 | for na in self.tpg.node_acls: 653 | if name in {na.node_wwn, na.tag}: 654 | yield na 655 | 656 | def all_names(self): 657 | names = set() 658 | 659 | for na in self.tpg.node_acls: 660 | if na.tag: 661 | names.add(na.tag) 662 | else: 663 | names.add(na.node_wwn) 664 | 665 | return names 666 | 667 | def ui_command_tag(self, wwn_or_tag, new_tag): 668 | ''' 669 | Tag a NodeACL. 670 | 671 | Usage: tag 672 | 673 | Tags help manage initiator WWNs. A tag can apply to one or 674 | more WWNs. This can give a more meaningful name to a single 675 | initiator's configuration, or allow multiple initiators with 676 | identical settings to be configured en masse. 677 | 678 | The WWNs described by will be given the new 679 | tag. If new_tag already exists, its new members will adopt the 680 | current tag's configuration. 681 | 682 | Within a tag, the 'info' command shows the WWNs the tag applies to. 683 | 684 | Use 'untag' to remove tags. 685 | 686 | NOTE: tags are only supported in kernel 3.8 and above. 687 | ''' 688 | if wwn_or_tag == new_tag: 689 | return 690 | 691 | # Since all WWNs have a '.' in them, let's avoid confusion. 692 | if '.' in new_tag: 693 | raise ExecutionError("'.' not permitted in tag names.") 694 | 695 | src = list(self.find_tagged(wwn_or_tag)) 696 | if not src: 697 | raise ExecutionError(f"wwn_or_tag {wwn_or_tag} not found.") 698 | 699 | old_tag_members = list(self.find_tagged(new_tag)) 700 | 701 | # handle overlap 702 | src_wwns = [na.node_wwn for na in src] 703 | old_tag_members = [old for old in old_tag_members if old.node_wwn not in src_wwns] 704 | 705 | for na in src: 706 | na.tag = new_tag 707 | 708 | # if joining a tag, take its config 709 | if old_tag_members: 710 | model = old_tag_members[0] 711 | 712 | for mlun in na.mapped_luns: 713 | mlun.delete() 714 | 715 | for mlun in model.mapped_luns: 716 | MappedLUN(na, mlun.mapped_lun, mlun.tpg_lun, mlun.write_protect) 717 | 718 | if self.parent.rtsnode.has_feature("auth"): 719 | for param in auth_params: 720 | setattr(na, "chap_" + param, getattr(model, "chap_" + param)) 721 | 722 | for item in model.list_attributes(writable=True): 723 | na.set_attribute(item, model.get_attribute(item)) 724 | for item in model.list_parameters(writable=True): 725 | na.set_parameter(item, model.get_parameter(item)) 726 | 727 | self.refresh() 728 | 729 | def ui_command_untag(self, wwn_or_tag): 730 | ''' 731 | Untag a NodeACL. 732 | 733 | Usage: untag 734 | 735 | Remove the tag given to one or more initiator WWNs. They will 736 | return to being displayed by WWN in the configuration tree, and 737 | will maintain settings from when they were tagged. 738 | ''' 739 | for na in list(self.find_tagged(wwn_or_tag)): 740 | na.tag = None 741 | 742 | self.refresh() 743 | 744 | def ui_complete_tag(self, parameters, text, current_param): 745 | ''' 746 | Parameter auto-completion method for user command tag 747 | @param parameters: Parameters on the command line. 748 | @type parameters: dict 749 | @param text: Current text of parameter being typed by the user. 750 | @type text: str 751 | @param current_param: Name of parameter to complete. 752 | @type current_param: str 753 | @return: Possible completions 754 | @rtype: list of str 755 | ''' 756 | completions = [n for n in self.all_names() if n.startswith(text)] if current_param == 'wwn_or_tag' else [] 757 | 758 | if len(completions) == 1: 759 | return [completions[0] + ' '] 760 | return completions 761 | 762 | ui_complete_untag = ui_complete_tag 763 | 764 | 765 | class UINodeACL(UIRTSLibNode): 766 | ''' 767 | A generic UI for a node ACL. 768 | 769 | Handles grouping multiple NodeACLs in UI via tags. 770 | All gets are performed against first NodeACL. 771 | All sets are performed on all NodeACLs. 772 | This is to make management of multiple ACLs easier. 773 | ''' 774 | ui_desc_attributes = { 775 | 'dataout_timeout': ('number', 'Data-Out timeout in seconds before invoking recovery.'), 776 | 'dataout_timeout_retries': ('number', 'Number of Data-Out timeout recovery attempts before failing a path.'), 777 | 'default_erl': ('number', 'Default Error Recovery Level.'), 778 | 'nopin_response_timeout': ('number', 'Nop-In response timeout in seconds.'), 779 | 'nopin_timeout': ('number', 'Nop-In timeout in seconds.'), 780 | 'random_datain_pdu_offsets': ('number', 'If set to 1, request random Data-In PDU offsets.'), 781 | 'random_datain_seq_offsets': ('number', 'If set to 1, request random Data-In sequence offsets.'), 782 | 'random_r2t_offsets': ('number', 'If set to 1, request random R2T (Ready To Transfer) offsets.'), 783 | } 784 | 785 | ui_desc_parameters = UITPG.ui_desc_parameters 786 | 787 | def __init__(self, name, parent): 788 | 789 | # Don't want to duplicate work in UIRTSLibNode, so call it but 790 | # del self.rtsnode to make sure we always use self.rtsnodes. 791 | self.rtsnodes = list(parent.find_tagged(name)) 792 | super().__init__(name, self.rtsnodes[0], parent) 793 | del self.rtsnode 794 | 795 | if self.parent.parent.rtsnode.has_feature('auth'): 796 | for parameter in auth_params: 797 | self.define_config_group_param('auth', parameter, 'string') 798 | 799 | self.refresh() 800 | 801 | def ui_getgroup_auth(self, auth_attr): 802 | ''' 803 | This is the backend method for getting auths attributes. 804 | @param auth_attr: The auth attribute to get the value of. 805 | @type auth_attr: str 806 | @return: The auth attribute's value 807 | @rtype: str 808 | ''' 809 | # All should return same, so just return from the first one 810 | return getattr(self.rtsnodes[0], "chap_" + auth_attr) 811 | 812 | def ui_setgroup_auth(self, auth_attr, value): 813 | ''' 814 | This is the backend method for setting auths attributes. 815 | @param auth_attr: The auth attribute to set the value of. 816 | @type auth_attr: str 817 | @param value: The auth's value 818 | @type value: str 819 | ''' 820 | self.assert_root() 821 | 822 | if value is None: 823 | value = '' 824 | 825 | for na in self.rtsnodes: 826 | setattr(na, "chap_" + auth_attr, value) 827 | 828 | def refresh(self): 829 | self._children = set() 830 | for mlun in self.rtsnodes[0].mapped_luns: 831 | UIMappedLUN(mlun, self) 832 | 833 | def summary(self): 834 | msg = [] 835 | 836 | if self.name != self.rtsnodes[0].node_wwn: 837 | if len(self.rtsnodes) > 1: 838 | msg.append(f"(group of {len(self.rtsnodes)})") 839 | else: 840 | msg.append(f"({self.rtsnodes[0].node_wwn})") 841 | 842 | status = None 843 | na = self.rtsnodes[0] 844 | tpg = self.parent.parent.rtsnode 845 | if tpg.has_feature("auth") and \ 846 | int(tpg.get_attribute("authentication")): 847 | if int(tpg.get_attribute("generate_node_acls")): 848 | msg.append("auth via tpg") 849 | else: 850 | status = True 851 | if not (na.chap_password and na.chap_userid): 852 | status = False 853 | 854 | if na.authenticate_target: 855 | msg.append("mutual auth") 856 | else: 857 | msg.append("1-way auth") 858 | 859 | msg.append(f"Mapped LUNs: {len(self._children)}") 860 | 861 | return (", ".join(msg), status) 862 | 863 | def ui_command_create(self, mapped_lun, tpg_lun_or_backstore, write_protect=None): 864 | ''' 865 | Creates a mapping to one of the TPG LUNs for the initiator referenced 866 | by the ACL. The provided "tpg_lun_or_backstore" will appear to that 867 | initiator as LUN "mapped_lun". If the "write_protect" flag is set to 868 | 1, the initiator will not have write access to the mapped LUN. 869 | 870 | A storage object may also be given for the "tpg_lun_or_backstore" parameter, 871 | in which case the TPG LUN will be created for that backstore before 872 | mapping the LUN to the initiator. If a TPG LUN for the backstore already 873 | exists, the mapped LUN will map to that TPG LUN. 874 | 875 | Finally, a path to an existing block device or file can be given. If so, 876 | a storage object of the appropriate type is created with default parameters, 877 | followed by the TPG LUN and the Mapped LUN. 878 | 879 | SEE ALSO 880 | ======== 881 | delete 882 | ''' 883 | self.assert_root() 884 | try: 885 | mapped_lun = int(mapped_lun) 886 | except ValueError: 887 | raise ExecutionError("mapped_lun must be an integer") 888 | 889 | try: 890 | if tpg_lun_or_backstore.startswith("lun"): 891 | tpg_lun_or_backstore = tpg_lun_or_backstore.removeprefix("lun") 892 | tpg_lun = int(tpg_lun_or_backstore) 893 | except ValueError: 894 | try: 895 | so = self.get_node(tpg_lun_or_backstore).rtsnode 896 | except ValueError: 897 | try: 898 | so = StorageObjectFactory(tpg_lun_or_backstore) 899 | self.shell.log.info(f"Created storage object {so.name}.") 900 | except RTSLibError: 901 | raise ExecutionError("LUN, storage object, or path not valid") 902 | self.get_node("/backstores").refresh() 903 | 904 | ui_tpg = self.parent.parent 905 | 906 | for lun in ui_tpg.rtsnode.luns: 907 | if so == lun.storage_object: 908 | tpg_lun = lun.lun 909 | break 910 | else: 911 | lun_object = LUN(ui_tpg.rtsnode, storage_object=so) 912 | self.shell.log.info(f"Created LUN {lun_object.lun}.") 913 | ui_lun = UILUN(lun_object, ui_tpg.get_node("luns")) 914 | tpg_lun = ui_lun.rtsnode.lun 915 | 916 | if tpg_lun in (ml.tpg_lun.lun for ml in self.rtsnodes[0].mapped_luns): 917 | self.shell.log.warning( 918 | "Warning: TPG LUN %d already mapped to this NodeACL" % tpg_lun) 919 | 920 | for na in self.rtsnodes: 921 | mlun = MappedLUN(na, mapped_lun, tpg_lun, write_protect) 922 | 923 | ui_mlun = UIMappedLUN(mlun, self) 924 | self.shell.log.info(f"Created Mapped LUN {mlun.mapped_lun}.") 925 | return self.new_node(ui_mlun) 926 | 927 | def ui_complete_create(self, parameters, text, current_param): 928 | ''' 929 | Parameter auto-completion method for user command create. 930 | @param parameters: Parameters on the command line. 931 | @type parameters: dict 932 | @param text: Current text of parameter being typed by the user. 933 | @type text: str 934 | @param current_param: Name of parameter to complete. 935 | @type current_param: str 936 | @return: Possible completions 937 | @rtype: list of str 938 | ''' 939 | if current_param == 'tpg_lun_or_backstore': 940 | completions = [] 941 | for backstore in self.get_node('/backstores').children: 942 | completions.extend(storage_object.path 943 | for storage_object in backstore.children) 944 | 945 | completions.extend(lun.name 946 | for lun in self.parent.parent.get_node("luns").children) 947 | 948 | if text: 949 | completions.extend(complete_path(text, 950 | lambda x: stat.S_ISREG(x) or stat.S_ISBLK(x))) 951 | 952 | completions = [c for c in completions if c.startswith(text)] 953 | else: 954 | completions = [] 955 | 956 | if len(completions) == 1: 957 | return [completions[0] + ' '] 958 | return completions 959 | 960 | def ui_command_delete(self, mapped_lun): 961 | ''' 962 | Deletes the specified mapped LUN. 963 | 964 | SEE ALSO 965 | ======== 966 | create 967 | ''' 968 | self.assert_root() 969 | for na in self.rtsnodes: 970 | mlun = MappedLUN(na, mapped_lun) 971 | mlun.delete() 972 | self.shell.log.info(f"Deleted Mapped LUN {mapped_lun}.") 973 | self.refresh() 974 | 975 | def ui_complete_delete(self, parameters, text, current_param): 976 | ''' 977 | Parameter auto-completion method for user command delete. 978 | @param parameters: Parameters on the command line. 979 | @type parameters: dict 980 | @param text: Current text of parameter being typed by the user. 981 | @type text: str 982 | @param current_param: Name of parameter to complete. 983 | @type current_param: str 984 | @return: Possible completions 985 | @rtype: list of str 986 | ''' 987 | if current_param == 'mapped_lun': 988 | mluns = [str(mlun.mapped_lun) for mlun in self.rtsnodes[0].mapped_luns] 989 | completions = [mlun for mlun in mluns if mlun.startswith(text)] 990 | else: 991 | completions = [] 992 | 993 | if len(completions) == 1: 994 | return [completions[0] + ' '] 995 | return completions 996 | 997 | # Override these four methods to handle multiple NodeACLs 998 | def ui_getgroup_attribute(self, attribute): 999 | return self.rtsnodes[0].get_attribute(attribute) 1000 | 1001 | def ui_setgroup_attribute(self, attribute, value): 1002 | self.assert_root() 1003 | 1004 | for na in self.rtsnodes: 1005 | na.set_attribute(attribute, value) 1006 | 1007 | def ui_getgroup_parameter(self, parameter): 1008 | return self.rtsnodes[0].get_parameter(parameter) 1009 | 1010 | def ui_setgroup_parameter(self, parameter, value): 1011 | self.assert_root() 1012 | 1013 | for na in self.rtsnodes: 1014 | na.set_parameter(parameter, value) 1015 | 1016 | def ui_command_info(self): 1017 | ''' 1018 | Since we don't have a self.rtsnode we can't use the base implementation 1019 | of this method. We also want to not print node_wwn, but list *all* 1020 | wwns for this entry. 1021 | ''' 1022 | info = self.rtsnodes[0].dump() 1023 | for item in ('attributes', 'parameters', "node_wwn"): 1024 | if item in info: 1025 | del info[item] 1026 | for name, value in sorted(info.items()): 1027 | if not isinstance(value, (dict, list)): 1028 | self.shell.log.info(f"{name}: {value}") 1029 | self.shell.log.info("wwns:") 1030 | for na in self.parent.find_tagged(self.name): 1031 | self.shell.log.info(na.node_wwn) 1032 | 1033 | 1034 | class UIMappedLUN(UIRTSLibNode): 1035 | ''' 1036 | A generic UI for MappedLUN objects. 1037 | ''' 1038 | def __init__(self, mapped_lun, parent): 1039 | name = "mapped_lun%d" % mapped_lun.mapped_lun 1040 | super().__init__(name, mapped_lun, parent) 1041 | self.refresh() 1042 | 1043 | def summary(self): 1044 | mapped_lun = self.rtsnode 1045 | is_healthy = True 1046 | try: 1047 | tpg_lun = mapped_lun.tpg_lun 1048 | except RTSLibBrokenLink: 1049 | description = "BROKEN LUN LINK" 1050 | is_healthy = False 1051 | else: 1052 | access_mode = 'ro' if mapped_lun.write_protect else 'rw' 1053 | description = "lun%d %s/%s (%s)" \ 1054 | % (tpg_lun.lun, tpg_lun.storage_object.plugin, 1055 | tpg_lun.storage_object.name, access_mode) 1056 | 1057 | return (description, is_healthy) 1058 | 1059 | 1060 | class UILUNs(UINode): 1061 | ''' 1062 | A generic UI for TPG LUNs. 1063 | ''' 1064 | def __init__(self, tpg, parent): 1065 | super().__init__("luns", parent) 1066 | self.tpg = tpg 1067 | self.refresh() 1068 | 1069 | def refresh(self): 1070 | self._children = set() 1071 | for lun in self.tpg.luns: 1072 | UILUN(lun, self) 1073 | 1074 | def summary(self): 1075 | return (f"LUNs: {len(self._children)}", None) 1076 | 1077 | def ui_command_create(self, storage_object, lun=None, 1078 | add_mapped_luns=None): 1079 | ''' 1080 | Creates a new LUN in the Target Portal Group, attached to a storage 1081 | object. If the "lun" parameter is omitted, the first available LUN in 1082 | the TPG will be used. If present, it must be a number greater than 0. 1083 | Alternatively, the syntax "lunX" where "X" is a positive number is 1084 | also accepted. 1085 | 1086 | The "storage_object" may be the path of an existing storage object, 1087 | i.e. "/backstore/pscsi0/mydisk" to reference the "mydisk" storage 1088 | object of the virtual HBA "pscsi0". It also may be the path to an 1089 | existing block device or image file, in which case a storage object 1090 | will be created for it first, with default parameters. 1091 | 1092 | "add_mapped_luns" can be "true" of "false". If true, then 1093 | after creating the ACL, mapped LUNs will be automatically 1094 | created for all existing LUNs. If the parameter is omitted, 1095 | the global parameter "auto_add_mapped_luns" is used. 1096 | 1097 | SEE ALSO 1098 | ======== 1099 | delete 1100 | ''' 1101 | self.assert_root() 1102 | 1103 | add_mapped_luns = \ 1104 | self.ui_eval_param(add_mapped_luns, 'bool', 1105 | self.shell.prefs['auto_add_mapped_luns']) 1106 | 1107 | try: 1108 | so = self.get_node(storage_object).rtsnode 1109 | except ValueError: 1110 | try: 1111 | so = StorageObjectFactory(storage_object) 1112 | self.shell.log.info(f"Created storage object {so.name}.") 1113 | except RTSLibError: 1114 | raise ExecutionError("storage object or path not valid") 1115 | self.get_node("/backstores").refresh() 1116 | 1117 | if so in (lun.storage_object for lun in self.parent.rtsnode.luns): 1118 | raise ExecutionError(f"lun for storage object {so.plugin}/{so.name} already exists") 1119 | 1120 | if lun and lun.lower().startswith('lun'): 1121 | lun = lun[3:] 1122 | lun_object = LUN(self.tpg, lun, so) 1123 | self.shell.log.info(f"Created LUN {lun_object.lun}.") 1124 | ui_lun = UILUN(lun_object, self) 1125 | 1126 | if add_mapped_luns: 1127 | for acl in self.tpg.node_acls: 1128 | mapped_lun = lun or 0 1129 | existing_mluns = [mlun.mapped_lun for mlun in acl.mapped_luns] 1130 | if mapped_lun in existing_mluns: 1131 | possible_mlun = 0 1132 | while possible_mlun in existing_mluns: 1133 | possible_mlun += 1 1134 | mapped_lun = possible_mlun 1135 | 1136 | mlun = MappedLUN(acl, mapped_lun, lun_object, write_protect=False) 1137 | self.shell.log.info("Created LUN %d->%d mapping in node ACL %s" 1138 | % (mlun.tpg_lun.lun, mlun.mapped_lun, acl.node_wwn)) 1139 | self.parent.refresh() 1140 | 1141 | return self.new_node(ui_lun) 1142 | 1143 | def ui_complete_create(self, parameters, text, current_param): 1144 | ''' 1145 | Parameter auto-completion method for user command create. 1146 | @param parameters: Parameters on the command line. 1147 | @type parameters: dict 1148 | @param text: Current text of parameter being typed by the user. 1149 | @type text: str 1150 | @param current_param: Name of parameter to complete. 1151 | @type current_param: str 1152 | @return: Possible completions 1153 | @rtype: list of str 1154 | ''' 1155 | if current_param == 'storage_object': 1156 | storage_objects = [] 1157 | for backstore in self.get_node('/backstores').children: 1158 | storage_objects.extend(storage_object.path 1159 | for storage_object in backstore.children) 1160 | completions = [so for so in storage_objects if so.startswith(text)] 1161 | 1162 | if text: 1163 | completions.extend(complete_path(text, 1164 | lambda x: stat.S_ISREG(x) or stat.S_ISBLK(x))) 1165 | else: 1166 | completions = [] 1167 | 1168 | if len(completions) == 1: 1169 | return [completions[0] + ' '] 1170 | return completions 1171 | 1172 | def ui_command_delete(self, lun): 1173 | ''' 1174 | Deletes the supplied LUN from the Target Portal Group. "lun" must 1175 | be a positive number matching an existing LUN. 1176 | 1177 | Alternatively, the syntax "lunX" where "X" is a positive number is 1178 | also accepted. 1179 | 1180 | SEE ALSO 1181 | ======== 1182 | create 1183 | ''' 1184 | self.assert_root() 1185 | if lun.lower().startswith("lun"): 1186 | lun = lun[3:] 1187 | try: 1188 | lun_object = LUN(self.tpg, lun) 1189 | except: 1190 | raise RTSLibError("Invalid LUN") 1191 | lun_object.delete() 1192 | self.shell.log.info(f"Deleted LUN {lun}.") 1193 | # Refresh the TPG as we need to also refresh acls MappedLUNs 1194 | self.parent.refresh() 1195 | 1196 | def ui_complete_delete(self, parameters, text, current_param): 1197 | ''' 1198 | Parameter auto-completion method for user command delete. 1199 | @param parameters: Parameters on the command line. 1200 | @type parameters: dict 1201 | @param text: Current text of parameter being typed by the user. 1202 | @type text: str 1203 | @param current_param: Name of parameter to complete. 1204 | @type current_param: str 1205 | @return: Possible completions 1206 | @rtype: list of str 1207 | ''' 1208 | if current_param == 'lun': 1209 | luns = [str(lun.lun) for lun in self.tpg.luns] 1210 | completions = [lun for lun in luns if lun.startswith(text)] 1211 | else: 1212 | completions = [] 1213 | 1214 | if len(completions) == 1: 1215 | return [completions[0] + ' '] 1216 | return completions 1217 | 1218 | 1219 | class UILUN(UIRTSLibNode): 1220 | ''' 1221 | A generic UI for LUN objects. 1222 | ''' 1223 | def __init__(self, lun, parent): 1224 | name = "lun%d" % lun.lun 1225 | super().__init__(name, lun, parent) 1226 | self.refresh() 1227 | 1228 | self.define_config_group_param("alua", "alua_tg_pt_gp_name", 'string') 1229 | 1230 | def summary(self): 1231 | lun = self.rtsnode 1232 | is_healthy = True 1233 | try: 1234 | storage_object = lun.storage_object 1235 | except RTSLibBrokenLink: 1236 | description = "BROKEN STORAGE LINK" 1237 | is_healthy = False 1238 | else: 1239 | description = f"{storage_object.plugin}/{storage_object.name}" 1240 | if storage_object.udev_path: 1241 | description += f" ({storage_object.udev_path})" 1242 | 1243 | description += f" ({lun.alua_tg_pt_gp_name})" 1244 | 1245 | return (description, is_healthy) 1246 | 1247 | def ui_getgroup_alua(self, alua_attr): 1248 | return getattr(self.rtsnode, alua_attr) 1249 | 1250 | def ui_setgroup_alua(self, alua_attr, value): 1251 | self.assert_root() 1252 | 1253 | if value is None: 1254 | return 1255 | 1256 | setattr(self.rtsnode, alua_attr, value) 1257 | 1258 | class UIPortals(UINode): 1259 | ''' 1260 | A generic UI for TPG network portals. 1261 | ''' 1262 | def __init__(self, tpg, parent): 1263 | super().__init__("portals", parent) 1264 | self.tpg = tpg 1265 | self.refresh() 1266 | 1267 | def refresh(self): 1268 | self._children = set() 1269 | for portal in self.tpg.network_portals: 1270 | UIPortal(portal, self) 1271 | 1272 | def summary(self): 1273 | return (f"Portals: {len(self._children)}", None) 1274 | 1275 | def _canonicalize_ip(self, ip_address): 1276 | """ 1277 | rtslib expects ipv4 addresses as a dotted-quad string, and IPv6 1278 | addresses surrounded by brackets. 1279 | """ 1280 | 1281 | # Contains a '.'? Must be ipv4, right? 1282 | if "." in ip_address: 1283 | return ip_address 1284 | return "[" + ip_address + "]" 1285 | 1286 | def ui_command_create(self, ip_address=None, ip_port=None): 1287 | ''' 1288 | Creates a Network Portal with the specified IP address and 1289 | port. If the port is omitted, the default port for 1290 | the target fabric will be used. If the IP address is omitted, 1291 | IN6ADDR_ANY (::0) will be used. 1292 | 1293 | The default IN6ADDR_ANY (::0) will listen on all IPv6 interfaces 1294 | as well as IPv4, assuming IPV6_V6ONLY sockopt has not been 1295 | set. 1296 | 1297 | Note: Portals on Link-local IPv6 addresses are currently not 1298 | supported. 1299 | 1300 | SEE ALSO 1301 | ======== 1302 | delete 1303 | ''' 1304 | self.assert_root() 1305 | 1306 | # FIXME: Add a specfile parameter to determine default port 1307 | default_port = 3260 1308 | ip_port = self.ui_eval_param(ip_port, 'number', default_port) 1309 | ip_address = self.ui_eval_param(ip_address, 'string', default_portal_listen) 1310 | 1311 | if ip_port == default_port: 1312 | self.shell.log.info("Using default IP port %d" % ip_port) 1313 | if ip_address == default_portal_listen: 1314 | self.shell.log.info(f"Binding to INADDR_ANY ({default_portal_listen})") 1315 | 1316 | portal = NetworkPortal(self.tpg, self._canonicalize_ip(ip_address), 1317 | ip_port, mode='create') 1318 | self.shell.log.info("Created network portal %s:%d." 1319 | % (ip_address, ip_port)) 1320 | ui_portal = UIPortal(portal, self) 1321 | return self.new_node(ui_portal) 1322 | 1323 | def ui_complete_create(self, parameters, text, current_param): 1324 | ''' 1325 | Parameter auto-completion method for user command create. 1326 | @param parameters: Parameters on the command line. 1327 | @type parameters: dict 1328 | @param text: Current text of parameter being typed by the user. 1329 | @type text: str 1330 | @param current_param: Name of parameter to complete. 1331 | @type current_param: str 1332 | @return: Possible completions 1333 | @rtype: list of str 1334 | ''' 1335 | 1336 | def list_eth_ips(): 1337 | if not ethtool: 1338 | return [] 1339 | 1340 | devcfgs = ethtool.get_interfaces_info(ethtool.get_devices()) 1341 | addrs = set() 1342 | for d in devcfgs: 1343 | if d.ipv4_address: 1344 | addrs.add(d.ipv4_address) 1345 | addrs.add("0.0.0.0") 1346 | for ip6 in d.get_ipv6_addresses(): 1347 | addrs.add(ip6.address) 1348 | addrs.add("::0") # only list ::0 if ipv6 present 1349 | 1350 | return sorted(addrs) 1351 | 1352 | if current_param == 'ip_address': 1353 | completions = [addr for addr in list_eth_ips() 1354 | if addr.startswith(text)] 1355 | else: 1356 | completions = [] 1357 | 1358 | if len(completions) == 1: 1359 | return [completions[0] + ' '] 1360 | return completions 1361 | 1362 | def ui_command_delete(self, ip_address, ip_port): 1363 | ''' 1364 | Deletes the Network Portal with the specified IP address and port. 1365 | 1366 | SEE ALSO 1367 | ======== 1368 | create 1369 | ''' 1370 | self.assert_root() 1371 | portal = NetworkPortal(self.tpg, self._canonicalize_ip(ip_address), 1372 | ip_port, mode='lookup') 1373 | portal.delete() 1374 | self.shell.log.info(f"Deleted network portal {ip_address}:{ip_port}") 1375 | self.refresh() 1376 | 1377 | def ui_complete_delete(self, parameters, text, current_param): 1378 | ''' 1379 | Parameter auto-completion method for user command delete. 1380 | @param parameters: Parameters on the command line. 1381 | @type parameters: dict 1382 | @param text: Current text of parameter being typed by the user. 1383 | @type text: str 1384 | @param current_param: Name of parameter to complete. 1385 | @type current_param: str 1386 | @return: Possible completions 1387 | @rtype: list of str 1388 | ''' 1389 | completions = [] 1390 | # TODO: Check if a dict comprehension is acceptable here with supported 1391 | # XXX: python versions. 1392 | portals = {} 1393 | all_ports = set() 1394 | for portal in self.tpg.network_portals: 1395 | all_ports.add(str(portal.port)) 1396 | portal_ip = portal.ip_address.strip('[]') 1397 | if portal_ip not in portals: 1398 | portals[portal_ip] = [] 1399 | portals[portal_ip].append(str(portal.port)) 1400 | 1401 | if current_param == 'ip_address': 1402 | completions = [addr for addr in portals if addr.startswith(text)] 1403 | if 'ip_port' in parameters: 1404 | port = parameters['ip_port'] 1405 | completions = [addr for addr in completions 1406 | if port in portals[addr]] 1407 | elif current_param == 'ip_port': 1408 | if 'ip_address' in parameters: 1409 | addr = parameters['ip_address'] 1410 | if addr in portals: 1411 | completions = [port for port in portals[addr] 1412 | if port.startswith(text)] 1413 | else: 1414 | completions = [port for port in all_ports 1415 | if port.startswith(text)] 1416 | 1417 | if len(completions) == 1: 1418 | return [completions[0] + ' '] 1419 | return completions 1420 | 1421 | 1422 | class UIPortal(UIRTSLibNode): 1423 | ''' 1424 | A generic UI for a network portal. 1425 | ''' 1426 | def __init__(self, portal, parent): 1427 | name = f"{portal.ip_address}:{portal.port}" 1428 | super().__init__(name, portal, parent) 1429 | self.refresh() 1430 | 1431 | def summary(self): 1432 | if self.rtsnode.iser: 1433 | return ('iser', True) 1434 | if self.rtsnode.offload: 1435 | return ('offload', True) 1436 | return ('', True) 1437 | 1438 | def ui_command_enable_iser(self, boolean): 1439 | ''' 1440 | Enables or disables iSER for this NetworkPortal. 1441 | 1442 | If iSER is not supported by the kernel, this command will do nothing. 1443 | ''' 1444 | 1445 | boolean = self.ui_eval_param(boolean, 'bool', False) 1446 | self.rtsnode.iser = boolean 1447 | self.shell.log.info(f"iSER enable now: {self.rtsnode.iser}") 1448 | 1449 | def ui_command_enable_offload(self, boolean): 1450 | ''' 1451 | Enables or disables offload for this NetworkPortal. 1452 | 1453 | If offload is not supported by the kernel, this command will do nothing. 1454 | ''' 1455 | 1456 | boolean = self.ui_eval_param(boolean, 'bool', False) 1457 | self.rtsnode.offload = boolean 1458 | self.shell.log.info(f"offload enable now: {self.rtsnode.offload}") 1459 | --------------------------------------------------------------------------------