ops-cli 2.3.1__py3-none-any.whl → 2.4.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.
ops/cli/__init__.py CHANGED
@@ -11,6 +11,7 @@
11
11
  import os
12
12
  from subprocess import Popen, PIPE
13
13
  import sys
14
+ from shutil import which
14
15
 
15
16
 
16
17
  def get_output(command, trim=True):
@@ -41,3 +42,8 @@ def get_config_value(config, key):
41
42
  err("You must set the %s value in %s.yaml or in the cli as an extra variable: -e %s=value" %
42
43
  (e.message, config['cluster'], e.message))
43
44
  sys.exit(1)
45
+
46
+ def check_if_teleport_binary_installed():
47
+ if which("tsh") is None:
48
+ err('tsh binary needs to be installed for Teleport to work!')
49
+ sys.exit(2)
ops/cli/parser.py CHANGED
@@ -112,5 +112,7 @@ def configure_common_ansible_args(parser):
112
112
  parser.add_argument('--noscb', action='store_false', dest='use_scb',
113
113
  help='Disable use of Shell Control Box (SCB) even if '
114
114
  'it is enabled in the cluster config')
115
+ parser.add_argument('--teleport', action='store_false', dest='use_teleport',
116
+ help='Use Teleport for SSH')
115
117
 
116
118
  return parser
ops/cli/playbook.py CHANGED
@@ -73,9 +73,11 @@ class PlaybookRunner(object):
73
73
  def run(self, args, extra_args):
74
74
  logger.info("Found extra_args %s", extra_args)
75
75
  inventory_path, ssh_config_paths = self.inventory_generator.generate()
76
- ssh_config_path = SshConfigGenerator.get_ssh_config_path(self.cluster_config,
76
+ ssh_config_generator = SshConfigGenerator(self.ops_config.package_dir)
77
+ ssh_config_path = ssh_config_generator.get_ssh_config_path(self.cluster_config,
77
78
  ssh_config_paths,
78
- args.use_scb)
79
+ args)
80
+
79
81
  ssh_config = f"ANSIBLE_SSH_ARGS='-F {ssh_config_path}'"
80
82
 
81
83
  ansible_config = "ANSIBLE_CONFIG=%s" % self.ops_config.ansible_config_path
ops/cli/run.py CHANGED
@@ -68,9 +68,10 @@ class CommandRunner(object):
68
68
  logger.info("Found extra_args %s", extra_args)
69
69
  inventory_path, ssh_config_paths = self.inventory_generator.generate()
70
70
  limit = args.host_pattern
71
- ssh_config_path = SshConfigGenerator.get_ssh_config_path(self.cluster_config,
71
+ ssh_config_generator = SshConfigGenerator(self.ops_config.package_dir)
72
+ ssh_config_path = ssh_config_generator.get_ssh_config_path(self.cluster_config,
72
73
  ssh_config_paths,
73
- args.use_scb)
74
+ args)
74
75
  extra_args = ' '.join(args.extra_args)
