mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-07 13:52:54 +00:00
normalized plugin paths and names and configs
This commit is contained in:
167
lib/ansible/plugins/connection/__init__.py
Normal file
167
lib/ansible/plugins/connection/__init__.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# (c) 2015 Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import fcntl
|
||||
import gettext
|
||||
import select
|
||||
import os
|
||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
|
||||
from functools import wraps
|
||||
from six import with_metaclass
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins import shell_loader
|
||||
|
||||
try:
|
||||
from __main__ import display
|
||||
except ImportError:
|
||||
from ansible.utils.display import Display
|
||||
display = Display()
|
||||
|
||||
__all__ = ['ConnectionBase', 'ensure_connect']
|
||||
|
||||
|
||||
def ensure_connect(func):
|
||||
@wraps(func)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
self._connect()
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
class ConnectionBase(with_metaclass(ABCMeta, object)):
|
||||
'''
|
||||
A base class for connections to contain common code.
|
||||
'''
|
||||
|
||||
has_pipelining = False
|
||||
become_methods = C.BECOME_METHODS
|
||||
|
||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||
# All these hasattrs allow subclasses to override these parameters
|
||||
if not hasattr(self, '_play_context'):
|
||||
self._play_context = play_context
|
||||
if not hasattr(self, '_new_stdin'):
|
||||
self._new_stdin = new_stdin
|
||||
if not hasattr(self, '_display'):
|
||||
self._display = display
|
||||
if not hasattr(self, '_connected'):
|
||||
self._connected = False
|
||||
|
||||
self.success_key = None
|
||||
self.prompt = None
|
||||
|
||||
# load the shell plugin for this action/connection
|
||||
if play_context.shell:
|
||||
shell_type = play_context.shell
|
||||
elif hasattr(self, '_shell_type'):
|
||||
shell_type = getattr(self, '_shell_type')
|
||||
else:
|
||||
shell_type = os.path.basename(C.DEFAULT_EXECUTABLE)
|
||||
|
||||
self._shell = shell_loader.get(shell_type)
|
||||
if not self._shell:
|
||||
raise AnsibleError("Invalid shell type specified (%s), or the plugin for that shell type is missing." % shell_type)
|
||||
|
||||
def _become_method_supported(self):
|
||||
''' Checks if the current class supports this privilege escalation method '''
|
||||
|
||||
if self._play_context.become_method in self.become_methods:
|
||||
return True
|
||||
|
||||
raise AnsibleError("Internal Error: this connection module does not support running commands via %s" % become_method)
|
||||
|
||||
def set_host_overrides(self, host):
|
||||
'''
|
||||
An optional method, which can be used to set connection plugin parameters
|
||||
from variables set on the host (or groups to which the host belongs)
|
||||
|
||||
Any connection plugin using this should first initialize its attributes in
|
||||
an overridden `def __init__(self):`, and then use `host.get_vars()` to find
|
||||
variables which may be used to set those attributes in this method.
|
||||
'''
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def transport(self):
|
||||
"""String used to identify this Connection class from other classes"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _connect(self):
|
||||
"""Connect to the host we've been initialized with"""
|
||||
|
||||
# Check if PE is supported
|
||||
if self._play_context.become:
|
||||
self.__become_method_supported()
|
||||
|
||||
@ensure_connect
|
||||
@abstractmethod
|
||||
def exec_command(self, cmd, tmp_path, in_data=None, executable=None, sudoable=True):
|
||||
"""Run a command on the remote host"""
|
||||
pass
|
||||
|
||||
@ensure_connect
|
||||
@abstractmethod
|
||||
def put_file(self, in_path, out_path):
|
||||
"""Transfer a file from local to remote"""
|
||||
pass
|
||||
|
||||
@ensure_connect
|
||||
@abstractmethod
|
||||
def fetch_file(self, in_path, out_path):
|
||||
"""Fetch a file from remote to local"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close(self):
|
||||
"""Terminate the connection"""
|
||||
pass
|
||||
|
||||
def check_become_success(self, output):
|
||||
return self._play_context.success_key in output
|
||||
|
||||
def check_password_prompt(self, output):
|
||||
if self._play_context.prompt is None:
|
||||
return False
|
||||
elif isinstance(self._play_context.prompt, basestring):
|
||||
return output.endswith(self._play_context.prompt)
|
||||
else:
|
||||
return self._play_context.prompt(output)
|
||||
|
||||
def check_incorrect_password(self, output):
|
||||
incorrect_password = gettext.dgettext(self._play_context.become_method, C.BECOME_ERROR_STRINGS[self._play_context.become_method])
|
||||
if incorrect_password in output:
|
||||
raise AnsibleError('Incorrect %s password' % self._play_context.become_method)
|
||||
|
||||
def lock_connection(self):
|
||||
f = self._play_context.connection_lockfd
|
||||
self._display.vvvv('CONNECTION: pid %d waiting for lock on %d' % (os.getpid(), f))
|
||||
fcntl.lockf(f, fcntl.LOCK_EX)
|
||||
self._display.vvvv('CONNECTION: pid %d acquired lock on %d' % (os.getpid(), f))
|
||||
|
||||
def unlock_connection(self):
|
||||
f = self._play_context.connection_lockfd
|
||||
fcntl.lockf(f, fcntl.LOCK_UN)
|
||||
self._display.vvvv('CONNECTION: pid %d released lock on %d' % (os.getpid(), f))
|
||||
381
lib/ansible/plugins/connection/accelerate.py
Normal file
381
lib/ansible/plugins/connection/accelerate.py
Normal file
@@ -0,0 +1,381 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import os
|
||||
import base64
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
from ansible.callbacks import vvv, vvvv
|
||||
from ansible.errors import AnsibleError, AnsibleFileNotFound
|
||||
from . import ConnectionBase
|
||||
from .ssh import Connection as SSHConnection
|
||||
from .paramiko_ssh import Connection as ParamikoConnection
|
||||
from ansible import utils
|
||||
from ansible import constants
|
||||
|
||||
# the chunk size to read and send, assuming mtu 1500 and
|
||||
# leaving room for base64 (+33%) encoding and header (8 bytes)
|
||||
# ((1400-8)/4)*3) = 1044
|
||||
# which leaves room for the TCP/IP header. We set this to a
|
||||
# multiple of the value to speed up file reads.
|
||||
CHUNK_SIZE=1044*20
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' raw socket accelerated connection '''
|
||||
|
||||
def __init__(self, runner, host, port, user, password, private_key_file, *args, **kwargs):
|
||||
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
self.context = None
|
||||
self.conn = None
|
||||
self.user = user
|
||||
self.key = utils.key_for_hostname(host)
|
||||
self.port = port[0]
|
||||
self.accport = port[1]
|
||||
self.is_connected = False
|
||||
self.has_pipelining = False
|
||||
self.become_methods_supported=['sudo']
|
||||
|
||||
if not self.port:
|
||||
self.port = constants.DEFAULT_REMOTE_PORT
|
||||
elif not isinstance(self.port, int):
|
||||
self.port = int(self.port)
|
||||
|
||||
if not self.accport:
|
||||
self.accport = constants.ACCELERATE_PORT
|
||||
elif not isinstance(self.accport, int):
|
||||
self.accport = int(self.accport)
|
||||
|
||||
if self.runner.original_transport == "paramiko":
|
||||
self.ssh = ParamikoConnection(
|
||||
runner=self.runner,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
user=self.user,
|
||||
password=password,
|
||||
private_key_file=private_key_file
|
||||
)
|
||||
else:
|
||||
self.ssh = SSHConnection(
|
||||
runner=self.runner,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
user=self.user,
|
||||
password=password,
|
||||
private_key_file=private_key_file
|
||||
)
|
||||
|
||||
if not getattr(self.ssh, 'shell', None):
|
||||
self.ssh.shell = utils.plugins.shell_loader.get('sh')
|
||||
|
||||
# attempt to work around shared-memory funness
|
||||
if getattr(self.runner, 'aes_keys', None):
|
||||
utils.AES_KEYS = self.runner.aes_keys
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
"""String used to identify this Connection class from other classes"""
|
||||
return 'accelerate'
|
||||
|
||||
def _execute_accelerate_module(self):
|
||||
args = "password=%s port=%s minutes=%d debug=%d ipv6=%s" % (
|
||||
base64.b64encode(self.key.__str__()),
|
||||
str(self.accport),
|
||||
constants.ACCELERATE_DAEMON_TIMEOUT,
|
||||
int(utils.VERBOSITY),
|
||||
self.runner.accelerate_ipv6,
|
||||
)
|
||||
if constants.ACCELERATE_MULTI_KEY:
|
||||
args += " multi_key=yes"
|
||||
inject = dict(password=self.key)
|
||||
if getattr(self.runner, 'accelerate_inventory_host', False):
|
||||
inject = utils.combine_vars(inject, self.runner.inventory.get_variables(self.runner.accelerate_inventory_host))
|
||||
else:
|
||||
inject = utils.combine_vars(inject, self.runner.inventory.get_variables(self.host))
|
||||
vvvv("attempting to start up the accelerate daemon...")
|
||||
self.ssh.connect()
|
||||
tmp_path = self.runner._make_tmp_path(self.ssh)
|
||||
return self.runner._execute_module(self.ssh, tmp_path, 'accelerate', args, inject=inject)
|
||||
|
||||
def connect(self, allow_ssh=True):
|
||||
''' activates the connection object '''
|
||||
|
||||
try:
|
||||
if not self.is_connected:
|
||||
wrong_user = False
|
||||
tries = 3
|
||||
self.conn = socket.socket()
|
||||
self.conn.settimeout(constants.ACCELERATE_CONNECT_TIMEOUT)
|
||||
vvvv("attempting connection to %s via the accelerated port %d" % (self.host,self.accport))
|
||||
while tries > 0:
|
||||
try:
|
||||
self.conn.connect((self.host,self.accport))
|
||||
break
|
||||
except socket.error:
|
||||
vvvv("connection to %s failed, retrying..." % self.host)
|
||||
time.sleep(0.1)
|
||||
tries -= 1
|
||||
if tries == 0:
|
||||
vvv("Could not connect via the accelerated connection, exceeded # of tries")
|
||||
raise AnsibleError("FAILED")
|
||||
elif wrong_user:
|
||||
vvv("Restarting daemon with a different remote_user")
|
||||
raise AnsibleError("WRONG_USER")
|
||||
|
||||
self.conn.settimeout(constants.ACCELERATE_TIMEOUT)
|
||||
if not self.validate_user():
|
||||
# the accelerated daemon was started with a
|
||||
# different remote_user. The above command
|
||||
# should have caused the accelerate daemon to
|
||||
# shutdown, so we'll reconnect.
|
||||
wrong_user = True
|
||||
|
||||
except AnsibleError as e:
|
||||
if allow_ssh:
|
||||
if "WRONG_USER" in e:
|
||||
vvv("Switching users, waiting for the daemon on %s to shutdown completely..." % self.host)
|
||||
time.sleep(5)
|
||||
vvv("Falling back to ssh to startup accelerated mode")
|
||||
res = self._execute_accelerate_module()
|
||||
if not res.is_successful():
|
||||
raise AnsibleError("Failed to launch the accelerated daemon on %s (reason: %s)" % (self.host,res.result.get('msg')))
|
||||
return self.connect(allow_ssh=False)
|
||||
else:
|
||||
raise AnsibleError("Failed to connect to %s:%s" % (self.host,self.accport))
|
||||
self.is_connected = True
|
||||
return self
|
||||
|
||||
def send_data(self, data):
|
||||
packed_len = struct.pack('!Q',len(data))
|
||||
return self.conn.sendall(packed_len + data)
|
||||
|
||||
def recv_data(self):
|
||||
header_len = 8 # size of a packed unsigned long long
|
||||
data = b""
|
||||
try:
|
||||
vvvv("%s: in recv_data(), waiting for the header" % self.host)
|
||||
while len(data) < header_len:
|
||||
d = self.conn.recv(header_len - len(data))
|
||||
if not d:
|
||||
vvvv("%s: received nothing, bailing out" % self.host)
|
||||
return None
|
||||
data += d
|
||||
vvvv("%s: got the header, unpacking" % self.host)
|
||||
data_len = struct.unpack('!Q',data[:header_len])[0]
|
||||
data = data[header_len:]
|
||||
vvvv("%s: data received so far (expecting %d): %d" % (self.host,data_len,len(data)))
|
||||
while len(data) < data_len:
|
||||
d = self.conn.recv(data_len - len(data))
|
||||
if not d:
|
||||
vvvv("%s: received nothing, bailing out" % self.host)
|
||||
return None
|
||||
vvvv("%s: received %d bytes" % (self.host, len(d)))
|
||||
data += d
|
||||
vvvv("%s: received all of the data, returning" % self.host)
|
||||
return data
|
||||
except socket.timeout:
|
||||
raise AnsibleError("timed out while waiting to receive data")
|
||||
|
||||
def validate_user(self):
|
||||
'''
|
||||
Checks the remote uid of the accelerated daemon vs. the
|
||||
one specified for this play and will cause the accel
|
||||
daemon to exit if they don't match
|
||||
'''
|
||||
|
||||
vvvv("%s: sending request for validate_user" % self.host)
|
||||
data = dict(
|
||||
mode='validate_user',
|
||||
username=self.user,
|
||||
)
|
||||
data = utils.jsonify(data)
|
||||
data = utils.encrypt(self.key, data)
|
||||
if self.send_data(data):
|
||||
raise AnsibleError("Failed to send command to %s" % self.host)
|
||||
|
||||
vvvv("%s: waiting for validate_user response" % self.host)
|
||||
while True:
|
||||
# we loop here while waiting for the response, because a
|
||||
# long running command may cause us to receive keepalive packets
|
||||
# ({"pong":"true"}) rather than the response we want.
|
||||
response = self.recv_data()
|
||||
if not response:
|
||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
||||
response = utils.decrypt(self.key, response)
|
||||
response = utils.parse_json(response)
|
||||
if "pong" in response:
|
||||
# it's a keepalive, go back to waiting
|
||||
vvvv("%s: received a keepalive packet" % self.host)
|
||||
continue
|
||||
else:
|
||||
vvvv("%s: received the validate_user response: %s" % (self.host, response))
|
||||
break
|
||||
|
||||
if response.get('failed'):
|
||||
return False
|
||||
else:
|
||||
return response.get('rc') == 0
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the remote host '''
|
||||
|
||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
||||
|
||||
if in_data:
|
||||
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
if executable == "":
|
||||
executable = constants.DEFAULT_EXECUTABLE
|
||||
|
||||
if self.runner.become and sudoable:
|
||||
cmd, prompt, success_key = utils.make_become_cmd(cmd, become_user, executable, self.runner.become_method, '', self.runner.become_exe)
|
||||
|
||||
vvv("EXEC COMMAND %s" % cmd)
|
||||
|
||||
data = dict(
|
||||
mode='command',
|
||||
cmd=cmd,
|
||||
tmp_path=tmp_path,
|
||||
executable=executable,
|
||||
)
|
||||
data = utils.jsonify(data)
|
||||
data = utils.encrypt(self.key, data)
|
||||
if self.send_data(data):
|
||||
raise AnsibleError("Failed to send command to %s" % self.host)
|
||||
|
||||
while True:
|
||||
# we loop here while waiting for the response, because a
|
||||
# long running command may cause us to receive keepalive packets
|
||||
# ({"pong":"true"}) rather than the response we want.
|
||||
response = self.recv_data()
|
||||
if not response:
|
||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
||||
response = utils.decrypt(self.key, response)
|
||||
response = utils.parse_json(response)
|
||||
if "pong" in response:
|
||||
# it's a keepalive, go back to waiting
|
||||
vvvv("%s: received a keepalive packet" % self.host)
|
||||
continue
|
||||
else:
|
||||
vvvv("%s: received the response" % self.host)
|
||||
break
|
||||
|
||||
return (response.get('rc',None), '', response.get('stdout',''), response.get('stderr',''))
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
|
||||
''' transfer a file from local to remote '''
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
||||
|
||||
if not os.path.exists(in_path):
|
||||
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
|
||||
fd = file(in_path, 'rb')
|
||||
fstat = os.stat(in_path)
|
||||
try:
|
||||
vvv("PUT file is %d bytes" % fstat.st_size)
|
||||
last = False
|
||||
while fd.tell() <= fstat.st_size and not last:
|
||||
vvvv("file position currently %ld, file size is %ld" % (fd.tell(), fstat.st_size))
|
||||
data = fd.read(CHUNK_SIZE)
|
||||
if fd.tell() >= fstat.st_size:
|
||||
last = True
|
||||
data = dict(mode='put', data=base64.b64encode(data), out_path=out_path, last=last)
|
||||
if self.runner.become:
|
||||
data['user'] = self.runner.become_user
|
||||
data = utils.jsonify(data)
|
||||
data = utils.encrypt(self.key, data)
|
||||
|
||||
if self.send_data(data):
|
||||
raise AnsibleError("failed to send the file to %s" % self.host)
|
||||
|
||||
response = self.recv_data()
|
||||
if not response:
|
||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
||||
response = utils.decrypt(self.key, response)
|
||||
response = utils.parse_json(response)
|
||||
|
||||
if response.get('failed',False):
|
||||
raise AnsibleError("failed to put the file in the requested location")
|
||||
finally:
|
||||
fd.close()
|
||||
vvvv("waiting for final response after PUT")
|
||||
response = self.recv_data()
|
||||
if not response:
|
||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
||||
response = utils.decrypt(self.key, response)
|
||||
response = utils.parse_json(response)
|
||||
|
||||
if response.get('failed',False):
|
||||
raise AnsibleError("failed to put the file in the requested location")
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' save a remote file to the specified path '''
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
||||
|
||||
data = dict(mode='fetch', in_path=in_path)
|
||||
data = utils.jsonify(data)
|
||||
data = utils.encrypt(self.key, data)
|
||||
if self.send_data(data):
|
||||
raise AnsibleError("failed to initiate the file fetch with %s" % self.host)
|
||||
|
||||
fh = open(out_path, "w")
|
||||
try:
|
||||
bytes = 0
|
||||
while True:
|
||||
response = self.recv_data()
|
||||
if not response:
|
||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
||||
response = utils.decrypt(self.key, response)
|
||||
response = utils.parse_json(response)
|
||||
if response.get('failed', False):
|
||||
raise AnsibleError("Error during file fetch, aborting")
|
||||
out = base64.b64decode(response['data'])
|
||||
fh.write(out)
|
||||
bytes += len(out)
|
||||
# send an empty response back to signify we
|
||||
# received the last chunk without errors
|
||||
data = utils.jsonify(dict())
|
||||
data = utils.encrypt(self.key, data)
|
||||
if self.send_data(data):
|
||||
raise AnsibleError("failed to send ack during file fetch")
|
||||
if response.get('last', False):
|
||||
break
|
||||
finally:
|
||||
# we don't currently care about this final response,
|
||||
# we just receive it and drop it. It may be used at some
|
||||
# point in the future or we may just have the put/fetch
|
||||
# operations not send back a final response at all
|
||||
response = self.recv_data()
|
||||
vvv("FETCH wrote %d bytes to %s" % (bytes, out_path))
|
||||
fh.close()
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection '''
|
||||
# Be a good citizen
|
||||
try:
|
||||
self.conn.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
167
lib/ansible/plugins/connection/chroot.py
Normal file
167
lib/ansible/plugins/connection/chroot.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||
# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import distutils.spawn
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import traceback
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.connections import ConnectionBase
|
||||
from ansible.utils.path import is_executable
|
||||
from ansible.utils.unicode import to_bytes
|
||||
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' Local chroot based connections '''
|
||||
|
||||
BUFSIZE = 65536
|
||||
has_pipelining = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(Connection, self).__init__(*args, **kwargs)
|
||||
|
||||
self.chroot = self._play_context.remote_addr
|
||||
|
||||
if os.geteuid() != 0:
|
||||
raise AnsibleError("chroot connection requires running as root")
|
||||
|
||||
# we're running as root on the local system so do some
|
||||
# trivial checks for ensuring 'host' is actually a chroot'able dir
|
||||
if not os.path.isdir(self.chroot):
|
||||
raise AnsibleError("%s is not a directory" % self.chroot)
|
||||
|
||||
chrootsh = os.path.join(self.chroot, 'bin/sh')
|
||||
if not is_executable(chrootsh):
|
||||
raise AnsibleError("%s does not look like a chrootable dir (/bin/sh missing)" % self.chroot)
|
||||
|
||||
self.chroot_cmd = distutils.spawn.find_executable('chroot')
|
||||
if not self.chroot_cmd:
|
||||
raise AnsibleError("chroot command not found in PATH")
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
''' used to identify this connection object '''
|
||||
return 'chroot'
|
||||
|
||||
def _connect(self, port=None):
|
||||
''' connect to the chroot; nothing to do here '''
|
||||
|
||||
self._display.vvv("THIS IS A LOCAL CHROOT DIR", host=self.chroot)
|
||||
|
||||
return self
|
||||
|
||||
def _generate_cmd(self, executable, cmd):
|
||||
if executable:
|
||||
local_cmd = [self.chroot_cmd, self.chroot, executable, '-c', cmd]
|
||||
else:
|
||||
# Prev to python2.7.3, shlex couldn't handle unicode type strings
|
||||
cmd = to_bytes(cmd)
|
||||
cmd = shlex.split(cmd)
|
||||
local_cmd = [self.chroot_cmd, self.chroot]
|
||||
local_cmd += cmd
|
||||
return local_cmd
|
||||
|
||||
def _buffered_exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None, stdin=subprocess.PIPE):
|
||||
''' run a command on the chroot. This is only needed for implementing
|
||||
put_file() get_file() so that we don't have to read the whole file
|
||||
into memory.
|
||||
|
||||
compared to exec_command() it looses some niceties like being able to
|
||||
return the process's exit code immediately.
|
||||
'''
|
||||
|
||||
if sudoable and self._play_context.become and self._play_context.become_method not in self.become_methods_supported:
|
||||
raise AnsibleError("Internal Error: this module does not support running commands via %s" % self._play_context.become_method)
|
||||
|
||||
if in_data:
|
||||
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
# We enter zone as root so we ignore privilege escalation (probably need to fix in case we have to become a specific used [ex: postgres admin])?
|
||||
local_cmd = self._generate_cmd(executable, cmd)
|
||||
|
||||
self._display.vvv("EXEC %s" % (local_cmd), host=self.chroot)
|
||||
# FIXME: cwd= needs to be set to the basedir of the playbook, which
|
||||
# should come from loader, but is not in the connection plugins
|
||||
p = subprocess.Popen(local_cmd, shell=False,
|
||||
stdin=stdin,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
return p
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the chroot '''
|
||||
|
||||
p = self._buffered_exec_command(cmd, tmp_path, become_user, sudoable, executable, in_data)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to chroot '''
|
||||
|
||||
self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self.chroot)
|
||||
|
||||
try:
|
||||
with open(in_path, 'rb') as in_file:
|
||||
try:
|
||||
p = self._buffered_exec_command('dd of=%s bs=%s' % (out_path, self.BUFSIZE), None, stdin=in_file)
|
||||
except OSError:
|
||||
raise AnsibleError("chroot connection requires dd command in the chroot")
|
||||
try:
|
||||
stdout, stderr = p.communicate()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
|
||||
if p.returncode != 0:
|
||||
raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
||||
except IOError:
|
||||
raise AnsibleError("file or module does not exist at: %s" % in_path)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from chroot to local '''
|
||||
|
||||
self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self.chroot)
|
||||
|
||||
try:
|
||||
p = self._buffered_exec_command('dd if=%s bs=%s' % (in_path, self.BUFSIZE), None)
|
||||
except OSError:
|
||||
raise AnsibleError("chroot connection requires dd command in the chroot")
|
||||
|
||||
with open(out_path, 'wb+') as out_file:
|
||||
try:
|
||||
chunk = p.stdout.read(self.BUFSIZE)
|
||||
while chunk:
|
||||
out_file.write(chunk)
|
||||
chunk = p.stdout.read(self.BUFSIZE)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
pass
|
||||
162
lib/ansible/plugins/connection/docker.py
Normal file
162
lib/ansible/plugins/connection/docker.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# Based on the chroot connection plugin by Maykel Moya
|
||||
#
|
||||
# Connection plugin for configuring docker containers
|
||||
# (c) 2014, Lorin Hochstein
|
||||
# (c) 2015, Leendert Brouwer
|
||||
#
|
||||
# Maintainer: Leendert Brouwer (https://github.com/objectified)
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import re
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
import ansible.constants as C
|
||||
|
||||
from ansible import errors
|
||||
from ansible.plugins.connections import ConnectionBase
|
||||
|
||||
BUFSIZE = 65536
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
|
||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||
|
||||
if 'docker_command' in kwargs:
|
||||
self.docker_cmd = kwargs['docker_command']
|
||||
else:
|
||||
self.docker_cmd = 'docker'
|
||||
|
||||
self.can_copy_bothways = False
|
||||
|
||||
docker_version = self._get_docker_version()
|
||||
if LooseVersion(docker_version) >= LooseVersion('1.8.0'):
|
||||
self.can_copy_bothways = True
|
||||
|
||||
def _get_docker_version(self):
|
||||
|
||||
def sanitize_version(version):
|
||||
return re.sub('[^0-9a-zA-Z\.]', '', version)
|
||||
|
||||
cmd = [self.docker_cmd, 'version']
|
||||
|
||||
cmd_output = subprocess.check_output(cmd)
|
||||
|
||||
for line in cmd_output.split('\n'):
|
||||
if line.startswith('Server version:'): # old docker versions
|
||||
return sanitize_version(line.split()[2])
|
||||
|
||||
# no result yet, must be newer Docker version
|
||||
new_docker_cmd = [
|
||||
self.docker_cmd,
|
||||
'version', '--format', "'{{.Server.Version}}'"
|
||||
]
|
||||
|
||||
cmd_output = subprocess.check_output(new_docker_cmd)
|
||||
|
||||
return sanitize_version(cmd_output)
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
return 'docker'
|
||||
|
||||
def _connect(self, port=None):
|
||||
""" Connect to the container. Nothing to do """
|
||||
if not self._connected:
|
||||
self._display.vvv("ESTABLISH LOCAL CONNECTION FOR USER: {0}".format(
|
||||
self._play_context.remote_user, host=self._play_context.remote_addr)
|
||||
)
|
||||
self._connected = True
|
||||
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path, in_data=None, sudoable=False):
|
||||
""" Run a command on the local host """
|
||||
super(Connection, self).exec_command(cmd, tmp_path, in_data=in_data, sudoable=sudoable)
|
||||
|
||||
# Don't currently support su
|
||||
if in_data:
|
||||
raise errors.AnsibleError("Internal Error: this module does not "
|
||||
"support optimized module pipelining")
|
||||
|
||||
executable = C.DEFAULT_EXECUTABLE.split()[0] if C.DEFAULT_EXECUTABLE else None
|
||||
if executable:
|
||||
local_cmd = [self.docker_cmd, "exec", self._play_context.remote_addr, executable,
|
||||
'-c', cmd]
|
||||
else:
|
||||
local_cmd = '%s exec "%s" %s' % (self.docker_cmd, self._play_context.remote_addr, cmd)
|
||||
|
||||
self._display.vvv("EXEC %s" % (local_cmd), host=self._play_context.remote_addr)
|
||||
p = subprocess.Popen(local_cmd,
|
||||
shell=isinstance(local_cmd, basestring),
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
# Docker doesn't have native support for copying files into running
|
||||
# containers, so we use docker exec to implement this
|
||||
def put_file(self, in_path, out_path):
|
||||
""" Transfer a file from local to container """
|
||||
if not os.path.exists(in_path):
|
||||
raise errors.AnsibleFileNotFound(
|
||||
"file or module does not exist: %s" % in_path)
|
||||
|
||||
self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
|
||||
|
||||
if self.can_copy_bothways: # only docker >= 1.8.1 can do this natively
|
||||
args = [
|
||||
self.docker_cmd,
|
||||
"cp",
|
||||
"%s" % in_path,
|
||||
"%s:%s" % (self._play_context.remote_addr, out_path)
|
||||
]
|
||||
subprocess.check_call(args)
|
||||
else:
|
||||
args = [self.docker_cmd, "exec", "-i", self._play_context.remote_addr, "bash", "-c",
|
||||
"dd of=%s bs=%s" % (format(out_path), BUFSIZE)]
|
||||
|
||||
p = subprocess.Popen(args, stdin=open(in_path),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p.communicate()
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
""" Fetch a file from container to local. """
|
||||
# out_path is the final file path, but docker takes a directory, not a
|
||||
# file path
|
||||
out_dir = os.path.dirname(out_path)
|
||||
|
||||
args = [self.docker_cmd, "cp", "%s:%s" % (self._play_context.remote_addr, in_path), out_dir]
|
||||
|
||||
self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p.communicate()
|
||||
|
||||
# Rename if needed
|
||||
actual_out_path = os.path.join(out_dir, os.path.basename(in_path))
|
||||
if actual_out_path != out_path:
|
||||
os.rename(actual_out_path, out_path)
|
||||
|
||||
def close(self):
|
||||
""" Terminate the connection. Nothing to do for Docker"""
|
||||
self._connected = False
|
||||
99
lib/ansible/plugins/connection/funcd.py
Normal file
99
lib/ansible/plugins/connection/funcd.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# ---
|
||||
# The func transport permit to use ansible over func. For people who have already setup
|
||||
# func and that wish to play with ansible, this permit to move gradually to ansible
|
||||
# without having to redo completely the setup of the network.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
HAVE_FUNC=False
|
||||
try:
|
||||
import func.overlord.client as fc
|
||||
HAVE_FUNC=True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import os
|
||||
from ansible.callbacks import vvv
|
||||
from ansible import errors
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
|
||||
class Connection(object):
|
||||
''' Func-based connections '''
|
||||
|
||||
def __init__(self, runner, host, port, *args, **kwargs):
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
self.has_pipelining = False
|
||||
# port is unused, this go on func
|
||||
self.port = port
|
||||
|
||||
def connect(self, port=None):
|
||||
if not HAVE_FUNC:
|
||||
raise errors.AnsibleError("func is not installed")
|
||||
|
||||
self.client = fc.Client(self.host)
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False,
|
||||
executable='/bin/sh', in_data=None):
|
||||
''' run a command on the remote minion '''
|
||||
|
||||
if in_data:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
# totally ignores privlege escalation
|
||||
vvv("EXEC %s" % (cmd), host=self.host)
|
||||
p = self.client.command.run(cmd)[self.host]
|
||||
return (p[0], '', p[1], p[2])
|
||||
|
||||
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 put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to remote '''
|
||||
|
||||
out_path = self._normalize_path(out_path, '/')
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
||||
self.client.local.copyfile.send(in_path, out_path)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from remote to local '''
|
||||
|
||||
in_path = self._normalize_path(in_path, '/')
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
||||
# need to use a tmp dir due to difference of semantic for getfile
|
||||
# ( who take a # directory as destination) and fetch_file, who
|
||||
# take a file directly
|
||||
tmpdir = tempfile.mkdtemp(prefix="func_ansible")
|
||||
self.client.local.getfile.get(in_path, tmpdir)
|
||||
shutil.move(os.path.join(tmpdir, self.host, os.path.basename(in_path)),
|
||||
out_path)
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
pass
|
||||
184
lib/ansible/plugins/connection/jail.py
Normal file
184
lib/ansible/plugins/connection/jail.py
Normal file
@@ -0,0 +1,184 @@
|
||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# and chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
||||
# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import distutils.spawn
|
||||
import traceback
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
from ansible import errors
|
||||
from ansible.utils.unicode import to_bytes
|
||||
from ansible.callbacks import vvv
|
||||
import ansible.constants as C
|
||||
|
||||
BUFSIZE = 65536
|
||||
|
||||
class Connection(object):
|
||||
''' Local BSD Jail based connections '''
|
||||
|
||||
def _search_executable(self, executable):
|
||||
cmd = distutils.spawn.find_executable(executable)
|
||||
if not cmd:
|
||||
raise errors.AnsibleError("%s command not found in PATH") % executable
|
||||
return cmd
|
||||
|
||||
def list_jails(self):
|
||||
p = subprocess.Popen([self.jls_cmd, '-q', 'name'],
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
return stdout.split()
|
||||
|
||||
def get_jail_path(self):
|
||||
p = subprocess.Popen([self.jls_cmd, '-j', self.jail, '-q', 'path'],
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
# remove \n
|
||||
return stdout[:-1]
|
||||
|
||||
def __init__(self, runner, host, port, *args, **kwargs):
|
||||
self.jail = host
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
self.has_pipelining = False
|
||||
self.become_methods_supported=C.BECOME_METHODS
|
||||
|
||||
if os.geteuid() != 0:
|
||||
raise errors.AnsibleError("jail connection requires running as root")
|
||||
|
||||
self.jls_cmd = self._search_executable('jls')
|
||||
self.jexec_cmd = self._search_executable('jexec')
|
||||
|
||||
if not self.jail in self.list_jails():
|
||||
raise errors.AnsibleError("incorrect jail name %s" % self.jail)
|
||||
|
||||
|
||||
self.host = host
|
||||
# port is unused, since this is local
|
||||
self.port = port
|
||||
|
||||
def connect(self, port=None):
|
||||
''' connect to the jail; nothing to do here '''
|
||||
|
||||
vvv("THIS IS A LOCAL JAIL DIR", host=self.jail)
|
||||
|
||||
return self
|
||||
|
||||
# a modifier
|
||||
def _generate_cmd(self, executable, cmd):
|
||||
if executable:
|
||||
local_cmd = [self.jexec_cmd, self.jail, executable, '-c', cmd]
|
||||
else:
|
||||
# Prev to python2.7.3, shlex couldn't handle unicode type strings
|
||||
cmd = to_bytes(cmd)
|
||||
cmd = shlex.split(cmd)
|
||||
local_cmd = [self.jexec_cmd, self.jail]
|
||||
local_cmd += cmd
|
||||
return local_cmd
|
||||
|
||||
def _buffered_exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None, stdin=subprocess.PIPE):
|
||||
''' run a command on the jail. This is only needed for implementing
|
||||
put_file() get_file() so that we don't have to read the whole file
|
||||
into memory.
|
||||
|
||||
compared to exec_command() it looses some niceties like being able to
|
||||
return the process's exit code immediately.
|
||||
'''
|
||||
|
||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
||||
|
||||
if in_data:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
# We enter zone as root so we ignore privilege escalation (probably need to fix in case we have to become a specific used [ex: postgres admin])?
|
||||
local_cmd = self._generate_cmd(executable, cmd)
|
||||
|
||||
vvv("EXEC %s" % (local_cmd), host=self.jail)
|
||||
p = subprocess.Popen(local_cmd, shell=False,
|
||||
cwd=self.runner.basedir,
|
||||
stdin=stdin,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
return p
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the jail '''
|
||||
|
||||
p = self._buffered_exec_command(cmd, tmp_path, become_user, sudoable, executable, in_data)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to jail '''
|
||||
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.jail)
|
||||
|
||||
try:
|
||||
with open(in_path, 'rb') as in_file:
|
||||
try:
|
||||
p = self._buffered_exec_command('dd of=%s bs=%s' % (out_path, BUFSIZE), None, stdin=in_file)
|
||||
except OSError:
|
||||
raise errors.AnsibleError("jail connection requires dd command in the jail")
|
||||
try:
|
||||
stdout, stderr = p.communicate()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
|
||||
if p.returncode != 0:
|
||||
raise errors.AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
||||
except IOError:
|
||||
raise errors.AnsibleError("file or module does not exist at: %s" % in_path)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from jail to local '''
|
||||
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.jail)
|
||||
|
||||
try:
|
||||
p = self._buffered_exec_command('dd if=%s bs=%s' % (in_path, BUFSIZE), None)
|
||||
except OSError:
|
||||
raise errors.AnsibleError("jail connection requires dd command in the jail")
|
||||
|
||||
with open(out_path, 'wb+') as out_file:
|
||||
try:
|
||||
chunk = p.stdout.read(BUFSIZE)
|
||||
while chunk:
|
||||
out_file.write(chunk)
|
||||
chunk = p.stdout.read(BUFSIZE)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise errors.AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
pass
|
||||
131
lib/ansible/plugins/connection/libvirt_lxc.py
Normal file
131
lib/ansible/plugins/connection/libvirt_lxc.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import distutils.spawn
|
||||
import os
|
||||
import subprocess
|
||||
from ansible import errors
|
||||
from ansible.callbacks import vvv
|
||||
import ansible.constants as C
|
||||
|
||||
class Connection(object):
|
||||
''' Local lxc based connections '''
|
||||
|
||||
def _search_executable(self, executable):
|
||||
cmd = distutils.spawn.find_executable(executable)
|
||||
if not cmd:
|
||||
raise errors.AnsibleError("%s command not found in PATH") % executable
|
||||
return cmd
|
||||
|
||||
def _check_domain(self, domain):
|
||||
p = subprocess.Popen([self.cmd, '-q', '-c', 'lxc:///', 'dominfo', domain],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p.communicate()
|
||||
if p.returncode:
|
||||
raise errors.AnsibleError("%s is not a lxc defined in libvirt" % domain)
|
||||
|
||||
def __init__(self, runner, host, port, *args, **kwargs):
|
||||
self.lxc = host
|
||||
|
||||
self.cmd = self._search_executable('virsh')
|
||||
|
||||
self._check_domain(host)
|
||||
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
# port is unused, since this is local
|
||||
self.port = port
|
||||
self.become_methods_supported=C.BECOME_METHODS
|
||||
|
||||
def connect(self, port=None):
|
||||
''' connect to the lxc; nothing to do here '''
|
||||
|
||||
vvv("THIS IS A LOCAL LXC DIR", host=self.lxc)
|
||||
|
||||
return self
|
||||
|
||||
def _generate_cmd(self, executable, cmd):
|
||||
if executable:
|
||||
local_cmd = [self.cmd, '-q', '-c', 'lxc:///', 'lxc-enter-namespace', self.lxc, '--', executable , '-c', cmd]
|
||||
else:
|
||||
local_cmd = '%s -q -c lxc:/// lxc-enter-namespace %s -- %s' % (self.cmd, self.lxc, cmd)
|
||||
return local_cmd
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user, sudoable=False, executable='/bin/sh', in_data=None):
|
||||
''' run a command on the chroot '''
|
||||
|
||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
||||
|
||||
if in_data:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
# We ignore privilege escalation!
|
||||
local_cmd = self._generate_cmd(executable, cmd)
|
||||
|
||||
vvv("EXEC %s" % (local_cmd), host=self.lxc)
|
||||
p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
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 put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to lxc '''
|
||||
|
||||
out_path = self._normalize_path(out_path, '/')
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.lxc)
|
||||
|
||||
local_cmd = [self.cmd, '-q', '-c', 'lxc:///', 'lxc-enter-namespace', self.lxc, '--', '/bin/tee', out_path]
|
||||
vvv("EXEC %s" % (local_cmd), host=self.lxc)
|
||||
|
||||
p = subprocess.Popen(local_cmd, cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate(open(in_path,'rb').read())
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from lxc to local '''
|
||||
|
||||
in_path = self._normalize_path(in_path, '/')
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.lxc)
|
||||
|
||||
local_cmd = [self.cmd, '-q', '-c', 'lxc:///', 'lxc-enter-namespace', self.lxc, '--', '/bin/cat', in_path]
|
||||
vvv("EXEC %s" % (local_cmd), host=self.lxc)
|
||||
|
||||
p = subprocess.Popen(local_cmd, cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
open(out_path,'wb').write(stdout)
|
||||
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
pass
|
||||
128
lib/ansible/plugins/connection/local.py
Normal file
128
lib/ansible/plugins/connection/local.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# (c) 2015 Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import traceback
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import select
|
||||
import fcntl
|
||||
|
||||
import ansible.constants as C
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleFileNotFound
|
||||
from ansible.plugins.connections import ConnectionBase
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' Local based connections '''
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
''' used to identify this connection object '''
|
||||
return 'local'
|
||||
|
||||
def _connect(self, port=None):
|
||||
''' connect to the local host; nothing to do here '''
|
||||
|
||||
if not self._connected:
|
||||
self._display.vvv("ESTABLISH LOCAL CONNECTION FOR USER: {0}".format(self._play_context.remote_user, host=self._play_context.remote_addr))
|
||||
self._connected = True
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path, in_data=None, sudoable=True):
|
||||
''' run a command on the local host '''
|
||||
|
||||
super(Connection, self).exec_command(cmd, tmp_path, in_data=in_data, sudoable=sudoable)
|
||||
|
||||
self._display.debug("in local.exec_command()")
|
||||
|
||||
if in_data:
|
||||
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
executable = C.DEFAULT_EXECUTABLE.split()[0] if C.DEFAULT_EXECUTABLE else None
|
||||
|
||||
self._display.vvv("{0} EXEC {1}".format(self._play_context.remote_addr, cmd))
|
||||
# FIXME: cwd= needs to be set to the basedir of the playbook
|
||||
self._display.debug("opening command with Popen()")
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
shell=isinstance(cmd, basestring),
|
||||
executable=executable, #cwd=...
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
self._display.debug("done running command with Popen()")
|
||||
|
||||
if self._play_context.prompt and sudoable:
|
||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
become_output = ''
|
||||
while not self.check_become_success(become_output) and not self.check_password_prompt(become_output):
|
||||
|
||||
rfd, wfd, efd = select.select([p.stdout, p.stderr], [], [p.stdout, p.stderr], self._play_context.timeout)
|
||||
if p.stdout in rfd:
|
||||
chunk = p.stdout.read()
|
||||
elif p.stderr in rfd:
|
||||
chunk = p.stderr.read()
|
||||
else:
|
||||
stdout, stderr = p.communicate()
|
||||
raise AnsibleError('timeout waiting for privilege escalation password prompt:\n' + become_output)
|
||||
if not chunk:
|
||||
stdout, stderr = p.communicate()
|
||||
raise AnsibleError('privilege output closed while waiting for password prompt:\n' + become_output)
|
||||
become_output += chunk
|
||||
if not self.check_become_success(become_output):
|
||||
p.stdin.write(self._play_context.become_pass + '\n')
|
||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
|
||||
self._display.debug("getting output with communicate()")
|
||||
stdout, stderr = p.communicate()
|
||||
self._display.debug("done communicating")
|
||||
|
||||
self._display.debug("done with local.exec_command()")
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to local '''
|
||||
|
||||
super(Connection, self).put_file(in_path, out_path)
|
||||
|
||||
self._display.vvv("{0} PUT {1} TO {2}".format(self._play_context.remote_addr, in_path, out_path))
|
||||
if not os.path.exists(in_path):
|
||||
raise AnsibleFileNotFound("file or module does not exist: {0}".format(in_path))
|
||||
try:
|
||||
shutil.copyfile(in_path, out_path)
|
||||
except shutil.Error:
|
||||
raise AnsibleError("failed to copy: {0} and {1} are the same".format(in_path, out_path))
|
||||
except IOError as e:
|
||||
raise AnsibleError("failed to transfer file to {0}: {1}".format(out_path, e))
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from local to local -- for copatibility '''
|
||||
|
||||
super(Connection, self).fetch_file(in_path, out_path)
|
||||
|
||||
self._display.vvv("{0} FETCH {1} TO {2}".format(self._play_context.remote_addr, in_path, out_path))
|
||||
self.put_file(in_path, out_path)
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
self._connected = False
|
||||
406
lib/ansible/plugins/connection/paramiko_ssh.py
Normal file
406
lib/ansible/plugins/connection/paramiko_ssh.py
Normal file
@@ -0,0 +1,406 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
# ---
|
||||
# The paramiko transport is provided because many distributions, in particular EL6 and before
|
||||
# do not support ControlPersist in their SSH implementations. This is needed on the Ansible
|
||||
# control machine to be reasonably efficient with connections. Thus paramiko is faster
|
||||
# for most users on these platforms. Users with ControlPersist capability can consider
|
||||
# using -c ssh or configuring the transport in ansible.cfg.
|
||||
|
||||
import warnings
|
||||
import os
|
||||
import pipes
|
||||
import socket
|
||||
import random
|
||||
import logging
|
||||
import tempfile
|
||||
import traceback
|
||||
import fcntl
|
||||
import re
|
||||
import sys
|
||||
|
||||
from termios import tcflush, TCIFLUSH
|
||||
from binascii import hexlify
|
||||
|
||||
from six import iteritems
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
|
||||
from ansible.plugins.connections import ConnectionBase
|
||||
from ansible.utils.path import makedirs_safe
|
||||
|
||||
AUTHENTICITY_MSG="""
|
||||
paramiko: The authenticity of host '%s' can't be established.
|
||||
The %s key fingerprint is %s.
|
||||
Are you sure you want to continue connecting (yes/no)?
|
||||
"""
|
||||
|
||||
# prevent paramiko warning noise -- see http://stackoverflow.com/questions/3920502/
|
||||
HAVE_PARAMIKO=False
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
try:
|
||||
import paramiko
|
||||
HAVE_PARAMIKO=True
|
||||
logging.getLogger("paramiko").setLevel(logging.WARNING)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class MyAddPolicy(object):
|
||||
"""
|
||||
Based on AutoAddPolicy in paramiko so we can determine when keys are added
|
||||
and also prompt for input.
|
||||
|
||||
Policy for automatically adding the hostname and new host key to the
|
||||
local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
|
||||
"""
|
||||
|
||||
def __init__(self, new_stdin, connection):
|
||||
self._new_stdin = new_stdin
|
||||
self.connection = connection
|
||||
|
||||
def missing_host_key(self, client, hostname, key):
|
||||
|
||||
if C.HOST_KEY_CHECKING:
|
||||
|
||||
self.connection.lock_connection()
|
||||
|
||||
old_stdin = sys.stdin
|
||||
sys.stdin = self._new_stdin
|
||||
|
||||
# clear out any premature input on sys.stdin
|
||||
tcflush(sys.stdin, TCIFLUSH)
|
||||
|
||||
fingerprint = hexlify(key.get_fingerprint())
|
||||
ktype = key.get_name()
|
||||
|
||||
inp = raw_input(AUTHENTICITY_MSG % (hostname, ktype, fingerprint))
|
||||
sys.stdin = old_stdin
|
||||
|
||||
self.connection.unlock_connection()
|
||||
|
||||
if inp not in ['yes','y','']:
|
||||
raise AnsibleError("host connection rejected by user")
|
||||
|
||||
key._added_by_ansible_this_time = True
|
||||
|
||||
# existing implementation below:
|
||||
client._host_keys.add(hostname, key.get_name(), key)
|
||||
|
||||
# host keys are actually saved in close() function below
|
||||
# in order to control ordering.
|
||||
|
||||
|
||||
# keep connection objects on a per host basis to avoid repeated attempts to reconnect
|
||||
|
||||
SSH_CONNECTION_CACHE = {}
|
||||
SFTP_CONNECTION_CACHE = {}
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' SSH based connections with Paramiko '''
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
''' used to identify this connection object from other classes '''
|
||||
return 'paramiko'
|
||||
|
||||
def _cache_key(self):
|
||||
return "%s__%s__" % (self._play_context.remote_addr, self._play_context.remote_user)
|
||||
|
||||
def _connect(self):
|
||||
cache_key = self._cache_key()
|
||||
if cache_key in SSH_CONNECTION_CACHE:
|
||||
self.ssh = SSH_CONNECTION_CACHE[cache_key]
|
||||
else:
|
||||
self.ssh = SSH_CONNECTION_CACHE[cache_key] = self._connect_uncached()
|
||||
return self
|
||||
|
||||
def _connect_uncached(self):
|
||||
''' activates the connection object '''
|
||||
|
||||
if not HAVE_PARAMIKO:
|
||||
raise AnsibleError("paramiko is not installed")
|
||||
|
||||
port = self._play_context.port or 22
|
||||
self._display.vvv("ESTABLISH CONNECTION FOR USER: %s on PORT %s TO %s" % (self._play_context.remote_user, port, self._play_context.remote_addr), host=self._play_context.remote_addr)
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
|
||||
self.keyfile = os.path.expanduser("~/.ssh/known_hosts")
|
||||
|
||||
if C.HOST_KEY_CHECKING:
|
||||
try:
|
||||
#TODO: check if we need to look at several possible locations, possible for loop
|
||||
ssh.load_system_host_keys("/etc/ssh/ssh_known_hosts")
|
||||
except IOError:
|
||||
pass # file was not found, but not required to function
|
||||
ssh.load_system_host_keys()
|
||||
|
||||
ssh.set_missing_host_key_policy(MyAddPolicy(self._new_stdin, self))
|
||||
|
||||
allow_agent = True
|
||||
|
||||
if self._play_context.password is not None:
|
||||
allow_agent = False
|
||||
|
||||
try:
|
||||
key_filename = None
|
||||
if self._play_context.private_key_file:
|
||||
key_filename = os.path.expanduser(self._play_context.private_key_file)
|
||||
|
||||
ssh.connect(
|
||||
self._play_context.remote_addr,
|
||||
username=self._play_context.remote_user,
|
||||
allow_agent=allow_agent,
|
||||
look_for_keys=True,
|
||||
key_filename=key_filename,
|
||||
password=self._play_context.password,
|
||||
timeout=self._play_context.timeout,
|
||||
compress=True,
|
||||
port=port,
|
||||
)
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
if "PID check failed" in msg:
|
||||
raise AnsibleError("paramiko version issue, please upgrade paramiko on the machine running ansible")
|
||||
elif "Private key file is encrypted" in msg:
|
||||
msg = 'ssh %s@%s:%s : %s\nTo connect as a different user, use -u <username>.' % (
|
||||
self._play_context.remote_user, self._play_context.remote_addr, port, msg)
|
||||
raise AnsibleConnectionFailure(msg)
|
||||
else:
|
||||
raise AnsibleConnectionFailure(msg)
|
||||
|
||||
return ssh
|
||||
|
||||
def exec_command(self, cmd, tmp_path, in_data=None, sudoable=True):
|
||||
''' run a command on the remote host '''
|
||||
|
||||
super(Connection, self).exec_command(cmd, tmp_path, in_data=in_data, sudoable=sudoable)
|
||||
|
||||
if in_data:
|
||||
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
bufsize = 4096
|
||||
|
||||
try:
|
||||
self.ssh.get_transport().set_keepalive(5)
|
||||
chan = self.ssh.get_transport().open_session()
|
||||
except Exception as e:
|
||||
msg = "Failed to open session"
|
||||
if len(str(e)) > 0:
|
||||
msg += ": %s" % str(e)
|
||||
raise AnsibleConnectionFailure(msg)
|
||||
|
||||
# sudo usually requires a PTY (cf. requiretty option), therefore
|
||||
# we give it one by default (pty=True in ansble.cfg), and we try
|
||||
# to initialise from the calling environment
|
||||
if C.PARAMIKO_PTY:
|
||||
chan.get_pty(term=os.getenv('TERM', 'vt100'), width=int(os.getenv('COLUMNS', 0)), height=int(os.getenv('LINES', 0)))
|
||||
|
||||
self._display.vvv("EXEC %s" % cmd, host=self._play_context.remote_addr)
|
||||
|
||||
no_prompt_out = ''
|
||||
no_prompt_err = ''
|
||||
become_output = ''
|
||||
|
||||
try:
|
||||
chan.exec_command(cmd)
|
||||
if self._play_context.prompt:
|
||||
passprompt = False
|
||||
while True:
|
||||
self._display.debug('Waiting for Privilege Escalation input')
|
||||
if self.check_become_success(become_output):
|
||||
break
|
||||
elif self.check_password_prompt(become_output):
|
||||
passprompt = True
|
||||
break
|
||||
|
||||
chunk = chan.recv(bufsize)
|
||||
self._display.debug("chunk is: %s" % chunk)
|
||||
if not chunk:
|
||||
if 'unknown user' in become_output:
|
||||
raise AnsibleError( 'user %s does not exist' % become_user)
|
||||
else:
|
||||
break
|
||||
#raise AnsibleError('ssh connection closed waiting for password prompt')
|
||||
become_output += chunk
|
||||
if passprompt:
|
||||
if self._play_context.become and self._play_context.become_pass:
|
||||
chan.sendall(self._play_context.become_pass + '\n')
|
||||
else:
|
||||
raise AnsibleError("A password is reqired but none was supplied")
|
||||
else:
|
||||
no_prompt_out += become_output
|
||||
no_prompt_err += become_output
|
||||
except socket.timeout:
|
||||
raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + become_output)
|
||||
|
||||
stdout = ''.join(chan.makefile('rb', bufsize))
|
||||
stderr = ''.join(chan.makefile_stderr('rb', bufsize))
|
||||
|
||||
return (chan.recv_exit_status(), '', no_prompt_out + stdout, no_prompt_out + stderr)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to remote '''
|
||||
|
||||
super(Connection, self).put_file(in_path, out_path)
|
||||
|
||||
self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
|
||||
|
||||
if not os.path.exists(in_path):
|
||||
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
|
||||
try:
|
||||
self.sftp = self.ssh.open_sftp()
|
||||
except Exception as e:
|
||||
raise AnsibleError("failed to open a SFTP connection (%s)" % e)
|
||||
|
||||
try:
|
||||
self.sftp.put(in_path, out_path)
|
||||
except IOError:
|
||||
raise AnsibleError("failed to transfer file to %s" % out_path)
|
||||
|
||||
def _connect_sftp(self):
|
||||
|
||||
cache_key = "%s__%s__" % (self._play_context.remote_addr, self._play_context.remote_user)
|
||||
if cache_key in SFTP_CONNECTION_CACHE:
|
||||
return SFTP_CONNECTION_CACHE[cache_key]
|
||||
else:
|
||||
result = SFTP_CONNECTION_CACHE[cache_key] = self._connect().ssh.open_sftp()
|
||||
return result
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' save a remote file to the specified path '''
|
||||
|
||||
super(Connection, self).fetch_file(in_path, out_path)
|
||||
|
||||
self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
|
||||
|
||||
try:
|
||||
self.sftp = self._connect_sftp()
|
||||
except Exception as e:
|
||||
raise AnsibleError("failed to open a SFTP connection (%s)", e)
|
||||
|
||||
try:
|
||||
self.sftp.get(in_path, out_path)
|
||||
except IOError:
|
||||
raise AnsibleError("failed to transfer file from %s" % in_path)
|
||||
|
||||
def _any_keys_added(self):
|
||||
|
||||
added_any = False
|
||||
for hostname, keys in iteritems(self.ssh._host_keys):
|
||||
for keytype, key in iteritems(keys):
|
||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||
if added_this_time:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _save_ssh_host_keys(self, filename):
|
||||
'''
|
||||
not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
|
||||
don't complain about it :)
|
||||
'''
|
||||
|
||||
if not self._any_keys_added():
|
||||
return False
|
||||
|
||||
path = os.path.expanduser("~/.ssh")
|
||||
makedirs_safe(path)
|
||||
|
||||
f = open(filename, 'w')
|
||||
|
||||
for hostname, keys in iteritems(self.ssh._host_keys):
|
||||
|
||||
for keytype, key in iteritems(keys):
|
||||
|
||||
# was f.write
|
||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||
if not added_this_time:
|
||||
f.write("%s %s %s\n" % (hostname, keytype, key.get_base64()))
|
||||
|
||||
for hostname, keys in iteritems(self.ssh._host_keys):
|
||||
|
||||
for keytype, key in iteritems(keys):
|
||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||
if added_this_time:
|
||||
f.write("%s %s %s\n" % (hostname, keytype, key.get_base64()))
|
||||
|
||||
f.close()
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection '''
|
||||
|
||||
cache_key = self._cache_key()
|
||||
SSH_CONNECTION_CACHE.pop(cache_key, None)
|
||||
SFTP_CONNECTION_CACHE.pop(cache_key, None)
|
||||
|
||||
if self.sftp is not None:
|
||||
self.sftp.close()
|
||||
|
||||
if C.HOST_KEY_CHECKING and C.PARAMIKO_RECORD_HOST_KEYS and self._any_keys_added():
|
||||
|
||||
# add any new SSH host keys -- warning -- this could be slow
|
||||
# (This doesn't acquire the connection lock because it needs
|
||||
# to exclude only other known_hosts writers, not connections
|
||||
# that are starting up.)
|
||||
lockfile = self.keyfile.replace("known_hosts",".known_hosts.lock")
|
||||
dirname = os.path.dirname(self.keyfile)
|
||||
makedirs_safe(dirname)
|
||||
|
||||
KEY_LOCK = open(lockfile, 'w')
|
||||
fcntl.lockf(KEY_LOCK, fcntl.LOCK_EX)
|
||||
|
||||
try:
|
||||
# just in case any were added recently
|
||||
|
||||
self.ssh.load_system_host_keys()
|
||||
self.ssh._host_keys.update(self.ssh._system_host_keys)
|
||||
|
||||
# gather information about the current key file, so
|
||||
# we can ensure the new file has the correct mode/owner
|
||||
|
||||
key_dir = os.path.dirname(self.keyfile)
|
||||
key_stat = os.stat(self.keyfile)
|
||||
|
||||
# Save the new keys to a temporary file and move it into place
|
||||
# rather than rewriting the file. We set delete=False because
|
||||
# the file will be moved into place rather than cleaned up.
|
||||
|
||||
tmp_keyfile = tempfile.NamedTemporaryFile(dir=key_dir, delete=False)
|
||||
os.chmod(tmp_keyfile.name, key_stat.st_mode & 0o7777)
|
||||
os.chown(tmp_keyfile.name, key_stat.st_uid, key_stat.st_gid)
|
||||
|
||||
self._save_ssh_host_keys(tmp_keyfile.name)
|
||||
tmp_keyfile.close()
|
||||
|
||||
os.rename(tmp_keyfile.name, self.keyfile)
|
||||
|
||||
except:
|
||||
|
||||
# unable to save keys, including scenario when key was invalid
|
||||
# and caught earlier
|
||||
traceback.print_exc()
|
||||
pass
|
||||
fcntl.lockf(KEY_LOCK, fcntl.LOCK_UN)
|
||||
|
||||
self.ssh.close()
|
||||
|
||||
531
lib/ansible/plugins/connection/ssh.py
Normal file
531
lib/ansible/plugins/connection/ssh.py
Normal file
@@ -0,0 +1,531 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import gettext
|
||||
import fcntl
|
||||
import hmac
|
||||
import os
|
||||
import pipes
|
||||
import pty
|
||||
import pwd
|
||||
import random
|
||||
import re
|
||||
import select
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from hashlib import sha1
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
|
||||
from ansible.plugins.connections import ConnectionBase
|
||||
from ansible.utils.path import unfrackpath, makedirs_safe
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' ssh based connections '''
|
||||
|
||||
has_pipelining = True
|
||||
become_methods = frozenset(C.BECOME_METHODS).difference(['runas'])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# SSH connection specific init stuff
|
||||
self._common_args = []
|
||||
self.HASHED_KEY_MAGIC = "|1|"
|
||||
|
||||
super(Connection, self).__init__(*args, **kwargs)
|
||||
|
||||
self.host = self._play_context.remote_addr
|
||||
self.ssh_extra_args = ''
|
||||
self.ssh_args = ''
|
||||
|
||||
def set_host_overrides(self, host):
|
||||
v = host.get_vars()
|
||||
if 'ansible_ssh_extra_args' in v:
|
||||
self.ssh_extra_args = v['ansible_ssh_extra_args']
|
||||
if 'ansible_ssh_args' in v:
|
||||
self.ssh_args = v['ansible_ssh_args']
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
''' used to identify this connection object from other classes '''
|
||||
return 'ssh'
|
||||
|
||||
def _split_args(self, argstring):
|
||||
"""
|
||||
Takes a string like '-o Foo=1 -o Bar="foo bar"' and returns a
|
||||
list ['-o', 'Foo=1', '-o', 'Bar=foo bar'] that can be added to
|
||||
the argument list. The list will not contain any empty elements.
|
||||
"""
|
||||
return [x.strip() for x in shlex.split(argstring) if x.strip()]
|
||||
|
||||
def add_args(self, explanation, args):
|
||||
"""
|
||||
Adds the given args to _common_args and displays a
|
||||
caller-supplied explanation of why they were added.
|
||||
"""
|
||||
self._common_args += args
|
||||
self._display.vvvvv('SSH: ' + explanation + ': (%s)' % ')('.join(args), host=self._play_context.remote_addr)
|
||||
|
||||
def _connect(self):
|
||||
''' connect to the remote host '''
|
||||
|
||||
self._display.vvv("ESTABLISH SSH CONNECTION FOR USER: {0}".format(self._play_context.remote_user), host=self._play_context.remote_addr)
|
||||
|
||||
if self._connected:
|
||||
return self
|
||||
|
||||
# We start with ansible_ssh_args from the inventory if it's set,
|
||||
# or [ssh_connection]ssh_args from ansible.cfg, or the default
|
||||
# Control* settings.
|
||||
|
||||
if self.ssh_args:
|
||||
args = self._split_args(self.ssh_args)
|
||||
self.add_args("inventory set ansible_ssh_args", args)
|
||||
elif C.ANSIBLE_SSH_ARGS:
|
||||
args = self._split_args(C.ANSIBLE_SSH_ARGS)
|
||||
self.add_args("ansible.cfg set ssh_args", args)
|
||||
else:
|
||||
args = (
|
||||
"-o", "ControlMaster=auto",
|
||||
"-o", "ControlPersist=60s"
|
||||
)
|
||||
self.add_args("default arguments", args)
|
||||
|
||||
# If any of the above have set ControlPersist but not a
|
||||
# ControlPath, add one ourselves.
|
||||
|
||||
cp_in_use = False
|
||||
cp_path_set = False
|
||||
for arg in self._common_args:
|
||||
if "ControlPersist" in arg:
|
||||
cp_in_use = True
|
||||
if "ControlPath" in arg:
|
||||
cp_path_set = True
|
||||
|
||||
if cp_in_use and not cp_path_set:
|
||||
self._cp_dir = unfrackpath('$HOME/.ansible/cp')
|
||||
|
||||
args = ("-o", "ControlPath=\"{0}\"".format(
|
||||
C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self._cp_dir))
|
||||
)
|
||||
self.add_args("found only ControlPersist; added ControlPath", args)
|
||||
|
||||
# The directory must exist and be writable.
|
||||
makedirs_safe(self._cp_dir, 0o700)
|
||||
if not os.access(self._cp_dir, os.W_OK):
|
||||
raise AnsibleError("Cannot write to ControlPath %s" % self._cp_dir)
|
||||
|
||||
if not C.HOST_KEY_CHECKING:
|
||||
self.add_args(
|
||||
"ANSIBLE_HOST_KEY_CHECKING/host_key_checking disabled",
|
||||
("-o", "StrictHostKeyChecking=no")
|
||||
)
|
||||
|
||||
if self._play_context.port is not None:
|
||||
self.add_args(
|
||||
"ANSIBLE_REMOTE_PORT/remote_port/ansible_ssh_port set",
|
||||
("-o", "Port={0}".format(self._play_context.port))
|
||||
)
|
||||
|
||||
key = self._play_context.private_key_file
|
||||
if key:
|
||||
self.add_args(
|
||||
"ANSIBLE_PRIVATE_KEY_FILE/private_key_file/ansible_ssh_private_key_file set",
|
||||
("-o", "IdentityFile=\"{0}\"".format(os.path.expanduser(key)))
|
||||
)
|
||||
|
||||
if not self._play_context.password:
|
||||
self.add_args(
|
||||
"ansible_password/ansible_ssh_pass not set", (
|
||||
"-o", "KbdInteractiveAuthentication=no",
|
||||
"-o", "PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey",
|
||||
"-o", "PasswordAuthentication=no"
|
||||
)
|
||||
)
|
||||
|
||||
user = self._play_context.remote_user
|
||||
if user and user != pwd.getpwuid(os.geteuid())[0]:
|
||||
self.add_args(
|
||||
"ANSIBLE_REMOTE_USER/remote_user/ansible_ssh_user/user/-u set",
|
||||
("-o", "User={0}".format(self._play_context.remote_user))
|
||||
)
|
||||
|
||||
self.add_args(
|
||||
"ANSIBLE_TIMEOUT/timeout set",
|
||||
("-o", "ConnectTimeout={0}".format(self._play_context.timeout))
|
||||
)
|
||||
|
||||
# If any extra SSH arguments are specified in the inventory for
|
||||
# this host, or specified as an override on the command line,
|
||||
# add them in.
|
||||
|
||||
if self._play_context.ssh_extra_args:
|
||||
args = self._split_args(self._play_context.ssh_extra_args)
|
||||
self.add_args("command-line added --ssh-extra-args", args)
|
||||
elif self.ssh_extra_args:
|
||||
args = self._split_args(self.ssh_extra_args)
|
||||
self.add_args("inventory added ansible_ssh_extra_args", args)
|
||||
|
||||
self._connected = True
|
||||
|
||||
return self
|
||||
|
||||
def _run(self, cmd, indata):
|
||||
if indata:
|
||||
# do not use pseudo-pty
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdin = p.stdin
|
||||
else:
|
||||
# try to use upseudo-pty
|
||||
try:
|
||||
# Make sure stdin is a proper (pseudo) pty to avoid: tcgetattr errors
|
||||
master, slave = pty.openpty()
|
||||
p = subprocess.Popen(cmd, stdin=slave, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdin = os.fdopen(master, 'w', 0)
|
||||
os.close(slave)
|
||||
except:
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdin = p.stdin
|
||||
|
||||
return (p, stdin)
|
||||
|
||||
def _password_cmd(self):
|
||||
if self._play_context.password:
|
||||
try:
|
||||
p = subprocess.Popen(["sshpass"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p.communicate()
|
||||
except OSError:
|
||||
raise AnsibleError("to use the 'ssh' connection type with passwords, you must install the sshpass program")
|
||||
(self.rfd, self.wfd) = os.pipe()
|
||||
return ["sshpass", "-d{0}".format(self.rfd)]
|
||||
return []
|
||||
|
||||
def _send_password(self):
|
||||
if self._play_context.password:
|
||||
os.close(self.rfd)
|
||||
os.write(self.wfd, "{0}\n".format(self._play_context.password))
|
||||
os.close(self.wfd)
|
||||
|
||||
def _communicate(self, p, stdin, indata, sudoable=True):
|
||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
# We can't use p.communicate here because the ControlMaster may have stdout open as well
|
||||
stdout = ''
|
||||
stderr = ''
|
||||
rpipes = [p.stdout, p.stderr]
|
||||
if indata:
|
||||
try:
|
||||
stdin.write(indata)
|
||||
stdin.close()
|
||||
except:
|
||||
raise AnsibleConnectionFailure('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
|
||||
# Read stdout/stderr from process
|
||||
while True:
|
||||
rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
|
||||
|
||||
# fail early if the become password is wrong
|
||||
if self._play_context.become and sudoable:
|
||||
if self._play_context.become_pass:
|
||||
self.check_incorrect_password(stdout)
|
||||
elif self.check_password_prompt(stdout):
|
||||
raise AnsibleError('Missing %s password' % self._play_context.become_method)
|
||||
|
||||
if p.stderr in rfd:
|
||||
dat = os.read(p.stderr.fileno(), 9000)
|
||||
stderr += dat
|
||||
if dat == '':
|
||||
rpipes.remove(p.stderr)
|
||||
elif p.stdout in rfd:
|
||||
dat = os.read(p.stdout.fileno(), 9000)
|
||||
stdout += dat
|
||||
if dat == '':
|
||||
rpipes.remove(p.stdout)
|
||||
|
||||
# only break out if no pipes are left to read or
|
||||
# the pipes are completely read and
|
||||
# the process is terminated
|
||||
if (not rpipes or not rfd) and p.poll() is not None:
|
||||
break
|
||||
# No pipes are left to read but process is not yet terminated
|
||||
# Only then it is safe to wait for the process to be finished
|
||||
# NOTE: Actually p.poll() is always None here if rpipes is empty
|
||||
elif not rpipes and p.poll() == None:
|
||||
p.wait()
|
||||
# The process is terminated. Since no pipes to read from are
|
||||
# left, there is no need to call select() again.
|
||||
break
|
||||
# close stdin after process is terminated and stdout/stderr are read
|
||||
# completely (see also issue #848)
|
||||
stdin.close()
|
||||
return (p.returncode, stdout, stderr)
|
||||
|
||||
def lock_host_keys(self, lock):
|
||||
|
||||
# lock around the initial SSH connectivity so the user prompt about
|
||||
# whether to add the host to known hosts is not intermingled with
|
||||
# multiprocess output.
|
||||
#
|
||||
# This is a noop for now, pending further investigation. The lock file
|
||||
# should be opened in TaskQueueManager and passed down through the
|
||||
# PlayContext.
|
||||
|
||||
pass
|
||||
|
||||
def exec_command(self, *args, **kwargs):
|
||||
"""
|
||||
Wrapper around _exec_command to retry in the case of an ssh failure
|
||||
|
||||
Will retry if:
|
||||
* an exception is caught
|
||||
* ssh returns 255
|
||||
Will not retry if
|
||||
* remaining_tries is <2
|
||||
* retries limit reached
|
||||
"""
|
||||
|
||||
remaining_tries = int(C.ANSIBLE_SSH_RETRIES) + 1
|
||||
cmd_summary = "%s..." % args[0]
|
||||
for attempt in xrange(remaining_tries):
|
||||
try:
|
||||
return_tuple = self._exec_command(*args, **kwargs)
|
||||
# 0 = success
|
||||
# 1-254 = remote command return code
|
||||
# 255 = failure from the ssh command itself
|
||||
if return_tuple[0] != 255 or attempt == (remaining_tries - 1):
|
||||
break
|
||||
else:
|
||||
raise AnsibleConnectionFailure("Failed to connect to the host via ssh.")
|
||||
except (AnsibleConnectionFailure, Exception) as e:
|
||||
if attempt == remaining_tries - 1:
|
||||
raise e
|
||||
else:
|
||||
pause = 2 ** attempt - 1
|
||||
if pause > 30:
|
||||
pause = 30
|
||||
|
||||
if isinstance(e, AnsibleConnectionFailure):
|
||||
msg = "ssh_retry: attempt: %d, ssh return code is 255. cmd (%s), pausing for %d seconds" % (attempt, cmd_summary, pause)
|
||||
else:
|
||||
msg = "ssh_retry: attempt: %d, caught exception(%s) from cmd (%s), pausing for %d seconds" % (attempt, e, cmd_summary, pause)
|
||||
|
||||
self._display.vv(msg)
|
||||
|
||||
time.sleep(pause)
|
||||
continue
|
||||
|
||||
|
||||
return return_tuple
|
||||
|
||||
def _exec_command(self, cmd, tmp_path, in_data=None, sudoable=True):
|
||||
''' run a command on the remote host '''
|
||||
|
||||
super(Connection, self).exec_command(cmd, tmp_path, in_data=in_data, sudoable=sudoable)
|
||||
|
||||
ssh_cmd = self._password_cmd()
|
||||
ssh_cmd += ("ssh", "-C")
|
||||
if not in_data:
|
||||
# we can only use tty when we are not pipelining the modules. piping data into /usr/bin/python
|
||||
# inside a tty automatically invokes the python interactive-mode but the modules are not
|
||||
# compatible with the interactive-mode ("unexpected indent" mainly because of empty lines)
|
||||
ssh_cmd.append("-tt")
|
||||
if self._play_context.verbosity > 3:
|
||||
ssh_cmd.append("-vvv")
|
||||
else:
|
||||
ssh_cmd.append("-q")
|
||||
ssh_cmd += self._common_args
|
||||
|
||||
ssh_cmd.append(self.host)
|
||||
|
||||
ssh_cmd.append(cmd)
|
||||
self._display.vvv("EXEC {0}".format(' '.join(ssh_cmd)), host=self.host)
|
||||
|
||||
self.lock_host_keys(True)
|
||||
|
||||
# create process
|
||||
(p, stdin) = self._run(ssh_cmd, in_data)
|
||||
|
||||
self._send_password()
|
||||
|
||||
no_prompt_out = ''
|
||||
no_prompt_err = ''
|
||||
|
||||
if self._play_context.prompt:
|
||||
'''
|
||||
Several cases are handled for privileges with password
|
||||
* NOPASSWD (tty & no-tty): detect success_key on stdout
|
||||
* without NOPASSWD:
|
||||
* detect prompt on stdout (tty)
|
||||
* detect prompt on stderr (no-tty)
|
||||
'''
|
||||
|
||||
self._display.debug("Handling privilege escalation password prompt.")
|
||||
|
||||
|
||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
|
||||
become_output = ''
|
||||
become_errput = ''
|
||||
passprompt = False
|
||||
while True:
|
||||
self._display.debug('Waiting for Privilege Escalation input')
|
||||
|
||||
if self.check_become_success(become_output + become_errput):
|
||||
self._display.debug('Succeded!')
|
||||
break
|
||||
elif self.check_password_prompt(become_output) or self.check_password_prompt(become_errput):
|
||||
self._display.debug('Password prompt!')
|
||||
passprompt = True
|
||||
break
|
||||
|
||||
self._display.debug('Read next chunks')
|
||||
rfd, wfd, efd = select.select([p.stdout, p.stderr], [], [p.stdout], self._play_context.timeout)
|
||||
if not rfd:
|
||||
# timeout. wrap up process communication
|
||||
stdout, stderr = p.communicate()
|
||||
raise AnsibleError('Connection error waiting for privilege escalation password prompt: %s' % become_output)
|
||||
|
||||
elif p.stderr in rfd:
|
||||
chunk = p.stderr.read()
|
||||
become_errput += chunk
|
||||
self._display.debug('stderr chunk is: %s' % chunk)
|
||||
self.check_incorrect_password(become_errput)
|
||||
|
||||
elif p.stdout in rfd:
|
||||
chunk = p.stdout.read()
|
||||
become_output += chunk
|
||||
self._display.debug('stdout chunk is: %s' % chunk)
|
||||
|
||||
|
||||
if not chunk:
|
||||
break
|
||||
#raise AnsibleError('Connection closed waiting for privilege escalation password prompt: %s ' % become_output)
|
||||
|
||||
if passprompt:
|
||||
self._display.debug("Sending privilege escalation password.")
|
||||
stdin.write(self._play_context.become_pass + '\n')
|
||||
else:
|
||||
no_prompt_out = become_output
|
||||
no_prompt_err = become_errput
|
||||
|
||||
|
||||
(returncode, stdout, stderr) = self._communicate(p, stdin, in_data, sudoable=sudoable)
|
||||
|
||||
self.lock_host_keys(False)
|
||||
|
||||
controlpersisterror = 'Bad configuration option: ControlPersist' in stderr or 'unknown configuration option: ControlPersist' in stderr
|
||||
|
||||
if C.HOST_KEY_CHECKING:
|
||||
if ssh_cmd[0] == "sshpass" and p.returncode == 6:
|
||||
raise AnsibleError('Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this. Please add this host\'s fingerprint to your known_hosts file to manage this host.')
|
||||
|
||||
if p.returncode != 0 and controlpersisterror:
|
||||
raise AnsibleError('using -c ssh on certain older ssh versions may not support ControlPersist, set ANSIBLE_SSH_ARGS="" (or ssh_args in [ssh_connection] section of the config file) before running again')
|
||||
# FIXME: module name isn't in runner
|
||||
#if p.returncode == 255 and (in_data or self.runner.module_name == 'raw'):
|
||||
if p.returncode == 255 and in_data:
|
||||
raise AnsibleConnectionFailure('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
|
||||
|
||||
return (p.returncode, '', no_prompt_out + stdout, no_prompt_err + stderr)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to remote '''
|
||||
|
||||
super(Connection, self).put_file(in_path, out_path)
|
||||
|
||||
self._display.vvv("PUT {0} TO {1}".format(in_path, out_path), host=self.host)
|
||||
if not os.path.exists(in_path):
|
||||
raise AnsibleFileNotFound("file or module does not exist: {0}".format(in_path))
|
||||
cmd = self._password_cmd()
|
||||
|
||||
# scp and sftp require square brackets for IPv6 addresses, but
|
||||
# accept them for hostnames and IPv4 addresses too.
|
||||
host = '[%s]' % self.host
|
||||
|
||||
if C.DEFAULT_SCP_IF_SSH:
|
||||
cmd.append('scp')
|
||||
cmd.extend(self._common_args)
|
||||
cmd.extend([in_path, '{0}:{1}'.format(host, pipes.quote(out_path))])
|
||||
indata = None
|
||||
else:
|
||||
cmd.append('sftp')
|
||||
cmd.extend(self._common_args)
|
||||
cmd.append(host)
|
||||
indata = "put {0} {1}\n".format(pipes.quote(in_path), pipes.quote(out_path))
|
||||
|
||||
(p, stdin) = self._run(cmd, indata)
|
||||
|
||||
self._send_password()
|
||||
|
||||
(returncode, stdout, stderr) = self._communicate(p, stdin, indata)
|
||||
|
||||
if returncode != 0:
|
||||
raise AnsibleError("failed to transfer file to {0}:\n{1}\n{2}".format(out_path, stdout, stderr))
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from remote to local '''
|
||||
|
||||
super(Connection, self).fetch_file(in_path, out_path)
|
||||
|
||||
self._display.vvv("FETCH {0} TO {1}".format(in_path, out_path), host=self.host)
|
||||
cmd = self._password_cmd()
|
||||
|
||||
|
||||
if C.DEFAULT_SCP_IF_SSH:
|
||||
cmd.append('scp')
|
||||
cmd.extend(self._common_args)
|
||||
cmd.extend(['{0}:{1}'.format(self.host, in_path), out_path])
|
||||
indata = None
|
||||
else:
|
||||
cmd.append('sftp')
|
||||
# sftp batch mode allows us to correctly catch failed transfers,
|
||||
# but can be disabled if for some reason the client side doesn't
|
||||
# support the option
|
||||
if C.DEFAULT_SFTP_BATCH_MODE:
|
||||
cmd.append('-b')
|
||||
cmd.append('-')
|
||||
cmd.extend(self._common_args)
|
||||
cmd.append(self.host)
|
||||
indata = "get {0} {1}\n".format(in_path, out_path)
|
||||
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
self._send_password()
|
||||
stdout, stderr = p.communicate(indata)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise AnsibleError("failed to transfer file from {0}:\n{1}\n{2}".format(in_path, stdout, stderr))
|
||||
|
||||
def close(self):
|
||||
|
||||
if self._connected:
|
||||
|
||||
# TODO: reenable once winrm issues are fixed
|
||||
# temporarily disabled as we are forced to currently close connections after every task because of winrm
|
||||
#if and 'ControlMaster' in self._common_args:
|
||||
# cmd = ['ssh','-O','stop']
|
||||
# cmd.extend(self._common_args)
|
||||
# cmd.append(self._play_context.remote_addr)
|
||||
|
||||
# p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
# stdout, stderr = p.communicate()
|
||||
|
||||
self._connected = False
|
||||
|
||||
291
lib/ansible/plugins/connection/winrm.py
Normal file
291
lib/ansible/plugins/connection/winrm.py
Normal file
@@ -0,0 +1,291 @@
|
||||
# (c) 2014, Chris Church <chris@ninemoreminutes.com>
|
||||
#
|
||||
# This file is part of Ansible.
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import traceback
|
||||
|
||||
from six.moves.urllib import parse
|
||||
|
||||
try:
|
||||
from winrm import Response
|
||||
from winrm.exceptions import WinRMTransportError
|
||||
from winrm.protocol import Protocol
|
||||
except ImportError:
|
||||
raise AnsibleError("winrm is not installed")
|
||||
|
||||
HAVE_KERBEROS = False
|
||||
try:
|
||||
import kerberos
|
||||
HAVE_KERBEROS = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
|
||||
from ansible.plugins.connections import ConnectionBase
|
||||
from ansible.plugins import shell_loader
|
||||
from ansible.utils.path import makedirs_safe
|
||||
from ansible.utils.unicode import to_bytes, to_unicode
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
'''WinRM connections over HTTP/HTTPS.'''
|
||||
|
||||
transport_schemes = {
|
||||
'http': [('kerberos', 'http'), ('plaintext', 'http'), ('plaintext', 'https')],
|
||||
'https': [('kerberos', 'https'), ('plaintext', 'https')],
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
self.has_pipelining = False
|
||||
self.default_suffixes = ['.ps1', '']
|
||||
self.protocol = None
|
||||
self.shell_id = None
|
||||
self.delegate = None
|
||||
self._shell_type = 'powershell'
|
||||
|
||||
# TODO: Add runas support
|
||||
self.become_methods_supported=[]
|
||||
|
||||
super(Connection, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
''' used to identify this connection object from other classes '''
|
||||
return 'winrm'
|
||||
|
||||
def _winrm_connect(self):
|
||||
'''
|
||||
Establish a WinRM connection over HTTP/HTTPS.
|
||||
'''
|
||||
port = self._play_context.port or 5986
|
||||
self._display.vvv("ESTABLISH WINRM CONNECTION FOR USER: %s on PORT %s TO %s" % \
|
||||
(self._play_context.remote_user, port, self._play_context.remote_addr), host=self._play_context.remote_addr)
|
||||
netloc = '%s:%d' % (self._play_context.remote_addr, port)
|
||||
exc = None
|
||||
for transport, scheme in self.transport_schemes['http' if port == 5985 else 'https']:
|
||||
if transport == 'kerberos' and (not HAVE_KERBEROS or not '@' in self._play_context.remote_user):
|
||||
continue
|
||||
|
||||
if transport == 'kerberos':
|
||||
realm = self._play_context.remote_user.split('@', 1)[1].strip() or None
|
||||
else:
|
||||
realm = None
|
||||
|
||||
endpoint = parse.urlunsplit((scheme, netloc, '/wsman', '', ''))
|
||||
|
||||
self._display.vvvvv('WINRM CONNECT: transport=%s endpoint=%s' % (transport, endpoint), host=self._play_context.remote_addr)
|
||||
protocol = Protocol(
|
||||
endpoint,
|
||||
transport=transport,
|
||||
username=self._play_context.remote_user,
|
||||
password=self._play_context.password,
|
||||
realm=realm
|
||||
)
|
||||
|
||||
try:
|
||||
protocol.send_message('')
|
||||
return protocol
|
||||
except WinRMTransportError as exc:
|
||||
err_msg = str(exc)
|
||||
if re.search(r'Operation\s+?timed\s+?out', err_msg, re.I):
|
||||
raise AnsibleError("the connection attempt timed out")
|
||||
m = re.search(r'Code\s+?(\d{3})', err_msg)
|
||||
if m:
|
||||
code = int(m.groups()[0])
|
||||
if code == 401:
|
||||
raise AnsibleError("the username/password specified for this server was incorrect")
|
||||
elif code == 411:
|
||||
return protocol
|
||||
self._display.vvvvv('WINRM CONNECTION ERROR: %s' % err_msg, host=self._play_context.remote_addr)
|
||||
continue
|
||||
if exc:
|
||||
raise AnsibleError(str(exc))
|
||||
|
||||
def _winrm_exec(self, command, args=(), from_exec=False):
|
||||
if from_exec:
|
||||
self._display.vvvvv("WINRM EXEC %r %r" % (command, args), host=self._play_context.remote_addr)
|
||||
else:
|
||||
self._display.vvvvvv("WINRM EXEC %r %r" % (command, args), host=self._play_context.remote_addr)
|
||||
if not self.protocol:
|
||||
self.protocol = self._winrm_connect()
|
||||
if not self.shell_id:
|
||||
self.shell_id = self.protocol.open_shell(codepage=65001) # UTF-8
|
||||
command_id = None
|
||||
try:
|
||||
command_id = self.protocol.run_command(self.shell_id, to_bytes(command), map(to_bytes, args))
|
||||
response = Response(self.protocol.get_command_output(self.shell_id, command_id))
|
||||
if from_exec:
|
||||
self._display.vvvvv('WINRM RESULT %r' % to_unicode(response), host=self._play_context.remote_addr)
|
||||
else:
|
||||
self._display.vvvvv('WINRM RESULT %r' % to_unicode(response), host=self._play_context.remote_addr)
|
||||
self._display.vvvvvv('WINRM STDOUT %s' % to_unicode(response.std_out), host=self._play_context.remote_addr)
|
||||
self._display.vvvvvv('WINRM STDERR %s' % to_unicode(response.std_err), host=self._play_context.remote_addr)
|
||||
return response
|
||||
finally:
|
||||
if command_id:
|
||||
self.protocol.cleanup_command(self.shell_id, command_id)
|
||||
|
||||
def _connect(self):
|
||||
if not self.protocol:
|
||||
self.protocol = self._winrm_connect()
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path, in_data=None, sudoable=True):
|
||||
super(Connection, self).exec_command(cmd, tmp_path, in_data=in_data, sudoable=sudoable)
|
||||
cmd_parts = shlex.split(to_bytes(cmd), posix=False)
|
||||
cmd_parts = map(to_unicode, cmd_parts)
|
||||
script = None
|
||||
cmd_ext = cmd_parts and self._shell._unquote(cmd_parts[0]).lower()[-4:] or ''
|
||||
# Support running .ps1 files (via script/raw).
|
||||
if cmd_ext == '.ps1':
|
||||
script = ' '.join(['&'] + cmd_parts)
|
||||
# Support running .bat/.cmd files; change back to the default system encoding instead of UTF-8.
|
||||
elif cmd_ext in ('.bat', '.cmd'):
|
||||
script = ' '.join(['[System.Console]::OutputEncoding = [System.Text.Encoding]::Default;', '&'] + cmd_parts)
|
||||
# Encode the command if not already encoded; supports running simple PowerShell commands via raw.
|
||||
elif '-EncodedCommand' not in cmd_parts:
|
||||
script = ' '.join(cmd_parts)
|
||||
if script:
|
||||
cmd_parts = self._shell._encode_script(script, as_list=True)
|
||||
if '-EncodedCommand' in cmd_parts:
|
||||
encoded_cmd = cmd_parts[cmd_parts.index('-EncodedCommand') + 1]
|
||||
decoded_cmd = to_unicode(base64.b64decode(encoded_cmd))
|
||||
self._display.vvv("EXEC %s" % decoded_cmd, host=self._play_context.remote_addr)
|
||||
else:
|
||||
self._display.vvv("EXEC %s" % cmd, host=self._play_context.remote_addr)
|
||||
try:
|
||||
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:], from_exec=True)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise AnsibleError("failed to exec cmd %s" % cmd)
|
||||
result.std_out = to_unicode(result.std_out)
|
||||
result.std_err = to_unicode(result.std_err)
|
||||
return (result.status_code, '', result.std_out, result.std_err)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
super(Connection, self).put_file(in_path, out_path)
|
||||
out_path = self._shell._unquote(out_path)
|
||||
self._display.vvv('PUT "%s" TO "%s"' % (in_path, out_path), host=self._play_context.remote_addr)
|
||||
if not os.path.exists(in_path):
|
||||
raise AnsibleFileNotFound('file or module does not exist: "%s"' % in_path)
|
||||
with open(in_path) as in_file:
|
||||
in_size = os.path.getsize(in_path)
|
||||
script_template = '''
|
||||
$s = [System.IO.File]::OpenWrite("%s");
|
||||
[void]$s.Seek(%d, [System.IO.SeekOrigin]::Begin);
|
||||
$b = [System.Convert]::FromBase64String("%s");
|
||||
[void]$s.Write($b, 0, $b.length);
|
||||
[void]$s.SetLength(%d);
|
||||
[void]$s.Close();
|
||||
'''
|
||||
# Determine max size of data we can pass per command.
|
||||
script = script_template % (self._shell._escape(out_path), in_size, '', in_size)
|
||||
cmd = self._shell._encode_script(script)
|
||||
# Encode script with no data, subtract its length from 8190 (max
|
||||
# windows command length), divide by 2.67 (UTF16LE base64 command
|
||||
# encoding), then by 1.35 again (data base64 encoding).
|
||||
buffer_size = int(((8190 - len(cmd)) / 2.67) / 1.35)
|
||||
for offset in xrange(0, in_size, buffer_size):
|
||||
try:
|
||||
out_data = in_file.read(buffer_size)
|
||||
if offset == 0:
|
||||
if out_data.lower().startswith('#!powershell') and not out_path.lower().endswith('.ps1'):
|
||||
out_path = out_path + '.ps1'
|
||||
b64_data = base64.b64encode(out_data)
|
||||
script = script_template % (self._shell._escape(out_path), offset, b64_data, in_size)
|
||||
self._display.vvvvv('WINRM PUT "%s" to "%s" (offset=%d size=%d)' % (in_path, out_path, offset, len(out_data)), host=self._play_context.remote_addr)
|
||||
cmd_parts = self._shell._encode_script(script, as_list=True)
|
||||
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
|
||||
if result.status_code != 0:
|
||||
raise IOError(to_unicode(result.std_err))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
raise AnsibleError('failed to transfer file to "%s"' % out_path)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
super(Connection, self).fetch_file(in_path, out_path)
|
||||
in_path = self._shell._unquote(in_path)
|
||||
out_path = out_path.replace('\\', '/')
|
||||
self._display.vvv('FETCH "%s" TO "%s"' % (in_path, out_path), host=self._play_context.remote_addr)
|
||||
buffer_size = 2**19 # 0.5MB chunks
|
||||
makedirs_safe(os.path.dirname(out_path))
|
||||
out_file = None
|
||||
try:
|
||||
offset = 0
|
||||
while True:
|
||||
try:
|
||||
script = '''
|
||||
If (Test-Path -PathType Leaf "%(path)s")
|
||||
{
|
||||
$stream = [System.IO.File]::OpenRead("%(path)s");
|
||||
$stream.Seek(%(offset)d, [System.IO.SeekOrigin]::Begin) | Out-Null;
|
||||
$buffer = New-Object Byte[] %(buffer_size)d;
|
||||
$bytesRead = $stream.Read($buffer, 0, %(buffer_size)d);
|
||||
$bytes = $buffer[0..($bytesRead-1)];
|
||||
[System.Convert]::ToBase64String($bytes);
|
||||
$stream.Close() | Out-Null;
|
||||
}
|
||||
ElseIf (Test-Path -PathType Container "%(path)s")
|
||||
{
|
||||
Write-Host "[DIR]";
|
||||
}
|
||||
Else
|
||||
{
|
||||
Write-Error "%(path)s does not exist";
|
||||
Exit 1;
|
||||
}
|
||||
''' % dict(buffer_size=buffer_size, path=self._shell._escape(in_path), offset=offset)
|
||||
self._display.vvvvv('WINRM FETCH "%s" to "%s" (offset=%d)' % (in_path, out_path, offset), host=self._play_context.remote_addr)
|
||||
cmd_parts = self._shell._encode_script(script, as_list=True)
|
||||
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
|
||||
if result.status_code != 0:
|
||||
raise IOError(to_unicode(result.std_err))
|
||||
if result.std_out.strip() == '[DIR]':
|
||||
data = None
|
||||
else:
|
||||
data = base64.b64decode(result.std_out.strip())
|
||||
if data is None:
|
||||
makedirs_safe(out_path)
|
||||
break
|
||||
else:
|
||||
if not out_file:
|
||||
# If out_path is a directory and we're expecting a file, bail out now.
|
||||
if os.path.isdir(out_path):
|
||||
break
|
||||
out_file = open(out_path, 'wb')
|
||||
out_file.write(data)
|
||||
if len(data) < buffer_size:
|
||||
break
|
||||
offset += len(data)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
raise AnsibleError('failed to transfer file to "%s"' % out_path)
|
||||
finally:
|
||||
if out_file:
|
||||
out_file.close()
|
||||
|
||||
def close(self):
|
||||
if self.protocol and self.shell_id:
|
||||
self.protocol.close_shell(self.shell_id)
|
||||
self.shell_id = None
|
||||
199
lib/ansible/plugins/connection/zone.py
Normal file
199
lib/ansible/plugins/connection/zone.py
Normal file
@@ -0,0 +1,199 @@
|
||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# and chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||
# and jail.py (c) 2013, Michael Scherer <misc@zarb.org>
|
||||
# (c) 2015, Dagobert Michelsen <dam@baltic-online.de>
|
||||
# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import distutils.spawn
|
||||
import traceback
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
from ansible import errors
|
||||
from ansible.utils.unicode import to_bytes
|
||||
from ansible.callbacks import vvv
|
||||
import ansible.constants as C
|
||||
|
||||
BUFSIZE = 65536
|
||||
|
||||
class Connection(object):
|
||||
''' Local zone based connections '''
|
||||
|
||||
def _search_executable(self, executable):
|
||||
cmd = distutils.spawn.find_executable(executable)
|
||||
if not cmd:
|
||||
raise errors.AnsibleError("%s command not found in PATH") % executable
|
||||
return cmd
|
||||
|
||||
def list_zones(self):
|
||||
pipe = subprocess.Popen([self.zoneadm_cmd, 'list', '-ip'],
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
zones = []
|
||||
for l in pipe.stdout.readlines():
|
||||
# 1:work:running:/zones/work:3126dc59-9a07-4829-cde9-a816e4c5040e:native:shared
|
||||
s = l.split(':')
|
||||
if s[1] != 'global':
|
||||
zones.append(s[1])
|
||||
|
||||
return zones
|
||||
|
||||
def get_zone_path(self):
|
||||
#solaris10vm# zoneadm -z cswbuild list -p
|
||||
#-:cswbuild:installed:/zones/cswbuild:479f3c4b-d0c6-e97b-cd04-fd58f2c0238e:native:shared
|
||||
pipe = subprocess.Popen([self.zoneadm_cmd, '-z', self.zone, 'list', '-p'],
|
||||
cwd=self.runner.basedir,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
#stdout, stderr = p.communicate()
|
||||
path = pipe.stdout.readlines()[0].split(':')[3]
|
||||
return path + '/root'
|
||||
|
||||
def __init__(self, runner, host, port, *args, **kwargs):
|
||||
self.zone = host
|
||||
self.runner = runner
|
||||
self.host = host
|
||||
self.has_pipelining = False
|
||||
self.become_methods_supported=C.BECOME_METHODS
|
||||
|
||||
if os.geteuid() != 0:
|
||||
raise errors.AnsibleError("zone connection requires running as root")
|
||||
|
||||
self.zoneadm_cmd = self._search_executable('zoneadm')
|
||||
self.zlogin_cmd = self._search_executable('zlogin')
|
||||
|
||||
if not self.zone in self.list_zones():
|
||||
raise errors.AnsibleError("incorrect zone name %s" % self.zone)
|
||||
|
||||
|
||||
self.host = host
|
||||
# port is unused, since this is local
|
||||
self.port = port
|
||||
|
||||
def connect(self, port=None):
|
||||
''' connect to the zone; nothing to do here '''
|
||||
|
||||
vvv("THIS IS A LOCAL ZONE DIR", host=self.zone)
|
||||
|
||||
return self
|
||||
|
||||
# a modifier
|
||||
def _generate_cmd(self, executable, cmd):
|
||||
if executable:
|
||||
### TODO: Why was "-c" removed from here? (vs jail.py)
|
||||
local_cmd = [self.zlogin_cmd, self.zone, executable, cmd]
|
||||
else:
|
||||
# Prev to python2.7.3, shlex couldn't handle unicode type strings
|
||||
cmd = to_bytes(cmd)
|
||||
cmd = shlex.split(cmd)
|
||||
local_cmd = [self.zlogin_cmd, self.zone]
|
||||
local_cmd += cmd
|
||||
return local_cmd
|
||||
|
||||
def _buffered_exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable=None, in_data=None, stdin=subprocess.PIPE):
|
||||
''' run a command on the zone. This is only needed for implementing
|
||||
put_file() get_file() so that we don't have to read the whole file
|
||||
into memory.
|
||||
|
||||
compared to exec_command() it looses some niceties like being able to
|
||||
return the process's exit code immediately.
|
||||
'''
|
||||
|
||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
||||
|
||||
if in_data:
|
||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||
|
||||
# We enter zone as root so we ignore privilege escalation (probably need to fix in case we have to become a specific used [ex: postgres admin])?
|
||||
local_cmd = self._generate_cmd(executable, cmd)
|
||||
|
||||
vvv("EXEC %s" % (local_cmd), host=self.zone)
|
||||
p = subprocess.Popen(local_cmd, shell=False,
|
||||
cwd=self.runner.basedir,
|
||||
stdin=stdin,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
return p
|
||||
|
||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable=None, in_data=None):
|
||||
''' run a command on the zone '''
|
||||
|
||||
### TODO: Why all the precautions not to specify /bin/sh? (vs jail.py)
|
||||
if executable == '/bin/sh':
|
||||
executable = None
|
||||
|
||||
p = self._buffered_exec_command(cmd, tmp_path, become_user, sudoable, executable, in_data)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
return (p.returncode, '', stdout, stderr)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to zone '''
|
||||
|
||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.zone)
|
||||
|
||||
try:
|
||||
with open(in_path, 'rb') as in_file:
|
||||
try:
|
||||
p = self._buffered_exec_command('dd of=%s bs=%s' % (out_path, BUFSIZE), None, stdin=in_file)
|
||||
except OSError:
|
||||
raise errors.AnsibleError("jail connection requires dd command in the jail")
|
||||
try:
|
||||
stdout, stderr = p.communicate()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
|
||||
if p.returncode != 0:
|
||||
raise errors.AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
||||
except IOError:
|
||||
raise errors.AnsibleError("file or module does not exist at: %s" % in_path)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from zone to local '''
|
||||
|
||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.zone)
|
||||
|
||||
|
||||
try:
|
||||
p = self._buffered_exec_command('dd if=%s bs=%s' % (in_path, BUFSIZE), None)
|
||||
except OSError:
|
||||
raise errors.AnsibleError("zone connection requires dd command in the zone")
|
||||
|
||||
|
||||
with open(out_path, 'wb+') as out_file:
|
||||
try:
|
||||
chunk = p.stdout.read(BUFSIZE)
|
||||
while chunk:
|
||||
out_file.write(chunk)
|
||||
chunk = p.stdout.read(BUFSIZE)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise errors.AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
pass
|
||||
Reference in New Issue
Block a user