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.
@@ -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.
@@ -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,3 @@
1
+ """rsh - ssh helpers: sr (root shell), sExec/pExec (run a command on hosts), inventory plugins."""
2
+
3
+ __version__ = '3.0.1'
@@ -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
+ # }}