75
76
  command = """cd {root_dir}
76
77
  ANSIBLE_SSH_ARGS='-F {ssh_config}' ANSIBLE_CONFIG={ansible_config_path} ansible -i {inventory_path} '{limit}' \\
ops/cli/ssh.py CHANGED
@@ -16,7 +16,7 @@ from .parser import configure_common_arguments
16
16
  from ansible.inventory.host import Host
17
17
  from ops.inventory.sshconfig import SshConfigGenerator
18
18
 
19
- from . import err
19
+ from . import err, check_if_teleport_binary_installed
20
20
  import sys
21
21
  import getpass
22
22
  import re
@@ -75,7 +75,7 @@ class SshParserConfig(SubParserConfig):
75
75
  action="store_true",
76
76
  help="Port tunnel a machine that does not have SSH. "
77
77
  "Implies --ipaddress, and --tunnel; requires --local and --remote"
78
- )
78
+ )
79
79
  parser.add_argument(
80
80
  '--keygen',
81
81
  action='store_true',
@@ -86,6 +86,11 @@ class SshParserConfig(SubParserConfig):
86
86
  dest='use_scb',
87
87
  help='Disable use of Shell Control Box (SCB) even if it is '
88
88
  'enabled in the cluster config')
89
+ parser.add_argument(
90
+ '--teleport',
91
+ action='store_false',
92
+ dest='use_teleport',
93
+ help='Use Teleport for SSH')
89
94
  parser.add_argument(
90
95
  '--auto_scb_port',
91
96
  action='store_true',
@@ -162,6 +167,60 @@ class SshRunner(object):
162
167
 
163
168
  def run(self, args, extra_args):
164
169
  logger.info("Found extra_args %s", extra_args)
170
+
171
+ self.check_if_keygen_arg(args)
172
+ self.exit_when_local_arg_not_match_regex(args)
173
+ self.exit_when_tunnel_used_with_incorrect_parameters(args)
174
+ self.exit_when_scb_enabled_but_scb_host_not_set(self.is_scb_enabled(args), self.get_scb_host(args))
175
+ self.exit_when_scb_proxy_used_with_incorect_parameters(args, self.get_scb_proxy_port(args))
176
+
177
+ group = "%s,&%s" % (self.cluster_name, args.role)
178
+ self.set_args_index(args)
179
+ self.set_args_if_nossh(args)
180
+
181
+ display("Expression %s matched hosts (max 10): " % group, stderr=True)
182
+ host_names = self.get_host_names(group, self.get_hosts_with_fallback(args, group))
183
+ display('\n'.join(host_names), color='blue')
184
+
185
+ host = self.resolve_host(args, host_names, group)
186
+ ssh_host = self.resolve_ssh_host(args, host)
187
+ ssh_user = args.user or self.get_ssh_user_from_config()
188
+
189
+ self.ssh_opts_extend_with_user_arg(ssh_user, args)
190
+
191
+
192
+ ssh_config = SshConfig(self.is_scb_enabled(args),
193
+ self.is_teleport_enabled(args),
194
+ self.get_ssh_config_prop(args),
195
+ ssh_user,
196
+ ssh_host,
197
+ self.get_ssh_host_dest(args, ssh_host),
198
+ self.get_ssh_host_bastion(args, ssh_host),
199
+ self.get_scb_host(args),
200
+ self.get_scb_ssh_host(ssh_host, args),
201
+ host,
202
+ self.get_scb_proxy_port(args))
203
+
204
+ command = self.build_ssh_command(args, ssh_config)
205
+ display(command, color="purple")
206
+
207
+ self.check_passwordless_wrapper()
208
+
209
+ display(
210
+ "SSH-ing to %s[%d] => %s" %
211
+ (args.role,
212
+ args.index,
213
+ host.name),
214
+ color="green",
215
+ stderr=True)
216
+
217
+ return dict(command=command)
218
+
219
+ def get_ssh_config_prop(self, args):
220
+ return args.ssh_config or self.ops_config.get(
221
+ 'ssh.config') or self.ansible_inventory.get_ssh_config()
222
+
223
+ def check_if_keygen_arg(self, args):
165
224
  if args.keygen:
166
225
  if self.cluster_config.has_ssh_keys:
167
226
  err('Cluster already has ssh keys, refusing to overwrite')
@@ -172,64 +231,41 @@ class SshRunner(object):
172
231
  display(
173
232
  'Trying to generate ssh keys in:\n{} and \n{}'.format(
174
233
  pub_key_file, prv_key_file))
175
- if os.path.isfile(
176
- pub_key_file) or os.path.isfile(prv_key_file):
234
+ if os.path.isfile(pub_key_file) or os.path.isfile(prv_key_file):
177
235
  err('Although we do not have a complete keyset, one of the files exists and we refuse to overwrite\n')
178
236
  sys.exit(2)
179
237
  else:
180
238
  # generate ssh keypair. The passphrase will be the name of
181
239
  # the cluster
182
- cmd = "ssh-keygen -t rsa -b 4096 -N {} -f {}".format(
183
- self.cluster_name, prv_key_file).split(' ')
240
+ cmd = "ssh-keygen -t rsa -b 4096 -N {} -f {}".format(self.cluster_name, prv_key_file).split(' ')
184
241
  print(cmd)
185
242
  call(cmd)
186
243
  return
187
244
 
188
- if args.local and not IP_HOST_REG_EX.match(args.local):
189
- err('The --local parameter must be in the form of host-ip:port or port')
190
- sys.exit(2)
191
-
192
- if args.tunnel or args.nossh:
193
- if args.local is None or args.remote is None:
194
- err('When using --tunnel or --nossh both the --local and --remote parameters are required')
195
- sys.exit(2)
196
-
197
- scb_settings = self.cluster_config.get('scb', {})
198
- scb_enabled = scb_settings.get('enabled') and args.use_scb
199
- scb_host = scb_settings.get('host') or self.ops_config.get('scb.host')
200
- scb_proxy_port = scb_settings.get('proxy_port')
201
-
202
- if scb_enabled and not scb_host:
203
- err('When scb is enabled scb_host is required!')
204
- sys.exit(2)
205
-
206
- if args.proxy:
207
- if args.local is None and (args.auto_scb_port is False and not scb_proxy_port):
208
- err('When using --proxy the --local parameter is required if not using '
209
- '--auto_scb_port and scb.proxy_port is not configured in the cluster config')
210
- sys.exit(2)
211
-
212
- group = "%s,&%s" % (self.cluster_name, args.role)
213
-
245
+ def set_args_index(self, args):
214
246
  args.index = args.index - 1
215
247
  if args.index < 0:
216
248
  args.index = 0
217
249
 
218
- hosts = self.ansible_inventory.get_hosts(group)
250
+ def get_hosts_with_fallback(self, args, group):
251
+ hosts = self.get_ansible_hosts(group)
219
252
  if len(hosts) <= args.index:
220
- group = args.role
221
- hosts = self.ansible_inventory.get_hosts(group)
222
- if not hosts:
223
- display(
224
- "No host found in inventory, using provided name %s" %
253
+ hosts = self.ansible_inventory.get_hosts(args.role)
254
+ self.exit_if_hosts_null(args, hosts)
255
+ return hosts
256
+
257
+ def exit_if_hosts_null(self, args, hosts):
258
+ if not hosts:
259
+ display("No host found in inventory, using provided name %s" %
225
260
  (args.role), color="purple", stderr=True)
226
261
 
227
- display("Expression %s matched hosts (max 10): " % group, stderr=True)
228
- host_names = [host.name for host in hosts]
229
- for name in host_names[:10]:
230
- display(name, color='blue')
262
+ def get_host_names(self, group, hosts):
263
+ return [host.name for host in hosts[:10]]
264
+
265
+ def get_ansible_hosts(self, group):
266
+ return self.ansible_inventory.get_hosts(group)
231
267
 
232
- host = None
268
+ def resolve_host(self, args, host_names, group):
233
269
  if host_names:
234
270
  if args.index < len(host_names):
235
271
  host = self.ansible_inventory.get_host(host_names[args.index])
@@ -238,85 +274,102 @@ class SshRunner(object):
238
274
  "Index out of bounds for %s" %
239
275
  (group), color="red", stderr=True)
240
276
  return
241
- if host:
242
- ssh_host = host.vars.get('ansible_ssh_host') or host.name
243
277
  else:
244
278
  # no host found in inventory, use the role provided
245
- bastion = self.ansible_inventory.get_hosts(
246
- 'bastion')[0].vars.get('ansible_ssh_host')
247
279
  host = Host(name=args.role)
248
- ssh_host = f'{bastion}--{host.name}'
249
- ssh_user = self.cluster_config.get('ssh_user') or self.ops_config.get(
250
- 'ssh.user') or getpass.getuser()
251
- if args.user:
252
- ssh_user = args.user
253
- if ssh_user and '-l' not in args.ssh_opts:
254
- args.ssh_opts.extend(['-l', ssh_user])
280
+ return host
281
+
282
+ def resolve_ssh_host(self, args, host):
283
+
284
+ if host and self.is_teleport_enabled(args):
285
+ return host.vars.get('ec2_tag_hostname') or host.vars.get('ec2_tag_CMDB_hostname') or host.vars.get('ec2_tag_CMDB_hostname') or host.name
286
+
287
+ if host and not self.is_teleport_enabled(args):
288
+ return host.vars.get('ansible_ssh_host') or host.name
255
289
 
290
+ if args.nossh:
291
+ return self.ansible_inventory.get_hosts(
292
+ 'bastion')[0].vars.get('ansible_ssh_host')
293
+
294
+ bastion = self.ansible_inventory.get_hosts(
295
+ 'bastion')[0].vars.get('ansible_ssh_host')
296
+ return f'{bastion}--{host.name}'
297
+
298
+
299
+ def set_args_if_nossh(self, args):
256
300
  if args.nossh:
257
301
  args.tunnel = True
258
302
  args.ipaddress = True
259
- ssh_host = self.ansible_inventory.get_hosts(
260
- 'bastion')[0].vars.get('ansible_ssh_host')
261
303
 
262
- # if args.tunnel or args.proxy:
263
- # ssh_config = args.ssh_config or 'ssh.tunnel.config'
264
- # else:
265
- # ssh_config = args.ssh_config or self.ansible_inventory.get_ssh_config()
266
- ssh_config = args.ssh_config or self.ops_config.get(
267
- 'ssh.config') or self.ansible_inventory.get_ssh_config()
304
+ def get_ssh_host_parts(self, ssh_host):
305
+ return ssh_host.split('--')
268
306
 
269
- ssh_host_bastion, ssh_host_dest = None, None
270
- if args.ssh_dest_user:
271
- ssh_host_parts = ssh_host.split('--')
272
- ssh_host_bastion = ssh_host_parts[0]
273
- ssh_host_dest = ssh_host_parts[1] if len(ssh_host_parts) > 1 else None
307
+ def get_ssh_host_dest(self, args, ssh_host):
308
+ return self.get_ssh_host_parts(ssh_host)[1] if args.ssh_dest_user and len(self.get_ssh_host_parts(ssh_host)) > 1 else None
274
309
 
275
- scb_ssh_host = None
276
- if scb_enabled:
277
- # scb->bastion->host vs scb->bastion
278
- scb_delimiter = "--" if "--" in ssh_host else "@"
279
- scb_ssh_host = f"{ssh_host}{scb_delimiter}{scb_host}"
310
+ def get_ssh_host_bastion(self, args, ssh_host):
311
+ return self.get_ssh_host_parts(ssh_host)[0] if args.ssh_dest_user else None
280
312
 
281
- if args.tunnel:
282
- if args.ipaddress:
283
- host_ip = host.vars.get('private_ip_address')
284
- else:
285
- host_ip = 'localhost'
286
- if scb_enabled:
287
- command = f"ssh -F {ssh_config} {ssh_user}@{scb_ssh_host} " \
288
- f"-4 -T -L {args.local}:{host_ip}:{args.remote:d}"
289
- else:
290
- command = f"ssh -F {ssh_config} {ssh_host} " \
291
- f"-4 -N -L {args.local}:{host_ip}:{args.remote:d}"
292
- else:
293
- if scb_enabled:
294
- command = f"ssh -F {ssh_config} {ssh_user}@{scb_ssh_host}"
295
- if args.ssh_dest_user and ssh_host_dest:
296
- command = (f"ssh -F {ssh_config} -t {ssh_user}@{ssh_host_bastion}@{scb_host} "
297
- f"ssh {args.ssh_dest_user}@{ssh_host_dest}")
298
- else:
299
- command = f"ssh -F {ssh_config} {ssh_host}"
300
- if args.ssh_dest_user and ssh_host_dest:
301
- command = (f"ssh -F {ssh_config} -t {ssh_user}@{ssh_host_bastion} "
302
- f"ssh {args.ssh_dest_user}@{ssh_host_dest}")
303
-
304
- if args.proxy:
305
- if scb_enabled:
306
- proxy_port = args.local or SshConfigGenerator.generate_ssh_scb_proxy_port(
307
- self.ansible_inventory.generated_path.removesuffix("/inventory"),
308
- args.auto_scb_port,
309
- scb_proxy_port
310
- )
311
- command = f"ssh -F {ssh_config} {ssh_user}@{scb_ssh_host} " \
312
- f"-4 -T -D {proxy_port} -o 'ExitOnForwardFailure yes'"
313
- else:
314
- command = f"ssh -F {ssh_config} {ssh_host} " \
315
- f"-4 -N -D {args.local} -f -o 'ExitOnForwardFailure yes'"
313
+ def get_scb_ssh_host(self, ssh_host, args):
314
+ scb_delimiter = ("--" if "--" in ssh_host else "@") if self.is_scb_enabled(args) else None # scb->bastion->host vs scb->bastion
315
+ return f"{ssh_host}{scb_delimiter}{self.get_scb_host(args)}" if self.is_scb_enabled(args) else None
316
316
 
317
- if args.ssh_opts:
318
- command = f"{command} {' '.join(args.ssh_opts)}"
317
+ def ssh_opts_extend_with_user_arg(self, ssh_user, args):
318
+ if ssh_user and '-l' not in args.ssh_opts and not self.is_teleport_enabled(args):
319
+ args.ssh_opts.extend(['-l', ssh_user])
320
+
321
+ def get_ssh_user_from_config(self):
322
+ return self.cluster_config.get('ssh_user') or self.ops_config.get('ssh.user') or getpass.getuser()
319
323
 
324
+ def exit_when_local_arg_not_match_regex(self, args):
325
+ if args.local and not IP_HOST_REG_EX.match(args.local):
326
+ err('The --local parameter must be in the form of host-ip:port or port')
327
+ sys.exit(2)
328
+
329
+ def exit_when_tunnel_used_with_incorrect_parameters(self, args):
330
+ if args.tunnel or args.nossh:
331
+ if args.local is None or args.remote is None:
332
+ err('When using --tunnel or --nossh both the --local and --remote parameters are required')
333
+ sys.exit(2)
334
+
335
+ def exit_when_scb_proxy_used_with_incorect_parameters(self, args, scb_proxy_port):
336
+ if self.is_scb_enabled(args) and args.proxy and args.local is None and (args.auto_scb_port is False and not scb_proxy_port):
337
+ err('When using --proxy the --local parameter is required if not using '
338
+ '--auto_scb_port and scb.proxy_port is not configured in the cluster config')
339
+ sys.exit(2)
340
+
341
+ def exit_when_scb_enabled_but_scb_host_not_set(self, scb_enabled, scb_host):
342
+ if scb_enabled and not scb_host:
343
+ err('When scb is enabled scb_host is required!')
344
+ sys.exit(2)
345
+
346
+ def get_scb_info_if_enabled(self, args):
347
+ scb_settings = self.cluster_config.get('scb', {})
348
+ return (True,
349
+ scb_settings.get('host') or self.ops_config.get('scb.host'),
350
+ scb_settings.get('proxy_port')) \
351
+ if scb_settings.get('enabled') and args.use_scb \
352
+ else (False, None, None)
353
+
354
+ def is_scb_enabled(self, args):
355
+ return True if self.get_cluster_config('scb').get('enabled') and self.use_scb_arg_set(args) else False
356
+
357
+ def get_scb_host(self, args):
358
+ return self.get_cluster_config('scb').get('host') if self.is_scb_enabled(args) and self.use_scb_arg_set(args) else None
359
+
360
+ def get_scb_proxy_port(self, args):
361
+ return self.get_cluster_config('scb').get('proxy_port') if self.is_scb_enabled(args) and self.use_scb_arg_set(args) else None
362
+
363
+ def use_scb_arg_set(self, args):
364
+ return True if args.use_scb else False
365
+
366
+ def get_cluster_config(self, section):
367
+ return self.cluster_config.get(section, {})
368
+
369
+ def is_teleport_enabled(self, args):
370
+ return True if self.get_cluster_config('teleport').get('enabled') and args.use_teleport else False
371
+
372
+ def check_passwordless_wrapper(self):
320
373
  # Check if optional sshpass is available and print info message
321
374
  sshpass_path = os.path.expanduser("~/bin/sshpass")
322
375
  if (os.path.isfile(sshpass_path) and os.access(sshpass_path, os.X_OK)):
@@ -326,12 +379,82 @@ class SshRunner(object):
326
379
  display("sshpass passwordless wrapper NOT available in %s" %
327
380
  (sshpass_path), color="purple", stderr=True)
328
381
 
329
- display(
330
- "SSH-ing to %s[%d] => %s" %
331
- (args.role,
332
- args.index,
333
- host.name),
334
- color="green",
335
- stderr=True)
382
+ def wrap_command_with_opts(self, initial_command, args):
383
+ return f"{initial_command} {' '.join(args.ssh_opts)}"
336
384
 
337
- return dict(command=command)
385
+ def build_ssh_command(self, args, ssh_config):
386
+ if args.tunnel:
387
+ return self.build_tunnel_command(args, ssh_config)
388
+ elif args.proxy:
389
+ return self.build_proxy_command(args, ssh_config)
390
+ else:
391
+ return self.build_regular_command(args, ssh_config)
392
+
393
+ def build_regular_command(self, args, ssh_config):
394
+ if ssh_config.scb_enabled:
395
+ command = f"ssh -F {ssh_config.ssh_config_prop} {ssh_config.ssh_user}@{ssh_config.scb_ssh_host}"
396
+ if args.ssh_dest_user and ssh_config.ssh_host_dest:
397
+ command = (f"ssh -F {ssh_config.ssh_config_prop} -t {ssh_config.ssh_user}@{ssh_config.ssh_host_bastion}@{ssh_config.scb_host} "
398
+ f"ssh {args.ssh_dest_user}@{ssh_config.ssh_host_dest}")
399
+ elif ssh_config.teleport_enabled:
400
+ check_if_teleport_binary_installed()
401
+ ssh_opts = ' '.join(args.ssh_opts) if args.ssh_opts else ''
402
+ return (f"tsh ssh {ssh_opts} {ssh_config.ssh_user}@{ssh_config.ssh_host}" if ssh_opts
403
+ else f"tsh ssh {ssh_config.ssh_user}@{ssh_config.ssh_host}")
404
+ else:
405
+ command = f"ssh -F {ssh_config.ssh_config_prop} {ssh_config.ssh_host}"
406
+ if args.ssh_dest_user and ssh_config.ssh_host_dest:
407
+ command = (f"ssh -F {ssh_config.ssh_config_prop} -t {ssh_config.ssh_user}@{ssh_config.ssh_host_bastion} "
408
+ f"ssh {args.ssh_dest_user}@{ssh_config.ssh_host_dest}")
409
+ return self.wrap_command_with_opts(command, args)
410
+
411
+ def build_tunnel_command(self, args, ssh_config):
412
+ if ssh_config.scb_enabled:
413
+ target_host = f"{ssh_config.ssh_user}@{ssh_config.scb_ssh_host}"
414
+ command = f"ssh -F {ssh_config.ssh_config_prop} {target_host} " \
415
+ f"-4 -N -L {args.local}:{self.get_host_ip(args, ssh_config.host)}:{args.remote:d}"
416
+ elif ssh_config.teleport_enabled:
417
+ check_if_teleport_binary_installed()
418
+ command = f"tsh ssh -L {args.local}:{self.get_host_ip(args, ssh_config.host)}:{args.remote} {ssh_config.ssh_host}"
419
+ else:
420
+ command = f"ssh -F {ssh_config.ssh_config_prop} {ssh_config.ssh_host} " \
421
+ f"-4 -N -L {args.local}:{self.get_host_ip(args, ssh_config.host)}:{args.remote:d}"
422
+ return self.wrap_command_with_opts(command, args)
423
+
424
+ def build_proxy_command(self, args, ssh_config):
425
+ ssh_config_generator = SshConfigGenerator(self.ops_config.package_dir)
426
+ if ssh_config.scb_enabled:
427
+ proxy_port = args.local or ssh_config_generator.generate_ssh_scb_proxy_port(
428
+ self.ansible_inventory.generated_path.removesuffix("/inventory"), args.auto_scb_port,
429
+ ssh_config.scb_proxy_port)
430
+ command = f"ssh -F {ssh_config.ssh_config_prop} {ssh_config.ssh_user}@{ssh_config.scb_ssh_host} " \
431
+ f"-4 -T -D {proxy_port} -o 'ExitOnForwardFailure yes'"
432
+ elif ssh_config.teleport_enabled:
433
+ check_if_teleport_binary_installed()
434
+ proxy_port = args.local or ssh_config_generator.get_random_generated_port()
435
+ command = f"tsh ssh -D {proxy_port} {ssh_config.ssh_host}"
436
+ else:
437
+ command = f"ssh -F {ssh_config.ssh_config_prop} {ssh_config.ssh_host} " \
438
+ f"-4 -N -D {args.local} -f -o 'ExitOnForwardFailure yes'"
439
+ return self.wrap_command_with_opts(command, args)
440
+
441
+ def get_host_ip(self, args, host):
442
+ return host.vars.get('private_ip_address') if args.ipaddress else 'localhost'
443
+
444
+
445
+
446
+
447
+ class SshConfig(object):
448
+ def __init__(self, scb_enabled, teleport_enabled, ssh_config_prop, ssh_user, ssh_host, ssh_host_dest,
449
+ ssh_host_bastion, scb_host, scb_ssh_host, host, scb_proxy_port):
450
+ self.scb_enabled = scb_enabled
451
+ self.teleport_enabled = teleport_enabled
452
+ self.ssh_config_prop = ssh_config_prop
453
+ self.ssh_user = ssh_user
454
+ self.ssh_host = ssh_host
455
+ self.ssh_host_dest = ssh_host_dest
456
+ self.ssh_host_bastion = ssh_host_bastion
457
+ self.scb_host = scb_host
458
+ self.scb_ssh_host = scb_ssh_host
459
+ self.host = host
460
+ self.scb_proxy_port = scb_proxy_port
ops/cli/sync.py CHANGED
@@ -31,11 +31,15 @@ class SyncParserConfig(SubParserConfig):
31
31
  parser.add_argument('--noscb', action='store_false', dest='use_scb',
32
32
  help='Disable use of Shell Control Box (SCB) '
33
33
  'even if it is enabled in the cluster config')
34
+ parser.add_argument(
35
+ '--teleport',
36
+ action='store_false',
37
+ dest='use_teleport',
38
+ help='Use Teleport for SSH')
34
39
  parser.add_argument(
35
40
  'opts',
36
- default=['-va --progress'],
37
41
  nargs='*',
38
- help='Rsync opts')
42
+ help='Sync opts')
39
43
 
40
44
  def get_help(self):
41
45
  return 'Sync files from/to a cluster'
@@ -54,6 +58,9 @@ class SyncParserConfig(SubParserConfig):
54
58
 
55
59
  # extra rsync options
56
60
  ops cluster.yml sync 'dcs[0]:/usr/local/demdex/conf' /tmp/configurator-data -l remote_user -- --progress
61
+
62
+ # extra sync option for Teleport (recursive download, quiet, port)
63
+ ops cluster.yml sync 'dcs[0]:/usr/local/demdex/conf' /tmp/configurator-data -- --recursive/port/quiet
57
64
  """
