├── README ├── debian ├── compat ├── patches │ └── series ├── source │ └── format ├── clean ├── rules ├── control └── copyright ├── linstor_client ├── argparse │ └── __init__.py ├── commands │ ├── utils │ │ ├── __init__.py │ │ └── skip_disk_utils.py │ ├── __init__.py │ ├── zsh_completer.py │ ├── key_value_store.py │ ├── file_cmds.py │ ├── physical_storage_cmds.py │ ├── drbd_setup_cmds.py │ ├── error_report_cmds.py │ ├── migrate_cmds.py │ ├── vlm_grp_cmds.py │ ├── drbd_proxy_cmds.py │ ├── controller_cmds.py │ └── node_conn_cmds.py ├── __init__.py ├── argcomplete │ ├── compat.py │ ├── _check_module.py │ ├── completers.py │ ├── LICENSE.rst │ └── my_shlex.py ├── consts.py ├── tree.py └── utils.py ├── .pep8 ├── MANIFEST.in ├── setup.cfg.py2 ├── tests.py ├── .gitignore ├── scripts ├── bash_completion │ └── linstor └── linstor ├── setup.cfg ├── tests ├── __init__.py ├── test_client_commands.py ├── test_ctrl_list_commands.py ├── test_tables.py ├── test_ctrl_props.py ├── test_ctrl_nodes.py ├── test_drbd_options.py └── linstor_testcase.py ├── README.md ├── man-pages ├── linstor_trailer.xml └── linstor_header.xml ├── Dockerfile ├── Makefile ├── .gitlab-ci.yml ├── CHANGELOG.md └── setup.py /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/patches/series: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /linstor_client/argparse/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/clean: -------------------------------------------------------------------------------- 1 | linstor_client.egg-info/* 2 | -------------------------------------------------------------------------------- /linstor_client/commands/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.pep8: -------------------------------------------------------------------------------- 1 | [pep8] 2 | 3 | max-line-length=120 4 | -------------------------------------------------------------------------------- /linstor_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .table import Table, TableHeader 2 | from . import consts 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include man-pages *.gz 2 | recursive-include man-pages *.xml 3 | recursive-include scripts * 4 | include MANIFEST.in 5 | include COPYING 6 | include README.md 7 | include Makefile 8 | include Dockerfile 9 | include setup.cfg.py2 10 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export PYBUILD_NAME = linstor-client 4 | export PYBUILD_DISABLE=test 5 | 6 | %: 7 | dh $@ --with python3 --buildsystem=pybuild 8 | 9 | override_dh_auto_build: 10 | dh_auto_build 11 | 12 | override_dh_install: 13 | dh_install --fail-missing 14 | -------------------------------------------------------------------------------- /setup.cfg.py2: -------------------------------------------------------------------------------- 1 | [bdist_rpm] 2 | release = 1 3 | group = System Environment/Daemons 4 | packager = LINSTOR Team 5 | vendor = LINBIT HA-Solutions GmbH 6 | build_requires = 7 | python2-setuptools 8 | requires = 9 | python2-setuptools 10 | python-linstor >= 1.27.1 11 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | 4 | if __name__ == '__main__': 5 | import xmlrunner 6 | loader = unittest.TestLoader() 7 | suite = loader.discover(start_dir='tests') 8 | runner = xmlrunner.XMLTestRunner(output='test-reports') 9 | res = runner.run(suite) 10 | sys.exit(1 if len(res.errors) > 0 else 0) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ide files and dirs 2 | .idea/ 3 | *.iml 4 | 5 | # ignore "compiled" python 6 | *.pyc 7 | 8 | # make generated files 9 | man-pages/ 10 | dist/ 11 | linstor_client.egg-info/ 12 | build 13 | MANIFEST 14 | 15 | # ignore generated githash version 16 | linstor_client/consts_githash.py 17 | 18 | # unittests ignores 19 | build/ 20 | test-reports/ 21 | .cache/ 22 | /.project 23 | /.pydevproject 24 | -------------------------------------------------------------------------------- /scripts/bash_completion/linstor: -------------------------------------------------------------------------------- 1 | 2 | _linstor() { 3 | local IFS=' ' 4 | COMPREPLY=( $(IFS="$IFS" COMP_LINE="$COMP_LINE" COMP_POINT="$COMP_POINT" _ARGCOMPLETE_COMP_WORDBREAKS="$COMP_WORDBREAKS" _ARGCOMPLETE=1 "$1" 8>&1 9>&2 1>/dev/null 2>/dev/null) ) 5 | if [[ $? != 0 ]]; then 6 | unset COMPREPLY 7 | fi 8 | } 9 | complete -o nospace -o default -F _linstor "linstor" 10 | -------------------------------------------------------------------------------- /linstor_client/argcomplete/compat.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, unicode_literals 2 | 3 | import locale 4 | import sys 5 | 6 | sys_encoding = locale.getpreferredencoding() 7 | 8 | USING_PYTHON2 = True if sys.version_info < (3, 0) else False 9 | 10 | if USING_PYTHON2: 11 | str = unicode # noqa 12 | else: 13 | str = str 14 | 15 | 16 | def ensure_bytes(x, encoding=sys_encoding): 17 | if not isinstance(x, bytes): 18 | x = x.encode(encoding) 19 | return x 20 | 21 | 22 | def ensure_str(x, encoding=sys_encoding): 23 | if not isinstance(x, str): 24 | x = x.decode(encoding) 25 | return x 26 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_rpm] 2 | release = 1 3 | group = System Environment/Daemons 4 | packager = LINSTOR Team 5 | vendor = LINBIT HA-Solutions GmbH 6 | build_requires = 7 | python3-setuptools 8 | requires = 9 | python3-setuptools 10 | python-linstor >= 1.27.1 11 | 12 | [flake8] 13 | count = True 14 | show-source = True 15 | statistics = True 16 | ignore = C901,W503,F541 17 | max-complexity = 10 18 | 19 | max-line-length = 120 20 | 21 | builtins = Optional, raw_input 22 | 23 | extend-exclude = 24 | dist/, 25 | test-reports/, 26 | venv/, 27 | build/, 28 | doc/, 29 | scripts/, 30 | debian/, 31 | linstor_client/argcomplete/, 32 | linstor_client/argparse/, 33 | linstor_client/consts_githash.py, 34 | 35 | per-file-ignores = 36 | __init__.py:F401 37 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from .linstor_testcase import LinstorTestCase 3 | from . import test_ctrl_list_commands, test_ctrl_nodes, test_ctrl_usecases 4 | from . import test_ctrl_props, test_drbd_options 5 | 6 | _controller_tests = [ 7 | "tests.test_ctrl_list_commands", 8 | "tests.test_ctrl_nodes", 9 | "tests.test_ctrl_usecases", 10 | "tests.test_ctrl_props", 11 | "tests.test_drbd_options" 12 | ] 13 | 14 | _std_tests = [ 15 | "tests.test_client_commands", 16 | "tests.test_tables" 17 | ] 18 | 19 | 20 | def load_all(): 21 | suite = unittest.TestSuite() 22 | loaded_tests = unittest.defaultTestLoader.loadTestsFromNames(_controller_tests + _std_tests) 23 | suite.addTest(loaded_tests) 24 | return suite 25 | 26 | 27 | def test_without_controller(): 28 | suite = unittest.TestSuite() 29 | loaded_tests = unittest.defaultTestLoader.loadTestsFromNames(_std_tests) 30 | suite.addTest(loaded_tests) 31 | return suite 32 | -------------------------------------------------------------------------------- /scripts/linstor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | """ 3 | linstor - LINBIT storage management CLI 4 | Copyright (C) 2017 LINBIT HA-Solutions GmbH 5 | Author: Roland Kammerer 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | import linstor_client_main 22 | 23 | if __name__ == "__main__": 24 | linstor_client_main.main() 25 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: linstor-client 2 | Maintainer: LINBIT HA Solutions GmbH 3 | Uploaders: Roland Kammerer 4 | Section: python 5 | Priority: optional 6 | Build-Depends: bash-completion, 7 | debhelper (>= 9), 8 | dh-python, 9 | docbook-xsl, 10 | help2man, 11 | python3-all (>= 3.5), 12 | python3-setuptools, 13 | xsltproc 14 | Standards-Version: 3.9.6 15 | 16 | Package: linstor-client 17 | Architecture: all 18 | # keep python-natsort on its own line! 19 | Depends: ${misc:Depends}, 20 | python3-natsort, 21 | python3-setuptools, 22 | python-linstor (>=1.27.1), 23 | ${python3:Depends} 24 | Description: Linstor client command line tool 25 | This is a command client that communicates with the Linstor controller 26 | and gives a userfriendly command line syntax to add/remove/query 27 | Linstor objects 28 | . 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linstor Client 2 | 3 | This repository contains the user space client to ease DRBD9 resource management. 4 | 5 | Linstor, developed by [LINBIT](https://www.linbit.com), is a software that manages DRBD replicated 6 | LVM/ZFS volumes across a group of machines. It maintains DRBD configuration on the participating machines. It 7 | creates/deletes the backing LVM/ZFS volumes. It automatically places the backing LVM/ZFS volumes among the 8 | participating machines. 9 | 10 | # Using Linstor 11 | Please read the user-guide provided at [docs.linbit.com](https://docs.linbit.com). 12 | 13 | # Support 14 | For further products and professional support, please 15 | [contact](http://links.linbit.com/support) us. 16 | 17 | # Releases 18 | Releases generated by git tags on github are snapshots of the git repository at the given time. You most 19 | likely do not want to use these. They might lack things such as generated man pages, the `configure` script, 20 | and other generated files. If you want to build from a tarball, use the ones [provided by us](https://www.linbit.com/en/drbd-community/drbd-download/). 21 | -------------------------------------------------------------------------------- /tests/test_client_commands.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime, timedelta 3 | 4 | import linstor_client_main 5 | from linstor_client.commands import Commands 6 | from linstor_client.utils import LinstorClientError 7 | 8 | 9 | class TestClientCommands(unittest.TestCase): 10 | def test_main_commands(self): 11 | cli = linstor_client_main.LinStorCLI() 12 | cli.check_parser_commands() 13 | 14 | def _assert_parse_time_str(self, timestr, delta): 15 | dt_now = datetime.now() 16 | dt_now = dt_now.replace(microsecond=0) 17 | 18 | dt = Commands.parse_time_str(timestr) 19 | dt = dt.replace(microsecond=0) 20 | dt_diff = dt_now - delta 21 | self.assertEqual(dt_diff, dt) 22 | 23 | def test_parse_time_str(self): 24 | self._assert_parse_time_str("5d", timedelta(days=5)) 25 | self._assert_parse_time_str("3", timedelta(hours=3)) 26 | self._assert_parse_time_str("3h", timedelta(hours=3)) 27 | 28 | self.assertRaises(LinstorClientError, Commands.parse_time_str, "10m") 29 | self.assertRaises(LinstorClientError, Commands.parse_time_str, "") 30 | 31 | 32 | if __name__ == '__main__': 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /linstor_client/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .commands import DefaultState, Commands, MiscCommands, ArgumentError 2 | from .drbd_setup_cmds import DrbdOptions 3 | from .controller_cmds import ControllerCommands 4 | from .node_cmds import NodeCommands 5 | from .node_conn_cmds import NodeConnectionCommands 6 | from .rsc_dfn_cmds import ResourceDefinitionCommands 7 | from .rsc_grp_cmds import ResourceGroupCommands 8 | from .vlm_grp_cmds import VolumeGroupCommands 9 | from .storpool_cmds import StoragePoolCommands 10 | from .rsc_cmds import ResourceCommands 11 | from .rsc_conn_cmds import ResourceConnectionCommands 12 | from .vlm_cmds import VolumeCommands 13 | from .vlm_dfn_cmds import VolumeDefinitionCommands 14 | from .snapshot_cmds import SnapshotCommands 15 | from .drbd_proxy_cmds import DrbdProxyCommands 16 | from .migrate_cmds import MigrateCommands 17 | from .zsh_completer import ZshGenerator 18 | from .physical_storage_cmds import PhysicalStorageCommands 19 | from .error_report_cmds import ErrorReportCommands 20 | from .advise import AdviceCommands 21 | from .backup_cmds import BackupCommands 22 | from .remote_cmds import RemoteCommands 23 | from .file_cmds import FileCommands 24 | from .schedule import ScheduleCommands 25 | from .key_value_store import KeyValueStoreCommands 26 | -------------------------------------------------------------------------------- /linstor_client/argcomplete/_check_module.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | try: 5 | from importlib.util import find_spec 6 | except ImportError: 7 | from collections import namedtuple 8 | from imp import find_module 9 | 10 | ModuleSpec = namedtuple( 11 | 'ModuleSpec', ['origin', 'has_location', 'submodule_search_locations']) 12 | 13 | def find_spec(name): 14 | """Minimal implementation as required by `find`.""" 15 | f, path, _ = find_module(name) 16 | has_location = path is not None 17 | if f is None: 18 | return ModuleSpec(None, has_location, [path]) 19 | f.close() 20 | return ModuleSpec(path, has_location, None) 21 | 22 | 23 | def find(name): 24 | names = name.split('.') 25 | spec = find_spec(names[0]) 26 | if not spec.has_location: 27 | raise Exception('cannot locate file') 28 | if spec.submodule_search_locations is None: 29 | if len(names) != 1: 30 | raise Exception('{} is not a package'.format(names[0])) 31 | return spec.origin 32 | if len(spec.submodule_search_locations) != 1: 33 | raise Exception('expecting one search location') 34 | path = os.path.join(spec.submodule_search_locations[0], *names[1:]) 35 | if os.path.isdir(path): 36 | return os.path.join(path, '__main__.py') 37 | else: 38 | return path + '.py' 39 | 40 | 41 | def main(): 42 | with open(find(sys.argv[1])) as f: 43 | head = f.read(1024) 44 | if 'PYTHON_ARGCOMPLETE_OK' not in head: 45 | raise Exception('marker not found') 46 | 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /man-pages/linstor_trailer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Examples 6 | 7 | To define five nodes with the node names iron, silicon, carbon, copper and 8 | oxygen that have the IPv4 addresses 192.168.33.201 to 192.168.33.205: 9 | 10 | drbdmanage add-node iron 192.168.33.201 11 | drbdmanage add-node silicon 192.168.33.202 12 | drbdmanage add-node carbon 192.168.33.203 13 | drbdmanage add-node copper 192.168.33.204 14 | drbdmanage add-node oxygen 192.168.33.205 15 | 16 | 17 | To define a DRBD resource named "archive" with one 15 GiB volume 18 | and deploy this volume on three of the five nodes: 19 | 20 | drbdmanage add-volume archive 15 --deploy 3 21 | 22 | 23 | To define a DRBD resource named "files" that is replicated 24 | using network port number 10970 and one 840 MiB volume that uses minor 25 | number 70: 26 | 27 | drbdmanage add-resource -p 10970 files 28 | drbdmanage add-volume files 840MiB 29 | 30 | 31 | To manually deploy the resource "files" on nodes carbon 32 | and copper: 33 | 34 | drbdmanage assign-resource files carbon 35 | drbdmanage assign-resource files copper 36 | 37 | 38 | 39 | 40 | See Also 41 | 42 | 43 | drbd.conf 44 | 5, 45 | 46 | 47 | drbd 48 | 8, 49 | 50 | 51 | drbdadm 52 | 8 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /tests/test_ctrl_list_commands.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tests import LinstorTestCase 3 | 4 | 5 | class TestListCommands(LinstorTestCase): 6 | 7 | def test_nodes(self): 8 | jout = self.execute_with_machine_output(["node", "list"]) 9 | self.assertIsNotNone(jout) 10 | 11 | def test_nodes_text(self): 12 | text_out = self.execute_with_text_output(["node", "list"]) 13 | self.assertIn("Node", text_out) 14 | 15 | def test_resource_defs(self): 16 | jout = self.execute_with_machine_output(["resource-definition", "list"]) 17 | self.assertIsNotNone(jout) 18 | 19 | def test_resource_defs_text(self): 20 | text_out = self.execute_with_text_output(["resource-definition", "list"]) 21 | self.assertIn("ResourceName", text_out) 22 | 23 | def test_resources(self): 24 | jout = self.execute_with_machine_output(["resource", "list"]) 25 | self.assertIsNotNone(jout) 26 | 27 | def test_resources_text(self): 28 | text_out = self.execute_with_text_output(["resource", "list"]) 29 | self.assertIn("ResourceName", text_out) 30 | 31 | def test_volume_text(self): 32 | text_out = self.execute_with_text_output(["resource", "list-volumes"]) 33 | self.assertIn("VolNr", text_out) 34 | 35 | def test_storage_pools(self): 36 | jout = self.execute_with_machine_output(["storage-pool", "list"]) 37 | self.assertIsNotNone(jout) 38 | 39 | def test_storage_pools_text(self): 40 | text_out = self.execute_with_text_output(["storage-pool", "list"]) 41 | self.assertIn("StoragePool", text_out) 42 | 43 | def test_volume_definitions(self): 44 | jout = self.execute_with_machine_output(["volume-definition", "list"]) 45 | self.assertIsNotNone(jout) 46 | 47 | def test_volume_definitions_text(self): 48 | text_out = self.execute_with_text_output(["volume-definition", "list"]) 49 | self.assertIn("ResourceName", text_out) 50 | 51 | 52 | if __name__ == '__main__': 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /linstor_client/commands/utils/skip_disk_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from linstor_client.commands.commands import Commands 4 | from linstor_client.consts import Color 5 | from linstor_client.utils import Output 6 | 7 | 8 | def get_skip_disk_state_str(rsc): 9 | occurrences = [] 10 | # originally we checked for "if not linstor_api.api_version_smaller('1.20.2')" 11 | # but this method needs to be called from "r l", "v l" and also from "r lv" 12 | # "r lv" requires "v l"'s show_volumes to be a classmethod. Such a classmethod 13 | # however cannot get an instance of linstor_api. That makes the original check 14 | # hard to achieve. 15 | # as a workaround we simply check if the effective_properties exist or not... 16 | if hasattr(rsc, "effective_properties"): 17 | if "DrbdOptions/SkipDisk" in rsc.effective_properties: 18 | skip_disk_eff_prop = rsc.effective_properties["DrbdOptions/SkipDisk"] 19 | if skip_disk_eff_prop.value == "True": 20 | occurrences = [Commands.EFFECTIVE_PROPS_TYPES[skip_disk_eff_prop.type]] 21 | if skip_disk_eff_prop.other: 22 | occurrences += [Commands.EFFECTIVE_PROPS_TYPES[other.type] 23 | for other in skip_disk_eff_prop.other] 24 | return ", SkipDisk (" + ', '.join(occurrences) + ")" if occurrences else "" 25 | 26 | 27 | def print_skip_disk_info(no_color): 28 | print(Output.color_str("SkipDisk", Color.YELLOW, no_color) + ":") 29 | print(" At least one resource has 'DrbdOptions/SkipDisk' enabled. This indicates an IO error on the") 30 | print(" affected resource(s). Remove this property (using " 31 | "'linstor resource set-property $node $rsc DrbdOptions/SkipDisk') ") 32 | print(" to instruct LINSTOR and DRBD to adjust (and recreate if necessary) the affected logical volumes " 33 | "again.") 34 | print(" For more information please visit: " 35 | "https://linbit.com/drbd-user-guide/linstor-guide-1_0-en/#s-linstor-drbd-skip-disk") 36 | -------------------------------------------------------------------------------- /linstor_client/consts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | """ 3 | LINSTOR - management of distributed storage/DRBD9 resources 4 | Copyright (C) 2013 - 2017 LINBIT HA-Solutions GmbH 5 | Author: Robert. Altnoeder, Roland Kammerer 6 | 7 | You can use this file under the terms of the GNU Lesser General 8 | Public License as as published by the Free Software Foundation, 9 | either version 3 of the License, or (at your option) any later 10 | version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU Lesser General Public License for more details. 16 | 17 | See . 18 | """ 19 | 20 | """ 21 | Global constants for linstor client 22 | """ 23 | 24 | VERSION = "1.27.1" 25 | 26 | try: 27 | from linstor_client.consts_githash import GITHASH 28 | except ImportError: 29 | GITHASH = 'GIT-hash: UNKNOWN' 30 | 31 | # Default terminal dimensions 32 | # Used by get_terminal_size() 33 | DEFAULT_TERM_WIDTH, DEFAULT_TERM_HEIGHT = 80, 25 34 | 35 | FILE_GLOBAL_COMMON_CONF = "linstor_global_common.conf" 36 | 37 | # boolean expressions 38 | BOOL_TRUE = "true" 39 | BOOL_FALSE = "false" 40 | 41 | KEY_LS_CONTROLLERS = 'LS_CONTROLLERS' 42 | ENV_OUTPUT_VERSION = "LS_OUTPUT_VERSION" 43 | 44 | 45 | class ExitCode(object): 46 | OK = 0 47 | UNKNOWN_ERROR = 1 48 | ARGPARSE_ERROR = 2 49 | OBJECT_NOT_FOUND = 3 50 | OPTION_NOT_SUPPORTED = 4 51 | ILLEGAL_STATE = 5 52 | CONNECTION_ERROR = 20 53 | CONNECTION_TIMEOUT = 21 54 | UNEXPECTED_REPLY = 22 55 | API_ERROR = 10 56 | NO_SATELLITE_CONNECTION = 11 57 | 58 | 59 | class Color(object): 60 | # Do not reorder 61 | (BLACK, 62 | DARKRED, 63 | DARKGREEN, 64 | BROWN, 65 | DARKBLUE, 66 | DARKPINK, 67 | TEAL, 68 | GRAY, 69 | DARKGRAY, 70 | RED, 71 | GREEN, 72 | YELLOW, 73 | BLUE, 74 | PINK, 75 | TURQUOIS, 76 | WHITE, 77 | NONE) = [chr(0x1b) + "[%d;%dm" % (i, j) for i in range(2) for j in range(30, 38)] + [chr(0x1b) + "[0m"] 78 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILDER=registry.access.redhat.com/ubi8/ubi 2 | FROM $BUILDER as builder 3 | 4 | ENV LINSTOR_CLI_VERSION 1.27.1 5 | ENV PYTHON_LINSTOR_VERSION 1.27.1 6 | 7 | ENV LINSTOR_CLI_PKGNAME linstor-client 8 | ENV LINSTOR_CLI_TGZ ${LINSTOR_CLI_PKGNAME}-${LINSTOR_CLI_VERSION}.tar.gz 9 | ENV PYTHON_LINSTOR_PKGNAME python-linstor 10 | ENV PYTHON_LINSTOR_TGZ ${PYTHON_LINSTOR_PKGNAME}-${PYTHON_LINSTOR_VERSION}.tar.gz 11 | 12 | USER root 13 | RUN yum -y update-minimal --security --sec-severity=Important --sec-severity=Critical 14 | RUN groupadd makepkg # !lbbuild 15 | RUN useradd -m -g makepkg makepkg # !lbbuild 16 | 17 | RUN yum install -y sudo # !lbbuild 18 | RUN usermod -a -G wheel makepkg # !lbbuild 19 | 20 | RUN yum install -y rpm-build python3 python3-setuptools make && yum clean all -y # !lbbuild 21 | 22 | # one can not comment COPY 23 | RUN cd /tmp && curl -sSf https://pkg.linbit.com/downloads/linstor/$PYTHON_LINSTOR_TGZ > $PYTHON_LINSTOR_TGZ # !lbbuild 24 | RUN cd /tmp && curl -sSf https://pkg.linbit.com/downloads/linstor/$LINSTOR_CLI_TGZ > $LINSTOR_CLI_TGZ # !lbbuild 25 | # =lbbuild COPY /dist/${PYTHON_LINSTOR_TGZ} /tmp/ 26 | # =lbbuild COPY /dist/${LINSTOR_TGZ} /tmp/ 27 | 28 | USER makepkg 29 | RUN cd ${HOME} && \ 30 | cp /tmp/${PYTHON_LINSTOR_TGZ} ${HOME} && \ 31 | tar xvf ${PYTHON_LINSTOR_TGZ} && \ 32 | cd ${PYTHON_LINSTOR_PKGNAME}-${PYTHON_LINSTOR_VERSION} && \ 33 | make gensrc && \ 34 | make rpm && mv ./dist/*.rpm /tmp/ 35 | RUN cd ${HOME} && \ 36 | cp /tmp/${LINSTOR_CLI_TGZ} ${HOME} && \ 37 | tar xvf ${LINSTOR_CLI_TGZ} && \ 38 | cd ${LINSTOR_CLI_PKGNAME}-${LINSTOR_CLI_VERSION} && \ 39 | make rpm && mv ./dist/*.rpm /tmp/ 40 | 41 | FROM registry.access.redhat.com/ubi8/ubi 42 | MAINTAINER Roland Kammerer 43 | 44 | # ENV can not be shared between builder and "main" 45 | ENV LINSTOR_CLI_VERSION 1.27.1 46 | ARG release=1 47 | 48 | LABEL name="linstor-client" \ 49 | vendor="LINBIT" \ 50 | version="$LINSTOR_CLI_VERSION" \ 51 | release="$release" \ 52 | summary="LINSTOR's client component" \ 53 | description="LINSTOR's client component" 54 | 55 | COPY COPYING /licenses/gpl-3.0.txt 56 | 57 | COPY --from=builder /tmp/*.noarch.rpm /tmp/ 58 | RUN yum -y update-minimal --security --sec-severity=Important --sec-severity=Critical && \ 59 | yum install -y /tmp/python-linstor-*.rpm /tmp/linstor-client*.rpm && yum clean all -y 60 | 61 | RUN groupadd linstor 62 | RUN useradd -m -g linstor linstor 63 | 64 | USER linstor 65 | ENTRYPOINT ["linstor"] 66 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: linstor 3 | Source: https://www.linbit.com 4 | 5 | Files: * 6 | Copyright: Copyright (C) 2013 - 2017, LINBIT HA-Solutions GmbH 7 | License: GPL-2+ 8 | This program is free software; you can redistribute it 9 | and/or modify it under the terms of the GNU General Public 10 | License as published by the Free Software Foundation; either 11 | version 2 of the License, or (at your option) any later 12 | version. 13 | . 14 | This program is distributed in the hope that it will be 15 | useful, but WITHOUT ANY WARRANTY; without even the implied 16 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 17 | PURPOSE. See the GNU General Public License for more 18 | details. 19 | . 20 | You should have received a copy of the GNU General Public 21 | License along with this package; if not, write to the Free 22 | Software Foundation, Inc., 51 Franklin St, Fifth Floor, 23 | Boston, MA 02110-1301 USA 24 | . 25 | On Debian systems, the full text of the GNU General Public 26 | License version 2 can be found in the file 27 | `/usr/share/common-licenses/GPL-3'. 28 | 29 | Files: debian/* 30 | Copyright: Copyright (C) 2014 - 2017, LINBIT HA-Solutions GmbH 31 | Copyright (C) 2014, Martin Loschwitz 32 | License: GPL-2+ 33 | This program is free software; you can redistribute it 34 | and/or modify it under the terms of the GNU General Public 35 | License as published by the Free Software Foundation; either 36 | version 2 of the License, or (at your option) any later 37 | version. 38 | . 39 | This program is distributed in the hope that it will be 40 | useful, but WITHOUT ANY WARRANTY; without even the implied 41 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 42 | PURPOSE. See the GNU General Public License for more 43 | details. 44 | . 45 | You should have received a copy of the GNU General Public 46 | License along with this package; if not, write to the Free 47 | Software Foundation, Inc., 51 Franklin St, Fifth Floor, 48 | Boston, MA 02110-1301 USA 49 | . 50 | On Debian systems, the full text of the GNU General Public 51 | License version 2 can be found in the file 52 | `/usr/share/common-licenses/GPL-2'. 53 | 54 | Files: 55 | linstor/consts.py 56 | linstor/sharedconsts.py 57 | linstor/utils.py 58 | Copyright: Copyright (C) 2014 - 2017, LINBIT HA-Solutions GmbH 59 | License: LGPL-3+ 60 | You can use this file under the terms of the GNU Lesser General 61 | Public License as as published by the Free Software Foundation, 62 | either version 3 of the License, or (at your option) any later 63 | version. 64 | . 65 | This program is distributed in the hope that it will be useful, 66 | but WITHOUT ANY WARRANTY; without even the implied warranty of 67 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 68 | GNU Lesser General Public License for more details. 69 | . 70 | On Debian systems, the full text of the GNU General Public 71 | License version 2 can be found in the file 72 | "/usr/share/common-licenses/GPL-3". 73 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GIT = git 2 | INSTALLFILES=.installfiles 3 | PYTHON ?= python3 4 | LINSTORAPI = ../linstor-api-py 5 | override GITHEAD := $(shell test -e .git && $(GIT) rev-parse HEAD) 6 | 7 | U := $(shell $(PYTHON) ./setup.py versionup2date >/dev/null 2>&1; echo $$?;) 8 | TESTS = $(wildcard unit-tests/*_test.py) 9 | DOCKERREGISTRY := drbd.io 10 | ARCH ?= amd64 11 | ifneq ($(strip $(ARCH)),) 12 | DOCKERREGISTRY := $(DOCKERREGISTRY)/$(ARCH) 13 | endif 14 | DOCKERREGPATH = $(DOCKERREGISTRY)/linstor-client 15 | DOCKER_TAG ?= latest 16 | NO_DOC ?= 17 | 18 | all: doc 19 | $(PYTHON) setup.py build 20 | 21 | doc: 22 | PYTHONPATH=$(LINSTORAPI):. $(PYTHON) setup.py build_man 23 | 24 | install: 25 | $(PYTHON) setup.py install --record $(INSTALLFILES) 26 | 27 | uninstall: 28 | test -f $(INSTALLFILES) && cat $(INSTALLFILES) | xargs rm -rf || true 29 | rm -f $(INSTALLFILES) 30 | 31 | ifneq ($(U),0) 32 | up2date: 33 | $(error "Update your Version strings/Changelogs") 34 | else 35 | up2date: linstor_client/consts_githash.py 36 | $(info "Version strings/Changelogs up to date") 37 | endif 38 | 39 | release: doc 40 | make release-no-doc 41 | 42 | release-no-doc: up2date clean 43 | $(PYTHON) setup.py sdist 44 | @echo && echo "Did you run distclean?" 45 | 46 | debrelease: 47 | echo 'recursive-include debian *' >> MANIFEST.in 48 | dh_clean || true 49 | make release$(NO_DOC) 50 | git checkout MANIFEST.in 51 | 52 | ifneq ($(FORCE),1) 53 | dockerimage: debrelease 54 | cd $(LINSTORAPI) && make debrelease 55 | cp $(LINSTORAPI)/dist/*.tar.gz dist/ 56 | else 57 | dockerimage: 58 | endif 59 | docker build -t $(DOCKERREGPATH):$(DOCKER_TAG) $(EXTRA_DOCKER_BUILDARGS) . 60 | docker tag $(DOCKERREGPATH):$(DOCKER_TAG) $(DOCKERREGPATH):latest 61 | @echo && echo "Did you run distclean?" 62 | 63 | .PHONY: dockerpath 64 | dockerpath: 65 | @echo $(DOCKERREGPATH):latest $(DOCKERREGPATH):$(DOCKER_TAG) 66 | 67 | # no gensrc here, that is in debian/rules 68 | deb: up2date 69 | [ -d ./debian ] || (echo "Your checkout/tarball does not contain a debian directory" && false) 70 | debuild -i -us -uc -b 71 | 72 | # it is up to you (or the buildenv) to provide a distri specific setup.cfg 73 | rpm: up2date 74 | $(PYTHON) setup.py bdist_rpm --python /usr/bin/$(PYTHON) 75 | 76 | .PHONY: linstor_client/consts_githash.py 77 | ifdef GITHEAD 78 | override GITDIFF := $(shell $(GIT) diff --name-only HEAD 2>/dev/null | \ 79 | grep -vxF "MANIFEST.in" | \ 80 | tr -s '\t\n' ' ' | \ 81 | sed -e 's/^/ /;s/ *$$//') 82 | linstor_client/consts_githash.py: 83 | @echo "GITHASH = 'GIT-hash: $(GITHEAD)$(GITDIFF)'" > $@ 84 | else 85 | linstor_client/consts_githash.py: 86 | @echo >&2 "Need a git checkout to regenerate $@"; test -s $@ 87 | endif 88 | 89 | md5sums: 90 | CURDATE=$$(date +%s); for i in $$(${GIT} ls-files | sort); do md5sum $$i >> md5sums.$${CURDATE}; done 91 | 92 | clean: 93 | $(PYTHON) setup.py clean 94 | rm -f man-pages/*.gz 95 | 96 | distclean: clean 97 | git clean -d -f || true 98 | 99 | check: 100 | # currently none 101 | # $(PYTHON) $(TESTS) 102 | -------------------------------------------------------------------------------- /man-pages/linstor_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1 November 2017 6 | linstor 7 | 1.0 8 | 9 | 10 | linstor 11 | 8 12 | System Administration 13 | 14 | 15 | linstor 16 | Distributed configuration management for DRBD 17 | linstor 18 | 19 | 20 | 21 | 22 | 23 | Description 24 | is a utility for the simplified 25 | and automated administration of resources on a multiple-node DRBD 9 26 | cluster. It accomplishes this task by running a local server process on 27 | each member node of a linstor domain. As these local servers communicate 28 | with servers on other nodes, administrators can use any member node to 29 | control resources on all nodes of a linstor domain. 30 | 31 | 32 | 33 | 34 | Quick overview of linstor basics 35 | 36 | The objects that linstor works with are , 37 | , 38 | and . linstor keeps records describing these 39 | objects in its data tables. 40 | A record defines a member node of a linstor domain. 41 | A record defines a DRBD resource for use with 42 | linstor, and records define the DRBD volumes 43 | of a . can then be 44 | deployed on a number of , thereby implicitly creating 45 | a number of associated records. 46 | 47 | 48 | 49 | 50 | Configuration 51 | The configuration of is stored in the 52 | of linstor. This has the advantage that 53 | the configuration is distributed among all nodes in the cluster without 54 | touching every node's configuration file manually. Please see 55 | for further details. 56 | In rare situations it might be nessessary to provide an initial 57 | configuration (e.g., if the linstor control volume itself is stored on a 58 | non-standard LVM volume group). For these situations it is possible to 59 | provide a local configuration file, namely . 60 | This file has to provide a section. Values allowed 61 | in this section are: and 62 | . It is important to note that providing a 63 | local configuration file is the exception and should only be used in very 64 | rare situations. 65 | 66 | 67 | 68 | 69 | Commands 70 | 71 | -------------------------------------------------------------------------------- /linstor_client/tree.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | from linstor_client.consts import Color 4 | import locale 5 | 6 | 7 | class TreeFormatter: 8 | TREE_DRAWING_TABLE = { 9 | 'utf8': { 10 | 'connector_continue': u' │', 11 | 'connector_end': u' ', 12 | 'child_marker_continue': u' ├───', 13 | 'child_marker_end': u' └───' 14 | }, 15 | 'ascii': { 16 | 'connector_continue': ' |', 17 | 'connector_end': ' ', 18 | 'child_marker_continue': ' |---', 19 | 'child_marker_end': ' +---' 20 | } 21 | } 22 | 23 | def __init__(self, no_utf8, no_color): 24 | enc = 'ascii' 25 | if not no_utf8: 26 | locales = locale.getdefaultlocale() 27 | if len(locales) > 1 and locales[1] and isinstance(locales[1], str) and locales[1].lower() == 'utf-8': 28 | enc = 'utf8' 29 | 30 | self.tree_drawing_strings = TreeFormatter.TREE_DRAWING_TABLE[enc] 31 | self.no_color = no_color 32 | 33 | def apply_color(self, text, color): 34 | return text if self.no_color else color + text + Color.NONE 35 | 36 | def get_drawing_string(self, key): 37 | return self.tree_drawing_strings[key] 38 | 39 | 40 | class TreeNode: 41 | def __init__(self, name, description, color): 42 | """ 43 | Creates a new TreeNode object 44 | 45 | :param str name: name of the node 46 | :param str description: description of the node 47 | :param color: color for the node name 48 | """ 49 | 50 | self.name = name 51 | self.description = description 52 | self.color = color 53 | self.child_list = [] 54 | 55 | def print_node(self, no_utf8, no_color): 56 | self.print_node_in_tree("", "", "", TreeFormatter(no_utf8, no_color)) 57 | 58 | def print_node_in_tree(self, connector, element_marker, child_prefix, formatter): 59 | if connector: 60 | print(connector) 61 | 62 | print(element_marker + formatter.apply_color(self.name, self.color) + ' (' + self.description + ')') 63 | 64 | for child_node in self.child_list[:-1]: 65 | child_node.print_node_in_tree( 66 | child_prefix + formatter.get_drawing_string('connector_continue'), 67 | child_prefix + formatter.get_drawing_string('child_marker_continue'), 68 | child_prefix + formatter.get_drawing_string('connector_continue'), 69 | formatter 70 | ) 71 | 72 | for child_node in self.child_list[-1:]: 73 | child_node.print_node_in_tree( 74 | child_prefix + formatter.get_drawing_string('connector_continue'), 75 | child_prefix + formatter.get_drawing_string('child_marker_end'), 76 | child_prefix + formatter.get_drawing_string('connector_end'), 77 | formatter 78 | ) 79 | 80 | def add_child(self, child): 81 | self.child_list.append(child) 82 | 83 | def find_child(self, name): 84 | for child in self.child_list: 85 | if child.name == name: 86 | return child 87 | return None 88 | 89 | def set_description(self, description): 90 | self.description = description 91 | 92 | def add_description(self, description): 93 | self.description += description 94 | 95 | def to_data(self): 96 | return { 97 | 'name': self.name, 98 | 'description': self.description, 99 | 'children': [x.to_data() for x in self.child_list] 100 | } 101 | 102 | def __repr__(self): 103 | return "TreeNode({n}, {d})".format(n=self.name, d=self.description) 104 | -------------------------------------------------------------------------------- /tests/test_tables.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from linstor_client import TableHeader, Table 3 | from linstor_client.consts import Color 4 | 5 | 6 | class TestUtils(unittest.TestCase): 7 | 8 | @unittest.skip("jenkins is not happy about the color codes") 9 | def test_cell_color(self): 10 | tbl = Table(colors=True, utf8=False) 11 | tbl.add_header(TableHeader("FirstName")) 12 | tbl.add_header(TableHeader("LastName")) 13 | tbl.add_header(TableHeader("Age")) 14 | tbl.add_header(TableHeader("Comment")) 15 | 16 | tbl.add_row(["Max", "Mustermann", tbl.color_cell("62", Color.RED), ""]) 17 | tbl.add_row(["Heinrich", "Mueller", "29", ""]) 18 | tbl.show() 19 | 20 | def test_row_expand(self): 21 | multirow = Table._row_expand( 22 | [ 23 | "column1_line1\ncolumn1_line2", 24 | "column2_line1", 25 | "column3_line1\ncolumn3_line2\ncolumn3_line3" 26 | ] 27 | ) 28 | self.assertListEqual( 29 | [ 30 | ["column1_line1", "column2_line1", "column3_line1"], 31 | ["column1_line2", "", "column3_line2"], 32 | ["", "", "column3_line3"] 33 | ], 34 | multirow 35 | ) 36 | 37 | def test_multiline_colums(self): 38 | tbl = Table() 39 | tbl.add_header(TableHeader("id")) 40 | tbl.add_header(TableHeader("description")) 41 | tbl.add_header(TableHeader("text")) 42 | 43 | tbl.add_row([ 44 | "0", 45 | "In a land far far away in a time long long ago\nThere were 3 pigs with 3 wigs and a chair to despair\n" 46 | "in a house with no mouse.", 47 | "PlaceCount: 2\nDisklessOnRemaining: True\nStoragePool: DfltStorPool\nLayerList: storage,drbd"] 48 | ) 49 | table_out = tbl.show() 50 | 51 | self.assertEqual( 52 | """+---------------------------------------------------------------------------------------+ 53 | | id | description | text | 54 | |=======================================================================================| 55 | | 0 | In a land far far away in a time long long ago | PlaceCount: 2 | 56 | | | There were 3 pigs with 3 wigs and a chair to despair | DisklessOnRemaining: True | 57 | | | in a house with no mouse. | StoragePool: DfltStorPool | 58 | | | | LayerList: storage,drbd | 59 | +---------------------------------------------------------------------------------------+ 60 | """, 61 | table_out 62 | ) 63 | 64 | tbl = Table() 65 | tbl.add_header(TableHeader("id")) 66 | tbl.add_header(TableHeader("vlmgroups")) 67 | tbl.add_header(TableHeader("text")) 68 | tbl.add_header(TableHeader("description")) 69 | 70 | tbl.add_row([ 71 | "DfltRscGrp", 72 | "", 73 | "", 74 | "" 75 | ]) 76 | tbl.add_row([ 77 | "testrg", 78 | "0", 79 | "PlaceCount: 2\nStoragePool: DfltStorPool", 80 | "bla" 81 | ]) 82 | 83 | table_out = tbl.show() 84 | 85 | self.assertEqual( 86 | """+------------------------------------------------------------------+ 87 | | id | vlmgroups | text | description | 88 | |==================================================================| 89 | | DfltRscGrp | | | | 90 | |------------------------------------------------------------------| 91 | | testrg | 0 | PlaceCount: 2 | bla | 92 | | | | StoragePool: DfltStorPool | | 93 | +------------------------------------------------------------------+ 94 | """, 95 | table_out 96 | ) 97 | -------------------------------------------------------------------------------- /linstor_client/argcomplete/completers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2013, Andrey Kislyuk and argcomplete contributors. 2 | # Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info. 3 | 4 | from __future__ import absolute_import, division, print_function, unicode_literals 5 | 6 | import os 7 | import subprocess 8 | from .compat import str, sys_encoding 9 | 10 | def _call(*args, **kwargs): 11 | try: 12 | return subprocess.check_output(*args, **kwargs).decode(sys_encoding).splitlines() 13 | except subprocess.CalledProcessError: 14 | return [] 15 | 16 | class ChoicesCompleter(object): 17 | def __init__(self, choices): 18 | self.choices = choices 19 | 20 | def _convert(self, choice): 21 | if isinstance(choice, bytes): 22 | choice = choice.decode(sys_encoding) 23 | if not isinstance(choice, str): 24 | choice = str(choice) 25 | return choice 26 | 27 | def __call__(self, **kwargs): 28 | return (self._convert(c) for c in self.choices) 29 | 30 | EnvironCompleter = ChoicesCompleter(os.environ) 31 | 32 | class FilesCompleter(object): 33 | """ 34 | File completer class, optionally takes a list of allowed extensions 35 | """ 36 | 37 | def __init__(self, allowednames=(), directories=True): 38 | # Fix if someone passes in a string instead of a list 39 | if isinstance(allowednames, (str, bytes)): 40 | allowednames = [allowednames] 41 | 42 | self.allowednames = [x.lstrip("*").lstrip(".") for x in allowednames] 43 | self.directories = directories 44 | 45 | def __call__(self, prefix, **kwargs): 46 | completion = [] 47 | if self.allowednames: 48 | if self.directories: 49 | files = _call(["bash", "-c", "compgen -A directory -- '{p}'".format(p=prefix)]) 50 | completion += [f + "/" for f in files] 51 | for x in self.allowednames: 52 | completion += _call(["bash", "-c", "compgen -A file -X '!*.{0}' -- '{p}'".format(x, p=prefix)]) 53 | else: 54 | completion += _call(["bash", "-c", "compgen -A file -- '{p}'".format(p=prefix)]) 55 | anticomp = _call(["bash", "-c", "compgen -A directory -- '{p}'".format(p=prefix)]) 56 | completion = list(set(completion) - set(anticomp)) 57 | 58 | if self.directories: 59 | completion += [f + "/" for f in anticomp] 60 | return completion 61 | 62 | class _FilteredFilesCompleter(object): 63 | def __init__(self, predicate): 64 | """ 65 | Create the completer 66 | 67 | A predicate accepts as its only argument a candidate path and either 68 | accepts it or rejects it. 69 | """ 70 | assert predicate, "Expected a callable predicate" 71 | self.predicate = predicate 72 | 73 | def __call__(self, prefix, **kwargs): 74 | """ 75 | Provide completions on prefix 76 | """ 77 | target_dir = os.path.dirname(prefix) 78 | try: 79 | names = os.listdir(target_dir or ".") 80 | except: 81 | return # empty iterator 82 | incomplete_part = os.path.basename(prefix) 83 | # Iterate on target_dir entries and filter on given predicate 84 | for name in names: 85 | if not name.startswith(incomplete_part): 86 | continue 87 | candidate = os.path.join(target_dir, name) 88 | if not self.predicate(candidate): 89 | continue 90 | yield candidate + "/" if os.path.isdir(candidate) else candidate 91 | 92 | class DirectoriesCompleter(_FilteredFilesCompleter): 93 | def __init__(self): 94 | _FilteredFilesCompleter.__init__(self, predicate=os.path.isdir) 95 | 96 | class SuppressCompleter(object): 97 | """ 98 | A completer used to suppress the completion of specific arguments 99 | """ 100 | 101 | def __init__(self): 102 | pass 103 | 104 | def suppress(self): 105 | """ 106 | Decide if the completion should be suppressed 107 | """ 108 | return True 109 | -------------------------------------------------------------------------------- /tests/test_ctrl_props.py: -------------------------------------------------------------------------------- 1 | from tests import LinstorTestCase 2 | from linstor.sharedconsts import NAMESPC_AUXILIARY 3 | 4 | 5 | class TestProperties(LinstorTestCase): 6 | 7 | def test_set_properties(self): 8 | # create all object kinds 9 | cnode_resp = self.execute_with_resp(['node', 'create', 'node1', '192.168.100.1']) 10 | self.assert_api_succuess(cnode_resp[0]) 11 | 12 | # create resource def 13 | rsc_dfn_resp = self.execute_with_single_resp(['resource-definition', 'create', 'rsc1']) 14 | self.assert_api_succuess(rsc_dfn_resp) 15 | 16 | # create volume def 17 | vlm_dfn_resp = self.execute_with_single_resp(['volume-definition', 'create', 'rsc1', '1Gib']) 18 | self.assert_api_succuess(vlm_dfn_resp) 19 | 20 | # create resource on node1 21 | # rsc_resps = self.execute_with_resp(['resource', 'create', '-s', 'storage', 'node1', 'rsc1']) 22 | # self.assertEqual(3, len(rsc_resps)) 23 | # self.assertTrue(rsc_resps[0].is_success()) # resource created 24 | # self.assertTrue(rsc_resps[1].is_success()) # volume created 25 | # self.assertTrue(rsc_resps[2].is_warning()) # satellite not reachable 26 | 27 | # start prop tests 28 | node_resp = self.execute_with_resp( 29 | ['node', 'set-property', 'node1', '--aux', 'test_prop', 'val'] 30 | ) 31 | self.assert_apis_success(node_resp) 32 | 33 | node_props = self.execute_with_machine_output(['node', 'list-properties', 'node1']) 34 | self.assertEqual(1, len(node_props)) 35 | node_props = node_props[0] 36 | self.assertEqual(2, len(node_props)) 37 | prop = self.find_prop(node_props, NAMESPC_AUXILIARY + '/test_prop') 38 | self.check_prop(prop, NAMESPC_AUXILIARY + '/test_prop', 'val') 39 | 40 | node_resp = self.execute_with_resp( 41 | ['node', 'set-property', 'node1', '--aux', 'another_prop', 'value with spaces'] 42 | ) 43 | self.assert_apis_success(node_resp) 44 | 45 | node_props = self.execute_with_machine_output(['node', 'list-properties', 'node1']) 46 | self.assertEqual(1, len(node_props)) 47 | node_props = node_props[0] 48 | self.assertEqual(3, len(node_props)) 49 | prop = self.find_prop(node_props, NAMESPC_AUXILIARY + '/test_prop') 50 | self.check_prop(prop, NAMESPC_AUXILIARY + '/test_prop', 'val') 51 | 52 | prop = self.find_prop(node_props, NAMESPC_AUXILIARY + '/another_prop') 53 | self.check_prop(prop, NAMESPC_AUXILIARY + '/another_prop', 'value with spaces') 54 | 55 | # resource definition 56 | resourcedef_resp = self.execute_with_resp( 57 | ['resource-definition', 'set-property', 'rsc1', '--aux', 'user', 'alexa'] 58 | ) 59 | print(resourcedef_resp) 60 | self.assert_apis_success(resourcedef_resp) 61 | 62 | resourcedef_props = self.execute_with_machine_output(['resource-definition', 'list-properties', 'rsc1']) 63 | self.assertEqual(1, len(resourcedef_props)) 64 | resourcedef_props = resourcedef_props[0] 65 | self.assertEqual(1, len(resourcedef_props)) 66 | prop = self.find_prop(resourcedef_props, NAMESPC_AUXILIARY + '/user') 67 | self.check_prop(prop, NAMESPC_AUXILIARY + '/user', 'alexa') 68 | 69 | # volume definition 70 | volumedef_resp = self.execute_with_resp( 71 | ['volume-definition', 'set-property', 'rsc1', '0', '--aux', 'volumespec', 'cascading'] 72 | ) 73 | self.assertEqual(2, len(volumedef_resp)) 74 | 75 | volumedef_props = self.execute_with_machine_output(['volume-definition', 'list-properties', 'rsc1', '0']) 76 | self.assertEqual(1, len(volumedef_props)) 77 | volumedef_props = volumedef_props[0] 78 | self.assertEqual(2, len(volumedef_props)) 79 | prop = self.find_prop(volumedef_props, NAMESPC_AUXILIARY + '/volumespec') 80 | self.check_prop(prop, NAMESPC_AUXILIARY + '/volumespec', 'cascading') 81 | 82 | # resource 83 | # resource_props = self.execute_with_machine_output(['resource', 'list-properties', 'node1', 'rsc1']) 84 | # self.assertEqual(1, len(resource_props)) 85 | # resource_props = resource_props[0] 86 | # self.assertEqual(2, len(resource_props)) 87 | # prop = self.find_prop(resource_props, KEY_STOR_POOL_NAME) 88 | # self.check_prop(prop, KEY_STOR_POOL_NAME, 'storage') 89 | 90 | # storage_resp = self.execute_with_resp( 91 | # ['resource', 'set-property', 'node1', 'rsc1', '--aux', 'NIC', '10.0.0.1'] 92 | # ) 93 | # self.assertEqual(2, len(storage_resp)) 94 | # self.assertTrue(storage_resp[0].is_warning()) 95 | # self.assertTrue(storage_resp[1].is_success()) 96 | 97 | # resource_props = self.execute_with_machine_output(['resource', 'list-properties', 'node1', 'rsc1']) 98 | # self.assertEqual(1, len(resource_props)) 99 | # resource_props = resource_props[0] 100 | # self.assertEqual(3, len(resource_props)) 101 | # prop = self.find_prop(resource_props, KEY_STOR_POOL_NAME) 102 | # self.check_prop(prop, KEY_STOR_POOL_NAME, 'storage') 103 | # prop = self.find_prop(resource_props, NAMESPC_AUXILIARY + '/NIC') 104 | # self.check_prop(prop, NAMESPC_AUXILIARY + '/NIC', '10.0.0.1') 105 | -------------------------------------------------------------------------------- /linstor_client/commands/zsh_completer.py: -------------------------------------------------------------------------------- 1 | from .commands import Commands 2 | 3 | _header = """#compdef linstor_client_main.py linstor 4 | #autoload 5 | 6 | # ------------------------------------------------------------------------------ 7 | # Description 8 | # ----------- 9 | # 10 | # Completion script for linstor-client 11 | # 12 | # ------------------------------------------------------------------------------ 13 | # Authors 14 | # ------- 15 | # 16 | # * Rene Peinthor 17 | # 18 | # ------------------------------------------------------------------------------ 19 | 20 | _linstor() { 21 | local context curcontext="$curcontext" state line 22 | typeset -A opt_args 23 | 24 | local ret=1 25 | 26 | _arguments -C '--no-utf8[No UTF8 characters in output]' '--no-color[Do not use color output]' \ 27 | '--machine-readable[Output in json format]' '--version[Show version]' '--disable-config[Disable config loading]' \ 28 | '--help[Show help]' '--timeout[Timeout in seconds]: :()' '--controllers[list of controllers to try]: :()' \ 29 | '1: :_linstor_cmds' \ 30 | '*::arg:->args' \ 31 | && ret=0 32 | 33 | case $state in 34 | (args) 35 | curcontext="${curcontext%:*:*}:linstor-cmd-$words[1]:" 36 | case $line[1] in 37 | """ 38 | 39 | _mid = """ *) 40 | _call_function ret _linstor_cmd_$words[1] && ret=0 41 | (( ret )) && _message 'no more arguments' 42 | ;; 43 | esac 44 | ;; 45 | esac 46 | } 47 | 48 | (( $+functions[_linstor_cmds] )) || 49 | _linstor_cmds() { 50 | local commands; commands=(""" 51 | 52 | _footer = """ ) 53 | _describe -t commands 'linstor command' commands "$@" 54 | } 55 | 56 | _linstor "$@" 57 | 58 | # Local Variables: 59 | # mode: Shell-Script 60 | # sh-indentation: 2 61 | # indent-tabs-mode: nil 62 | # sh-basic-offset: 2 63 | # End: 64 | # vim: ft=zsh sw=2 ts=2 et 65 | """ 66 | 67 | 68 | class ZshGenerator(object): 69 | def __init__(self, parser): 70 | self._parser = parser 71 | 72 | def cmd_completer(self, args): 73 | print(_header) 74 | for cmd in Commands.MainList: 75 | print(self.cmd(cmd)) 76 | print(_mid) 77 | print(self.cmds_list_str()) 78 | print(_footer) 79 | 80 | def describe_cmds(self, cmd, indent=0): 81 | argparse_cmd = self._parser._name_parser_map[cmd] 82 | safe_str = cmd.replace('-', '_') 83 | c = " " * indent + "local {cmd}_cmds;\n".format(cmd=safe_str) 84 | c += " " * indent + "{cmd}_cmds=(\n".format(cmd=safe_str) 85 | for action in argparse_cmd._actions: 86 | subcmds = action.choices if action.choices else [] 87 | for subcmd in subcmds: 88 | c += " " * indent + " '{subcmd}:'\n".format(subcmd=subcmd) 89 | c += " " * indent + ")\n" 90 | c += " " * indent + "_describe -t {cmd}_cmds '{cmd} cmds' {cmd}_cmds \"$@\" && ret=0\n".format(cmd=safe_str) 91 | return c 92 | 93 | @classmethod 94 | def arguments_str(cls, argparse_cmd): 95 | c = "" 96 | opts = [] 97 | for action in argparse_cmd._actions: 98 | if action.option_strings: 99 | # get longest option string 100 | optstr = sorted(action.option_strings, key=len, reverse=True)[0] 101 | helptxt = action.help.replace("'", "''") if action.help else ' ' 102 | helptxt = helptxt.replace(":", "\\:") 103 | opt_data = [optstr, helptxt] 104 | if action.choices: 105 | opt_data.append("(" + " ".join(action.choices) + ")") 106 | opts.append("'" + ':'.join(opt_data) + "'") 107 | else: # positional 108 | opt_data = ['', action.dest] 109 | if action.choices: 110 | opt_data.append("(" + " ".join(action.choices) + ")") 111 | else: 112 | opt_data.append("()") 113 | opts.append("'" + ':'.join(opt_data) + "'") 114 | if opts: 115 | c += "_arguments " + " ".join(opts) + " && ret=0\n" 116 | return c 117 | 118 | def cmd(self, cmd): 119 | c = " ({cmd})\n".format(cmd=cmd) 120 | c += " case $line[2] in\n" 121 | # argparse_cmd = self._parser._name_parser_map[cmd] 122 | # for action in argparse_cmd._actions: 123 | # subcmds = action.choices if action.choices else [] 124 | # for subcmd in subcmds: 125 | # c += " ({subcmd})\n".format(subcmd=subcmd) 126 | # c += " " + self.arguments_str(action.choices[subcmd]) 127 | # c += " ;;\n" 128 | c += " *)\n" 129 | c += self.describe_cmds(cmd, indent=14) 130 | c += " ;;\n" 131 | c += " esac\n" 132 | # c += self.arguments_str(argparse_cmd) 133 | c += " ;;" 134 | return c 135 | 136 | def cmds_list_str(self): 137 | tuples = [] 138 | for x in Commands.MainList: 139 | cmd = self._parser._name_parser_map[x] 140 | desc = cmd.description if cmd.description else "" 141 | shortlen = 40 142 | if desc and len(desc) > shortlen: 143 | desc = desc[:shortlen - 3] + '...' 144 | tuples.append((x, desc)) 145 | return "\n ".join(["'" + x[0] + ':' + x[1] + "'" for x in tuples]) 146 | -------------------------------------------------------------------------------- /linstor_client/commands/key_value_store.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import linstor_client 4 | import linstor_client.argparse.argparse as argparse 5 | from linstor_client.commands import Commands 6 | 7 | 8 | class KeyValueStoreCommands(Commands): 9 | _kv_list_headers = [ 10 | linstor_client.TableHeader("Name"), 11 | ] 12 | 13 | _kv_show_headers = [ 14 | linstor_client.TableHeader("Key"), 15 | linstor_client.TableHeader("Value"), 16 | ] 17 | 18 | def __init__(self): 19 | super(KeyValueStoreCommands, self).__init__() 20 | 21 | def instance_completer(self, prefix, **kwargs): 22 | lapi = self.get_linstorapi(**kwargs) 23 | possible = set() 24 | 25 | instances = lapi.keyvaluestores().instances() 26 | if instances: 27 | possible = set(instances) 28 | 29 | if prefix: 30 | return {instance for instance in possible if instance.startswith(prefix)} 31 | 32 | return possible 33 | 34 | def key_completer(self, prefix, **kwargs): 35 | lapi = self.get_linstorapi(**kwargs) 36 | instance = kwargs['parsed_args'].instance 37 | possible = set() 38 | 39 | lst = lapi.keyvaluestore_list(instance) 40 | if lst: 41 | possible = set(lst.properties.keys()) 42 | 43 | if prefix: 44 | return {key for key in possible if key.startswith(prefix)} 45 | 46 | return possible 47 | 48 | def setup_commands(self, parser): 49 | subcommands = [ 50 | Commands.Subcommands.List, 51 | Commands.Subcommands.Show, 52 | Commands.Subcommands.Modify, 53 | ] 54 | 55 | kv_parser = parser.add_parser( 56 | Commands.KEY_VALUE_STORE, 57 | aliases=["kv"], 58 | formatter_class=argparse.RawTextHelpFormatter, 59 | description="Key-value store subcommands") 60 | kv_subp = kv_parser.add_subparsers( 61 | title="key-value store commands", 62 | metavar="", 63 | description=Commands.Subcommands.generate_desc(subcommands) 64 | ) 65 | 66 | p_kv_list = kv_subp.add_parser( 67 | Commands.Subcommands.List.LONG, 68 | aliases=[Commands.Subcommands.List.SHORT], 69 | description='Lists all key-value store instances.') 70 | p_kv_list.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output') 71 | p_kv_list.set_defaults(func=self.list) 72 | 73 | p_kv_show = kv_subp.add_parser( 74 | Commands.Subcommands.Show.LONG, 75 | aliases=[Commands.Subcommands.Show.SHORT], 76 | description='Lists all key-value pairs in a key-value store instance.') 77 | p_kv_show.add_argument( 78 | 'instance', 79 | type=str, 80 | help='Key-value store instance to operate on').completer = self.instance_completer 81 | p_kv_show.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output.') 82 | p_kv_show.set_defaults(func=self.show) 83 | 84 | p_kv_modify = kv_subp.add_parser( 85 | Commands.Subcommands.Modify.LONG, 86 | aliases=[Commands.Subcommands.Modify.SHORT], 87 | description='Modify the value for a given key in the key-value store.') 88 | p_kv_modify.add_argument( 89 | 'instance', 90 | type=str, 91 | help='Key-value store instance to operate on').completer = self.instance_completer 92 | p_kv_modify.add_argument( 93 | 'key', 94 | type=str, 95 | help='Key to modify').completer = self.key_completer 96 | p_kv_modify.add_argument( 97 | 'value', 98 | nargs='?', 99 | type=str, 100 | help='Value to set key to') 101 | p_kv_modify.set_defaults(func=self.modify) 102 | 103 | self.check_subcommands(kv_subp, subcommands) 104 | 105 | def list(self, args): 106 | list_message = self._linstor.keyvaluestores() 107 | return self.output_list(args, list_message.instances(), self.table_list, single_item=False) 108 | 109 | def show(self, args): 110 | list_message = self._linstor.keyvaluestore_list(args.instance) 111 | items = list(list_message.properties.items()) 112 | return self.output_list(args, items, self.table_show, single_item=False) 113 | 114 | def modify(self, args): 115 | mod_prop_dict = Commands.parse_key_value_pairs([(args.key, args.value)]) 116 | replies = self._linstor.keyvaluestore_modify( 117 | args.instance, 118 | property_dict=mod_prop_dict['pairs'], 119 | delete_props=mod_prop_dict['delete'] 120 | ) 121 | self.handle_replies(args, replies) 122 | 123 | @staticmethod 124 | def table_list(args, lstmsg): 125 | tbl = linstor_client.Table(utf8=not args.no_utf8, colors=not args.no_color, pastable=args.pastable) 126 | for hdr in KeyValueStoreCommands._kv_list_headers: 127 | tbl.add_header(hdr) 128 | 129 | for name in lstmsg: 130 | tbl.add_row([name]) 131 | 132 | tbl.show() 133 | 134 | @staticmethod 135 | def table_show(args, lstmsg): 136 | tbl = linstor_client.Table(utf8=not args.no_utf8, colors=not args.no_color, pastable=args.pastable) 137 | for hdr in KeyValueStoreCommands._kv_show_headers: 138 | tbl.add_header(hdr) 139 | 140 | for key, value in lstmsg: 141 | tbl.add_row([key, value]) 142 | 143 | tbl.show() 144 | -------------------------------------------------------------------------------- /tests/test_ctrl_nodes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tests import LinstorTestCase 3 | import linstor.sharedconsts as apiconsts 4 | 5 | 6 | class TestNodeCommands(LinstorTestCase): 7 | 8 | def create_node(self, node_name, subip): 9 | node = self.execute_with_resp(['node', 'create', node_name, '195.0.0.' + str(subip)]) 10 | self.assert_api_succuess(node[0]) 11 | self.assertEqual(apiconsts.MASK_NODE | apiconsts.MASK_CRT | apiconsts.CREATED, node[0].ret_code) 12 | 13 | def test_create_node(self): 14 | node_name = 'nodeCommands1' 15 | self.create_node(node_name, 2) 16 | 17 | node_list = self.execute_with_machine_output(['node', 'list']) 18 | self.assertIsNotNone(node_list) 19 | self.assertIs(len(node_list), 1) 20 | node_list = node_list[0] 21 | self.assertTrue('nodes' in node_list) 22 | nodes = node_list['nodes'] 23 | self.assertGreater(len(nodes), 0) 24 | self.assertTrue([n for n in nodes if n['name'] == node_name]) 25 | 26 | # args = self.parse_args(['node', 'list']) # any valid command, just need the parsed args object 27 | # completer_nodes = NodeCommands.node_completer('node1', parsed_args=args) 28 | # self.assertTrue('node1' in completer_nodes) 29 | 30 | retcode = self.execute(['node', 'delete', node_name]) 31 | self.assertEqual(0, retcode) 32 | 33 | def find_node(self, nodelist, node_name): 34 | fnodes = [x for x in nodelist if x['name'] == node_name] 35 | if fnodes: 36 | self.assertEqual(1, len(fnodes)) 37 | return fnodes[0] 38 | return None 39 | 40 | def assert_netinterface(self, netif_data, netif_name, netif_addr): 41 | self.assertEqual(netif_data['name'], netif_name) 42 | self.assertEqual(netif_data['address'], netif_addr) 43 | 44 | def assert_netinterfaces(self, node, expected_netifs): 45 | netifs = self.execute_with_machine_output(['node', 'interface', 'list', node]) 46 | self.assertEqual(1, len(netifs)) 47 | netifs = netifs[0] 48 | self.assertIn("nodes", netifs) 49 | nodes = netifs['nodes'] 50 | node = self.find_node(nodes, 'nodenetif') 51 | self.assertIsNotNone(node) 52 | self.assertEqual(len(expected_netifs), len(node['net_interfaces'])) 53 | netifs = node['net_interfaces'] 54 | 55 | for i in range(0, len(expected_netifs)): 56 | self.assert_netinterface(netifs[i], expected_netifs[i][0], expected_netifs[i][1]) 57 | 58 | def test_add_netif(self): 59 | self.create_node('nodenetif', 1) 60 | 61 | self.assert_netinterfaces('nodenetif', [("default", '195.0.0.1')]) 62 | 63 | netif = self.execute_with_single_resp(['node', 'interface', 'create', 'nodenetif', 'othernic', '10.0.0.1']) 64 | self.assert_api_succuess(netif) 65 | self.assertEqual(apiconsts.MASK_NET_IF | apiconsts.MASK_CRT | apiconsts.CREATED, netif.ret_code) 66 | 67 | self.assert_netinterfaces('nodenetif', [("default", '195.0.0.1'), ("othernic", '10.0.0.1')]) 68 | 69 | # modify netif 70 | netif = self.execute_with_single_resp( 71 | ['node', 'interface', 'modify', 'nodenetif', 'othernic', '--ip', '192.168.0.1'] 72 | ) 73 | self.assert_api_succuess(netif) 74 | self.assertEqual(apiconsts.MASK_NET_IF | apiconsts.MASK_MOD | apiconsts.MODIFIED, netif.ret_code) 75 | 76 | self.assert_netinterfaces('nodenetif', [("default", '195.0.0.1'), ("othernic", '192.168.0.1')]) 77 | 78 | # delete netif 79 | netif = self.execute_with_single_resp(['node', 'interface', 'delete', 'nodenetif', 'othernic']) 80 | self.assert_api_succuess(netif) 81 | self.assertEqual(apiconsts.MASK_NET_IF | apiconsts.MASK_DEL | apiconsts.DELETED, netif.ret_code) 82 | 83 | self.assert_netinterfaces('nodenetif', [("default", '195.0.0.1')]) 84 | 85 | def get_nodes(self, args=None): 86 | cmd = ['node', 'list'] 87 | if args: 88 | cmd += args 89 | node_list = self.execute_with_machine_output(cmd) 90 | self.assertIsNotNone(node_list) 91 | self.assertIs(len(node_list), 1) 92 | return node_list[0]['nodes'] 93 | 94 | def test_property_filtering(self): 95 | self.create_node('alpha', 50) 96 | self.create_node('bravo', 51) 97 | self.create_node('charly', 52) 98 | self.create_node('delta', 53) 99 | 100 | node_resp = self.execute_with_resp(['node', 'set-property', 'alpha', '--aux', 'site', 'a']) 101 | self.assert_apis_success(node_resp) 102 | 103 | node_resp = self.execute_with_resp(['node', 'set-property', 'bravo', '--aux', 'site', 'a']) 104 | self.assert_apis_success(node_resp) 105 | 106 | node_resp = self.execute_with_resp(['node', 'set-property', 'charly', '--aux', 'site', 'b']) 107 | self.assert_apis_success(node_resp) 108 | 109 | node_resp = self.execute_with_resp(['node', 'set-property', 'delta', '--aux', 'site', 'b']) 110 | self.assert_apis_success(node_resp) 111 | 112 | node_resp = self.execute_with_resp(['node', 'set-property', 'delta', '--aux', 'disks', 'fast']) 113 | self.assert_apis_success(node_resp) 114 | 115 | nodes = self.get_nodes(['--props', 'Aux/site=b', '--props', 'Aux/disks']) 116 | self.assertEqual(len(nodes), 1, "Only delta node expected") 117 | self.assertEqual("delta", nodes[0]['name']) 118 | 119 | nodes = self.get_nodes(['--props', 'Aux/site=a']) 120 | self.assertEqual(len(nodes), 2, "Only alpha, bravo nodes expected") 121 | self.assertEqual({'alpha', 'bravo'}, {x['name'] for x in nodes}) 122 | 123 | # delete nodes 124 | for node_name in ['alpha', 'bravo', 'charly', 'delta']: 125 | retcode = self.execute(['node', 'delete', node_name]) 126 | self.assertEqual(0, retcode) 127 | 128 | 129 | if __name__ == '__main__': 130 | unittest.main() 131 | -------------------------------------------------------------------------------- /tests/test_drbd_options.py: -------------------------------------------------------------------------------- 1 | from tests import LinstorTestCase 2 | import linstor.sharedconsts as apiconsts 3 | 4 | 5 | class TestListFilters(LinstorTestCase): 6 | def get_resource_dfn_properties(self, rsc_dfn_name): 7 | resourcedef_props = self.execute_with_machine_output(['resource-definition', 'list-properties', rsc_dfn_name]) 8 | self.assertEqual(1, len(resourcedef_props)) 9 | return resourcedef_props[0] 10 | 11 | def get_volume_dfn_properties(self, rsc_dfn_name, vlm_nr): 12 | volumedef_props = self.execute_with_machine_output( 13 | ['volume-definition', 'list-properties', rsc_dfn_name, vlm_nr] 14 | ) 15 | self.assertEqual(1, len(volumedef_props)) 16 | return volumedef_props[0] 17 | 18 | def get_resource_conn_properties(self, node_a, node_b, rsc_name): 19 | res_con_props = self.execute_with_machine_output( 20 | ['resource-connection', 'list-properties', node_a, node_b, rsc_name] 21 | ) 22 | self.assertEqual(1, len(res_con_props)) 23 | return res_con_props[0] 24 | 25 | def test_resource_protocol(self): 26 | """symbolic option test""" 27 | rsc_name = "resource-protocol" 28 | 29 | self.execute(['resource-definition', 'create', rsc_name]) 30 | 31 | resp = self.execute_with_resp(['resource-definition', 'drbd-options', '--protocol', 'A', rsc_name]) 32 | self.assertGreater(len(resp), 1) 33 | resp_upd = resp[1] 34 | self.assertEqual(apiconsts.MODIFIED | apiconsts.MASK_MOD | apiconsts.MASK_RSC_DFN, resp_upd.ret_code) 35 | 36 | resourcedef_props = self.get_resource_dfn_properties(rsc_name) 37 | self.find_and_check_prop(resourcedef_props, apiconsts.NAMESPC_DRBD_NET_OPTIONS + '/protocol', 'A') 38 | 39 | resp = self.execute_with_resp(['resource-definition', 'drbd-options', '--protocol', 'C', rsc_name]) 40 | self.assertGreater(len(resp), 1) 41 | resp_upd = resp[1] 42 | self.assertEqual(apiconsts.MODIFIED | apiconsts.MASK_MOD | apiconsts.MASK_RSC_DFN, resp_upd.ret_code) 43 | 44 | resourcedef_props = self.get_resource_dfn_properties(rsc_name) 45 | self.find_and_check_prop(resourcedef_props, apiconsts.NAMESPC_DRBD_NET_OPTIONS + '/protocol', 'C') 46 | 47 | self.execute(['resource-definition', 'delete', rsc_name]) 48 | 49 | def test_resource_mdflushes(self): 50 | """Boolean option test""" 51 | rsc_name = "resource-mdflushes" 52 | self.execute(['resource-definition', 'create', rsc_name]) 53 | 54 | resp = self.execute_with_resp(['resource-definition', 'drbd-options', '--md-flushes', 'no', rsc_name]) 55 | self.assertGreater(len(resp), 1) 56 | resp_upd = resp[1] 57 | self.assertEqual(apiconsts.MODIFIED | apiconsts.MASK_MOD | apiconsts.MASK_RSC_DFN, resp_upd.ret_code) 58 | 59 | resourcedef_props = self.get_resource_dfn_properties(rsc_name) 60 | self.find_and_check_prop(resourcedef_props, apiconsts.NAMESPC_DRBD_DISK_OPTIONS + '/md-flushes', 'no') 61 | 62 | resp = self.execute_with_resp(['resource-definition', 'drbd-options', '--md-flushes', 'yes', rsc_name]) 63 | self.assertGreater(len(resp), 1) 64 | resp_upd = resp[1] 65 | self.assertEqual(apiconsts.MODIFIED | apiconsts.MASK_MOD | apiconsts.MASK_RSC_DFN, resp_upd.ret_code) 66 | 67 | resourcedef_props = self.get_resource_dfn_properties(rsc_name) 68 | self.find_and_check_prop(resourcedef_props, apiconsts.NAMESPC_DRBD_DISK_OPTIONS + '/md-flushes', 'yes') 69 | 70 | self.execute(['resource-definition', 'delete', rsc_name]) 71 | 72 | def test_resource_disktimeout(self): 73 | """Numeric option test""" 74 | rsc_name = "resource-mdflushes" 75 | self.execute(['resource-definition', 'create', rsc_name]) 76 | 77 | resp = self.execute_with_resp(['resource-definition', 'drbd-options', '--disk-timeout', '5000', rsc_name]) 78 | self.assertGreater(len(resp), 1) 79 | resp_upd = resp[1] 80 | self.assertEqual(apiconsts.MODIFIED | apiconsts.MASK_MOD | apiconsts.MASK_RSC_DFN, resp_upd.ret_code) 81 | 82 | resourcedef_props = self.get_resource_dfn_properties(rsc_name) 83 | self.find_and_check_prop(resourcedef_props, apiconsts.NAMESPC_DRBD_DISK_OPTIONS + '/disk-timeout', '5000') 84 | 85 | resp = self.execute_with_resp(['resource-definition', 'drbd-options', '--disk-timeout', '0', rsc_name]) 86 | self.assertGreater(len(resp), 1) 87 | resp_upd = resp[1] 88 | self.assertEqual(apiconsts.MODIFIED | apiconsts.MASK_MOD | apiconsts.MASK_RSC_DFN, resp_upd.ret_code) 89 | 90 | resourcedef_props = self.get_resource_dfn_properties(rsc_name) 91 | self.find_and_check_prop(resourcedef_props, apiconsts.NAMESPC_DRBD_DISK_OPTIONS + '/disk-timeout', '0') 92 | 93 | self.execute(['resource-definition', 'delete', rsc_name]) 94 | 95 | def test_volume_on_io_error(self): 96 | rsc_name = "resource-volume-io-error" 97 | self.execute(['resource-definition', 'create', rsc_name]) 98 | self.execute(['volume-definition', 'create', rsc_name, "20M"]) 99 | 100 | resp = self.execute_with_resp(['volume-definition', 'drbd-options', '--on-io-error', 'detach', rsc_name, '0']) 101 | self.assertGreater(len(resp), 1) 102 | resp_upd = resp[1] 103 | self.assertEqual(apiconsts.MODIFIED | apiconsts.MASK_MOD | apiconsts.MASK_VLM_DFN, resp_upd.ret_code) 104 | 105 | volumedef_props = self.get_volume_dfn_properties(rsc_name, '0') 106 | self.find_and_check_prop(volumedef_props, apiconsts.NAMESPC_DRBD_DISK_OPTIONS + '/on-io-error', 'detach') 107 | 108 | resp = self.execute_with_resp(['volume-definition', 'drbd-options', '--on-io-error', 'pass_on', rsc_name, '0']) 109 | self.assertGreater(len(resp), 1) 110 | resp_upd = resp[1] 111 | self.assertEqual(apiconsts.MODIFIED | apiconsts.MASK_MOD | apiconsts.MASK_VLM_DFN, resp_upd.ret_code) 112 | 113 | volumedef_props = self.get_volume_dfn_properties(rsc_name, '0') 114 | self.find_and_check_prop(volumedef_props, apiconsts.NAMESPC_DRBD_DISK_OPTIONS + '/on-io-error', 'pass_on') 115 | 116 | self.execute(['resource-definition', 'delete', rsc_name]) 117 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | LINSTOR_CONTROLLER_HOST: 'linstor-controller' 3 | LINSTOR_CONTROLLER_PORT: '3370' 4 | 5 | stages: 6 | - lint 7 | - build 8 | - test 9 | - deploy 10 | - deploy_tests 11 | 12 | before_script: 13 | # setup ssh access to clone python-linstor dep 14 | - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' 15 | - eval $(ssh-agent -s) 16 | - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - 17 | - mkdir -p ~/.ssh 18 | - chmod 700 ~/.ssh 19 | - ssh-keyscan $GIT_HOST >> ~/.ssh/known_hosts 20 | # now install python-linstor into venv 21 | - pushd /tmp 22 | - python -m venv venv 23 | - git clone --recursive git@$GIT_HOST:$PYTHON_LINSTOR_PATH 24 | - cd linstor-api-py 25 | - git checkout $CI_COMMIT_REF_NAME || true 26 | - git submodule update 27 | - make gensrc 28 | - ../venv/bin/python setup.py install 29 | # and also install xmlrunner package 30 | - ../venv/bin/pip install --trusted-host pypi.python.org xmlrunner 31 | - popd 32 | 33 | services: 34 | - name: $CI_REGISTRY/linstor/linstor-server/controller 35 | alias: linstor-controller 36 | 37 | pep8: 38 | stage: lint 39 | image: registry.gitlab.com/pipeline-components/flake8:latest 40 | rules: 41 | - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' 42 | - if: '$CI_COMMIT_BRANCH == "jenkins"' 43 | - if: $CI_COMMIT_BRANCH == 'master' 44 | before_script: 45 | - echo noop 46 | script: 47 | flake8 . 48 | 49 | test:2.7: 50 | stage: test 51 | image: python:2.7 52 | rules: 53 | - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' 54 | - if: '$CI_COMMIT_BRANCH == "jenkins"' 55 | - if: $CI_COMMIT_BRANCH == 'master' 56 | before_script: 57 | # setup ssh access to clone python-linstor dep 58 | - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' 59 | - eval $(ssh-agent -s) 60 | - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - 61 | - mkdir -p ~/.ssh 62 | - chmod 700 ~/.ssh 63 | - ssh-keyscan $GIT_HOST >> ~/.ssh/known_hosts 64 | - pip install virtualenv 65 | # now install python-linstor into venv 66 | - pushd /tmp 67 | - python -m virtualenv venv 68 | - git clone --recursive git@$GIT_HOST:$PYTHON_LINSTOR_PATH 69 | - cd linstor-api-py 70 | - git checkout $CI_COMMIT_REF_NAME || true 71 | - git submodule update 72 | - make gensrc 73 | - ../venv/bin/python setup.py install 74 | # and also install xmlrunner package 75 | - ../venv/bin/pip install xmlrunner 76 | - popd 77 | script: 78 | - /tmp/venv/bin/python tests.py 79 | artifacts: 80 | reports: 81 | junit: test-reports/TEST-*.xml 82 | 83 | test:3.5: 84 | stage: test 85 | image: python:3.5 86 | rules: 87 | - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' 88 | - if: '$CI_COMMIT_BRANCH == "jenkins"' 89 | - if: $CI_COMMIT_BRANCH == 'master' 90 | script: 91 | - /tmp/venv/bin/python tests.py 92 | artifacts: 93 | reports: 94 | junit: test-reports/TEST-*.xml 95 | 96 | test:3.10: 97 | stage: test 98 | image: python:3.10 99 | rules: 100 | - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' 101 | - if: '$CI_COMMIT_BRANCH == "jenkins"' 102 | - if: $CI_COMMIT_BRANCH == 'master' 103 | script: 104 | - /tmp/venv/bin/python tests.py 105 | artifacts: 106 | reports: 107 | junit: test-reports/TEST-*.xml 108 | 109 | deploy_client: 110 | stage: deploy 111 | image: $LINBIT_DOCKER_REGISTRY/build-helpers:latest 112 | rules: 113 | - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' 114 | - if: $CI_COMMIT_BRANCH == 'master' 115 | before_script: 116 | - curl -sSL $LINBIT_REGISTRY_URL/repository/lbbuild/lbbuildctl-latest -o /usr/local/bin/lbbuildctl 117 | - chmod +x /usr/local/bin/lbbuildctl 118 | script: 119 | - | 120 | case "$CI_COMMIT_REF_NAME" in 121 | "master") VERSION_SUFFIX="" ;; 122 | *) VERSION_SUFFIX=.dev$(echo -n $CI_COMMIT_REF_NAME | md5sum| sed -e 's/[^1-9]//g' | cut -c -9) ;; 123 | esac 124 | - LINSTOR_CLIENT_VERSION=1.99.0$VERSION_SUFFIX 125 | - PYTHON_LINSTOR_VERSION=1.99.0 126 | - | 127 | if curl --head -f $LINBIT_REGISTRY_URL/repository/lbbuild-upstream/python-linstor-$PYTHON_LINSTOR_VERSION$VERSION_SUFFIX.tar.gz ; then 128 | PYTHON_LINSTOR_VERSION=$LINSTOR_CLIENT_VERSION 129 | echo "USING PYTHON_LINSTOR: $PYTHON_LINSTOR_VERSION" 130 | fi 131 | - awk -f "/usr/local/bin/dch.awk" -v PROJECT_VERSION="$LINSTOR_CLIENT_VERSION" -v PROJECT_NAME="linstor-client" debian/changelog > debian/changelog.tmp 132 | - mv debian/changelog{.tmp,} 133 | - sed -i "s/LINSTOR_CLI_VERSION [0-9.]*/LINSTOR_CLI_VERSION $LINSTOR_CLIENT_VERSION/g" Dockerfile 134 | - sed -i "s/PYTHON_LINSTOR_VERSION [0-9.]*/PYTHON_LINSTOR_VERSION $LINSTOR_CLIENT_VERSION/g" Dockerfile 135 | - sed -i "s/python-linstor >= [0-9.]*/python-linstor >= $LINSTOR_CLIENT_VERSION/g" setup.cfg setup.cfg.py2 136 | - sed -i "s/\"python-linstor>=[0-9.]*\"/\"python-linstor>=$LINSTOR_CLIENT_VERSION\"/g" setup.py 137 | - sed -i "s/VERSION = \"[0-9.]*\"/VERSION = \"$LINSTOR_CLIENT_VERSION\"/g" linstor_client/consts.py 138 | # - dummy-release.sh linstor-client $LINSTOR_CLIENT_VERSION ignore 139 | - NO_DOC="-no-doc" make debrelease 140 | - curl -isSf -u $LINBIT_REGISTRY_USER:$LINBIT_REGISTRY_PASSWORD --upload-file dist/linstor-client-$LINSTOR_CLIENT_VERSION.tar.gz $LINBIT_REGISTRY_URL/repository/lbbuild-upstream/ 141 | - curl -X DELETE -u $LINBIT_REGISTRY_USER:$LINBIT_REGISTRY_PASSWORD $LINBIT_REGISTRY_URL/repository/rhel8/x86_64/linstor-client-$LINSTOR_CLIENT_VERSION-1.noarch.rpm 142 | - lbbuildctl build linstor-client --arch amd64 --ci -v "$LINSTOR_CLIENT_VERSION" -p "$PYTHON_LINSTOR_VERSION" 143 | -e LINBIT_REGISTRY_USER=$LINBIT_REGISTRY_USER 144 | -e LINBIT_REGISTRY_PASSWORD=$LINBIT_REGISTRY_PASSWORD 145 | -e LINBIT_REGISTRY_URL=$LINBIT_REGISTRY_URL 146 | -d ubuntu-bionic,ubuntu-focal,ubuntu-noble,debian-bookworm,rhel7.0,rhel8.0 147 | 148 | staging: 149 | stage: deploy_tests 150 | variables: 151 | ARG_COMMIT_BRANCH: $CI_COMMIT_REF_NAME 152 | rules: 153 | - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' 154 | - if: $CI_COMMIT_BRANCH == 'master' 155 | trigger: linstor/linstor-tests 156 | -------------------------------------------------------------------------------- /tests/linstor_testcase.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import linstor_client_main 3 | import sys 4 | try: 5 | from StringIO import StringIO 6 | except ImportError: 7 | from io import StringIO 8 | import json 9 | import os 10 | from linstor.linstorapi import ApiCallResponse 11 | 12 | 13 | controller_port = os.environ.get('LINSTOR_CONTROLLER_PORT', 63370) 14 | 15 | 16 | class LinstorTestCase(unittest.TestCase): 17 | @classmethod 18 | def find_linstor_tar(cls, paths): 19 | for spath in paths: 20 | tarpath = os.path.join(spath, "linstor-server.tar") 21 | if os.path.exists(tarpath): 22 | return tarpath 23 | return None 24 | 25 | @classmethod 26 | def setUpClass(cls): 27 | pass 28 | 29 | @classmethod 30 | def tearDownClass(cls): 31 | pass 32 | 33 | @classmethod 34 | def host(cls): 35 | return os.environ.get('LINSTOR_CONTROLLER_HOST', 'localhost') 36 | 37 | @classmethod 38 | def port(cls): 39 | return controller_port 40 | 41 | @classmethod 42 | def rest_port(cls): 43 | return controller_port 44 | 45 | @classmethod 46 | def signed_mask(cls, mask): 47 | return mask - 2**64 48 | 49 | @classmethod 50 | def add_controller_arg(cls, cmd_args): 51 | cmd_args.insert(0, '--controllers') 52 | cmd_args.insert(1, cls.host() + ':' + str(cls.rest_port())) 53 | 54 | @classmethod 55 | def execute(cls, cmd_args): 56 | LinstorTestCase.add_controller_arg(cmd_args) 57 | print(cmd_args) 58 | linstor_cli = linstor_client_main.LinStorCLI() 59 | 60 | try: 61 | return linstor_cli.parse_and_execute(cmd_args) 62 | except SystemExit as e: 63 | print(e) 64 | return e.code 65 | 66 | @classmethod 67 | def parse_args(cls, cmd_args): 68 | LinstorTestCase.add_controller_arg(cmd_args) 69 | linstor_cli = linstor_client_main.LinStorCLI() 70 | 71 | return linstor_cli.parse(cmd_args) 72 | 73 | def execute_with_machine_output(self, cmd_args): 74 | """ 75 | Execute the given cmd_args command and adds the machine readable flag. 76 | Returns the parsed json output. 77 | """ 78 | LinstorTestCase.add_controller_arg(cmd_args) 79 | linstor_cli = linstor_client_main.LinStorCLI() 80 | backupstd = sys.stdout 81 | jout = None 82 | try: 83 | sys.stdout = StringIO() 84 | retcode = linstor_cli.parse_and_execute(["-m", "--output-version", "v0"] + cmd_args) 85 | self.assertEqual(0, retcode) 86 | finally: 87 | stdval = sys.stdout.getvalue() 88 | sys.stdout.close() 89 | sys.stdout = backupstd 90 | if stdval: 91 | try: 92 | jout = json.loads(stdval) 93 | except ValueError as ve: 94 | sys.stderr.write("Could not parse: {j}\n".format(j=stdval)) 95 | raise ve 96 | self.assertIsInstance(jout, list) 97 | else: 98 | sys.stderr.write(str(cmd_args) + " Result empty") 99 | return jout 100 | 101 | def execute_with_text_output(self, cmd_args): 102 | """ 103 | Execute the given cmd_args command and adds the machine readable flag. 104 | Returns the parsed json output. 105 | """ 106 | LinstorTestCase.add_controller_arg(cmd_args) 107 | linstor_cli = linstor_client_main.LinStorCLI() 108 | backupstd = sys.stdout 109 | 110 | try: 111 | sys.stdout = StringIO() 112 | retcode = linstor_cli.parse_and_execute(["--no-utf8", "--no-color"] + cmd_args) 113 | self.assertEqual(0, retcode) 114 | finally: 115 | stdval = sys.stdout.getvalue() 116 | sys.stdout.close() 117 | sys.stdout = backupstd 118 | return stdval 119 | 120 | def execute_with_resp(self, cmd_args): 121 | """ 122 | 123 | :param cmd_args: 124 | :return: 125 | :rtype: list[ApiCallResponse] 126 | """ 127 | d = self.execute_with_machine_output(cmd_args) 128 | self.assertIsNotNone(d, "No result returned") 129 | return [ApiCallResponse.from_json(x) for x in d] 130 | 131 | def execute_with_single_resp(self, cmd_args): 132 | responses = self.execute_with_resp(cmd_args) 133 | if len(responses) != 1: 134 | print(responses) 135 | self.assertEqual(len(responses), 1, "Zero or more than 1 api call responses") 136 | return responses[0] 137 | 138 | @classmethod 139 | def assertHasProp(cls, props, key, val): 140 | for prop in props: 141 | if prop['key'] == key and prop['value'] == val: 142 | return True 143 | raise AssertionError("Prop {prop} with value {val} not in container.".format(prop=key, val=val)) 144 | 145 | @classmethod 146 | def assert_api_succuess(cls, apicall_rc): 147 | """ 148 | 149 | :param ApiCallResponse apicall_rc: apicall rc to check 150 | :return: 151 | """ 152 | if not apicall_rc.is_success(): 153 | raise AssertionError("ApiCall no success: " + str(apicall_rc)) 154 | return True 155 | 156 | @classmethod 157 | def assert_apis_success(cls, apicalls): 158 | """ 159 | 160 | :param list[ApiCallResponse] apicalls: 161 | :return: 162 | """ 163 | if not all([not x.is_error() for x in apicalls]): 164 | raise AssertionError("ApiCall no success: " + str([x for x in apicalls if x.is_error()][0])) 165 | return True 166 | 167 | def find_prop(self, props, key): 168 | for prop in props: 169 | self.assertIn('key', prop) 170 | if key == prop['key']: 171 | return prop 172 | 173 | self.assertTrue(False, "Property '{key}' not found.".format(key=key)) 174 | 175 | def check_prop(self, prop, key, value): 176 | self.assertEqual(2, len(prop.keys())) 177 | self.assertIn('key', prop) 178 | self.assertIn('value', prop) 179 | self.assertEqual(key, prop['key']) 180 | self.assertEqual(value, prop['value']) 181 | 182 | def find_and_check_prop(self, props, key, value): 183 | prop = self.find_prop(props, key) 184 | self.check_prop(prop, key, value) 185 | -------------------------------------------------------------------------------- /linstor_client/commands/file_cmds.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import base64 4 | import os 5 | import sys 6 | import tempfile 7 | from subprocess import call 8 | 9 | import linstor 10 | 11 | import linstor_client 12 | import linstor_client.argparse.argparse as argparse 13 | from linstor_client.commands import Commands 14 | 15 | 16 | class FileCommands(Commands): 17 | _file_headers = [ 18 | linstor_client.TableHeader("Path"), 19 | ] 20 | 21 | def __init__(self): 22 | super(FileCommands, self).__init__() 23 | 24 | def setup_commands(self, parser): 25 | subcmds = [ 26 | Commands.Subcommands.List, 27 | Commands.Subcommands.Show, 28 | Commands.Subcommands.Modify, 29 | Commands.Subcommands.Delete, 30 | Commands.Subcommands.Deploy, 31 | Commands.Subcommands.Undeploy, 32 | ] 33 | 34 | # Resource subcommands 35 | file_parser = parser.add_parser( 36 | Commands.FILE, 37 | aliases=["f"], 38 | formatter_class=argparse.RawTextHelpFormatter, 39 | description="File subcommands") 40 | file_subp = file_parser.add_subparsers( 41 | title="file commands", 42 | metavar="", 43 | description=Commands.Subcommands.generate_desc(subcmds) 44 | ) 45 | 46 | p_file_list = file_subp.add_parser( 47 | Commands.Subcommands.List.LONG, 48 | aliases=[Commands.Subcommands.List.SHORT], 49 | description='Lists all files in the cluster.') 50 | p_file_list.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output.') 51 | p_file_list.set_defaults(func=self.list) 52 | 53 | p_file_show = file_subp.add_parser( 54 | Commands.Subcommands.Show.LONG, 55 | aliases=[Commands.Subcommands.Show.SHORT], 56 | description='Show a single file, including its content.') 57 | p_file_show.add_argument( 58 | 'file_name', 59 | type=str, 60 | help='Name of the file to show') 61 | p_file_show.set_defaults(func=self.show) 62 | 63 | p_file_modify = file_subp.add_parser( 64 | Commands.Subcommands.Modify.LONG, 65 | aliases=[Commands.Subcommands.Modify.SHORT], 66 | description='Modify the contents of a file.') 67 | p_file_modify.add_argument( 68 | 'file_name', 69 | type=str, 70 | help='Name of the file to modify') 71 | p_file_modify.set_defaults(func=self.modify) 72 | 73 | p_file_delete = file_subp.add_parser( 74 | Commands.Subcommands.Delete.LONG, 75 | aliases=[Commands.Subcommands.Delete.SHORT], 76 | description='Delete a file.') 77 | p_file_delete.add_argument( 78 | 'file_name', 79 | type=str, 80 | help='Name of the file to delete') 81 | p_file_delete.set_defaults(func=self.delete) 82 | 83 | p_file_deploy = file_subp.add_parser( 84 | Commands.Subcommands.Deploy.LONG, 85 | aliases=[Commands.Subcommands.Deploy.SHORT], 86 | description='Deploy a file with a resource definition.') 87 | p_file_deploy.add_argument( 88 | 'file_name', 89 | type=str, 90 | help='Name of the file to deploy') 91 | p_file_deploy.add_argument( 92 | 'resource_name', 93 | type=str, 94 | help='Name of the resource definition to deploy the file with') 95 | p_file_deploy.set_defaults(func=self.deploy) 96 | 97 | p_file_undeploy = file_subp.add_parser( 98 | Commands.Subcommands.Undeploy.LONG, 99 | aliases=[Commands.Subcommands.Undeploy.SHORT], 100 | description='Undeploy a file from a resource definition.') 101 | p_file_undeploy.add_argument( 102 | 'file_name', 103 | type=str, 104 | help='Name of the file to undeploy') 105 | p_file_undeploy.add_argument( 106 | 'resource_name', 107 | type=str, 108 | help='Name of the resource definition to undeploy the file from') 109 | p_file_undeploy.set_defaults(func=self.undeploy) 110 | 111 | self.check_subcommands(file_subp, subcmds) 112 | 113 | def list(self, args): 114 | lstmsg = self._linstor.file_list() 115 | return self.output_list(args, lstmsg, self.show_table) 116 | 117 | def show(self, args): 118 | showmsg = self._linstor.file_show(args.file_name) 119 | print(base64.b64decode(showmsg.files.content).decode(), end="") 120 | 121 | def modify(self, args): 122 | if sys.stdin.isatty(): 123 | editor = os.environ.get('EDITOR', 'nano') 124 | try: 125 | showmsg = self._linstor.file_show(args.file_name) 126 | initial_content = base64.b64decode(showmsg.files.content).decode() 127 | except linstor.LinstorApiCallError: 128 | # file does not exist yet 129 | initial_content = "" 130 | 131 | with tempfile.NamedTemporaryFile(suffix=".tmp") as tf: 132 | tf.write(initial_content.encode()) 133 | tf.flush() 134 | call([editor, tf.name]) 135 | tf.seek(0) 136 | input_str = tf.read() 137 | else: 138 | input_str = sys.stdin.read().encode() 139 | replies = self._linstor.file_modify(args.file_name, input_str) 140 | self.handle_replies(args, replies) 141 | 142 | def delete(self, args): 143 | replies = self._linstor.file_delete(args.file_name) 144 | self.handle_replies(args, replies) 145 | 146 | def deploy(self, args): 147 | replies = self._linstor.file_deploy(args.file_name, args.resource_name) 148 | self.handle_replies(args, replies) 149 | 150 | def undeploy(self, args): 151 | replies = self._linstor.file_undeploy(args.file_name, args.resource_name) 152 | self.handle_replies(args, replies) 153 | 154 | def show_table(self, args, lstmsg): 155 | tbl = linstor_client.Table(utf8=not args.no_utf8, colors=not args.no_color, pastable=args.pastable) 156 | for hdr in FileCommands._file_headers: 157 | tbl.add_header(hdr) 158 | 159 | for file in lstmsg.files: 160 | tbl.add_row([file.path]) 161 | 162 | tbl.show() 163 | -------------------------------------------------------------------------------- /linstor_client/commands/physical_storage_cmds.py: -------------------------------------------------------------------------------- 1 | import linstor_client.argparse.argparse as argparse 2 | 3 | from linstor_client.commands import Commands 4 | from linstor_client import Table, TableHeader 5 | 6 | 7 | class PhysicalStorageCommands(Commands): 8 | _phys_storage_headers = [ 9 | TableHeader("Size"), 10 | TableHeader("Rotational"), 11 | TableHeader("Nodes") 12 | ] 13 | 14 | def setup_commands(self, parser): 15 | subcmds = [ 16 | Commands.Subcommands.List, 17 | Commands.Subcommands.CreateDevicePool 18 | ] 19 | 20 | phys_parser = parser.add_parser( 21 | Commands.PHYSICAL_STORAGE, 22 | aliases=["ps"], 23 | formatter_class=argparse.RawTextHelpFormatter, 24 | description="Physical-storage subcommands" 25 | ) 26 | 27 | phys_subp = phys_parser.add_subparsers( 28 | title="Physical-storage commands", 29 | metavar="", 30 | description=Commands.Subcommands.generate_desc(subcmds) 31 | ) 32 | 33 | p_lphys = phys_subp.add_parser( 34 | Commands.Subcommands.List.LONG, 35 | aliases=[Commands.Subcommands.List.SHORT], 36 | description='Prints a list of all physical storage available for LINSTOR use. By default, the list is ' 37 | 'printed as a human readable table. Criteria are:\n' 38 | ' * Device size must be greater than 1GiB\n' 39 | ' * Device must be a root device, for example, `/dev/vda`, `/dev/sda`, and not have any children.\n' 40 | ' * Device must not have any file system or other `blkid` marker.\n' 41 | ' * Device must not be an existing DRBD device.') 42 | p_lphys.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output') 43 | p_lphys.set_defaults(func=self.list) 44 | 45 | p_create = phys_subp.add_parser( 46 | Commands.Subcommands.CreateDevicePool.LONG, 47 | aliases=[Commands.Subcommands.CreateDevicePool.SHORT], 48 | description='Creates an LVM or ZFS(thin) pool with an optional VDO on the device.' 49 | ) 50 | p_create.add_argument('provider_kind', 51 | choices=[x.lower() for x in ["LVM", "LVMTHIN", "ZFS", "ZFSTHIN", "SPDK"]], 52 | type=str.lower, 53 | help='Provider kind') 54 | p_create.add_argument('node_name', help="Node name").completer = self.node_completer 55 | p_create.add_argument('device_paths', nargs='+', help="List of full device paths to use") 56 | p_create.add_argument( 57 | '--pool-name', 58 | required=True, 59 | help="Name of the new pool" 60 | ) 61 | p_create.add_argument('--vdo-enable', action="store_true", help="Use VDO.(only Centos/RHEL)") 62 | p_create.add_argument('--vdo-logical-size', help="VDO logical size.") 63 | p_create.add_argument('--vdo-slab-size', help="VDO slab size.") 64 | p_create.add_argument("--storage-pool", help="Create a Linstor storage pool with the given name") 65 | p_create.add_argument('--sed', 66 | action="store_true", 67 | help="Setup self encrypting drive with Linstor. " 68 | + "Needs SED/OPAL2 capable drive and sedutil installed and --storage-pool") 69 | p_create.add_argument( 70 | '--pv-create-arguments', nargs='*', help='Arguments to pass to pvcreate', action='append', default=[]) 71 | p_create.add_argument( 72 | '--vg-create-arguments', nargs='*', help='Arguments to pass to vgcreate', action='append', default=[]) 73 | p_create.add_argument( 74 | '--lv-create-arguments', nargs='*', help='Arguments to pass to lvcreate', action='append', default=[]) 75 | p_create.add_argument( 76 | '--zpool-create-arguments', nargs='*', help='Arguments to pass to zpool create', action='append', 77 | default=[]) 78 | p_create.set_defaults(func=self.create_device_pool) 79 | 80 | self.check_subcommands(phys_subp, subcmds) 81 | 82 | @classmethod 83 | def show_physical_storage(cls, args, physical_storage_list): 84 | """ 85 | 86 | :param args: 87 | :param PhysicalStorageList physical_storage_list: 88 | :return: 89 | """ 90 | tbl = Table(utf8=not args.no_utf8, colors=not args.no_color, pastable=args.pastable) 91 | for hdr in cls._phys_storage_headers: 92 | tbl.add_header(hdr) 93 | 94 | for devices in physical_storage_list.physical_devices: 95 | node_rows = [] 96 | for node, node_devices in devices.nodes.items(): 97 | s = node + '[' 98 | node_out_devs = [] 99 | for device_obj in node_devices: 100 | ns = device_obj.device 101 | node_data = [] 102 | if device_obj.serial: 103 | node_data.append(device_obj.serial) 104 | if device_obj.wwn: 105 | node_data.append(device_obj.wwn) 106 | if node_data: 107 | ns += '(' + ','.join(node_data) + ')' 108 | node_out_devs.append(ns) 109 | s += ','.join(node_out_devs) + ']' 110 | node_rows.append(s) 111 | tbl.add_row([ 112 | devices.size, 113 | devices.rotational, 114 | "\n".join(node_rows) 115 | ]) 116 | 117 | tbl.show() 118 | 119 | def list(self, args): 120 | lstmsg = self._linstor.physical_storage_list() 121 | 122 | return self.output_list(args, [lstmsg], self.show_physical_storage) 123 | 124 | def create_device_pool(self, args): 125 | replies = self.get_linstorapi().physical_storage_create_device_pool( 126 | node_name=args.node_name, 127 | provider_kind=args.provider_kind, 128 | device_paths=args.device_paths, 129 | pool_name=args.pool_name, 130 | vdo_enable=args.vdo_enable, 131 | vdo_logical_size_kib=Commands.parse_size_str(args.vdo_logical_size, "KiB"), 132 | vdo_slab_size_kib=Commands.parse_size_str(args.vdo_slab_size, "KiB"), 133 | storage_pool_name=args.storage_pool, 134 | sed=args.sed, 135 | pv_create_arguments=[x for subargs in args.pv_create_arguments for x in subargs], 136 | vg_create_arguments=[x for subargs in args.vg_create_arguments for x in subargs], 137 | lv_create_arguments=[x for subargs in args.lv_create_arguments for x in subargs], 138 | zpool_create_arguments=[x for subargs in args.zpool_create_arguments for x in subargs], 139 | ) 140 | return self.handle_replies(args, replies) 141 | -------------------------------------------------------------------------------- /linstor_client/commands/drbd_setup_cmds.py: -------------------------------------------------------------------------------- 1 | import linstor_client.argparse.argparse as argparse 2 | from linstor_client.utils import rangecheck, filter_new_args 3 | from linstor.properties import properties 4 | from linstor_client.commands import ArgumentError 5 | from linstor import SizeCalc, LinstorError 6 | 7 | 8 | def _drbd_options(): 9 | drbd_options = {} 10 | for object_name, options in properties.items(): 11 | object_drbd_options = {} 12 | for option in options: 13 | opt_key = option.get('drbd_option_name') 14 | if opt_key is None or opt_key in ['help', '_name']: 15 | continue 16 | if option['key'].startswith('DrbdOptions/Handlers'): 17 | opt_key = "handler-" + opt_key 18 | object_drbd_options[opt_key] = option 19 | drbd_options[object_name] = object_drbd_options 20 | return drbd_options 21 | 22 | 23 | class DrbdOptions(object): 24 | drbd_options = _drbd_options() 25 | 26 | CLASH_OPTIONS = ["timeout"] 27 | 28 | unsetprefix = 'unset' 29 | 30 | @staticmethod 31 | def description(_type): 32 | return "Set DRBD {t} options on the given LINSTOR object. Use --unset-[option_name] to unset.".format(t=_type) 33 | 34 | @staticmethod 35 | def numeric_symbol(_min, _max, _symbols): 36 | def foo(x): 37 | try: 38 | i = int(x) 39 | if i not in range(_min, _max): 40 | raise ArgumentError("{v} not in range [{min}-{max}].".format(v=i, min=_min, max=_max)) 41 | return i 42 | except ValueError: 43 | pass 44 | if x not in _symbols: 45 | raise ArgumentError("'{v}' must be one of {s}.".format(v=x, s=_symbols)) 46 | return x 47 | 48 | return foo 49 | 50 | @classmethod 51 | def unit_str(cls, unit, unit_prefix): 52 | """ 53 | 54 | :param str unit: 55 | :param str unit_prefix: 56 | :return: String correctly describing the unit 57 | """ 58 | if unit_prefix == 'k' and unit == "bytes/second": 59 | return 'KiB/s' 60 | return unit 61 | 62 | @classmethod 63 | def add_arguments(cls, parser, object_name, allow_unset=True): 64 | for opt_key, option in sorted(cls.drbd_options[object_name].items(), key=lambda k: k[0]): 65 | if opt_key in cls.CLASH_OPTIONS: 66 | opt_key = "drbd-" + opt_key 67 | if option['type'] == 'symbol': 68 | parser.add_argument('--' + opt_key, choices=option['values']) 69 | elif option['type'] == 'boolean': 70 | parser.add_argument( 71 | '--' + opt_key, 72 | choices=['yes', 'no'], 73 | type=str.lower, 74 | help="yes/no (Default: %s)" % (option['default']) 75 | ) 76 | elif option['type'] == 'string': 77 | parser.add_argument('--' + opt_key) 78 | elif option['type'] == 'numeric-or-symbol': 79 | min_ = int(option['min']) 80 | max_ = int(option['max']) 81 | parser.add_argument( 82 | '--' + opt_key, 83 | type=DrbdOptions.numeric_symbol(min_, max_, option['values']), 84 | help="Integer between [{min}-{max}] or one of ['{syms}']".format( 85 | min=min_, 86 | max=max_, 87 | syms="','".join(option['values']) 88 | ) 89 | ) 90 | elif option['type'] == 'range': 91 | min_ = option['min'] 92 | max_ = option['max'] 93 | default = option['default'] 94 | unit = "" 95 | if "unit" in option: 96 | unit = " in " + option['unit'] 97 | # sp.add_argument('--' + opt, type=rangecheck(min_, max_), 98 | # default=default, help="Range: [%d, %d]; Default: %d" %(min_, max_, default)) 99 | # setting a default sets the option to != None, which makes 100 | # filterNew relatively complex 101 | if DrbdOptions._is_byte_unit(option): 102 | default_unit_prefix = option.get('unit_prefix', '') 103 | if default_unit_prefix != "1": 104 | dflt_unit_prefix_txt = "; Default unit: %s" % default_unit_prefix 105 | else: 106 | dflt_unit_prefix_txt = "" 107 | parser.add_argument( 108 | '--' + opt_key, 109 | type=str, 110 | help="Range: [%d, %d]%s; Default value: %s%s" % 111 | (min_, max_, unit, str(default), dflt_unit_prefix_txt) 112 | ) 113 | else: 114 | parser.add_argument('--' + opt_key, type=rangecheck(min_, max_), 115 | help="Range: [%d, %d]%s; Default: %s" % (min_, max_, unit, str(default))) 116 | else: 117 | raise LinstorError('Unknown option type ' + option['type']) 118 | 119 | if allow_unset: 120 | parser.add_argument('--%s-%s' % (cls.unsetprefix, opt_key), 121 | action='store_true', 122 | help=argparse.SUPPRESS) 123 | 124 | @classmethod 125 | def filter_new(cls, args): 126 | """return a dict containing all non-None args""" 127 | return filter_new_args(cls.unsetprefix, args) 128 | 129 | @classmethod 130 | def _is_byte_unit(cls, option): 131 | return option.get('unit') in ['bytes', 'bytes/second'] or option['drbd_option_name'] in [ 132 | 'al-extents', 'max-io-depth', 'congestion-extents', 'max-buffers' 133 | ] # the named options are thought to make sense here, even they are not directly bytes 134 | 135 | @classmethod 136 | def parse_opts(cls, new_args, object_name): 137 | modify = {} 138 | deletes = [] 139 | for arg in new_args: 140 | is_unset = arg.startswith(cls.unsetprefix) 141 | value = new_args[arg] 142 | prop_name = arg[len(cls.unsetprefix) + 1:] if is_unset else arg 143 | if prop_name.startswith("drbd-") and prop_name[5:] in cls.CLASH_OPTIONS: 144 | prop_name = prop_name[5:] 145 | option = cls.drbd_options[object_name][prop_name] 146 | 147 | key = option['key'] 148 | if is_unset: 149 | deletes.append(key) 150 | else: 151 | if DrbdOptions._is_byte_unit(option): 152 | unit = SizeCalc.UNIT_B 153 | if option.get('unit_prefix') == 'k': 154 | unit = SizeCalc.UNIT_KiB 155 | elif option.get('unit_prefix') == 's': 156 | unit = SizeCalc.UNIT_S 157 | value = SizeCalc.auto_convert(value, unit) 158 | if option['min'] <= value <= option['max']: 159 | value = str(value) 160 | else: 161 | raise ArgumentError( 162 | prop_name + " value {v}{u} is out of range [{mi}-{ma}]{un}".format( 163 | v=value, 164 | u=SizeCalc.unit_to_str(unit), 165 | mi=option['min'], 166 | ma=option['max'], 167 | un=option.get('unit', ''), 168 | )) 169 | modify[key] = str(value) 170 | 171 | return modify, deletes 172 | -------------------------------------------------------------------------------- /linstor_client/commands/error_report_cmds.py: -------------------------------------------------------------------------------- 1 | import linstor_client.argparse.argparse as argparse 2 | 3 | from linstor_client.table import Table, TableHeader 4 | from linstor_client.utils import Output, LinstorClientError 5 | from linstor_client.commands import Commands 6 | 7 | from datetime import datetime 8 | 9 | 10 | class ErrorReportCommands(Commands): 11 | def __init__(self): 12 | super(ErrorReportCommands, self).__init__() 13 | 14 | def setup_commands(self, parser): 15 | # Error subcommands 16 | error_subcmds = [ 17 | Commands.Subcommands.List, 18 | Commands.Subcommands.Show, 19 | Commands.Subcommands.Delete 20 | ] 21 | error_parser = parser.add_parser( 22 | Commands.ERROR_REPORTS, 23 | aliases=["err"], 24 | formatter_class=argparse.RawTextHelpFormatter, 25 | description="Error report subcommands") 26 | 27 | error_subp = error_parser.add_subparsers( 28 | title="Error report commands", 29 | metavar="", 30 | description=Commands.Subcommands.generate_desc(error_subcmds) 31 | ) 32 | 33 | c_list_error_reports = error_subp.add_parser( 34 | Commands.Subcommands.List.LONG, 35 | aliases=[Commands.Subcommands.List.SHORT], 36 | description='List error reports.' 37 | ) 38 | c_list_error_reports.add_argument('-s', '--since', help='Show errors since n days. e.g. "3days"') 39 | c_list_error_reports.add_argument('-t', '--to', help='Show errors to specified date. Format YYYY-MM-DD.') 40 | c_list_error_reports.add_argument( 41 | '-n', 42 | '--nodes', 43 | help='Only show error reports from these nodes.', 44 | nargs='+' 45 | ) 46 | c_list_error_reports.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output') 47 | c_list_error_reports.add_argument( 48 | '--report-id', 49 | nargs='+', 50 | help="Restrict to id's that begin with the given ones." 51 | ) 52 | c_list_error_reports.add_argument('-f', '--full', action="store_true", help='Show all error info fields') 53 | c_list_error_reports.set_defaults(func=self.cmd_list_error_reports) 54 | 55 | c_error_report = error_subp.add_parser( 56 | Commands.Subcommands.Show.LONG, 57 | aliases=[Commands.Subcommands.Show.SHORT], 58 | description='Output content of an error report.' 59 | ) 60 | c_error_report.add_argument("report_id", nargs='+') 61 | c_error_report.set_defaults(func=self.cmd_error_report) 62 | 63 | c_del_err_report = error_subp.add_parser( 64 | Commands.Subcommands.Delete.LONG, 65 | aliases=[Commands.Subcommands.Delete.SHORT], 66 | description='Delete one or more error reports.' 67 | ) 68 | c_del_err_report.add_argument("--nodes", nargs="+", help="Only delete error reports from the given nodes") 69 | c_del_err_report.add_argument( 70 | "--since", 71 | help="Datetime since when to delete error reports. Date format: '2020-08-30 13:40:00' or '5d6h'") 72 | c_del_err_report.add_argument( 73 | "--to", 74 | help="Datetime until to delete error reports. Date format: '2020-08-30 13:40:00' or '5d6h'") 75 | c_del_err_report.add_argument("--exception", help="Only delete error reports matching the exception") 76 | c_del_err_report.add_argument("id", nargs="*", help="Delete error reports matching the given ids") 77 | c_del_err_report.set_defaults(func=self.cmd_del_error_report) 78 | 79 | self.check_subcommands(error_subp, error_subcmds) 80 | 81 | @classmethod 82 | def show_error_report_list(cls, args, lstmsg): 83 | """ 84 | 85 | :param args: 86 | :param list[linstor.responses.ErrorReport] lstmsg: 87 | :return: 88 | """ 89 | tbl = Table(utf8=not args.no_utf8, colors=not args.no_color, pastable=args.pastable) 90 | tbl.add_header(TableHeader("Id")) 91 | tbl.add_header(TableHeader("Datetime")) 92 | tbl.add_header(TableHeader("Node")) 93 | tbl.add_header(TableHeader("Exception")) 94 | if args.full: 95 | tbl.add_header(TableHeader("Location")) 96 | tbl.add_header(TableHeader("Version")) 97 | 98 | for error in lstmsg: 99 | msg = error.exception_message \ 100 | if len(error.exception_message) < 60 else error.exception_message[0:57] + '...' 101 | row = [ 102 | error.id, 103 | str(error.datetime)[:19], 104 | (error.module[0] + '|' if error.module else "") + error.node_names, 105 | error.exception + (": " + msg if msg else "")] 106 | if args.full: 107 | row += ["{f}:{l}".format(f=error.origin_file, l=error.origin_line) if error.origin_file else "", 108 | error.version] 109 | tbl.add_row(row) 110 | tbl.show() 111 | 112 | def cmd_list_error_reports(self, args): 113 | since = args.since 114 | since_dt = None 115 | if since: 116 | since_dt = self.parse_time_str(since) 117 | 118 | to_dt = None 119 | if args.to: 120 | to_dt = datetime.strptime(args.to, '%Y-%m-%d') 121 | to_dt = to_dt.replace(hour=23, minute=59, second=59) 122 | 123 | lstmsg = self._linstor.error_report_list(nodes=args.nodes, since=since_dt, to=to_dt, ids=args.report_id) 124 | return self.output_list(args, lstmsg, self.show_error_report_list, single_item=False) 125 | 126 | def show_error_report(self, args, lstmsg): 127 | for error in lstmsg: 128 | print(Output.utf8(error.text)) 129 | 130 | def cmd_error_report(self, args): 131 | lstmsg = self._linstor.error_report_list(with_content=True, ids=args.report_id) 132 | return self.output_list(args, lstmsg, self.show_error_report, single_item=False) 133 | 134 | @classmethod 135 | def fill_str_part(cls, fill_str, default_str): 136 | """ 137 | Fill fill_str with missing parts from default_str. 138 | 139 | :param fill_str: 140 | :param default_str: 141 | :return: 142 | """ 143 | return fill_str + default_str[len(fill_str):] 144 | 145 | def cmd_del_error_report(self, args): 146 | since_dt = None 147 | to_dt = None 148 | 149 | dt_format = '%Y-%m-%d %H:%M:%S' 150 | def_dt_str = '0000-00-00 23:59:59' 151 | 152 | if args.since: 153 | try: 154 | since_dt = self.parse_time_str(args.since) 155 | except LinstorClientError: 156 | since_str = self.fill_str_part(args.since, def_dt_str) 157 | try: 158 | since_dt = datetime.strptime(since_str, dt_format) 159 | except ValueError as ve: 160 | raise LinstorClientError("Unable to parse 'since' date: " + str(ve), exit_code=2) 161 | 162 | if args.to: 163 | try: 164 | to_dt = self.parse_time_str(args.to) 165 | except LinstorClientError: 166 | to_str = self.fill_str_part(args.to, def_dt_str) 167 | try: 168 | to_dt = datetime.strptime(to_str, dt_format) 169 | except ValueError as ve: 170 | raise LinstorClientError("Unable to parse 'to' date: " + str(ve), exit_code=2) 171 | 172 | replies = self.get_linstorapi().error_report_delete( 173 | args.nodes, 174 | since=since_dt, 175 | to=to_dt, 176 | exception=args.exception, 177 | version=None, 178 | ids=args.id 179 | ) 180 | return self.handle_replies(args, replies) 181 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to linstor-client will be documented in this file starting from version 1.13.0, 4 | for older version see github releases. 5 | 6 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 7 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | 11 | ## [1.27.1] - 2025-12-11 12 | 13 | ### Added 14 | 15 | - Added new alias --drbd-diskless to command "r td" to mimic the option from "r c" 16 | - Added new sub-command "encryption status" to show the current locked-state of the controller 17 | 18 | ## [1.27.0] - 2025-11-11 19 | 20 | ### Added 21 | 22 | - Added new option to "backup ship": --source-snapshot 23 | - Added new option to "backup ship": --full 24 | - Added new alias for "backup create -s": --source-snapshot 25 | - Added --target and --do-not-target to 'node evacuate' 26 | - from-file parameter to resource-definition list and list-properties 27 | - from-file parameter to node lp and resource-group lp 28 | 29 | ### Fixed 30 | 31 | - Fixed help-text for "backup restore --snapshot" 32 | 33 | ## [1.26.1] - 2025-08-26 34 | 35 | ### Added 36 | 37 | - Added --target-resorce-name to 'backup schedule enable' 38 | 39 | ## [1.26.0] - 2025-08-18 40 | 41 | ### Added 42 | 43 | - snapshot list and set property commands 44 | - r l: optional --show-drbd-ports 45 | - s rb: optional --zfs-rollback-strategy 46 | 47 | ### Changed 48 | 49 | - rd l: Ports column now hidden by default (moved to resource level) 50 | 51 | ### Fixed 52 | 53 | - Fixed incorrect unit in out of range error message 54 | 55 | ### Removed 56 | 57 | - Removed "exos" commands 58 | - Removed "snapshot ship" command. Use "backup ship" instead 59 | 60 | ## [1.25.4] - 2025-04-10 61 | 62 | ### Fixed 63 | 64 | - rd clone: fix crash if --curl was set 65 | 66 | ## [1.25.3] - 2025-04-08 67 | 68 | ### Fixed 69 | 70 | - volume-list: show empty replication states if there is no data provided 71 | 72 | ## [1.25.2] - 2025-04-08 73 | 74 | ### Changed 75 | 76 | - replication column is now resolved by checking the replication_states map 77 | - added --hide-replication-states column option to v l and r lv 78 | 79 | ## [1.25.1] - 2025-04-02 80 | 81 | ### Fixed 82 | 83 | - Missing color argument in color_repl_state 84 | 85 | ## [1.25.0] - 2025-03-19 86 | 87 | ### Changed 88 | 89 | - resource-definition-list: add more state values CLONING/FAILED... 90 | - volume-list: show drbd replication state in its own column 91 | 92 | ### Fixed 93 | 94 | - Incorrect environment controllers priority if given by commandline 95 | 96 | ## [1.24.0] - 2024-12-17 97 | 98 | ### Added 99 | 100 | - Added options --target-resource-group and --force-move-resource-group to backup ship, restore and schedule enable. 101 | - Added --layer-list argument to resource-definition clone 102 | - Added --resource-group argument to resource-definition clone 103 | - Added layer-list to resource-definition list 104 | 105 | ### Changed 106 | 107 | - Column order and coloring in volume list 108 | - Show layer-list instead of ports column in resource-list 109 | - resource/volume list show multiple primaries as yellow 110 | 111 | ## [1.23.2] - 2024-09-25 112 | 113 | ### Fixed 114 | - missing commands.utils package 115 | 116 | ## [1.23.1] - 2024-09-25 117 | 118 | ### Changed 119 | - Added info text for SkipDisk scenarios 120 | - error-report delete: allow 5d or 3d10h strings to be used for --to and --since 121 | 122 | ### Fixed 123 | - parse_time_str/since argument: better wrong input handling 124 | 125 | ## [1.23.0] - 2024-07-11 126 | 127 | ### Added 128 | 129 | - Autoplacer: Add --x-replicas-on-different option 130 | - Resource delete: Add --keep-tiebreaker option 131 | 132 | ## [1.22.1] - 2024-04-25 133 | 134 | ### Changed 135 | - encryption modify-passphrase now asks again for the new password 136 | - non-DRBD resource now show Created instead of Unknown 137 | 138 | ### Fixed 139 | - resource list not showing ports 140 | 141 | ## [1.22.0] - 2024-04-02 142 | 143 | ### Added 144 | - Allow to specify options for list commands in the client config file 145 | - Added --from-file to most list commands to read input data from a file 146 | - Added --volume-passphrase and modify-passphrase options/commands 147 | - Backups added --force-restore option 148 | 149 | ### Changed 150 | - Default machine-readable output-version is now v1 151 | - Improved command help descriptions 152 | 153 | ### Removed 154 | - Unused vg l -R option 155 | 156 | ## [1.21.1] - 2024-02-22 157 | 158 | ### Added 159 | 160 | - PhysicalStorageCreate: Allow zfsthin as provider kind 161 | - Added node connectionstatus MISSING_EXT_TOOLS handler 162 | 163 | ### Fixed 164 | 165 | - Do not hide evicted resources in volume list 166 | 167 | ### Removed 168 | 169 | - OpenFlex commands removed 170 | 171 | ## [1.21.0] - 2024-01-22 172 | 173 | ### Added 174 | 175 | - Added --peer-slots to "rg c", "rg m" and "rg spawn" 176 | - Added storpool rename for schedule enable, restore and l2l shippings 177 | 178 | ### Changed 179 | 180 | - "rg query-size-info" no longer shows 'OversubscriptionRatio' (multiple ambiguous sources) 181 | 182 | ### Fixed 183 | 184 | - skipDisk property access on list commands 185 | 186 | ## [1.20.1] - 2023-10-25 187 | 188 | ### Added 189 | 190 | - Add "set-log-level" subcommand for controller and node 191 | 192 | ## [1.20.0] - 2023-10-11 193 | 194 | ### Added 195 | 196 | - Show skip-disk property and level resource list 197 | 198 | ### Changed 199 | 200 | - Typo in remote command argument --availability-zone 201 | 202 | ### Fixed 203 | 204 | - Fixed exos help message 205 | - Show deleting state for DRBD_DELETE flag 206 | - Fixed resource involved command AttributeError 207 | 208 | ## [1.19.0] - 2023-07-19 209 | 210 | ### Added 211 | 212 | - Backup queue list command 213 | 214 | ### Changed 215 | 216 | - `linstor -v` now shows that it is the client version 217 | 218 | ### Fixed 219 | 220 | - Fix aux argument list handling for replicas-on-same and similar 221 | 222 | ## [1.18.0] - 2023-04-17 223 | 224 | ### Added 225 | 226 | - Subcommand for snapshots create-multiple 227 | 228 | ### Changed 229 | 230 | - drbd-options(opts) now correctly handle sector units 231 | 232 | ### Fixed 233 | 234 | - RscDfn create do not ignore peerslots 235 | - NodeCon fixed issues with path list 236 | 237 | ## [1.17.0] - 2023-03-14 238 | 239 | ### Added 240 | 241 | - Added rg query-size-info command 242 | - volume list: added --show-props option 243 | 244 | ### Changed 245 | 246 | - NodeCon,RscCon: Remove DrbdOptions subcommand 247 | 248 | ### Fixed 249 | 250 | - Improved broken pipe error handling 251 | 252 | ## [1.16.0] - 2022-12-13 253 | 254 | ### Added 255 | 256 | - Added node connection commands 257 | 258 | ## [1.15.1] - 2022-10-18 259 | 260 | ### Added 261 | 262 | - Snap,EBS: Added State message 263 | - Added column for storage spaces thin for node info 264 | 265 | ### Fixed 266 | 267 | - node info: also ignore underliners in table headers 268 | 269 | ## [1.15.0] - 2022-09-20 270 | 271 | ### Added 272 | 273 | - Added autoplace-options to resource-group spawn command 274 | - Added `--show-props` option to all possible list commands to add custom props columns 275 | - Added commands for the key-value-stora API 276 | - Added SED support in the physical-storage-create command 277 | - Added EBS support/commands 278 | - Advise added too many replicas issue and filtering by issue type 279 | 280 | ### Fixed 281 | 282 | - Fixed typos in vd help 283 | 284 | ## [1.14.0] - 2022-07-06 285 | 286 | ### Added 287 | 288 | - Added commands for backup schedule 289 | - SOS-Report: Added filters 290 | - Added backup delete `keep-snaps` option 291 | 292 | ## [1.13.1] - 2022-05-12 293 | 294 | ### Changed 295 | 296 | - file editor fallback switched to nano 297 | 298 | ### Fixes 299 | 300 | - Fixed loading remotes with ETCD backend 301 | - Autosnapshot: fix property not working on RG or controller 302 | 303 | ## [1.13.0] - 2022-05-22 304 | 305 | ### Added 306 | 307 | - Added ZFS clone option for clone resource-definition 308 | - Added resource-definition wait sync command 309 | - Added backup snapshot name 310 | - Added controller backup DB command 311 | - Show resource/snapshot for backups 312 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | linstor - management of distributed DRBD9 resources 4 | Copyright (C) 2013 - 2017 LINBIT HA-Solutions GmbH 5 | Author: Robert Altnoeder, Philipp Reisner 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | import os 22 | import glob 23 | import sys 24 | import codecs 25 | 26 | from setuptools import setup, Command 27 | 28 | 29 | def get_version(): 30 | from linstor_client.consts import VERSION 31 | return VERSION 32 | 33 | 34 | # used to overwrite version tag by internal build tools 35 | # keep it, even if you don't understand it. 36 | def get_setup_version(): 37 | return get_version() 38 | 39 | 40 | class CheckUpToDate(Command): 41 | description = "Check if version strings are up to date" 42 | user_options = [] 43 | 44 | def initialize_options(self): 45 | self.cwd = None 46 | 47 | def finalize_options(self): 48 | self.cwd = os.getcwd() 49 | 50 | def run(self): 51 | version = get_version() 52 | try: 53 | with codecs.open("debian/changelog", encoding='utf8', errors='ignore') as f: 54 | firstline = f.readline() 55 | if version not in firstline: 56 | # returning false is not promoted 57 | sys.exit(1) 58 | with open("Dockerfile") as f: 59 | found = 0 60 | content = [line.strip() for line in f.readlines()] 61 | for line in content: 62 | fields = [f.strip() for f in line.split()] 63 | if len(fields) == 3 and fields[0] == 'ENV' and \ 64 | fields[1] == 'LINSTOR_CLI_VERSION' and fields[2] == version: 65 | found += 1 66 | if found != 2: 67 | # returning false is not promoted 68 | sys.exit(1) 69 | except IOError: 70 | # probably a release tarball without the debian directory but with Makefile 71 | return True 72 | 73 | 74 | class BuildManCommand(Command): 75 | """ 76 | Builds manual pages using docbook 77 | """ 78 | 79 | description = "Build manual pages" 80 | user_options = [] 81 | 82 | def initialize_options(self): 83 | self.cwd = None 84 | 85 | def finalize_options(self): 86 | self.cwd = os.getcwd() 87 | 88 | def run(self): 89 | assert os.getcwd() == self.cwd, "Must be in package root: %s" % self.cwd 90 | from linstor_client_main import LinStorCLI 91 | outdir = "man-pages" 92 | name = "linstor" 93 | mansection = '8' 94 | client = LinStorCLI() 95 | descriptions = client.parser_cmds_description(client._all_commands) 96 | 97 | if not os.path.isfile(os.path.join(outdir, "linstor.8.gz")): 98 | h = open(os.path.join(outdir, "linstor_header.xml")) 99 | t = open(os.path.join(outdir, "linstor_trailer.xml")) 100 | linstorxml = open(os.path.join(outdir, "linstor.xml"), 'w') 101 | linstorxml.write(h.read()) 102 | for cmd in [cmds[0] for cmds in client._all_commands]: 103 | linstorxml.write(""" 104 | 105 | 106 | linstor 107 | %s 108 | 109 | 110 | 111 | 112 | %s 113 | 114 | For furter information see 115 | 116 | %s 117 | %s 118 | 119 | 120 | 121 | """ % (cmd, descriptions[cmd], name + '-' + cmd, mansection)) 122 | linstorxml.write(t.read()) 123 | h.close() 124 | t.close() 125 | linstorxml.close() 126 | 127 | os.system("cd %s; " % outdir 128 | + " xsltproc --xinclude --stringparam variablelist.term.break.after 1 " 129 | "http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl " 130 | "linstor.xml; gzip -f -9 linstor.8") 131 | # subcommands 132 | import gzip 133 | if "__enter__" not in dir(gzip.GzipFile): # duck punch it in! 134 | def __enter(self): 135 | if self.fileobj is None: 136 | raise ValueError("I/O operation on closed GzipFile object") 137 | return self 138 | 139 | def __exit(self, *args): 140 | self.close() 141 | 142 | gzip.GzipFile.__enter__ = __enter 143 | gzip.GzipFile.__exit__ = __exit 144 | 145 | from linstor_client.utils import check_output 146 | 147 | replace = ("linstor_client_main.py", "linstor") 148 | 149 | for cmd in client._all_commands: 150 | toplevel = cmd[0] 151 | # aliases = cmd[1:] 152 | # we could use the aliases to symlink them to the toplevel cmd 153 | outfile = os.path.join('.', outdir, name + '-' + toplevel + '.' + mansection + ".gz") 154 | if os.path.isfile(outfile): 155 | continue 156 | sys.stdout.write("Generating %s ...\n" % (outfile)) 157 | mangen = ["help2man", "-n", toplevel, '-s', mansection, 158 | '--version-string=%s' % (get_version()), "-N", 159 | '"./linstor_client_main.py %s"' % (toplevel)] 160 | 161 | toexec = " ".join(mangen) 162 | manpage = check_output(toexec, shell=True).decode() 163 | manpage = manpage.replace(replace[0], replace[1]) 164 | manpage = manpage.replace(replace[0].upper(), replace[1].upper()) 165 | manpage = manpage.replace(toplevel.upper(), mansection) 166 | manpage = manpage.replace("%s %s" % (replace[1], toplevel), 167 | "%s_%s" % (replace[1], toplevel)) 168 | with gzip.open(outfile, 'wb') as f: 169 | f.write(manpage.encode()) 170 | 171 | 172 | def gen_data_files(): 173 | data_files = [("/etc/bash_completion.d", ["scripts/bash_completion/linstor"])] 174 | 175 | for manpage in glob.glob(os.path.join("man-pages", "*.8.gz")): 176 | data_files.append(("/usr/share/man/man8", [manpage])) 177 | 178 | return data_files 179 | 180 | 181 | setup( 182 | name="linstor-client", 183 | version=get_setup_version(), 184 | description="DRBD distributed resource management utility", 185 | long_description="This client program communicates to controller node which manages the resources", 186 | author="Robert Altnoeder , Roland Kammerer " 187 | + ", Rene Peinthor ", 188 | author_email="roland.kammerer@linbit.com", 189 | maintainer="LINBIT HA-Solutions GmbH", 190 | maintainer_email="drbd-user@lists.linbit.com", 191 | url="https://www.linbit.com", 192 | license="GPLv3", 193 | packages=[ 194 | "linstor_client", 195 | "linstor_client.argparse", 196 | "linstor_client.argcomplete", 197 | "linstor_client.commands", 198 | "linstor_client.commands.utils", 199 | ], 200 | install_requires=[ 201 | "python3-setuptools" 202 | "python-linstor>=1.27.1" 203 | ], 204 | py_modules=["linstor_client_main"], 205 | scripts=["scripts/linstor"], 206 | data_files=gen_data_files(), 207 | cmdclass={ 208 | "build_man": BuildManCommand, 209 | "versionup2date": CheckUpToDate 210 | }, 211 | test_suite="tests.test_without_controller" 212 | ) 213 | -------------------------------------------------------------------------------- /linstor_client/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | """ 4 | linstor - management of distributed DRBD9 resources 5 | Copyright (C) 2013 - 2017 LINBIT HA-Solutions GmbH 6 | Author: Robert Altnoeder, Roland Kammerer 7 | 8 | You can use this file under the terms of the GNU Lesser General 9 | Public License as as published by the Free Software Foundation, 10 | either version 3 of the License, or (at your option) any later 11 | version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU Lesser General Public License for more details. 17 | 18 | See . 19 | """ 20 | 21 | import subprocess 22 | import sys 23 | 24 | from linstor_client.consts import ( 25 | Color, 26 | ExitCode 27 | ) 28 | 29 | 30 | class Output(object): 31 | @staticmethod 32 | def handle_ret(answer, no_color, warn_as_error, outstream=sys.stdout): 33 | from linstor.sharedconsts import (MASK_ERROR, MASK_WARN, MASK_INFO) 34 | 35 | rc = answer.ret_code 36 | ret = 0 37 | message = answer.message 38 | cause = answer.cause 39 | correction = answer.correction 40 | details = answer.details 41 | if rc & MASK_ERROR == MASK_ERROR: 42 | ret = ExitCode.API_ERROR 43 | category = Output.color_str('ERROR:\n', Color.RED, no_color) 44 | elif rc & MASK_WARN == MASK_WARN: 45 | if warn_as_error: # otherwise keep at 0 46 | ret = ExitCode.API_ERROR 47 | category = Output.color_str('WARNING:\n', Color.YELLOW, no_color) 48 | elif rc & MASK_INFO == MASK_INFO: 49 | category = Output.color_str('INFO:\n', Color.BLUE, no_color) 50 | else: # do not use MASK_SUCCESS 51 | category = Output.color_str('SUCCESS:\n', Color.GREEN, no_color) 52 | 53 | outstream.write(category) 54 | have_message = message is not None and len(message) > 0 55 | have_cause = cause is not None and len(cause) > 0 56 | have_correction = correction is not None and len(correction) > 0 57 | have_details = details is not None and len(details) > 0 58 | if (have_cause or have_correction or have_details) and have_message: 59 | outstream.write("Description:\n") 60 | if have_message: 61 | Output.print_with_indent(outstream, 4, message) 62 | if have_cause: 63 | outstream.write("Cause:\n") 64 | Output.print_with_indent(outstream, 4, cause) 65 | if have_correction: 66 | outstream.write("Correction:\n") 67 | Output.print_with_indent(outstream, 4, correction) 68 | if have_details: 69 | outstream.write("Details:\n") 70 | Output.print_with_indent(outstream, 4, details) 71 | 72 | if answer.error_report_ids: 73 | outstream.write("Show reports:\n") 74 | Output.print_with_indent(outstream, 4, "linstor error-reports show " + " ".join(answer.error_report_ids)) 75 | return ret 76 | 77 | @staticmethod 78 | def print_with_indent(stream, indent, text): 79 | spacer = indent * ' ' 80 | offset = 0 81 | index = 0 82 | while index < len(text): 83 | if text[index] == '\n': 84 | stream.write(spacer) 85 | stream.write(text[offset:index]) 86 | stream.write('\n') 87 | offset = index + 1 88 | index += 1 89 | if offset < len(text): 90 | stream.write(spacer) 91 | stream.write(text[offset:]) 92 | stream.write('\n') 93 | 94 | @staticmethod 95 | def color_str(string, color, no_color): 96 | return '%s%s%s' % (Output.color(color, no_color), string, Output.color(Color.NONE, no_color)) 97 | 98 | @staticmethod 99 | def color(col, no_color): 100 | if no_color: 101 | return '' 102 | else: 103 | return col 104 | 105 | @staticmethod 106 | def err(msg, no_color): 107 | Output.bail_out(msg, Color.RED, 1, no_color) 108 | 109 | @staticmethod 110 | def bail_out(msg, color, ret, no_color): 111 | sys.stderr.write(Output.color_str(msg, color, no_color) + '\n') 112 | sys.exit(ret) 113 | 114 | @staticmethod 115 | def utf8(msg): 116 | # e.g., redirect, then force utf8 encoding (default on py3) 117 | if sys.stdout.encoding is None: 118 | msg = msg.encode('utf-8') 119 | return msg 120 | 121 | 122 | # a wrapper for subprocess.check_output 123 | def check_output(*args, **kwargs): 124 | def _wrapcall_2_6(*args, **kwargs): 125 | # no check_output in 2.6 126 | if "stdout" in kwargs: 127 | raise ValueError("stdout argument not allowed, it will be overridden.") 128 | process = subprocess.Popen(stdout=subprocess.PIPE, *args, **kwargs) 129 | output, unused_err = process.communicate() 130 | retcode = process.poll() 131 | if retcode: 132 | cmd = kwargs.get("args") 133 | if cmd is None: 134 | cmd = args[0] 135 | raise subprocess.CalledProcessError(retcode, cmd) 136 | return output 137 | 138 | try: 139 | return subprocess.check_output(*args, **kwargs) 140 | except AttributeError: 141 | return _wrapcall_2_6(*args, **kwargs) 142 | 143 | 144 | # base range check 145 | def checkrange(v, i, j): 146 | return i <= v <= j 147 | 148 | 149 | # "type" used for argparse 150 | def rangecheck(i, j): 151 | def range(v): 152 | v = int(v) 153 | if not checkrange(v, i, j): 154 | raise LinstorClientError('%d not in range: [%d, %d]' % (v, i, j), exit_code=2) 155 | return v 156 | return range 157 | 158 | 159 | def ip_completer(where): 160 | def completer(prefix, parsed_args, **kwargs): 161 | import socket 162 | opt = where 163 | if opt == "name": 164 | name = parsed_args.name 165 | elif opt == "peer_ip": 166 | name = parsed_args.peer_ip 167 | else: 168 | return "" 169 | 170 | ip = socket.gethostbyname(name) 171 | ip = [ip] 172 | return ip 173 | return completer 174 | 175 | 176 | # mainly used for DrbdSetupOpts() 177 | # but also usefull for 'handlers' subcommand 178 | def filter_new_args(unsetprefix, args): 179 | new = dict() 180 | reserved_keys = [ 181 | "func", "optsobj", "common", "command", 182 | "controllers", "warn_as_error", "no_utf8", "no_color", 183 | "machine_readable", "disable_config", "timeout", 184 | "verbose", "output_version", "curl", "allow_insecure_auth", 185 | "certfile", "keyfile", "cafile" 186 | ] 187 | for k, v in args.__dict__.items(): 188 | if v is not None and k not in reserved_keys: 189 | key = k.replace('_', '-') 190 | 191 | # handle --unset 192 | if key.startswith(unsetprefix) and not v: 193 | continue 194 | 195 | strv = str(v) 196 | if strv == 'False': 197 | strv = 'no' 198 | if strv == 'True': 199 | strv = 'yes' 200 | 201 | new[key] = strv 202 | 203 | for k in new.keys(): 204 | if "unset-" + k in new: 205 | sys.stderr.write('Error: You are not allowed to set and unset' 206 | ' an option at the same time!\n') 207 | return False 208 | return new 209 | 210 | 211 | def filter_prohibited(to_filter, prohibited): 212 | for k in prohibited: 213 | if k in to_filter: 214 | del (to_filter[k]) 215 | return to_filter 216 | 217 | 218 | def filter_allowed(to_filter, allowed): 219 | for k in to_filter.keys(): 220 | if k not in allowed: 221 | del (to_filter[k]) 222 | return to_filter 223 | 224 | 225 | class LinstorClientError(Exception): 226 | """ 227 | Linstor exception with a message and exit code information 228 | """ 229 | def __init__(self, msg, exit_code): 230 | self._msg = msg 231 | self._exit_code = exit_code 232 | 233 | @property 234 | def exit_code(self): 235 | return self._exit_code 236 | 237 | @property 238 | def message(self): 239 | return self._msg 240 | 241 | def __str__(self): 242 | return "Error: {msg}".format(msg=self._msg) 243 | 244 | def __repr__(self): 245 | return "LinstorError('{msg}', {ec})".format(msg=self._msg, ec=self._exit_code) 246 | -------------------------------------------------------------------------------- /linstor_client/commands/migrate_cmds.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from linstor_client.commands import Commands 5 | from linstor.sharedconsts import ( 6 | VAL_NODE_TYPE_STLT, 7 | VAL_NODE_TYPE_CTRL, 8 | VAL_NODE_TYPE_AUX, 9 | VAL_NODE_TYPE_CMBD, 10 | ) 11 | 12 | 13 | class MigrateCommands(Commands): 14 | _pool = 'drbdpool' 15 | 16 | @staticmethod 17 | def _header(of): 18 | of.write(''' 19 | ### IMPORTANT: ### 20 | # -) start with a setup where drbdmanage is in a healthy state (drbdmanage a) 21 | # -) make sure the LINSTOR DB is empty on the controller node 22 | # -) drbdmanage shutdown -qc # on all nodes 23 | # -) mv /etc/drbd.d/drbdctrl.res{,.dis} # on all nodes 24 | # -) mv /etc/drbd.d/drbdmanage-resources.res{,.dis} # on all nodes 25 | # 26 | # CURRENTLY THIS SCRIPT IS MISSING THESE FEATURES: 27 | # -) snapshots (will not be supported) 28 | # 29 | # If the controller is not executed on the local host, set this variable: 30 | LS_CONTROLLERS="localhost" 31 | export LS_CONTROLLERS 32 | # 33 | # This script is meant to be reviewed for plausibility 34 | # To make sure you did that, you have to remove the following line 35 | echo "migration disabled, review script and remove this line"; exit 1\n 36 | ''') 37 | 38 | @staticmethod 39 | def lsc(of, cmd, *args): 40 | of.write('linstor %s %s\n' % (cmd, ' '.join(args))) 41 | 42 | @staticmethod 43 | def _get_selection(question, options, default=''): 44 | # py2/3 45 | if sys.version_info < (3,): 46 | my_input = raw_input 47 | else: 48 | my_input = input 49 | 50 | def ask(prefix): 51 | if default: 52 | prefix += ' or for "%s"' % default 53 | answer = my_input('%s: ' % prefix) 54 | if answer == '': # 55 | return default # which is the set default or '' 56 | return answer 57 | 58 | os.system('clear') 59 | while True: 60 | sys.stdout.write('%s\n\n' % question) 61 | 62 | if len(options) > 0: 63 | for k in sorted(options.keys()): 64 | sys.stdout.write('%s) %s\n' % (k, options[k])) 65 | ans = ask('Type a number') 66 | try: 67 | if ans != default: 68 | ans = options.get(int(ans), False) 69 | except ValueError: 70 | continue 71 | else: 72 | ans = ask('Your answer') 73 | 74 | if ans: 75 | return ans 76 | 77 | @staticmethod 78 | def _get_node_type(name, default=''): 79 | node_types = { 80 | 1: VAL_NODE_TYPE_CTRL, 81 | 2: VAL_NODE_TYPE_CMBD, 82 | 3: VAL_NODE_TYPE_STLT, 83 | 4: VAL_NODE_TYPE_AUX, 84 | } 85 | 86 | return MigrateCommands._get_selection('Node type for ' + name, node_types, default) 87 | 88 | @staticmethod 89 | def _create_resource(of, res_name, assg): 90 | overall_args = [] 91 | for nr, v in assg.items(): 92 | n, r = nr.split(':') 93 | if r == res_name: 94 | diskless = False 95 | args = ['--node-id', str(v['_node_id']), ] 96 | if v['_tstate'] == 7: 97 | args.append('--diskless') 98 | diskless = True 99 | else: 100 | args += ['--storage-pool', MigrateCommands._pool] 101 | args += [n, r] 102 | 103 | # order does not really matter, but we want at least one node with disk 104 | # before we create the first diskless. 105 | if diskless: 106 | overall_args.append(args) 107 | else: 108 | overall_args.insert(0, args) 109 | 110 | needs_transaction = True if len(overall_args) > 1 else False 111 | 112 | if needs_transaction: 113 | MigrateCommands.lsc(of, 'resource', 'create-transactional', 114 | 'begin', '--terminate-on-error', '<= 10 else "0" + vnr_str 205 | MigrateCommands.lsc(of, 'volume-definition', 'create', '--vlmnr', vnr_str, 206 | '--minor', str(vol['minor']), r, str(vol['_size_kiB']) + 'K') 207 | MigrateCommands.lsc(of, 'volume-definition', 'set-property', r, vnr_str, 208 | 'OverrideVlmId', bdname) 209 | cgi = vol.get('props', {}).get('current-gi', None) 210 | if cgi is not None: 211 | MigrateCommands.lsc(of, 'volume-definition', 'set-property', r, vnr_str, 212 | 'DrbdCurrentGi', '{:0>16}'.format(cgi)) 213 | 214 | MigrateCommands._create_resource(of, r, assg) 215 | of.write('\n') 216 | 217 | of.close() 218 | sys.stdout.write('Successfully wrote %s\n' % (args.script)) 219 | return None 220 | -------------------------------------------------------------------------------- /linstor_client/commands/vlm_grp_cmds.py: -------------------------------------------------------------------------------- 1 | import linstor_client.argparse.argparse as argparse 2 | 3 | import linstor_client 4 | from linstor.responses import VolumeGroupResponse # noqa: F401 5 | from linstor_client.commands import Commands, DrbdOptions 6 | 7 | 8 | class VolumeGroupCommands(Commands): 9 | OBJECT_NAME = 'volume-definition' 10 | 11 | _vlm_grp_headers = [ 12 | linstor_client.TableHeader("VolumeNr"), 13 | linstor_client.TableHeader("Flags") 14 | ] 15 | 16 | def __init__(self): 17 | super(VolumeGroupCommands, self).__init__() 18 | 19 | def setup_commands(self, parser): 20 | subcmds = [ 21 | Commands.Subcommands.Create, 22 | Commands.Subcommands.List, 23 | Commands.Subcommands.Delete, 24 | Commands.Subcommands.SetProperty, 25 | Commands.Subcommands.ListProperties, 26 | Commands.Subcommands.DrbdOptions 27 | ] 28 | 29 | # volume group subcommands 30 | vlm_grp_parser = parser.add_parser( 31 | Commands.VOLUME_GRP, 32 | aliases=["vg"], 33 | formatter_class=argparse.RawTextHelpFormatter, 34 | description="Resource definition subcommands") 35 | 36 | vlm_grp_subp = vlm_grp_parser.add_subparsers( 37 | title="resource definition subcommands", 38 | metavar="", 39 | description=Commands.Subcommands.generate_desc(subcmds) 40 | ) 41 | 42 | # ------------ CREATE START 43 | p_new_vlm_grp = vlm_grp_subp.add_parser( 44 | Commands.Subcommands.Create.LONG, 45 | aliases=[Commands.Subcommands.Create.SHORT], 46 | description='Creates a LINSTOR volume group.') 47 | p_new_vlm_grp.add_argument('name', 48 | type=str, 49 | help='Name of the resource group.') 50 | p_new_vlm_grp.add_argument('-n', '--vlmnr', type=int) 51 | p_new_vlm_grp.add_argument('--gross', action="store_true", help="Size for this volume is gross size.") 52 | p_new_vlm_grp.set_defaults(func=self.create) 53 | # ------------ CREATE END 54 | 55 | # ------------ DELETE START 56 | p_rm_vlm_grp = vlm_grp_subp.add_parser( 57 | Commands.Subcommands.Delete.LONG, 58 | aliases=[Commands.Subcommands.Delete.SHORT], 59 | description=" Removes a volume group from the LINSTOR cluster.") 60 | p_rm_vlm_grp.add_argument( 61 | 'name', 62 | help='Name of the resource group').completer = self.resource_grp_completer 63 | p_rm_vlm_grp.add_argument( 64 | 'volume_nr', 65 | type=int, 66 | help="Volume number to delete.") 67 | p_rm_vlm_grp.set_defaults(func=self.delete) 68 | # ------------ DELETE END 69 | 70 | # ------------ LIST START 71 | vlm_grp_groupby = [x.name.lower() for x in self._vlm_grp_headers] 72 | vlm_grp_group_completer = Commands.show_group_completer(vlm_grp_groupby, "groupby") 73 | 74 | p_lvlmgrps = vlm_grp_subp.add_parser( 75 | Commands.Subcommands.List.LONG, 76 | aliases=[Commands.Subcommands.List.SHORT], 77 | description='Lists all volume groups for a specified resource group. ' 78 | 'By default, the list is printed as a human readable table.') 79 | p_lvlmgrps.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output') 80 | p_lvlmgrps.add_argument('-g', '--groupby', nargs='+', 81 | choices=vlm_grp_groupby, 82 | type=str.lower).completer = vlm_grp_group_completer 83 | p_lvlmgrps.add_argument( 84 | '-s', 85 | '--show-props', 86 | nargs='+', 87 | type=str, 88 | default=[], 89 | help='Show these props in the list. ' 90 | + 'Can be key=value pairs where key is the property name and value column header') 91 | p_lvlmgrps.add_argument('name', help="Resource group name.") 92 | p_lvlmgrps.set_defaults(func=self.list) 93 | # ------------ LIST END 94 | 95 | # ------------ LISTPROPS START 96 | p_sp = vlm_grp_subp.add_parser( 97 | Commands.Subcommands.ListProperties.LONG, 98 | aliases=[Commands.Subcommands.ListProperties.SHORT], 99 | description="Shows all properties of the specified volume group.") 100 | p_sp.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output') 101 | p_sp.add_argument( 102 | 'name', 103 | help="Resource group for which to print the properties" 104 | ).completer = self.resource_grp_completer 105 | p_sp.add_argument( 106 | 'volume_nr', 107 | type=int, 108 | help="Volume number") 109 | p_sp.set_defaults(func=self.print_props) 110 | # ------------ LISTPROPS END 111 | 112 | # ------------ SETPROPS START 113 | p_setprop = vlm_grp_subp.add_parser( 114 | Commands.Subcommands.SetProperty.LONG, 115 | aliases=[Commands.Subcommands.SetProperty.SHORT], 116 | formatter_class=argparse.RawTextHelpFormatter, 117 | description='Sets properties for the given volume group.') 118 | p_setprop.add_argument('name', type=str, help='Name of the resource group') 119 | p_setprop.add_argument( 120 | 'volume_nr', 121 | type=int, 122 | help="Volume number") 123 | Commands.add_parser_keyvalue(p_setprop, self.OBJECT_NAME) 124 | p_setprop.set_defaults(func=self.set_props) 125 | # ------------ SETPROPS END 126 | 127 | # ------------ SETDRBDOPTS START 128 | p_drbd_opts = vlm_grp_subp.add_parser( 129 | Commands.Subcommands.DrbdOptions.LONG, 130 | aliases=[Commands.Subcommands.DrbdOptions.SHORT], 131 | description=DrbdOptions.description("resource") 132 | ) 133 | p_drbd_opts.add_argument( 134 | 'name', 135 | type=str, 136 | help="Resource group name" 137 | ).completer = self.resource_grp_completer 138 | p_drbd_opts.add_argument( 139 | 'volume_nr', 140 | type=int, 141 | help="Volume number") 142 | DrbdOptions.add_arguments(p_drbd_opts, self.OBJECT_NAME) 143 | p_drbd_opts.set_defaults(func=self.set_drbd_opts) 144 | # ------------ SETDRBDOPTS END 145 | 146 | self.check_subcommands(vlm_grp_subp, subcmds) 147 | 148 | def create(self, args): 149 | replies = self._linstor.volume_group_create( 150 | args.name, 151 | volume_nr=args.vlmnr, 152 | gross=args.gross 153 | ) 154 | return self.handle_replies(args, replies) 155 | 156 | def delete(self, args): 157 | replies = self._linstor.volume_group_delete(args.name, args.volume_nr) 158 | return self.handle_replies(args, replies) 159 | 160 | @classmethod 161 | def show(cls, args, lstmsg): 162 | vlm_grps = lstmsg # type: VolumeGroupResponse 163 | tbl = linstor_client.Table(utf8=not args.no_utf8, colors=not args.no_color, pastable=args.pastable) 164 | 165 | for hdr in cls._vlm_grp_headers: 166 | tbl.add_header(hdr) 167 | 168 | show_props = cls._append_show_props_hdr(tbl, args.show_props) 169 | 170 | tbl.set_groupby(args.groupby if args.groupby else [tbl.header_name(0)]) 171 | 172 | for vlm_grp in vlm_grps.volume_groups: 173 | row = [str(vlm_grp.number), ", ".join(vlm_grp.flags)] 174 | for sprop in show_props: 175 | row.append(vlm_grp.properties.get(sprop, '')) 176 | tbl.add_row(row) 177 | tbl.show() 178 | 179 | def list(self, args): 180 | args = self.merge_config_args('volume-group.list', args) 181 | lstmsg = [self._linstor.volume_group_list_raise(args.name)] 182 | return self.output_list(args, lstmsg, self.show) 183 | 184 | @classmethod 185 | def _props_list(cls, args, lstmsg): 186 | """ 187 | 188 | :param args: 189 | :param linstor.responses.VolumeGroupResponse lstmsg: 190 | :return: 191 | """ 192 | result = [] 193 | if lstmsg: 194 | for vlm_grp in lstmsg.volume_groups: 195 | if vlm_grp.number == args.volume_nr: 196 | result.append(vlm_grp.properties) 197 | break 198 | return result 199 | 200 | def print_props(self, args): 201 | lstmsg = [self._linstor.volume_group_list_raise(args.name)] 202 | 203 | return self.output_props_list(args, lstmsg, self._props_list) 204 | 205 | def set_props(self, args): 206 | args = self._attach_aux_prop(args) 207 | mod_prop_dict = Commands.parse_key_value_pairs([(args.key, args.value)]) 208 | replies = self._linstor.volume_group_modify( 209 | args.name, 210 | args.volume_nr, 211 | mod_prop_dict['pairs'], 212 | mod_prop_dict['delete'] 213 | ) 214 | return self.handle_replies(args, replies) 215 | 216 | def set_drbd_opts(self, args): 217 | a = DrbdOptions.filter_new(args) 218 | del a['name'] # remove resource group key 219 | del a['volume-nr'] 220 | 221 | mod_props, del_props = DrbdOptions.parse_opts(a, self.OBJECT_NAME) 222 | 223 | replies = self._linstor.volume_group_modify( 224 | args.name, 225 | args.volume_nr, 226 | mod_props, 227 | del_props 228 | ) 229 | return self.handle_replies(args, replies) 230 | -------------------------------------------------------------------------------- /linstor_client/commands/drbd_proxy_cmds.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from linstor.sharedconsts import VAL_DRBD_PROXY_COMPRESSION_NONE, VAL_DRBD_PROXY_COMPRESSION_ZLIB, \ 4 | VAL_DRBD_PROXY_COMPRESSION_LZMA, VAL_DRBD_PROXY_COMPRESSION_LZ4, VAL_DRBD_PROXY_COMPRESSION_ZSTD 5 | import linstor_client.argparse.argparse as argparse 6 | from linstor_client.commands import Commands, DrbdOptions, ArgumentError 7 | from linstor_client.utils import rangecheck 8 | 9 | 10 | class DrbdProxyCommands(Commands): 11 | OBJECT_NAME = 'drbd-proxy' 12 | OBJECT_NAME_LZMA = 'drbd-proxy-lzma' 13 | OBJECT_NAME_ZLIB = 'drbd-proxy-zlib' 14 | OBJECT_NAME_ZSTD = 'drbd-proxy-zstd' 15 | 16 | class Enable(object): 17 | LONG = "enable" 18 | SHORT = "e" 19 | 20 | class Disable(object): 21 | LONG = "disable" 22 | SHORT = "d" 23 | 24 | class Options(object): 25 | LONG = "options" 26 | SHORT = "opt" 27 | 28 | class Compression(object): 29 | LONG = "compression" 30 | SHORT = "c" 31 | 32 | class NoCompression(object): 33 | LONG = "none" 34 | SHORT = "none" 35 | 36 | class Zlib(object): 37 | LONG = "zlib" 38 | SHORT = "zlib" 39 | 40 | class Lzma(object): 41 | LONG = "lzma" 42 | SHORT = "lzma" 43 | 44 | class Lz4(object): 45 | LONG = "lz4" 46 | SHORT = "lz4" 47 | 48 | class Zstd(object): 49 | LONG = "zstd" 50 | SHORT = "zstd" 51 | 52 | def __init__(self): 53 | super(DrbdProxyCommands, self).__init__() 54 | 55 | def setup_commands(self, parser): 56 | subcmds = [ 57 | self.Enable, 58 | self.Disable, 59 | self.Options, 60 | self.Compression 61 | ] 62 | 63 | res_conn_parser = parser.add_parser( 64 | Commands.DRBD_PROXY, 65 | aliases=["proxy"], 66 | formatter_class=argparse.RawTextHelpFormatter, 67 | description="DRBD Proxy subcommands") 68 | subp = res_conn_parser.add_subparsers( 69 | title="DRBD Proxy commands", 70 | metavar="", 71 | description=Commands.Subcommands.generate_desc(subcmds) 72 | ) 73 | 74 | # enable proxy 75 | p_proxy_enable = subp.add_parser( 76 | self.Enable.LONG, 77 | aliases=[self.Enable.SHORT], 78 | description='Enables DRBD Proxy on a resource connection.') 79 | p_proxy_enable.add_argument( 80 | 'node_name_a', 81 | help="Node name source of the connection.").completer = self.node_completer 82 | p_proxy_enable.add_argument( 83 | 'node_name_b', 84 | help="Node name target of the connection.").completer = self.node_completer 85 | p_proxy_enable.add_argument( 86 | 'resource_name', 87 | type=str, 88 | help='Name of the resource' 89 | ).completer = self.resource_completer 90 | p_proxy_enable.add_argument('-p', '--port', type=rangecheck(1, 65535)) 91 | p_proxy_enable.set_defaults(func=self.enable) 92 | 93 | # disable proxy 94 | p_proxy_disable = subp.add_parser( 95 | self.Disable.LONG, 96 | aliases=[self.Disable.SHORT], 97 | description='Disables DRBD Proxy on a resource connection.') 98 | p_proxy_disable.add_argument( 99 | 'node_name_a', 100 | help="Node name source of the connection.").completer = self.node_completer 101 | p_proxy_disable.add_argument( 102 | 'node_name_b', 103 | help="Node name target of the connection.").completer = self.node_completer 104 | p_proxy_disable.add_argument( 105 | 'resource_name', 106 | type=str, 107 | help='Name of the resource' 108 | ).completer = self.resource_completer 109 | p_proxy_disable.set_defaults(func=self.disable) 110 | 111 | # drbd options 112 | p_drbd_opts = subp.add_parser( 113 | self.Options.LONG, 114 | aliases=[self.Options.SHORT], 115 | description=DrbdOptions.description("resource") 116 | ) 117 | self._add_resource_name_argument(p_drbd_opts) 118 | DrbdOptions.add_arguments(p_drbd_opts, self.OBJECT_NAME) 119 | p_drbd_opts.set_defaults(func=self.set_drbd_opts) 120 | 121 | compression_subcmds = [ 122 | self.NoCompression, 123 | self.Zlib, 124 | self.Lzma, 125 | self.Lz4, 126 | self.Zstd 127 | ] 128 | 129 | p_compression = subp.add_parser( 130 | self.Compression.LONG, 131 | aliases=[self.Compression.SHORT], 132 | formatter_class=argparse.RawTextHelpFormatter, 133 | description='DRBD Proxy compression subcommands. ' 134 | 'Each subcommand overrides any previous compression configuration.' 135 | ) 136 | compression_subp = p_compression.add_subparsers( 137 | title="DRBD Proxy compression options", 138 | metavar="", 139 | description=Commands.Subcommands.generate_desc(compression_subcmds) 140 | ) 141 | 142 | p_compression_none = compression_subp.add_parser( 143 | self.NoCompression.LONG, 144 | aliases=[self.NoCompression.SHORT], 145 | description='Do not use compression.' 146 | ) 147 | self._add_resource_name_argument(p_compression_none) 148 | p_compression_none.set_defaults(func=self.set_compression, compression_type=VAL_DRBD_PROXY_COMPRESSION_NONE) 149 | 150 | p_compression_zlib = compression_subp.add_parser( 151 | self.Zlib.LONG, 152 | aliases=[self.Zlib.SHORT], 153 | description='Use ZLIB compression. Options are reset to those given here.' 154 | ) 155 | self._add_resource_name_argument(p_compression_zlib) 156 | DrbdOptions.add_arguments(p_compression_zlib, self.OBJECT_NAME_ZLIB, allow_unset=False) 157 | p_compression_zlib.set_defaults(func=self.set_compression, compression_type=VAL_DRBD_PROXY_COMPRESSION_ZLIB) 158 | 159 | p_compression_lzma = compression_subp.add_parser( 160 | self.Lzma.LONG, 161 | aliases=[self.Lzma.SHORT], 162 | description='Use LZMA compression. Options are reset to those given here.' 163 | ) 164 | self._add_resource_name_argument(p_compression_lzma) 165 | DrbdOptions.add_arguments(p_compression_lzma, self.OBJECT_NAME_LZMA, allow_unset=False) 166 | p_compression_lzma.set_defaults(func=self.set_compression, compression_type=VAL_DRBD_PROXY_COMPRESSION_LZMA) 167 | 168 | p_compression_lz4 = compression_subp.add_parser( 169 | self.Lz4.LONG, 170 | aliases=[self.Lz4.SHORT], 171 | description='Use LZ4 compression.' 172 | ) 173 | self._add_resource_name_argument(p_compression_lz4) 174 | p_compression_lz4.set_defaults(func=self.set_compression, compression_type=VAL_DRBD_PROXY_COMPRESSION_LZ4) 175 | 176 | p_compression_zstd = compression_subp.add_parser( 177 | self.Zstd.LONG, 178 | aliases=[self.Zstd.SHORT], 179 | description='Use ZStandard compression.' 180 | ) 181 | self._add_resource_name_argument(p_compression_zstd) 182 | p_compression_zstd.set_defaults(func=self.set_compression, compression_type=VAL_DRBD_PROXY_COMPRESSION_ZSTD) 183 | 184 | self.check_subcommands(compression_subp, compression_subcmds) 185 | 186 | self.check_subcommands(subp, subcmds) 187 | 188 | def _add_resource_name_argument(self, parser): 189 | parser.add_argument( 190 | 'resource_name', 191 | type=str, 192 | help="Resource name" 193 | ).completer = self.resource_dfn_completer 194 | 195 | def enable(self, args): 196 | replies = self._linstor.drbd_proxy_enable( 197 | args.resource_name, 198 | args.node_name_a, 199 | args.node_name_b, 200 | args.port 201 | ) 202 | return self.handle_replies(args, replies) 203 | 204 | def disable(self, args): 205 | replies = self._linstor.drbd_proxy_disable( 206 | args.resource_name, 207 | args.node_name_a, 208 | args.node_name_b 209 | ) 210 | return self.handle_replies(args, replies) 211 | 212 | def set_drbd_opts(self, args): 213 | a = DrbdOptions.filter_new(args) 214 | del a['resource-name'] # remove resource name key 215 | 216 | mod_props, del_props = DrbdOptions.parse_opts(a, self.OBJECT_NAME) 217 | 218 | replies = self._linstor.drbd_proxy_modify( 219 | args.resource_name, 220 | mod_props, 221 | del_props 222 | ) 223 | return self.handle_replies(args, replies) 224 | 225 | def set_compression(self, args): 226 | a = DrbdOptions.filter_new(args) 227 | del a['resource-name'] # remove resource name key 228 | del a['compression-type'] # remove compression_type key 229 | 230 | if args.compression_type == VAL_DRBD_PROXY_COMPRESSION_NONE: 231 | set_props = {} 232 | elif args.compression_type == VAL_DRBD_PROXY_COMPRESSION_ZLIB: 233 | set_props, _ = DrbdOptions.parse_opts(a, self.OBJECT_NAME_ZLIB) 234 | elif args.compression_type == VAL_DRBD_PROXY_COMPRESSION_LZMA: 235 | set_props, _ = DrbdOptions.parse_opts(a, self.OBJECT_NAME_LZMA) 236 | elif args.compression_type == VAL_DRBD_PROXY_COMPRESSION_ZSTD: 237 | set_props, _ = DrbdOptions.parse_opts(a, self.OBJECT_NAME_ZSTD) 238 | elif args.compression_type == VAL_DRBD_PROXY_COMPRESSION_LZ4: 239 | set_props = {} 240 | else: 241 | raise ArgumentError("Unknown compression type") 242 | 243 | replies = self._linstor.drbd_proxy_modify( 244 | args.resource_name, 245 | compression_type=args.compression_type, 246 | compression_property_dict=set_props 247 | ) 248 | return self.handle_replies(args, replies) 249 | -------------------------------------------------------------------------------- /linstor_client/argcomplete/LICENSE.rst: -------------------------------------------------------------------------------- 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 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /linstor_client/commands/controller_cmds.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import linstor 4 | from linstor import LogLevelEnum 5 | import linstor_client.argparse.argparse as argparse 6 | from linstor_client.commands import Commands, DrbdOptions 7 | 8 | 9 | class ControllerCommands(Commands): 10 | OBJECT_NAME = 'controller' 11 | 12 | def __init__(self): 13 | super(ControllerCommands, self).__init__() 14 | 15 | def setup_commands(self, parser): 16 | # Controller commands 17 | subcmds = [ 18 | Commands.Subcommands.SetProperty, 19 | Commands.Subcommands.ListProperties, 20 | Commands.Subcommands.DrbdOptions, 21 | Commands.Subcommands.Version, 22 | Commands.Subcommands.QueryMaxVlmSize, 23 | Commands.Subcommands.Which, 24 | Commands.Subcommands.BackupDb, 25 | Commands.Subcommands.LogLevel 26 | ] 27 | 28 | con_parser = parser.add_parser( 29 | Commands.CONTROLLER, 30 | aliases=["c"], 31 | formatter_class=argparse.RawTextHelpFormatter, 32 | description="Controller subcommands") 33 | 34 | con_subp = con_parser.add_subparsers( 35 | title="Controller commands", 36 | metavar="", 37 | description=Commands.Subcommands.generate_desc(subcmds) 38 | ) 39 | 40 | # Controller - get props 41 | c_ctrl_props = con_subp.add_parser( 42 | Commands.Subcommands.ListProperties.LONG, 43 | aliases=[Commands.Subcommands.ListProperties.SHORT], 44 | description='Print current controller config properties.') 45 | c_ctrl_props.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output') 46 | c_ctrl_props.set_defaults(func=self.cmd_print_controller_props) 47 | 48 | # controller - set props 49 | c_set_ctrl_props = con_subp.add_parser( 50 | Commands.Subcommands.SetProperty.LONG, 51 | aliases=[Commands.Subcommands.SetProperty.SHORT], 52 | formatter_class=argparse.RawTextHelpFormatter, 53 | description='Set a controller config property.') 54 | Commands.add_parser_keyvalue(c_set_ctrl_props, "controller") 55 | c_set_ctrl_props.set_defaults(func=self.set_props) 56 | 57 | c_drbd_opts = con_subp.add_parser( 58 | Commands.Subcommands.DrbdOptions.LONG, 59 | aliases=[Commands.Subcommands.DrbdOptions.SHORT], 60 | description=DrbdOptions.description("drbd") 61 | ) 62 | DrbdOptions.add_arguments(c_drbd_opts, self.OBJECT_NAME) 63 | c_drbd_opts.set_defaults(func=self.cmd_controller_drbd_opts) 64 | 65 | # Controller - set-log-level 66 | c_set_log_level = con_subp.add_parser( 67 | Commands.Subcommands.LogLevel.LONG, 68 | aliases=[Commands.Subcommands.LogLevel.SHORT], 69 | description="Sets the log level") 70 | c_set_log_level.add_argument('level', 71 | type=LogLevelEnum.check, 72 | choices=list(LogLevelEnum)) 73 | c_set_log_level.add_argument('--library', '--lib', 74 | action='store_true', 75 | help='Modify the log level of external libraries instead of LINSTOR itself') 76 | c_set_log_level.add_argument('--global', 77 | action='store_true', 78 | dest='glob', # "global" is a reserved keyword 79 | help='Set the log level for the controller and ALL satellites') 80 | c_set_log_level.set_defaults(func=self.cmd_controller_set_log_level) 81 | 82 | # Controller - version 83 | c_shutdown = con_subp.add_parser( 84 | Commands.Subcommands.Version.LONG, 85 | aliases=[Commands.Subcommands.Version.SHORT], 86 | description='Prints the LINSTOR controller version.' 87 | ) 88 | c_shutdown.set_defaults(func=self.cmd_version) 89 | 90 | p_query_max_vlm_size = con_subp.add_parser( 91 | Commands.Subcommands.QueryMaxVlmSize.LONG, 92 | aliases=[Commands.Subcommands.QueryMaxVlmSize.SHORT], 93 | description='Queries the controller for the maximum volume size of storage pools, given a specified ' 94 | 'replica count.') 95 | p_query_max_vlm_size.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output') 96 | p_query_max_vlm_size.add_argument( 97 | '--storage-pool', '-s', 98 | type=str, 99 | help="Storage pool name to query.").completer = self.storage_pool_dfn_completer 100 | p_query_max_vlm_size.add_argument( 101 | '--do-not-place-with', 102 | type=str, 103 | nargs='+', 104 | metavar="RESOURCE_NAME", 105 | help='Try to avoid nodes that already have a given resource deployed.' 106 | ).completer = self.resource_completer 107 | p_query_max_vlm_size.add_argument( 108 | '--do-not-place-with-regex', 109 | type=str, 110 | metavar="RESOURCE_REGEX", 111 | help='Try to avoid nodes that already have a resource ' 112 | 'deployed whos name is matching the given regular expression.' 113 | ) 114 | p_query_max_vlm_size.add_argument( 115 | '--replicas-on-same', 116 | nargs='+', 117 | default=[], 118 | metavar="AUX_NODE_PROPERTY", 119 | help='Tries to place resources on nodes with the same given auxiliary node property values.' 120 | ) 121 | p_query_max_vlm_size.add_argument( 122 | '--replicas-on-different', 123 | nargs='+', 124 | default=[], 125 | metavar="AUX_NODE_PROPERTY", 126 | help='Tries to place resources on nodes with a different value for the given auxiliary node property.' 127 | ) 128 | p_query_max_vlm_size.add_argument( 129 | '--x-replicas-on-different', 130 | nargs='*', 131 | metavar="AUX_PROPERTY", 132 | help='Accepts a list of pairs as argument. Example: "--x-replicas-on-different datacenter 2" will allow 2 ' 133 | 'replicas on nodes that have the same value for the property "Aux/datacenter"' 134 | ) 135 | p_query_max_vlm_size.add_argument( 136 | 'replica_count', 137 | type=int, 138 | metavar="REPLICA_COUNT", 139 | help='The least amount of replicas.' 140 | ) 141 | p_query_max_vlm_size.set_defaults(func=self.query_max_volume_size) 142 | 143 | p_which_controller = con_subp.add_parser( 144 | Commands.Subcommands.Which.LONG, 145 | description='Shows controller currently used.') 146 | p_which_controller.set_defaults(func=self.which_controller) 147 | 148 | p_backup_db = con_subp.add_parser( 149 | Commands.Subcommands.BackupDb.LONG, 150 | aliases=[Commands.Subcommands.BackupDb.SHORT], 151 | description='Create a backup of the controller database.' 152 | ) 153 | p_backup_db.add_argument( 154 | 'backup_name', 155 | metavar="BACKUP_NAME", 156 | help='Base name of the backup' 157 | ) 158 | p_backup_db.set_defaults(func=self.backup_controller_db) 159 | 160 | self.check_subcommands(con_subp, subcmds) 161 | 162 | @classmethod 163 | def _props_list(cls, args, lstmsg): 164 | result = [] 165 | if lstmsg: 166 | result.append(lstmsg.properties) 167 | return result 168 | 169 | def cmd_print_controller_props(self, args): 170 | lstmsg = self._linstor.controller_props() 171 | 172 | return self.output_props_list(args, lstmsg, self._props_list) 173 | 174 | def set_props(self, args): 175 | args = self._attach_aux_prop(args) 176 | props = Commands.parse_key_value_pairs([(args.key, args.value)]) 177 | 178 | replies = [] 179 | for prop_key, prop_value in props['pairs'].items(): 180 | replies.extend(self._linstor.controller_set_prop(prop_key, prop_value)) 181 | for prop_key in props['delete']: 182 | replies.extend(self._linstor.controller_del_prop(prop_key)) 183 | 184 | return self.handle_replies(args, replies) 185 | 186 | def cmd_controller_drbd_opts(self, args): 187 | a = DrbdOptions.filter_new(args) 188 | 189 | mod_props, del_props = DrbdOptions.parse_opts(a, self.OBJECT_NAME) 190 | 191 | replies = [] 192 | for prop, val in mod_props.items(): 193 | replies.extend(self._linstor.controller_set_prop(prop, val)) 194 | 195 | for delkey in del_props: 196 | replies.extend(self._linstor.controller_del_prop(delkey)) 197 | 198 | return self.handle_replies(args, replies) 199 | 200 | def cmd_controller_set_log_level(self, args): 201 | replies = self._linstor.controller_set_log_level( 202 | args.level, 203 | args.glob if args.glob else False, 204 | args.library if args.library else False) 205 | 206 | return self.handle_replies(args, replies) 207 | 208 | def cmd_version(self, args): 209 | controller_info = self.get_linstorapi().controller_info() 210 | if controller_info: 211 | version_info = controller_info.split(',') 212 | if args.machine_readable: 213 | print(json.dumps(self.get_linstorapi().controller_version().data(args.output_version))) 214 | else: 215 | print("linstor controller " + version_info[2] + "; GIT-hash: " + version_info[3]) 216 | 217 | def query_max_volume_size(self, args): 218 | replies = self.get_linstorapi().storage_pool_dfn_max_vlm_sizes( 219 | args.replica_count, 220 | args.storage_pool, 221 | args.do_not_place_with, 222 | args.do_not_place_with_regex, 223 | [linstor.consts.NAMESPC_AUXILIARY + '/' + x for x in args.replicas_on_same], 224 | [linstor.consts.NAMESPC_AUXILIARY + '/' + x for x in args.replicas_on_different], 225 | self.prepare_argparse_dict_str_int(args.x_replicas_on_different, linstor.consts.NAMESPC_AUXILIARY + '/') 226 | ) 227 | 228 | api_responses = self.get_linstorapi().filter_api_call_response(replies) 229 | if api_responses: 230 | return self.handle_replies(args, api_responses) 231 | 232 | return self.output_list(args, replies, self._show_query_max_volume) 233 | 234 | def which_controller(self, args): 235 | ctrl_uri = self.get_linstorapi().controller_host() 236 | if args.machine_readable: 237 | print(json.dumps({"controller_uri": ctrl_uri})) 238 | else: 239 | print(ctrl_uri) 240 | 241 | def backup_controller_db(self, args): 242 | replies = self.get_linstorapi().controller_backupdb(args.backup_name) 243 | return self.handle_replies(args, replies) 244 | -------------------------------------------------------------------------------- /linstor_client/commands/node_conn_cmds.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | 5 | import linstor_client.argparse.argparse as argparse 6 | from linstor_client.commands import Commands, DrbdOptions 7 | from linstor_client import TableHeader, Table 8 | from linstor import consts as apiconsts 9 | 10 | 11 | class NodeConnectionCommands(Commands): 12 | DRBD_OBJECT_NAME = 'rsc-conn' # although this is a node-connection, for drbd-options we still want to use 13 | # resource-connections 14 | 15 | _headers = [ 16 | TableHeader("Node A"), 17 | TableHeader("Node B"), 18 | TableHeader("Properties") 19 | ] 20 | 21 | class Path(object): 22 | LONG = "path" 23 | SHORT = "p" 24 | 25 | def __init__(self): 26 | super(NodeConnectionCommands, self).__init__() 27 | 28 | def setup_commands(self, parser): 29 | subcmds = [ 30 | Commands.Subcommands.List, 31 | Commands.Subcommands.SetProperty, 32 | Commands.Subcommands.ListProperties, 33 | Commands.Subcommands.DrbdPeerDeviceOptions, 34 | NodeConnectionCommands.Path 35 | ] 36 | 37 | node_conn_parser = parser.add_parser( 38 | Commands.NODE_CONN, 39 | aliases=["nc"], 40 | formatter_class=argparse.RawTextHelpFormatter, 41 | description="Node connection subcommands") 42 | subp = node_conn_parser.add_subparsers( 43 | title="node connection commands", 44 | metavar="", 45 | description=Commands.Subcommands.generate_desc(subcmds) 46 | ) 47 | 48 | node_con_groubby = [x.name.lower() for x in NodeConnectionCommands._headers] 49 | node_group_completer = Commands.show_group_completer(node_con_groubby, "groupby") 50 | 51 | p_lnodeconn = subp.add_parser( 52 | Commands.Subcommands.List.LONG, 53 | aliases=[Commands.Subcommands.List.SHORT], 54 | description='Prints a list of all non-empty node connections. ' 55 | 'By default, the list is printed as a human readable table.' 56 | ) 57 | p_lnodeconn.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output') 58 | p_lnodeconn.add_argument( 59 | '-g', '--groupby', 60 | nargs='+', 61 | choices=node_con_groubby, 62 | type=str.lower).completer = node_group_completer 63 | p_lnodeconn.add_argument( 64 | '-s', 65 | '--show-props', 66 | nargs='+', 67 | type=str, 68 | default=[], 69 | help='Show these props in the list. ' 70 | + 'Can be key=value pairs where key is the property name and value column header') 71 | p_lnodeconn.add_argument( 72 | 'node_name_a', 73 | nargs='?', 74 | help="Node name" 75 | ).completer = self.node_completer 76 | p_lnodeconn.add_argument( 77 | 'node_name_b', 78 | nargs='?', 79 | help="Node name" 80 | ).completer = self.node_completer 81 | p_lnodeconn.set_defaults(func=self.list) 82 | 83 | # show properties 84 | p_sp = subp.add_parser( 85 | Commands.Subcommands.ListProperties.LONG, 86 | aliases=[Commands.Subcommands.ListProperties.SHORT], 87 | description="Prints all properties of the given node connection.") 88 | p_sp.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output') 89 | p_sp.add_argument( 90 | 'node_name_a', 91 | help="Node name source of the connection.").completer = self.node_completer 92 | p_sp.add_argument( 93 | 'node_name_b', 94 | help="Node name target of the connection.").completer = self.node_completer 95 | p_sp.set_defaults(func=self.print_props) 96 | 97 | # set properties 98 | p_setprop = subp.add_parser( 99 | Commands.Subcommands.SetProperty.LONG, 100 | aliases=[Commands.Subcommands.SetProperty.SHORT], 101 | formatter_class=argparse.RawTextHelpFormatter, 102 | description='Sets properties for the given node connection.') 103 | p_setprop.add_argument( 104 | 'node_name_a', 105 | help="Node name source of the connection.").completer = self.node_completer 106 | p_setprop.add_argument( 107 | 'node_name_b', 108 | help="Node name target of the connection.").completer = self.node_completer 109 | Commands.add_parser_keyvalue(p_setprop, "node-conn") 110 | p_setprop.set_defaults(func=self.set_props) 111 | 112 | # drbd peer device options 113 | p_drbd_peer_opts = subp.add_parser( 114 | Commands.Subcommands.DrbdPeerDeviceOptions.LONG, 115 | aliases=[ 116 | Commands.Subcommands.DrbdPeerDeviceOptions.SHORT 117 | ], 118 | description=DrbdOptions.description("peer-device") 119 | ) 120 | p_drbd_peer_opts.add_argument( 121 | 'node_a', 122 | type=str, 123 | help="1. Node in the node connection" 124 | ).completer = self.node_completer 125 | p_drbd_peer_opts.add_argument( 126 | 'node_b', 127 | type=str, 128 | help="2. Node in the node connection" 129 | ).completer = self.node_completer 130 | 131 | DrbdOptions.add_arguments(p_drbd_peer_opts, self.DRBD_OBJECT_NAME) 132 | p_drbd_peer_opts.set_defaults(func=self.drbd_opts) 133 | 134 | # Path commands 135 | path_subcmds = [ 136 | Commands.Subcommands.Create, 137 | Commands.Subcommands.List, 138 | Commands.Subcommands.Delete 139 | ] 140 | 141 | path_parser = subp.add_parser( 142 | NodeConnectionCommands.Path.LONG, 143 | formatter_class=argparse.RawTextHelpFormatter, 144 | aliases=[NodeConnectionCommands.Path.SHORT], 145 | description="%s subcommands" % NodeConnectionCommands.Path.LONG) 146 | 147 | path_subp = path_parser.add_subparsers( 148 | title="%s subcommands" % Commands.Subcommands.Interface.LONG, 149 | metavar="", 150 | description=Commands.Subcommands.generate_desc(path_subcmds)) 151 | 152 | # create path 153 | path_create = path_subp.add_parser( 154 | Commands.Subcommands.Create.LONG, 155 | aliases=[Commands.Subcommands.Create.SHORT], 156 | description='Creates a new node connection path.' 157 | ) 158 | path_create.add_argument( 159 | "node_a", 160 | type=str, 161 | help="1. Node of the connection" 162 | ).completer = self.node_completer 163 | path_create.add_argument( 164 | "node_b", 165 | type=str, 166 | help="2. Node of the connection" 167 | ).completer = self.node_completer 168 | path_create.add_argument( 169 | "path_name", 170 | help="Name of the created path" 171 | ) 172 | path_create.add_argument( 173 | "netinterface_a", 174 | help="Netinterface name to use for 1. node" 175 | ).completer = self.netif_completer 176 | path_create.add_argument( 177 | "netinterface_b", 178 | help="Netinterface name to use for the 2. node" 179 | ).completer = self.netif_completer 180 | path_create.set_defaults(func=self.path_create) 181 | 182 | # delete path 183 | path_delete = path_subp.add_parser( 184 | Commands.Subcommands.Delete.LONG, 185 | aliases=[Commands.Subcommands.Delete.SHORT], 186 | description='Deletes an existing node connection path.' 187 | ) 188 | path_delete.add_argument( 189 | "node_a", 190 | type=str, 191 | help="1. Node of the connection" 192 | ).completer = self.node_completer 193 | path_delete.add_argument( 194 | "node_b", 195 | type=str, 196 | help="2. Node of the connection" 197 | ).completer = self.node_completer 198 | path_delete.add_argument( 199 | "path_name", 200 | help="Name of the created path" 201 | ) 202 | path_delete.set_defaults(func=self.path_delete) 203 | 204 | # list path 205 | path_list = path_subp.add_parser( 206 | Commands.Subcommands.List.LONG, 207 | aliases=[Commands.Subcommands.List.SHORT], 208 | description='List all existing node connection paths.' 209 | ) 210 | path_list.add_argument('-p', '--pastable', action="store_true", help='Generate pastable output') 211 | path_list.add_argument( 212 | "node_a", 213 | type=str, 214 | help="1. Node of the connection" 215 | ).completer = self.node_completer 216 | path_list.add_argument( 217 | "node_b", 218 | type=str, 219 | help="2. Node of the connection" 220 | ).completer = self.node_completer 221 | path_list.set_defaults(func=self.path_list) 222 | 223 | self.check_subcommands(path_subp, path_subcmds) 224 | self.check_subcommands(subp, subcmds) 225 | 226 | @classmethod 227 | def show(cls, args, lstmsg): 228 | tbl = Table(utf8=not args.no_utf8, colors=not args.no_color, pastable=args.pastable) 229 | tbl.add_headers(NodeConnectionCommands._headers) 230 | show_props = cls._append_show_props_hdr(tbl, args.show_props) 231 | 232 | tbl.set_groupby(args.groupby if args.groupby else [NodeConnectionCommands._headers[0].name]) 233 | 234 | props_str_size = 30 235 | 236 | for node_con in [x for x in lstmsg.node_connections if "DELETED" not in x.flags]: 237 | opts = [os.path.basename(x) + '=' + node_con.properties[x] for x in node_con.properties] 238 | props_str = ",".join(opts) 239 | row = [ 240 | node_con.node_a, 241 | node_con.node_b, 242 | props_str if len(props_str) < props_str_size else props_str[:props_str_size] + '...' 243 | ] 244 | for sprop in show_props: 245 | row.append(node_con.properties.get(sprop, '')) 246 | tbl.add_row(row) 247 | tbl.show() 248 | 249 | def list(self, args): 250 | lstmsg = self._linstor.node_conn_list(args.node_name_a, args.node_name_b) 251 | return self.output_list(args, lstmsg, self.show) 252 | 253 | @classmethod 254 | def _props_show(cls, args, lstmsg): 255 | """ 256 | 257 | :param args: 258 | :param linstor.responses.NodeConnection lstmsg: 259 | :return: 260 | """ 261 | result = [] 262 | if lstmsg: 263 | result.append(lstmsg.properties) 264 | return result 265 | 266 | def print_props(self, args): 267 | lstmsg = self._linstor.node_conn_list(args.node_name_a, args.node_name_b) 268 | node_con = [] 269 | if lstmsg: 270 | node_con = lstmsg[0].node_connections 271 | return self.output_props_list(args, node_con, self._props_show) 272 | 273 | def set_props(self, args): 274 | args = self._attach_aux_prop(args) 275 | mod_prop_dict = Commands.parse_key_value_pairs([(args.key, args.value)]) 276 | replies = self._linstor.node_conn_modify( 277 | args.node_name_a, 278 | args.node_name_b, 279 | mod_prop_dict['pairs'], 280 | mod_prop_dict['delete'] 281 | ) 282 | return self.handle_replies(args, replies) 283 | 284 | def drbd_opts(self, args): 285 | a = DrbdOptions.filter_new(args) 286 | del a['node-a'] 287 | del a['node-b'] 288 | 289 | mod_props, del_props = DrbdOptions.parse_opts(a, self.DRBD_OBJECT_NAME) 290 | 291 | replies = self._linstor.node_conn_modify( 292 | args.node_a, 293 | args.node_b, 294 | mod_props, 295 | del_props 296 | ) 297 | return self.handle_replies(args, replies) 298 | 299 | def path_create(self, args): 300 | prop_ns = "{ns}/{pn}".format(ns=apiconsts.NAMESPC_CONNECTION_PATHS, pn=args.path_name) 301 | props = { 302 | "{ns}/{n}".format(ns=prop_ns, n=args.node_a): args.netinterface_a, 303 | "{ns}/{n}".format(ns=prop_ns, n=args.node_b): args.netinterface_b, 304 | } 305 | replies = self.get_linstorapi().node_conn_modify( 306 | args.node_a, 307 | args.node_b, 308 | property_dict=props, 309 | delete_props=[] 310 | ) 311 | return self.handle_replies(args, replies) 312 | 313 | @classmethod 314 | def _path_list(cls, args, lstmsg): 315 | result = [] 316 | if lstmsg: 317 | for node_con in lstmsg.node_connections: 318 | if (node_con.node_a == args.node_a and node_con.node_b == args.node_b) or \ 319 | (node_con.node_b == args.node_a and node_con.node_a == args.node_b): 320 | result.append({x: node_con.properties[x] for x in node_con.properties 321 | if x.startswith(apiconsts.NAMESPC_CONNECTION_PATHS + '/')}) 322 | break 323 | return result 324 | 325 | def path_list(self, args): 326 | lstmsg = self._linstor.node_conn_list(args.node_a, args.node_b) 327 | return self.output_props_list(args, lstmsg, self._path_list) 328 | 329 | def path_delete(self, args): 330 | prop_ns = "{ns}/{pn}".format(ns=apiconsts.NAMESPC_CONNECTION_PATHS, pn=args.path_name) 331 | replies = self.get_linstorapi().node_conn_modify( 332 | args.node_a, 333 | args.node_b, 334 | property_dict={}, 335 | delete_props=["{ns}/{n}".format(ns=prop_ns, n=args.node_a), 336 | "{ns}/{n}".format(ns=prop_ns, n=args.node_b)] 337 | ) 338 | return self.handle_replies(args, replies) 339 | -------------------------------------------------------------------------------- /linstor_client/argcomplete/my_shlex.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This copy of shlex.py from Python 3.6 is distributed with argcomplete. 4 | # It contains only the shlex class, with modifications as noted. 5 | 6 | """A lexical analyzer class for simple shell-like syntaxes.""" 7 | 8 | # Module and documentation by Eric S. Raymond, 21 Dec 1998 9 | # Input stacking and error message cleanup added by ESR, March 2000 10 | # push_source() and pop_source() made explicit by ESR, January 2001. 11 | # Posix compliance, split(), string arguments, and 12 | # iterator interface by Gustavo Niemeyer, April 2003. 13 | # changes to tokenize more like Posix shells by Vinay Sajip, July 2016. 14 | 15 | import os 16 | import sys 17 | from collections import deque 18 | 19 | # Modified by argcomplete: 2/3 compatibility 20 | # Note: cStringIO is not compatible with Unicode 21 | try: 22 | from StringIO import StringIO 23 | except ImportError: 24 | from io import StringIO 25 | 26 | # Modified by argcomplete: 2/3 compatibility 27 | try: 28 | basestring 29 | except NameError: 30 | basestring = str 31 | 32 | class shlex: 33 | "A lexical analyzer class for simple shell-like syntaxes." 34 | def __init__(self, instream=None, infile=None, posix=False, 35 | punctuation_chars=False): 36 | # Modified by argcomplete: 2/3 compatibility 37 | if isinstance(instream, basestring): 38 | instream = StringIO(instream) 39 | if instream is not None: 40 | self.instream = instream 41 | self.infile = infile 42 | else: 43 | self.instream = sys.stdin 44 | self.infile = None 45 | self.posix = posix 46 | if posix: 47 | self.eof = None 48 | else: 49 | self.eof = '' 50 | self.commenters = '#' 51 | self.wordchars = ('abcdfeghijklmnopqrstuvwxyz' 52 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_') 53 | # Modified by argcomplete: 2/3 compatibility 54 | # if self.posix: 55 | # self.wordchars += ('ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ' 56 | # 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ') 57 | self.whitespace = ' \t\r\n' 58 | self.whitespace_split = False 59 | self.quotes = '\'"' 60 | self.escape = '\\' 61 | self.escapedquotes = '"' 62 | self.state = ' ' 63 | self.pushback = deque() 64 | self.lineno = 1 65 | self.debug = 0 66 | self.token = '' 67 | self.filestack = deque() 68 | self.source = None 69 | if not punctuation_chars: 70 | punctuation_chars = '' 71 | elif punctuation_chars is True: 72 | punctuation_chars = '();<>|&' 73 | self.punctuation_chars = punctuation_chars 74 | if punctuation_chars: 75 | # _pushback_chars is a push back queue used by lookahead logic 76 | self._pushback_chars = deque() 77 | # these chars added because allowed in file names, args, wildcards 78 | self.wordchars += '~-./*?=' 79 | #remove any punctuation chars from wordchars 80 | t = self.wordchars.maketrans(dict.fromkeys(punctuation_chars)) 81 | self.wordchars = self.wordchars.translate(t) 82 | 83 | # Modified by argcomplete: Record last wordbreak position 84 | self.last_wordbreak_pos = None 85 | self.wordbreaks = '' 86 | 87 | def push_token(self, tok): 88 | "Push a token onto the stack popped by the get_token method" 89 | if self.debug >= 1: 90 | print("shlex: pushing token " + repr(tok)) 91 | self.pushback.appendleft(tok) 92 | 93 | def push_source(self, newstream, newfile=None): 94 | "Push an input source onto the lexer's input source stack." 95 | # Modified by argcomplete: 2/3 compatibility 96 | if isinstance(newstream, basestring): 97 | newstream = StringIO(newstream) 98 | self.filestack.appendleft((self.infile, self.instream, self.lineno)) 99 | self.infile = newfile 100 | self.instream = newstream 101 | self.lineno = 1 102 | if self.debug: 103 | if newfile is not None: 104 | print('shlex: pushing to file %s' % (self.infile,)) 105 | else: 106 | print('shlex: pushing to stream %s' % (self.instream,)) 107 | 108 | def pop_source(self): 109 | "Pop the input source stack." 110 | self.instream.close() 111 | (self.infile, self.instream, self.lineno) = self.filestack.popleft() 112 | if self.debug: 113 | print('shlex: popping to %s, line %d' \ 114 | % (self.instream, self.lineno)) 115 | self.state = ' ' 116 | 117 | def get_token(self): 118 | "Get a token from the input stream (or from stack if it's nonempty)" 119 | if self.pushback: 120 | tok = self.pushback.popleft() 121 | if self.debug >= 1: 122 | print("shlex: popping token " + repr(tok)) 123 | return tok 124 | # No pushback. Get a token. 125 | raw = self.read_token() 126 | # Handle inclusions 127 | if self.source is not None: 128 | while raw == self.source: 129 | spec = self.sourcehook(self.read_token()) 130 | if spec: 131 | (newfile, newstream) = spec 132 | self.push_source(newstream, newfile) 133 | raw = self.get_token() 134 | # Maybe we got EOF instead? 135 | while raw == self.eof: 136 | if not self.filestack: 137 | return self.eof 138 | else: 139 | self.pop_source() 140 | raw = self.get_token() 141 | # Neither inclusion nor EOF 142 | if self.debug >= 1: 143 | if raw != self.eof: 144 | print("shlex: token=" + repr(raw)) 145 | else: 146 | print("shlex: token=EOF") 147 | return raw 148 | 149 | def read_token(self): 150 | quoted = False 151 | escapedstate = ' ' 152 | while True: 153 | if self.punctuation_chars and self._pushback_chars: 154 | nextchar = self._pushback_chars.pop() 155 | else: 156 | nextchar = self.instream.read(1) 157 | if nextchar == '\n': 158 | self.lineno += 1 159 | if self.debug >= 3: 160 | print("shlex: in state %r I see character: %r" % (self.state, 161 | nextchar)) 162 | if self.state is None: 163 | self.token = '' # past end of file 164 | break 165 | elif self.state == ' ': 166 | if not nextchar: 167 | self.state = None # end of file 168 | break 169 | elif nextchar in self.whitespace: 170 | if self.debug >= 2: 171 | print("shlex: I see whitespace in whitespace state") 172 | if self.token or (self.posix and quoted): 173 | break # emit current token 174 | else: 175 | continue 176 | elif nextchar in self.commenters: 177 | self.instream.readline() 178 | self.lineno += 1 179 | elif self.posix and nextchar in self.escape: 180 | escapedstate = 'a' 181 | self.state = nextchar 182 | elif nextchar in self.wordchars: 183 | self.token = nextchar 184 | self.state = 'a' 185 | elif nextchar in self.punctuation_chars: 186 | self.token = nextchar 187 | self.state = 'c' 188 | elif nextchar in self.quotes: 189 | if not self.posix: 190 | self.token = nextchar 191 | self.state = nextchar 192 | elif self.whitespace_split: 193 | self.token = nextchar 194 | self.state = 'a' 195 | else: 196 | self.token = nextchar 197 | if self.token or (self.posix and quoted): 198 | break # emit current token 199 | else: 200 | continue 201 | elif self.state in self.quotes: 202 | quoted = True 203 | if not nextchar: # end of file 204 | if self.debug >= 2: 205 | print("shlex: I see EOF in quotes state") 206 | # XXX what error should be raised here? 207 | raise ValueError("No closing quotation") 208 | if nextchar == self.state: 209 | if not self.posix: 210 | self.token += nextchar 211 | self.state = ' ' 212 | break 213 | else: 214 | self.state = 'a' 215 | elif (self.posix and nextchar in self.escape and self.state 216 | in self.escapedquotes): 217 | escapedstate = self.state 218 | self.state = nextchar 219 | else: 220 | self.token += nextchar 221 | elif self.state in self.escape: 222 | if not nextchar: # end of file 223 | if self.debug >= 2: 224 | print("shlex: I see EOF in escape state") 225 | # XXX what error should be raised here? 226 | raise ValueError("No escaped character") 227 | # In posix shells, only the quote itself or the escape 228 | # character may be escaped within quotes. 229 | if (escapedstate in self.quotes and 230 | nextchar != self.state and nextchar != escapedstate): 231 | self.token += self.state 232 | self.token += nextchar 233 | self.state = escapedstate 234 | elif self.state in ('a', 'c'): 235 | if not nextchar: 236 | self.state = None # end of file 237 | break 238 | elif nextchar in self.whitespace: 239 | if self.debug >= 2: 240 | print("shlex: I see whitespace in word state") 241 | self.state = ' ' 242 | if self.token or (self.posix and quoted): 243 | break # emit current token 244 | else: 245 | continue 246 | elif nextchar in self.commenters: 247 | self.instream.readline() 248 | self.lineno += 1 249 | if self.posix: 250 | self.state = ' ' 251 | if self.token or (self.posix and quoted): 252 | break # emit current token 253 | else: 254 | continue 255 | elif self.posix and nextchar in self.quotes: 256 | self.state = nextchar 257 | elif self.posix and nextchar in self.escape: 258 | escapedstate = 'a' 259 | self.state = nextchar 260 | elif self.state == 'c': 261 | if nextchar in self.punctuation_chars: 262 | self.token += nextchar 263 | else: 264 | if nextchar not in self.whitespace: 265 | self._pushback_chars.append(nextchar) 266 | self.state = ' ' 267 | break 268 | elif (nextchar in self.wordchars or nextchar in self.quotes 269 | or self.whitespace_split): 270 | self.token += nextchar 271 | # Modified by argcomplete: Record last wordbreak position 272 | if nextchar in self.wordbreaks: 273 | self.last_wordbreak_pos = len(self.token) - 1 274 | else: 275 | if self.punctuation_chars: 276 | self._pushback_chars.append(nextchar) 277 | else: 278 | self.pushback.appendleft(nextchar) 279 | if self.debug >= 2: 280 | print("shlex: I see punctuation in word state") 281 | self.state = ' ' 282 | if self.token or (self.posix and quoted): 283 | break # emit current token 284 | else: 285 | continue 286 | result = self.token 287 | self.token = '' 288 | if self.posix and not quoted and result == '': 289 | result = None 290 | if self.debug > 1: 291 | if result: 292 | print("shlex: raw token=" + repr(result)) 293 | else: 294 | print("shlex: raw token=EOF") 295 | # Modified by argcomplete: Record last wordbreak position 296 | if self.state == ' ': 297 | self.last_wordbreak_pos = None 298 | return result 299 | 300 | def sourcehook(self, newfile): 301 | "Hook called on a filename to be sourced." 302 | if newfile[0] == '"': 303 | newfile = newfile[1:-1] 304 | # This implements cpp-like semantics for relative-path inclusion. 305 | # Modified by argcomplete: 2/3 compatibility 306 | if isinstance(self.infile, basestring) and not os.path.isabs(newfile): 307 | newfile = os.path.join(os.path.dirname(self.infile), newfile) 308 | return (newfile, open(newfile, "r")) 309 | 310 | def error_leader(self, infile=None, lineno=None): 311 | "Emit a C-compiler-like, Emacs-friendly error-message leader." 312 | if infile is None: 313 | infile = self.infile 314 | if lineno is None: 315 | lineno = self.lineno 316 | return "\"%s\", line %d: " % (infile, lineno) 317 | 318 | def __iter__(self): 319 | return self 320 | 321 | def __next__(self): 322 | token = self.get_token() 323 | if token == self.eof: 324 | raise StopIteration 325 | return token 326 | 327 | # Modified by argcomplete: 2/3 compatibility 328 | next = __next__ 329 | --------------------------------------------------------------------------------