├── .codeclimate.yml ├── .codespell.ignore.files ├── .codespell.ignore.words ├── .coveragerc ├── .github └── workflows │ └── shaptools-ci.yml ├── .gitignore ├── .pylintrc ├── .yamllint.yaml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── _service ├── bin └── shapcli ├── docs ├── SHAPCLI.md └── shapcli.config.example ├── pytest.ini ├── python-shaptools.changes ├── python-shaptools.spec ├── requirements.txt ├── setup.py ├── shaptools ├── __init__.py ├── hana.py ├── hdb_connector │ ├── __init__.py │ └── connectors │ │ ├── __init__.py │ │ ├── base_connector.py │ │ ├── dbapi_connector.py │ │ └── pyhdb_connector.py ├── netweaver.py ├── saputils.py ├── shapcli.py ├── shell.py └── support │ └── ssh_askpass ├── tests ├── __init__.py ├── hana_test.py ├── hdb_connector │ ├── __init__.py │ ├── base_connect_test.py │ ├── dbapi_connector_test.py │ ├── init_test.py │ └── pyhdb_connector_test.py ├── netweaver_test.py ├── requirements.2.7.yaml ├── requirements.3.6.yaml ├── saputils_test.py ├── shapcli_test.py ├── shell_test.py └── support │ ├── modified.conf │ ├── modified.conf.xml │ ├── modified.inifile.params │ ├── new.inifile.params │ ├── original.conf │ ├── original.conf.xml │ └── original.inifile.params └── tox.ini /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | checks: 4 | method-complexity: 5 | config: 6 | threshold: 8 7 | file-lines: 8 | config: 9 | threshold: 1024 10 | -------------------------------------------------------------------------------- /.codespell.ignore.files: -------------------------------------------------------------------------------- 1 | venv,.git 2 | -------------------------------------------------------------------------------- /.codespell.ignore.words: -------------------------------------------------------------------------------- 1 | aas 2 | enque 3 | versionn 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */tests/* 4 | */test/* 5 | *setup.py* 6 | tests/* 7 | -------------------------------------------------------------------------------- /.github/workflows/shaptools-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Package CI 3 | # - this workflow will 4 | # - test on 5 | # - SLE 12 SP5 6 | # - python 2.7 7 | # - SLE 15 SP5 8 | # - python 3.6 9 | # - deliver the package content to the configured repository 10 | # - submit the new package content to the upstream repository 11 | on: [push, pull_request] # yamllint disable-line rule:truthy 12 | env: 13 | PACKAGE_NAME: python-shaptools 14 | TAR_NAME: shaptools 15 | jobs: 16 | tab: 17 | name: 'tabspace checking' 18 | runs-on: ubuntu-22.04 19 | 20 | # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest 21 | defaults: 22 | run: 23 | shell: bash 24 | 25 | steps: 26 | # Checkout the repository to the GitHub Actions runner 27 | - name: Checkout 28 | uses: actions/checkout@v2 29 | 30 | - name: tab 31 | run: make test-tab 32 | codespell: 33 | name: 'spell checking' 34 | runs-on: ubuntu-22.04 35 | 36 | # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest 37 | defaults: 38 | run: 39 | shell: bash 40 | 41 | steps: 42 | # Checkout the repository to the GitHub Actions runner 43 | - name: Checkout 44 | uses: actions/checkout@v2 45 | 46 | - name: Install linting tools 47 | run: | 48 | sudo apt-get update 49 | sudo apt-get install -y git python3 python3-pip 50 | python3 -m pip install codespell 51 | 52 | - name: codespell 53 | run: make test-codespell 54 | 55 | shellcheck: 56 | name: 'script syntax check' 57 | runs-on: ubuntu-22.04 58 | 59 | # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest 60 | defaults: 61 | run: 62 | shell: bash 63 | 64 | steps: 65 | # Checkout the repository to the GitHub Actions runner 66 | - name: Checkout 67 | uses: actions/checkout@v2 68 | 69 | - name: Install linting tools 70 | run: | 71 | sudo apt-get update 72 | sudo apt-get install -y git python3 python3-pip shellcheck 73 | 74 | - name: shellcheck 75 | run: make test-shellcheck 76 | 77 | yamllint: 78 | name: 'yaml linting' 79 | runs-on: ubuntu-22.04 80 | 81 | # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest 82 | defaults: 83 | run: 84 | shell: bash 85 | 86 | steps: 87 | # Checkout the repository to the GitHub Actions runner 88 | - name: Checkout 89 | uses: actions/checkout@v2 90 | 91 | - name: Install linting tools 92 | run: | 93 | sudo apt-get update 94 | sudo apt-get install -y git python3 python3-pip 95 | python3 -m pip install yamllint 96 | 97 | - name: yamllint 98 | run: make test-yamllint 99 | 100 | jsonlint: 101 | name: 'json linting' 102 | runs-on: ubuntu-22.04 103 | 104 | # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest 105 | defaults: 106 | run: 107 | shell: bash 108 | 109 | steps: 110 | # Checkout the repository to the GitHub Actions runner 111 | - name: Checkout 112 | uses: actions/checkout@v2 113 | 114 | - name: Install linting tools 115 | run: | 116 | sudo apt-get update 117 | sudo apt-get install -y git python3 python3-pip 118 | python3 -m pip install jsonlint 119 | 120 | - name: jsonlint 121 | run: make test-jsonlint 122 | 123 | mlc: 124 | name: 'markup link checker' 125 | runs-on: ubuntu-22.04 126 | 127 | # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest 128 | defaults: 129 | run: 130 | shell: bash 131 | 132 | steps: 133 | # Checkout the repository to the GitHub Actions runner 134 | - name: Checkout 135 | uses: actions/checkout@v2 136 | 137 | - name: Install linting tools 138 | run: | 139 | mkdir -p bin 140 | curl -L https://github.com/becheran/mlc/releases/download/v0.14.3/mlc-x86_64-linux -o bin/mlc 141 | chmod +x bin/mlc 142 | echo "$PWD/bin" >> $GITHUB_PATH 143 | 144 | - name: mlc 145 | run: make test-mlc 146 | 147 | python: 148 | runs-on: ubuntu-22.04 149 | strategy: 150 | # do not fail if other test fails 151 | fail-fast: false 152 | matrix: 153 | container: 154 | - registry.suse.com/suse/sles12sp5:latest # python 2.7 155 | - registry.suse.com/bci/bci-base:15.5 # python 3.6 156 | container: 157 | image: ${{ matrix.container }} 158 | steps: 159 | - name: Auth to SCC and minimal dependencies 160 | run: | 161 | echo "username=${{ secrets.SCC_USERNAME }}" >/etc/zypp/credentials.d/SCCcredentials 162 | echo "password=${{ secrets.SCC_PASSWORD }}" >>/etc/zypp/credentials.d/SCCcredentials 163 | zypper ref -s 164 | zypper -n in -y tar gzip git 165 | - uses: actions/checkout@v2 166 | with: 167 | fetch-depth: 0 168 | - name: Install dependencies 169 | run: | 170 | zypper -n in -y make python 171 | if test -f /usr/bin/python3; then 172 | # minimal salt, python packages and compilers 173 | zypper -n in -y salt python3-pip python3-devel gcc 174 | # use current salt version shipped with SLE 15 175 | git clone --branch=openSUSE/release/3006.0 --depth=50 https://github.com/openSUSE/salt ../salt 176 | # python 3.6 - official requirements from salt (works with python >3.6) 177 | pip install -r ../salt/requirements/pytest.txt 178 | pip install -r tests/requirements.3.6.yaml # pinned pytest-cov 179 | else 180 | zypper -n in -y SUSEConnect 181 | SUSEConnect -p sle-module-adv-systems-management/12/x86_64 182 | # minimal salt, python packages and compilers 183 | zypper -n in -y salt python-pip python-devel gcc gcc-c++ 184 | # python 2.7 - latest available versions for old python release 185 | pip install --ignore-installed -r tests/requirements.2.7.yaml 186 | # use current salt version shipped with SLE 12 187 | git clone --branch=openSUSE/release/3000.3 --depth=50 https://github.com/openSUSE/salt ../salt 188 | fi 189 | rm ../salt/tests/conftest.py 190 | - name: execute test script 191 | run: make test-python 192 | 193 | 194 | delivery: 195 | needs: [tab, codespell, shellcheck, yamllint, jsonlint, mlc, python] 196 | runs-on: ubuntu-22.04 197 | if: ${{ github.event_name != 'pull_request' }} 198 | container: 199 | image: shap/continuous_deliver 200 | env: 201 | OBS_USER: ${{ secrets.OBS_USER }} 202 | OBS_PASS: ${{ secrets.OBS_PASS }} 203 | OBS_PROJECT: ${{ secrets.OBS_PROJECT }} 204 | steps: 205 | - uses: actions/checkout@v2 206 | with: 207 | fetch-depth: 0 208 | - name: configure OSC 209 | # OSC credentials must be configured beforehand as the HOME variables cannot be changed from /github/home 210 | # that is used to run osc commands 211 | run: | 212 | /scripts/init_osc_creds.sh 213 | mkdir -p $HOME/.config/osc 214 | cp /root/.config/osc/oscrc $HOME/.config/osc 215 | - name: deliver package 216 | run: | 217 | sed -i 's~%%VERSION%%~${{ github.sha }}~' _service && \ 218 | sed -i 's~%%REPOSITORY%%~${{ github.repository }}~' _service && \ 219 | /scripts/upload.sh 220 | 221 | 222 | submit: 223 | needs: [tab, codespell, shellcheck, yamllint, jsonlint, mlc, python, delivery] 224 | runs-on: ubuntu-22.04 225 | if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/master' }} 226 | container: 227 | image: shap/continuous_deliver 228 | env: 229 | OBS_USER: ${{ secrets.OBS_USER }} 230 | OBS_PASS: ${{ secrets.OBS_PASS }} 231 | OBS_PROJECT: ${{ secrets.OBS_PROJECT}} 232 | TARGET_PROJECT: ${{ secrets.TARGET_PROJECT}} 233 | steps: 234 | - uses: actions/checkout@v2 235 | with: 236 | fetch-depth: 0 237 | - name: configure OSC 238 | # OSC credentials must be configured beforehand as the HOME variables cannot be changed from /github/home 239 | # that is used to run osc commands 240 | run: | 241 | /scripts/init_osc_creds.sh 242 | mkdir -p $HOME/.config/osc 243 | cp /root/.config/osc/oscrc $HOME/.config/osc 244 | - name: submit package 245 | run: | 246 | sed -i 's~%%VERSION%%~${{ github.sha }}~' _service && \ 247 | sed -i 's~%%REPOSITORY%%~${{ github.repository }}~' _service && \ 248 | /scripts/submit.sh 249 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /htmlcov 2 | /tests/htmlcov 3 | dist/* 4 | .coverage 5 | .cache 6 | .idea 7 | *.pyc 8 | *.egg-info* 9 | .tox/* 10 | venv 11 | .envrc 12 | .direnv 13 | shell.nix 14 | .ropeproject 15 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns=.*_test.py 17 | 18 | # Pickle collected data for later comparisons. 19 | persistent=yes 20 | 21 | # List of plugins (as comma separated values of python modules names) to load, 22 | # usually to register additional checkers. 23 | load-plugins= 24 | 25 | # Use multiple processes to speed up Pylint. 26 | jobs=1 27 | 28 | # Allow loading of arbitrary C extensions. Extensions are imported into the 29 | # active Python interpreter and may run arbitrary code. 30 | unsafe-load-any-extension=no 31 | 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code 35 | extension-pkg-whitelist= 36 | 37 | # Allow optimization of some AST trees. This will activate a peephole AST 38 | # optimizer, which will apply various small optimizations. For instance, it can 39 | # be used to obtain the result of joining multiple strings with the addition 40 | # operator. Joining a lot of strings can lead to a maximum recursion error in 41 | # Pylint and this flag can prevent that. It has one side effect, the resulting 42 | # AST will be different than the one from reality. This option is deprecated 43 | # and it will be removed in Pylint 2.0. 44 | optimize-ast=no 45 | 46 | 47 | [MESSAGES CONTROL] 48 | 49 | # Only show warnings with the listed confidence levels. Leave empty to show 50 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 51 | confidence= 52 | 53 | # Enable the message, report, category or checker with the given id(s). You can 54 | # either give multiple identifier separated by comma (,) or put this option 55 | # multiple time (only on the command line, not in the configuration file where 56 | # it should appear only once). See also the "--disable" option for examples. 57 | #enable= 58 | 59 | # Disable the message, report, category or checker with the given id(s). You 60 | # can either give multiple identifiers separated by comma (,) or put this 61 | # option multiple times (only on the command line, not in the configuration 62 | # file where it should appear only once).You can also use "--disable=all" to 63 | # disable everything first and then re-enable specific checks. For example, if 64 | # you want to run only the similarities checker, you can use "--disable=all 65 | # --enable=similarities". If you want to run only the classes checker, but have 66 | # no Warning level messages displayed, use"--disable=all --enable=classes 67 | # --disable=W" 68 | disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating 69 | 70 | 71 | [REPORTS] 72 | 73 | # Set the output format. Available formats are text, parseable, colorized, msvs 74 | # (visual studio) and html. You can also give a reporter class, eg 75 | # mypackage.mymodule.MyReporterClass. 76 | output-format=text 77 | 78 | # Put messages in a separate file for each module / package specified on the 79 | # command line instead of printing them on stdout. Reports (if any) will be 80 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 81 | # and it will be removed in Pylint 2.0. 82 | files-output=no 83 | 84 | # Tells whether to display a full report or only the messages 85 | reports=yes 86 | 87 | # Python expression which should return a note less than 10 (10 is the highest 88 | # note). You have access to the variables errors warning, statement which 89 | # respectively contain the number of errors / warnings messages and the total 90 | # number of statements analyzed. This is used by the global evaluation report 91 | # (RP0004). 92 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 93 | 94 | # Template used to display messages. This is a python new-style format string 95 | # used to format the message information. See doc for all details 96 | #msg-template= 97 | 98 | 99 | [BASIC] 100 | 101 | # Good variable names which should always be accepted, separated by a comma 102 | good-names=i,j,k,ex,Run,_ 103 | 104 | # Bad variable names which should always be refused, separated by a comma 105 | bad-names=foo,bar,baz,toto,tutu,tata 106 | 107 | # Colon-delimited sets of names that determine each other's naming style when 108 | # the name regexes allow several styles. 109 | name-group= 110 | 111 | # Include a hint for the correct naming format with invalid-name 112 | include-naming-hint=no 113 | 114 | # List of decorators that produce properties, such as abc.abstractproperty. Add 115 | # to this list to register other decorators that produce valid properties. 116 | property-classes=abc.abstractproperty 117 | 118 | # Regular expression matching correct function names 119 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 120 | 121 | # Naming hint for function names 122 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 123 | 124 | # Regular expression matching correct variable names 125 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 126 | 127 | # Naming hint for variable names 128 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 129 | 130 | # Regular expression matching correct constant names 131 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 132 | 133 | # Naming hint for constant names 134 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 135 | 136 | # Regular expression matching correct attribute names 137 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 138 | 139 | # Naming hint for attribute names 140 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 141 | 142 | # Regular expression matching correct argument names 143 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 144 | 145 | # Naming hint for argument names 146 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 147 | 148 | # Regular expression matching correct class attribute names 149 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 150 | 151 | # Naming hint for class attribute names 152 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 153 | 154 | # Regular expression matching correct inline iteration names 155 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 156 | 157 | # Naming hint for inline iteration names 158 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 159 | 160 | # Regular expression matching correct class names 161 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 162 | 163 | # Naming hint for class names 164 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 165 | 166 | # Regular expression matching correct module names 167 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 168 | 169 | # Naming hint for module names 170 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 171 | 172 | # Regular expression matching correct method names 173 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 174 | 175 | # Naming hint for method names 176 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 177 | 178 | # Regular expression which should only match function or class names that do 179 | # not require a docstring. 180 | no-docstring-rgx=^_ 181 | 182 | # Minimum line length for functions/classes that require docstrings, shorter 183 | # ones are exempt. 184 | docstring-min-length=-1 185 | 186 | 187 | [ELIF] 188 | 189 | # Maximum number of nested blocks for function / method body 190 | max-nested-blocks=5 191 | 192 | 193 | [FORMAT] 194 | 195 | # Maximum number of characters on a single line. 196 | max-line-length=100 197 | 198 | # Regexp for a line that is allowed to be longer than the limit. 199 | ignore-long-lines=^\s*(# )??$ 200 | 201 | # Allow the body of an if to be on the same line as the test if there is no 202 | # else. 203 | single-line-if-stmt=no 204 | 205 | # List of optional constructs for which whitespace checking is disabled. `dict- 206 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 207 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 208 | # `empty-line` allows space-only lines. 209 | no-space-check=trailing-comma,dict-separator 210 | 211 | # Maximum number of lines in a module 212 | max-module-lines=1000 213 | 214 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 215 | # tab). 216 | indent-string=' ' 217 | 218 | # Number of spaces of indent required inside a hanging or continued line. 219 | indent-after-paren=4 220 | 221 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 222 | expected-line-ending-format= 223 | 224 | 225 | [LOGGING] 226 | 227 | # Logging modules to check that the string format arguments are in logging 228 | # function parameter format 229 | logging-modules=logging 230 | 231 | 232 | [MISCELLANEOUS] 233 | 234 | # List of note tags to take in consideration, separated by a comma. 235 | notes=FIXME,XXX,TODO 236 | 237 | 238 | [SIMILARITIES] 239 | 240 | # Minimum lines number of a similarity. 241 | min-similarity-lines=4 242 | 243 | # Ignore comments when computing similarities. 244 | ignore-comments=yes 245 | 246 | # Ignore docstrings when computing similarities. 247 | ignore-docstrings=yes 248 | 249 | # Ignore imports when computing similarities. 250 | ignore-imports=no 251 | 252 | 253 | [SPELLING] 254 | 255 | # Spelling dictionary name. Available dictionaries: none. To make it working 256 | # install python-enchant package. 257 | spelling-dict= 258 | 259 | # List of comma separated words that should not be checked. 260 | spelling-ignore-words= 261 | 262 | # A path to a file that contains private dictionary; one word per line. 263 | spelling-private-dict-file= 264 | 265 | # Tells whether to store unknown words to indicated private dictionary in 266 | # --spelling-private-dict-file option instead of raising a message. 267 | spelling-store-unknown-words=no 268 | 269 | 270 | [TYPECHECK] 271 | 272 | # Tells whether missing members accessed in mixin class should be ignored. A 273 | # mixin class is detected if its name ends with "mixin" (case insensitive). 274 | ignore-mixin-members=yes 275 | 276 | # List of module names for which member attributes should not be checked 277 | # (useful for modules/projects where namespaces are manipulated during runtime 278 | # and thus existing member attributes cannot be deduced by static analysis. It 279 | # supports qualified module names, as well as Unix pattern matching. 280 | ignored-modules= 281 | 282 | # List of class names for which member attributes should not be checked (useful 283 | # for classes with dynamically set attributes). This supports the use of 284 | # qualified names. 285 | ignored-classes=optparse.Values,thread._local,_thread._local 286 | 287 | # List of members which are set dynamically and missed by pylint inference 288 | # system, and so shouldn't trigger E1101 when accessed. Python regular 289 | # expressions are accepted. 290 | generated-members= 291 | 292 | # List of decorators that produce context managers, such as 293 | # contextlib.contextmanager. Add to this list to register other decorators that 294 | # produce valid context managers. 295 | contextmanager-decorators=contextlib.contextmanager 296 | 297 | 298 | [VARIABLES] 299 | 300 | # Tells whether we should check for unused import in __init__ files. 301 | init-import=no 302 | 303 | # A regular expression matching the name of dummy variables (i.e. expectedly 304 | # not used). 305 | dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy 306 | 307 | # List of additional names supposed to be defined in builtins. Remember that 308 | # you should avoid to define new builtins when possible. 309 | additional-builtins= 310 | 311 | # List of strings which can identify a callback function by name. A callback 312 | # name must start or end with one of those strings. 313 | callbacks=cb_,_cb 314 | 315 | # List of qualified module names which can have objects that can redefine 316 | # builtins. 317 | redefining-builtins-modules=six.moves,future.builtins 318 | 319 | 320 | [CLASSES] 321 | 322 | # List of method names used to declare (i.e. assign) instance attributes. 323 | defining-attr-methods=__init__,__new__,setUp 324 | 325 | # List of valid names for the first argument in a class method. 326 | valid-classmethod-first-arg=cls 327 | 328 | # List of valid names for the first argument in a metaclass class method. 329 | valid-metaclass-classmethod-first-arg=mcs 330 | 331 | # List of member names, which should be excluded from the protected access 332 | # warning. 333 | exclude-protected=_asdict,_fields,_replace,_source,_make 334 | 335 | 336 | [DESIGN] 337 | 338 | # Maximum number of arguments for function / method 339 | max-args=5 340 | 341 | # Argument names that match this expression will be ignored. Default to name 342 | # with leading underscore 343 | ignored-argument-names=_.* 344 | 345 | # Maximum number of locals for function / method body 346 | max-locals=15 347 | 348 | # Maximum number of return / yield for function / method body 349 | max-returns=6 350 | 351 | # Maximum number of branch for function / method body 352 | max-branches=12 353 | 354 | # Maximum number of statements in function / method body 355 | max-statements=50 356 | 357 | # Maximum number of parents for a class (see R0901). 358 | max-parents=7 359 | 360 | # Maximum number of attributes for a class (see R0902). 361 | max-attributes=7 362 | 363 | # Minimum number of public methods for a class (see R0903). 364 | min-public-methods=2 365 | 366 | # Maximum number of public methods for a class (see R0904). 367 | max-public-methods=20 368 | 369 | # Maximum number of boolean expressions in a if statement 370 | max-bool-expr=5 371 | 372 | 373 | [IMPORTS] 374 | 375 | # Deprecated modules which should not be used, separated by a comma 376 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 377 | 378 | # Create a graph of every (i.e. internal and external) dependencies in the 379 | # given file (report RP0402 must not be disabled) 380 | import-graph= 381 | 382 | # Create a graph of external dependencies in the given file (report RP0402 must 383 | # not be disabled) 384 | ext-import-graph= 385 | 386 | # Create a graph of internal dependencies in the given file (report RP0402 must 387 | # not be disabled) 388 | int-import-graph= 389 | 390 | # Force import order to recognize a module as part of the standard 391 | # compatibility libraries. 392 | known-standard-library= 393 | 394 | # Force import order to recognize a module as part of a third party library. 395 | known-third-party=enchant 396 | 397 | # Analyse import fallback blocks. This can be used to support both Python 2 and 398 | # 3 compatible code, which means that the block might have code that exists 399 | # only in one or another interpreter, leading to false positives when analysed. 400 | analyse-fallback-blocks=no 401 | 402 | 403 | [EXCEPTIONS] 404 | 405 | # Exceptions that will emit a warning when being caught. Defaults to 406 | # "Exception" 407 | overgeneral-exceptions=Exception 408 | -------------------------------------------------------------------------------- /.yamllint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | ignore: | 5 | venv 6 | 7 | rules: 8 | # 80 chars should be enough, but don't fail if a line is longer 9 | line-length: 10 | max: 160 11 | level: warning 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## OBS Releases 4 | 5 | The CI will automatically interact with SUSE's [Open Build Service](https://build.opensuse.org): the master branch will be kept in sync with the `network:ha-clustering:sap-deployments:devel` project (the development upstream), while a new Submit Request against the `openSUSE:Factory` project (the stable downstream) can be triggered by publishing a new GitHub release or pushing a new Git tag. 6 | 7 | When releasing to `openSUSE:Factory`, please ensure that tags always follow the [SemVer](https://semver.org/) scheme, and that [the changelog](python-shaptools.changes) contains a new entry, otherwise the request submission might fail. 8 | 9 | #### Note to maintainers 10 | 11 | The OBS projects can be changed via various environment variables like `OBS_PROJECT` and `TARGET_PROJECT` in the Travis settings. 12 | You can enable the OBS delivery for feature branches in your own fork by setting the variable `DELIVER_BRANCHES` to a non-empty value. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | =========================================================================== 204 | 205 | Below is a summary of the licensing used by external modules that are 206 | bundled with SaltStack. 207 | 208 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 209 | Upstream-Name: salt 210 | Upstream-Contact: salt-users@googlegroups.com 211 | Source: https://github.com/saltstack/salt 212 | 213 | Files: * 214 | Copyright: 2014 SaltStack Team 215 | License: Apache-2.0 216 | Licensed under the Apache License, Version 2.0 (the "License"); 217 | you may not use this file except in compliance with the License. 218 | You may obtain a copy of the License at 219 | . 220 | http://www.apache.org/licenses/LICENSE-2.0 221 | . 222 | Unless required by applicable law or agreed to in writing, software 223 | distributed under the License is distributed on an "AS IS" BASIS, 224 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 225 | See the License for the specific language governing permissions and 226 | limitations under the License. 227 | . 228 | On Debian systems, the full text of the Apache License, Version 2.0 can be 229 | found in the file 230 | `/usr/share/common-licenses/Apache-2.0'. 231 | 232 | Files: debian/* 233 | Copyright: 2013 Joe Healy 234 | 2012 Michael Prokop 235 | 2012 Christian Hofstaedtler 236 | 2012 Ulrich Dangel 237 | 2012 Corey Quinn 238 | 2011 Aaron Toponce 239 | License: Apache-2.0 240 | Licensed under the Apache License, Version 2.0 (the "License"); 241 | you may not use this file except in compliance with the License. 242 | You may obtain a copy of the License at 243 | . 244 | http://www.apache.org/licenses/LICENSE-2.0 245 | . 246 | Unless required by applicable law or agreed to in writing, software 247 | distributed under the License is distributed on an "AS IS" BASIS, 248 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 249 | See the License for the specific language governing permissions and 250 | limitations under the License. 251 | . 252 | On Debian systems, the full text of the Apache License, Version 2.0 can be 253 | found in the file 254 | `/usr/share/common-licenses/Apache-2.0'. 255 | 256 | Files: salt/auth/pam.py 257 | Copyright: 2007 Chris AtLee 258 | License: MIT License 259 | Permission is hereby granted, free of charge, to any person obtaining a copy 260 | of this software and associated documentation files (the "Software"), to deal 261 | in the Software without restriction, including without limitation the rights 262 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 263 | copies of the Software, and to permit persons to whom the Software is 264 | furnished to do so, subject to the following conditions: 265 | . 266 | The above copyright notice and this permission notice shall be included in 267 | all copies or substantial portions of the Software. 268 | . 269 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 270 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 271 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 272 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 273 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 274 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 275 | THE SOFTWARE. 276 | 277 | Files: doc/_ext/youtube.py 278 | Copyright: 2009 Chris Pickel 279 | License: BSD-2-clause 280 | Redistribution and use in source and binary forms, with or without 281 | modification, are permitted provided that the following conditions are 282 | met: 283 | . 284 | * Redistributions of source code must retain the above copyright 285 | notice, this list of conditions and the following disclaimer. 286 | . 287 | * Redistributions in binary form must reproduce the above copyright 288 | notice, this list of conditions and the following disclaimer in the 289 | documentation and/or other materials provided with the distribution. 290 | . 291 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 292 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 293 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 294 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 295 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 296 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 297 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 298 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 299 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 300 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 301 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 302 | 303 | Files: salt/ext/six.py 304 | Copyright: 2010-2014 Benjamin Peterson 305 | License: MIT License 306 | Permission is hereby granted, free of charge, to any person obtaining a copy 307 | of this software and associated documentation files (the "Software"), to deal 308 | in the Software without restriction, including without limitation the rights 309 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 310 | copies of the Software, and to permit persons to whom the Software is 311 | furnished to do so, subject to the following conditions: 312 | . 313 | The above copyright notice and this permission notice shall be included in 314 | all copies or substantial portions of the Software. 315 | . 316 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 317 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 318 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 319 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 320 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 321 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 322 | THE SOFTWARE. 323 | 324 | Files: doc/_ext/images 325 | Copyright: 2013 SaltStack Team 326 | License: Apache-2.0 327 | Licensed under the Apache License, Version 2.0 (the "License"); 328 | you may not use this file except in compliance with the License. 329 | You may obtain a copy of the License at 330 | . 331 | http://www.apache.org/licenses/LICENSE-2.0 332 | . 333 | Unless required by applicable law or agreed to in writing, software 334 | distributed under the License is distributed on an "AS IS" BASIS, 335 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 336 | See the License for the specific language governing permissions and 337 | limitations under the License. 338 | . 339 | On Debian systems, the full text of the Apache License, Version 2.0 can be 340 | found in the file 341 | `/usr/share/common-licenses/Apache-2.0'. 342 | . 343 | Files in this directory were created in-house. 344 | 345 | Files: tests/utils/cptestcase.py 346 | Copyright: (c) 2014 Adam Hajari 347 | The MIT License (MIT) 348 | 349 | Permission is hereby granted, free of charge, to any person obtaining a copy 350 | of this software and associated documentation files (the "Software"), to deal 351 | in the Software without restriction, including without limitation the rights 352 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 353 | copies of the Software, and to permit persons to whom the Software is 354 | furnished to do so, subject to the following conditions: 355 | 356 | The above copyright notice and this permission notice shall be included in all 357 | copies or substantial portions of the Software. 358 | 359 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 360 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 361 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 362 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 363 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 364 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 365 | SOFTWARE. 366 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include useful user documentation 2 | include README.md 3 | include CHANGELOG.md 4 | include LICENSE 5 | include requirements.txt 6 | 7 | # Exclude unitary test files 8 | global-exclude tests/* 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # kudos: 2 | # - https://medium.com/@exustash/three-good-practices-for-better-ci-cd-makefiles-5b93452e4cc3 3 | # - https://le-gall.bzh/post/makefile-based-ci-chain-for-go/ 4 | # - https://makefiletutorial.com/ 5 | # - https://www.cl.cam.ac.uk/teaching/0910/UnixTools/make.pdf 6 | # 7 | SHELL := /usr/bin/env bash # set default shell 8 | .SHELLFLAGS = -c # Run commands in a -c flag 9 | 10 | .NOTPARALLEL: ; # wait for this target to finish 11 | .EXPORT_ALL_VARIABLES: ; # send all vars to shell 12 | 13 | .PHONY: all # All targets are accessible for user 14 | .DEFAULT: help # Running Make will run the help target 15 | 16 | BRANCH := $(shell git rev-parse --abbrev-ref HEAD) 17 | ifeq ($(BRANCH), HEAD) 18 | BRANCH := ${CI_BUILD_REF_NAME} 19 | endif 20 | 21 | # help: @ List available tasks of the project 22 | help: 23 | @grep -E '[a-zA-Z\.\-]+:.*?@ .*$$' $(MAKEFILE_LIST)| tr -d '#' | awk 'BEGIN {FS = ":.*?@ "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 24 | 25 | ## test section 26 | # All tests are called on "." if possible. 27 | # If this is not possible a special loop is used 28 | # to sum up all error codes. 29 | 30 | # test: @ Run all defined tests 31 | test: test-tab test-codespell test-shellcheck test-yamllint test-jsonlint test-python 32 | @echo "All tests Done!" 33 | 34 | # test-tab: @ Run linting to find files containing tabspaces 35 | test-tab: 36 | @for file in $(shell find . -regextype egrep -regex '.*\.(sls|yml|yaml)' ! -path "**/venv/*"); do\ 37 | grep -q -P '\t' $${file} ;\ 38 | if [ "$$?" -eq 0 ]; then\ 39 | err_add=1 ;\ 40 | echo "Tab found in $${file}" ;\ 41 | grep -H -n -P '\t' $${file} ;\ 42 | else \ 43 | err_add=0 ;\ 44 | fi;\ 45 | err=$$((err_add + err)) ;\ 46 | done; exit $$err 47 | 48 | # test-codespell: @ Run spell check 49 | test-codespell: 50 | codespell -H -f -s -I .codespell.ignore.words -S $(shell cat .codespell.ignore.files) -C 4 -q 6 51 | 52 | # test-shellcheck: @ Run linting on all shell scripts 53 | test-shellcheck: 54 | for file in $(shell find . -name '*.sh' ! -path "**/venv/*"); do\ 55 | echo $${file} ;\ 56 | shellcheck -s bash -x $${file};\ 57 | err=$$(($$? + err)) ;\ 58 | done; exit $$err 59 | 60 | # test-yamllint: @ Run linting on all yaml files 61 | test-yamllint: 62 | # yamllint -c .yamllint.yaml -s . 63 | yamllint -c .yamllint.yaml . 64 | 65 | # test-jsonlint: @ Run linting on all json files 66 | test-jsonlint: 67 | for file in $(shell find . -name '*.json' ! -path "**/venv/*"); do\ 68 | echo $${file} ;\ 69 | jq << $${file} >/dev/null;\ 70 | err=$$(($$? + err)) ;\ 71 | done; exit $$err 72 | 73 | # test-mlc: @ Run markup link checker 74 | test-mlc: 75 | mkdir -p aws/.terraform # make sure ingore-path exists 76 | mlc --throttle 1000 \ 77 | --ignore-path \ 78 | **/.terraform \ 79 | --ignore-links \ 80 | ./terraform.tvars.example \ 81 | ../pillar/*/* \ 82 | https://github.com/SUSE/ha-sap-terraform-deployments/actions \ 83 | https://github.com/SUSE/ha-sap-terraform-deployments/workflows/CI%20tests/badge.svg 84 | 85 | # test-python: @ Run Python Unit Tests 86 | test-python: 87 | py.test -vv --cov=shaptools --cov-config .coveragerc --cov-report term --cov-report xml tests 88 | 89 | # all: @ Runs everything 90 | all: test 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Package CI](https://github.com/SUSE/shaptools/actions/workflows/shaptools-ci.yml/badge.svg)](https://github.com/SUSE/shaptools/actions/workflows/shaptools-ci.yml) 2 | [![Maintainability](https://api.codeclimate.com/v1/badges/7521df0f216bd2dbed73/maintainability)](https://codeclimate.com/github/SUSE/shaptools/maintainability) 3 | [![Test Coverage](https://api.codeclimate.com/v1/badges/7521df0f216bd2dbed73/test_coverage)](https://codeclimate.com/github/SUSE/shaptools/test_coverage) 4 | 5 | # SHAPTOOLS 6 | 7 | Project created to expose an API with the SAP HANA platform major functionalities. 8 | The main idea is to have an easy and friendly environment to deploy and configure 9 | the SAP HANA environment and enable System replication feature. 10 | 11 | Example of how to use the package: 12 | 13 | ```python 14 | from shaptools import hana 15 | 16 | h = hana.HanaInstance('prd', '00', 'Qwerty1234') 17 | 18 | if not h.is_installed(): 19 | conf_file = hana.HanaInstance.create_conf_file( 20 | '/sap_inst/51052481', '/home/myuser/hana.conf', 'root', 'root') 21 | hana.HanaInstance.update_conf_file( 22 | conf_file, sid='PRD', password='Qwerty1234', system_user_password='Qwerty1234') 23 | hana.HanaInstance.install('/sap_inst/51052481', conf_file, 'root', 'root') 24 | 25 | if not h.is_running(): 26 | h.start() 27 | 28 | state = h.get_sr_state() 29 | 30 | h.create_user_key( 31 | 'backupkey', 'hana01:30013', 'SYSTEM', 'Qwerty1234', 'SYSTEMDB') 32 | h.create_backup('SYSTEMDB', 'backup', 'backupkey', 'SYSTEM', 'Qwerty1234') 33 | h.sr_enable_primary('NUREMBERG') 34 | ``` 35 | 36 | ## Installation 37 | 38 | To install the **shaptools** package run the pip command: 39 | 40 | pip install . 41 | 42 | To install it in a development state run: 43 | 44 | pip install -e . 45 | 46 | ## Dependencies 47 | 48 | List of dependencies are specified in the ["Requirements file"](requirements.txt). Items can be installed using pip: 49 | 50 | pip install -r requirements.txt 51 | 52 | ## License 53 | 54 | See the [LICENSE](LICENSE) file for license rights and limitations. 55 | 56 | ## Author 57 | 58 | Xabier Arbulu Insausti (xarbulu@suse.com) 59 | 60 | ## Reviewers 61 | 62 | _Pull request_ preferred reviewers for this project: 63 | 64 | - Xabier Arbulu Insausti (xarbulu@suse.com) 65 | -------------------------------------------------------------------------------- /_service: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://github.com/%%REPOSITORY%%.git 4 | git 5 | .git 6 | python-shaptools 7 | 0.3.14+git.%ct.%h 8 | %%VERSION%% 9 | 10 | 11 | 12 | *.tar 13 | gz 14 | 15 | 16 | 17 | python-shaptools 18 | 19 | 20 | -------------------------------------------------------------------------------- /bin/shapcli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Shaptools command line tool 4 | """ 5 | 6 | from shaptools import shapcli 7 | 8 | if __name__ == "__main__": 9 | shapcli.run() 10 | -------------------------------------------------------------------------------- /docs/SHAPCLI.md: -------------------------------------------------------------------------------- 1 | # SHAPCLI 2 | 3 | shapcli is an executable tool to use the api provided by shaptools. It wraps most of the commands and 4 | exposes them as a command line tool. 5 | 6 | In order to use the utility the `shaptools` library must be installed (either using `pip` or `rpm` package). 7 | 8 | ## Disclaimer 9 | 10 | This tool will only work if `shaptools` is installed for `python 3` version. 11 | 12 | ## Motivation 13 | 14 | The major motivation behind this tools is to provide an easy way to run all of the tools provided by 15 | SAP regarding HANA. It wraps the next commands: `HDB`, `hdbnsutil`, `hdbuserstore`, 16 | `HDBSettings.sh`, `hdbsql`. 17 | 18 | Using this tool be avoid the need to change to SAP users every time we need to run any of these 19 | commands. This is really helpful when we are running other commands in the same time (as `crmsh` 20 | commands for example). Besides, having all of them gathered in the same place makes the usage 21 | easier. 22 | 23 | ## How to use 24 | 25 | `shapcli` can be used providing the SAP HANA database information through command line or using a 26 | json configuration file (the options are mutually exclusive). 27 | Here an example of how to create the configuration file: [config.json](shapcli.config.example) 28 | 29 | Here some examples: 30 | 31 | ``` 32 | shapcli -s sid -i 00 -p HANAPASSWORD hana version 33 | shapcli -c config.json hana version 34 | ``` 35 | 36 | Check how it works and help output running: 37 | 38 | ``` 39 | shapcli -h 40 | ``` 41 | 42 | The main options are: `hana` and `sr`; 43 | 44 | * `hana`: Commands to manage SAP HANA database general functionalities. 45 | * `sr`: Commands to manage SAP HANA system replication. 46 | 47 | Using the `-h` flag in each option will output a new help output. For example: 48 | 49 | ``` 50 | shapcli hana -h 51 | ``` 52 | 53 | ### Running commands in remote nodes 54 | 55 | The commands can be executed in remote nodes too. For that the `-r` or `--remote` flag have to be 56 | used (or adding the `remote` entry in the configuration file [the `-r` flag has priority over the configuration file entry]). 57 | 58 | ``` 59 | shapcli -c config.json -r remotehost hana version 60 | ``` 61 | 62 | If the ssh keys of the current node is not installed in the remote host, the password must be 63 | provided after the command. To avoid this, the ssh key of the current node can be authorized in the 64 | remote node. By default, the ssh public key must be added in: `/usr/sap/PRD/home/.ssh/authorized_keys` 65 | (where `PRD` is the SAP HANA instanse sid in uppercase) 66 | -------------------------------------------------------------------------------- /docs/shapcli.config.example: -------------------------------------------------------------------------------- 1 | { 2 | "sid": "prd", 3 | "instance": "00", 4 | "password": "HANAPASSWORD" 5 | } 6 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = *_test.py 3 | testpaths = shaptools 4 | norecursedirs = 5 | -------------------------------------------------------------------------------- /python-shaptools.changes: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------- 2 | Tue Nov 21 11:22:48 UTC 2023 - Pablo Suárez Hernández 3 | 4 | - Create version 0.3.14 5 | - Make shaptools available for venv-salt-minion (bsc#1212695) 6 | 7 | ------------------------------------------------------------------- 8 | Fri Jan 13 02:13:36 UTC 2023 - Steve Kowalik 9 | 10 | - Correct macro usage, %ifpython2 is only suitable for Requires. 11 | 12 | ------------------------------------------------------------------- 13 | Thu Jan 5 23:44:22 UTC 2023 - Steve Kowalik 14 | 15 | - Only BuildRequire python-mock under Python 2. 16 | 17 | ------------------------------------------------------------------- 18 | Fri Mar 25 15:45:21 UTC 2021 - Eike Walldt 19 | 20 | - Create version 0.3.13 21 | - add HANA add_hosts feature 22 | 23 | ------------------------------------------------------------------- 24 | Fri Mar 12 14:59:00 UTC 2021 - Xabier Arbulu 25 | 26 | - Create version 0.3.12 27 | - Fix the HANA sidadm user creation to transform to lowercase 28 | properly 29 | (bsc#1185090) 30 | 31 | ------------------------------------------------------------------- 32 | Tue Feb 9 13:26:18 UTC 2021 - Xabier Arbulu 33 | 34 | - Fix spec file to build properly the shapcli executable 35 | 36 | ------------------------------------------------------------------- 37 | Thu Sep 24 12:16:46 UTC 2020 - Xabier Arbulu 38 | 39 | - Create version 0.3.11 40 | - Add new functionalities to know the currently installed ENSA 41 | version for Netweaver (only for ASCS and ERS instances) 42 | 43 | (jsc#SLE-4047) 44 | 45 | ------------------------------------------------------------------- 46 | Thu Sep 3 06:44:31 UTC 2020 - Xabier Arbulu 47 | 48 | - Create version 0.3.10 49 | - Fix how HANA database is started and stopped to work in multi 50 | host environment. sapcontrol commands are used instead of HDB 51 | now 52 | 53 | (jsc#SLE-4047) 54 | 55 | ------------------------------------------------------------------- 56 | Tue Aug 25 06:53:29 UTC 2020 - Xabier Arbulu 57 | 58 | - Create version 0.3.9 59 | - Fix issue when secondary registration fails after a successful 60 | SSFS files copy process. Now the registration return code will 61 | be checked in the new call (bsc#1175709) 62 | 63 | ------------------------------------------------------------------- 64 | Fri Mar 27 18:15:37 UTC 2020 - Simranpal Singh 65 | 66 | - Create version 0.3.8 67 | - Add functionality to extract SAP sar files using SAPCAR tool 68 | 69 | (jsc#SLE-10902, jsc#SLE-10903, jsc#ECO-817, jsc#ECO-818) 70 | 71 | ------------------------------------------------------------------- 72 | Tue Mar 24 11:29:14 UTC 2020 - Xabier Arbulu 73 | 74 | - Create version 0.3.7 75 | - Improve hana installation software detection to allow more use 76 | cases 77 | 78 | ------------------------------------------------------------------- 79 | Thu Mar 19 15:31:22 UTC 2020 - Xabier Arbulu 80 | 81 | - Create version 0.3.6 82 | - Change the get_platform method to include the system OS type 83 | 84 | ------------------------------------------------------------------- 85 | Thu Jan 2 21:59:30 UTC 2020 - Simranpal Singh 86 | 87 | - Create package version 0.3.5 88 | - Add function to install HANA with XML passwords file 89 | - Add functionality to update XML passwords file 90 | 91 | ------------------------------------------------------------------- 92 | Thu Dec 5 10:48:53 UTC 2019 - Xabier Arbulu 93 | 94 | - Create package version 0.3.4 95 | - Fix ascs restart conditions in ers installation 96 | 97 | ------------------------------------------------------------------- 98 | Thu Nov 7 00:36:08 UTC 2019 - Simranpal Singh 99 | 100 | - Create package version 0.3.3 101 | - Add update_conf_file for Netweaver 102 | 103 | ------------------------------------------------------------------- 104 | Tue Oct 22 02:41:35 UTC 2019 - Xabier Arbulu 105 | 106 | - Create package version 0.3.2 107 | - Add isconnected and reconnect methods 108 | 109 | ------------------------------------------------------------------- 110 | Wed Aug 7 12:50:36 UTC 2019 - Xabier Arbulu Insausti 111 | 112 | - Add the required code to install SAP Netweaver instances 113 | * Wrap sapcontrol command usage 114 | * Install and uninstall SAP instances 115 | * Check current installation status 116 | 117 | ------------------------------------------------------------------- 118 | Tue Jul 23 11:04:25 UTC 2019 - Xabier Arbulu Insausti 119 | 120 | - Create package version 0.3.1 121 | - Add support for Power machines 122 | (jsc#SLE-4031, jsc#SLE-4143, boo#1137989) 123 | ------------------------------------------------------------------- 124 | Thu Jul 18 08:46:15 UTC 2019 - Xabier Arbulu Insausti 125 | 126 | - Add an option to run the commands in remote nodes to shapcli 127 | 128 | ------------------------------------------------------------------- 129 | Wed Jul 17 09:34:22 UTC 2019 - Xabier Arbulu Insausti 130 | 131 | - Create package version 0.3.0 132 | - shapcli is provided to expose shaptools api methods as command line tool 133 | 134 | ------------------------------------------------------------------- 135 | Tue Jun 11 11:29:44 UTC 2019 - Xabier Arbulu Insausti 136 | 137 | - Create package version 0.2.1 with fixed spec files. Now the package 138 | is available from SLE12-SP2 to SLE15 versions 139 | (jsc#SLE-4031, jsc#SLE-4143, boo#1137989) 140 | 141 | ------------------------------------------------------------------- 142 | Tue Jun 4 07:23:40 UTC 2019 - Xabier Arbulu Insausti 143 | 144 | - Create package version 0.2.0 with the latest changes 145 | 146 | ------------------------------------------------------------------- 147 | Wed May 29 12:26:08 UTC 2019 - Ayoub Belarbi (abelarbi@suse.com) 148 | 149 | - Update hdb connector to return metadata besides the query 150 | records. 151 | 152 | ------------------------------------------------------------------- 153 | Thu May 16 09:35:41 UTC 2019 - Xabier Arbulu Insausti 154 | 155 | - Update SR registration process. Now the methods retries the 156 | registration command until a successful return and copies the 157 | SSFS files from the primary node as well 158 | 159 | ------------------------------------------------------------------- 160 | Tue Apr 23 11:04:53 UTC 2019 - Xabier Arbulu Insausti 161 | 162 | - Remove enum34 dependency from code 163 | 164 | ------------------------------------------------------------------- 165 | Wed Mar 6 11:01:10 UTC 2019 - dakechi@suse.com 166 | 167 | - Fix the package license to be in synch with repo license. 168 | 169 | ------------------------------------------------------------------- 170 | Mon Mar 04 15:09:10 UTC 2019 - xarbulu@suse.com 171 | 172 | - Improved the use of keystore access. When the key_name is informed, 173 | the user_name/user_password is not needed. 174 | 175 | ------------------------------------------------------------------- 176 | Thu Feb 25 10:20:10 UTC 2019 - dakechi@suse.com 177 | 178 | - Fix UT to check the inst formatting correctly 179 | - Move the comment to the right place 180 | - Enforce the HANA instance nr format. 181 | - Forces Instance nr always with 2 positions filled with 0 182 | - Forces right formatting on HANA OS admin user. 183 | 184 | ------------------------------------------------------------------- 185 | Thu Dec 20 08:33:10 UTC 2018 - xarbulu@suse.com 186 | 187 | - First package version 188 | -------------------------------------------------------------------------------- /python-shaptools.spec: -------------------------------------------------------------------------------- 1 | # 2 | # spec file for package python-shaptools 3 | # 4 | # Copyright (c) 2019 SUSE LLC 5 | # 6 | # All modifications and additions to the file contributed by third parties 7 | # remain the property of their copyright owners, unless otherwise agreed 8 | # upon. The license for this file, and modifications and additions to the 9 | # file, is the same license as for the pristine package itself (unless the 10 | # license for the pristine package is not an Open Source License, in which 11 | # case the license is the MIT License). An "Open Source License" is a 12 | # license that conforms to the Open Source Definition (Version 1.9) 13 | # published by the Open Source Initiative. 14 | 15 | # Please submit bugfixes or comments via http://bugs.opensuse.org/ 16 | 17 | %if 0%{?suse_version} < 1500 18 | %bcond_with test 19 | %else 20 | %bcond_without test 21 | %endif 22 | 23 | %if 0%{?sle_version} <= 150300 && !0%{?is_opensuse} 24 | %bcond_without python2 25 | %else 26 | %bcond_with python2 27 | %endif 28 | 29 | %{?!python_module:%define python_module() python-%{**} python3-%{**}} 30 | Name: python-shaptools 31 | Version: 0 32 | Release: 0 33 | Summary: Python tools to interact with SAP HANA utilities 34 | License: Apache-2.0 35 | Group: Development/Languages/Python 36 | Url: https://github.com/SUSE/shaptools 37 | Source: %{name}-%{version}.tar.gz 38 | %if %{with test} 39 | BuildRequires: %{python_module pytest} 40 | %endif 41 | %if %{with python2} 42 | BuildRequires: python-mock 43 | %endif 44 | BuildRequires: %{python_module setuptools} 45 | BuildRequires: fdupes 46 | BuildRequires: python-rpm-macros 47 | Requires(post): update-alternatives 48 | Requires(postun): update-alternatives 49 | BuildArch: noarch 50 | %python_subpackages 51 | 52 | %description 53 | API to expose SAP HANA functionalities 54 | 55 | %package -n python3-shaptools-venv-salt-minion 56 | Summary: Shaptools integration with Salt Bundle 57 | Group: Development/Languages/Python 58 | Requires: venv-salt-minion 59 | Requires: python3-shaptools 60 | Supplements: packageand(python3-shaptools:venv-salt-minion) 61 | BuildArch: noarch 62 | 63 | %description -n python3-shaptools-venv-salt-minion 64 | Integration of shaptools library inside the Salt Bundle, aka venv-salt-minion. 65 | 66 | %prep 67 | %setup -q -n %{name}-%{version} 68 | 69 | %build 70 | %python_build 71 | 72 | %install 73 | %python_install 74 | %python_expand %fdupes %{buildroot}%{$python_sitelib} 75 | # do not install tests 76 | %python_expand rm -r %{buildroot}%{$python_sitelib}/tests 77 | %python_clone -a %{buildroot}%{_bindir}/shapcli 78 | 79 | %post 80 | %python_install_alternative shapcli 81 | 82 | %postun 83 | %python_uninstall_alternative shapcli 84 | 85 | %post -n python3-shaptools-venv-salt-minion 86 | BUNDLE_SITELIB= 87 | if [ -f /usr/lib/venv-salt-minion/bin/python ] 88 | then 89 | BUNDLE_SITELIB=`/usr/lib/venv-salt-minion/bin/python -c "import sysconfig as s; print(s.get_paths().get('purelib'))"` 90 | fi 91 | if [ ! -z "$BUNDLE_SITELIB" ] && [ -d "%{python_sitelib}/shaptools" ] && [ ! -f "$BUNDLE_SITELIB/shaptools" ] 92 | then 93 | ln -s %{python_sitelib}/shaptools/ $BUNDLE_SITELIB/shaptools 94 | fi 95 | 96 | %postun -n python3-shaptools-venv-salt-minion 97 | BUNDLE_SITELIB= 98 | if [ -f /usr/lib/venv-salt-minion/bin/python ] 99 | then 100 | BUNDLE_SITELIB=`/usr/lib/venv-salt-minion/bin/python -c "import sysconfig as s; print(s.get_paths().get('purelib'))"` 101 | fi 102 | if [ ! -z "$BUNDLE_SITELIB" ] && [ -L "$BUNDLE_SITELIB/shaptools" ] 103 | then 104 | rm $BUNDLE_SITELIB/shaptools 105 | fi 106 | 107 | 108 | %if %{with test} 109 | %check 110 | %pytest tests 111 | %endif 112 | 113 | %files %{python_files} 114 | %if 0%{?sle_version:1} && 0%{?sle_version} < 120300 115 | %doc README.md LICENSE 116 | %else 117 | %doc README.md 118 | %license LICENSE 119 | %endif 120 | %{python_sitelib}/* 121 | %python_alternative %{_bindir}/shapcli 122 | 123 | %files -n python3-shaptools-venv-salt-minion 124 | 125 | %changelog 126 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUSE/shaptools/094003dada1554fb4ac621d0bc90d6e598ac93a3/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup script. 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2018-11-15 9 | """ 10 | 11 | import os 12 | 13 | from setuptools import find_packages 14 | try: 15 | from setuptools import setup 16 | except ImportError: 17 | from distutils.core import setup 18 | 19 | import shaptools 20 | 21 | def read(fname): 22 | """ 23 | Utility function to read the README file. README file is used to create 24 | the long description. 25 | """ 26 | 27 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 28 | 29 | VERSION = shaptools.__version__ 30 | NAME = "shaptools" 31 | DESCRIPTION = "API to expose SAP HANA functionalities" 32 | 33 | AUTHOR = "xarbulu" 34 | AUTHOR_EMAIL = "xarbulu@suse.com" 35 | URL = "" 36 | 37 | LICENSE = "Apache-2.0" 38 | 39 | CLASSIFIERS = [ 40 | 41 | ] 42 | 43 | SCRIPTS = ['bin/shapcli'] 44 | 45 | DEPENDENCIES = read('requirements.txt').split() 46 | 47 | PACKAGE_DATA = { 48 | 'shaptools': ['support/ssh_askpass'] 49 | } 50 | DATA_FILES = [] 51 | 52 | 53 | SETUP_PARAMS = dict( 54 | name=NAME, 55 | version=VERSION, 56 | description=DESCRIPTION, 57 | author=AUTHOR, 58 | author_email=AUTHOR_EMAIL, 59 | url=URL, 60 | long_description=read('README.md'), 61 | packages=find_packages(), 62 | package_data=PACKAGE_DATA, 63 | license=LICENSE, 64 | scripts=SCRIPTS, 65 | data_files=DATA_FILES, 66 | install_requires=DEPENDENCIES, 67 | classifiers=CLASSIFIERS, 68 | ) 69 | 70 | def main(): 71 | """ 72 | Setup.py main. 73 | """ 74 | 75 | setup(**SETUP_PARAMS) 76 | 77 | if __name__ == "__main__": 78 | main() 79 | -------------------------------------------------------------------------------- /shaptools/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | :author: xarbulu 3 | :organization: SUSE LLC 4 | :contact: xarbulu@suse.com 5 | 6 | :since: 2018-11-15 7 | """ 8 | 9 | __version__ = "0.3.14" 10 | -------------------------------------------------------------------------------- /shaptools/hdb_connector/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SAP HANA database connector factory 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2019-05-08 9 | """ 10 | 11 | try: 12 | from shaptools.hdb_connector.connectors import dbapi_connector 13 | API = 'dbapi' # pragma: no cover 14 | except ImportError: 15 | try: 16 | from shaptools.hdb_connector.connectors import pyhdb_connector 17 | API = 'pyhdb' # pragma: no cover 18 | except ImportError: 19 | from shaptools.hdb_connector.connectors import base_connector 20 | API = None 21 | 22 | 23 | class HdbConnector(object): 24 | """ 25 | HDB factory connector 26 | """ 27 | 28 | # pragma: no cover 29 | def __new__(cls): 30 | if API == 'dbapi': 31 | return dbapi_connector.DbapiConnector() # pragma: no cover 32 | elif API == 'pyhdb': 33 | return pyhdb_connector.PyhdbConnector() # pragma: no cover 34 | raise base_connector.DriverNotAvailableError('dbapi nor pyhdb are installed') 35 | -------------------------------------------------------------------------------- /shaptools/hdb_connector/connectors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUSE/shaptools/094003dada1554fb4ac621d0bc90d6e598ac93a3/shaptools/hdb_connector/connectors/__init__.py -------------------------------------------------------------------------------- /shaptools/hdb_connector/connectors/base_connector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base connector 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2019-05-08 9 | """ 10 | 11 | import logging 12 | 13 | 14 | class BaseError(Exception): 15 | """ 16 | Base exception 17 | """ 18 | 19 | 20 | class DriverNotAvailableError(Exception): 21 | """ 22 | dbapi nor pyhdb are installed 23 | """ 24 | 25 | 26 | class ConnectionError(Exception): 27 | """ 28 | Error during connection 29 | """ 30 | 31 | 32 | class QueryError(BaseError): 33 | """ 34 | Error during query 35 | """ 36 | 37 | class QueryResult(object): 38 | """ 39 | Class to manage query results 40 | 41 | Args: 42 | records (list of tuples): rows of a query result 43 | metadata (tuple): Sequence of 7-item sequences that describe one result column 44 | """ 45 | 46 | def __init__(self, records, metadata): 47 | self._logger = logging.getLogger(__name__) 48 | self.records = records 49 | self.metadata = metadata 50 | 51 | @classmethod 52 | def load_cursor(cls, cursor): 53 | """ 54 | load cursor and extract records and metadata 55 | 56 | Args: 57 | cursor (obj): Cursor object created by the connector (dbapi or pydhb) 58 | """ 59 | records = cursor.fetchall() # TODO: catch any exceptions raised by fetchall() 60 | metadata = cursor.description 61 | instance = cls(records, metadata) 62 | instance._logger.info('query records: %s', instance.records) 63 | return instance 64 | 65 | class BaseConnector(object): 66 | """ 67 | Base SAP HANA database connector 68 | """ 69 | 70 | def __init__(self): 71 | self._logger = logging.getLogger(__name__) 72 | self._connection = None 73 | 74 | def connect(self, host, port=30015, **kwargs): 75 | """ 76 | Connect to the SAP HANA database 77 | 78 | # TODO: Add option to connect using the key 79 | # TODO: Add encryption options 80 | 81 | Args: 82 | host (str): Host where the database is running 83 | port (int): Database port (3{inst_number}15 by default) 84 | user (str): Existing username in the database 85 | password (str): User password 86 | timeout (int, optional): Connection timeout in seconds (only for pyhdb) 87 | properties: Additional properties can be used with named parameters (only for dbapi) 88 | """ 89 | raise NotImplementedError( 90 | 'method must be implemented in inherited connectors') 91 | 92 | def query(self, sql_statement): 93 | """ 94 | Query a sql statement and return response 95 | """ 96 | raise NotImplementedError( 97 | 'method must be implemented in inherited connectors') 98 | 99 | def disconnect(self): 100 | """ 101 | Disconnect from SAP HANA database 102 | """ 103 | raise NotImplementedError( 104 | 'method must be implemented in inherited connectors') 105 | 106 | def isconnected(self): 107 | """ 108 | Check the connection status 109 | 110 | INFO: Sometimes the state is not changed unless a query is performed 111 | 112 | Returns: 113 | bool: True if connected False otherwise 114 | """ 115 | raise NotImplementedError( 116 | 'method must be implemented in inherited connectors') 117 | 118 | def reconnect(self): 119 | """ 120 | Reconnect to the previously connected SAP HANA database if the connection is lost 121 | """ 122 | raise NotImplementedError( 123 | 'method must be implemented in inherited connectors') 124 | -------------------------------------------------------------------------------- /shaptools/hdb_connector/connectors/dbapi_connector.py: -------------------------------------------------------------------------------- 1 | """ 2 | SAP HANA database connector using official dbapi package 3 | 4 | How to install: 5 | https://help.sap.com/viewer/1efad1691c1f496b8b580064a6536c2d/Cloud/en-US/39eca89d94ca464ca52385ad50fc7dea.html 6 | 7 | :author: xarbulu 8 | :organization: SUSE LLC 9 | :contact: xarbulu@suse.com 10 | 11 | :since: 2019-05-08 12 | """ 13 | 14 | from hdbcli import dbapi 15 | 16 | from shaptools.hdb_connector.connectors import base_connector 17 | 18 | 19 | class DbapiConnector(base_connector.BaseConnector): 20 | """ 21 | Class to manage dbapi connection and queries 22 | """ 23 | 24 | def __init__(self): 25 | super(DbapiConnector, self).__init__() 26 | self._logger.info('dbapi package loaded') 27 | self.__properties = {} 28 | 29 | def connect(self, host, port=30015, **kwargs): 30 | """ 31 | Connect to the SAP HANA database 32 | 33 | # TODO: Add option to connect using the key 34 | # TODO: Add encryption options 35 | 36 | Args: 37 | host (str): Host where the database is running 38 | port (int): Database port (3{inst_number}15 by default) 39 | user (str): Existing username in the database 40 | password (str): User password 41 | properties : Additional properties can be used with named parameters. More info at: 42 | https://help.sap.com/viewer/0eec0d68141541d1b07893a39944924e/2.0.02/en-US/ee592e89dcce4480a99571a4ae7a702f.html 43 | 44 | Example: 45 | To avoid automatic reconnection set RECONNECT='FALSE' as parameter 46 | """ 47 | self._logger.info('connecting to SAP HANA database at %s:%s', host, port) 48 | self.__properties = kwargs 49 | try: 50 | self._connection = dbapi.connect( 51 | address=host, 52 | port=port, 53 | #user=kwargs.get('user'), 54 | #password=kwargs.get('password'), 55 | **self.__properties 56 | ) 57 | except dbapi.Error as err: 58 | raise base_connector.ConnectionError('connection failed: {}'.format(err)) 59 | self._logger.info('connected successfully') 60 | 61 | def query(self, sql_statement): 62 | """ 63 | Query a sql query result and return a result object 64 | """ 65 | self._logger.info('executing sql query: %s', sql_statement) 66 | try: 67 | with self._connection.cursor() as cursor: 68 | cursor.execute(sql_statement) 69 | result = base_connector.QueryResult.load_cursor(cursor) 70 | except dbapi.Error as err: 71 | raise base_connector.QueryError('query failed: {}'.format(err)) 72 | return result 73 | 74 | def disconnect(self): 75 | """ 76 | Disconnect from SAP HANA database 77 | """ 78 | self._logger.info('disconnecting from SAP HANA database') 79 | self._connection.close() 80 | self._logger.info('disconnected successfully') 81 | 82 | def isconnected(self): 83 | """ 84 | Check the connection status 85 | 86 | INFO: Sometimes the state is not changed unless a query is performed 87 | 88 | Returns: 89 | bool: True if connected False otherwise 90 | """ 91 | if self._connection: 92 | return self._connection.isconnected() 93 | return False 94 | 95 | def reconnect(self): 96 | """ 97 | Reconnect to the previously connected SAP HANA database if the connection is lost 98 | 99 | The dbapi object str result example: 100 | 101 | 102 | """ 103 | if not self._connection: 104 | raise base_connector.ConnectionError('connect method must be used first to reconnect') 105 | if not self.isconnected(): 106 | connection_data = str(self._connection).split(':')[-1].strip()[:-1].split(',') 107 | host = connection_data[0] 108 | port = int(connection_data[1]) 109 | #user = connection_data[2] 110 | #password = connection_data[3] 111 | #self.connect(host, port, user=user, password=password, **self._properties) 112 | self._logger.info('reconnecting...') 113 | self.connect(host, port, **self.__properties) 114 | else: 115 | self._logger.info('connection already created') 116 | -------------------------------------------------------------------------------- /shaptools/hdb_connector/connectors/pyhdb_connector.py: -------------------------------------------------------------------------------- 1 | """ 2 | SAP HANA database connector using pyhdb open sourced package 3 | 4 | How to install: 5 | https://github.com/SAP/PyHDB 6 | 7 | :author: xarbulu 8 | :organization: SUSE LLC 9 | :contact: xarbulu@suse.com 10 | 11 | :since: 2019-05-08 12 | """ 13 | 14 | import socket 15 | import pyhdb 16 | 17 | from shaptools.hdb_connector.connectors import base_connector 18 | 19 | 20 | class PyhdbConnector(base_connector.BaseConnector): 21 | """ 22 | Class to manage pyhdb connection and queries 23 | """ 24 | 25 | def __init__(self): 26 | super(PyhdbConnector, self).__init__() 27 | self._logger.info('pyhdb package loaded') 28 | 29 | def connect(self, host, port=30015, **kwargs): 30 | """ 31 | Connect to the SAP HANA database 32 | 33 | # TODO: Add option to connect using the key 34 | # TODO: Add encryption options 35 | 36 | Args: 37 | host (str): Host where the database is running 38 | port (int): Database port (3{inst_number}15 by default) 39 | user (str): Existing username in the database 40 | password (str): User password 41 | timeout (int, optional): Connection and queries timeout in seconds 42 | """ 43 | self._logger.info('connecting to SAP HANA database at %s:%s', host, port) 44 | try: 45 | self._connection = pyhdb.connect( 46 | host=host, 47 | port=port, 48 | user=kwargs.get('user'), 49 | password=kwargs.get('password') 50 | ) 51 | self._connection.timeout = kwargs.get('timeout', None) 52 | except (socket.error, pyhdb.exceptions.DatabaseError) as err: 53 | raise base_connector.ConnectionError('connection failed: {}'.format(err)) 54 | self._logger.info('connected successfully') 55 | 56 | def query(self, sql_statement): 57 | """ 58 | Query a sql query result and return a result object 59 | """ 60 | self._logger.info('executing sql query: %s', sql_statement) 61 | try: 62 | cursor = None 63 | cursor = self._connection.cursor() 64 | cursor.execute(sql_statement) 65 | result = base_connector.QueryResult.load_cursor(cursor) 66 | except pyhdb.exceptions.DatabaseError as err: 67 | raise base_connector.QueryError('query failed: {}'.format(err)) 68 | finally: 69 | if cursor: 70 | cursor.close() 71 | return result 72 | 73 | def disconnect(self): 74 | """ 75 | Disconnect from SAP HANA database 76 | """ 77 | self._logger.info('disconnecting from SAP HANA database') 78 | self._connection.close() 79 | self._logger.info('disconnected successfully') 80 | 81 | def isconnected(self): 82 | """ 83 | Check the connection status. It checks if the socket is properly working 84 | 85 | INFO: Sometimes the state is not changed unless a query is performed 86 | 87 | Returns: 88 | bool: True if connected False otherwise 89 | """ 90 | if self._connection and self._connection.isconnected(): 91 | try: 92 | self._connection._socket.getpeername() 93 | return True 94 | except OSError: 95 | self._logger.error('socket is not correctly working. closing socket') 96 | self._connection._socket = None 97 | return False 98 | return False 99 | 100 | def reconnect(self): 101 | """ 102 | Reconnect to the previously connected SAP HANA database if the connection is lost 103 | """ 104 | if not self._connection: 105 | raise base_connector.ConnectionError('connect method must be used first to reconnect') 106 | if not self.isconnected(): 107 | # Initialize the socket connection parameters as a new connection will be created 108 | self._connection.session_id = -1 109 | self._connection.packet_count = -1 110 | try: 111 | self._logger.info('reconnecting...') 112 | self._connection.connect() 113 | except (socket.error, pyhdb.exceptions.DatabaseError) as err: 114 | raise base_connector.ConnectionError('connection failed: {}'.format(err)) 115 | else: 116 | self._logger.info('connection already created') 117 | -------------------------------------------------------------------------------- /shaptools/netweaver.py: -------------------------------------------------------------------------------- 1 | """ 2 | SAP Netweaver management module 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2010-07-30 9 | """ 10 | 11 | from __future__ import print_function 12 | 13 | import logging 14 | import time 15 | import fileinput 16 | import re 17 | 18 | from shaptools import shell 19 | 20 | # python2 and python3 compatibility for string usage 21 | try: 22 | basestring 23 | except NameError: # pragma: no cover 24 | basestring = str 25 | 26 | 27 | class NetweaverError(Exception): 28 | """ 29 | Error during Netweaver command execution 30 | """ 31 | 32 | 33 | class NetweaverInstance(object): 34 | """ 35 | SAP Netweaver instance implementation 36 | 37 | Args: 38 | sid (str): SAP Netweaver sid 39 | inst (str): SAP Netweaver instance number 40 | password (str): Netweaver instance password 41 | remote_host (str, opt): Remote host where the command will be executed 42 | """ 43 | 44 | # SID is usually written uppercased, but the OS user is always created lower case. 45 | NETWEAVER_USER = '{sid}adm'.lower() 46 | UNINSTALL_PRODUCT = 'NW_Uninstall:GENERIC.IND.PD' 47 | GETPROCESSLIST_SUCCESS_CODES = [0, 3, 4] 48 | SUCCESSFULLY_INSTALLED = 0 49 | UNSPECIFIED_ERROR = 111 50 | 51 | def __init__(self, sid, inst, password, **kwargs): 52 | # Force instance nr always with 2 positions. 53 | inst = '{:0>2}'.format(inst) 54 | if not all(isinstance(i, basestring) for i in [sid, inst, password]): 55 | raise TypeError( 56 | 'provided sid, inst and password parameters must be str type') 57 | 58 | self._logger = logging.getLogger('{}{}'.format(sid, inst)) 59 | self.sid = sid 60 | self.inst = inst 61 | self._password = password 62 | self.remote_host = kwargs.get('remote_host', None) 63 | 64 | def _execute_sapcontrol(self, sapcontrol_function, **kwargs): 65 | """ 66 | Execute sapcontrol commands and return result 67 | 68 | Args: 69 | sapcontrol_function (str): sapcontrol function 70 | exception (boolean): Raise NetweaverError non-zero return code (default true) 71 | host (str, optional): Host where the command will be executed 72 | inst (str, optional): Use a different instance number 73 | user (str, optional): Define a different user for the command 74 | password (str, optional): The new user password 75 | 76 | Returns: 77 | ProcessResult: ProcessResult instance storing subprocess returncode, 78 | stdout and stderr 79 | """ 80 | exception = kwargs.get('exception', True) 81 | # The -host and -user parameters are used in sapcontrol to authorize commands execution 82 | # in remote host Netweaver instances 83 | host = kwargs.get('host', None) 84 | inst = kwargs.get('inst', self.inst) 85 | user = kwargs.get('user', None) 86 | password = kwargs.get('password', None) 87 | if user and not password: 88 | raise NetweaverError('Password must be provided together with user') 89 | 90 | host_str = '-host {} '.format(host) if host else '' 91 | user_str = '-user {} {} '.format(user, password) if user else '' 92 | 93 | user = self.NETWEAVER_USER.format(sid=self.sid) 94 | cmd = 'sapcontrol {host}{user}-nr {instance} -function {sapcontrol_function}'.format( 95 | host=host_str, user=user_str, instance=inst, sapcontrol_function=sapcontrol_function) 96 | 97 | result = shell.execute_cmd(cmd, user, self._password, self.remote_host) 98 | 99 | if exception and result.returncode != 0: 100 | raise NetweaverError('Error running sapcontrol command: {}'.format(result.cmd)) 101 | 102 | return result 103 | 104 | @staticmethod 105 | def get_attribute_from_file(conf_file, attribute_pattern): 106 | """ 107 | Get attribute from file using a pattern 108 | """ 109 | with open(conf_file, 'r') as file_content: 110 | attribute_data = shell.find_pattern(attribute_pattern, file_content.read()) 111 | return attribute_data 112 | 113 | @staticmethod 114 | def _is_ascs_installed(processes): 115 | """ 116 | Check if ASCS instance is installed 117 | """ 118 | msg_server = shell.find_pattern(r'msg_server, MessageServer,.*', processes.output) 119 | enserver_ensa1 = shell.find_pattern(r'enserver, EnqueueServer,.*', processes.output) 120 | enq_server_ensa2 = shell.find_pattern(r'enq_server, Enqueue Server 2,.*', processes.output) 121 | return bool(msg_server and (enserver_ensa1 or enq_server_ensa2)) 122 | 123 | @staticmethod 124 | def _is_ers_installed(processes): 125 | """ 126 | Check if ERS instance is installed 127 | """ 128 | enrepserver_ensa1 = shell.find_pattern( 129 | r'enrepserver, EnqueueReplicator,.*', processes.output) 130 | enq_replicator_ensa2 = shell.find_pattern( 131 | r'enq_replicator, Enqueue Replicator 2,.*', processes.output) 132 | return bool(enrepserver_ensa1 or enq_replicator_ensa2) 133 | 134 | @staticmethod 135 | def _is_app_server_installed(processes): 136 | """ 137 | Check if an application server (PAS or AAS) instance is installed 138 | """ 139 | disp = shell.find_pattern(r'disp\+work, Dispatcher,.*', processes.output) 140 | igswd = shell.find_pattern(r'igswd_mt, IGS Watchdog,.*', processes.output) 141 | gwrd = shell.find_pattern(r'gwrd, Gateway,.*', processes.output) 142 | icman = shell.find_pattern(r'icman, ICM,.*', processes.output) 143 | return bool(disp and igswd and gwrd and icman) 144 | 145 | def is_installed(self, sap_instance=None): 146 | """ 147 | Check if SAP Netweaver is installed 148 | 149 | Args: 150 | sap_instance (str): SAP instance type. Available options: ascs, ers, ci, di 151 | If None, if any NW installation is existing will be checked 152 | 153 | Returns: 154 | bool: True if SAP instance is installed, False otherwise 155 | """ 156 | processes = self.get_process_list(False) 157 | # TODO: Might be done using a dictionary to store the methods and keys 158 | if processes.returncode not in self.GETPROCESSLIST_SUCCESS_CODES: 159 | state = False 160 | elif not sap_instance: 161 | state = True 162 | elif sap_instance == 'ascs': 163 | state = self._is_ascs_installed(processes) 164 | elif sap_instance == 'ers': 165 | state = self._is_ers_installed(processes) 166 | elif sap_instance in ['ci', 'di']: 167 | state = self._is_app_server_installed(processes) 168 | else: 169 | raise ValueError('provided sap instance type is not valid: {}'.format(sap_instance)) 170 | return state 171 | 172 | @staticmethod 173 | def _get_ascs_ensa_version(processes): 174 | """ 175 | Get ASCS ENSA version 176 | """ 177 | if shell.find_pattern(r'enserver, EnqueueServer,.*', processes.output): 178 | return 1 179 | elif shell.find_pattern(r'enq_server, Enqueue Server 2,.*', processes.output): 180 | return 2 181 | raise ValueError('ASCS not installed or found') 182 | 183 | @staticmethod 184 | def _get_ers_ensa_version(processes): 185 | """ 186 | Get ERS ENSA version 187 | """ 188 | if shell.find_pattern(r'enrepserver, EnqueueReplicator,.*', processes.output): 189 | return 1 190 | elif shell.find_pattern(r'enq_replicator, Enqueue Replicator 2,.*', processes.output): 191 | return 2 192 | raise ValueError('ERS not installed or found') 193 | 194 | def get_ensa_version(self, sap_instance): 195 | """ 196 | Get currently installed ENSA version 197 | 198 | Args: 199 | sap_instance (str): SAP instance type. Available options: ascs, ers 200 | 201 | Returns: 202 | int: Returns the ENSA version number 203 | 204 | Raises: 205 | ValueError: ENSA system is not installed or found properly 206 | """ 207 | 208 | processes = self.get_process_list(exception=True) 209 | if sap_instance == 'ascs': 210 | return self._get_ascs_ensa_version(processes) 211 | elif sap_instance == 'ers': 212 | return self._get_ers_ensa_version(processes) 213 | raise ValueError('provided sap instance type is not valid: {}'.format(sap_instance)) 214 | 215 | @staticmethod 216 | def _remove_old_files(cwd, root_user, password, remote_host): 217 | """ 218 | Remove old files from SAP installation cwd folder. Only start_dir.cd must remain 219 | """ 220 | # TODO: check start_dir.cd exists 221 | remove_files_cmd = "printf '%q ' {}/*".format(cwd) 222 | remove_files = shell.execute_cmd( 223 | remove_files_cmd, root_user, password, remote_host) 224 | remove_files = remove_files.output.replace('{}/start_dir.cd'.format(cwd), '') 225 | cmd = 'rm -rf {}'.format(remove_files) 226 | shell.execute_cmd(cmd, root_user, password, remote_host) 227 | 228 | @classmethod 229 | def update_conf_file(cls, conf_file, **kwargs): 230 | """ 231 | Update NW installation config file parameters. Add the parameters if they don't exist 232 | 233 | Args: 234 | conf_file (str): Path to the netweaver installation configuration file 235 | kwargs (opt): Dictionary with the values to be updated. 236 | Use the exact name of the netweaver configuration file 237 | 238 | kwargs can be used in the next two modes: 239 | update_conf_file(conf_file, sid='HA1', hostname='hacert01') 240 | update_conf_file(conf_file, **{'sid': 'HA1', 'hostname': 'hacert01'}) 241 | """ 242 | for key, value in kwargs.items(): 243 | pattern = '{key}\s+=.*'.format(key=key) 244 | new_value = '{key} = {value}'.format(key=key, value=value) 245 | with open(conf_file, 'r+') as file_cache: 246 | if key in file_cache.read(): 247 | for line in fileinput.input(conf_file, inplace=1): 248 | line = re.sub(pattern, new_value, line) 249 | print(line, end='') 250 | else: 251 | file_cache.write('\n'+new_value) 252 | return conf_file 253 | 254 | @classmethod 255 | def install( 256 | cls, software_path, virtual_host, product_id, conf_file, root_user, password, **kwargs): 257 | """ 258 | Install SAP Netweaver instance 259 | 260 | Args: 261 | software_path (str): Path where SAP Netweaver 'sapinst' tool is located 262 | virtual_host (str): Virtual host name of the machine 263 | product_id (str): SAP instance product id 264 | conf_file (str): Path to the configuration file 265 | root_user (str): Root user name 266 | password (str): Root user password 267 | cwd (str, opt): New value for SAPINST_CWD parameter 268 | CAUTION: All of the files stored in this path will be removed except the 269 | start_dir.cd. This folder only will contain temporary files about the installation. 270 | exception (bool, opt): Raise and exception in case of error if True, return result 271 | object otherwise 272 | remote_host (str, opt): Remote host where the command will be executed 273 | """ 274 | cwd = kwargs.get('cwd', None) 275 | raise_exception = kwargs.get('exception', True) 276 | remote_host = kwargs.get('remote_host', None) 277 | 278 | if cwd: 279 | # This operation must be done in order to avoid incorrect files usage 280 | cls._remove_old_files(cwd, root_user, password, remote_host) 281 | 282 | cmd = '{software_path}/sapinst SAPINST_USE_HOSTNAME={virtual_host} '\ 283 | 'SAPINST_EXECUTE_PRODUCT_ID={product_id} '\ 284 | 'SAPINST_SKIP_SUCCESSFULLY_FINISHED_DIALOG=true SAPINST_START_GUISERVER=false '\ 285 | 'SAPINST_INPUT_PARAMETERS_URL={conf_file}{cwd}'.format( 286 | software_path=software_path, 287 | virtual_host=virtual_host, 288 | product_id=product_id, 289 | conf_file=conf_file, 290 | cwd=' SAPINST_CWD={}'.format(cwd) if cwd else '') 291 | result = shell.execute_cmd(cmd, root_user, password, remote_host) 292 | if result.returncode and raise_exception: 293 | if cwd: 294 | raise NetweaverError( 295 | 'SAP Netweaver installation failed. Please check swpm installation logs'\ 296 | '(sapinst_dev.log and sapinst.log) located at {0} for further '\ 297 | 'information'.format(cwd)) 298 | else: 299 | raise NetweaverError( 300 | 'SAP Netweaver installation failed.'\ 301 | ' Please check swpm installation logs'\ 302 | '(sapinst_dev.log and sapinst.log) located at'\ 303 | ' /tmp/sapinst_instdir default folder for further information') 304 | return result 305 | 306 | @classmethod 307 | def _ascs_restart_needed(cls, installation_result): 308 | """ 309 | Check the ERS installation return code and output to see if the ASCS instance restart 310 | is needed 311 | 312 | Args: 313 | installation_result (shell.ProcessResult): ERS installation result 314 | 315 | Returns: True if ASCS restart is needed, False otherwise 316 | """ 317 | expected_msg = \ 318 | '

Error when stopping instance.

Cannot stop instance (.*)'\ 319 | ' on host (.*).

Stop the instance manually and choose OK to '\ 320 | 'continue.' 321 | if installation_result.returncode == cls.UNSPECIFIED_ERROR: 322 | if shell.find_pattern(expected_msg, installation_result.output): 323 | return True 324 | return False 325 | 326 | @classmethod 327 | def _restart_ascs(cls, conf_file, ers_pass, ascs_pass, remote_host=None): 328 | """ 329 | Restart ascs from the ERS host. 330 | 331 | Args: 332 | conf_file (str): Path to the configuration file 333 | ascs_pass (str): ASCS instance password 334 | remote_host (str, optional): Remote host where the command will be executed 335 | """ 336 | # Get sid and instance number from configuration file 337 | sid = cls.get_attribute_from_file( 338 | conf_file, 'NW_readProfileDir.profileDir += +.*/(.*)/profile').group(1).lower() 339 | instance_number = cls.get_attribute_from_file( 340 | conf_file, 'nw_instance_ers.ersInstanceNumber += +(.*)').group(1) 341 | ers = cls(sid, instance_number, ers_pass, remote_host=remote_host) 342 | result = ers.get_system_instances(exception=False) 343 | ascs_data = shell.find_pattern( 344 | '(.*), (.*), (.*), (.*), (.*), MESSAGESERVER|ENQUE, GREEN', result.output) 345 | 346 | ascs_user = '{}adm'.format(sid).lower() 347 | ascs_hostname = ascs_data.group(1) 348 | ascs_instance_number = ascs_data.group(2) 349 | 350 | ers.stop(host=ascs_hostname, inst=ascs_instance_number, user=ascs_user, password=ascs_pass) 351 | ers.start(host=ascs_hostname, inst=ascs_instance_number, user=ascs_user, password=ascs_pass) 352 | 353 | @classmethod 354 | def install_ers( 355 | cls, software_path, virtual_host, product_id, conf_file, root_user, password, **kwargs): 356 | """ 357 | Install SAP Netweaver ERS instance. ERS instance installation needs an active polling 358 | the instance where the ASCS is installed. 359 | 360 | Args: 361 | software_path (str): Path where SAP Netweaver 'sapinst' tool is located 362 | virtual_host (str): Virtual host name of the machine 363 | product_id (str): SAP instance product id 364 | conf_file (str): Path to the configuration file 365 | root_user (str): Root user name 366 | password (str): Root user password 367 | ascs_password (str, optional): Password of the SAP user in the machine hosting the 368 | ASCS instance. If it's not set the same password used to install ERS will be used 369 | timeout (int, optional): Timeout of the installation process. If 0 it will try to 370 | install the instance only once 371 | interval (int, optional): Retry interval in seconds 372 | remote_host (str, opt): Remote host where the command will be executed 373 | """ 374 | timeout = kwargs.get('timeout', 0) 375 | interval = kwargs.get('interval', 5) 376 | ers_pass = cls.get_attribute_from_file( 377 | conf_file, 'nwUsers.sidadmPassword += +(.*)').group(1) 378 | ascs_pass = kwargs.get('ascs_password', ers_pass) 379 | remote_host = kwargs.get('remote_host', None) 380 | cwd = kwargs.get('cwd', None) 381 | 382 | current_time = time.time() 383 | current_timeout = current_time + timeout 384 | while current_time <= current_timeout: 385 | result = cls.install( 386 | software_path, virtual_host, product_id, conf_file, root_user, password, 387 | exception=False, remote_host=remote_host, cwd=cwd) 388 | 389 | if result.returncode == cls.SUCCESSFULLY_INSTALLED: 390 | break 391 | elif cls._ascs_restart_needed(result): 392 | cls._restart_ascs(conf_file, ers_pass, ascs_pass, remote_host) 393 | break 394 | 395 | time.sleep(interval) 396 | current_time = time.time() 397 | else: 398 | raise NetweaverError( 399 | 'SAP Netweaver ERS installation failed after {} seconds'.format(timeout)) 400 | 401 | def uninstall(self, software_path, virtual_host, conf_file, root_user, password, **kwargs): 402 | """ 403 | Uninstall SAP Netweaver instance 404 | 405 | Args: 406 | software_path (str): Path where SAP Netweaver 'sapinst' tool is located 407 | virtual_host (str): Virtual host name of the machine 408 | conf_file (str): Path to the configuration file 409 | root_user (str): Root user name 410 | password (str): Root user password 411 | remote_host (str, opt): Remote host where the command will be executed 412 | """ 413 | remote_host = kwargs.get('remote_host', None) 414 | user = self.NETWEAVER_USER.format(sid=self.sid) 415 | self.install( 416 | software_path, virtual_host, self.UNINSTALL_PRODUCT, conf_file, root_user, password, 417 | remote_host=remote_host) 418 | shell.remove_user(user, True, root_user, password, remote_host) 419 | 420 | def get_process_list(self, exception=True, **kwargs): 421 | """ 422 | Get SAP processes list 423 | """ 424 | result = self._execute_sapcontrol('GetProcessList', exception=False, **kwargs) 425 | if exception and result.returncode not in self.GETPROCESSLIST_SUCCESS_CODES: 426 | raise NetweaverError('Error running sapcontrol command: {}'.format(result.cmd)) 427 | return result 428 | 429 | def get_system_instances(self, exception=True, **kwargs): 430 | """ 431 | Get SAP system instances list 432 | """ 433 | result = self._execute_sapcontrol('GetSystemInstanceList', exception=False, **kwargs) 434 | if exception and result.returncode: 435 | raise NetweaverError('Error running sapcontrol command: {}'.format(result.cmd)) 436 | return result 437 | 438 | def get_instance_properties(self, exception=True, **kwargs): 439 | """ 440 | Get SAP instance properties 441 | """ 442 | result = self._execute_sapcontrol('GetInstanceProperties', exception=False, **kwargs) 443 | if exception and result.returncode: 444 | raise NetweaverError('Error running sapcontrol command: {}'.format(result.cmd)) 445 | return result 446 | 447 | def start(self, wait=15, delay=0, exception=True, **kwargs): 448 | """ 449 | Start SAP instance 450 | Args: 451 | wait (int): Time to wait until the processes are started in seconds 452 | """ 453 | if wait: 454 | cmd = 'StartWait {} {}'.format(wait, delay) 455 | else: 456 | cmd = 'Start' 457 | result = self._execute_sapcontrol(cmd, exception=False, **kwargs) 458 | if exception and result.returncode: 459 | raise NetweaverError('Error running sapcontrol command: {}'.format(result.cmd)) 460 | return result 461 | 462 | def stop(self, wait=15, delay=0, exception=True, **kwargs): 463 | """ 464 | Stop SAP instance 465 | Args: 466 | wait (int): Time to wait until the processes are stopped in seconds 467 | """ 468 | if wait: 469 | cmd = 'StopWait {} {}'.format(wait, delay) 470 | else: 471 | cmd = 'Stop' 472 | result = self._execute_sapcontrol(cmd, exception=False, **kwargs) 473 | if exception and result.returncode: 474 | raise NetweaverError('Error running sapcontrol command: {}'.format(result.cmd)) 475 | return result 476 | -------------------------------------------------------------------------------- /shaptools/saputils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to utilize SAP Technology components 3 | 4 | :author: sisingh 5 | :organization: SUSE LLC 6 | :contact: sisingh@suse.com 7 | 8 | :since: 2020-03-12 9 | """ 10 | #TODO: Add support for other SAPCAR functionalities apart from extraction 11 | 12 | import os 13 | 14 | from shaptools import shell 15 | 16 | 17 | class SapUtilsError(Exception): 18 | """ 19 | Error during SapUtils command execution 20 | """ 21 | 22 | 23 | class FileDoesNotExistError(SapUtilsError): 24 | """ 25 | Error when the specified files does not exist 26 | """ 27 | 28 | 29 | def extract_sapcar_file(sapcar_exe, sar_file, **kwargs): 30 | """ 31 | Execute SAPCAR command to decompress a SAP CAR or SAR archive files. 32 | If user and password are provided it will be executed with this user. 33 | 34 | Args: 35 | sapcar_exe(str): Path to the SAPCAR executable 36 | sar_file (str): Path to the sar file to be extracted 37 | options (str, opt): Additional options to SAPCAR command 38 | output_dir (str, opt): Directory where archive will be extracted. It creates the dir 39 | if the path doesn't exist. If it's not set the current dir is used 40 | user (str, opt): User to execute the SAPCAR command 41 | password (str, opt): User password 42 | remote_host (str, opt): Remote host where the command will be executed 43 | """ 44 | if not os.path.isfile(sapcar_exe): 45 | raise FileDoesNotExistError('SAPCAR executable \'{}\' does not exist'.format(sapcar_exe)) 46 | if not os.path.isfile(sar_file): 47 | raise FileDoesNotExistError('The SAR file \'{}\' does not exist'.format(sar_file)) 48 | 49 | options = kwargs.get('options', None) 50 | output_dir = kwargs.get('output_dir', None) 51 | user = kwargs.get('user', None) 52 | password = kwargs.get('password', None) 53 | remote_host = kwargs.get('remote_host', None) 54 | 55 | output_dir_str = ' -R {}'.format(output_dir) if output_dir else '' 56 | options_str = ' {}'.format(options) if options else '' 57 | 58 | cmd = '{sapcar_exe} -xvf {sar_file}{options_str}{output_dir_str}'.format( 59 | sapcar_exe=sapcar_exe, sar_file=sar_file, 60 | options_str=options_str, output_dir_str=output_dir_str) 61 | 62 | result = shell.execute_cmd(cmd, user=user, password=password, remote_host=remote_host) 63 | if result.returncode: 64 | raise SapUtilsError('Error running SAPCAR command') 65 | return result 66 | -------------------------------------------------------------------------------- /shaptools/shapcli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Code to expose some useful methods using the command line 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2019-07-11 9 | """ 10 | 11 | import logging 12 | import argparse 13 | import json 14 | 15 | from shaptools import hana 16 | 17 | PROG = 'shapcli' 18 | LOGGING_FORMAT = '%(message)s' 19 | 20 | 21 | class DecodedFormatter(logging.Formatter): 22 | """ 23 | Custom formatter to remove the b'' from the logged text 24 | """ 25 | 26 | def format(self, record): 27 | message = super(DecodedFormatter, self).format(record) 28 | if message.startswith('b\''): 29 | message = message.split('\'')[1] 30 | return message 31 | 32 | 33 | class ConfigData(object): 34 | """ 35 | Class to store the required configuration data 36 | """ 37 | 38 | def __init__(self, data_dict, logger): 39 | try: 40 | self.sid = data_dict['sid'] 41 | self.instance = data_dict['instance'] 42 | self.password = data_dict['password'] 43 | self.remote = data_dict.get('remote', None) 44 | except KeyError as err: 45 | logger.error(err) 46 | logger.error('Configuration file must have the sid, instance and password entries') 47 | raise 48 | 49 | 50 | def setup_logger(level): 51 | """ 52 | Setup logging 53 | """ 54 | logger = logging.getLogger() 55 | handler = logging.StreamHandler() 56 | formatter = DecodedFormatter(LOGGING_FORMAT) 57 | handler.setFormatter(formatter) 58 | logger.addHandler(handler) 59 | logger.setLevel(level=level) 60 | return logger 61 | 62 | 63 | def parse_arguments(): 64 | """ 65 | Parse command line arguments 66 | """ 67 | parser = argparse.ArgumentParser(PROG) 68 | 69 | parser.add_argument( 70 | '-v', '--verbosity', 71 | help='Python logging level. Options: DEBUG, INFO, WARN, ERROR (INFO by default)') 72 | parser.add_argument( 73 | '-r', '--remote', 74 | help='Run the command in other machine using ssh') 75 | parser.add_argument( 76 | '-c', '--config', 77 | help='JSON configuration file with SAP HANA instance data (sid, instance and password)') 78 | parser.add_argument( 79 | '-s', '--sid', help='SAP HANA sid') 80 | parser.add_argument( 81 | '-i', '--instance', help='SAP HANA instance') 82 | parser.add_argument( 83 | '-p', '--password', help='SAP HANA password') 84 | 85 | subcommands = parser.add_subparsers( 86 | title='subcommands', description='valid subcommands', help='additional help') 87 | hana_subparser = subcommands.add_parser( 88 | 'hana', help='Commands to interact with SAP HANA databse') 89 | sr_subparser = subcommands.add_parser( 90 | 'sr', help='Commands to interact with SAP HANA system replication') 91 | 92 | parse_hana_arguments(hana_subparser) 93 | parse_sr_arguments(sr_subparser) 94 | 95 | args = parser.parse_args() 96 | return parser, args 97 | 98 | 99 | def parse_hana_arguments(hana_subparser): 100 | """ 101 | Parse hana subcommand arguments 102 | """ 103 | subcommands = hana_subparser.add_subparsers( 104 | title='hana', dest='hana', help='Commands to interact with SAP HANA databse') 105 | subcommands.add_parser( 106 | 'is_running', help='Check if SAP HANA database is running') 107 | subcommands.add_parser( 108 | 'version', help='Show SAP HANA database version') 109 | subcommands.add_parser( 110 | 'start', help='Start SAP HANA database') 111 | subcommands.add_parser( 112 | 'stop', help='Stop SAP HANA database') 113 | subcommands.add_parser( 114 | 'info', help='Show SAP HANA database information') 115 | subcommands.add_parser( 116 | 'kill', help='Kill all SAP HANA database processes') 117 | subcommands.add_parser( 118 | 'overview', help='Show SAP HANA database overview') 119 | subcommands.add_parser( 120 | 'landscape', help='Show SAP HANA database landscape') 121 | subcommands.add_parser( 122 | 'uninstall', help='Uninstall SAP HANA database instance') 123 | dummy = subcommands.add_parser( 124 | 'dummy', help='Get data from DUMMY table') 125 | dummy.add_argument( 126 | '--key_name', 127 | help='Keystore to connect to sap hana db '\ 128 | '(if this value is set user, password and database are omitted') 129 | dummy.add_argument( 130 | '--user_name', help='User to connect to sap hana db') 131 | dummy.add_argument( 132 | '--user_password', help='Password to connect to sap hana db') 133 | dummy.add_argument( 134 | '--database', help='Database name to connect') 135 | 136 | hdbsql = subcommands.add_parser( 137 | 'hdbsql', help='Run a sql command with hdbsql') 138 | hdbsql.add_argument( 139 | '--key_name', 140 | help='Keystore to connect to sap hana db '\ 141 | '(if this value is set user, password and database are omitted') 142 | hdbsql.add_argument( 143 | '--user_name', help='User to connect to sap hana db') 144 | hdbsql.add_argument( 145 | '--user_password', help='Password to connect to sap hana db') 146 | hdbsql.add_argument( 147 | '--database', help='Database name to connect') 148 | hdbsql.add_argument( 149 | '--query', help='Query to execute') 150 | 151 | user_key = subcommands.add_parser( 152 | 'user', help='Create a new user key') 153 | user_key.add_argument( 154 | '--key_name', help='Key name', required=True) 155 | user_key.add_argument( 156 | '--environment', help='Database location (host:port)', required=True) 157 | user_key.add_argument( 158 | '--user_name', help='User to connect to sap hana db', required=True) 159 | user_key.add_argument( 160 | '--user_password', help='Password to connect to sap hana db', required=True) 161 | user_key.add_argument( 162 | '--database', help='Database name to connect', required=True) 163 | 164 | backup = subcommands.add_parser( 165 | 'backup', help='Create node backup') 166 | backup.add_argument( 167 | '--name', help='Backup file name', required=True) 168 | backup.add_argument( 169 | '--database', help='Database name to connect', required=True) 170 | backup.add_argument( 171 | '--key_name', help='Key name') 172 | backup.add_argument( 173 | '--user_name', help='User to connect to sap hana db') 174 | backup.add_argument( 175 | '--user_password', help='Password to connect to sap hana db') 176 | 177 | 178 | def parse_sr_arguments(sr_subparser): 179 | """ 180 | Parse hana sr subcommand arguments 181 | """ 182 | subcommands = sr_subparser.add_subparsers( 183 | title='sr', dest='sr', help='Commands to interact with SAP HANA system replication') 184 | state = subcommands.add_parser( 185 | 'state', help='Show SAP HANA system replication state') 186 | state.add_argument('--sapcontrol', help='Run with sapcontrol', action='store_true') 187 | status = subcommands.add_parser( 188 | 'status', help='Show SAP HANAsystem replication status') 189 | status.add_argument('--sapcontrol', help='Run with sapcontrol', action='store_true') 190 | subcommands.add_parser( 191 | 'disable', help='Disable SAP HANA system replication (to be executed in Primary node)') 192 | cleanup = subcommands.add_parser( 193 | 'cleanup', help='Cleanup SAP HANA system replication') 194 | cleanup.add_argument('--force', help='Force the cleanup', action='store_true') 195 | subcommands.add_parser( 196 | 'takeover', help='Perform a takeover operation (to be executed in Secondary node)') 197 | enable = subcommands.add_parser( 198 | 'enable', help='Enable SAP HANA system replication primary site') 199 | enable.add_argument('--name', help='Primary site name', required=True) 200 | register = subcommands.add_parser( 201 | 'register', help='Register SAP HANA system replication secondary site') 202 | register.add_argument('--name', help='Secondary site name', required=True) 203 | register.add_argument('--remote_host', help='Primary site hostname', required=True) 204 | register.add_argument( 205 | '--remote_instance', help='Primary site SAP HANA instance number', required=True) 206 | register.add_argument( 207 | '--replication_mode', help='System replication replication mode', default='sync') 208 | register.add_argument( 209 | '--operation_mode', help='System replication operation mode', default='logreplay') 210 | unregister = subcommands.add_parser( 211 | 'unregister', help='Unegister SAP HANA system replication secondary site') 212 | unregister.add_argument('--name', help='Primary site name', required=True) 213 | copy_ssfs = subcommands.add_parser( 214 | 'copy_ssfs', help='Copy current node ssfs files to other host') 215 | copy_ssfs.add_argument('--remote_host', help='Other host name', required=True) 216 | copy_ssfs.add_argument( 217 | '--remote_password', 218 | help='Other host SAP HANA instance password (sid and instance must match '\ 219 | 'with the current host)', required=True) 220 | 221 | 222 | # pylint:disable=W0212 223 | def uninstall(hana_instance, logger): 224 | """ 225 | Uninstall SAP HANA database instance 226 | """ 227 | logger.info( 228 | 'This command will uninstall SAP HANA instance '\ 229 | 'with sid %s and instance number %s (y/n): ', 230 | hana_instance.sid, hana_instance.inst) 231 | response = input() 232 | if response == 'y': 233 | user = hana.HanaInstance.sidadm_user(sid=hana_instance.sid) 234 | hana_instance.uninstall(user, hana_instance._password) 235 | else: 236 | logger.info('Command execution canceled') 237 | 238 | 239 | def run_hdbsql(hana_instance, hana_args, cmd): 240 | """ 241 | Run hdbsql command 242 | """ 243 | hdbsql_cmd = hana_instance._hdbsql_connect( 244 | key_name=hana_args.key_name, 245 | user_name=hana_args.user_name, 246 | user_password=hana_args.user_password) 247 | cmd = '{hdbsql_cmd} {database}\\"{cmd}\\"'.format( 248 | hdbsql_cmd=hdbsql_cmd, 249 | database='-d {} '.format(hana_args.database) if hana_args.database else '', 250 | cmd=cmd) 251 | hana_instance._run_hana_command(cmd) 252 | 253 | def run_hana_subcommands(hana_instance, hana_args, logger): 254 | """ 255 | Run hana subcommands 256 | """ 257 | str_args = hana_args.hana 258 | if str_args == 'is_running': 259 | result = hana_instance.is_running() 260 | logger.info('SAP HANA database running state: %s', result) 261 | elif str_args == 'version': 262 | hana_instance.get_version() 263 | elif str_args == 'start': 264 | hana_instance.start() 265 | elif str_args == 'stop': 266 | hana_instance.stop() 267 | elif str_args == 'info': 268 | hana_instance._run_hana_command('HDB info') 269 | elif str_args == 'kill': 270 | hana_instance._run_hana_command('HDB kill-9') 271 | elif str_args == 'overview': 272 | hana_instance._run_hana_command('HDBSettings.sh systemOverview.py') 273 | elif str_args == 'landscape': 274 | hana_instance._run_hana_command('HDBSettings.sh landscapeHostConfiguration.py') 275 | elif str_args == 'uninstall': 276 | uninstall(hana_instance, logger) 277 | elif str_args == 'dummy': 278 | run_hdbsql(hana_instance, hana_args, 'SELECT * FROM DUMMY') 279 | elif str_args == 'hdbsql': 280 | run_hdbsql(hana_instance, hana_args, hana_args.query) 281 | elif str_args == 'user': 282 | hana_instance.create_user_key( 283 | hana_args.key_name, hana_args.environment, hana_args.user_name, 284 | hana_args.user_password, hana_args.database) 285 | elif str_args == 'backup': 286 | hana_instance.create_backup( 287 | hana_args.database, hana_args.name, hana_args.key_name, 288 | hana_args.user_name, hana_args.user_password) 289 | 290 | 291 | def run_sr_subcommands(hana_instance, sr_args, logger): 292 | """ 293 | Run hana subcommands 294 | """ 295 | str_args = sr_args.sr 296 | if str_args == 'state': 297 | # hana_instance.get_sr_state() 298 | cmd = 'hdbnsutil -sr_state{}'.format(' --sapcontrol=1' if sr_args.sapcontrol else '') 299 | hana_instance._run_hana_command(cmd) 300 | elif str_args == 'status': 301 | # hana_instance.get_sr_status() 302 | cmd = 'HDBSettings.sh systemReplicationStatus.py{}'.format( 303 | ' --sapcontrol=1' if sr_args.sapcontrol else '') 304 | hana_instance._run_hana_command(cmd, exception=False) 305 | elif str_args == 'disable': 306 | hana_instance.sr_disable_primary() 307 | elif str_args == 'cleanup': 308 | hana_instance.sr_cleanup(sr_args.force) 309 | elif str_args == 'takeover': 310 | hana_instance._run_hana_command('hdbnsutil -sr_takeover') 311 | elif str_args == 'enable': 312 | hana_instance.sr_enable_primary(sr_args.name) 313 | elif str_args == 'register': 314 | hana_instance.sr_register_secondary( 315 | sr_args.name, sr_args.remote_host, sr_args.remote_instance, 316 | sr_args.replication_mode, sr_args.operation_mode) 317 | elif str_args == 'unregister': 318 | hana_instance.sr_unregister_secondary(sr_args.name) 319 | elif str_args == 'copy_ssfs': 320 | hana_instance.copy_ssfs_files(sr_args.remote_host, sr_args.remote_password) 321 | 322 | 323 | def load_config_file(config_file, logger): 324 | """ 325 | Load configuration file data 326 | """ 327 | with open(config_file, 'r') as f_ptr: 328 | json_data = json.load(f_ptr) 329 | return json_data 330 | 331 | 332 | # pylint:disable=W0212 333 | def run(): 334 | """ 335 | Main execution 336 | """ 337 | parser, args = parse_arguments() 338 | logger = setup_logger(args.verbosity or logging.DEBUG) 339 | 340 | # If -c or --config flag is received data is loaded from the configuration file 341 | if args.config: 342 | data = load_config_file(args.config, logger) 343 | config_data = ConfigData(data, logger) 344 | elif args.sid and args.instance and args.password: 345 | config_data = ConfigData(vars(args), logger) 346 | else: 347 | logger.info( 348 | 'Configuration file or sid, instance and passwords parameters must be provided\n') 349 | parser.print_help() 350 | exit(1) 351 | 352 | if args.remote: 353 | config_data.remote = args.remote 354 | 355 | try: 356 | hana_instance = hana.HanaInstance( 357 | config_data.sid, config_data.instance, 358 | config_data.password, remote_host=config_data.remote) 359 | if vars(args).get('hana'): 360 | run_hana_subcommands(hana_instance, args, logger) 361 | elif vars(args).get('sr'): 362 | run_sr_subcommands(hana_instance, args, logger) 363 | else: 364 | parser.print_help() 365 | except Exception as err: 366 | logger.error(err) 367 | exit(1) 368 | 369 | 370 | if __name__ == "__main__": # pragma: no cover 371 | run() 372 | -------------------------------------------------------------------------------- /shaptools/shell.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module to interact with the shell commands. 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2018-11-15 9 | """ 10 | 11 | import logging 12 | import os 13 | import subprocess 14 | import shlex 15 | import re 16 | 17 | LOGGER = logging.getLogger('shell') 18 | ASKPASS_SCRIPT = 'support/ssh_askpass' 19 | 20 | 21 | class ShellError(Exception): 22 | """ 23 | Exceptions in shell module 24 | """ 25 | 26 | 27 | class ProcessResult: 28 | """ 29 | Class to store subprocess.Popen output information and offer some 30 | functionalities 31 | 32 | Args: 33 | cmd (str): Executed command 34 | returncode (int): Subprocess return code 35 | output (str): Subprocess output string 36 | err (str): Subprocess error string 37 | """ 38 | 39 | def __init__(self, cmd, returncode, output, err): 40 | self.cmd = cmd 41 | self.returncode = returncode 42 | self.output = output.decode() # Make it compatible with python2 and 3 43 | self.err = err.decode() 44 | 45 | 46 | def log_command_results(stdout, stderr): 47 | """ 48 | Log process stdout and stderr text 49 | """ 50 | logger = logging.getLogger(__name__) 51 | if stdout: 52 | for line in stdout.splitlines(): 53 | logger.info(line) 54 | if stderr: 55 | for line in stderr.splitlines(): 56 | logger.error(line) 57 | 58 | 59 | def find_pattern(pattern, text): 60 | """ 61 | Find pattern in multiline string 62 | 63 | Args: 64 | pattern (str): Regular expression pattern 65 | text (str): string to search in 66 | 67 | Returns: 68 | Match object if the pattern is found, None otherwise 69 | """ 70 | for line in text.splitlines(): 71 | found = re.match(pattern, line) 72 | if found: 73 | return found 74 | return None 75 | 76 | 77 | def format_su_cmd(cmd, user): 78 | """ 79 | Format the command to be executed by other user using su option 80 | 81 | Args: 82 | cmd (str): Command to be formatted 83 | user (str): User to executed the command 84 | 85 | Returns: 86 | str: Formatted command 87 | """ 88 | return 'su -lc "{cmd}" {user}'.format(cmd=cmd, user=user) 89 | 90 | 91 | def format_remote_cmd(cmd, remote_host, user): 92 | """ 93 | Format cmd to run remotely using ssh 94 | 95 | Args: 96 | cmd (str): Command to be executed 97 | remote_host (str): User password 98 | user (str): User to execute the command 99 | 100 | Returns: 101 | str: cmd adapted to be executed remotely 102 | """ 103 | if not user: 104 | raise ValueError('user must be provided') 105 | 106 | cmd = 'ssh {user}@{remote_host} "bash --login -c \'{cmd}\'"'.format( 107 | user=user, remote_host=remote_host, cmd=cmd) 108 | return cmd 109 | 110 | 111 | def create_ssh_askpass(password, cmd): 112 | """ 113 | Create ask pass command 114 | Note: subprocess os.setsid doesn't work as the user might have a password 115 | 116 | Args: 117 | password (str): ssh command password 118 | cmd (str): Command to run 119 | """ 120 | dirname = os.path.dirname(__file__) 121 | ask_pass_script = os.path.join(dirname, ASKPASS_SCRIPT) 122 | ssh_askpass_str = 'export SSH_ASKPASS={};export PASS={};export DISPLAY=:0;setsid {}'.format( 123 | ask_pass_script, password, cmd) 124 | return ssh_askpass_str 125 | 126 | 127 | def execute_cmd(cmd, user=None, password=None, remote_host=None): 128 | """ 129 | Execute a shell command. If user and password are provided it will be 130 | executed with this user. 131 | 132 | Args: 133 | cmd (str): Command to be executed 134 | user (str, opt): User to execute the command 135 | password (str, opt): User password 136 | remote_host (str, opt): Remote host where the command will be executed 137 | 138 | Returns: 139 | ProcessResult: ProcessResult instance storing subprocess returncode, 140 | stdout and stderr 141 | """ 142 | 143 | LOGGER.debug('Executing command "%s" with user %s', cmd, user) 144 | 145 | if remote_host: 146 | cmd = format_remote_cmd(cmd, remote_host, user) 147 | LOGGER.debug('Command updated to "%s"', cmd) 148 | 149 | elif user: 150 | cmd = format_su_cmd(cmd, user) 151 | LOGGER.debug('Command updated to "%s"', cmd) 152 | 153 | proc = subprocess.Popen( 154 | shlex.split(cmd), 155 | stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) 156 | 157 | # Make it compatible with python2 and 3 158 | if password: 159 | password = password.encode() 160 | out, err = proc.communicate(input=password) 161 | 162 | result = ProcessResult(cmd, proc.returncode, out, err) 163 | log_command_results(out, err) 164 | 165 | return result 166 | 167 | def remove_user(user, force=False, root_user=None, root_password=None, remote_host=None): 168 | """ 169 | Remove user from system 170 | Args: 171 | user (str): User to remove 172 | force (bool): Force the remove process even though the user is used in some process 173 | remote_host (str, opt): Remote host where the command will be executed 174 | """ 175 | cmd = 'userdel {}'.format(user) 176 | process_executing = r'userdel: user {} is currently used by process (.*)'.format(user) 177 | while True: 178 | result = execute_cmd(cmd, root_user, root_password, remote_host) 179 | if result.returncode == 0: 180 | return 181 | elif force: 182 | process_pid = find_pattern(process_executing, result.err) 183 | if not process_pid: 184 | break 185 | kill_cmd = 'kill -9 {}'.format(process_pid.group(1)) 186 | execute_cmd(kill_cmd, root_user, root_password, remote_host) 187 | else: 188 | break 189 | raise ShellError('error removing user {}'.format(user)) 190 | -------------------------------------------------------------------------------- /shaptools/support/ssh_askpass: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "$PASS" 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUSE/shaptools/094003dada1554fb4ac621d0bc90d6e598ac93a3/tests/__init__.py -------------------------------------------------------------------------------- /tests/hdb_connector/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/hdb_connector/base_connect_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unitary tests for hdb_connector/connector/base_connector.py. 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2019-05-14 9 | """ 10 | 11 | # pylint:disable=C0103,C0111,W0212,W0611 12 | 13 | import os 14 | import sys 15 | import logging 16 | import unittest 17 | 18 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 19 | 20 | try: 21 | from unittest import mock 22 | except ImportError: 23 | import mock 24 | 25 | 26 | class TestQueryResul(unittest.TestCase): 27 | """ 28 | Unitary tests for base_connector.py QueryResult class 29 | """ 30 | 31 | @classmethod 32 | def setUpClass(cls): 33 | """ 34 | Global setUp. 35 | """ 36 | 37 | logging.basicConfig(level=logging.INFO) 38 | from shaptools.hdb_connector.connectors import base_connector 39 | cls._base_connector = base_connector 40 | 41 | def setUp(self): 42 | """ 43 | Test setUp. 44 | """ 45 | 46 | def tearDown(self): 47 | """ 48 | Test tearDown. 49 | """ 50 | 51 | @classmethod 52 | def tearDownClass(cls): 53 | """ 54 | Global tearDown. 55 | """ 56 | 57 | @mock.patch('logging.Logger.info') 58 | def test_load_cursor(self, logger): 59 | mock_cursor = mock.Mock() 60 | mock_cursor.description = 'metadata' 61 | mock_cursor.fetchall.return_value = ['data1', 'data2'] 62 | result = self._base_connector.QueryResult.load_cursor(mock_cursor) 63 | logger.assert_called_once_with('query records: %s', ['data1', 'data2']) 64 | self.assertEqual(result.records, ['data1', 'data2']) 65 | self.assertEqual(result.metadata, 'metadata') 66 | 67 | 68 | class TestHana(unittest.TestCase): 69 | """ 70 | Unitary tests for base_connector.py BaseConnector class 71 | """ 72 | 73 | @classmethod 74 | def setUpClass(cls): 75 | """ 76 | Global setUp. 77 | """ 78 | 79 | logging.basicConfig(level=logging.INFO) 80 | sys.modules['hdbcli'] = mock.Mock() 81 | sys.modules['pyhdb'] = mock.Mock() 82 | 83 | from shaptools.hdb_connector.connectors import base_connector 84 | cls._base_connector = base_connector 85 | 86 | def setUp(self): 87 | """ 88 | Test setUp. 89 | """ 90 | self._conn = self._base_connector.BaseConnector() 91 | 92 | def tearDown(self): 93 | """ 94 | Test tearDown. 95 | """ 96 | 97 | @classmethod 98 | def tearDownClass(cls): 99 | """ 100 | Global tearDown. 101 | """ 102 | sys.modules.pop('hdbcli') 103 | sys.modules.pop('pyhdb') 104 | 105 | def test_connect(self): 106 | with self.assertRaises(NotImplementedError) as err: 107 | self._conn.connect('host') 108 | self.assertTrue( 109 | 'method must be implemented in inherited connectors' 110 | in str(err.exception)) 111 | 112 | def test_query(self): 113 | with self.assertRaises(NotImplementedError) as err: 114 | self._conn.query('query') 115 | self.assertTrue( 116 | 'method must be implemented in inherited connectors' 117 | in str(err.exception)) 118 | 119 | def test_disconnect(self): 120 | with self.assertRaises(NotImplementedError) as err: 121 | self._conn.disconnect() 122 | self.assertTrue( 123 | 'method must be implemented in inherited connectors' 124 | in str(err.exception)) 125 | 126 | def test_isconnected(self): 127 | with self.assertRaises(NotImplementedError) as err: 128 | self._conn.isconnected() 129 | self.assertTrue( 130 | 'method must be implemented in inherited connectors' 131 | in str(err.exception)) 132 | 133 | def test_reconnect(self): 134 | with self.assertRaises(NotImplementedError) as err: 135 | self._conn.reconnect() 136 | self.assertTrue( 137 | 'method must be implemented in inherited connectors' 138 | in str(err.exception)) 139 | -------------------------------------------------------------------------------- /tests/hdb_connector/dbapi_connector_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unitary tests for hdb_connector/connector/dbapi_connector.py. 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2019-05-14 9 | """ 10 | 11 | # pylint:disable=C0103,C0111,W0212,W0611 12 | 13 | import os 14 | import sys 15 | import logging 16 | import unittest 17 | 18 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 19 | 20 | try: 21 | from unittest import mock 22 | except ImportError: 23 | import mock 24 | 25 | 26 | class DbapiException(Exception): 27 | """ 28 | dbapi.Error mock exception 29 | """ 30 | 31 | 32 | class TestDbapiConnector(unittest.TestCase): 33 | """ 34 | Unitary tests for dbapi_connector.py. 35 | """ 36 | 37 | @classmethod 38 | def setUpClass(cls): 39 | """ 40 | Global setUp. 41 | """ 42 | 43 | logging.basicConfig(level=logging.INFO) 44 | sys.modules['hdbcli'] = mock.Mock() 45 | from shaptools.hdb_connector.connectors import dbapi_connector 46 | cls._dbapi_connector = dbapi_connector 47 | 48 | def setUp(self): 49 | """ 50 | Test setUp. 51 | """ 52 | self._conn = self._dbapi_connector.DbapiConnector() 53 | 54 | def tearDown(self): 55 | """ 56 | Test tearDown. 57 | """ 58 | 59 | @classmethod 60 | def tearDownClass(cls): 61 | """ 62 | Global tearDown. 63 | """ 64 | sys.modules.pop('hdbcli') 65 | 66 | @mock.patch('shaptools.hdb_connector.connectors.dbapi_connector.dbapi') 67 | @mock.patch('logging.Logger.info') 68 | def test_connect(self, mock_logger, mock_dbapi): 69 | self._conn.connect('host', 1234, user='user', password='pass', RECONNECT='FALSE') 70 | mock_dbapi.connect.assert_called_once_with( 71 | address='host', port=1234, user='user', password='pass', RECONNECT='FALSE') 72 | mock_logger.assert_has_calls([ 73 | mock.call('connecting to SAP HANA database at %s:%s', 'host', 1234), 74 | mock.call('connected successfully') 75 | ]) 76 | self.assertEqual( 77 | self._conn._DbapiConnector__properties, 78 | {'user':'user', 'password':'pass', 'RECONNECT': 'FALSE'}) 79 | 80 | @mock.patch('shaptools.hdb_connector.connectors.dbapi_connector.dbapi') 81 | @mock.patch('logging.Logger.info') 82 | def test_connect_error(self, mock_logger, mock_dbapi): 83 | mock_dbapi.Error = DbapiException 84 | mock_dbapi.connect.side_effect = DbapiException('error') 85 | with self.assertRaises(self._dbapi_connector.base_connector.ConnectionError) as err: 86 | self._conn.connect('host', 1234, user='user', password='pass') 87 | 88 | self.assertTrue('connection failed: {}'.format('error') in str(err.exception)) 89 | mock_dbapi.connect.assert_called_once_with( 90 | address='host', port=1234, user='user', password='pass') 91 | 92 | mock_logger.assert_called_once_with( 93 | 'connecting to SAP HANA database at %s:%s', 'host', 1234) 94 | 95 | @mock.patch('shaptools.hdb_connector.connectors.base_connector.QueryResult') 96 | @mock.patch('logging.Logger.info') 97 | def test_query(self, mock_logger, mock_result): 98 | cursor_mock_instance = mock.Mock() 99 | cursor_mock = mock.Mock(return_value=cursor_mock_instance) 100 | mock_result_inst = mock.Mock() 101 | mock_result_inst.records = ['data1', 'data2'] 102 | mock_result_inst.metadata = 'metadata' 103 | mock_result.load_cursor.return_value = mock_result_inst 104 | context_manager_mock = mock.Mock( 105 | __enter__ = cursor_mock, 106 | __exit__ = mock.Mock() 107 | ) 108 | self._conn._connection = mock.Mock() 109 | self._conn._connection.cursor.return_value = context_manager_mock 110 | 111 | result = self._conn.query('query') 112 | 113 | cursor_mock_instance.execute.assert_called_once_with('query') 114 | mock_result.load_cursor.assert_called_once_with(cursor_mock_instance) 115 | 116 | self.assertEqual(result.records, ['data1', 'data2']) 117 | self.assertEqual(result.metadata, 'metadata') 118 | mock_logger.assert_called_once_with('executing sql query: %s', 'query') 119 | 120 | @mock.patch('shaptools.hdb_connector.connectors.dbapi_connector.dbapi') 121 | @mock.patch('logging.Logger.info') 122 | def test_query_error(self, mock_logger, mock_dbapi): 123 | mock_dbapi.Error = DbapiException 124 | self._conn._connection = mock.Mock() 125 | self._conn._connection.cursor.side_effect = DbapiException('error') 126 | with self.assertRaises(self._dbapi_connector.base_connector.QueryError) as err: 127 | self._conn.query('query') 128 | 129 | self.assertTrue('query failed: {}'.format('error') in str(err.exception)) 130 | self._conn._connection.cursor.assert_called_once_with() 131 | mock_logger.assert_called_once_with('executing sql query: %s', 'query') 132 | 133 | @mock.patch('logging.Logger.info') 134 | def test_disconnect(self, mock_logger): 135 | self._conn._connection = mock.Mock() 136 | self._conn.disconnect() 137 | self._conn._connection.close.assert_called_once_with() 138 | mock_logger.assert_has_calls([ 139 | mock.call('disconnecting from SAP HANA database'), 140 | mock.call('disconnected successfully') 141 | ]) 142 | 143 | def test_isconnected_true(self): 144 | self._conn._connection = mock.Mock() 145 | self._conn._connection.isconnected.return_value = True 146 | self.assertTrue(self._conn.isconnected()) 147 | 148 | def test_isconnected_false(self): 149 | self._conn._connection = mock.Mock() 150 | self._conn._connection.isconnected.return_value = False 151 | self.assertFalse(self._conn.isconnected()) 152 | 153 | self._conn._connection = None 154 | self.assertFalse(self._conn.isconnected()) 155 | 156 | def test_reconnect_error(self): 157 | self._conn._connection = None 158 | with self.assertRaises(self._dbapi_connector.base_connector.ConnectionError) as err: 159 | self._conn.reconnect() 160 | self.assertTrue('connect method must be used first to reconnect' in str(err.exception)) 161 | 162 | @mock.patch('logging.Logger.info') 163 | def test_reconnect_connected(self, logger): 164 | self._conn._connection = mock.Mock() 165 | self._conn.isconnected = mock.Mock(return_value=True) 166 | self._conn.reconnect() 167 | logger.assert_called_once_with('connection already created') 168 | 169 | @mock.patch('logging.Logger.info') 170 | def test_reconnect(self, logger): 171 | self._conn._connection = mock.Mock() 172 | self._conn._DbapiConnector__properties = {'user': 'SYSTEM', 'password': 'Qwerty1234'} 173 | self._conn.connect = mock.Mock() 174 | self._conn._connection.__str__ = mock.Mock(return_value=\ 175 | '') 176 | self._conn.isconnected = mock.Mock(return_value=False) 177 | self._conn.reconnect() 178 | 179 | self._conn.connect.assert_called_once_with( 180 | '10.10.10.10', 30015, user='SYSTEM', password='Qwerty1234') 181 | logger.assert_called_once_with('reconnecting...') 182 | -------------------------------------------------------------------------------- /tests/hdb_connector/init_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unitary tests for hdb_connector/__init__.py. 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2019-05-14 9 | """ 10 | 11 | # pylint:disable=C0103,C0111,W0212,W0611 12 | 13 | import os 14 | import sys 15 | import logging 16 | import unittest 17 | 18 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 19 | 20 | try: 21 | from unittest import mock 22 | except ImportError: 23 | import mock 24 | 25 | from shaptools.hdb_connector.connectors import base_connector 26 | 27 | class TestInit(unittest.TestCase): 28 | """ 29 | Unitary tests for __init__.py. 30 | """ 31 | 32 | @classmethod 33 | def setUpClass(cls): 34 | """ 35 | Global setUp. 36 | """ 37 | 38 | logging.basicConfig(level=logging.INFO) 39 | 40 | def setUp(self): 41 | """ 42 | Test setUp. 43 | """ 44 | 45 | def tearDown(self): 46 | """ 47 | Test tearDown. 48 | """ 49 | 50 | @classmethod 51 | def tearDownClass(cls): 52 | """ 53 | Global tearDown. 54 | """ 55 | 56 | def test_error(self): 57 | from shaptools import hdb_connector 58 | with self.assertRaises(base_connector.DriverNotAvailableError) as err: 59 | hdb_connector.HdbConnector() 60 | self.assertTrue('dbapi nor pyhdb are installed' in str(err.exception)) 61 | -------------------------------------------------------------------------------- /tests/hdb_connector/pyhdb_connector_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unitary tests for hdb_connector/connector/pyhdb_connector.py. 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2019-05-14 9 | """ 10 | 11 | # pylint:disable=C0103,C0111,W0212,W0611 12 | 13 | import os 14 | import sys 15 | import logging 16 | import unittest 17 | 18 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 19 | 20 | try: 21 | from unittest import mock 22 | except ImportError: 23 | import mock 24 | 25 | 26 | class TestHDBConnector(unittest.TestCase): 27 | """ 28 | Unitary tests for pyhdb_connector.py. 29 | """ 30 | 31 | @classmethod 32 | def setUpClass(cls): 33 | """ 34 | Global setUp. 35 | """ 36 | 37 | logging.basicConfig(level=logging.INFO) 38 | sys.modules['pyhdb'] = mock.Mock() 39 | from shaptools.hdb_connector.connectors import pyhdb_connector 40 | cls._pyhdb_connector = pyhdb_connector 41 | 42 | def setUp(self): 43 | """ 44 | Test setUp. 45 | """ 46 | self._conn = self._pyhdb_connector.PyhdbConnector() 47 | 48 | def tearDown(self): 49 | """ 50 | Test tearDown. 51 | """ 52 | 53 | @classmethod 54 | def tearDownClass(cls): 55 | """ 56 | Global tearDown. 57 | """ 58 | sys.modules.pop('pyhdb') 59 | 60 | @mock.patch('shaptools.hdb_connector.connectors.pyhdb_connector.pyhdb') 61 | @mock.patch('logging.Logger.info') 62 | def test_connect(self, mock_logger, mock_pyhdb): 63 | self._conn.connect('host', 1234, user='user', password='pass', timeout=1) 64 | mock_pyhdb.connect.assert_called_once_with( 65 | host='host', port=1234, user='user', password='pass') 66 | mock_logger.assert_has_calls([ 67 | mock.call('connecting to SAP HANA database at %s:%s', 'host', 1234), 68 | mock.call('connected successfully') 69 | ]) 70 | self.assertEqual(self._conn._connection.timeout, 1) 71 | 72 | @mock.patch('shaptools.hdb_connector.connectors.pyhdb_connector.socket') 73 | @mock.patch('shaptools.hdb_connector.connectors.pyhdb_connector.pyhdb') 74 | @mock.patch('logging.Logger.info') 75 | def test_connect_socket_error(self, mock_logger, mock_pyhdb, mock_socket): 76 | mock_socket.error = Exception 77 | mock_pyhdb.exceptions.DatabaseError = Exception 78 | mock_pyhdb.connect.side_effect = mock_socket.error('socket error') 79 | with self.assertRaises(self._pyhdb_connector.base_connector.ConnectionError) as err: 80 | self._conn.connect('host', 1234, user='user', password='pass') 81 | 82 | self.assertTrue('connection failed: {}'.format('socket error') in str(err.exception)) 83 | mock_pyhdb.connect.assert_called_once_with( 84 | host='host', port=1234, user='user', password='pass') 85 | 86 | mock_logger.assert_called_once_with( 87 | 'connecting to SAP HANA database at %s:%s', 'host', 1234) 88 | 89 | @mock.patch('shaptools.hdb_connector.connectors.pyhdb_connector.socket') 90 | @mock.patch('shaptools.hdb_connector.connectors.pyhdb_connector.pyhdb') 91 | @mock.patch('logging.Logger.info') 92 | def test_connect_pyhdb_error(self, mock_logger, mock_pyhdb, mock_socket): 93 | mock_socket.error = Exception 94 | mock_pyhdb.exceptions.DatabaseError = Exception 95 | mock_pyhdb.connect.side_effect = mock_pyhdb.exceptions.DatabaseError('pyhdb error') 96 | with self.assertRaises(self._pyhdb_connector.base_connector.ConnectionError) as err: 97 | self._conn.connect('host', 1234, user='user', password='pass') 98 | 99 | self.assertTrue('connection failed: {}'.format('pyhdb error') in str(err.exception)) 100 | mock_pyhdb.connect.assert_called_once_with( 101 | host='host', port=1234, user='user', password='pass') 102 | 103 | mock_logger.assert_called_once_with( 104 | 'connecting to SAP HANA database at %s:%s', 'host', 1234) 105 | 106 | @mock.patch('shaptools.hdb_connector.connectors.base_connector.QueryResult') 107 | @mock.patch('logging.Logger.info') 108 | def test_query(self, mock_logger, mock_result): 109 | 110 | mock_cursor = mock.Mock() 111 | self._conn._connection = mock.Mock() 112 | self._conn._connection.cursor.return_value = mock_cursor 113 | 114 | mock_result_inst = mock.Mock() 115 | mock_result_inst.records = ['data1', 'data2'] 116 | mock_result_inst.metadata = 'metadata' 117 | mock_result.load_cursor.return_value = mock_result_inst 118 | 119 | result = self._conn.query('query') 120 | 121 | mock_cursor.execute.assert_called_once_with('query') 122 | mock_result.load_cursor.assert_called_once_with(mock_cursor) 123 | 124 | self.assertEqual(result.records, ['data1', 'data2']) 125 | self.assertEqual(result.metadata, 'metadata') 126 | mock_logger.assert_called_once_with('executing sql query: %s', 'query') 127 | mock_cursor.close.assert_called_once_with() 128 | 129 | @mock.patch('shaptools.hdb_connector.connectors.pyhdb_connector.pyhdb') 130 | @mock.patch('logging.Logger.info') 131 | def test_query_error(self, mock_logger, mock_pyhdb): 132 | mock_pyhdb.exceptions.DatabaseError = Exception 133 | self._conn._connection = mock.Mock() 134 | self._conn._connection.cursor.side_effect = Exception('error') 135 | with self.assertRaises(self._pyhdb_connector.base_connector.QueryError) as err: 136 | self._conn.query('query') 137 | 138 | self.assertTrue('query failed: {}'.format('error') in str(err.exception)) 139 | self._conn._connection.cursor.assert_called_once_with() 140 | mock_logger.assert_called_once_with('executing sql query: %s', 'query') 141 | 142 | @mock.patch('shaptools.hdb_connector.connectors.pyhdb_connector.pyhdb') 143 | @mock.patch('logging.Logger.info') 144 | def test_query_error_execute(self, mock_logger, mock_pyhdb): 145 | mock_pyhdb.exceptions.DatabaseError = Exception 146 | self._conn._connection = mock.Mock() 147 | cursor_mock = mock.Mock() 148 | self._conn._connection.cursor.return_value = cursor_mock 149 | cursor_mock.execute = mock.Mock() 150 | cursor_mock.execute.side_effect = Exception('error') 151 | with self.assertRaises(self._pyhdb_connector.base_connector.QueryError) as err: 152 | self._conn.query('query') 153 | 154 | self.assertTrue('query failed: {}'.format('error') in str(err.exception)) 155 | self._conn._connection.cursor.assert_called_once_with() 156 | mock_logger.assert_called_once_with('executing sql query: %s', 'query') 157 | cursor_mock.close.assert_called_once_with() 158 | 159 | @mock.patch('logging.Logger.info') 160 | def test_disconnect(self, mock_logger): 161 | self._conn._connection = mock.Mock() 162 | self._conn.disconnect() 163 | self._conn._connection.close.assert_called_once_with() 164 | mock_logger.assert_has_calls([ 165 | mock.call('disconnecting from SAP HANA database'), 166 | mock.call('disconnected successfully') 167 | ]) 168 | 169 | def test_isconnected_false(self): 170 | self._conn._connection = None 171 | self.assertFalse(self._conn.isconnected()) 172 | 173 | self._conn._connection = mock.Mock() 174 | self._conn._connection.isconnected.return_value = False 175 | self.assertFalse(self._conn.isconnected()) 176 | 177 | @mock.patch('logging.Logger.error') 178 | def test_isconnected_error(self, logger): 179 | self._conn._connection = mock.Mock() 180 | self._conn._connection.isconnected.return_value = True 181 | self._conn._connection._socket = mock.Mock() 182 | self._conn._connection._socket.getpeername.side_effect = OSError('error') 183 | 184 | self.assertFalse(self._conn.isconnected()) 185 | logger.assert_called_once_with('socket is not correctly working. closing socket') 186 | self.assertEqual(self._conn._connection._socket, None) 187 | 188 | def test_isconnected_correct(self): 189 | self._conn._connection = mock.Mock() 190 | self._conn._connection.isconnected.return_value = True 191 | self._conn._connection._socket = mock.Mock() 192 | self._conn._connection._socket.getpeername.return_value = 'data' 193 | 194 | self.assertTrue(self._conn.isconnected()) 195 | 196 | def test_reconnect_notconnected(self): 197 | self._conn._connection = None 198 | with self.assertRaises(self._pyhdb_connector.base_connector.ConnectionError) as err: 199 | self._conn.reconnect() 200 | self.assertTrue('connect method must be used first to reconnect' in str(err.exception)) 201 | 202 | @mock.patch('logging.Logger.info') 203 | def test_reconnect_connected(self, logger): 204 | self._conn._connection = mock.Mock() 205 | self._conn.isconnected = mock.Mock(return_value=True) 206 | self._conn.reconnect() 207 | logger.assert_called_once_with('connection already created') 208 | 209 | @mock.patch('logging.Logger.info') 210 | def test_reconnect(self, logger): 211 | self._conn._connection = mock.Mock() 212 | self._conn.isconnected = mock.Mock(return_value=False) 213 | self._conn.reconnect() 214 | 215 | self._conn._connection.connect.assert_called_once_with() 216 | logger.assert_called_once_with('reconnecting...') 217 | self.assertEqual(self._conn._connection.session_id, -1) 218 | self.assertEqual(self._conn._connection.packet_count, -1) 219 | 220 | @mock.patch('shaptools.hdb_connector.connectors.pyhdb_connector.socket') 221 | @mock.patch('shaptools.hdb_connector.connectors.pyhdb_connector.pyhdb') 222 | @mock.patch('logging.Logger.info') 223 | def test_reconnect_connect_error(self, logger, mock_pyhdb, mock_socket): 224 | mock_socket.error = Exception 225 | mock_pyhdb.exceptions.DatabaseError = Exception 226 | self._conn._connection = mock.Mock() 227 | self._conn.isconnected = mock.Mock(return_value=False) 228 | self._conn._connection.connect.side_effect = mock_socket.error('socket error') 229 | 230 | with self.assertRaises(self._pyhdb_connector.base_connector.ConnectionError) as err: 231 | self._conn.reconnect() 232 | self.assertTrue('socket error' in str(err.exception)) 233 | 234 | self._conn._connection.connect.assert_called_once_with() 235 | logger.assert_called_once_with('reconnecting...') 236 | self.assertEqual(self._conn._connection.session_id, -1) 237 | self.assertEqual(self._conn._connection.packet_count, -1) 238 | -------------------------------------------------------------------------------- /tests/requirements.2.7.yaml: -------------------------------------------------------------------------------- 1 | setuptools==44.1.1 2 | wheel==0.37.1 3 | pytest-cov==2.3.1 4 | jinja2==2.6 5 | pyyaml==3.12 6 | markupsafe==0.23 7 | requests==2.27 8 | certifi==2020.4.5.1 9 | pyzmq==18.0.1 10 | mock==3.0.0 11 | funcsigs==1.0.2 12 | -------------------------------------------------------------------------------- /tests/requirements.3.6.yaml: -------------------------------------------------------------------------------- 1 | pytest-cov==4.0.0 2 | -------------------------------------------------------------------------------- /tests/saputils_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unitary tests for saputils.py. 3 | 4 | :author: sisingh 5 | :organization: SUSE LLC 6 | :contact: sisingh@suse.com 7 | 8 | :since: 2020-03-26 9 | """ 10 | 11 | # pylint:disable=C0103,C0111,W0212,W0611 12 | 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 16 | 17 | import logging 18 | import unittest 19 | 20 | try: 21 | from unittest import mock 22 | except ImportError: 23 | import mock 24 | 25 | 26 | from shaptools import saputils 27 | 28 | class TestSapUtils(unittest.TestCase): 29 | """ 30 | Unitary tests for shaptools/saputis.py. 31 | """ 32 | 33 | @classmethod 34 | def setUpClass(cls): 35 | """ 36 | Global setUp. 37 | """ 38 | 39 | logging.basicConfig(level=logging.INFO) 40 | 41 | def setUp(self): 42 | """ 43 | Test setUp. 44 | """ 45 | 46 | def tearDown(self): 47 | """ 48 | Test tearDown. 49 | """ 50 | 51 | @classmethod 52 | def tearDownClass(cls): 53 | """ 54 | Global tearDown. 55 | """ 56 | 57 | @mock.patch('shaptools.shell.execute_cmd') 58 | @mock.patch('os.path.isfile') 59 | def test_extract_sapcar_file(self, mock_sapcar_file, mock_execute_cmd): 60 | proc_mock = mock.Mock() 61 | proc_mock.returncode = 0 62 | mock_sapcar_file.side_effect = [True, True] 63 | mock_execute_cmd.return_value = proc_mock 64 | 65 | result = saputils.extract_sapcar_file( 66 | sapcar_exe='/sapmedia/sapcar.exe', sar_file='/sapmedia/IMDB_SERVER_LINUX.SAR', 67 | output_dir='/sapmedia/HANA', options='-v') 68 | 69 | cmd = '/sapmedia/sapcar.exe -xvf /sapmedia/IMDB_SERVER_LINUX.SAR -v -R /sapmedia/HANA' 70 | mock_execute_cmd.assert_called_once_with(cmd, user=None, password=None, remote_host=None) 71 | self.assertEqual(proc_mock, result) 72 | 73 | @mock.patch('shaptools.shell.execute_cmd') 74 | @mock.patch('os.path.isfile') 75 | def test_extract_sapcar_error(self, mock_sapcar_file, mock_execute_cmd): 76 | mock_sapcar_file.side_effect = [True, True] 77 | proc_mock = mock.Mock() 78 | proc_mock.returncode = 1 79 | mock_execute_cmd.return_value = proc_mock 80 | 81 | with self.assertRaises(saputils.SapUtilsError) as err: 82 | saputils.extract_sapcar_file( 83 | sapcar_exe='/sapmedia/sapcar.exe', sar_file='/sapmedia/IMDB_SERVER_LINUX.SAR') 84 | 85 | cmd = '/sapmedia/sapcar.exe -xvf /sapmedia/IMDB_SERVER_LINUX.SAR' 86 | mock_execute_cmd.assert_called_once_with(cmd, user=None, password=None, remote_host=None) 87 | 88 | self.assertTrue( 89 | 'Error running SAPCAR command' in str(err.exception)) 90 | 91 | @mock.patch('os.path.isfile') 92 | def test_extract_sapcar_FileDoesNotExistError(self, mock_sapcar_file): 93 | mock_sapcar_file.return_value = False 94 | 95 | with self.assertRaises(saputils.FileDoesNotExistError) as err: 96 | saputils.extract_sapcar_file( 97 | sapcar_exe='/sapmedia/sapcar.exe', sar_file='/sapmedia/IMDB_SERVER_LINUX.SAR') 98 | 99 | self.assertTrue( 100 | 'SAPCAR executable \'{}\' does not exist'.format('/sapmedia/sapcar.exe') in str(err.exception)) 101 | 102 | @mock.patch('os.path.isfile') 103 | def test_extract_sar_FileDoesNotExistError(self, mock_sar_file): 104 | mock_sar_file.side_effect = [True, False] 105 | 106 | with self.assertRaises(saputils.FileDoesNotExistError) as err: 107 | saputils.extract_sapcar_file( 108 | sapcar_exe='/sapmedia/sapcar.exe', sar_file='/sapmedia/IMDB_SERVER_LINUX.SAR') 109 | 110 | self.assertTrue( 111 | 'The SAR file \'{}\' does not exist'.format('/sapmedia/IMDB_SERVER_LINUX.SAR') in str(err.exception)) -------------------------------------------------------------------------------- /tests/shell_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unitary tests for shell.py. 3 | 4 | :author: xarbulu 5 | :organization: SUSE LLC 6 | :contact: xarbulu@suse.com 7 | 8 | :since: 2018-11-16 9 | """ 10 | 11 | # pylint:disable=C0103,C0111,W0212,W0611 12 | 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 16 | 17 | import logging 18 | import unittest 19 | import subprocess 20 | 21 | try: 22 | from unittest import mock 23 | except ImportError: 24 | import mock 25 | 26 | from shaptools import shell 27 | 28 | class TestShell(unittest.TestCase): 29 | """ 30 | Unitary tests for shell.py. 31 | """ 32 | 33 | @classmethod 34 | def setUpClass(cls): 35 | """ 36 | Global setUp. 37 | """ 38 | 39 | logging.basicConfig(level=logging.INFO) 40 | 41 | def setUp(self): 42 | """ 43 | Test setUp. 44 | """ 45 | 46 | def tearDown(self): 47 | """ 48 | Test tearDown. 49 | """ 50 | 51 | @classmethod 52 | def tearDownClass(cls): 53 | """ 54 | Global tearDown. 55 | """ 56 | 57 | @mock.patch('logging.Logger.info') 58 | @mock.patch('logging.Logger.error') 59 | def test_log_results(self, logger_error, logger_info): 60 | out = ("Output text\n" 61 | "line1\n" 62 | "line2") 63 | err = ("Error text\n" 64 | "err1\n" 65 | "err2") 66 | 67 | shell.log_command_results(out, err) 68 | 69 | logger_info.assert_has_calls([ 70 | mock.call('Output text'), 71 | mock.call('line1'), 72 | mock.call('line2') 73 | ]) 74 | 75 | logger_error.assert_has_calls([ 76 | mock.call('Error text'), 77 | mock.call('err1'), 78 | mock.call('err2') 79 | ]) 80 | 81 | @mock.patch('logging.Logger.info') 82 | @mock.patch('logging.Logger.error') 83 | def test_show_output_empty(self, logger_error, logger_info): 84 | shell.log_command_results("", "") 85 | self.assertEqual(0, logger_info.call_count) 86 | self.assertEqual(0, logger_error.call_count) 87 | 88 | def test_find_pattern(self): 89 | out = ("Output text\n" 90 | " line1 \n" 91 | "line2") 92 | result = shell.find_pattern('.*line1.*', out) 93 | self.assertIsNotNone(result) 94 | 95 | def test_find_pattern_fail(self): 96 | out = ("Output text\n" 97 | " line1 \n" 98 | "line2") 99 | result = shell.find_pattern('.*line3.*', out) 100 | self.assertIsNone(result) 101 | 102 | def test_format_su_cmd(self): 103 | cmd = shell.format_su_cmd('ls -la', 'test') 104 | self.assertEqual('su -lc "ls -la" test', cmd) 105 | 106 | cmd = shell.format_su_cmd('hdbnsutil -sr_enable --name=PRAGUE', 'prdadm') 107 | self.assertEqual('su -lc "hdbnsutil -sr_enable --name=PRAGUE" prdadm', cmd) 108 | 109 | def test_format_remote_cmd(self): 110 | cmd = shell.format_remote_cmd('ls -la', 'remote', 'test') 111 | self.assertEqual('ssh test@remote "bash --login -c \'ls -la\'"', cmd) 112 | 113 | cmd = shell.format_remote_cmd('hdbnsutil -sr_enable --name=PRAGUE', 'remote', 'prdadm') 114 | self.assertEqual( 115 | 'ssh prdadm@remote "bash --login -c \'hdbnsutil -sr_enable --name=PRAGUE\'"', cmd) 116 | 117 | def test_format_remote_cmd_error(self): 118 | with self.assertRaises(ValueError) as err: 119 | shell.format_remote_cmd('ls -la', 'remote', None) 120 | self.assertTrue('user must be provided' in str(err.exception)) 121 | 122 | def test_execute_cmd_popen(self): 123 | # This test is used to check popen correct usage 124 | result = shell.execute_cmd('ls -la') 125 | self.assertEqual(result.returncode, 0) 126 | 127 | @mock.patch('shaptools.shell.ProcessResult') 128 | @mock.patch('subprocess.Popen') 129 | @mock.patch('logging.Logger.debug') 130 | def test_execute_cmd(self, logger, mock_popen, mock_process): 131 | 132 | mock_popen_inst = mock.Mock() 133 | mock_popen_inst.returncode = 5 134 | mock_popen_inst.communicate.return_value = (b'out', b'err') 135 | mock_popen.return_value = mock_popen_inst 136 | 137 | mock_process_inst = mock.Mock() 138 | mock_process.return_value = mock_process_inst 139 | 140 | result = shell.execute_cmd('ls -la') 141 | 142 | logger.assert_called_once_with( 143 | 'Executing command "%s" with user %s', 'ls -la', None) 144 | 145 | mock_popen.assert_called_once_with( 146 | ['ls', '-la'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, 147 | stderr=subprocess.PIPE) 148 | 149 | mock_popen_inst.communicate.assert_called_once_with(input=None) 150 | 151 | mock_process.assert_called_once_with('ls -la', 5, b'out', b'err') 152 | 153 | self.assertEqual(mock_process_inst, result) 154 | 155 | @mock.patch('shaptools.shell.format_remote_cmd') 156 | @mock.patch('shaptools.shell.ProcessResult') 157 | @mock.patch('subprocess.Popen') 158 | @mock.patch('logging.Logger.debug') 159 | def test_execute_cmd_remote( 160 | self, logger, mock_popen, mock_process, mock_format): 161 | 162 | mock_format.return_value = 'updated command' 163 | 164 | mock_popen_inst = mock.Mock() 165 | mock_popen_inst.returncode = 5 166 | mock_popen_inst.communicate.return_value = (b'out', b'err') 167 | mock_popen.return_value = mock_popen_inst 168 | 169 | mock_process_inst = mock.Mock() 170 | mock_process.return_value = mock_process_inst 171 | 172 | result = shell.execute_cmd('ls -la', 'test', 'pass', 'remote') 173 | 174 | logger.assert_has_calls([ 175 | mock.call('Executing command "%s" with user %s', 'ls -la', 'test'), 176 | mock.call('Command updated to "%s"', 'updated command') 177 | ]) 178 | 179 | mock_format.assert_called_once_with('ls -la', 'remote', 'test') 180 | 181 | mock_popen.assert_called_once_with( 182 | ['updated', 'command'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, 183 | stderr=subprocess.PIPE) 184 | 185 | mock_popen_inst.communicate.assert_called_once_with(input=b'pass') 186 | 187 | mock_process.assert_called_once_with('updated command', 5, b'out', b'err') 188 | 189 | self.assertEqual(mock_process_inst, result) 190 | 191 | @mock.patch('shaptools.shell.format_su_cmd') 192 | @mock.patch('shaptools.shell.ProcessResult') 193 | @mock.patch('subprocess.Popen') 194 | @mock.patch('logging.Logger.debug') 195 | def test_execute_cmd_user( 196 | self, logger, mock_popen, mock_process, mock_format): 197 | 198 | mock_format.return_value = 'updated command' 199 | 200 | mock_popen_inst = mock.Mock() 201 | mock_popen_inst.returncode = 5 202 | mock_popen_inst.communicate.return_value = (b'out', b'err') 203 | mock_popen.return_value = mock_popen_inst 204 | 205 | mock_process_inst = mock.Mock() 206 | mock_process.return_value = mock_process_inst 207 | 208 | result = shell.execute_cmd('ls -la', 'test', 'pass') 209 | 210 | logger.assert_has_calls([ 211 | mock.call('Executing command "%s" with user %s', 'ls -la', 'test'), 212 | mock.call('Command updated to "%s"', 'updated command') 213 | ]) 214 | 215 | mock_popen.assert_called_once_with( 216 | ['updated', 'command'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, 217 | stderr=subprocess.PIPE) 218 | 219 | mock_popen_inst.communicate.assert_called_once_with(input=b'pass') 220 | 221 | mock_process.assert_called_once_with('updated command', 5, b'out', b'err') 222 | 223 | self.assertEqual(mock_process_inst, result) 224 | 225 | @mock.patch('os.path') 226 | def test_create_ssh_askpass(self, mock_path): 227 | 228 | mock_path.dirname.return_value = 'file' 229 | mock_path.join.return_value = 'file/support/ssh_askpass' 230 | 231 | result = shell.create_ssh_askpass('pass', 'my_command') 232 | 233 | self.assertEqual( 234 | 'export SSH_ASKPASS=file/support/ssh_askpass;export PASS=pass;export DISPLAY=:0;setsid my_command', 235 | result) 236 | 237 | @mock.patch('shaptools.shell.execute_cmd') 238 | def test_remove_user(self, mock_execute_cmd): 239 | 240 | result = mock.Mock(returncode=0) 241 | mock_execute_cmd.return_value = result 242 | 243 | shell.remove_user('user', False, 'root', 'pass', 'remote_host') 244 | 245 | mock_execute_cmd.assert_called_once_with('userdel user', 'root', 'pass', 'remote_host') 246 | 247 | @mock.patch('shaptools.shell.execute_cmd') 248 | def test_remove_user_force(self, mock_execute_cmd): 249 | 250 | result1 = mock.Mock(returncode=1, err='userdel: user user is currently used by process 1') 251 | result2 = mock.Mock(returncode=1, err='userdel: user user is currently used by process 2') 252 | result3 = mock.Mock(returncode=0) 253 | mock_execute_cmd.side_effect = [result1, None, result2, None, result3] 254 | 255 | shell.remove_user('user', True, 'root', 'pass') 256 | 257 | mock_execute_cmd.assert_has_calls([ 258 | mock.call('userdel user', 'root', 'pass', None), 259 | mock.call('kill -9 1', 'root', 'pass', None), 260 | mock.call('userdel user', 'root', 'pass', None), 261 | mock.call('kill -9 2', 'root', 'pass', None), 262 | mock.call('userdel user', 'root', 'pass', None), 263 | ]) 264 | 265 | @mock.patch('shaptools.shell.execute_cmd') 266 | def test_remove_user_error(self, mock_execute_cmd): 267 | 268 | result = mock.Mock(returncode=1) 269 | mock_execute_cmd.return_value = result 270 | 271 | with self.assertRaises(shell.ShellError) as err: 272 | shell.remove_user('user', False, 'root', 'pass') 273 | 274 | mock_execute_cmd.assert_called_once_with('userdel user', 'root', 'pass', None) 275 | self.assertTrue('error removing user user' in str(err.exception)) 276 | 277 | @mock.patch('shaptools.shell.execute_cmd') 278 | def test_remove_user_force_error(self, mock_execute_cmd): 279 | 280 | result1 = mock.Mock(returncode=1, err='userdel: user user is currently used by process 1') 281 | result2 = mock.Mock(returncode=1, err='other error') 282 | result3 = mock.Mock(returncode=0) 283 | mock_execute_cmd.side_effect = [result1, None, result2, None, result3] 284 | 285 | with self.assertRaises(shell.ShellError) as err: 286 | shell.remove_user('user', True, 'root', 'pass') 287 | 288 | mock_execute_cmd.assert_has_calls([ 289 | mock.call('userdel user', 'root', 'pass', None), 290 | mock.call('kill -9 1', 'root', 'pass', None), 291 | mock.call('userdel user', 'root', 'pass', None) 292 | ]) 293 | self.assertTrue('error removing user user' in str(err.exception)) 294 | -------------------------------------------------------------------------------- /tests/support/modified.conf: -------------------------------------------------------------------------------- 1 | 2 | [General] 3 | 4 | # Location of SAP HANA Database Installation Medium 5 | component_medium= 6 | 7 | # Comma separated list of component directories 8 | component_dirs= 9 | 10 | # Directory root to search for components 11 | component_root= 12 | 13 | # Use single master password for all users, created during installation ( Default: n ) 14 | use_master_password=n 15 | 16 | # Skip all SAP Host Agent calls ( Default: n ) 17 | skip_hostagent_calls=n 18 | 19 | # Remote Execution ( Default: ssh; Valid values: ssh | saphostagent ) 20 | remote_execution=ssh 21 | 22 | # Verify the authenticity of SAP HANA components ( Default: n ) 23 | verify_signature=n 24 | 25 | # Components ( Valid values: all | client | es | ets | lcapps | server | smartda | streaming | rdsync | xs | studio | afl | sca | sop | eml | rme | rtl | trp | vch ) 26 | components= 27 | 28 | # Ignore failing prerequisite checks 29 | ignore= 30 | 31 | # Do not Modify '/etc/sudoers' File ( Default: n ) 32 | skip_modify_sudoers=n 33 | 34 | [Server] 35 | 36 | # Enable usage of persistent memory ( Default: n ) 37 | use_pmem=n 38 | 39 | # Enable the installation or upgrade of the SAP Host Agent ( Default: y ) 40 | install_hostagent=y 41 | 42 | # Database Isolation ( Default: low; Valid values: low | high ) 43 | db_isolation=low 44 | 45 | # Create initial tenant database ( Default: y ) 46 | create_initial_tenant=y 47 | 48 | # Non-standard Shared File System 49 | checkmnt= 50 | 51 | # Installation Path ( Default: /hana/shared ) 52 | sapmnt=/hana/shared 53 | 54 | # Local Host Name ( Default: hana01 ) 55 | hostname=hana01 56 | 57 | # Install SSH Key ( Default: y ) 58 | install_ssh_key=y 59 | 60 | # Root User Name ( Default: root ) 61 | root_user=root 62 | 63 | # Root User Password 64 | root_password= 65 | 66 | # SAP Host Agent User (sapadm) Password 67 | sapadm_password= 68 | 69 | # Directory containing a storage configuration 70 | storage_cfg= 71 | 72 | # Internal Network Address 73 | internal_network= 74 | 75 | # SAP HANA System ID 76 | sid=PRD 77 | 78 | # Instance Number 79 | number= 80 | 81 | # Local Host Worker Group ( Default: default ) 82 | workergroup=default 83 | 84 | # System Usage ( Default: custom; Valid values: production | test | development | custom ) 85 | system_usage=custom 86 | 87 | # Location of Data Volumes ( Default: /hana/data/${sid} ) 88 | datapath=/hana/data/${sid} 89 | 90 | # Location of Log Volumes ( Default: /hana/log/${sid} ) 91 | logpath=/hana/log/${sid} 92 | 93 | # Location of Persistent Memory Volumes ( Default: /hana/pmem/${sid} ) 94 | pmempath=/hana/pmem/${sid} 95 | 96 | # Directory containing custom configurations 97 | custom_cfg= 98 | 99 | # Restrict maximum memory allocation? 100 | restrict_max_mem= 101 | 102 | # Maximum Memory Allocation in MB 103 | max_mem= 104 | 105 | # Certificate Host Names 106 | certificates_hostmap= 107 | 108 | # Master Password 109 | master_password= 110 | 111 | # System Administrator Password 112 | password=Qwerty1234 113 | 114 | # System Administrator Home Directory ( Default: /usr/sap/${sid}/home ) 115 | home=/usr/sap/${sid}/home 116 | 117 | # System Administrator Login Shell ( Default: /bin/sh ) 118 | shell=/bin/sh 119 | 120 | # System Administrator User ID 121 | userid= 122 | 123 | # ID of User Group (sapsys) 124 | groupid= 125 | 126 | # Database User (SYSTEM) Password 127 | system_user_password=Qwerty1234 128 | 129 | # Restart system after machine reboot? ( Default: n ) 130 | autostart=n 131 | 132 | # Enable HANA repository ( Default: y ) 133 | repository=y 134 | 135 | # Inter Service Communication Mode ( Valid values: standard | ssl ) 136 | isc_mode= 137 | 138 | [Action] 139 | 140 | # Action ( Default: exit; Valid values: install | update | extract_components ) 141 | action=install 142 | 143 | [AddHosts] 144 | 145 | # Auto Initialize Services ( Default: y ) 146 | auto_initialize_services=y 147 | 148 | # Additional Hosts 149 | addhosts= 150 | 151 | # Additional Local Host Roles ( Valid values: extended_storage_worker | extended_storage_standby | ets_worker | ets_standby | streaming | rdsync | xs_worker | xs_standby ) 152 | add_local_roles= 153 | 154 | # Automatically assign XS Advanced Runtime roles to the hosts with database roles (y/n) ( Default: y ) 155 | autoadd_xs_roles=y 156 | 157 | # Import initial content of XS Advanced Runtime ( Default: y ) 158 | import_xs_content=y 159 | 160 | [Client] 161 | 162 | # SAP HANA Database Client Installation Path ( Default: ${sapmnt}/${sid}/hdbclient ) 163 | client_path=/hana/shared/${sid}/hdbclient 164 | 165 | [Studio] 166 | 167 | # SAP HANA Studio Installation Path ( Default: ${sapmnt}/${sid}/hdbstudio ) 168 | studio_path=/hana/shared/${sid}/hdbstudio 169 | 170 | # Enables copying of SAP HANA Studio repository ( Default: y ) 171 | studio_repository=y 172 | 173 | # Target path to which SAP HANA Studio repository should be copied 174 | copy_repository= 175 | 176 | # Java Runtime ( Default: ) 177 | vm= 178 | 179 | [Reference_Data] 180 | 181 | # Installation Path for Address Directories and Reference Data 182 | reference_data_path= 183 | 184 | [streaming] 185 | 186 | # Streaming Cluster Manager Password 187 | streaming_cluster_manager_password= 188 | 189 | # Location of Streaming logstores and runtime information ( Default: /hana/data_streaming/${sid} ) 190 | basepath_streaming=/hana/data_streaming/${sid} 191 | 192 | [es] 193 | 194 | # Location of Dynamic Tiering Data Volumes ( Default: /hana/data_es/${sid} ) 195 | es_datapath=/hana/data_es/${sid} 196 | 197 | # Location of Dynamic Tiering Log Volumes ( Default: /hana/log_es/${sid} ) 198 | es_logpath=/hana/log_es/${sid} 199 | 200 | [ets] 201 | 202 | # Location of Data Volumes of the Accelerator for SAP ASE ( Default: /hana/data_ase/${sid} ) 203 | ase_datapath=/hana/data_ase/${sid} 204 | 205 | # Location of Log Volumes of the Accelerator for SAP ASE ( Default: /hana/log_ase/${sid} ) 206 | ase_logpath=/hana/log_ase/${sid} 207 | 208 | # SAP ASE Administrator User ( Default: sa ) 209 | ase_user=sa 210 | 211 | # SAP ASE Administrator Password 212 | ase_user_password= 213 | 214 | [rdsync] 215 | 216 | # Location of SAP HANA Remote Data Sync file download directory ( Default: /hana/download_rdsync/${sid} ) 217 | rdsync_downloadpath=/hana/download_rdsync/${sid} 218 | 219 | # Location of SAP HANA Remote Data Sync file upload directory ( Default: /hana/upload_rdsync/${sid} ) 220 | rdsync_uploadpath=/hana/upload_rdsync/${sid} 221 | 222 | [XS_Advanced] 223 | 224 | # XS Advanced App Working Path 225 | xs_app_working_path= 226 | 227 | # Organization Name For Space "SAP" ( Default: orgname ) 228 | org_name=orgname 229 | 230 | # XS Advanced Admin User ( Default: XSA_ADMIN ) 231 | org_manager_user=XSA_ADMIN 232 | 233 | # XS Advanced Admin User Password 234 | org_manager_password= 235 | 236 | # Customer Space Name ( Default: PROD ) 237 | prod_space_name=PROD 238 | 239 | # Routing Mode ( Default: ports; Valid values: ports | hostnames ) 240 | xs_routing_mode=ports 241 | 242 | # XS Advanced Domain Name (see SAP Note 2245631) 243 | xs_domain_name= 244 | 245 | # Run Applications in SAP Space with Separate OS User (y/n) ( Default: y ) 246 | xs_sap_space_isolation=y 247 | 248 | # Run Applications in Customer Space with Separate OS User (y/n) ( Default: y ) 249 | xs_customer_space_isolation=y 250 | 251 | # XS Advanced SAP Space OS User ID 252 | xs_sap_space_user_id= 253 | 254 | # XS Advanced Customer Space OS User ID 255 | xs_customer_space_user_id= 256 | 257 | # XS Advanced Components 258 | xs_components= 259 | 260 | # Do not start the selected XS Advanced components after installation ( Default: none ) 261 | xs_components_nostart=none 262 | 263 | # XS Advanced Components Configurations 264 | xs_components_cfg= 265 | 266 | # XS Advanced Certificate 267 | xs_cert_pem= 268 | 269 | # XS Advanced Certificate Key 270 | xs_cert_key= 271 | 272 | # XS Advanced Trust Certificate 273 | xs_trust_pem= 274 | -------------------------------------------------------------------------------- /tests/support/modified.conf.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/support/modified.inifile.params: -------------------------------------------------------------------------------- 1 | ################################################################################################################################################################################################ 2 | # # 3 | # Installation service 'SAP NetWeaver 7.5 > SAP HANA Database > Installation > Application Server ABAP > High-Availability System > ASCS Instance', product id 'NW_ABAP_ASCS:NW750.HDB.ABAPHA' # 4 | # # 5 | ################################################################################################################################################################################################ 6 | # Password for the Diagnostics Agent specific adm user. Provided value may be encoded. 7 | # DiagnosticsAgent.dasidAdmPassword = 8 | 9 | 10 | 11 | 12 | # Windows domain in which the Diagnostics Agent users must be created. This is an optional property (Windows only). 13 | # DiagnosticsAgent.domain = 14 | 15 | 16 | 17 | 18 | # Windows only: Password for the Diagnostics Agent specific 'SAPService' user. 19 | # DiagnosticsAgent.sapServiceDASIDPassword = 20 | 21 | 22 | 23 | 24 | # Specify whether the all operating system users are to be removed from group 'sapinst' after the execution of Software Provisioning Manager has completed. 25 | NW_Delete_Sapinst_Users.removeUsers = true 26 | 27 | 28 | 29 | 30 | # Master password 31 | NW_GetMasterPassword.masterPwd = Suse1234 32 | 33 | 34 | 35 | 36 | # Human readable form of the default login language to be preselected in SAPGUI. This Parameter is potentially prompted in addition in the screen that also asks for the . It is only prompted in systems that have an ABAP stack. It is prompted for installation but not for system copy. It is asked in those installations, that perform the ABAP load. That could be the database load installation in case of a distributed system scenario, or in case of a standard system installation with all instances on one host. This Parameter is saved in the 'DEFAULT' profile. It is has no influence on language settings in a Java stack. Valid names are stored in a table of subcomponent 'NW_languagesInLoadChecks'. The available languages must be declaired in the 'LANGUAGES_IN_LOAD' parameter of the 'product.xml' file . In this file, the one-character representation of the languages is used. Check the same table in subcomponent 'NW_languagesInLoadChecks'. 37 | # NW_GetSidNoProfiles.SAP_GUI_DEFAULT_LANGUAGE = 38 | 39 | 40 | 41 | 42 | # Windows only: The drive to use 43 | # NW_GetSidNoProfiles.sapdrive = 44 | 45 | 46 | 47 | 48 | # Unix only: The SAP mount directory path. Default value is '/sapmnt'. 49 | # NW_GetSidNoProfiles.sapmnt = /sapmnt 50 | 51 | 52 | 53 | 54 | # The SAP system ID of the system to be installed 55 | NW_GetSidNoProfiles.sid = HA1 56 | 57 | 58 | 59 | 60 | # Only use this parameter if recommended by SAP. 61 | # NW_GetSidNoProfiles.strictSidCheck = true 62 | 63 | 64 | 65 | 66 | # Specify whether this system is to be a Unicode system. 67 | # NW_GetSidNoProfiles.unicode = true 68 | 69 | 70 | 71 | 72 | # Add gateway process in the (A)SCS instance 73 | # NW_SCS_Instance.ascsInstallGateway = false 74 | 75 | 76 | 77 | 78 | # Add Web Dispatcher process in the (A)SCS instance 79 | # NW_SCS_Instance.ascsInstallWebDispatcher = false 80 | 81 | 82 | 83 | 84 | # Dual stack only: Specify the ASCS instance number. Leave empty for default. 85 | NW_SCS_Instance.ascsInstanceNumber = 86 | 87 | 88 | 89 | 90 | # Specify whether a 'prxyinfo(.DAT)' file is to be created in the 'global' directory if this file does not yet exist and that the 'gw/prxy_info' in the 'DEFAULT' profile is to be set accordingly. Default is false. 91 | # NW_SCS_Instance.createGlobalProxyInfoFile = false 92 | 93 | 94 | 95 | 96 | # Specify whether a 'reginfo(.DAT)' file is to be created in the 'global' directory. Default is 'false'. 97 | # NW_SCS_Instance.createGlobalRegInfoFile = false 98 | 99 | 100 | 101 | 102 | # The instance number of the (A)SCS instance. Leave this value empty if you want to use the default instance number or if you want to specify two different numbers for ASCS and SCS instance. 103 | NW_SCS_Instance.instanceNumber = 01 104 | 105 | 106 | 107 | 108 | # Dual stack only: The SCS instance number. Leave empty for default. 109 | # NW_SCS_Instance.scsInstanceNumber = 110 | 111 | 112 | 113 | 114 | # The (A)SCS message server port. Leave empty for default. 115 | # NW_SCS_Instance.scsMSPort = 116 | 117 | 118 | 119 | 120 | # You can specify a virtual host name for the ASCS instance. Leave empty for default. 121 | NW_SCS_Instance.scsVirtualHostname = sapha1as 122 | 123 | 124 | 125 | 126 | # SAP INTERNAL USE ONLY 127 | # NW_System.installSAPHostAgent = true 128 | 129 | 130 | 131 | 132 | # SAP INTERNAL USE ONLY 133 | # NW_adaptProfile.templateFiles = 134 | 135 | 136 | 137 | 138 | # The FQDN of the system 139 | # NW_getFQDN.FQDN = 140 | 141 | 142 | 143 | 144 | # Specify whether you want to set FQDN for the system. 145 | NW_getFQDN.setFQDN = false 146 | 147 | 148 | 149 | 150 | # The ASP device name where the SAP system will be in installed. The property is IBM i only. 151 | # Values from 1 to 256 can be specified. The default is 1, the System ASP. 152 | # OS4.DestinationASP = 153 | 154 | 155 | 156 | 157 | # The folder containing all archives that have been downloaded from http://support.sap.com/swdc and are supposed to be used in this procedure 158 | archives.downloadBasket = /root/sim_mount/NW/kernel_nw75_sar 159 | 160 | 161 | 162 | 163 | # Windows only: The domain of the SAP Host Agent user 164 | # hostAgent.domain = 165 | 166 | 167 | 168 | 169 | # Password for the 'sapadm' user of the SAP Host Agent 170 | # hostAgent.sapAdmPassword = 171 | 172 | 173 | 174 | 175 | # Windows only: The domain of all users of this SAP system. Leave empty for default. 176 | # nwUsers.sapDomain = 177 | 178 | 179 | 180 | 181 | # Windows only: The password of the 'SAPServiceSID' user 182 | # nwUsers.sapServiceSIDPassword = 183 | 184 | 185 | 186 | 187 | # UNIX only: The user ID of the 'sapadm' user, leave empty for default. The ID is ignored if the user already exists. 188 | nwUsers.sapadmUID = 189 | 190 | 191 | 192 | 193 | # UNIX only: The group id of the 'sapsys' group, leave empty for default. The ID is ignored if the group already exists. 194 | nwUsers.sapsysGID = 195 | 196 | 197 | 198 | 199 | # UNIX only: The user id of the adm user, leave empty for default. The ID is ignored if the user already exists. 200 | nwUsers.sidAdmUID = 201 | 202 | 203 | 204 | 205 | # The password of the 'adm' user 206 | # nwUsers.sidadmPassword = testpwd -------------------------------------------------------------------------------- /tests/support/new.inifile.params: -------------------------------------------------------------------------------- 1 | ################################################################################################################################################################################################ 2 | # # 3 | # Installation service 'SAP NetWeaver 7.5 > SAP HANA Database > Installation > Application Server ABAP > High-Availability System > ASCS Instance', product id 'NW_ABAP_ASCS:NW750.HDB.ABAPHA' # 4 | # # 5 | ################################################################################################################################################################################################ 6 | # Password for the Diagnostics Agent specific adm user. Provided value may be encoded. 7 | # DiagnosticsAgent.dasidAdmPassword = 8 | 9 | 10 | 11 | 12 | # Windows domain in which the Diagnostics Agent users must be created. This is an optional property (Windows only). 13 | # DiagnosticsAgent.domain = 14 | 15 | 16 | 17 | 18 | # Windows only: Password for the Diagnostics Agent specific 'SAPService' user. 19 | # DiagnosticsAgent.sapServiceDASIDPassword = 20 | 21 | 22 | 23 | 24 | # Specify whether the all operating system users are to be removed from group 'sapinst' after the execution of Software Provisioning Manager has completed. 25 | NW_Delete_Sapinst_Users.removeUsers = true 26 | 27 | 28 | 29 | 30 | # Master password 31 | NW_GetMasterPassword.masterPwd = 32 | 33 | 34 | 35 | 36 | # Human readable form of the default login language to be preselected in SAPGUI. This Parameter is potentially prompted in addition in the screen that also asks for the . It is only prompted in systems that have an ABAP stack. It is prompted for installation but not for system copy. It is asked in those installations, that perform the ABAP load. That could be the database load installation in case of a distributed system scenario, or in case of a standard system installation with all instances on one host. This Parameter is saved in the 'DEFAULT' profile. It is has no influence on language settings in a Java stack. Valid names are stored in a table of subcomponent 'NW_languagesInLoadChecks'. The available languages must be declaired in the 'LANGUAGES_IN_LOAD' parameter of the 'product.xml' file . In this file, the one-character representation of the languages is used. Check the same table in subcomponent 'NW_languagesInLoadChecks'. 37 | # NW_GetSidNoProfiles.SAP_GUI_DEFAULT_LANGUAGE = 38 | 39 | 40 | 41 | 42 | # Windows only: The drive to use 43 | # NW_GetSidNoProfiles.sapdrive = 44 | 45 | 46 | 47 | 48 | # Unix only: The SAP mount directory path. Default value is '/sapmnt'. 49 | # NW_GetSidNoProfiles.sapmnt = /sapmnt 50 | 51 | 52 | 53 | 54 | # The SAP system ID of the system to be installed 55 | NW_GetSidNoProfiles.sid = HA2 56 | 57 | 58 | 59 | 60 | # Only use this parameter if recommended by SAP. 61 | # NW_GetSidNoProfiles.strictSidCheck = true 62 | 63 | 64 | 65 | 66 | # Specify whether this system is to be a Unicode system. 67 | # NW_GetSidNoProfiles.unicode = true 68 | 69 | 70 | 71 | 72 | # Add gateway process in the (A)SCS instance 73 | # NW_SCS_Instance.ascsInstallGateway = false 74 | 75 | 76 | 77 | 78 | # Add Web Dispatcher process in the (A)SCS instance 79 | # NW_SCS_Instance.ascsInstallWebDispatcher = false 80 | 81 | 82 | 83 | 84 | # Dual stack only: Specify the ASCS instance number. Leave empty for default. 85 | NW_SCS_Instance.ascsInstanceNumber = 86 | 87 | 88 | 89 | 90 | # Specify whether a 'prxyinfo(.DAT)' file is to be created in the 'global' directory if this file does not yet exist and that the 'gw/prxy_info' in the 'DEFAULT' profile is to be set accordingly. Default is false. 91 | # NW_SCS_Instance.createGlobalProxyInfoFile = false 92 | 93 | 94 | 95 | 96 | # Specify whether a 'reginfo(.DAT)' file is to be created in the 'global' directory. Default is 'false'. 97 | # NW_SCS_Instance.createGlobalRegInfoFile = false 98 | 99 | 100 | 101 | 102 | # The instance number of the (A)SCS instance. Leave this value empty if you want to use the default instance number or if you want to specify two different numbers for ASCS and SCS instance. 103 | NW_SCS_Instance.instanceNumber = 01 104 | 105 | 106 | 107 | 108 | # Dual stack only: The SCS instance number. Leave empty for default. 109 | # NW_SCS_Instance.scsInstanceNumber = 110 | 111 | 112 | 113 | 114 | # The (A)SCS message server port. Leave empty for default. 115 | # NW_SCS_Instance.scsMSPort = 116 | 117 | 118 | 119 | 120 | # You can specify a virtual host name for the ASCS instance. Leave empty for default. 121 | NW_SCS_Instance.scsVirtualHostname = sapha1as 122 | 123 | 124 | 125 | 126 | # SAP INTERNAL USE ONLY 127 | # NW_System.installSAPHostAgent = true 128 | 129 | 130 | 131 | 132 | # SAP INTERNAL USE ONLY 133 | # NW_adaptProfile.templateFiles = 134 | 135 | 136 | 137 | 138 | # The FQDN of the system 139 | # NW_getFQDN.FQDN = 140 | 141 | 142 | 143 | 144 | # Specify whether you want to set FQDN for the system. 145 | NW_getFQDN.setFQDN = false 146 | 147 | 148 | 149 | 150 | # The ASP device name where the SAP system will be in installed. The property is IBM i only. 151 | # Values from 1 to 256 can be specified. The default is 1, the System ASP. 152 | # OS4.DestinationASP = 153 | 154 | 155 | 156 | 157 | # The folder containing all archives that have been downloaded from http://support.sap.com/swdc and are supposed to be used in this procedure 158 | archives.downloadBasket = /root/sim_mount/NW/kernel_nw75_sar 159 | 160 | 161 | 162 | 163 | # Windows only: The domain of the SAP Host Agent user 164 | # hostAgent.domain = 165 | 166 | 167 | 168 | 169 | # Password for the 'sapadm' user of the SAP Host Agent 170 | # hostAgent.sapAdmPassword = 171 | 172 | 173 | 174 | 175 | # Windows only: The domain of all users of this SAP system. Leave empty for default. 176 | # nwUsers.sapDomain = 177 | 178 | 179 | 180 | 181 | # Windows only: The password of the 'SAPServiceSID' user 182 | # nwUsers.sapServiceSIDPassword = 183 | 184 | 185 | 186 | 187 | # UNIX only: The user ID of the 'sapadm' user, leave empty for default. The ID is ignored if the user already exists. 188 | nwUsers.sapadmUID = 189 | 190 | 191 | 192 | 193 | # UNIX only: The group id of the 'sapsys' group, leave empty for default. The ID is ignored if the group already exists. 194 | nwUsers.sapsysGID = 195 | 196 | 197 | 198 | 199 | # UNIX only: The user id of the adm user, leave empty for default. The ID is ignored if the user already exists. 200 | nwUsers.sidAdmUID = 201 | 202 | 203 | 204 | 205 | # The password of the 'adm' user 206 | # nwUsers.sidadmPassword = 207 | NW_HDB_getDBInfo.systemPassword = test1234 -------------------------------------------------------------------------------- /tests/support/original.conf: -------------------------------------------------------------------------------- 1 | 2 | [General] 3 | 4 | # Location of SAP HANA Database Installation Medium 5 | component_medium= 6 | 7 | # Comma separated list of component directories 8 | component_dirs= 9 | 10 | # Directory root to search for components 11 | component_root= 12 | 13 | # Use single master password for all users, created during installation ( Default: n ) 14 | use_master_password=n 15 | 16 | # Skip all SAP Host Agent calls ( Default: n ) 17 | skip_hostagent_calls=n 18 | 19 | # Remote Execution ( Default: ssh; Valid values: ssh | saphostagent ) 20 | remote_execution=ssh 21 | 22 | # Verify the authenticity of SAP HANA components ( Default: n ) 23 | verify_signature=n 24 | 25 | # Components ( Valid values: all | client | es | ets | lcapps | server | smartda | streaming | rdsync | xs | studio | afl | sca | sop | eml | rme | rtl | trp | vch ) 26 | components= 27 | 28 | # Ignore failing prerequisite checks 29 | ignore= 30 | 31 | # Do not Modify '/etc/sudoers' File ( Default: n ) 32 | skip_modify_sudoers=n 33 | 34 | [Server] 35 | 36 | # Enable usage of persistent memory ( Default: n ) 37 | use_pmem=n 38 | 39 | # Enable the installation or upgrade of the SAP Host Agent ( Default: y ) 40 | install_hostagent=y 41 | 42 | # Database Isolation ( Default: low; Valid values: low | high ) 43 | db_isolation=low 44 | 45 | # Create initial tenant database ( Default: y ) 46 | create_initial_tenant=y 47 | 48 | # Non-standard Shared File System 49 | checkmnt= 50 | 51 | # Installation Path ( Default: /hana/shared ) 52 | sapmnt=/hana/shared 53 | 54 | # Local Host Name ( Default: hana01 ) 55 | hostname=hana01 56 | 57 | # Install SSH Key ( Default: y ) 58 | install_ssh_key=y 59 | 60 | # Root User Name ( Default: root ) 61 | root_user=root 62 | 63 | # Root User Password 64 | root_password= 65 | 66 | # SAP Host Agent User (sapadm) Password 67 | sapadm_password= 68 | 69 | # Directory containing a storage configuration 70 | storage_cfg= 71 | 72 | # Internal Network Address 73 | internal_network= 74 | 75 | # SAP HANA System ID 76 | sid= 77 | 78 | # Instance Number 79 | number= 80 | 81 | # Local Host Worker Group ( Default: default ) 82 | workergroup=default 83 | 84 | # System Usage ( Default: custom; Valid values: production | test | development | custom ) 85 | system_usage=custom 86 | 87 | # Location of Data Volumes ( Default: /hana/data/${sid} ) 88 | datapath=/hana/data/${sid} 89 | 90 | # Location of Log Volumes ( Default: /hana/log/${sid} ) 91 | logpath=/hana/log/${sid} 92 | 93 | # Location of Persistent Memory Volumes ( Default: /hana/pmem/${sid} ) 94 | pmempath=/hana/pmem/${sid} 95 | 96 | # Directory containing custom configurations 97 | custom_cfg= 98 | 99 | # Restrict maximum memory allocation? 100 | restrict_max_mem= 101 | 102 | # Maximum Memory Allocation in MB 103 | max_mem= 104 | 105 | # Certificate Host Names 106 | certificates_hostmap= 107 | 108 | # Master Password 109 | master_password= 110 | 111 | # System Administrator Password 112 | password= 113 | 114 | # System Administrator Home Directory ( Default: /usr/sap/${sid}/home ) 115 | home=/usr/sap/${sid}/home 116 | 117 | # System Administrator Login Shell ( Default: /bin/sh ) 118 | shell=/bin/sh 119 | 120 | # System Administrator User ID 121 | userid= 122 | 123 | # ID of User Group (sapsys) 124 | groupid= 125 | 126 | # Database User (SYSTEM) Password 127 | system_user_password= 128 | 129 | # Restart system after machine reboot? ( Default: n ) 130 | autostart=n 131 | 132 | # Enable HANA repository ( Default: y ) 133 | repository=y 134 | 135 | # Inter Service Communication Mode ( Valid values: standard | ssl ) 136 | isc_mode= 137 | 138 | [Action] 139 | 140 | # Action ( Default: exit; Valid values: install | update | extract_components ) 141 | action=install 142 | 143 | [AddHosts] 144 | 145 | # Auto Initialize Services ( Default: y ) 146 | auto_initialize_services=y 147 | 148 | # Additional Hosts 149 | addhosts= 150 | 151 | # Additional Local Host Roles ( Valid values: extended_storage_worker | extended_storage_standby | ets_worker | ets_standby | streaming | rdsync | xs_worker | xs_standby ) 152 | add_local_roles= 153 | 154 | # Automatically assign XS Advanced Runtime roles to the hosts with database roles (y/n) ( Default: y ) 155 | autoadd_xs_roles=y 156 | 157 | # Import initial content of XS Advanced Runtime ( Default: y ) 158 | import_xs_content=y 159 | 160 | [Client] 161 | 162 | # SAP HANA Database Client Installation Path ( Default: ${sapmnt}/${sid}/hdbclient ) 163 | client_path=/hana/shared/${sid}/hdbclient 164 | 165 | [Studio] 166 | 167 | # SAP HANA Studio Installation Path ( Default: ${sapmnt}/${sid}/hdbstudio ) 168 | studio_path=/hana/shared/${sid}/hdbstudio 169 | 170 | # Enables copying of SAP HANA Studio repository ( Default: y ) 171 | studio_repository=y 172 | 173 | # Target path to which SAP HANA Studio repository should be copied 174 | copy_repository= 175 | 176 | # Java Runtime ( Default: ) 177 | vm= 178 | 179 | [Reference_Data] 180 | 181 | # Installation Path for Address Directories and Reference Data 182 | reference_data_path= 183 | 184 | [streaming] 185 | 186 | # Streaming Cluster Manager Password 187 | streaming_cluster_manager_password= 188 | 189 | # Location of Streaming logstores and runtime information ( Default: /hana/data_streaming/${sid} ) 190 | basepath_streaming=/hana/data_streaming/${sid} 191 | 192 | [es] 193 | 194 | # Location of Dynamic Tiering Data Volumes ( Default: /hana/data_es/${sid} ) 195 | es_datapath=/hana/data_es/${sid} 196 | 197 | # Location of Dynamic Tiering Log Volumes ( Default: /hana/log_es/${sid} ) 198 | es_logpath=/hana/log_es/${sid} 199 | 200 | [ets] 201 | 202 | # Location of Data Volumes of the Accelerator for SAP ASE ( Default: /hana/data_ase/${sid} ) 203 | ase_datapath=/hana/data_ase/${sid} 204 | 205 | # Location of Log Volumes of the Accelerator for SAP ASE ( Default: /hana/log_ase/${sid} ) 206 | ase_logpath=/hana/log_ase/${sid} 207 | 208 | # SAP ASE Administrator User ( Default: sa ) 209 | ase_user=sa 210 | 211 | # SAP ASE Administrator Password 212 | ase_user_password= 213 | 214 | [rdsync] 215 | 216 | # Location of SAP HANA Remote Data Sync file download directory ( Default: /hana/download_rdsync/${sid} ) 217 | rdsync_downloadpath=/hana/download_rdsync/${sid} 218 | 219 | # Location of SAP HANA Remote Data Sync file upload directory ( Default: /hana/upload_rdsync/${sid} ) 220 | rdsync_uploadpath=/hana/upload_rdsync/${sid} 221 | 222 | [XS_Advanced] 223 | 224 | # XS Advanced App Working Path 225 | xs_app_working_path= 226 | 227 | # Organization Name For Space "SAP" ( Default: orgname ) 228 | org_name=orgname 229 | 230 | # XS Advanced Admin User ( Default: XSA_ADMIN ) 231 | org_manager_user=XSA_ADMIN 232 | 233 | # XS Advanced Admin User Password 234 | org_manager_password= 235 | 236 | # Customer Space Name ( Default: PROD ) 237 | prod_space_name=PROD 238 | 239 | # Routing Mode ( Default: ports; Valid values: ports | hostnames ) 240 | xs_routing_mode=ports 241 | 242 | # XS Advanced Domain Name (see SAP Note 2245631) 243 | xs_domain_name= 244 | 245 | # Run Applications in SAP Space with Separate OS User (y/n) ( Default: y ) 246 | xs_sap_space_isolation=y 247 | 248 | # Run Applications in Customer Space with Separate OS User (y/n) ( Default: y ) 249 | xs_customer_space_isolation=y 250 | 251 | # XS Advanced SAP Space OS User ID 252 | xs_sap_space_user_id= 253 | 254 | # XS Advanced Customer Space OS User ID 255 | xs_customer_space_user_id= 256 | 257 | # XS Advanced Components 258 | xs_components= 259 | 260 | # Do not start the selected XS Advanced components after installation ( Default: none ) 261 | xs_components_nostart=none 262 | 263 | # XS Advanced Components Configurations 264 | xs_components_cfg= 265 | 266 | # XS Advanced Certificate 267 | xs_cert_pem= 268 | 269 | # XS Advanced Certificate Key 270 | xs_cert_key= 271 | 272 | # XS Advanced Trust Certificate 273 | xs_trust_pem= 274 | -------------------------------------------------------------------------------- /tests/support/original.conf.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/support/original.inifile.params: -------------------------------------------------------------------------------- 1 | ################################################################################################################################################################################################ 2 | # # 3 | # Installation service 'SAP NetWeaver 7.5 > SAP HANA Database > Installation > Application Server ABAP > High-Availability System > ASCS Instance', product id 'NW_ABAP_ASCS:NW750.HDB.ABAPHA' # 4 | # # 5 | ################################################################################################################################################################################################ 6 | # Password for the Diagnostics Agent specific adm user. Provided value may be encoded. 7 | # DiagnosticsAgent.dasidAdmPassword = 8 | 9 | 10 | 11 | 12 | # Windows domain in which the Diagnostics Agent users must be created. This is an optional property (Windows only). 13 | # DiagnosticsAgent.domain = 14 | 15 | 16 | 17 | 18 | # Windows only: Password for the Diagnostics Agent specific 'SAPService' user. 19 | # DiagnosticsAgent.sapServiceDASIDPassword = 20 | 21 | 22 | 23 | 24 | # Specify whether the all operating system users are to be removed from group 'sapinst' after the execution of Software Provisioning Manager has completed. 25 | NW_Delete_Sapinst_Users.removeUsers = true 26 | 27 | 28 | 29 | 30 | # Master password 31 | NW_GetMasterPassword.masterPwd = 32 | 33 | 34 | 35 | 36 | # Human readable form of the default login language to be preselected in SAPGUI. This Parameter is potentially prompted in addition in the screen that also asks for the . It is only prompted in systems that have an ABAP stack. It is prompted for installation but not for system copy. It is asked in those installations, that perform the ABAP load. That could be the database load installation in case of a distributed system scenario, or in case of a standard system installation with all instances on one host. This Parameter is saved in the 'DEFAULT' profile. It is has no influence on language settings in a Java stack. Valid names are stored in a table of subcomponent 'NW_languagesInLoadChecks'. The available languages must be declaired in the 'LANGUAGES_IN_LOAD' parameter of the 'product.xml' file . In this file, the one-character representation of the languages is used. Check the same table in subcomponent 'NW_languagesInLoadChecks'. 37 | # NW_GetSidNoProfiles.SAP_GUI_DEFAULT_LANGUAGE = 38 | 39 | 40 | 41 | 42 | # Windows only: The drive to use 43 | # NW_GetSidNoProfiles.sapdrive = 44 | 45 | 46 | 47 | 48 | # Unix only: The SAP mount directory path. Default value is '/sapmnt'. 49 | # NW_GetSidNoProfiles.sapmnt = /sapmnt 50 | 51 | 52 | 53 | 54 | # The SAP system ID of the system to be installed 55 | NW_GetSidNoProfiles.sid = HA2 56 | 57 | 58 | 59 | 60 | # Only use this parameter if recommended by SAP. 61 | # NW_GetSidNoProfiles.strictSidCheck = true 62 | 63 | 64 | 65 | 66 | # Specify whether this system is to be a Unicode system. 67 | # NW_GetSidNoProfiles.unicode = true 68 | 69 | 70 | 71 | 72 | # Add gateway process in the (A)SCS instance 73 | # NW_SCS_Instance.ascsInstallGateway = false 74 | 75 | 76 | 77 | 78 | # Add Web Dispatcher process in the (A)SCS instance 79 | # NW_SCS_Instance.ascsInstallWebDispatcher = false 80 | 81 | 82 | 83 | 84 | # Dual stack only: Specify the ASCS instance number. Leave empty for default. 85 | NW_SCS_Instance.ascsInstanceNumber = 86 | 87 | 88 | 89 | 90 | # Specify whether a 'prxyinfo(.DAT)' file is to be created in the 'global' directory if this file does not yet exist and that the 'gw/prxy_info' in the 'DEFAULT' profile is to be set accordingly. Default is false. 91 | # NW_SCS_Instance.createGlobalProxyInfoFile = false 92 | 93 | 94 | 95 | 96 | # Specify whether a 'reginfo(.DAT)' file is to be created in the 'global' directory. Default is 'false'. 97 | # NW_SCS_Instance.createGlobalRegInfoFile = false 98 | 99 | 100 | 101 | 102 | # The instance number of the (A)SCS instance. Leave this value empty if you want to use the default instance number or if you want to specify two different numbers for ASCS and SCS instance. 103 | NW_SCS_Instance.instanceNumber = 01 104 | 105 | 106 | 107 | 108 | # Dual stack only: The SCS instance number. Leave empty for default. 109 | # NW_SCS_Instance.scsInstanceNumber = 110 | 111 | 112 | 113 | 114 | # The (A)SCS message server port. Leave empty for default. 115 | # NW_SCS_Instance.scsMSPort = 116 | 117 | 118 | 119 | 120 | # You can specify a virtual host name for the ASCS instance. Leave empty for default. 121 | NW_SCS_Instance.scsVirtualHostname = sapha1as 122 | 123 | 124 | 125 | 126 | # SAP INTERNAL USE ONLY 127 | # NW_System.installSAPHostAgent = true 128 | 129 | 130 | 131 | 132 | # SAP INTERNAL USE ONLY 133 | # NW_adaptProfile.templateFiles = 134 | 135 | 136 | 137 | 138 | # The FQDN of the system 139 | # NW_getFQDN.FQDN = 140 | 141 | 142 | 143 | 144 | # Specify whether you want to set FQDN for the system. 145 | NW_getFQDN.setFQDN = false 146 | 147 | 148 | 149 | 150 | # The ASP device name where the SAP system will be in installed. The property is IBM i only. 151 | # Values from 1 to 256 can be specified. The default is 1, the System ASP. 152 | # OS4.DestinationASP = 153 | 154 | 155 | 156 | 157 | # The folder containing all archives that have been downloaded from http://support.sap.com/swdc and are supposed to be used in this procedure 158 | archives.downloadBasket = /root/sim_mount/NW/kernel_nw75_sar 159 | 160 | 161 | 162 | 163 | # Windows only: The domain of the SAP Host Agent user 164 | # hostAgent.domain = 165 | 166 | 167 | 168 | 169 | # Password for the 'sapadm' user of the SAP Host Agent 170 | # hostAgent.sapAdmPassword = 171 | 172 | 173 | 174 | 175 | # Windows only: The domain of all users of this SAP system. Leave empty for default. 176 | # nwUsers.sapDomain = 177 | 178 | 179 | 180 | 181 | # Windows only: The password of the 'SAPServiceSID' user 182 | # nwUsers.sapServiceSIDPassword = 183 | 184 | 185 | 186 | 187 | # UNIX only: The user ID of the 'sapadm' user, leave empty for default. The ID is ignored if the user already exists. 188 | nwUsers.sapadmUID = 189 | 190 | 191 | 192 | 193 | # UNIX only: The group id of the 'sapsys' group, leave empty for default. The ID is ignored if the group already exists. 194 | nwUsers.sapsysGID = 195 | 196 | 197 | 198 | 199 | # UNIX only: The user id of the adm user, leave empty for default. The ID is ignored if the user already exists. 200 | nwUsers.sidAdmUID = 201 | 202 | 203 | 204 | 205 | # The password of the 'adm' user 206 | # nwUsers.sidadmPassword = -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | envlist = py27, py36, py37, py38, py39 4 | 5 | [gh-actions] 6 | python = 7 | 2.7: py27 8 | 3.6: py36 9 | 3.7: py37 10 | 3.8: py38 11 | 3.9: py39 12 | 13 | [base] 14 | deps = 15 | pylint 16 | pytest 17 | pytest-cov 18 | 19 | [testenv] 20 | changedir=tests 21 | deps = 22 | {[base]deps} 23 | py27: mock 24 | 25 | commands = 26 | pytest -vv --cov=shaptools --cov-config .coveragerc --cov-report term --cov-report xml {posargs} 27 | 28 | --------------------------------------------------------------------------------