58
65
 
59
66
 
@@ -73,29 +80,37 @@ class SyncRunner(object):
73
80
 
74
81
  def run(self, args, extra_args):
75
82
  logger.info("Found extra_args %s", extra_args)
76
- inventory_path, ssh_config_paths = self.inventory_generator.generate()
83
+
77
84
  src = PathExpr(args.src)
78
85
  dest = PathExpr(args.dest)
86
+ remote = self.get_remote(dest, src)
79
87
 
80
- ssh_config_path = SshConfigGenerator.get_ssh_config_path(self.cluster_config,
81
- ssh_config_paths,
82
- args.use_scb)
83
88
  if src.is_remote and dest.is_remote:
84
89
  display(
85
- 'Too remote expressions are not allowed',
90
+ 'Two remote expressions are not allowed',
86
91
  stderr=True,
87
92
  color='red')
88
93
  return
89
94
 
90
- if src.is_remote:
91
- remote = src
92
- else:
93
- remote = dest
94
-
95
95
  display(
96
96
  "Looking for hosts for pattern '%s'" %
97
97
  remote.pattern, stderr=True)
98
98
 
99
+ if self.is_teleport_enabled(args):
100
+ check_if_teleport_binary_installed()
101
+ command = self.execute_teleport_scp(args, src, dest)
102
+ else:
103
+ ssh_user = self.cluster_config.get('ssh_user') or self.ops_config.get('ssh.user') or getpass.getuser()
104
+ if remote.remote_user:
105
+ ssh_user = remote.remote_user
106
+ elif args.user:
107
+ ssh_user = args.user
108
+ ssh_host = self.populate_remote_hosts(remote)[0]
109
+ command = self.execute_rsync_scp(args, src, dest, ssh_user, ssh_host, self.get_ssh_config_path(args))
110
+
111
+ return dict(command=command)
112
+
113
+ def populate_remote_hosts(self, remote):
99
114
  remote_hosts = []
