teuthology 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
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.
- scripts/describe.py +1 -0
- scripts/dispatcher.py +55 -26
- scripts/exporter.py +18 -0
- scripts/lock.py +1 -1
- scripts/node_cleanup.py +58 -0
- scripts/openstack.py +9 -9
- scripts/results.py +12 -11
- scripts/schedule.py +4 -0
- scripts/suite.py +57 -16
- scripts/supervisor.py +44 -0
- scripts/update_inventory.py +10 -4
- teuthology/__init__.py +24 -26
- teuthology/beanstalk.py +4 -3
- teuthology/config.py +16 -6
- teuthology/contextutil.py +18 -14
- teuthology/describe_tests.py +25 -18
- teuthology/dispatcher/__init__.py +210 -35
- teuthology/dispatcher/supervisor.py +140 -58
- teuthology/exceptions.py +43 -0
- teuthology/exporter.py +347 -0
- teuthology/kill.py +76 -81
- teuthology/lock/cli.py +3 -3
- teuthology/lock/ops.py +135 -61
- teuthology/lock/query.py +61 -44
- teuthology/ls.py +1 -1
- teuthology/misc.py +61 -75
- teuthology/nuke/__init__.py +12 -353
- teuthology/openstack/__init__.py +4 -3
- teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
- teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
- teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
- teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
- teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
- teuthology/openstack/openstack-teuthology.cron +0 -1
- teuthology/orchestra/cluster.py +49 -7
- teuthology/orchestra/connection.py +16 -5
- teuthology/orchestra/console.py +111 -50
- teuthology/orchestra/daemon/cephadmunit.py +17 -4
- teuthology/orchestra/daemon/state.py +8 -1
- teuthology/orchestra/daemon/systemd.py +4 -4
- teuthology/orchestra/opsys.py +30 -11
- teuthology/orchestra/remote.py +405 -338
- teuthology/orchestra/run.py +3 -3
- teuthology/packaging.py +19 -16
- teuthology/provision/__init__.py +30 -10
- teuthology/provision/cloud/openstack.py +12 -6
- teuthology/provision/cloud/util.py +1 -2
- teuthology/provision/downburst.py +4 -3
- teuthology/provision/fog.py +68 -20
- teuthology/provision/openstack.py +5 -4
- teuthology/provision/pelagos.py +1 -1
- teuthology/repo_utils.py +43 -13
- teuthology/report.py +57 -35
- teuthology/results.py +5 -3
- teuthology/run.py +13 -14
- teuthology/run_tasks.py +27 -43
- teuthology/schedule.py +4 -3
- teuthology/scrape.py +28 -22
- teuthology/suite/__init__.py +74 -45
- teuthology/suite/build_matrix.py +34 -24
- teuthology/suite/fragment-merge.lua +105 -0
- teuthology/suite/matrix.py +31 -2
- teuthology/suite/merge.py +175 -0
- teuthology/suite/placeholder.py +6 -9
- teuthology/suite/run.py +175 -100
- teuthology/suite/util.py +64 -218
- teuthology/task/__init__.py +1 -1
- teuthology/task/ansible.py +101 -32
- teuthology/task/buildpackages.py +2 -2
- teuthology/task/ceph_ansible.py +13 -6
- teuthology/task/cephmetrics.py +2 -1
- teuthology/task/clock.py +33 -14
- teuthology/task/exec.py +18 -0
- teuthology/task/hadoop.py +2 -2
- teuthology/task/install/__init__.py +29 -7
- teuthology/task/install/bin/adjust-ulimits +16 -0
- teuthology/task/install/bin/daemon-helper +114 -0
- teuthology/task/install/bin/stdin-killer +263 -0
- teuthology/task/install/deb.py +1 -1
- teuthology/task/install/rpm.py +17 -5
- teuthology/task/install/util.py +3 -3
- teuthology/task/internal/__init__.py +41 -10
- teuthology/task/internal/edit_sudoers.sh +10 -0
- teuthology/task/internal/lock_machines.py +2 -9
- teuthology/task/internal/redhat.py +31 -1
- teuthology/task/internal/syslog.py +31 -8
- teuthology/task/kernel.py +152 -145
- teuthology/task/lockfile.py +1 -1
- teuthology/task/mpi.py +10 -10
- teuthology/task/pcp.py +1 -1
- teuthology/task/selinux.py +16 -8
- teuthology/task/ssh_keys.py +4 -4
- teuthology/task/tests/__init__.py +137 -77
- teuthology/task/tests/test_fetch_coredumps.py +116 -0
- teuthology/task/tests/test_run.py +4 -4
- teuthology/timer.py +3 -3
- teuthology/util/loggerfile.py +19 -0
- teuthology/util/scanner.py +159 -0
- teuthology/util/sentry.py +52 -0
- teuthology/util/time.py +52 -0
- teuthology-1.2.0.data/scripts/adjust-ulimits +16 -0
- teuthology-1.2.0.data/scripts/daemon-helper +114 -0
- teuthology-1.2.0.data/scripts/stdin-killer +263 -0
- teuthology-1.2.0.dist-info/METADATA +89 -0
- teuthology-1.2.0.dist-info/RECORD +174 -0
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/WHEEL +1 -1
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/entry_points.txt +3 -2
- scripts/nuke.py +0 -47
- scripts/worker.py +0 -37
- teuthology/nuke/actions.py +0 -456
- teuthology/openstack/test/__init__.py +0 -0
- teuthology/openstack/test/openstack-integration.py +0 -286
- teuthology/openstack/test/test_config.py +0 -35
- teuthology/openstack/test/test_openstack.py +0 -1695
- teuthology/orchestra/test/__init__.py +0 -0
- teuthology/orchestra/test/integration/__init__.py +0 -0
- teuthology/orchestra/test/integration/test_integration.py +0 -94
- teuthology/orchestra/test/test_cluster.py +0 -240
- teuthology/orchestra/test/test_connection.py +0 -106
- teuthology/orchestra/test/test_console.py +0 -217
- teuthology/orchestra/test/test_opsys.py +0 -404
- teuthology/orchestra/test/test_remote.py +0 -185
- teuthology/orchestra/test/test_run.py +0 -286
- teuthology/orchestra/test/test_systemd.py +0 -54
- teuthology/orchestra/test/util.py +0 -12
- teuthology/test/__init__.py +0 -0
- teuthology/test/fake_archive.py +0 -107
- teuthology/test/fake_fs.py +0 -92
- teuthology/test/integration/__init__.py +0 -0
- teuthology/test/integration/test_suite.py +0 -86
- teuthology/test/task/__init__.py +0 -205
- teuthology/test/task/test_ansible.py +0 -624
- teuthology/test/task/test_ceph_ansible.py +0 -176
- teuthology/test/task/test_console_log.py +0 -88
- teuthology/test/task/test_install.py +0 -337
- teuthology/test/task/test_internal.py +0 -57
- teuthology/test/task/test_kernel.py +0 -243
- teuthology/test/task/test_pcp.py +0 -379
- teuthology/test/task/test_selinux.py +0 -35
- teuthology/test/test_config.py +0 -189
- teuthology/test/test_contextutil.py +0 -68
- teuthology/test/test_describe_tests.py +0 -316
- teuthology/test/test_email_sleep_before_teardown.py +0 -81
- teuthology/test/test_exit.py +0 -97
- teuthology/test/test_get_distro.py +0 -47
- teuthology/test/test_get_distro_version.py +0 -47
- teuthology/test/test_get_multi_machine_types.py +0 -27
- teuthology/test/test_job_status.py +0 -60
- teuthology/test/test_ls.py +0 -48
- teuthology/test/test_misc.py +0 -391
- teuthology/test/test_nuke.py +0 -290
- teuthology/test/test_packaging.py +0 -763
- teuthology/test/test_parallel.py +0 -28
- teuthology/test/test_repo_utils.py +0 -225
- teuthology/test/test_report.py +0 -77
- teuthology/test/test_results.py +0 -155
- teuthology/test/test_run.py +0 -239
- teuthology/test/test_safepath.py +0 -55
- teuthology/test/test_schedule.py +0 -45
- teuthology/test/test_scrape.py +0 -167
- teuthology/test/test_timer.py +0 -80
- teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
- teuthology/test/test_worker.py +0 -303
- teuthology/worker.py +0 -354
- teuthology-1.1.0.dist-info/METADATA +0 -76
- teuthology-1.1.0.dist-info/RECORD +0 -213
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/LICENSE +0 -0
- {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/top_level.txt +0 -0
teuthology/task/ceph_ansible.py
CHANGED
@@ -22,7 +22,7 @@ class CephAnsible(Task):
|
|
22
22
|
|
23
23
|
- ceph-ansible:
|
24
24
|
repo: {git_base}ceph-ansible.git
|
25
|
-
branch: mybranch # defaults to
|
25
|
+
branch: mybranch # defaults to main
|
26
26
|
ansible-version: 2.4 # defaults to 2.5
|
27
27
|
vars:
|
28
28
|
ceph_dev: True ( default)
|
@@ -73,7 +73,7 @@ class CephAnsible(Task):
|
|
73
73
|
if 'ceph_dev_key' not in vars:
|
74
74
|
vars['ceph_dev_key'] = 'https://download.ceph.com/keys/autobuild.asc'
|
75
75
|
if 'ceph_dev_branch' not in vars:
|
76
|
-
vars['ceph_dev_branch'] = ctx.config.get('branch', '
|
76
|
+
vars['ceph_dev_branch'] = ctx.config.get('branch', 'main')
|
77
77
|
self.cluster_name = vars.get('cluster', 'ceph')
|
78
78
|
|
79
79
|
def setup(self):
|
@@ -296,8 +296,11 @@ class CephAnsible(Task):
|
|
296
296
|
roles = self.ctx.cluster.remotes[remote]
|
297
297
|
dev_needed = len([role for role in roles
|
298
298
|
if role.startswith('osd')])
|
299
|
-
if
|
300
|
-
|
299
|
+
if (
|
300
|
+
teuth_config.get('ceph_ansible') and
|
301
|
+
hasattr(self.ctx, "machine_type") and
|
302
|
+
self.ctx.machine_type in teuth_config['ceph_ansible']['has_lvm_scratch_disks']
|
303
|
+
):
|
301
304
|
devices = get_file(remote, "/scratch_devs").decode().split()
|
302
305
|
vols = []
|
303
306
|
|
@@ -375,7 +378,7 @@ class CephAnsible(Task):
|
|
375
378
|
'python-dev'
|
376
379
|
])
|
377
380
|
ansible_repo = self.config['repo']
|
378
|
-
branch = '
|
381
|
+
branch = 'main'
|
379
382
|
if self.config.get('branch'):
|
380
383
|
branch = self.config.get('branch')
|
381
384
|
ansible_ver = 'ansible==2.5'
|
@@ -404,7 +407,6 @@ class CephAnsible(Task):
|
|
404
407
|
run.Raw('cd ~/ceph-ansible'),
|
405
408
|
run.Raw(';'),
|
406
409
|
'virtualenv',
|
407
|
-
run.Raw('--system-site-packages'),
|
408
410
|
run.Raw('--python=python3'),
|
409
411
|
'venv',
|
410
412
|
run.Raw(';'),
|
@@ -425,6 +427,11 @@ class CephAnsible(Task):
|
|
425
427
|
run.Raw('setuptools>=11.3'),
|
426
428
|
run.Raw('notario>=0.0.13'), # FIXME: use requirements.txt
|
427
429
|
run.Raw('netaddr'),
|
430
|
+
run.Raw('six'),
|
431
|
+
run.Raw(';'),
|
432
|
+
'LANG=en_US.utf8',
|
433
|
+
'pip',
|
434
|
+
'install',
|
428
435
|
run.Raw(ansible_ver),
|
429
436
|
run.Raw(';'),
|
430
437
|
run.Raw(str_args)
|
teuthology/task/cephmetrics.py
CHANGED
@@ -5,8 +5,9 @@ import time
|
|
5
5
|
|
6
6
|
from teuthology.config import config as teuth_config
|
7
7
|
from teuthology.exceptions import CommandFailedError
|
8
|
+
from teuthology.task.ansible import Ansible
|
9
|
+
from teuthology.util.loggerfile import LoggerFile
|
8
10
|
|
9
|
-
from teuthology.ansible import Ansible, LoggerFile
|
10
11
|
|
11
12
|
log = logging.getLogger(__name__)
|
12
13
|
|
teuthology/task/clock.py
CHANGED
@@ -8,6 +8,13 @@ from teuthology.orchestra import run
|
|
8
8
|
|
9
9
|
log = logging.getLogger(__name__)
|
10
10
|
|
11
|
+
def filter_out_containers(cluster):
|
12
|
+
"""
|
13
|
+
Returns a cluster that excludes remotes which should skip this task.
|
14
|
+
Currently, only skips containerized remotes.
|
15
|
+
"""
|
16
|
+
return cluster.filter(lambda r: not r.is_container)
|
17
|
+
|
11
18
|
@contextlib.contextmanager
|
12
19
|
def task(ctx, config):
|
13
20
|
"""
|
@@ -30,8 +37,9 @@ def task(ctx, config):
|
|
30
37
|
"""
|
31
38
|
|
32
39
|
log.info('Syncing clocks and checking initial clock skew...')
|
33
|
-
|
34
|
-
|
40
|
+
cluster = filter_out_containers(ctx.cluster)
|
41
|
+
run.wait(
|
42
|
+
cluster.run(
|
35
43
|
args = [
|
36
44
|
'sudo', 'systemctl', 'stop', 'ntp.service', run.Raw('||'),
|
37
45
|
'sudo', 'systemctl', 'stop', 'ntpd.service', run.Raw('||'),
|
@@ -50,22 +58,27 @@ def task(ctx, config):
|
|
50
58
|
'true'
|
51
59
|
],
|
52
60
|
timeout = 360,
|
61
|
+
wait=False,
|
53
62
|
)
|
63
|
+
)
|
54
64
|
|
55
65
|
try:
|
56
66
|
yield
|
57
67
|
|
58
68
|
finally:
|
59
69
|
log.info('Checking final clock skew...')
|
60
|
-
|
61
|
-
|
70
|
+
cluster = filter_out_containers(ctx.cluster)
|
71
|
+
run.wait(
|
72
|
+
cluster.run(
|
62
73
|
args=[
|
63
74
|
'PATH=/usr/bin:/usr/sbin', 'ntpq', '-p', run.Raw('||'),
|
64
75
|
'PATH=/usr/bin:/usr/sbin', 'chronyc', 'sources',
|
65
76
|
run.Raw('||'),
|
66
77
|
'true'
|
67
|
-
|
68
|
-
|
78
|
+
],
|
79
|
+
wait=False,
|
80
|
+
)
|
81
|
+
)
|
69
82
|
|
70
83
|
|
71
84
|
@contextlib.contextmanager
|
@@ -77,27 +90,33 @@ def check(ctx, config):
|
|
77
90
|
:param config: Configuration
|
78
91
|
"""
|
79
92
|
log.info('Checking initial clock skew...')
|
80
|
-
|
81
|
-
|
93
|
+
cluster = filter_out_containers(ctx.cluster)
|
94
|
+
run.wait(
|
95
|
+
cluster.run(
|
82
96
|
args=[
|
83
97
|
'PATH=/usr/bin:/usr/sbin', 'ntpq', '-p', run.Raw('||'),
|
84
98
|
'PATH=/usr/bin:/usr/sbin', 'chronyc', 'sources',
|
85
99
|
run.Raw('||'),
|
86
100
|
'true'
|
87
|
-
|
88
|
-
|
101
|
+
],
|
102
|
+
wait=False,
|
103
|
+
)
|
104
|
+
)
|
89
105
|
|
90
106
|
try:
|
91
107
|
yield
|
92
108
|
|
93
109
|
finally:
|
94
110
|
log.info('Checking final clock skew...')
|
95
|
-
|
96
|
-
|
111
|
+
cluster = filter_out_containers(ctx.cluster)
|
112
|
+
run.wait(
|
113
|
+
cluster.run(
|
97
114
|
args=[
|
98
115
|
'PATH=/usr/bin:/usr/sbin', 'ntpq', '-p', run.Raw('||'),
|
99
116
|
'PATH=/usr/bin:/usr/sbin', 'chronyc', 'sources',
|
100
117
|
run.Raw('||'),
|
101
118
|
'true'
|
102
|
-
|
103
|
-
|
119
|
+
],
|
120
|
+
wait=False,
|
121
|
+
)
|
122
|
+
)
|
teuthology/task/exec.py
CHANGED
@@ -23,6 +23,16 @@ def task(ctx, config):
|
|
23
23
|
It stops and fails with the first command that does not return on success. It means
|
24
24
|
that if the first command fails, the second won't run at all.
|
25
25
|
|
26
|
+
You can run a command on all hosts `all-hosts`, or all roles with `all-roles`:
|
27
|
+
|
28
|
+
tasks:
|
29
|
+
- exec:
|
30
|
+
all-hosts:
|
31
|
+
- touch /etc/passwd
|
32
|
+
- exec:
|
33
|
+
all-roles:
|
34
|
+
- pwd
|
35
|
+
|
26
36
|
To avoid confusion it is recommended to explicitly enclose the commands in
|
27
37
|
double quotes. For instance if the command is false (without double quotes) it will
|
28
38
|
be interpreted as a boolean by the YAML parser.
|
@@ -39,6 +49,14 @@ def task(ctx, config):
|
|
39
49
|
a = config['all']
|
40
50
|
roles = teuthology.all_roles(ctx.cluster)
|
41
51
|
config = dict((id_, a) for id_ in roles)
|
52
|
+
elif 'all-roles' in config and len(config) == 1:
|
53
|
+
a = config['all-roles']
|
54
|
+
roles = teuthology.all_roles(ctx.cluster)
|
55
|
+
config = dict((id_, a) for id_ in roles)
|
56
|
+
elif 'all-hosts' in config and len(config) == 1:
|
57
|
+
a = config['all-hosts']
|
58
|
+
roles = [roles[0] for roles in ctx.cluster.remotes.values()]
|
59
|
+
config = dict((id_, a) for id_ in roles)
|
42
60
|
|
43
61
|
for role, ls in config.items():
|
44
62
|
(remote,) = ctx.cluster.only(role).remotes.keys()
|
teuthology/task/hadoop.py
CHANGED
@@ -9,6 +9,7 @@ from teuthology import misc as teuthology
|
|
9
9
|
from teuthology import contextutil, packaging
|
10
10
|
from teuthology.parallel import parallel
|
11
11
|
from teuthology.task import ansible
|
12
|
+
from teuthology.exceptions import ConfigError
|
12
13
|
|
13
14
|
from distutils.version import LooseVersion
|
14
15
|
from teuthology.task.install.util import (
|
@@ -282,7 +283,7 @@ def upgrade_remote_to_config(ctx, config):
|
|
282
283
|
# take any remote in the dict
|
283
284
|
remote = next(iter(remotes_dict))
|
284
285
|
if remote in remotes:
|
285
|
-
log.
|
286
|
+
log.warning('remote %s came up twice (role %s)', remote, role)
|
286
287
|
continue
|
287
288
|
remotes[remote] = config.get(role)
|
288
289
|
|
@@ -458,6 +459,12 @@ def task(ctx, config):
|
|
458
459
|
are welcome to add support for other distros.
|
459
460
|
|
460
461
|
|
462
|
+
Enable Fedora copr repositories using enable_coprs:
|
463
|
+
|
464
|
+
- install:
|
465
|
+
enable_coprs: [ceph/el9]
|
466
|
+
|
467
|
+
|
461
468
|
Overrides are project specific:
|
462
469
|
|
463
470
|
overrides:
|
@@ -542,8 +549,8 @@ def task(ctx, config):
|
|
542
549
|
sha1: 1234
|
543
550
|
|
544
551
|
where sha1 matches the --ceph argument. For instance if
|
545
|
-
teuthology-suite is called with --ceph
|
546
|
-
the tip of
|
552
|
+
teuthology-suite is called with --ceph main, the sha1 will be
|
553
|
+
the tip of main. If called with --ceph v0.94.1, the sha1 will be
|
547
554
|
the v0.94.1 (as returned by git rev-parse v0.94.1 which is not to
|
548
555
|
be confused with git rev-parse v0.94.1^{commit})
|
549
556
|
|
@@ -559,11 +566,25 @@ def task(ctx, config):
|
|
559
566
|
log.debug('project %s' % project)
|
560
567
|
overrides = ctx.config.get('overrides')
|
561
568
|
repos = None
|
569
|
+
|
562
570
|
if overrides:
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
571
|
+
try:
|
572
|
+
install_overrides = overrides.get('install', {})
|
573
|
+
log.debug('INSTALL overrides: %s' % install_overrides)
|
574
|
+
teuthology.deep_merge(config, install_overrides.get(project, {}))
|
575
|
+
overrides_extra_system_packages = install_overrides.get('extra_system_packages')
|
576
|
+
if overrides_extra_system_packages:
|
577
|
+
extra_system_packages = config.get('extra_system_packages')
|
578
|
+
config['extra_system_packages'] = teuthology.deep_merge(extra_system_packages, overrides_extra_system_packages)
|
579
|
+
repos = install_overrides.get('repos', None)
|
580
|
+
except AssertionError:
|
581
|
+
raise ConfigError(
|
582
|
+
"'install' task config and its overrides contain" \
|
583
|
+
"conflicting types for the same config key. Ensure that " \
|
584
|
+
"the configuration is of the same type (dict, list, etc.) " \
|
585
|
+
"in both the task definition and its overrides."
|
586
|
+
)
|
587
|
+
|
567
588
|
log.debug('config %s' % config)
|
568
589
|
|
569
590
|
rhbuild = None
|
@@ -599,6 +620,7 @@ def task(ctx, config):
|
|
599
620
|
extra_packages=config.get('extra_packages', []),
|
600
621
|
extra_system_packages=config.get('extra_system_packages', []),
|
601
622
|
extras=config.get('extras', None),
|
623
|
+
enable_coprs=config.get('enable_coprs', []),
|
602
624
|
flavor=flavor,
|
603
625
|
install_ceph_packages=config.get('install_ceph_packages', True),
|
604
626
|
packages=config.get('packages', dict()),
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# If we're running as root, allow large amounts of open files.
|
3
|
+
USER=$(whoami)
|
4
|
+
|
5
|
+
# If a ulimit call fails, exit immediately.
|
6
|
+
set -e
|
7
|
+
|
8
|
+
if [ "$USER" = "root" ]
|
9
|
+
then
|
10
|
+
# Enable large number of open files
|
11
|
+
ulimit -n 65536
|
12
|
+
fi
|
13
|
+
|
14
|
+
# Enable core dumps for everything
|
15
|
+
ulimit -c unlimited
|
16
|
+
exec "$@"
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#!/usr/bin/python3
|
2
|
+
|
3
|
+
"""
|
4
|
+
Helper script for running long-living processes.
|
5
|
+
|
6
|
+
(Name says daemon, but that is intended to mean "long-living", we
|
7
|
+
assume child process does not double-fork.)
|
8
|
+
|
9
|
+
We start the command passed as arguments, with /dev/null as stdin, and
|
10
|
+
then wait for EOF on stdin.
|
11
|
+
|
12
|
+
When EOF is seen on stdin, the child process is killed.
|
13
|
+
|
14
|
+
When the child process exits, this helper exits too.
|
15
|
+
|
16
|
+
Usage:
|
17
|
+
daemon-helper <signal> [--kill-group] [nostdin] COMMAND ...
|
18
|
+
"""
|
19
|
+
|
20
|
+
from __future__ import print_function
|
21
|
+
|
22
|
+
import fcntl
|
23
|
+
import os
|
24
|
+
import select
|
25
|
+
import signal
|
26
|
+
import struct
|
27
|
+
import subprocess
|
28
|
+
import sys
|
29
|
+
from argparse import ArgumentParser
|
30
|
+
|
31
|
+
parser = ArgumentParser(epilog=
|
32
|
+
'The remaining parameters are the command to be run. If these\n' +
|
33
|
+
'parameters start wih nostdin, then no stdin input is expected.')
|
34
|
+
parser.add_argument('signal')
|
35
|
+
parser.add_argument('--kill-group', action='store_true',
|
36
|
+
help='kill all processes in the group')
|
37
|
+
parser.add_argument('--nostdin', action='store_true',
|
38
|
+
help='no stdin input expected')
|
39
|
+
parsed, args = parser.parse_known_args()
|
40
|
+
end_signal = signal.SIGKILL
|
41
|
+
if parsed.signal == 'term':
|
42
|
+
end_signal = signal.SIGTERM
|
43
|
+
group = parsed.kill_group
|
44
|
+
nostdin = parsed.nostdin
|
45
|
+
skip_nostdin = 0
|
46
|
+
try:
|
47
|
+
if args[0] == 'nostdin':
|
48
|
+
nostdin = True
|
49
|
+
skip_nostdin = 1
|
50
|
+
except IndexError:
|
51
|
+
print('No command specified')
|
52
|
+
sys.exit(1)
|
53
|
+
|
54
|
+
|
55
|
+
proc = None
|
56
|
+
if nostdin:
|
57
|
+
if len(args) - skip_nostdin == 0:
|
58
|
+
print('No command specified')
|
59
|
+
sys.exit(1)
|
60
|
+
proc = subprocess.Popen(
|
61
|
+
args=args[skip_nostdin:],
|
62
|
+
)
|
63
|
+
else:
|
64
|
+
with open('/dev/null', 'rb') as devnull:
|
65
|
+
proc = subprocess.Popen(
|
66
|
+
args=args,
|
67
|
+
stdin=devnull,
|
68
|
+
preexec_fn=os.setsid,
|
69
|
+
)
|
70
|
+
|
71
|
+
flags = fcntl.fcntl(0, fcntl.F_GETFL)
|
72
|
+
fcntl.fcntl(0, fcntl.F_SETFL, flags | os.O_NDELAY)
|
73
|
+
|
74
|
+
saw_eof = False
|
75
|
+
while True:
|
76
|
+
r,w,x = select.select([0], [], [0], 0.2)
|
77
|
+
if r:
|
78
|
+
data = os.read(0, 1)
|
79
|
+
if not data:
|
80
|
+
saw_eof = True
|
81
|
+
if not group:
|
82
|
+
proc.send_signal(end_signal)
|
83
|
+
else:
|
84
|
+
os.killpg(proc.pid, end_signal)
|
85
|
+
break
|
86
|
+
else:
|
87
|
+
sig, = struct.unpack('!b', data)
|
88
|
+
if not group:
|
89
|
+
proc.send_signal(sig)
|
90
|
+
else:
|
91
|
+
os.killpg(proc.pid, end_signal)
|
92
|
+
|
93
|
+
|
94
|
+
if proc.poll() is not None:
|
95
|
+
# child exited
|
96
|
+
break
|
97
|
+
|
98
|
+
exitstatus = proc.wait()
|
99
|
+
if exitstatus > 0:
|
100
|
+
print('{me}: command failed with exit status {exitstatus:d}'.format(
|
101
|
+
me=os.path.basename(sys.argv[0]),
|
102
|
+
exitstatus=exitstatus,
|
103
|
+
), file=sys.stderr)
|
104
|
+
sys.exit(exitstatus)
|
105
|
+
elif exitstatus < 0:
|
106
|
+
if saw_eof and exitstatus == -end_signal:
|
107
|
+
# suppress error from the exit we intentionally caused
|
108
|
+
pass
|
109
|
+
else:
|
110
|
+
print('{me}: command crashed with signal {signal:d}'.format(
|
111
|
+
me=os.path.basename(sys.argv[0]),
|
112
|
+
signal=-exitstatus,
|
113
|
+
), file=sys.stderr)
|
114
|
+
sys.exit(1)
|
@@ -0,0 +1,263 @@
|
|
1
|
+
#!/bin/python3
|
2
|
+
|
3
|
+
# Forward stdin to a subcommand. If EOF is read from stdin or
|
4
|
+
# stdin/stdout/stderr are closed or hungup, then give the command "timeout"
|
5
|
+
# seconds to complete before it is killed.
|
6
|
+
#
|
7
|
+
# The command is run in a separate process group. This is mostly to simplify
|
8
|
+
# killing the set of processes (if well-behaving). You can configure that with
|
9
|
+
# --setpgrp switch.
|
10
|
+
|
11
|
+
# usage: stdin-killer [-h] [--timeout TIMEOUT] [--debug DEBUG] [--signal SIGNAL] [--verbose] [--setpgrp {no,self,child}] command [arguments ...]
|
12
|
+
#
|
13
|
+
# wait for stdin EOF then kill forked subcommand
|
14
|
+
#
|
15
|
+
# positional arguments:
|
16
|
+
# command command to execute
|
17
|
+
# arguments arguments to command
|
18
|
+
#
|
19
|
+
# options:
|
20
|
+
# -h, --help show this help message and exit
|
21
|
+
# --timeout TIMEOUT time to wait for forked subcommand to willing terminate
|
22
|
+
# --debug DEBUG debug file
|
23
|
+
# --signal SIGNAL signal to send
|
24
|
+
# --verbose increase debugging
|
25
|
+
# --setpgrp {no,self,child}
|
26
|
+
# create process group
|
27
|
+
|
28
|
+
|
29
|
+
import argparse
|
30
|
+
import fcntl
|
31
|
+
import logging
|
32
|
+
import os
|
33
|
+
import select
|
34
|
+
import signal
|
35
|
+
import struct
|
36
|
+
import subprocess
|
37
|
+
import sys
|
38
|
+
import time
|
39
|
+
|
40
|
+
NAME = "stdin-killer"
|
41
|
+
|
42
|
+
log = logging.getLogger(NAME)
|
43
|
+
PAGE_SIZE = 4096
|
44
|
+
|
45
|
+
POLL_HANGUP = select.POLLHUP | (select.POLLRDHUP if hasattr(select, 'POLLRDHUP') else 0) | select.POLLERR
|
46
|
+
|
47
|
+
|
48
|
+
def handle_event(poll, buffer, fd, event, p):
|
49
|
+
if sigfdr == fd:
|
50
|
+
b = os.read(sigfdr, 1)
|
51
|
+
(signum,) = struct.unpack("B", b)
|
52
|
+
log.debug("got signal %d", signum)
|
53
|
+
try:
|
54
|
+
p.wait(timeout=0)
|
55
|
+
return True
|
56
|
+
except subprocess.TimeoutExpired:
|
57
|
+
pass
|
58
|
+
elif 0 == fd:
|
59
|
+
if event & POLL_HANGUP:
|
60
|
+
log.debug("peer closed connection, waiting for process exit")
|
61
|
+
poll.unregister(0)
|
62
|
+
sys.stdin.close()
|
63
|
+
if len(buffer) == 0 and p.stdin is not None:
|
64
|
+
p.stdin.close()
|
65
|
+
p.stdin = None
|
66
|
+
return True
|
67
|
+
elif event & select.POLLIN:
|
68
|
+
b = os.read(0, PAGE_SIZE)
|
69
|
+
if b == b"":
|
70
|
+
log.debug("read EOF")
|
71
|
+
poll.unregister(0)
|
72
|
+
sys.stdin.close()
|
73
|
+
if len(buffer) == 0:
|
74
|
+
p.stdin.close()
|
75
|
+
return True
|
76
|
+
if p.stdin is not None:
|
77
|
+
buffer += b
|
78
|
+
# ignore further POLLIN until buffer is written to p.stdin
|
79
|
+
poll.register(0, POLL_HANGUP)
|
80
|
+
poll.register(p.stdin.fileno(), select.POLLOUT)
|
81
|
+
elif p.stdin is not None and p.stdin.fileno() == fd:
|
82
|
+
assert event & select.POLLOUT
|
83
|
+
b = buffer[:PAGE_SIZE]
|
84
|
+
log.debug("sending %d bytes to process", len(b))
|
85
|
+
try:
|
86
|
+
n = p.stdin.write(b)
|
87
|
+
p.stdin.flush()
|
88
|
+
log.debug("wrote %d bytes", n)
|
89
|
+
buffer = buffer[n:]
|
90
|
+
poll.register(0, select.POLLIN | POLL_HANGUP)
|
91
|
+
poll.unregister(p.stdin.fileno())
|
92
|
+
except BrokenPipeError:
|
93
|
+
log.debug("got SIGPIPE")
|
94
|
+
poll.unregister(p.stdin.fileno())
|
95
|
+
p.stdin.close()
|
96
|
+
p.stdin = None
|
97
|
+
return True
|
98
|
+
except BlockingIOError:
|
99
|
+
poll.register(p.stdin.fileno(), select.POLLOUT | POLL_HANGUP)
|
100
|
+
elif 1 == fd:
|
101
|
+
assert event & POLL_HANGUP
|
102
|
+
log.debug("stdout pipe has closed")
|
103
|
+
poll.unregister(1)
|
104
|
+
return True
|
105
|
+
elif 2 == fd:
|
106
|
+
assert event & POLL_HANGUP
|
107
|
+
log.debug("stderr pipe has closed")
|
108
|
+
poll.unregister(2)
|
109
|
+
return True
|
110
|
+
else:
|
111
|
+
assert False
|
112
|
+
return False
|
113
|
+
|
114
|
+
|
115
|
+
def listen_for_events(sigfdr, p, timeout):
|
116
|
+
poll = select.poll()
|
117
|
+
# listen for data on stdin
|
118
|
+
poll.register(0, select.POLLIN | POLL_HANGUP)
|
119
|
+
# listen for stdout/stderr to be closed, if they are closed then my parent
|
120
|
+
# is gone and I should expire the command and myself.
|
121
|
+
poll.register(1, POLL_HANGUP)
|
122
|
+
poll.register(2, POLL_HANGUP)
|
123
|
+
# for SIGCHLD
|
124
|
+
poll.register(sigfdr, select.POLLIN)
|
125
|
+
buffer = bytearray()
|
126
|
+
expired = 0.0
|
127
|
+
while True:
|
128
|
+
if expired > 0.0:
|
129
|
+
since = time.monotonic() - expired
|
130
|
+
wait = int((timeout - since) * 1000.0)
|
131
|
+
if wait <= 0:
|
132
|
+
return
|
133
|
+
else:
|
134
|
+
wait = 5000
|
135
|
+
log.debug("polling for %d milliseconds", wait)
|
136
|
+
events = poll.poll(wait)
|
137
|
+
for fd, event in events:
|
138
|
+
log.debug("event: (%d, %d)", fd, event)
|
139
|
+
if handle_event(poll, buffer, fd, event, p):
|
140
|
+
if p.returncode is not None:
|
141
|
+
return
|
142
|
+
if expired == 0.0:
|
143
|
+
expired = time.monotonic()
|
144
|
+
log.info(
|
145
|
+
"expiration expected; waiting %d seconds for command to complete",
|
146
|
+
NS.timeout,
|
147
|
+
)
|
148
|
+
|
149
|
+
|
150
|
+
if __name__ == "__main__":
|
151
|
+
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
152
|
+
try:
|
153
|
+
(sigfdr, sigfdw) = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC)
|
154
|
+
except AttributeError:
|
155
|
+
# pipe2 is only available on "some flavors of Unix"
|
156
|
+
# https://docs.python.org/3.10/library/os.html?highlight=pipe2#os.pipe2
|
157
|
+
pipe_ends = os.pipe()
|
158
|
+
for fd in pipe_ends:
|
159
|
+
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
160
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK | os.O_CLOEXEC)
|
161
|
+
(sigfdr, sigfdw) = pipe_ends
|
162
|
+
|
163
|
+
signal.set_wakeup_fd(sigfdw)
|
164
|
+
|
165
|
+
def do_nothing(signum, frame):
|
166
|
+
pass
|
167
|
+
|
168
|
+
signal.signal(signal.SIGCHLD, do_nothing)
|
169
|
+
|
170
|
+
P = argparse.ArgumentParser(
|
171
|
+
description="wait for stdin EOF then kill forked subcommand"
|
172
|
+
)
|
173
|
+
P.add_argument(
|
174
|
+
"--timeout",
|
175
|
+
action="store",
|
176
|
+
default=5,
|
177
|
+
help="time to wait for forked subcommand to willing terminate",
|
178
|
+
type=int,
|
179
|
+
)
|
180
|
+
P.add_argument("--debug", action="store", help="debug file", type=str)
|
181
|
+
P.add_argument(
|
182
|
+
"--signal",
|
183
|
+
action="store",
|
184
|
+
help="signal to send",
|
185
|
+
type=int,
|
186
|
+
default=signal.SIGKILL,
|
187
|
+
)
|
188
|
+
P.add_argument("--verbose", action="store_true", help="increase debugging")
|
189
|
+
P.add_argument(
|
190
|
+
"--setpgrp",
|
191
|
+
action="store",
|
192
|
+
choices=["no", "self", "child"],
|
193
|
+
default="self",
|
194
|
+
help="create process group",
|
195
|
+
)
|
196
|
+
P.add_argument(
|
197
|
+
"cmd", metavar="command", type=str, nargs=1, help="command to execute"
|
198
|
+
)
|
199
|
+
P.add_argument(
|
200
|
+
"args", metavar="arguments", type=str, nargs="*", help="arguments to command"
|
201
|
+
)
|
202
|
+
NS = P.parse_args()
|
203
|
+
|
204
|
+
logargs = {}
|
205
|
+
if NS.debug is not None:
|
206
|
+
logargs["filename"] = NS.debug
|
207
|
+
else:
|
208
|
+
logargs["stream"] = sys.stderr
|
209
|
+
if NS.verbose:
|
210
|
+
logargs["level"] = logging.DEBUG
|
211
|
+
else:
|
212
|
+
logargs["level"] = logging.INFO
|
213
|
+
logargs["format"] = f"%(asctime)s {NAME} %(levelname)s: %(message)s"
|
214
|
+
logargs["datefmt"] = "%Y-%m-%dT%H:%M:%S"
|
215
|
+
logging.basicConfig(**logargs)
|
216
|
+
|
217
|
+
cargs = NS.cmd + NS.args
|
218
|
+
popen_kwargs = {
|
219
|
+
"stdin": subprocess.PIPE,
|
220
|
+
}
|
221
|
+
|
222
|
+
if NS.setpgrp == "self":
|
223
|
+
pgrp = os.getpgrp()
|
224
|
+
if pgrp != os.getpid():
|
225
|
+
os.setpgrp()
|
226
|
+
pgrp = os.getpgrp()
|
227
|
+
elif NS.setpgrp == "child":
|
228
|
+
popen_kwargs["preexec_fn"] = os.setpgrp
|
229
|
+
pgrp = None
|
230
|
+
elif NS.setpgrp == "no":
|
231
|
+
pgrp = 0
|
232
|
+
else:
|
233
|
+
assert False
|
234
|
+
|
235
|
+
log.debug("executing %s", cargs)
|
236
|
+
p = subprocess.Popen(cargs, **popen_kwargs)
|
237
|
+
if pgrp is None:
|
238
|
+
pgrp = p.pid
|
239
|
+
flags = fcntl.fcntl(p.stdin.fileno(), fcntl.F_GETFL)
|
240
|
+
fcntl.fcntl(p.stdin.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
241
|
+
|
242
|
+
listen_for_events(sigfdr, p, NS.timeout)
|
243
|
+
|
244
|
+
if p.returncode is None:
|
245
|
+
log.error("timeout expired: sending signal %d to command and myself", NS.signal)
|
246
|
+
if pgrp == 0:
|
247
|
+
os.kill(p.pid, NS.signal)
|
248
|
+
else:
|
249
|
+
os.killpg(pgrp, NS.signal) # should kill me too
|
250
|
+
os.kill(os.getpid(), NS.signal) # to exit abnormally with same signal
|
251
|
+
log.error("signal did not cause termination, sending myself SIGKILL")
|
252
|
+
os.kill(os.getpid(), signal.SIGKILL) # failsafe
|
253
|
+
rc = p.returncode
|
254
|
+
log.debug("rc = %d", rc)
|
255
|
+
assert rc is not None
|
256
|
+
if rc < 0:
|
257
|
+
log.error("command terminated with signal %d: sending same signal to myself!", -rc)
|
258
|
+
os.kill(os.getpid(), -rc) # kill myself with the same signal
|
259
|
+
log.error("signal did not cause termination, sending myself SIGKILL")
|
260
|
+
os.kill(os.getpid(), signal.SIGKILL) # failsafe
|
261
|
+
else:
|
262
|
+
log.info("command exited with status %d: exiting normally with same code!", rc)
|
263
|
+
sys.exit(rc)
|