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.
Files changed (168) hide show
  1. scripts/describe.py +1 -0
  2. scripts/dispatcher.py +55 -26
  3. scripts/exporter.py +18 -0
  4. scripts/lock.py +1 -1
  5. scripts/node_cleanup.py +58 -0
  6. scripts/openstack.py +9 -9
  7. scripts/results.py +12 -11
  8. scripts/schedule.py +4 -0
  9. scripts/suite.py +57 -16
  10. scripts/supervisor.py +44 -0
  11. scripts/update_inventory.py +10 -4
  12. teuthology/__init__.py +24 -26
  13. teuthology/beanstalk.py +4 -3
  14. teuthology/config.py +16 -6
  15. teuthology/contextutil.py +18 -14
  16. teuthology/describe_tests.py +25 -18
  17. teuthology/dispatcher/__init__.py +210 -35
  18. teuthology/dispatcher/supervisor.py +140 -58
  19. teuthology/exceptions.py +43 -0
  20. teuthology/exporter.py +347 -0
  21. teuthology/kill.py +76 -81
  22. teuthology/lock/cli.py +3 -3
  23. teuthology/lock/ops.py +135 -61
  24. teuthology/lock/query.py +61 -44
  25. teuthology/ls.py +1 -1
  26. teuthology/misc.py +61 -75
  27. teuthology/nuke/__init__.py +12 -353
  28. teuthology/openstack/__init__.py +4 -3
  29. teuthology/openstack/openstack-centos-7.0-user-data.txt +1 -1
  30. teuthology/openstack/openstack-centos-7.1-user-data.txt +1 -1
  31. teuthology/openstack/openstack-centos-7.2-user-data.txt +1 -1
  32. teuthology/openstack/openstack-debian-8.0-user-data.txt +1 -1
  33. teuthology/openstack/openstack-opensuse-42.1-user-data.txt +1 -1
  34. teuthology/openstack/openstack-teuthology.cron +0 -1
  35. teuthology/orchestra/cluster.py +49 -7
  36. teuthology/orchestra/connection.py +16 -5
  37. teuthology/orchestra/console.py +111 -50
  38. teuthology/orchestra/daemon/cephadmunit.py +17 -4
  39. teuthology/orchestra/daemon/state.py +8 -1
  40. teuthology/orchestra/daemon/systemd.py +4 -4
  41. teuthology/orchestra/opsys.py +30 -11
  42. teuthology/orchestra/remote.py +405 -338
  43. teuthology/orchestra/run.py +3 -3
  44. teuthology/packaging.py +19 -16
  45. teuthology/provision/__init__.py +30 -10
  46. teuthology/provision/cloud/openstack.py +12 -6
  47. teuthology/provision/cloud/util.py +1 -2
  48. teuthology/provision/downburst.py +4 -3
  49. teuthology/provision/fog.py +68 -20
  50. teuthology/provision/openstack.py +5 -4
  51. teuthology/provision/pelagos.py +1 -1
  52. teuthology/repo_utils.py +43 -13
  53. teuthology/report.py +57 -35
  54. teuthology/results.py +5 -3
  55. teuthology/run.py +13 -14
  56. teuthology/run_tasks.py +27 -43
  57. teuthology/schedule.py +4 -3
  58. teuthology/scrape.py +28 -22
  59. teuthology/suite/__init__.py +74 -45
  60. teuthology/suite/build_matrix.py +34 -24
  61. teuthology/suite/fragment-merge.lua +105 -0
  62. teuthology/suite/matrix.py +31 -2
  63. teuthology/suite/merge.py +175 -0
  64. teuthology/suite/placeholder.py +6 -9
  65. teuthology/suite/run.py +175 -100
  66. teuthology/suite/util.py +64 -218
  67. teuthology/task/__init__.py +1 -1
  68. teuthology/task/ansible.py +101 -32
  69. teuthology/task/buildpackages.py +2 -2
  70. teuthology/task/ceph_ansible.py +13 -6
  71. teuthology/task/cephmetrics.py +2 -1
  72. teuthology/task/clock.py +33 -14
  73. teuthology/task/exec.py +18 -0
  74. teuthology/task/hadoop.py +2 -2
  75. teuthology/task/install/__init__.py +29 -7
  76. teuthology/task/install/bin/adjust-ulimits +16 -0
  77. teuthology/task/install/bin/daemon-helper +114 -0
  78. teuthology/task/install/bin/stdin-killer +263 -0
  79. teuthology/task/install/deb.py +1 -1
  80. teuthology/task/install/rpm.py +17 -5
  81. teuthology/task/install/util.py +3 -3
  82. teuthology/task/internal/__init__.py +41 -10
  83. teuthology/task/internal/edit_sudoers.sh +10 -0
  84. teuthology/task/internal/lock_machines.py +2 -9
  85. teuthology/task/internal/redhat.py +31 -1
  86. teuthology/task/internal/syslog.py +31 -8
  87. teuthology/task/kernel.py +152 -145
  88. teuthology/task/lockfile.py +1 -1
  89. teuthology/task/mpi.py +10 -10
  90. teuthology/task/pcp.py +1 -1
  91. teuthology/task/selinux.py +16 -8
  92. teuthology/task/ssh_keys.py +4 -4
  93. teuthology/task/tests/__init__.py +137 -77
  94. teuthology/task/tests/test_fetch_coredumps.py +116 -0
  95. teuthology/task/tests/test_run.py +4 -4
  96. teuthology/timer.py +3 -3
  97. teuthology/util/loggerfile.py +19 -0
  98. teuthology/util/scanner.py +159 -0
  99. teuthology/util/sentry.py +52 -0
  100. teuthology/util/time.py +52 -0
  101. teuthology-1.2.0.data/scripts/adjust-ulimits +16 -0
  102. teuthology-1.2.0.data/scripts/daemon-helper +114 -0
  103. teuthology-1.2.0.data/scripts/stdin-killer +263 -0
  104. teuthology-1.2.0.dist-info/METADATA +89 -0
  105. teuthology-1.2.0.dist-info/RECORD +174 -0
  106. {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/WHEEL +1 -1
  107. {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/entry_points.txt +3 -2
  108. scripts/nuke.py +0 -47
  109. scripts/worker.py +0 -37
  110. teuthology/nuke/actions.py +0 -456
  111. teuthology/openstack/test/__init__.py +0 -0
  112. teuthology/openstack/test/openstack-integration.py +0 -286
  113. teuthology/openstack/test/test_config.py +0 -35
  114. teuthology/openstack/test/test_openstack.py +0 -1695
  115. teuthology/orchestra/test/__init__.py +0 -0
  116. teuthology/orchestra/test/integration/__init__.py +0 -0
  117. teuthology/orchestra/test/integration/test_integration.py +0 -94
  118. teuthology/orchestra/test/test_cluster.py +0 -240
  119. teuthology/orchestra/test/test_connection.py +0 -106
  120. teuthology/orchestra/test/test_console.py +0 -217
  121. teuthology/orchestra/test/test_opsys.py +0 -404
  122. teuthology/orchestra/test/test_remote.py +0 -185
  123. teuthology/orchestra/test/test_run.py +0 -286
  124. teuthology/orchestra/test/test_systemd.py +0 -54
  125. teuthology/orchestra/test/util.py +0 -12
  126. teuthology/test/__init__.py +0 -0
  127. teuthology/test/fake_archive.py +0 -107
  128. teuthology/test/fake_fs.py +0 -92
  129. teuthology/test/integration/__init__.py +0 -0
  130. teuthology/test/integration/test_suite.py +0 -86
  131. teuthology/test/task/__init__.py +0 -205
  132. teuthology/test/task/test_ansible.py +0 -624
  133. teuthology/test/task/test_ceph_ansible.py +0 -176
  134. teuthology/test/task/test_console_log.py +0 -88
  135. teuthology/test/task/test_install.py +0 -337
  136. teuthology/test/task/test_internal.py +0 -57
  137. teuthology/test/task/test_kernel.py +0 -243
  138. teuthology/test/task/test_pcp.py +0 -379
  139. teuthology/test/task/test_selinux.py +0 -35
  140. teuthology/test/test_config.py +0 -189
  141. teuthology/test/test_contextutil.py +0 -68
  142. teuthology/test/test_describe_tests.py +0 -316
  143. teuthology/test/test_email_sleep_before_teardown.py +0 -81
  144. teuthology/test/test_exit.py +0 -97
  145. teuthology/test/test_get_distro.py +0 -47
  146. teuthology/test/test_get_distro_version.py +0 -47
  147. teuthology/test/test_get_multi_machine_types.py +0 -27
  148. teuthology/test/test_job_status.py +0 -60
  149. teuthology/test/test_ls.py +0 -48
  150. teuthology/test/test_misc.py +0 -391
  151. teuthology/test/test_nuke.py +0 -290
  152. teuthology/test/test_packaging.py +0 -763
  153. teuthology/test/test_parallel.py +0 -28
  154. teuthology/test/test_repo_utils.py +0 -225
  155. teuthology/test/test_report.py +0 -77
  156. teuthology/test/test_results.py +0 -155
  157. teuthology/test/test_run.py +0 -239
  158. teuthology/test/test_safepath.py +0 -55
  159. teuthology/test/test_schedule.py +0 -45
  160. teuthology/test/test_scrape.py +0 -167
  161. teuthology/test/test_timer.py +0 -80
  162. teuthology/test/test_vps_os_vers_parameter_checking.py +0 -84
  163. teuthology/test/test_worker.py +0 -303
  164. teuthology/worker.py +0 -354
  165. teuthology-1.1.0.dist-info/METADATA +0 -76
  166. teuthology-1.1.0.dist-info/RECORD +0 -213
  167. {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/LICENSE +0 -0
  168. {teuthology-1.1.0.dist-info → teuthology-1.2.0.dist-info}/top_level.txt +0 -0
@@ -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 master
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', 'master')
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 teuth_config.get('ceph_ansible') and \
300
- self.ctx.machine_type in teuth_config['ceph_ansible']['has_lvm_scratch_disks']:
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 = 'master'
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)
@@ -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
- for rem in ctx.cluster.remotes.keys():
34
- rem.run(
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
- for rem in ctx.cluster.remotes.keys():
61
- rem.run(
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
- for rem in ctx.cluster.remotes.keys():
81
- rem.run(
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
- for rem in ctx.cluster.remotes.keys():
96
- rem.run(
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
@@ -259,8 +259,8 @@ def install_hadoop(ctx, config):
259
259
  format = "jar",
260
260
  dist = "precise",
261
261
  arch = "x86_64",
262
- flavor = "basic",
263
- branch = "master")
262
+ flavor = "default",
263
+ branch = "main")
264
264
 
265
265
  run.wait(
266
266
  hadoops.run(
@@ -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.warn('remote %s came up twice (role %s)', remote, role)
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 master, the sha1 will be
546
- the tip of master. If called with --ceph v0.94.1, the sha1 will be
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
- install_overrides = overrides.get('install', {})
564
- teuthology.deep_merge(config, install_overrides.get(project, {}))
565
- repos = install_overrides.get('repos', None)
566
- log.debug('INSTALL overrides: %s' % install_overrides)
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)