100
115
  hosts = self.ansible_inventory.get_hosts(remote.pattern)
101
116
  if not hosts:
@@ -106,28 +121,43 @@ class SyncRunner(object):
106
121
  for host in hosts:
107
122
  ssh_host = host.get_vars().get('ansible_ssh_host') or host
108
123
  remote_hosts.append(ssh_host)
124
+ return remote_hosts
109
125
 
110
- for ssh_host in remote_hosts:
111
- ssh_user = self.cluster_config.get('ssh_user') or self.ops_config.get(
112
- 'ssh.user') or getpass.getuser()
113
- if remote.remote_user:
114
- ssh_user = remote.remote_user
115
- elif args.user:
116
- ssh_user = args.user
117
-
118
- from_path = src.with_user_and_path(ssh_user, ssh_host)
119
- to_path = dest.with_user_and_path(ssh_user, ssh_host)
120
-
121
- command = 'rsync {opts} {from_path} {to_path} -e "ssh -F {ssh_config}"'.format(
122
- opts=" ".join(args.opts),
123
- from_path=from_path,
124
- to_path=to_path,
125
- ssh_config=ssh_config_path
126
-
127
- )
128
-
129
- return dict(command=command)
130
-
126
+ def get_remote(self, dest, src):
127
+ if src.is_remote:
128
+ remote = src
129
+ else:
130
+ remote = dest
131
+ return remote
132
+
133
+ def get_ssh_config_path(self, args):
134
+ ssh_config_generator = SshConfigGenerator(self.ops_config.package_dir)
135
+ _, ssh_config_paths = self.inventory_generator.generate()
136
+ return ssh_config_generator.get_ssh_config_path(self.cluster_config,
137
+ ssh_config_paths,
138
+
139
+ args)
140
+
141
+ def execute_teleport_scp(self, args, src, dest):
142
+ return 'tsh scp {opts} {from_path} {to_path}'.format(
143
+ from_path=src,
144
+ to_path=dest,
145
+ opts=" ".join(args.opts)
146
+ )
147
+
148
+ def execute_rsync_scp(self, args, src, dest, ssh_user, ssh_host, ssh_config_path):
149
+ from_path = src.with_user_and_path(ssh_user, ssh_host)
150
+ to_path = dest.with_user_and_path(ssh_user, ssh_host)
151
+ return 'rsync {opts} {from_path} {to_path} -e "ssh -F {ssh_config}"'.format(
152
+ opts=" ".join(args.opts),
153
+ from_path=from_path,
154
+ to_path=to_path,
155
+ ssh_config=ssh_config_path
156
+ )
157
+
158
+
159
+ def is_teleport_enabled(self, args):
160
+ return True if self.cluster_config.get('teleport', {}).get('enabled') and args.use_teleport else False
131
161
 
132
162
  class PathExpr(object):
133
163
 
@@ -0,0 +1,9 @@
1
+ # teleport.cfg
2
+ Host *
3
+ ProxyCommand "/usr/local/bin/tsh" proxy ssh --cluster=teleport-prod --proxy=teleport.adobe.net:443 %r@%n:%p
4
+ UserKnownHostsFile /dev/null
5
+ StrictHostKeyChecking no
6
+ ControlMaster auto
7
+ ControlPersist 600s
8
+ User {ssh_username}
9
+ Port 22
ops/inventory/__init__.py CHANGED
@@ -8,4 +8,22 @@
8
8
  # OF ANY KIND, either express or implied. See the License for the specific language
9
9
  # governing permissions and limitations under the License.
10
10
 
11
+ import sys
12
+ from shutil import which
11
13
  from .ec2inventory import Ec2Inventory
