atex 0.4__py3-none-any.whl → 0.7__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.
atex/__init__.py CHANGED
@@ -18,18 +18,8 @@ def __dir__():
18
18
 
19
19
  # lazily import submodules
20
20
  def __getattr__(attr):
21
- # # from mod import *
22
- # if attr == '__all__':
23
- # print("importing all")
24
- # for mod in __all__:
25
- # _importlib.import_module(f'.{mod}', __name__)
26
- # return __all__
27
- # # accessing __all__, __getattr__, etc. directly
28
- # elif attr in globals():
29
- # print("importing globals")
30
- # return globals()[attr]
31
21
  # importing a module known to exist
32
22
  if attr in __all__:
33
- return _importlib.import_module(f'.{attr}', __name__)
23
+ return _importlib.import_module(f".{attr}", __name__)
34
24
  else:
35
- raise AttributeError(f'module {__name__} has no attribute {attr}')
25
+ raise AttributeError(f"module '{__name__}' has no attribute '{attr}'")
atex/cli/__init__.py CHANGED
@@ -32,16 +32,16 @@ def setup_logging(level):
32
32
  logging.basicConfig(
33
33
  level=level,
34
34
  stream=sys.stderr,
35
- format='%(asctime)s %(name)s: %(message)s',
36
- datefmt='%Y-%m-%d %H:%M:%S',
35
+ format="%(asctime)s %(name)s: %(message)s",
36
+ datefmt="%Y-%m-%d %H:%M:%S",
37
37
  )
38
38
 
39
39
 
40
40
  def collect_modules():
41
41
  for info in pkgutil.iter_modules(__spec__.submodule_search_locations):
42
- mod = importlib.import_module(f'.{info.name}', __name__)
43
- if not hasattr(mod, 'CLI_SPEC'):
44
- raise ValueError(f"CLI submodule {info.name} does not define CLI_SPEC")
42
+ mod = importlib.import_module(f".{info.name}", __name__)
43
+ if not hasattr(mod, "CLI_SPEC"):
44
+ raise ValueError(f"CLI submodule '{info.name}' does not define CLI_SPEC")
45
45
  yield (info.name, mod.CLI_SPEC)
46
46
 
47
47
 
@@ -50,28 +50,28 @@ def main():
50
50
 
51
51
  log_grp = parser.add_mutually_exclusive_group()
52
52
  log_grp.add_argument(
53
- '--debug', '-d', action='store_const', dest='loglevel', const=logging.DEBUG,
53
+ "--debug", "-d", action="store_const", dest="loglevel", const=logging.DEBUG,
54
54
  help="enable extra debugging (logging.DEBUG)",
55
55
  )
56
56
  log_grp.add_argument(
57
- '--quiet', '-q', action='store_const', dest='loglevel', const=logging.WARNING,
57
+ "--quiet", "-q", action="store_const", dest="loglevel", const=logging.WARNING,
58
58
  help="be quiet during normal operation (logging.WARNING)",
59
59
  )
60
60
  parser.set_defaults(loglevel=logging.INFO)
61
61
 
62
62
  mains = {}
63
- subparsers = parser.add_subparsers(dest='_module', metavar='<module>', required=True)
63
+ subparsers = parser.add_subparsers(dest="_module", metavar="<module>", required=True)
64
64
  for name, spec in collect_modules():
65
- aliases = spec['aliases'] if 'aliases' in spec else ()
65
+ aliases = spec["aliases"] if "aliases" in spec else ()
66
66
  subp = subparsers.add_parser(
67
67
  name,
68
68
  aliases=aliases,
69
- help=spec['help'],
69
+ help=spec["help"],
70
70
  )
71
- spec['args'](subp)
72
- mains[name] = spec['main']
71
+ spec["args"](subp)
72
+ mains[name] = spec["main"]
73
73
  for alias in aliases:
74
- mains[alias] = spec['main']
74
+ mains[alias] = spec["main"]
75
75
 
76
76
  args = parser.parse_args()
77
77
 
