-
Notifications
You must be signed in to change notification settings - Fork 34
/
sshjail.py
485 lines (444 loc) · 17.5 KB
/
sshjail.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# Copyright (c) 2015-2018, Austin Hyde (@austinhyde)
from __future__ import (absolute_import, division, print_function)
import os
import pipes
from ansible.errors import AnsibleError
from ansible.plugins.connection.ssh import Connection as SSHConnection
from ansible.module_utils._text import to_text
from ansible.plugins.loader import get_shell_plugin
from contextlib import contextmanager
__metaclass__ = type
DOCUMENTATION = '''
connection: sshjail
short_description: connect via ssh client binary to jail
description:
- This connection plugin allows ansible to communicate to the target machines via normal ssh command line.
author: Austin Hyde (@austinhyde)
version_added: historical
options:
host:
description: Hostname/ip to connect to.
default: inventory_hostname
vars:
- name: ansible_host
- name: ansible_ssh_host
host_key_checking:
description: Determines if ssh should check host keys
type: boolean
ini:
- section: defaults
key: 'host_key_checking'
- section: ssh_connection
key: 'host_key_checking'
version_added: '2.5'
env:
- name: ANSIBLE_HOST_KEY_CHECKING
- name: ANSIBLE_SSH_HOST_KEY_CHECKING
version_added: '2.5'
vars:
- name: ansible_host_key_checking
version_added: '2.5'
- name: ansible_ssh_host_key_checking
version_added: '2.5'
password:
description: Authentication password for the C(remote_user). Can be supplied as CLI option.
vars:
- name: ansible_password
- name: ansible_ssh_pass
sshpass_prompt:
description: Password prompt that sshpass should search for. Supported by sshpass 1.06 and up
default: ''
ini:
- section: 'ssh_connection'
key: 'sshpass_prompt'
env:
- name: ANSIBLE_SSHPASS_PROMPT
vars:
- name: ansible_sshpass_prompt
version_added: '2.10'
ssh_args:
description: Arguments to pass to all ssh cli tools
default: '-C -o ControlMaster=auto -o ControlPersist=60s'
ini:
- section: 'ssh_connection'
key: 'ssh_args'
env:
- name: ANSIBLE_SSH_ARGS
vars:
- name: ansible_ssh_args
version_added: '2.7'
ssh_common_args:
description: Common extra args for all ssh CLI tools
ini:
- section: 'ssh_connection'
key: 'ssh_common_args'
version_added: '2.7'
env:
- name: ANSIBLE_SSH_COMMON_ARGS
version_added: '2.7'
vars:
- name: ansible_ssh_common_args
ssh_executable:
default: ssh
description:
- This defines the location of the ssh binary. It defaults to ``ssh`` which will use the first ssh binary available in $PATH.
- This option is usually not required, it might be useful when access to system ssh is restricted,
or when using ssh wrappers to connect to remote hosts.
env: [{name: ANSIBLE_SSH_EXECUTABLE}]
ini:
- {key: ssh_executable, section: ssh_connection}
#const: ANSIBLE_SSH_EXECUTABLE
version_added: "2.2"
vars:
- name: ansible_ssh_executable
version_added: '2.7'
sftp_executable:
default: sftp
description:
- This defines the location of the sftp binary. It defaults to ``sftp`` which will use the first binary available in $PATH.
env: [{name: ANSIBLE_SFTP_EXECUTABLE}]
ini:
- {key: sftp_executable, section: ssh_connection}
version_added: "2.6"
vars:
- name: ansible_sftp_executable
version_added: '2.7'
scp_executable:
default: scp
description:
- This defines the location of the scp binary. It defaults to `scp` which will use the first binary available in $PATH.
env: [{name: ANSIBLE_SCP_EXECUTABLE}]
ini:
- {key: scp_executable, section: ssh_connection}
version_added: "2.6"
vars:
- name: ansible_scp_executable
version_added: '2.7'
scp_extra_args:
description: Extra exclusive to the ``scp`` CLI
vars:
- name: ansible_scp_extra_args
env:
- name: ANSIBLE_SCP_EXTRA_ARGS
version_added: '2.7'
ini:
- key: scp_extra_args
section: ssh_connection
version_added: '2.7'
sftp_extra_args:
description: Extra exclusive to the ``sftp`` CLI
vars:
- name: ansible_sftp_extra_args
env:
- name: ANSIBLE_SFTP_EXTRA_ARGS
version_added: '2.7'
ini:
- key: sftp_extra_args
section: ssh_connection
version_added: '2.7'
ssh_extra_args:
description: Extra exclusive to the 'ssh' CLI
vars:
- name: ansible_ssh_extra_args
env:
- name: ANSIBLE_SSH_EXTRA_ARGS
version_added: '2.7'
ini:
- key: ssh_extra_args
section: ssh_connection
version_added: '2.7'
reconnection_retries:
description: Number of attempts to connect.
default: 0
type: integer
env:
- name: ANSIBLE_SSH_RETRIES
ini:
- section: connection
key: retries
- section: ssh_connection
key: retries
vars:
- name: ansible_ssh_retries
version_added: '2.7'
port:
description: Remote port to connect to.
type: int
ini:
- section: defaults
key: remote_port
env:
- name: ANSIBLE_REMOTE_PORT
vars:
- name: ansible_port
- name: ansible_ssh_port
remote_user:
description:
- User name with which to login to the remote server, normally set by the remote_user keyword.
- If no user is supplied, Ansible will let the ssh client binary choose the user as it normally
ini:
- section: defaults
key: remote_user
env:
- name: ANSIBLE_REMOTE_USER
vars:
- name: ansible_user
- name: ansible_ssh_user
pipelining:
default: ANSIBLE_PIPELINING
description:
- Pipelining reduces the number of SSH operations required to execute a module on the remote server,
by executing many Ansible modules without actual file transfer.
- This can result in a very significant performance improvement when enabled.
- However this conflicts with privilege escalation (become).
For example, when using sudo operations you must first disable 'requiretty' in the sudoers file for the target hosts,
which is why this feature is disabled by default.
env:
- name: ANSIBLE_PIPELINING
#- name: ANSIBLE_SSH_PIPELINING
ini:
- section: defaults
key: pipelining
#- section: ssh_connection
# key: pipelining
type: boolean
vars:
- name: ansible_pipelining
- name: ansible_ssh_pipelining
private_key_file:
description:
- Path to private key file to use for authentication
ini:
- section: defaults
key: private_key_file
env:
- name: ANSIBLE_PRIVATE_KEY_FILE
vars:
- name: ansible_private_key_file
- name: ansible_ssh_private_key_file
control_path:
description:
- This is the location to save ssh's ControlPath sockets, it uses ssh's variable substitution.
- Since 2.3, if null, ansible will generate a unique hash. Use `%(directory)s` to indicate where to use the control dir path setting.
env:
- name: ANSIBLE_SSH_CONTROL_PATH
ini:
- key: control_path
section: ssh_connection
vars:
- name: ansible_control_path
version_added: '2.7'
control_path_dir:
default: ~/.ansible/cp
description:
- This sets the directory to use for ssh control path if the control path setting is null.
- Also, provides the `%(directory)s` variable for the control path setting.
env:
- name: ANSIBLE_SSH_CONTROL_PATH_DIR
ini:
- section: ssh_connection
key: control_path_dir
vars:
- name: ansible_control_path_dir
version_added: '2.7'
sftp_batch_mode:
default: 'yes'
description: 'TODO: write it'
env: [{name: ANSIBLE_SFTP_BATCH_MODE}]
ini:
- {key: sftp_batch_mode, section: ssh_connection}
type: bool
vars:
- name: ansible_sftp_batch_mode
version_added: '2.7'
ssh_transfer_method:
default: smart
description:
- "Preferred method to use when transferring files over ssh"
- Setting to 'smart' (default) will try them in order, until one succeeds or they all fail
- Using 'piped' creates an ssh pipe with ``dd`` on either side to copy the data
choices: ['sftp', 'scp', 'piped', 'smart']
env: [{name: ANSIBLE_SSH_TRANSFER_METHOD}]
ini:
- {key: transfer_method, section: ssh_connection}
scp_if_ssh:
default: smart
description:
- "Prefered method to use when transfering files over ssh"
- When set to smart, Ansible will try them until one succeeds or they all fail
- If set to True, it will force 'scp', if False it will use 'sftp'
env: [{name: ANSIBLE_SCP_IF_SSH}]
ini:
- {key: scp_if_ssh, section: ssh_connection}
vars:
- name: ansible_scp_if_ssh
version_added: '2.7'
use_tty:
version_added: '2.5'
default: 'yes'
description: add -tt to ssh commands to force tty allocation
env: [{name: ANSIBLE_SSH_USETTY}]
ini:
- {key: usetty, section: ssh_connection}
type: bool
vars:
- name: ansible_ssh_use_tty
version_added: '2.7'
timeout:
default: 10
description:
- This is the default ammount of time we will wait while establishing an ssh connection
- It also controls how long we can wait to access reading the connection once established (select on the socket)
env:
- name: ANSIBLE_TIMEOUT
- name: ANSIBLE_SSH_TIMEOUT
version_added: '2.11'
ini:
- key: timeout
section: defaults
- key: timeout
section: ssh_connection
version_added: '2.11'
vars:
- name: ansible_ssh_timeout
version_added: '2.11'
cli:
- name: timeout
type: integer
'''
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
# HACK: Ansible core does classname-based validation checks, to ensure connection plugins inherit directly from a class
# named "ConnectionBase". This intermediate class works around this limitation.
class ConnectionBase(SSHConnection):
pass
class Connection(ConnectionBase):
''' ssh based connections '''
transport = 'sshjail'
def __init__(self, *args, **kwargs):
super(Connection, self).__init__(*args, **kwargs)
# self.host == jailname@jailhost
self.inventory_hostname = self.host
self.jailspec, self.host = self.host.split('@', 1)
# self.jailspec == jailname
# self.host == jailhost
# this way SSHConnection parent class uses the jailhost as the SSH remote host
# jail information loaded on first use by match_jail
self.jid = None
self.jname = None
self.jpath = None
self.connector = None
# logging.warning(self._play_context.connection)
def match_jail(self):
if self.jid is None:
code, stdout, stderr = self._jailhost_command("jls -q jid name host.hostname path")
if code != 0:
display.vvv("JLS stdout: %s" % stdout)
raise AnsibleError("jls returned non-zero!")
lines = stdout.strip().split(b'\n')
found = False
for line in lines:
if line.strip() == '':
break
jid, name, hostname, path = to_text(line).strip().split()
if name == self.jailspec or hostname == self.jailspec:
self.jid = jid
self.jname = name
self.jpath = path
found = True
break
if not found:
raise AnsibleError("failed to find a jail with name or hostname of '%s'" % self.jailspec)
def get_jail_path(self):
self.match_jail()
return self.jpath
def get_jail_id(self):
self.match_jail()
return self.jid
def get_jail_connector(self):
if self.connector is None:
code, _, _ = self._jailhost_command("which -s jailme")
if code != 0:
self.connector = 'jexec'
else:
self.connector = 'jailme'
return self.connector
def _strip_sudo(self, executable, cmd):
# Get the command without sudo
sudoless = cmd.rsplit(executable + ' -c ', 1)[1]
# Get the quotes
quotes = sudoless.partition('echo')[0]
# Get the string between the quotes
cmd = sudoless[len(quotes):-len(quotes+'?')]
# Drop the first command becasue we don't need it
cmd = cmd.split('; ', 1)[1]
return cmd
def _strip_sleep(self, cmd):
# Get the command without sleep
cmd = cmd.split(' && sleep 0', 1)[0]
# Add back trailing quote
cmd = '%s%s' % (cmd, "'")
return cmd
def _jailhost_command(self, cmd):
return super(Connection, self).exec_command(cmd, in_data=None, sudoable=True)
def exec_command(self, cmd, in_data=None, executable='/bin/sh', sudoable=True):
''' run a command in the jail '''
slpcmd = False
if '&& sleep 0' in cmd:
slpcmd = True
cmd = self._strip_sleep(cmd)
if 'sudo' in cmd:
cmd = self._strip_sudo(executable, cmd)
cmd = ' '.join([executable, '-c', pipes.quote(cmd)])
if slpcmd:
cmd = '%s %s %s %s' % (self.get_jail_connector(), self.get_jail_id(), cmd, '&& sleep 0')
else:
cmd = '%s %s %s' % (self.get_jail_connector(), self.get_jail_id(), cmd)
if self._play_context.become:
# display.debug("_low_level_execute_command(): using become for this command")
plugin = self.become
shell = get_shell_plugin(executable=executable)
cmd = plugin.build_become_command(cmd, shell)
# display.vvv("JAIL (%s) %s" % (local_cmd), host=self.host)
return super(Connection, self).exec_command(cmd, in_data, True)
def _normalize_path(self, path, prefix):
if not path.startswith(os.path.sep):
path = os.path.join(os.path.sep, path)
normpath = os.path.normpath(path)
return os.path.join(prefix, normpath[1:])
def _copy_file(self, from_file, to_file, executable='/bin/sh'):
plugin = self.become
shell = get_shell_plugin(executable=executable)
copycmd = plugin.build_become_command(' '.join(['cp', from_file, to_file]), shell)
display.vvv(u"REMOTE COPY {0} TO {1}".format(from_file, to_file), host=self.inventory_hostname)
code, stdout, stderr = self._jailhost_command(copycmd)
if code != 0:
raise AnsibleError("failed to copy file from %s to %s:\n%s\n%s" % (from_file, to_file, stdout, stderr))
@contextmanager
def tempfile(self):
code, stdout, stderr = self._jailhost_command('mktemp')
if code != 0:
raise AnsibleError("failed to make temp file:\n%s\n%s" % (stdout, stderr))
tmp = to_text(stdout.strip().split(b'\n')[-1])
code, stdout, stderr = self._jailhost_command(' '.join(['chmod 0644', tmp]))
if code != 0:
raise AnsibleError("failed to make temp file %s world readable:\n%s\n%s" % (tmp, stdout, stderr))
yield tmp
code, stdout, stderr = self._jailhost_command(' '.join(['rm', tmp]))
if code != 0:
raise AnsibleError("failed to remove temp file %s:\n%s\n%s" % (tmp, stdout, stderr))
def put_file(self, in_path, out_path):
''' transfer a file from local to remote jail '''
out_path = self._normalize_path(out_path, self.get_jail_path())
with self.tempfile() as tmp:
super(Connection, self).put_file(in_path, tmp)
self._copy_file(tmp, out_path)
def fetch_file(self, in_path, out_path):
''' fetch a file from remote to local '''
in_path = self._normalize_path(in_path, self.get_jail_path())
with self.tempfile() as tmp:
self._copy_file(in_path, tmp)
super(Connection, self).fetch_file(tmp, out_path)