rsh-tools 3.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rsh_tools-3.0.1/LICENSE +21 -0
- rsh_tools-3.0.1/PKG-INFO +113 -0
- rsh_tools-3.0.1/README.md +82 -0
- rsh_tools-3.0.1/pyproject.toml +56 -0
- rsh_tools-3.0.1/rsh/__init__.py +3 -0
- rsh_tools-3.0.1/rsh/build_inventory.py +99 -0
- rsh_tools-3.0.1/rsh/compgen.py +40 -0
- rsh_tools-3.0.1/rsh/completion.bash +10 -0
- rsh_tools-3.0.1/rsh/config.py +174 -0
- rsh_tools-3.0.1/rsh/exec_functions.py +161 -0
- rsh_tools-3.0.1/rsh/execute.py +171 -0
- rsh_tools-3.0.1/rsh/inventory.py +66 -0
- rsh_tools-3.0.1/rsh/plugins/__init__.py +13 -0
- rsh_tools-3.0.1/rsh/plugins/aws.py +46 -0
- rsh_tools-3.0.1/rsh/plugins/base.py +93 -0
- rsh_tools-3.0.1/rsh/plugins/gce.py +43 -0
- rsh_tools-3.0.1/rsh/plugins/ovh.py +41 -0
- rsh_tools-3.0.1/rsh/sr.py +239 -0
- rsh_tools-3.0.1/rsh_tools.egg-info/PKG-INFO +113 -0
- rsh_tools-3.0.1/rsh_tools.egg-info/SOURCES.txt +29 -0
- rsh_tools-3.0.1/rsh_tools.egg-info/dependency_links.txt +1 -0
- rsh_tools-3.0.1/rsh_tools.egg-info/entry_points.txt +6 -0
- rsh_tools-3.0.1/rsh_tools.egg-info/requires.txt +14 -0
- rsh_tools-3.0.1/rsh_tools.egg-info/top_level.txt +1 -0
- rsh_tools-3.0.1/setup.cfg +4 -0
- rsh_tools-3.0.1/tests/test_build_inventory.py +44 -0
- rsh_tools-3.0.1/tests/test_compgen.py +16 -0
- rsh_tools-3.0.1/tests/test_config.py +39 -0
- rsh_tools-3.0.1/tests/test_inventory.py +49 -0
- rsh_tools-3.0.1/tests/test_plugins.py +86 -0
- rsh_tools-3.0.1/tests/test_sr.py +46 -0
rsh_tools-3.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Grigory Efimov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rsh_tools-3.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rsh-tools
|
|
3
|
+
Version: 3.0.1
|
|
4
|
+
Summary: ssh helpers: sr (root shell with privilege escalation), sExec/pExec (run commands on inventory groups), cloud inventory plugins
|
|
5
|
+
Author: Grigory Efimov
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fb929/rsh
|
|
8
|
+
Project-URL: Issues, https://github.com/fb929/rsh/issues
|
|
9
|
+
Keywords: ssh,sudo,inventory,aws,gce,ovh,devops
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: Operating System :: POSIX
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: System :: Systems Administration
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: deepmerge>=1.1
|
|
19
|
+
Requires-Dist: pexpect>=4.8
|
|
20
|
+
Requires-Dist: python-gnupg>=0.5
|
|
21
|
+
Requires-Dist: PyYAML>=6.0
|
|
22
|
+
Requires-Dist: fabric>=3.2
|
|
23
|
+
Requires-Dist: braceexpand>=0.1.7
|
|
24
|
+
Provides-Extra: ovh
|
|
25
|
+
Requires-Dist: ovh>=1.1; extra == "ovh"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
28
|
+
Requires-Dist: build; extra == "dev"
|
|
29
|
+
Requires-Dist: twine; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# rsh
|
|
33
|
+
scripts for auto login and parallel/successively execute command on linux servers
|
|
34
|
+
|
|
35
|
+
## description
|
|
36
|
+
* sr - server root
|
|
37
|
+
login over ssh and escalate privileges
|
|
38
|
+
* sExec - successively execute command on servers list or group
|
|
39
|
+
* pExec - parallel execute command on servers list or group
|
|
40
|
+
|
|
41
|
+
## install
|
|
42
|
+
```
|
|
43
|
+
pip install rsh-tools
|
|
44
|
+
```
|
|
45
|
+
installs the commands `sr`, `sExec`, `pExec`, `rshInventory` and `rshCompgen`.
|
|
46
|
+
with the OVH inventory plugin: `pip install 'rsh-tools[ovh]'`
|
|
47
|
+
|
|
48
|
+
bash completion for hosts and groups — add to `~/.bashrc`:
|
|
49
|
+
```
|
|
50
|
+
eval "$(rshCompgen completion)"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### development
|
|
54
|
+
```
|
|
55
|
+
git clone https://github.com/fb929/rsh && cd rsh
|
|
56
|
+
make dev # pip install --editable '.[dev,ovh]'
|
|
57
|
+
make test # pytest
|
|
58
|
+
```
|
|
59
|
+
the editable install puts the real commands on PATH, so the checkout is what runs.
|
|
60
|
+
|
|
61
|
+
### release
|
|
62
|
+
bump `__version__` in `rsh/__init__.py` ([semver](https://semver.org)), then tag and push:
|
|
63
|
+
```
|
|
64
|
+
git tag v3.0.0 && git push origin v3.0.0
|
|
65
|
+
```
|
|
66
|
+
the release workflow checks that the tag matches `__version__`, builds sdist + wheel,
|
|
67
|
+
uploads them to PyPI (secret `PYPI_API_TOKEN`) and attaches them to the GitHub release.
|
|
68
|
+
|
|
69
|
+
## configure
|
|
70
|
+
### (optional) passwords file for "su" mode
|
|
71
|
+
* create encrypt file .pass.gpg
|
|
72
|
+
* add passwords in format:
|
|
73
|
+
```
|
|
74
|
+
<hostname regex>\t<root password>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
example
|
|
78
|
+
```
|
|
79
|
+
.* myRootPasswd
|
|
80
|
+
```
|
|
81
|
+
* configure ~/.rsh.yaml
|
|
82
|
+
```
|
|
83
|
+
sr:
|
|
84
|
+
escalatePrivilegesCommands:
|
|
85
|
+
- 'su -m'
|
|
86
|
+
- 'sudo -s -E'
|
|
87
|
+
```
|
|
88
|
+
commands are tried on the host in the listed order; the first one that gives a root shell wins,
|
|
89
|
+
if none of them succeeds `sr` exits with an error.
|
|
90
|
+
the old single-value option `escalatePrivilegesCommand` still works, but is used only when `escalatePrivilegesCommands` is empty
|
|
91
|
+
|
|
92
|
+
### aws inventory plugin
|
|
93
|
+
* install package [aws-cli](https://github.com/aws/aws-cli)
|
|
94
|
+
* configure ~/.aws/credentials
|
|
95
|
+
|
|
96
|
+
### ovh inventory plugin
|
|
97
|
+
* `pip install 'rsh-tools[ovh]'`
|
|
98
|
+
* configure ~/.rsh.yaml
|
|
99
|
+
```
|
|
100
|
+
ovhInventory:
|
|
101
|
+
api:
|
|
102
|
+
endpoint: ovh-ca
|
|
103
|
+
application_key: xxx
|
|
104
|
+
application_secret: yyy
|
|
105
|
+
consumer_key: xxx
|
|
106
|
+
```
|
|
107
|
+
[create api tokens](https://help.ovhcloud.com/csm/en-api-getting-started-ovhcloud-api?id=kb_article_view&sysparm_article=KB0042777)
|
|
108
|
+
|
|
109
|
+
### gce inventory plugin
|
|
110
|
+
* install add setup [gcloud cli](https://cloud.google.com/sdk/docs/install)
|
|
111
|
+
|
|
112
|
+
### all configuration options
|
|
113
|
+
see the defaults in [rsh/config.py](rsh/config.py); `~/.rsh.yaml` (and `/etc/rsh/config.yaml`, `./.config.yaml`) are deep-merged over them
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# rsh
|
|
2
|
+
scripts for auto login and parallel/successively execute command on linux servers
|
|
3
|
+
|
|
4
|
+
## description
|
|
5
|
+
* sr - server root
|
|
6
|
+
login over ssh and escalate privileges
|
|
7
|
+
* sExec - successively execute command on servers list or group
|
|
8
|
+
* pExec - parallel execute command on servers list or group
|
|
9
|
+
|
|
10
|
+
## install
|
|
11
|
+
```
|
|
12
|
+
pip install rsh-tools
|
|
13
|
+
```
|
|
14
|
+
installs the commands `sr`, `sExec`, `pExec`, `rshInventory` and `rshCompgen`.
|
|
15
|
+
with the OVH inventory plugin: `pip install 'rsh-tools[ovh]'`
|
|
16
|
+
|
|
17
|
+
bash completion for hosts and groups — add to `~/.bashrc`:
|
|
18
|
+
```
|
|
19
|
+
eval "$(rshCompgen completion)"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### development
|
|
23
|
+
```
|
|
24
|
+
git clone https://github.com/fb929/rsh && cd rsh
|
|
25
|
+
make dev # pip install --editable '.[dev,ovh]'
|
|
26
|
+
make test # pytest
|
|
27
|
+
```
|
|
28
|
+
the editable install puts the real commands on PATH, so the checkout is what runs.
|
|
29
|
+
|
|
30
|
+
### release
|
|
31
|
+
bump `__version__` in `rsh/__init__.py` ([semver](https://semver.org)), then tag and push:
|
|
32
|
+
```
|
|
33
|
+
git tag v3.0.0 && git push origin v3.0.0
|
|
34
|
+
```
|
|
35
|
+
the release workflow checks that the tag matches `__version__`, builds sdist + wheel,
|
|
36
|
+
uploads them to PyPI (secret `PYPI_API_TOKEN`) and attaches them to the GitHub release.
|
|
37
|
+
|
|
38
|
+
## configure
|
|
39
|
+
### (optional) passwords file for "su" mode
|
|
40
|
+
* create encrypt file .pass.gpg
|
|
41
|
+
* add passwords in format:
|
|
42
|
+
```
|
|
43
|
+
<hostname regex>\t<root password>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
example
|
|
47
|
+
```
|
|
48
|
+
.* myRootPasswd
|
|
49
|
+
```
|
|
50
|
+
* configure ~/.rsh.yaml
|
|
51
|
+
```
|
|
52
|
+
sr:
|
|
53
|
+
escalatePrivilegesCommands:
|
|
54
|
+
- 'su -m'
|
|
55
|
+
- 'sudo -s -E'
|
|
56
|
+
```
|
|
57
|
+
commands are tried on the host in the listed order; the first one that gives a root shell wins,
|
|
58
|
+
if none of them succeeds `sr` exits with an error.
|
|
59
|
+
the old single-value option `escalatePrivilegesCommand` still works, but is used only when `escalatePrivilegesCommands` is empty
|
|
60
|
+
|
|
61
|
+
### aws inventory plugin
|
|
62
|
+
* install package [aws-cli](https://github.com/aws/aws-cli)
|
|
63
|
+
* configure ~/.aws/credentials
|
|
64
|
+
|
|
65
|
+
### ovh inventory plugin
|
|
66
|
+
* `pip install 'rsh-tools[ovh]'`
|
|
67
|
+
* configure ~/.rsh.yaml
|
|
68
|
+
```
|
|
69
|
+
ovhInventory:
|
|
70
|
+
api:
|
|
71
|
+
endpoint: ovh-ca
|
|
72
|
+
application_key: xxx
|
|
73
|
+
application_secret: yyy
|
|
74
|
+
consumer_key: xxx
|
|
75
|
+
```
|
|
76
|
+
[create api tokens](https://help.ovhcloud.com/csm/en-api-getting-started-ovhcloud-api?id=kb_article_view&sysparm_article=KB0042777)
|
|
77
|
+
|
|
78
|
+
### gce inventory plugin
|
|
79
|
+
* install add setup [gcloud cli](https://cloud.google.com/sdk/docs/install)
|
|
80
|
+
|
|
81
|
+
### all configuration options
|
|
82
|
+
see the defaults in [rsh/config.py](rsh/config.py); `~/.rsh.yaml` (and `/etc/rsh/config.yaml`, `./.config.yaml`) are deep-merged over them
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rsh-tools"
|
|
7
|
+
description = "ssh helpers: sr (root shell with privilege escalation), sExec/pExec (run commands on inventory groups), cloud inventory plugins"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
authors = [{name = "Grigory Efimov"}]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dynamic = ["version"]
|
|
14
|
+
keywords = ["ssh", "sudo", "inventory", "aws", "gce", "ovh", "devops"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: System Administrators",
|
|
18
|
+
"Operating System :: POSIX",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: System :: Systems Administration",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"deepmerge>=1.1",
|
|
24
|
+
"pexpect>=4.8",
|
|
25
|
+
"python-gnupg>=0.5",
|
|
26
|
+
"PyYAML>=6.0",
|
|
27
|
+
"fabric>=3.2",
|
|
28
|
+
"braceexpand>=0.1.7",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
ovh = ["ovh>=1.1"]
|
|
33
|
+
dev = ["pytest>=7", "build", "twine"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/fb929/rsh"
|
|
37
|
+
Issues = "https://github.com/fb929/rsh/issues"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
sr = "rsh.sr:main"
|
|
41
|
+
sExec = "rsh.execute:main_sequential"
|
|
42
|
+
pExec = "rsh.execute:main_parallel"
|
|
43
|
+
rshInventory = "rsh.build_inventory:main"
|
|
44
|
+
rshCompgen = "rsh.compgen:main"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools]
|
|
47
|
+
packages = ["rsh", "rsh.plugins"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
rsh = ["completion.bash"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.dynamic]
|
|
53
|
+
version = {attr = "rsh.__version__"}
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""rshInventory - collect hosts from the enabled inventory plugins and write inventory.yaml."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
import rsh.config
|
|
13
|
+
from rsh.plugins import PLUGINS
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger('rshInventory')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def natural_sort_key(name):
|
|
19
|
+
"""Token-aware natural order: 'web2' < 'web10'; the domain part (e.g. '.aws') is ignored."""
|
|
20
|
+
left = str(name).split('.', 1)[0]
|
|
21
|
+
tokens = []
|
|
22
|
+
for rough in re.split(r'[^A-Za-z0-9]+', left):
|
|
23
|
+
for part in re.findall(r'[A-Za-z]+|\d+', rough):
|
|
24
|
+
tokens.append((1, int(part)) if part.isdigit() else (0, part.lower()))
|
|
25
|
+
return tokens
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def group_name(*parts):
|
|
29
|
+
name = '_'.join(part.replace('-', '_') for part in parts)
|
|
30
|
+
return re.sub(r'\s+', '_', name)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def collect_records(cfg):
|
|
34
|
+
"""Run every enabled plugin, return the merged list of normalized records."""
|
|
35
|
+
records = []
|
|
36
|
+
for key, plugin_class in PLUGINS.items():
|
|
37
|
+
if not (cfg.get(key) or {}).get('enable'):
|
|
38
|
+
logger.debug("plugin '%s' disabled", key)
|
|
39
|
+
continue
|
|
40
|
+
logger.debug("running plugin '%s'", key)
|
|
41
|
+
records.extend(plugin_class(cfg).instances_info())
|
|
42
|
+
for key in cfg:
|
|
43
|
+
if key.endswith('Inventory') and key not in PLUGINS:
|
|
44
|
+
logger.warning("config section '%s' has no matching inventory plugin, known: %s", key, sorted(PLUGINS))
|
|
45
|
+
return records
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_inventory(records, cfg):
|
|
49
|
+
"""Turn plugin records into the {'hosts': ..., 'groups': ...} structure."""
|
|
50
|
+
hosts = {}
|
|
51
|
+
groups = {}
|
|
52
|
+
|
|
53
|
+
def add_to_group(name, host):
|
|
54
|
+
groups.setdefault(name, [])
|
|
55
|
+
if host not in groups[name]:
|
|
56
|
+
groups[name].append(host)
|
|
57
|
+
|
|
58
|
+
for record in records:
|
|
59
|
+
host = record.get('host')
|
|
60
|
+
if not host:
|
|
61
|
+
logger.error("record without 'host': %s", record)
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
module = record.get('rshInventoryModule')
|
|
64
|
+
settings = cfg.get(module) or {}
|
|
65
|
+
if host not in hosts:
|
|
66
|
+
hosts[host] = {'sshHost': record['sshHost']}
|
|
67
|
+
for field in settings.get('hostInfoFields', []):
|
|
68
|
+
hosts[host][field] = record.get(field, 'unknown')
|
|
69
|
+
skip_tags = settings.get('skipTagsForMakeGroups', [])
|
|
70
|
+
for tag in record.get('tags', []):
|
|
71
|
+
if tag['Key'] in skip_tags:
|
|
72
|
+
continue
|
|
73
|
+
add_to_group(group_name('tag', tag['Key'], tag['Value']), host)
|
|
74
|
+
add_to_group(group_name('dc', record.get('hosting', 'unknown'), record['dc']), host)
|
|
75
|
+
|
|
76
|
+
hosts = {name: hosts[name] for name in sorted(hosts, key=natural_sort_key)}
|
|
77
|
+
groups = {name: sorted(groups[name], key=natural_sort_key) for name in sorted(groups, key=natural_sort_key)}
|
|
78
|
+
return {'hosts': hosts, 'groups': groups}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def write_inventory(inventory, path):
|
|
82
|
+
path = os.path.expanduser(path)
|
|
83
|
+
with open(path, 'w') as f:
|
|
84
|
+
yaml.dump(inventory, f, default_flow_style=False, sort_keys=False)
|
|
85
|
+
logger.info("inventory written to '%s': %d hosts, %d groups", path, len(inventory['hosts']), len(inventory['groups']))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
cfg = rsh.config.cfg
|
|
90
|
+
logger.debug("cfg=%s", json.dumps(cfg, indent=4))
|
|
91
|
+
records = collect_records(cfg)
|
|
92
|
+
logger.debug("records=%s", json.dumps(records, indent=4))
|
|
93
|
+
inventory = build_inventory(records, cfg)
|
|
94
|
+
logger.debug("inventory=%s", inventory)
|
|
95
|
+
write_inventory(inventory, cfg['inventoryFilePath'])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""rshCompgen - word lists for bash completion, and the completion script itself."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from importlib import resources
|
|
8
|
+
|
|
9
|
+
import rsh.config
|
|
10
|
+
from rsh.inventory import load_inventory
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger('rshCompgen')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def completion_script():
|
|
16
|
+
"""Bash completion for sr/sExec/pExec; install with: eval "$(rshCompgen completion)"."""
|
|
17
|
+
return resources.files('rsh').joinpath('completion.bash').read_text()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_args(argv=None):
|
|
21
|
+
parser = argparse.ArgumentParser(description='bash completion helper for sr, sExec and pExec')
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
'what',
|
|
24
|
+
choices=['hosts', 'groups', 'completion'],
|
|
25
|
+
help="'hosts' or 'groups' print inventory names, 'completion' prints the bash completion script",
|
|
26
|
+
)
|
|
27
|
+
return parser.parse_args(argv)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main(argv=None):
|
|
31
|
+
args = parse_args(argv)
|
|
32
|
+
if args.what == 'completion':
|
|
33
|
+
sys.stdout.write(completion_script())
|
|
34
|
+
return
|
|
35
|
+
inventory = load_inventory(rsh.config.cfg)
|
|
36
|
+
print(' '.join(inventory[args.what].keys()))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
main()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# bash completion for rsh tools; install with: eval "$(rshCompgen completion)"
|
|
2
|
+
_rsh_completion_hosts(){
|
|
3
|
+
COMPREPLY=( $(compgen -W "$(rshCompgen hosts)" -- "$2") )
|
|
4
|
+
}
|
|
5
|
+
_rsh_completion_groups(){
|
|
6
|
+
COMPREPLY=( $(compgen -W "$(rshCompgen groups)" -- "$2") )
|
|
7
|
+
}
|
|
8
|
+
complete -F _rsh_completion_hosts sr
|
|
9
|
+
complete -F _rsh_completion_groups sExec
|
|
10
|
+
complete -F _rsh_completion_groups pExec
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from os.path import expanduser
|
|
6
|
+
import sys
|
|
7
|
+
import yaml
|
|
8
|
+
import re
|
|
9
|
+
import logging
|
|
10
|
+
from deepmerge import always_merger
|
|
11
|
+
|
|
12
|
+
PROGRAM_NAME = 'rsh'
|
|
13
|
+
HOME_DIR = expanduser("~")
|
|
14
|
+
DEFAULT_CONFIG_FILES = [
|
|
15
|
+
'/etc/' + PROGRAM_NAME + '/config.yaml',
|
|
16
|
+
HOME_DIR + '/.' + PROGRAM_NAME + '.yaml',
|
|
17
|
+
'./.config.yaml',
|
|
18
|
+
]
|
|
19
|
+
cfg = {
|
|
20
|
+
#'logFile': HOME_DIR + '/log/' + PROGRAM_NAME + '/' + PROGRAM_NAME + '.log',
|
|
21
|
+
'logFile': 'stdout',
|
|
22
|
+
'logLevel': 'info',
|
|
23
|
+
'gpgFile': HOME_DIR + '/.pass.gpg',
|
|
24
|
+
'inventoryFilePath': HOME_DIR + '/inventory.yaml',
|
|
25
|
+
'sensitiveKeys': [
|
|
26
|
+
'application_key',
|
|
27
|
+
'application_secret',
|
|
28
|
+
'consumer_key',
|
|
29
|
+
],
|
|
30
|
+
'awsInventory': {
|
|
31
|
+
'enable': True,
|
|
32
|
+
'regions': [],
|
|
33
|
+
'sshHostField': 'host', # field for 'sshHost', variants: host, PublicDnsName, PublicIpAddress, PrivateDnsName, PrivateIpAddress
|
|
34
|
+
'skipTagsForMakeGroups': [ # these tag 'key' will skip when creating groups
|
|
35
|
+
'Name',
|
|
36
|
+
'Description',
|
|
37
|
+
'description',
|
|
38
|
+
],
|
|
39
|
+
'skipNotRunningInstance': True, # skipping not running instance
|
|
40
|
+
'hostInfoFields': [
|
|
41
|
+
'PrivateIpAddress',
|
|
42
|
+
'PublicIpAddress',
|
|
43
|
+
],
|
|
44
|
+
'nameSuffix': '.aws', # suffix for instance names
|
|
45
|
+
},
|
|
46
|
+
'ovhInventory': {
|
|
47
|
+
'enable': True,
|
|
48
|
+
'api': {
|
|
49
|
+
'endpoint': 'ovh-ca', # Endpoint of API OVH
|
|
50
|
+
'application_key': 'xxx', # Application Key
|
|
51
|
+
'application_secret': 'yyy', # Application Secret
|
|
52
|
+
'consumer_key': 'zzz', # Consumer Key
|
|
53
|
+
},
|
|
54
|
+
'sshHostField': 'host', # field for 'sshHost', variants: host, customName, publicIp, internalName
|
|
55
|
+
'skipTagsForMakeGroups': [], # these tag 'key' will skip when creating groups
|
|
56
|
+
'hostInfoFields': [
|
|
57
|
+
'publicIp',
|
|
58
|
+
'internalName', # internal name
|
|
59
|
+
'dc',
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
'gceInventory': {
|
|
63
|
+
'enable': True,
|
|
64
|
+
'skipNotRunningInstance': True, # skipping not running instance
|
|
65
|
+
'sshHostField': 'host', # field for 'sshHost', variants: host, PrivateIpAddress, PublicIpAddress
|
|
66
|
+
'hostInfoFields': [
|
|
67
|
+
'PrivateIpAddress',
|
|
68
|
+
'PublicIpAddress',
|
|
69
|
+
'dc',
|
|
70
|
+
],
|
|
71
|
+
'skipTagsForMakeGroups': [ # these tag 'key' will skip when creating groups
|
|
72
|
+
'name',
|
|
73
|
+
],
|
|
74
|
+
'nameSuffix': '.gc', # suffix for instance names
|
|
75
|
+
},
|
|
76
|
+
'sr': {
|
|
77
|
+
'ssh': {
|
|
78
|
+
'command': 'ssh -Y -o ConnectTimeout=5 -o StrictHostKeyChecking=no',
|
|
79
|
+
},
|
|
80
|
+
'pexpect': {
|
|
81
|
+
'timeout': 120,
|
|
82
|
+
},
|
|
83
|
+
'useInventorySshHost': True, # use sshHost from inventory for connect to host
|
|
84
|
+
'escalatePrivilegesCommands': [], # commands are tried on the host in the listed order until one gives a root shell,
|
|
85
|
+
# e.g. ['su -m', 'sudo -s -E'];
|
|
86
|
+
# must stay empty here: deepmerge appends lists, so a default value would be added to the user's list
|
|
87
|
+
'escalatePrivilegesCommand': 'sudo -s -E', # deprecated, used only when 'escalatePrivilegesCommands' is empty; variants: 'su -m', 'sudo -s', 'sudo -s -E'
|
|
88
|
+
'postLoginCommand': None # command will run after login, used to set up the environment
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# load config files {{
|
|
93
|
+
def load_config_files(cfg, paths):
|
|
94
|
+
"""Deep-merge every existing YAML mapping from paths into cfg, in order."""
|
|
95
|
+
for path in paths:
|
|
96
|
+
if not os.path.isfile(path):
|
|
97
|
+
continue
|
|
98
|
+
try:
|
|
99
|
+
with open(path, 'r') as f:
|
|
100
|
+
loaded = yaml.load(f, Loader=yaml.Loader)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logging.warning("skipping config file '%s': %s", path, e)
|
|
103
|
+
continue
|
|
104
|
+
if not isinstance(loaded, dict):
|
|
105
|
+
logging.warning("skipping config file '%s': top level is not a mapping", path)
|
|
106
|
+
continue
|
|
107
|
+
cfg = always_merger.merge(cfg, loaded)
|
|
108
|
+
return cfg
|
|
109
|
+
|
|
110
|
+
cfg = load_config_files(cfg, DEFAULT_CONFIG_FILES)
|
|
111
|
+
cfg['logDir'] = os.path.dirname(cfg['logFile']) or '.'
|
|
112
|
+
# }}
|
|
113
|
+
|
|
114
|
+
# logging {{
|
|
115
|
+
def parse_log_level(name):
|
|
116
|
+
if re.match(r"^(warn|warning)$", str(name), re.IGNORECASE):
|
|
117
|
+
return logging.WARNING
|
|
118
|
+
if re.match(r"^debug$", str(name), re.IGNORECASE):
|
|
119
|
+
return logging.DEBUG
|
|
120
|
+
return logging.INFO
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def find_values(data, keys):
|
|
124
|
+
"""Collect values of the given keys anywhere in a nested dict/list structure."""
|
|
125
|
+
if isinstance(keys, str):
|
|
126
|
+
keys = [keys]
|
|
127
|
+
found = []
|
|
128
|
+
if isinstance(data, dict):
|
|
129
|
+
for key, value in data.items():
|
|
130
|
+
if key in keys:
|
|
131
|
+
found.append(value)
|
|
132
|
+
else:
|
|
133
|
+
found.extend(find_values(value, keys))
|
|
134
|
+
elif isinstance(data, list):
|
|
135
|
+
for item in data:
|
|
136
|
+
found.extend(find_values(item, keys))
|
|
137
|
+
return found
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class SensitiveFormatter(logging.Formatter):
|
|
141
|
+
"""Masks sensitive config values (api keys etc.) in log output."""
|
|
142
|
+
|
|
143
|
+
def __init__(self, fmt, datefmt, sensitive_values):
|
|
144
|
+
super().__init__(fmt, datefmt=datefmt)
|
|
145
|
+
values = [str(value) for value in sensitive_values if value]
|
|
146
|
+
self.pattern = re.compile('|'.join(re.escape(value) for value in values)) if values else None
|
|
147
|
+
|
|
148
|
+
def format(self, record):
|
|
149
|
+
message = super().format(record)
|
|
150
|
+
if self.pattern is None:
|
|
151
|
+
return message
|
|
152
|
+
return self.pattern.sub('[CLASSIFIED]', message)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
LOG_FORMAT = '%(asctime)s\t%(name)s\t%(funcName)s\t%(levelname)s\t%(message)s'
|
|
156
|
+
LOG_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S'
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def setup_logging(cfg):
|
|
160
|
+
level = parse_log_level(cfg['logLevel'])
|
|
161
|
+
if level > logging.DEBUG:
|
|
162
|
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
163
|
+
logging.getLogger("requests").setLevel(logging.WARNING)
|
|
164
|
+
if cfg['logFile'] == 'stdout':
|
|
165
|
+
logging.basicConfig(level=level, format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT)
|
|
166
|
+
else:
|
|
167
|
+
os.makedirs(cfg['logDir'], exist_ok=True)
|
|
168
|
+
logging.basicConfig(filename=cfg['logFile'], level=level, format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT)
|
|
169
|
+
formatter = SensitiveFormatter(LOG_FORMAT, LOG_DATE_FORMAT, find_values(cfg, cfg['sensitiveKeys']))
|
|
170
|
+
for handler in logging.root.handlers:
|
|
171
|
+
handler.setFormatter(formatter)
|
|
172
|
+
|
|
173
|
+
setup_logging(cfg)
|
|
174
|
+
# }}
|