atex/cli/minitmt.py CHANGED
@@ -1,82 +1,175 @@
1
- import re
1
+ import sys
2
+ #import re
2
3
  import pprint
4
+ #import subprocess
5
+ from pathlib import Path
3
6
 
4
- #from .. import util
5
- from ..minitmt import fmf
7
+ from .. import connection, provision, minitmt
8
+ from ..orchestrator import aggregator
9
+
10
+
11
+ def _fatal(msg):
12
+ print(msg, file=sys.stderr)
13
+ sys.exit(1)
6
14
 
7
15
 
8
16
  def _get_context(args):
9
17
  context = {}
10
18
  if args.context:
11
19
  for c in args.context:
12
- key, value = c.split('=', 1)
20
+ key, value = c.split("=", 1)
13
21
  context[key] = value
14
22
  return context or None
15
23
 
16
24
 
17
25
  def discover(args):
18
- result = fmf.FMFData(args.root, args.plan, context=_get_context(args))
19
- for test in result.tests:
20
- print(test.name)
26
+ result = minitmt.fmf.FMFTests(args.root, args.plan, context=_get_context(args))
27
+ for name in result.tests:
28
+ print(name)
21
29
 
22
30
 
23
31
  def show(args):
24
- result = fmf.FMFData(args.root, args.plan, context=_get_context(args))
25
- for test in result.tests:
26
- if re.match(args.test, test.name):
32
+ result = minitmt.fmf.FMFTests(args.root, args.plan, context=_get_context(args))
33
+ if tests := list(result.match(args.test)):
34
+ for test in tests:
35
+ print(f"\n--- {test.name} ---")
27
36
  pprint.pprint(test.data)
28
- break
29
37
  else:
38
+ _fatal(f"Not reachable via {args.plan} discovery: {args.test}")
39
+
40
+
41
+ def execute(args):
42
+ # remote system connection
43
+ ssh_keypath = Path(args.ssh_identity)
44
+ if not ssh_keypath.exists():
45
+ _fatal(f"SSH Identity {args.ssh_identity} does not exist")
46
+ ssh_options = {
47
+ "User": args.user,
48
+ "Hostname": args.host,
49
+ "IdentityFile": ssh_keypath,
50
+ }
51
+ env = dict(x.split("=",1) for x in args.env)
52
+
53
+ # dummy Remote that just wraps the connection
54
+ class DummyRemote(provision.Remote, connection.ssh.ManagedSSHConn):
55
+ @staticmethod
56
+ def release():
57
+ return
58
+
59
+ @staticmethod
60
+ def alive():
61
+ return True
62
+
63
+ # result aggregation
64
+ with aggregator.CSVAggregator(args.results_csv, args.results_dir) as csv_aggregator:
65
+ platform_aggregator = csv_aggregator.for_platform(args.platform)
66
+
67
+ # tests discovery and selection
68
+ result = minitmt.fmf.FMFTests(args.root, args.plan, context=_get_context(args))
69
+ if args.test:
70
+ tests = list(result.match(args.test))
71
+ if not tests:
72
+ _fatal(f"Not reachable via plan {args.plan} discovery: {args.test}")
73
+ else:
74
+ tests = list(result.as_fmftests())
75
+ if not tests:
76
+ _fatal(f"No tests found for plan {args.plan}")
77
+
78
+ # test run
79
+ with DummyRemote(ssh_options) as remote:
80
+ executor = minitmt.executor.Executor(remote, platform_aggregator, env=env)
81
+ executor.upload_tests(args.root)
82
+ executor.setup_plan(result)
83
+ for test in tests:
84
+ executor.run_test(test)
85
+
86
+
87
+ def setup_script(args):
88
+ result = minitmt.fmf.FMFTests(args.root, args.plan, context=_get_context(args))
89
+ try:
90
+ test = result.as_fmftest(args.test)
91
+ except KeyError:
30
92
  print(f"Not reachable via {args.plan} discovery: {args.test}")
31
- raise SystemExit(1)
93
+ raise SystemExit(1) from None
94
+ output = minitmt.scripts.test_setup(
95
+ test=test,
96
+ tests_dir=args.remote_root,
97
+ debug=args.script_debug,
98
+ )
99
+ print(output, end="")
32
100
 
