# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import logging import sys import time from subprocess import Popen import botocore.session from ec2instanceconnectcli import __version__ as CLI_VERSION from ec2instanceconnectcli import ec2_util, key_publisher class EC2InstanceConnectCLI(object): """ SSH Transport via socket to EC2 Instance Pushes public key to EC2 Instance Metadata Service (via AWS EC2 Instance Connect) and establishes an SSH connection using the respective private key """ def __init__(self, instance_bundles, pub_key, cli_command, logger): """ :param instance_bundles: list of dicts that provide dns name, zone, etc information about EC2 instances :type instance_bundles: list :param pub_key: ssh public key :type pub_key: basestring :param cli_command: command to run in underlying shell :type cli_command: basestring :param logger: CLI logging utility to send log messages to :type logger: ec2instanceconnectcli.EC2InstanceConnectLogger.EC2InstanceConnectLogger """ self.instance_bundles = instance_bundles self.pub_key = pub_key self.logger = logger self.cli_command = cli_command def call_ec2(self): """ Fetches information on the associated EC2 instance """ for bundle in self.instance_bundles: session = bundle['session'] #If bundle['target'] has a value, then use it. if bundle['target']: bundle['host_info'] = bundle['target'] else: bundle['host_info'] = None if (bundle['target'] and bundle['zone']) or len(bundle['instance_id']) == 0: # If both are specified or we're not using an instance then we have no reason to call EC2 self.logger.debug("{0} does not require lookup".format(bundle['target'])) continue instance_info = ec2_util.get_instance_data(session, bundle['instance_id']) bundle['zone'] = instance_info.availability_zone #If host_info is not available, fallback to using public ipaddress and then private ipaddress. if not bundle['host_info']: bundle['host_info'] = instance_info.public_ip if instance_info.public_ip else instance_info.private_ip self.logger.debug('Successfully got instance information from EC2 API for {0}'.format(bundle['instance_id'])) def handle_keys(self): """ Pushes the public key to the EC2 Instance(s) using AWS EC2 Instance Connect """ for bundle in self.instance_bundles: session = bundle['session'] if len(bundle['instance_id']) == 0: self.logger.debug("{0} does not require pushing public key using EC2InstanceConnect".format(bundle['target'])) continue key_publisher.push_public_key(session, bundle['instance_id'], bundle['username'], self.pub_key, bundle['zone']) self.logger.debug('Successfully pushed the public key to {0}'.format(bundle['instance_id'])) def run_command(self, command=None): """ Runs the given command in a sub-shell :param command: Command to invoke :type command: basestring :return: Return code for remote command :rtype: int """ if not command: raise ValueError('Must provide a command') invocation_proc = Popen(command, shell=True) while invocation_proc.poll() is None: #sub-process not terminated time.sleep(0.1) return invocation_proc.returncode def invoke_command(self): """ Generates the appropriate shell command and invokes it :return: Return code for remote command :rtype: int """ try: for bundle in self.instance_bundles: session = self._get_botocore_session(profile_name=bundle['profile'], region=bundle['region']) # enable debug logging on botocore session if command line debug option is set if self.logger.getEffectiveLevel() == logging.DEBUG: session.set_debug_logger() bundle['session'] = session self.call_ec2() self.handle_keys() #important to generate the command after calling call_ec2 and handle_keys return self.run_command(self.cli_command.get_command()) except Exception as e: self.logger.error("Failed with: " + str(e)) sys.exit(1) @staticmethod def _get_botocore_session(profile_name=None, region=None): """ Generates a botocore session with Managed SSH CLI set as the user agent :param profile_name: The name of a profile to use. If not given, then the \ default profile is used. :type profile_name: string :param region: An AWS region name to set as the default for the Botocore session :type region: string :return: A Botocore session object :rtype: botocore.session.Session """ session = botocore.session.get_session() botocore_info = 'Botocore/{0}'.format(session.user_agent_version) if session.user_agent_extra: session.user_agent_extra += ' ' + botocore_info else: session.user_agent_extra = botocore_info session.user_agent_name = 'aws-ec2-instance-connect-cli' session.user_agent_version = CLI_VERSION """ # Credential precedence: # 1. set user passed profile. # 2. set user passed region. # 3. let botocore handle the rest. """ if profile_name: session.set_config_variable('profile', profile_name) if region is not None: session.set_config_variable('region', region) return session