14
+
15
+ def display(msg, **kwargs):
16
+ # use ansible pretty printer if available
17
+ try:
18
+ from ansible.playbook.play import display
19
+ display.display(msg, **kwargs)
20
+ except ImportError:
21
+ print(msg)
22
+
23
+ def err(msg):
24
+ display(str(msg), stderr=True, color='red')
25
+
26
+ def check_if_teleport_binary_installed():
27
+ if which("tsh") is None:
28
+ err('tsh binary needs to be installed for Teleport to work!')
29
+ sys.exit(2)
@@ -17,11 +17,13 @@ from botocore.exceptions import NoRegionError, NoCredentialsError, PartialCreden
17
17
 
18
18
 
19
19
  class Ec2Inventory(object):
20
+
21
+
20
22
  @staticmethod
21
23
  def _empty_inventory():
22
24
  return {"_meta": {"hostvars": {}}}
23
25
 
24
- def __init__(self, boto_profile, regions, filters=None, bastion_filters=None):
26
+ def __init__(self, boto_profile, regions, filters=None, bastion_filters=None, teleport_enabled=False):
25
27
 
26
28
  self.filters = filters or []
27
29
  self.regions = regions.split(',')
@@ -29,6 +31,7 @@ class Ec2Inventory(object):
29
31
  self.bastion_filters = bastion_filters or []
30
32
  self.group_callbacks = []
31
33
  self.boto3_session = self.create_boto3_session(boto_profile)
34
+ self.teleport_enabled = teleport_enabled
32
35
 
33
36
  # Inventory grouped by instance IDs, tags, security groups, regions,
34
37
  # and availability zones
@@ -145,7 +148,11 @@ class Ec2Inventory(object):
145
148
  if not dest:
146
149
  return
147
150
 
148
- if bastion_ip and bastion_ip != instance.get('PublicIpAddress'):
151
+ if self.teleport_enabled:
152
+ ansible_ssh_host = self.get_tag_value(instance, ['hostname','CMDB_hostname','Adobe:FQDN'])
153
+ if not ansible_ssh_host:
154
+ ansible_ssh_host = instance.get('PrivateIpAddress')
155
+ elif bastion_ip and bastion_ip != instance.get('PublicIpAddress'):
149
156
  ansible_ssh_host = bastion_ip + "--" + instance.get('PrivateIpAddress')
150
157
  elif instance.get('PublicIpAddress'):
151
158
  ansible_ssh_host = instance.get('PublicIpAddress')
@@ -183,6 +190,13 @@ class Ec2Inventory(object):
183
190
  self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance)
184
191
  self.inventory["_meta"]["hostvars"][dest]['ansible_ssh_host'] = ansible_ssh_host
185
192
 
193
+ def get_tag_value(self, instance, tag_keys):
194
+ for tag_key in tag_keys:
195
+ for tag in instance.get('Tags', []):
196
+ if tag['Key'] == tag_key:
197
+ return tag['Value']
198
+ return None
199
+
186
200
  def get_host_info_dict_from_instance(self, instance):
187
201
  instance_vars = {}
188
202
  for key, value in instance.items():