33
101
 
34
102
  def parse_args(parser):
35
- parser.add_argument('--root', default='.', help="path to directory with fmf tests")
36
- parser.add_argument('--context', '-c', help="tmt style key=value context", action='append')
103
+ parser.add_argument("--root", help="path to directory with fmf tests", default=".")
104
+ parser.add_argument("--context", "-c", help="tmt style key=value context", action="append")
37
105
  cmds = parser.add_subparsers(
38
- dest='_cmd', help="minitmt feature", metavar='<cmd>', required=True,
106
+ dest="_cmd", help="minitmt feature", metavar="<cmd>", required=True,
39
107
  )
40
108
 
41
109
  cmd = cmds.add_parser(
42
- 'discover', aliases=('di',),
110
+ "discover", aliases=("di",),
43
111
  help="list tests, post-processed by tmt plans",
44
112
  )
45
- cmd.add_argument('plan', help="tmt plan to use for discovery")
113
+ cmd.add_argument("plan", help="tmt plan to use for discovery")
46
114
 
47
115
  cmd = cmds.add_parser(
48
- 'show',
116
+ "show",
49
117
  help="show fmf data of a test",
50
118
  )
51
- cmd.add_argument('plan', help="tmt plan to use for discovery")
52
- cmd.add_argument('test', help="fmf style test regex")
119
+ cmd.add_argument("plan", help="tmt plan to use for discovery")
120
+ cmd.add_argument("test", help="fmf style test regex")
53
121
 
54
122
  cmd = cmds.add_parser(
55
- 'execute', aliases=('ex',),
123
+ "execute", aliases=("ex",),
56
124
  help="run a plan (or test) on a remote system",
57
125
  )
58
- grp = cmd.add_mutually_exclusive_group()
59
- grp.add_argument('--test', '-t', help="fmf style test regex")
60
- grp.add_argument('--plan', '-p', help="tmt plan name (path) inside metadata root")
61
- cmd.add_argument('--ssh-identity', '-i', help="path to a ssh keyfile for login")
62
- cmd.add_argument('user_host', help="ssh style user@host of the remote")
126
+ #grp = cmd.add_mutually_exclusive_group()
127
+ #grp.add_argument("--test", "-t", help="fmf style test regex")
128
+ #grp.add_argument("--plan", "-p", help="tmt plan name (path) inside metadata root")
129
+ cmd.add_argument("--env", "-e", help="environment to pass to prepare/test", action="append")
130
+ cmd.add_argument("--test", "-t", help="fmf style test regex")
131
+ cmd.add_argument(
132
+ "--plan", "-p", help="tmt plan name (path) inside metadata root", required=True,
133
+ )
134
+ cmd.add_argument("--platform", help="platform name, ie. rhel9@x86_64", required=True)
135
+ cmd.add_argument("--user", help="ssh user to connect via", required=True)
136
+ cmd.add_argument("--host", help="ssh host to connect to", required=True)
137
+ cmd.add_argument(
138
+ "--ssh-identity", help="path to a ssh keyfile for login", required=True,
139
+ )
140
+ cmd.add_argument(
141
+ "--results-csv", help="path to would-be-created .csv.gz results", required=True,
142
+ )
143
+ cmd.add_argument(
144
+ "--results-dir", help="path to would-be-created dir for uploaded files", required=True,
145
+ )
146
+
147
+ cmd = cmds.add_parser(
148
+ "setup-script",
149
+ help="generate a script prepping tests for run",
150
+ )
151
+ cmd.add_argument("--remote-root", help="path to tests repo on the remote", required=True)
152
+ cmd.add_argument("--script-debug", help="do 'set -x' in the script", action="store_true")
153
+ cmd.add_argument("plan", help="tmt plan to use for discovery")
154
+ cmd.add_argument("test", help="full fmf test name (not regex)")
63
155
 
64
156
 
65
157
  def main(args):
66
- if args._cmd in ('discover', 'di'):
158
+ if args._cmd in ("discover", "di"):
67
159
  discover(args)
