summaryrefslogtreecommitdiff
path: root/lib/ansible/plugins/connection/aws_ssm.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/plugins/connection/aws_ssm.py')
-rw-r--r--lib/ansible/plugins/connection/aws_ssm.py557
1 files changed, 0 insertions, 557 deletions
diff --git a/lib/ansible/plugins/connection/aws_ssm.py b/lib/ansible/plugins/connection/aws_ssm.py
deleted file mode 100644
index 359db76f3d..0000000000
--- a/lib/ansible/plugins/connection/aws_ssm.py
+++ /dev/null
@@ -1,557 +0,0 @@
-# Based on the ssh connection plugin by Michael DeHaan
-#
-# Copyright: (c) 2018, Pat Sharkey <psharkey@cleo.com>
-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-
-from __future__ import (absolute_import, division, print_function)
-__metaclass__ = type
-
-DOCUMENTATION = """
-author:
-- Pat Sharkey (@psharkey) <psharkey@cleo.com>
-- HanumanthaRao MVL (@hanumantharaomvl) <hanumanth@flux7.com>
-- Gaurav Ashtikar (@gau1991 )<gaurav.ashtikar@flux7.com>
-connection: aws_ssm
-short_description: execute via AWS Systems Manager
-description:
-- This connection plugin allows ansible to execute tasks on an EC2 instance via the aws ssm CLI.
-version_added: "2.10"
-requirements:
-- The remote EC2 instance must be running the AWS Systems Manager Agent (SSM Agent).
-- The control machine must have the aws session manager plugin installed.
-- The remote EC2 linux instance must have the curl installed.
-options:
- instance_id:
- description: The EC2 instance ID.
- vars:
- - name: ansible_aws_ssm_instance_id
- region:
- description: The region the EC2 instance is located.
- vars:
- - name: ansible_aws_ssm_region
- default: 'us-east-1'
- bucket_name:
- description: The name of the S3 bucket used for file transfers.
- vars:
- - name: ansible_aws_ssm_bucket_name
- plugin:
- description: This defines the location of the session-manager-plugin binary.
- vars:
- - name: ansible_aws_ssm_plugin
- default: '/usr/local/bin/session-manager-plugin'
- retries:
- description: Number of attempts to connect.
- default: 3
- type: integer
- vars:
- - name: ansible_aws_ssm_retries
- timeout:
- description: Connection timeout seconds.
- default: 60
- type: integer
- vars:
- - name: ansible_aws_ssm_timeout
-"""
-
-EXAMPLES = r'''
-
-# Stop Spooler Process on Windows Instances
-- name: Stop Spooler Service on Windows Instances
- vars:
- ansible_connection: aws_ssm
- ansible_shell_type: powershell
- ansible_aws_ssm_bucket_name: nameofthebucket
- ansible_aws_ssm_region: us-east-1
- tasks:
- - name: Stop spooler service
- win_service:
- name: spooler
- state: stopped
-
-# Install a Nginx Package on Linux Instance
-- name: Install a Nginx Package
- vars:
- ansible_connection: aws_ssm
- ansible_aws_ssm_bucket_name: nameofthebucket
- ansible_aws_ssm_region: us-west-2
- tasks:
- - name: Install a Nginx Package
- yum:
- name: nginx
- state: present
-
-# Create a directory in Windows Instances
-- name: Create a directory in Windows Instance
- vars:
- ansible_connection: aws_ssm
- ansible_shell_type: powershell
- ansible_aws_ssm_bucket_name: nameofthebucket
- ansible_aws_ssm_region: us-east-1
- tasks:
- - name: Create a Directory
- win_file:
- path: C:\Windows\temp
- state: directory
-
-# Making use of Dynamic Inventory Plugin
-# =======================================
-# aws_ec2.yml (Dynamic Inventory - Linux)
-# This will return the Instance IDs matching the filter
-#plugin: aws_ec2
-#regions:
-# - us-east-1
-#hostnames:
-# - instance-id
-#filters:
-# tag:SSMTag: ssmlinux
-# -----------------------
-- name: install aws-cli
- hosts: all
- gather_facts: false
- vars:
- ansible_connection: aws_ssm
- ansible_aws_ssm_bucket_name: nameofthebucket
- ansible_aws_ssm_region: us-east-1
- tasks:
- - name: aws-cli
- raw: yum install -y awscli
- tags: aws-cli
-# Execution: ansible-playbook linux.yaml -i aws_ec2.yml
-# The playbook tasks will get executed on the instance ids returned from the dynamic inventory plugin using ssm connection.
-# =====================================================
-# aws_ec2.yml (Dynamic Inventory - Windows)
-#plugin: aws_ec2
-#regions:
-# - us-east-1
-#hostnames:
-# - instance-id
-#filters:
-# tag:SSMTag: ssmwindows
-# -----------------------
-- name: Create a dir.
- hosts: all
- gather_facts: false
- vars:
- ansible_connection: aws_ssm
- ansible_shell_type: powershell
- ansible_aws_ssm_bucket_name: nameofthebucket
- ansible_aws_ssm_region: us-east-1
- tasks:
- - name: Create the directory
- win_file:
- path: C:\Temp\SSM_Testing5
- state: directory
-# Execution: ansible-playbook win_file.yaml -i aws_ec2.yml
-# The playbook tasks will get executed on the instance ids returned from the dynamic inventory plugin using ssm connection.
-'''
-
-import os
-import getpass
-import json
-import os
-import pty
-import random
-import re
-import select
-import string
-import subprocess
-import time
-
-try:
- import boto3
- HAS_BOTO_3 = True
-except ImportError as e:
- HAS_BOTO_3_ERROR = str(e)
- HAS_BOTO_3 = False
-
-from functools import wraps
-from ansible import constants as C
-from ansible.errors import AnsibleConnectionFailure, AnsibleError, AnsibleFileNotFound
-from ansible.module_utils.basic import missing_required_lib
-from ansible.module_utils.six import PY3
-from ansible.module_utils.six.moves import xrange
-from ansible.module_utils._text import to_bytes, to_native, to_text
-from ansible.plugins.connection import ConnectionBase
-from ansible.plugins.shell.powershell import _common_args
-from ansible.utils.display import Display
-
-display = Display()
-
-
-def _ssm_retry(func):
- """
- Decorator to retry in the case of a connection failure
- Will retry if:
- * an exception is caught
- Will not retry if
- * remaining_tries is <2
- * retries limit reached
- """
- @wraps(func)
- def wrapped(self, *args, **kwargs):
- remaining_tries = int(self.get_option('retries')) + 1
- cmd_summary = "%s..." % args[0]
- for attempt in range(remaining_tries):
- cmd = args[0]
-
- try:
- return_tuple = func(self, *args, **kwargs)
- display.vvv(return_tuple, host=self.host)
- break
-
- except (AnsibleConnectionFailure, Exception) as e:
- if attempt == remaining_tries - 1:
- raise
- else:
- pause = 2 ** attempt - 1
- if pause > 30:
- pause = 30
-
- if isinstance(e, AnsibleConnectionFailure):
- msg = "ssm_retry: attempt: %d, cmd (%s), pausing for %d seconds" % (attempt, cmd_summary, pause)
- else:
- msg = "ssm_retry: attempt: %d, caught exception(%s) from cmd (%s), pausing for %d seconds" % (attempt, e, cmd_summary, pause)
-
- display.vv(msg, host=self.host)
-
- time.sleep(pause)
-
- # Do not attempt to reuse the existing session on retries
- self.close()
-
- continue
-
- return return_tuple
- return wrapped
-
-
-def chunks(lst, n):
- """Yield successive n-sized chunks from lst."""
- for i in range(0, len(lst), n):
- yield lst[i:i + n]
-
-
-class Connection(ConnectionBase):
- ''' AWS SSM based connections '''
-
- transport = 'aws_ssm'
- allow_executable = False
- allow_extras = True
- has_pipelining = False
- is_windows = False
- _client = None
- _session = None
- _stdout = None
- _session_id = ''
- _timeout = False
- MARK_LENGTH = 26
-
- def __init__(self, *args, **kwargs):
- if not HAS_BOTO_3:
- raise AnsibleError('{0}: {1}'.format(missing_required_lib("boto3"), HAS_BOTO_3_ERROR))
-
- super(Connection, self).__init__(*args, **kwargs)
- self.host = self._play_context.remote_addr
-
- if getattr(self._shell, "SHELL_FAMILY", '') == 'powershell':
- self.delegate = None
- self.has_native_async = True
- self.always_pipeline_modules = True
- self.module_implementation_preferences = ('.ps1', '.exe', '')
- self.protocol = None
- self.shell_id = None
- self._shell_type = 'powershell'
- self.is_windows = True
-
- def _connect(self):
- ''' connect to the host via ssm '''
-
- self._play_context.remote_user = getpass.getuser()
-
- if not self._session_id:
- self.start_session()
- return self
-
- def start_session(self):
- ''' start ssm session '''
-
- if self.get_option('instance_id') is None:
- self.instance_id = self.host
- else:
- self.instance_id = self.get_option('instance_id')
-
- display.vvv(u"ESTABLISH SSM CONNECTION TO: {0}".format(self.instance_id), host=self.host)
-
- executable = self.get_option('plugin')
- if not os.path.exists(to_bytes(executable, errors='surrogate_or_strict')):
- raise AnsibleError("failed to find the executable specified %s."
- " Please verify if the executable exists and re-try." % executable)
-
- profile_name = ''
- region_name = self.get_option('region')
- ssm_parameters = dict()
-
- client = boto3.client('ssm', region_name=region_name)
- self._client = client
- response = client.start_session(Target=self.instance_id, Parameters=ssm_parameters)
- self._session_id = response['SessionId']
-
- cmd = [
- executable,
- json.dumps(response),
- region_name,
- "StartSession",
- profile_name,
- json.dumps({"Target": self.instance_id}),
- client.meta.endpoint_url
- ]
-
- display.vvvv(u"SSM COMMAND: {0}".format(to_text(cmd)), host=self.host)
-
- stdout_r, stdout_w = pty.openpty()
- session = subprocess.Popen(
- cmd,
- stdin=subprocess.PIPE,
- stdout=stdout_w,
- stderr=subprocess.PIPE,
- close_fds=True,
- bufsize=0,
- )
-
- os.close(stdout_w)
- self._stdout = os.fdopen(stdout_r, 'rb', 0)
- self._session = session
- self._poll_stdout = select.poll()
- self._poll_stdout.register(self._stdout, select.POLLIN)
-
- # Disable command echo and prompt.
- self._prepare_terminal()
-
- display.vvv(u"SSM CONNECTION ID: {0}".format(self._session_id), host=self.host)
-
- return session
-
- @_ssm_retry
- def exec_command(self, cmd, in_data=None, sudoable=True):
- ''' run a command on the ssm host '''
-
- super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
-
- display.vvv(u"EXEC {0}".format(to_text(cmd)), host=self.host)
-
- session = self._session
-
- mark_begin = "".join([random.choice(string.ascii_letters) for i in xrange(self.MARK_LENGTH)])
- if self.is_windows:
- mark_start = mark_begin + " $LASTEXITCODE"
- else:
- mark_start = mark_begin
- mark_end = "".join([random.choice(string.ascii_letters) for i in xrange(self.MARK_LENGTH)])
-
- # Wrap command in markers accordingly for the shell used
- cmd = self._wrap_command(cmd, sudoable, mark_start, mark_end)
-
- self._flush_stderr(session)
-
- for chunk in chunks(cmd, 1024):
- session.stdin.write(to_bytes(chunk, errors='surrogate_or_strict'))
-
- # Read stdout between the markers
- stdout = ''
- win_line = ''
- begin = False
- stop_time = int(round(time.time())) + self.get_option('timeout')
- while session.poll() is None:
- remaining = stop_time - int(round(time.time()))
- if remaining < 1:
- self._timeout = True
- display.vvvv(u"EXEC timeout stdout: {0}".format(to_text(stdout)), host=self.host)
- raise AnsibleConnectionFailure("SSM exec_command timeout on host: %s"
- % self.instance_id)
- if self._poll_stdout.poll(1000):
- line = self._filter_ansi(self._stdout.readline())
- display.vvvv(u"EXEC stdout line: {0}".format(to_text(line)), host=self.host)
- else:
- display.vvvv(u"EXEC remaining: {0}".format(remaining), host=self.host)
- continue
-
- if not begin and self.is_windows:
- win_line = win_line + line
- line = win_line
-
- if mark_start in line:
- begin = True
- if not line.startswith(mark_start):
- stdout = ''
- continue
- if begin:
- if mark_end in line:
- display.vvvv(u"POST_PROCESS: {0}".format(to_text(stdout)), host=self.host)
- returncode, stdout = self._post_process(stdout, mark_begin)
- break
- else:
- stdout = stdout + line
-
- stderr = self._flush_stderr(session)
-
- return (returncode, stdout, stderr)
-
- def _prepare_terminal(self):
- ''' perform any one-time terminal settings '''
-
- if not self.is_windows:
- cmd = "stty -echo\n" + "PS1=''\n"
- cmd = to_bytes(cmd, errors='surrogate_or_strict')
- self._session.stdin.write(cmd)
-
- def _wrap_command(self, cmd, sudoable, mark_start, mark_end):
- ''' wrap command so stdout and status can be extracted '''
-
- if self.is_windows:
- if not cmd.startswith(" ".join(_common_args) + " -EncodedCommand"):
- cmd = self._shell._encode_script(cmd, preserve_rc=True)
- cmd = cmd + "; echo " + mark_start + "\necho " + mark_end + "\n"
- else:
- if sudoable:
- cmd = "sudo " + cmd
- cmd = "echo " + mark_start + "\n" + cmd + "\necho $'\\n'$?\n" + "echo " + mark_end + "\n"
-
- display.vvvv(u"_wrap_command: '{0}'".format(to_text(cmd)), host=self.host)
- return cmd
-
- def _post_process(self, stdout, mark_begin):
- ''' extract command status and strip unwanted lines '''
-
- if self.is_windows:
- # Value of $LASTEXITCODE will be the line after the mark
- trailer = stdout[stdout.rfind(mark_begin):]
- last_exit_code = trailer.splitlines()[1]
- if last_exit_code.isdigit:
- returncode = int(last_exit_code)
- else:
- returncode = -1
- # output to keep will be before the mark
- stdout = stdout[:stdout.rfind(mark_begin)]
-
- # If it looks like JSON remove any newlines
- if stdout.startswith('{'):
- stdout = stdout.replace('\n', '')
-
- return (returncode, stdout)
- else:
- # Get command return code
- returncode = int(stdout.splitlines()[-2])
-
- # Throw away ending lines
- for x in range(0, 3):
- stdout = stdout[:stdout.rfind('\n')]
-
- return (returncode, stdout)
-
- def _filter_ansi(self, line):
- ''' remove any ANSI terminal control codes '''
- line = to_text(line)
-
- if self.is_windows:
- osc_filter = re.compile(r'\x1b\][^\x07]*\x07')
- line = osc_filter.sub('', line)
- ansi_filter = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
- line = ansi_filter.sub('', line)
-
- # Replace or strip sequence (at terminal width)
- line = line.replace('\r\r\n', '\n')
- if len(line) == 201:
- line = line[:-1]
-
- return line
-
- def _flush_stderr(self, subprocess):
- ''' read and return stderr with minimal blocking '''
-
- poll_stderr = select.poll()
- poll_stderr.register(subprocess.stderr, select.POLLIN)
- stderr = ''
-
- while subprocess.poll() is None:
- if poll_stderr.poll(1):
- line = subprocess.stderr.readline()
- display.vvvv(u"stderr line: {0}".format(to_text(line)), host=self.host)
- stderr = stderr + line
- else:
- break
-
- return stderr
-
- def _get_url(self, client_method, bucket_name, out_path, http_method):
- ''' Generate URL for get_object / put_object '''
- client = boto3.client('s3')
- return client.generate_presigned_url(client_method, Params={'Bucket': bucket_name, 'Key': out_path}, ExpiresIn=3600, HttpMethod=http_method)
-
- @_ssm_retry
- def _file_transport_command(self, in_path, out_path, ssm_action):
- ''' transfer a file from using an intermediate S3 bucket '''
-
- s3_path = out_path.replace('\\', '/')
- bucket_url = 's3://%s/%s' % (self.get_option('bucket_name'), s3_path)
-
- if self.is_windows:
- put_command = "Invoke-WebRequest -Method PUT -InFile '%s' -Uri '%s' -UseBasicParsing" % (
- in_path, self._get_url('put_object', self.get_option('bucket_name'), s3_path, 'PUT'))
- get_command = "Invoke-WebRequest '%s' -OutFile '%s'" % (
- self._get_url('get_object', self.get_option('bucket_name'), s3_path, 'GET'), out_path)
- else:
- put_command = "curl --request PUT --upload-file '%s' '%s'" % (
- in_path, self._get_url('put_object', self.get_option('bucket_name'), s3_path, 'PUT'))
- get_command = "curl '%s' -o '%s'" % (
- self._get_url('get_object', self.get_option('bucket_name'), s3_path, 'GET'), out_path)
-
- client = boto3.client('s3')
- if ssm_action == 'get':
- (returncode, stdout, stderr) = self.exec_command(put_command, in_data=None, sudoable=False)
- with open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb') as data:
- client.download_fileobj(self.get_option('bucket_name'), s3_path, data)
- else:
- with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as data:
- client.upload_fileobj(data, self.get_option('bucket_name'), s3_path)
- (returncode, stdout, stderr) = self.exec_command(get_command, in_data=None, sudoable=False)
-
- # Check the return code
- if returncode == 0:
- return (returncode, stdout, stderr)
- else:
- raise AnsibleError("failed to transfer file to %s %s:\n%s\n%s" %
- (to_native(in_path), to_native(out_path), to_native(stdout), to_native(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)
-
- display.vvv(u"PUT {0} TO {1}".format(in_path, out_path), host=self.host)
- if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
- raise AnsibleFileNotFound("file or module does not exist: {0}".format(to_native(in_path)))
-
- return self._file_transport_command(in_path, out_path, 'put')
-
- def fetch_file(self, in_path, out_path):
- ''' fetch a file from remote to local '''
-
- super(Connection, self).fetch_file(in_path, out_path)
-
- display.vvv(u"FETCH {0} TO {1}".format(in_path, out_path), host=self.host)
- return self._file_transport_command(in_path, out_path, 'get')
-
- def close(self):
- ''' terminate the connection '''
- if self._session_id:
-
- display.vvv(u"CLOSING SSM CONNECTION TO: {0}".format(self.instance_id), host=self.host)
- if self._timeout:
- self._session.terminate()
- else:
- cmd = b"\nexit\n"
- self._session.communicate(cmd)
-
- display.vvvv(u"TERMINATE SSM SESSION: {0}".format(self._session_id), host=self.host)
- self._client.terminate_session(SessionId=self._session_id)
- self._session_id = ''