@@ -26,6 +26,7 @@ def cns(args):
26
26
  jsn = ec2(dict(
27
27
  region=region,
28
28
  boto_profile=profile,
29
+ teleport_enabled=args.get('teleport_enabled', False),
29
30
  cache=args.get('cache', 3600 * 24),
30
31
  filters=[
31
32
  {'Name': 'tag:cluster', 'Values': [cns_cluster]}
@@ -14,6 +14,7 @@ from ops.inventory.ec2inventory import Ec2Inventory
14
14
  def ec2(args):
15
15
  filters = args.get('filters', [])
16
16
  bastion_filters = args.get('bastion', [])
17
+ teleport_enabled = args.get('teleport_enabled')
17
18
 
18
19
  if args.get('cluster') and not args.get('filters'):
19
20
  filters = [{'Name': 'tag:cluster', 'Values': [args.get('cluster')]}]
@@ -27,4 +28,6 @@ def ec2(args):
27
28
  return Ec2Inventory(boto_profile=args['boto_profile'],
28
29
  regions=args['region'],
29
30
  filters=filters,
30
- bastion_filters=bastion_filters).get_as_json()
31
+ bastion_filters=bastion_filters,
32
+ teleport_enabled=teleport_enabled
33
+ ).get_as_json()
@@ -13,16 +13,18 @@ import socketserver
13
13
  from shutil import copy
14
14
  from pathlib import Path
15
15
  from ansible.playbook.play import display
16
+ from . import check_if_teleport_binary_installed
16
17
 
17
18
 
18
19
  class SshConfigGenerator(object):
19
20
  SSH_CONFIG_FILE = "ssh.config"
20
21
  SSH_SCB_PROXY_TPL_FILE = "ssh.scb.proxy.config.tpl"
22
+ SSH_TELEPORT_PROXY_TPL_FILE = "ssh.teleport.config.tpl"
21
23
 
22
24
  def __init__(self, package_dir):
23
25
  self.package_dir = package_dir
24
26
  self.ssh_data_dir = self.package_dir + '/data/ssh'
25
- self.ssh_config_files = [self.SSH_CONFIG_FILE, self.SSH_SCB_PROXY_TPL_FILE]
27
+ self.ssh_config_files = [self.SSH_CONFIG_FILE, self.SSH_SCB_PROXY_TPL_FILE, self.SSH_TELEPORT_PROXY_TPL_FILE]
26
28
 
27
29
  def generate(self, directory):
28
30
  dest_ssh_config = {}
@@ -36,40 +38,39 @@ class SshConfigGenerator(object):
36
38
  return [f"{self.ssh_data_dir}/{ssh_config_file}"
37
39
  for ssh_config_file in self.ssh_config_files]
38
40
 
39
- @staticmethod
40
- def get_ssh_config_path(cluster_config, ssh_config_paths, use_scb):
41
- scb_settings = cluster_config.get('scb', {})
42
- scb_enabled = scb_settings.get('enabled') and use_scb
41
+ def get_ssh_config_path(self, cluster_config, ssh_config_paths, args):
42
+ scb_enabled, teleport_enabled = self.get_proxy_type_information(cluster_config, args)
43
43
  if scb_enabled:
44
44
  ssh_config_tpl_path = ssh_config_paths.get(SshConfigGenerator.SSH_SCB_PROXY_TPL_FILE)
45
- scb_proxy_port = SshConfigGenerator.get_ssh_scb_proxy_port(ssh_config_tpl_path)
46
- ssh_config_path = SshConfigGenerator.generate_ssh_scb_config(ssh_config_tpl_path,
47
- scb_proxy_port)
48
- display.display(f"Connecting via scb proxy at 127.0.0.1:{scb_proxy_port}.\n"
49
- f"This proxy should have already been started and running "
50
- f"in a different terminal window.\n"
51
- f"If there are connection issues double check that "
52
- f"the proxy is running.",
53
- color='blue',
54
- stderr=True)
45
+ ssh_proxy_port = self.get_ssh_proxy_port(ssh_config_tpl_path)
46
+ display.display(f"Connecting via scb proxy at 127.0.0.1:{ssh_proxy_port}.\n"
47
+ f"This proxy should have already been started and running "
48
+ f"in a different terminal window.\n"
49
+ f"If there are connection issues double check that "
50
+ f"the proxy is running.",
51
+ color='blue',
52
+ stderr=True)
53
+ return self.generate_ssh_config_from_template(ssh_config_tpl_path, scb_proxy_port=ssh_proxy_port)
54
+ elif teleport_enabled:
55
+ check_if_teleport_binary_installed()
56
+ display.display(f"Using Teleport for SSH connections.\n"
57
+ f"Make sure you are logged in with 'tsh login'.",
58
+ color='blue',
59
+ stderr=True)
60
+ ssh_config_tpl_path = ssh_config_paths.get(SshConfigGenerator.SSH_TELEPORT_PROXY_TPL_FILE)
61
+ return self.generate_ssh_config_from_template(ssh_config_tpl_path, ssh_username=os.getlogin())
55
62
  else:
56
- ssh_config_path = ssh_config_paths.get(SshConfigGenerator.SSH_CONFIG_FILE)
57
- return ssh_config_path
63
+ return ssh_config_paths.get(SshConfigGenerator.SSH_CONFIG_FILE)
58
64
 
59
- @staticmethod
60
- def generate_ssh_scb_proxy_port(ssh_config_path, auto_scb_port, scb_config_port):
65
+ def generate_ssh_scb_proxy_port(self, ssh_config_path, auto_scb_port, scb_config_port):
61
66
  ssh_config_port_path = f"{ssh_config_path}/ssh_scb_proxy_config_port"
62
67
  if auto_scb_port:
63
- with socketserver.TCPServer(("localhost", 0), None) as s:
64
- generated_port = s.server_address[1]
65
- display.display(f"Using auto generated port {generated_port} for scb proxy port",
66
- color='blue',
67
- stderr=True)
68
+ generated_port = self.get_random_generated_port()
68
69
  else:
69
70
  generated_port = scb_config_port
70
71
  display.display(f"Using port {generated_port} from cluster config for scb proxy port",
71
- color='blue',
72
- stderr=True)
72
+ color='blue',
73
+ stderr=True)
73
74
 
74
75
  with open(ssh_config_port_path, 'w') as f:
75
76
  f.write(str(generated_port))
@@ -77,22 +78,32 @@ class SshConfigGenerator(object):
77
78
 
78
79
  return generated_port
79
80
 
80
-
81
- @staticmethod
82
- def get_ssh_scb_proxy_port(ssh_config_path):
81
+ def get_ssh_proxy_port(self, ssh_config_path):
83
82
  ssh_port_path = ssh_config_path.replace("_tpl", "_port")
84
- ssh_scb_proxy_port = Path(ssh_port_path).read_text()
85
- return ssh_scb_proxy_port
86
-
87
- @staticmethod
88
- def generate_ssh_scb_config(ssh_config_tpl_path, scb_proxy_port):
89
- ssh_config_template = Path(ssh_config_tpl_path).read_text()
90
- ssh_config_content = ssh_config_template.format(
91
- scb_proxy_port=scb_proxy_port
92
- )
83
+ return Path(ssh_port_path).read_text()
84
+
85
+ def generate_ssh_config_from_template(self, ssh_config_tpl_path, **template_vars):
86
+ ssh_config_content = Path(ssh_config_tpl_path).read_text().format(**template_vars)
87
+ return self.generate_file_from_template(ssh_config_tpl_path, ssh_config_content)
88
+
89
+ def generate_file_from_template(self, ssh_config_tpl_path, ssh_config_content):
93
90
  ssh_config_path = ssh_config_tpl_path.removesuffix("_tpl")
94
91
  with open(ssh_config_path, 'w') as f:
95
92
  f.write(ssh_config_content)
96
93
  os.fchmod(f.fileno(), 0o644)
97
-
98
94
  return ssh_config_path
95
+
96
+ def get_proxy_type_information(self, cluster_config, args):
97
+ scb_settings = cluster_config.get('scb', {})
98
+ scb_enabled = scb_settings.get('enabled') and args.use_scb
99
+ teleport_settings = cluster_config.get('teleport', {})
100
+ teleport_enabled = teleport_settings.get('enabled') and args.use_teleport
101
+ return scb_enabled, teleport_enabled
102
+
103
+ def get_random_generated_port(self):
104
+ with socketserver.TCPServer(("localhost", 0), None) as s:
105
+ generated_port = s.server_address[1]
106
+ display.display(f"Using auto generated port {generated_port} for scb proxy port",
107
+ color='blue',
108
+ stderr=True)
109
+ return generated_port
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: ops-cli
3
- Version: 2.3.1
3
+ Version: 2.4.0
4
4
  Summary: Ops - wrapper for Terraform, Ansible, and SSH for cloud automation
5
5
  Home-page: https://github.com/adobe/ops-cli
6
6
  Author: Adobe
@@ -26,12 +26,12 @@ License-File: LICENSE
26
26
  Requires-Dist: adal==1.2.7
27
27
  Requires-Dist: ansible==8.7.0; python_version >= "3.9"
28
28
  Requires-Dist: ansible-core==2.15.13; python_version >= "3.9"
29
- Requires-Dist: awscli==1.32.6; python_version >= "3.8"
29
+ Requires-Dist: awscli==1.42.30; python_version >= "3.9"
30
30
  Requires-Dist: azure==4.0.0
31
31
  Requires-Dist: azure-applicationinsights==0.1.1
32
32
  Requires-Dist: azure-batch==4.1.3
33
33
  Requires-Dist: azure-common==1.1.28
34
- Requires-Dist: azure-core==1.32.0; python_version >= "3.8"
34
+ Requires-Dist: azure-core==1.38.0; python_version >= "3.9"
35
35
  Requires-Dist: azure-cosmosdb-nspkg==2.0.2
36
36
  Requires-Dist: azure-cosmosdb-table==1.0.6
37
37
  Requires-Dist: azure-datalake-store==0.0.53
@@ -110,58 +110,55 @@ Requires-Dist: azure-storage-blob==1.5.0
110
110
  Requires-Dist: azure-storage-common==1.4.2
111
111
  Requires-Dist: azure-storage-file==1.4.0
112
112
  Requires-Dist: azure-storage-queue==1.4.0
113
- Requires-Dist: backports.functools-lru-cache==1.6.6; python_version >= "2.6"
114
- Requires-Dist: boto3==1.34.6; python_version >= "3.8"
115
- Requires-Dist: botocore==1.34.6; python_version >= "3.8"
116
- Requires-Dist: cachetools==5.5.0; python_version >= "3.7"
117
- Requires-Dist: certifi==2024.12.14; python_version >= "3.6"
118
- Requires-Dist: cffi==1.17.1; python_version >= "3.8"
119
- Requires-Dist: charset-normalizer==3.4.1; python_version >= "3.7"
120
- Requires-Dist: colorama==0.4.4; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4"
121
- Requires-Dist: cryptography==44.0.0; python_version >= "3.7" and python_full_version not in "3.9.0, 3.9.1"
122
- Requires-Dist: deepmerge==1.1.1
123
- Requires-Dist: docutils==0.16; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4"
113
+ Requires-Dist: boto3==1.40.30; python_version >= "3.9"
114
+ Requires-Dist: botocore==1.40.30; python_version >= "3.9"
115
+ Requires-Dist: certifi==2026.1.4; python_version >= "3.7"
116
+ Requires-Dist: cffi==2.0.0; python_version >= "3.9"
117
+ Requires-Dist: charset-normalizer==3.4.4; python_version >= "3.7"
118
+ Requires-Dist: colorama==0.4.6; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6"
119
+ Requires-Dist: cryptography==46.0.4; python_version >= "3.8" and python_full_version not in "3.9.0, 3.9.1"
120
+ Requires-Dist: deepmerge==2.0; python_version >= "3.8"
121
+ Requires-Dist: docutils==0.19; python_version >= "3.7"
122
+ Requires-Dist: durationpy==0.10
124
123
  Requires-Dist: gitdb==4.0.12; python_version >= "3.7"
125
- Requires-Dist: gitpython==3.1.44; python_version >= "3.7"
126
- Requires-Dist: google-auth==2.37.0; python_version >= "3.7"
124
+ Requires-Dist: gitpython==3.1.46; python_version >= "3.7"
125
+ Requires-Dist: google-auth==2.48.0; python_version >= "3.8"
127
126
  Requires-Dist: hashmerge==0.2
128
- Requires-Dist: himl==0.15.2; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2, 3.3"
127
+ Requires-Dist: himl==0.18.0; python_version >= "3.9"
129
128
  Requires-Dist: hvac==1.2.1; python_full_version >= "3.6.2" and python_full_version < "4.0.0"
130
- Requires-Dist: idna==3.10; python_version >= "3.6"
129
+ Requires-Dist: idna==3.11; python_version >= "3.8"
131
130
  Requires-Dist: inflection==0.5.1; python_version >= "3.5"
132
131
  Requires-Dist: isodate==0.7.2; python_version >= "3.7"
133
- Requires-Dist: jinja2==3.1.4; python_version >= "3.7"
134
- Requires-Dist: jmespath==1.0.1; python_version >= "3.7"
135
- Requires-Dist: kubernetes==26.1.0; python_version >= "3.6"
136
- Requires-Dist: lru-cache==0.2.3
137
- Requires-Dist: markupsafe==3.0.2; python_version >= "3.9"
138
- Requires-Dist: msal==1.31.1; python_version >= "3.7"
132
+ Requires-Dist: jinja2==3.1.6; python_version >= "3.7"
133
+ Requires-Dist: jmespath==1.1.0; python_version >= "3.9"
134
+ Requires-Dist: kubernetes==33.1.0; python_version >= "3.6"
135
+ Requires-Dist: markupsafe==3.0.3; python_version >= "3.9"
136
+ Requires-Dist: msal==1.34.0; python_version >= "3.8"
139
137
  Requires-Dist: msrest==0.7.1; python_version >= "3.6"
140
138
  Requires-Dist: msrestazure==0.6.4
141
- Requires-Dist: oauthlib==3.2.2; python_version >= "3.6"
142
- Requires-Dist: packaging==24.2; python_version >= "3.8"
139
+ Requires-Dist: oauthlib==3.3.1; python_version >= "3.8"
140
+ Requires-Dist: packaging==26.0; python_version >= "3.8"
143
141
  Requires-Dist: passgen==1.1.1
144
142
  Requires-Dist: pathlib2==2.3.7.post1
145
- Requires-Dist: pyasn1==0.6.1; python_version >= "3.8"
146
- Requires-Dist: pyasn1-modules==0.4.1; python_version >= "3.8"
147
- Requires-Dist: pycparser==2.22; python_version >= "3.8"
143
+ Requires-Dist: pyasn1==0.6.2; python_version >= "3.8"
144
+ Requires-Dist: pyasn1-modules==0.4.2; python_version >= "3.8"
145
+ Requires-Dist: pycparser==3.0; python_version >= "3.10"
148
146
  Requires-Dist: pyhcl==0.4.5
149
- Requires-Dist: pyjwt[crypto]==2.10.1; python_version >= "3.9"
147
+ Requires-Dist: pyjwt[crypto]==2.11.0; python_version >= "3.9"
150
148
  Requires-Dist: python-consul==1.1.0
151
149
  Requires-Dist: python-dateutil==2.9.0.post0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2"
152
150
  Requires-Dist: pyyaml==6.0.1; python_version >= "3.6"
153
- Requires-Dist: requests==2.32.3; python_version >= "3.8"
151
+ Requires-Dist: requests==2.32.5; python_version >= "3.9"
154
152
  Requires-Dist: requests-oauthlib==2.0.0; python_version >= "3.4"
155
153
  Requires-Dist: resolvelib==1.0.1
156
154
  Requires-Dist: rsa==4.7.2; python_version >= "3.5" and python_version < "4"
157
- Requires-Dist: s3transfer==0.10.4; python_version >= "3.8"
158
- Requires-Dist: setuptools==75.8.0; python_version >= "3.9"
155
+ Requires-Dist: s3transfer==0.14.0; python_version >= "3.9"
159
156
  Requires-Dist: simpledi==0.4.1
160
157
  Requires-Dist: six==1.17.0; python_version >= "2.7" and python_version not in "3.0, 3.1, 3.2"
161
158
  Requires-Dist: smmap==5.0.2; python_version >= "3.7"
162
- Requires-Dist: typing-extensions==4.12.2; python_version >= "3.8"
163
- Requires-Dist: urllib3==2.0.7; python_version >= "3.7"
164
- Requires-Dist: websocket-client==1.8.0; python_version >= "3.8"
159
+ Requires-Dist: typing-extensions==4.15.0; python_version >= "3.9"
160
+ Requires-Dist: urllib3==2.6.0; python_version >= "3.9"
161
+ Requires-Dist: websocket-client==1.9.0; python_version >= "3.9"
165
162
  Dynamic: author
166
163
  Dynamic: author-email
167
164
  Dynamic: classifier
@@ -169,6 +166,7 @@ Dynamic: description
169
166
  Dynamic: description-content-type
170
167
  Dynamic: home-page
171
168
  Dynamic: license
169
+ Dynamic: license-file
172
170
  Dynamic: requires-dist
173
171
  Dynamic: requires-python
174
172
  Dynamic: summary
@@ -326,7 +324,7 @@ workon ops
326
324
  # uninstall previous `ops` version (if you have it)
327
325
  pip uninstall ops --yes
328
326
 
329
- # install ops-cli v2.3.1 stable release
327
+ # install ops-cli v2.4.0 stable release
330
328
  pip install --upgrade ops-cli
331
329
  ```
332
330
 
@@ -342,7 +340,7 @@ You can try out `ops-cli`, by using docker. The docker image has all required pr
342
340
 
343
341
  To start out a container, running the latest `ops-cli` docker image run:
344
342
  ```sh
345
- docker run -it ghcr.io/adobe/ops-cli:2.3.1 bash
343
+ docker run -it ghcr.io/adobe/ops-cli:2.4.0 bash
346
344
  ```
347
345
 
348
346
  After the container has started, you can start using `ops-cli`:
@@ -713,6 +711,23 @@ If there are connection issues double check that the proxy is running.
713
711
  ...
714
712
  ```
715
713
 
714
+ #### Teleport
715
+ Teleport (https://goteleport.com/) provides secretless SSH.
716
+ `ops` has support for using Teleport as ssh for the following operations: `ssh, tunnel, proxy, ansible play, run and sync`
717
+
718
+ In order to use Teleport an extra section needs to be added to the cluster config file:
719
+ ***
720
+ ```
721
+ inventory:
722
+ - plugin: cns
723
+ args:
724
+ teleport_enabled: True -> add this to existing configuration
725
+
726
+
727
+ teleport:
728
+ enabled: true -> add this whole block
729
+ ```
730
+
716
731
  ### Play
717
732
 
718
733
  Run an ansible playbook.
@@ -937,7 +952,7 @@ env LDFLAGS="-L$(brew --prefix openssl)/lib" CFLAGS="-I$(brew --prefix openssl)/
937
952
 
938
953
  ## Running tests
939
954
 
940
- - on your machine: `py.test tests`
955
+ - on your machine: `python -m pytest tests` or `build_scripts/run_tests.sh`
941
956
 
942
957
  # Troubleshooting
943
958
 
@@ -12,18 +12,18 @@ ops/ansible/filter_plugins/commonfilters.py,sha256=Ya1fKYAsoqYsuLbzP48Oaz2AYWfne
12
12
  ops/ansible/vars_plugins/__init__.py,sha256=v9uW7YO2C7EC4MQWr_TMH_K-1ALd0uLl9vZ_h3xCQkg,596
13
13
  ops/ansible/vars_plugins/clusterconfig.py,sha256=qmIrTp1V0gLdbtBI5A0HMmICe8z0xUq7LzO3pmIJlsI,1378
14
14
  ops/ansible/vars_plugins/opsconfig.py,sha256=lfKCD7XRoDTW80QXEGcm3tV6ASnzoCGdxy5zpaimT4k,1615
15
- ops/cli/__init__.py,sha256=LcPvjP6p7l8At8CIjyWvm0mhYzMGJqFv45AcTls1IhM,1376
15
+ ops/cli/__init__.py,sha256=npLihw242EI2DzH7pkAgZnTp7GgPxh9fdHnzANuYFRQ,1562
16
16
  ops/cli/aws.py,sha256=OsmpOVv1924_lvNESeElLgcEi1caYTnI88C7wzXie78,911
17
17
  ops/cli/config.py,sha256=T0ZfW1bWdSr1dF4oqsvTjUtUiCr7RVm4_rFdezgGCqM,7629
18
18
  ops/cli/config_generator.py,sha256=ARGp5kAgdMLqtrHyQb_dE9sEuM1bAnVddfWDxd_5OdA,1718
19
19
  ops/cli/helmfile.py,sha256=2ipKyFMY5M87U1mXDQle_UsOXICdQ-0VX2f97TAg0I8,6354
20
20
  ops/cli/inventory.py,sha256=kSqNw978_P7OrFzLng5ipzuJBh6WqtvjvLDtiNwFQ8A,3109
21
21
  ops/cli/packer.py,sha256=GAgi6uPWO0wv3uIEwl0O2sb4fQtpCbL1561nP8Y_Lpc,2626
22
- ops/cli/parser.py,sha256=tzXvpu4GycyHSjERFrjQVEFZYVzOszLhDEjpkFYJ-oc,4377
23
- ops/cli/playbook.py,sha256=rPaIfUUMSTgITxzrvkVyTpPJ2UfqYnk3WfjY3jDrHO0,5287
24
- ops/cli/run.py,sha256=cwT2rQEBRip_OndkSQLFR7Q8D5BD6L5BbAq6nehKhco,3373
25
- ops/cli/ssh.py,sha256=AGDtNqLnbOjIbVxAnxfEJDpi5m1D0tOO3ml3uQ3bhgQ,14196
26
- ops/cli/sync.py,sha256=8K3Dsh6cwv9gTat0KXvRemHmqpAWd46pHfS8wMS9Olk,5663
22
+ ops/cli/parser.py,sha256=YGxYgMwduTnTTjNNWXYZoasBOZ9LUkE4wlx8WIsOqiE,4511
23
+ ops/cli/playbook.py,sha256=5WaYFgt2wRMycPoRWDRSICXPUfYRlxHwpe9GhezytR4,5426
24
+ ops/cli/run.py,sha256=qvhikkF4Wjbn8CH4sj_e_jI_fZ8id-Q7VmlTtnXOjxs,3446
25
+ ops/cli/ssh.py,sha256=nehRK6CEyfybMgC_O_04Cl1lj1dbsJj1Is2xzec9Fu8,20426
26
+ ops/cli/sync.py,sha256=_eImwQWm125VZ-_NPUkeXCjROH0JDp6thBgAMmt2N6E,7057
27
27
  ops/cli/terraform.py,sha256=XeLXr_nFpdnsHdEfAFj_oiurvIhDQv-xE3L-NpMTcB0,12446
28
28
  ops/data/ansible/ansible.cfg,sha256=LE0Ve37qwQnbM1AJfHX442yh6HMMzM1FPCTCUKZElV8,186
29
29
  ops/data/ansible/tasks/deploy_prometheus_alert_rules.yml,sha256=JAtUk9s7r4Nd5s5BC6BzlhyZZr6aPGWVeyrd6HocDGI,888
@@ -31,29 +31,30 @@ ops/data/ansible/tasks/install_rpm.yml,sha256=DAGcuQnKeeJQd-DJByfQTL1MeNPguhJjWq
31
31
  ops/data/ansible/tasks/remove_prometheus_alert_rules.yml,sha256=LBNKpuAwY3gtyOrdD3EcHDVg9fMxUK4_uocOjiFaLuY,1397
32
32
  ops/data/ssh/ssh.config,sha256=nFgQkQ9VGLYCFKj9rm0KvuHtC-jHK5QCBFRv_XSKHxg,1208
33
33
  ops/data/ssh/ssh.scb.proxy.config.tpl,sha256=iFoNY3N_Oz1ADTX_B2NEoK4ysMngGLmpC7uUeQUSx8E,536
34
+ ops/data/ssh/ssh.teleport.config.tpl,sha256=ARGcK1VONCNnFEX3P9Q3hRuulkBx6C4dOZFCr70eb5k,278
34
35
  ops/data/ssh/ssh.tunnel.config,sha256=G12kyR8FAnoZKGNUSE01oQnwMQ8gTJJt3HXBiERRWHo,364
35
36
  ops/data/terraform/terraformrc,sha256=500iIw-7B3IwASuuO-0xqWolbdGbwmsneitIAcVw6hE,53
36
37
  ops/hierarchical/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
38
  ops/hierarchical/composition_config_generator.py,sha256=tOW5bl7frRFJOLtFGBv0HzSFPCEAp2YViRF071sTAq4,7219
38
39
  ops/inventory/SKMS.py,sha256=jv4nHTKbDmJBTNf3HgGyKSfAJ10akKdDxNoGOcPehfo,17616
39
- ops/inventory/__init__.py,sha256=yvYO8z4aCdcOEYIjy27YL7fcbPqZAwQk9y_3YcNVAGU,643
40
+ ops/inventory/__init__.py,sha256=WwwekRxaxU6S_HXSqZe-2XFkW59j0CAUh1OU6xadtHo,1119
40
41
  ops/inventory/azurerm.py,sha256=pWZqbZP9-7ChHNK1DwfSlt9hU6lcJjvNXmvduR2G5tQ,33507
41
42
  ops/inventory/caching.py,sha256=G2sJJ7nPHCqNBz3Vh0vxrFWQZt6B_11yS50GeZ8_1Bs,2000
42
- ops/inventory/ec2inventory.py,sha256=Zz3pB6hGLkzZa_ZIwpF2DfBXM_IQDIUmlwUIyy5D2HU,10655
43
+ ops/inventory/ec2inventory.py,sha256=AmUnCWfoYYKmk35sxfzMs0Pst35LDkqeCx2mT6PpVtU,11208
43
44
  ops/inventory/generator.py,sha256=BcjUxMoWTnmx-P_S-1fLjqG3PUhyMyAO2S-037KG5q0,10885
44
- ops/inventory/sshconfig.py,sha256=lxqKyH0uCRCk4XXZISGrtX_d0c7OREg2Hh77ndtz05k,4438
45
+ ops/inventory/sshconfig.py,sha256=8j5hwTjAE9ua0mcIl0jMp8vE5cz6kNRWc9nQZ_WNbL4,5437
45
46
  ops/inventory/plugin/__init__.py,sha256=kSOYPdE9T0Ixe6tQUYoOl_udNbFZdVatvRf6bkf92aE,725
46
47
  ops/inventory/plugin/azr.py,sha256=8-G3mEc0WQV7ZhJ4z6X-Wo90nQ7zAzHwgLpV4l_gbjo,5910
47
- ops/inventory/plugin/cns.py,sha256=X7CUMA8ltGxDfCkvaeEHsd60g8Y_C_gJk7287UM-qHw,1763
48
- ops/inventory/plugin/ec2.py,sha256=M9Z_MJukDUyA1XtvHeiRGuTUh8A3ibHfDINJDUwLo-U,1332
48
+ ops/inventory/plugin/cns.py,sha256=7VeQSaoy4v3gDSbej7TdcpxuCC6_DkH-kLRj5Z9Urv4,1833
49
+ ops/inventory/plugin/ec2.py,sha256=ySlR8l_uPE-RLxBBxUdaGVy1EeaBbs0zP6_BLpdoj5w,1468
49
50
  ops/inventory/plugin/legacy_pcs.py,sha256=ii1kOkwD9PIaOlleJa3_yE95ojCoPlBPKSBu5naZhxw,1184
50
51
  ops/inventory/plugin/skms.py,sha256=pFMpvfbXCf0KPgEOj5oAD7UzpUZ5zL1lcN4wESOjPuI,8517
51
52
  ops/jinja/__init__.py,sha256=Nvmvb1edSAehNKYyroo26Jrxjzbu_TOCBS8Y9mMEOyA,1743
52
53
  ops/terraform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
54
  ops/terraform/terraform_cmd_generator.py,sha256=5yfzS7aOOjVDUYHwBadFvPjWKEFJWk9SEOhTZHywlG0,21728
54
- ops_cli-2.3.1.dist-info/LICENSE,sha256=ff5lJoiLrFF1nJn5pRJiuicRqMEqBn8hgWCd2aQGa4Q,11335
55
- ops_cli-2.3.1.dist-info/METADATA,sha256=BPyIZTmGXQgCcflehCFyGRo1_fy23bwMebVqp7Bfo1g,39904
56
- ops_cli-2.3.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
57
- ops_cli-2.3.1.dist-info/entry_points.txt,sha256=maaS2Tf8WvxMXckssedK13LXegD9jgHB2AT8xiEfVpQ,37
58
- ops_cli-2.3.1.dist-info/top_level.txt,sha256=enC05wWafSg8iDKIvj3gvtAtEP2kYCyN5Gmd689q-_I,4
59
- ops_cli-2.3.1.dist-info/RECORD,,
55
+ ops_cli-2.4.0.dist-info/licenses/LICENSE,sha256=ff5lJoiLrFF1nJn5pRJiuicRqMEqBn8hgWCd2aQGa4Q,11335
56
+ ops_cli-2.4.0.dist-info/METADATA,sha256=4coN_Fja0ywn0cXXVwJ_w9mpn1Xr6hMX-C9RvAPpPTU,40163
57
+ ops_cli-2.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
58
+ ops_cli-2.4.0.dist-info/entry_points.txt,sha256=maaS2Tf8WvxMXckssedK13LXegD9jgHB2AT8xiEfVpQ,37
59
+ ops_cli-2.4.0.dist-info/top_level.txt,sha256=enC05wWafSg8iDKIvj3gvtAtEP2kYCyN5Gmd689q-_I,4
60
+ ops_cli-2.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5