68
- elif args._cmd == 'show':
160
+ elif args._cmd == "show":
69
161
  show(args)
70
- elif args._cmd in ('execute', 'ex'):
71
- #execute(args)
72
- print("not implemented yet")
162
+ elif args._cmd in ("execute", "ex"):
163
+ execute(args)
164
+ elif args._cmd == "setup-script":
165
+ setup_script(args)
73
166
  else:
74
167
  raise RuntimeError(f"unknown args: {args}")
75
168
 
76
169
 
77
170
  CLI_SPEC = {
78
- 'aliases': ('tmt',),
79
- 'help': "simple test executor using atex.minitmt",
80
- 'args': parse_args,
81
- 'main': main,
171
+ "aliases": ("tmt",),
172
+ "help": "simple test executor using atex.minitmt",
173
+ "args": parse_args,
174
+ "main": main,
82
175
  }
atex/cli/testingfarm.py CHANGED
@@ -2,15 +2,15 @@ import sys
2
2
  import pprint
3
3
 
4
4
  from .. import util
5
- from .. import testingfarm as tf
5
+ from ..provision.testingfarm import api as tf
6
6
 
7
7
 
8
8
  def _get_api(args):
9
9
  api_args = {}
10
10
  if args.url:
11
- api_args['url'] = args.url
11
+ api_args["url"] = args.url
12
12
  if args.token:
13
- api_args['token'] = args.token
13
+ api_args["token"] = args.token
14
14
  return tf.TestingFarmAPI(**api_args)
15
15
 
16
16
 
@@ -27,9 +27,9 @@ def whoami(args):
27
27
  def composes(args):
28
28
  api = _get_api(args)
29
29
  comps = api.composes(ranch=args.ranch)
30
- comps_list = comps['composes']
30
+ comps_list = comps["composes"]
31
31
  for comp in comps_list:
32
- print(comp['name'])
32
+ print(comp["name"])
33
33
 
34
34
 
35
35
  def get_request(args):
@@ -56,27 +56,27 @@ def search_requests(args):
56
56
  if not reply:
57
57
  return
58
58
 
59
- for req in sorted(reply, key=lambda x: x['created']):
60
- req_id = req['id']
61
- created = req['created'].partition('.')[0]
59
+ for req in sorted(reply, key=lambda x: x["created"]):
60
+ req_id = req["id"]
61
+ created = req["created"].partition(".")[0]
62
62
 
63
63
  envs = []
64
- for env in req['environments_requested']:
65
- if 'os' in env and env['os'] and 'compose' in env['os']:
66
- compose = env['os']['compose']
67
- arch = env['arch']
64
+ for env in req["environments_requested"]:
65
+ if "os" in env and env["os"] and "compose" in env["os"]:
66
+ compose = env["os"]["compose"]
67
+ arch = env["arch"]
68
68
  if compose and arch:
69
- envs.append(f'{compose}@{arch}')
70
- envs_str = ', '.join(envs)
69
+ envs.append(f"{compose}@{arch}")
70
+ envs_str = ", ".join(envs)
71
71
 
72
- print(f'{created} {req_id} : {envs_str}')
72
+ print(f"{created} {req_id} : {envs_str}")
73
73
 
74
74
 
75
75
  def reserve(args):
76
76
  util.info(f"Reserving {args.compose} on {args.arch} for {args.timeout} minutes")
77
77
 
78
78
  if args.hvm:
79
- hardware = {'virtualization': {'is-supported': True}}
79
+ hardware = {"virtualization": {"is-supported": True}}
80
80
  else:
81
81
  hardware = None
82
82
 
@@ -96,12 +96,12 @@ def reserve(args):
96
96
  res.request.assert_alive()
97
97
  except tf.GoneAwayError as e:
98
98
  print(e)
99
- raise SystemExit(1)
99
+ raise SystemExit(1) from None
100
100
 
101
101
  proc = util.subprocess_run([
102
- 'ssh', '-q', '-i', m.ssh_key,
103
- '-oStrictHostKeyChecking=no', '-oUserKnownHostsFile=/dev/null',
104
- f'{m.user}@{m.host}',
102
+ "ssh", "-q", "-i", m.ssh_key,
103
+ "-oStrictHostKeyChecking=no", "-oUserKnownHostsFile=/dev/null",
104
+ f"{m.user}@{m.host}",
105
105
  ])
106
106
  if proc.returncode != 0:
107
107
  print(
@@ -123,7 +123,7 @@ def watch_pipeline(args):
123
123
 
124
124
  util.info(f"Waiting for {args.request_id} to be 'running'")
125
125
  try:
126
- request.wait_for_state('running')
126
+ request.wait_for_state("running")
127
127
  except tf.GoneAwayError:
128
128
  util.info(f"Request {args.request_id} already finished")
129
129
  return
@@ -132,96 +132,96 @@ def watch_pipeline(args):
132
132
  try:
133
133
  for line in tf.PipelineLogStreamer(request):
134
134
  sys.stdout.write(line)
135
- sys.stdout.write('\n')
135
+ sys.stdout.write("\n")
136
136
  except tf.GoneAwayError:
137
137
  util.info(f"Request {args.request_id} finished, exiting")
138
138
 
139
139
 
140
140
  def parse_args(parser):
141
- parser.add_argument('--url', help="Testing Farm API URL")
142
- parser.add_argument('--token', help="Testing Farm API auth token")
141
+ parser.add_argument("--url", help="Testing Farm API URL")
142
+ parser.add_argument("--token", help="Testing Farm API auth token")
143
143
  cmds = parser.add_subparsers(
144
- dest='_cmd', help="TF helper to run", metavar='<cmd>', required=True,
144
+ dest="_cmd", help="TF helper to run", metavar="<cmd>", required=True,
145
145
  )
146
146
 
147
147
  cmd = cmds.add_parser(
148
- 'whoami',
148
+ "whoami",
149
149
  help="print out details about active TF token",
150
150
  )
151
151
  cmd = cmds.add_parser(
152
- 'about',
152
+ "about",
153
153
  help="print out details about TF instance (url)",
154
154
  )
155
155
 
156
156
  cmd = cmds.add_parser(
157
- 'composes',
157
+ "composes",
158
158
  help="list all composes available on a given ranch",
159
159
  )
160
- cmd.add_argument('ranch', nargs='?', help="Testing Farm ranch (autodetected if token)")
160
+ cmd.add_argument("ranch", nargs="?", help="Testing Farm ranch (autodetected if token)")
161
161
 
162
162
  cmd = cmds.add_parser(
163
- 'get-request', aliases=('gr',),
163
+ "get-request", aliases=("gr",),
164
164
  help="retrieve and print JSON of a Testing Farm request",
165
165
  )
166
- cmd.add_argument('request_id', help="Testing Farm request UUID")
166
+ cmd.add_argument("request_id", help="Testing Farm request UUID")
167
167
 
168
168
  cmd = cmds.add_parser(
169
- 'cancel',
169
+ "cancel",
170
170
  help="cancel a Testing Farm request",
171
171
  )
172
- cmd.add_argument('request_id', help="Testing Farm request UUID")
172
+ cmd.add_argument("request_id", help="Testing Farm request UUID")
173
173
 
174
174
  cmd = cmds.add_parser(
175
- 'search-requests', aliases=('sr',),
175
+ "search-requests", aliases=("sr",),
176
176
  help="return a list of requests matching the criteria",
177
177
  )
178
- cmd.add_argument('--state', help="request state (running, etc.)", required=True)
179
- cmd.add_argument('--all', help="all requests, not just owned by token", action='store_true')
180
- cmd.add_argument('--ranch', help="Testing Farm ranch")
181
- cmd.add_argument('--before', help="only requests created before ISO8601")
182
- cmd.add_argument('--after', help="only requests created after ISO8601")
178
+ cmd.add_argument("--state", help="request state (running, etc.)", required=True)
179
+ cmd.add_argument("--all", help="all requests, not just owned by token", action="store_true")
180
+ cmd.add_argument("--ranch", help="Testing Farm ranch")
181
+ cmd.add_argument("--before", help="only requests created before ISO8601")
182
+ cmd.add_argument("--after", help="only requests created after ISO8601")
183
183
 
184
184
  cmd = cmds.add_parser(
185
- 'reserve',
185
+ "reserve",
186
186
  help="reserve a system and ssh into it",
187
187
  )
188
- cmd.add_argument('--compose', '-c', help="OS compose to install", required=True)
189
- cmd.add_argument('--arch', '-a', help="system HW architecture", default='x86_64')
190
- cmd.add_argument('--timeout', '-t', help="pipeline timeout (in minutes)", type=int, default=60)
191
- cmd.add_argument('--ssh-key', help="path to a ssh private key file like 'id_rsa'")
192
- cmd.add_argument('--hvm', help="request a HVM virtualization capable HW", action='store_true')
188
+ cmd.add_argument("--compose", "-c", help="OS compose to install", required=True)
189
+ cmd.add_argument("--arch", "-a", help="system HW architecture", default="x86_64")
190
+ cmd.add_argument("--timeout", "-t", help="pipeline timeout (in minutes)", type=int, default=60)
191
+ cmd.add_argument("--ssh-key", help="path to a ssh private key file like 'id_rsa'")
192
+ cmd.add_argument("--hvm", help="request a HVM virtualization capable HW", action="store_true")
193
193
 
194
194
  cmd = cmds.add_parser(
195
- 'watch-pipeline', aliases=('wp',),
195
+ "watch-pipeline", aliases=("wp",),
196
196
  help="continuously output pipeline.log like 'tail -f'",
197
197
  )
198
- cmd.add_argument('request_id', help="Testing Farm request UUID")
198
+ cmd.add_argument("request_id", help="Testing Farm request UUID")
199
199
 
200
200
 
201
201
  def main(args):
202
- if args._cmd == 'whoami':
202
+ if args._cmd == "whoami":
203
203
  whoami(args)
204
- elif args._cmd == 'about':
204
+ elif args._cmd == "about":
205
205
  about(args)
206
- elif args._cmd == 'composes':
206
+ elif args._cmd == "composes":
207
207
  composes(args)
208
- elif args._cmd in ('get-request', 'gr'):
208
+ elif args._cmd in ("get-request", "gr"):
209
209
  get_request(args)
210
- elif args._cmd == 'cancel':
210
+ elif args._cmd == "cancel":
211
211
  cancel(args)
212
- elif args._cmd in ('search-requests', 'sr'):
212
+ elif args._cmd in ("search-requests", "sr"):
213
213
  search_requests(args)
214
- elif args._cmd == 'reserve':
214
+ elif args._cmd == "reserve":
215
215
  reserve(args)
216
- elif args._cmd in ('watch-pipeline', 'wp'):
216
+ elif args._cmd in ("watch-pipeline", "wp"):
217
217
  watch_pipeline(args)
218
218
  else:
219
219
  raise RuntimeError(f"unknown args: {args}")
220
220
 
221
221
 
222
222
  CLI_SPEC = {
223
- 'aliases': ('tf',),
224
- 'help': "various utils for Testing Farm",
225
- 'args': parse_args,
226
- 'main': main,
223
+ "aliases": ("tf",),
224
+ "help": "various utils for Testing Farm",
225
+ "args": parse_args,
226
+ "main": main,
227
227
  }
@@ -0,0 +1,125 @@
1
+ import importlib as _importlib
2
+ import pkgutil as _pkgutil
3
+ import threading as _threading
4
+
5
+ from .. import util as _util
6
+
7
+
8
+ class Connection:
9
+ """
10
+ A unified API for connecting to a remote system, running multiple commands,
11
+ rsyncing files to/from it and checking for connection state.
12
+
13
+ conn = Connection()
14
+ conn.connect()
15
+ proc = conn.cmd(["ls", "/"])
16
+ #proc = conn.cmd(["ls", "/"], func=subprocess.Popen) # non-blocking
17
+ #output = conn.cmd(["ls", "/"], func=subprocess.check_output) # stdout
18
+ conn.rsync("-v", "remote:/etc/passwd", "passwd")
19
+ conn.disconnect()
20
+
21
+ # or as try/except/finally
22
+ conn = Connection()
23
+ try:
24
+ conn.connect()
25
+ ...
26
+ finally:
27
+ conn.disconnect()
28
+
29
+ # or via Context Manager
30
+ with Connection() as conn:
31
+ ...
32
+
33
+ Note that internal connection handling must be implemented as thread-aware,
34
+ ie. disconnect() might be called from a different thread while connect()
35
+ or cmd() are still running.
36
+ Similarly, multiple threads may run cmd() or rsync() independently.
37
+ """
38
+
39
+ def __init__(self):
40
+ """
41
+ Initialize the connection instance.
42
+ If extending __init__, always call 'super().__init__()' at the top.
43
+ """
44
+ self.lock = _threading.RLock()
45
+
46
+ def __enter__(self):
47
+ self.connect()
48
+ return self
49
+
50
+ def __exit__(self, exc_type, exc_value, traceback):
51
+ self.disconnect()
52
+
53
+ def connect(self, block=True):
54
+ """
55
+ Establish a persistent connection to the remote.
56
+
57
+ If 'block' is True, wait for the connection to be up,
58
+ otherwise raise BlockingIOError if the connection is still down.
59
+ """
60
+ raise NotImplementedError(f"'connect' not implemented for {self.__class__.__name__}")
61
+
62
+ def disconnect(self):
63
+ """
64
+ Destroy the persistent connection to the remote.
65
+ """
66
+ raise NotImplementedError(f"'disconnect' not implemented for {self.__class__.__name__}")
67
+
68
+ # TODO: is this needed? .. we probably want Remote.alive() instead
69
+ #def alive(self):
70
+ # """
71
+ # Return True if the connection was established and is active,
72
+ # False otherwise.
73
+ # """
74
+ # raise NotImplementedError(f"'alive' not implemented for {self.__class__.__name__}")
75
+
76
+ def cmd(self, command, func=_util.subprocess_run, **func_args):
77
+ """
78
+ Execute a single command on the remote, using subprocess-like semantics.
79
+
80
+ 'command' is the command with arguments, as a tuple/list.
81
+
82
+ 'func' is the subprocess function to use (.run(), .Popen, etc.).
83
+
84
+ 'func_args' are further keyword arguments to pass to 'func'.
85
+ """
86
+ raise NotImplementedError(f"'cmd' not implemented for {self.__class__.__name__}")
87
+
88
+ def rsync(self, *args, func=_util.subprocess_run, **func_args):
89
+ """
90
+ Synchronize local/remote files/directories via 'rsync'.
91
+
92
+ Pass *args like rsync(1) CLI arguments, incl. option arguments, ie.
93
+ .rsync("-vr", "local_path/", "remote:remote_path")
94
+ .rsync("-z", "remote:remote_file" ".")
95
+
96
+ To indicate remote path, use any string followed by a colon, the remote
97
+ name does not matter as an internally-handled '-e' option dictates all
98
+ the connection details.
99
+
100
+ 'func' is a subprocess function to use (.run(), .Popen, etc.).
101
+
102
+ 'func_args' are further keyword arguments to pass to 'func'.
103
+
104
+ The remote must have rsync(1) already installed.
105
+ """
106
+ raise NotImplementedError(f"'rsync' not implemented for {self.__class__.__name__}")
107
+
108
+
109
+ _submodules = [
110
+ info.name for info in _pkgutil.iter_modules(__spec__.submodule_search_locations)
111
+ ]
112
+
113
+ __all__ = [*_submodules, Connection.__name__] # noqa: PLE0604
114
+
115
+
116
+ def __dir__():
117
+ return __all__
118
+
119
+
120
+ # lazily import submodules
121
+ def __getattr__(attr):
122
+ if attr in _submodules:
123
+ return _importlib.import_module(f".{attr}", __name__)
124
+ else:
125
+ raise AttributeError(f"module '{__name__}' has no attribute '{attr}'")