def _validate_iam_groups(config_aws_setup): """validate IAM groups' policies are subsets of described IAM policies""" setup_policies = set(config_aws_setup['IAM']['Policies'].keys()) for name, iam_group in config_aws_setup['IAM']['Groups'].items(): if not set(iam_group['Policies']).issubset(setup_policies): halt.err( "aws_setup.json incorrectly formatted:", f"IAM group {name} contains following invalid policy(s):", *[ policy for policy in iam_group['Policies'] if policy not in setup_policies ])
def main(self, cmd_args): """set another IAM user's access key as default in config""" user_name = cmd_args.user_name if user_name.lower() == consts.IAM_NAME.lower(): halt.err(f"You are already {consts.IAM_NAME}.") user_name = self._switch_access_key(user_name) print("") print(f"{user_name}'s access key set as default in config.")
def ec2_client(region: Optional[str]): """wrapper for ec2_client_no_validate which validates specified region""" if region is None: # True for when command has unused region argument if len(consts.REGIONS) > 1: halt.err("AWS region whitelist has more than one entry.", " A region must be specified using the -r argument.") region = consts.REGIONS[0] elif region not in consts.REGIONS: halt.err(f"\"{region}\" not in region whitelist.") return ec2_client_no_validate(region)
def main(elastic_ip_address): """attempt to return elastic IP address with specified IP Requires ec2:DescribeInstances and ec2:DescribeAddresses permissions. Halts if address with IP not found. This functionality is relied upon. """ try: return next(address for address in probe_regions() if address['ip'] == elastic_ip_address) except StopIteration: halt.err("You do not possess the specified elastic IP address.")
def upload_component(self, vpc_and_sg_info): """create VPC(s) and create/update SG(s) in AWS region(s) Args: vpc_and_sg_info (dict): See what check_component returns. """ vpc_regions, sg_names = vpc_and_sg_info vpc_threader = Threader() for region in vpc_regions['ToCreate']: vpc_threader.add_thread(self._create_vpc, (region, )) vpc_threader.get_results() create_num = len(vpc_regions['ToCreate']) if create_num > 0: print(f"VPC {consts.NAMESPACE} created in {create_num} region(s).") else: print(f"VPC {consts.NAMESPACE} already present " "in whitelisted region(s).") vpc_ids = {} threader = Threader() for region in consts.REGIONS: threader.add_thread(aws.get_region_vpc, (region, )) for region, vpc in threader.get_results(return_dict=True).items(): if vpc is None: halt.err(f"Namespace VPC not created in {region} region.") vpc_ids[region] = vpc['VpcId'] sg_threader = Threader() for sg_name, sg_regions in sg_names.items(): sg_desc = self._security_group_setup[sg_name] for region in sg_regions['ToCreate']: sg_threader.add_thread( self._create_sg, (region, sg_name, sg_desc, vpc_ids[region])) for region in sg_regions['ToUpdate']: sg_threader.add_thread(self._update_sg, (region, sg_name, vpc_ids[region])) sg_threader.get_results() for sg_name, sg_regions in sg_names.items(): if sg_regions['ToCreate']: print(f"VPC SG {sg_name} created in " f"{len(sg_regions['ToCreate'])} region(s).") if sg_regions['ToUpdate']: print(f"VPC SG {sg_name} updated in " f"{len(sg_regions['ToUpdate'])} region(s).") if not sg_regions['ToCreate'] and not sg_regions['ToUpdate']: print(f"VPC SG {sg_name} already up to date " "in whitelisted region(s).")
def _access_key_usable_waiter(new_key): """waiter for IAM user access key usability (not perfect)""" iam_client = boto3.client("iam", aws_access_key_id=next(iter(new_key)), aws_secret_access_key=next( iter(new_key.values()))) for _ in range(60): with aws.ClientErrorHalt(allow=["InvalidClientTokenId"]): # New IAM user is assumed to have the iam:GetUser permission. iam_client.get_user() break sleep(1) else: halt.err("Access key not usable even after waiting 1 minute.")
def main(self, cmd_args): """associate elastic IP address to an (other) instance Args: cmd_args (namedtuple): See add_documentation method. """ address = find_addresses.main(cmd_args.ip) ec2_client = aws.ec2_client(address['region']) all_instances = find_instances.probe_regions() try: instance = next(instance for instance in all_instances if instance['name'] == cmd_args.name) except StopIteration: halt.err(f"Instance named \"{cmd_args.name}\" not found.") if instance['region'] != address['region']: halt.err("Instance and address are in different regions.") if 'instance_name' in address: if instance['name'] == address['instance_name']: halt.err("Address already associated with specified instance.") if 'association_id' in address and cmd_args.force is False: halt.err(f"Elastic IP address {address['ip']} currently in use.", " Append the -f argument to force disassociation.") with aws.ClientErrorHalt(): if 'association_id' in address: ec2_client.disassociate_address( AssociationId=address['association_id']) ec2_client.associate_address(AllocationId=address['allocation_id'], InstanceId=instance['id']) print("") print("Address associated with instance.")
def get_region_vpc(region: str) -> Optional[Dict]: """get VPC from region with name of aws_setup's namespace Requires ec2:DescribeVpcs permission. """ vpcs = ec2_client(region).describe_vpcs( Filters=[{ 'Name': "tag:Name", 'Values': [consts.NAMESPACE] }])['Vpcs'] if len(vpcs) > 1: halt.err(f"Multiple VPCs named {consts.NAMESPACE} in {region} region.") elif vpcs: return vpcs[0] return None
def _open_putty_session(user_and_hostname, ppk_key_path): """open interactive SSH session using the PuTTY client""" if not ppk_key_path.is_file(): halt.err(f"{ppk_key_path.name} not found from config.", f" {ppk_key_path.name} file required to SSH with PuTTY.", " You can convert a .pem file to .ppk using puttygen.") ppk_key_path.chmod(consts.PK_PERMS) print("") print("Attempting to SSH into instance with PuTTY...") subprocess.run([ "putty", "-ssh", "-i", str(ppk_key_path), user_and_hostname ]) print("Connection closed.")
def pem_to_public_key(der_encoded: bool = False) -> bytes: """convert pem RSA private key string to public key bytes""" pem_str = consts.RSA_KEY_PEM.read_text(encoding="utf-8") try: private_key = serialization.load_pem_private_key( pem_str.encode("utf-8"), password=None, backend=default_backend()) except ValueError: halt.err(f"{consts.RSA_KEY_PEM} not a valid RSA private key.") if der_encoded is True: return private_key.public_key().public_bytes( serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo) return private_key.public_key().public_bytes( serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH)
def main(self, cmd_args): """create and initialize a new EC2 instance Args: cmd_args (namedtuple): See add_documentation method. """ template_yaml_files = os2.dir_files(consts.USER_DATA_DIR) if f"{cmd_args.template}.yaml" not in template_yaml_files: halt.err(f"Template {cmd_args.template} not found from config.") self._validate_name_is_unique(cmd_args.name) self._validate_limits_not_reached(cmd_args.elastic_ip) inst_template = os2.parse_yaml( consts.USER_DATA_DIR / f"{cmd_args.template}.yaml")['ec2mc_template_info'] self._validate_type_and_size_allowed(inst_template['instance_type'], inst_template['volume_size']) if cmd_args.use_ip is not None: address = self._validate_address(cmd_args.use_ip, cmd_args.region, cmd_args.force) creation_kwargs = self._parse_creation_kwargs(cmd_args, inst_template) user_data = self._process_user_data(cmd_args.template, inst_template) self._create_instance(creation_kwargs, user_data, dry_run=True) print("") if cmd_args.confirm is False: print("IAM permissions and instance template validated.") print("Append the -c argument to confirm instance creation.") return instance = self._create_instance(creation_kwargs, user_data, dry_run=False) print("Instance created. It may take a few minutes to initialize.") if consts.USE_HANDLER is True: print(" Utilize IP handler with \"ec2mc servers check\".") if cmd_args.elastic_ip is True: self._create_elastic_ip(cmd_args.region, instance['InstanceId']) print("New elastic IP associated with created instance.") elif cmd_args.use_ip is not None: self._reuse_elastic_ip(address, instance['InstanceId']) print("Existing elastic IP associated with created instance.")
def _open_openssh_session(user_and_hostname, pem_key_path): """open interactive SSH session using the OpenSSH client""" if not pem_key_path.is_file(): halt.err(f"{pem_key_path.name} not found from config.", f" {pem_key_path.name} file required to SSH with OpenSSH.") pem_key_path.chmod(consts.PK_PERMS) print("") print("Attempting to SSH into instance with OpenSSH...") subprocess.run([ "ssh", "-o", "LogLevel=ERROR", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-i", str(pem_key_path), user_and_hostname ])
def _parse_filters(cmd_args): """parses region and tag filters Args: cmd_args (namedtuple): See main's arguments. Returns: tuple: list[str]: Region(s) to probe. list[dict]: Filter to pass to EC2 client's describe_instances. """ regions = consts.REGIONS if cmd_args.region_filter is not None: region_filter = set(cmd_args.region_filter) # Validate region filter if not region_filter.issubset(set(regions)): halt.err("Following region(s) not in region whitelist:", *(region_filter - set(regions))) regions = tuple(region_filter) tag_filter = [] if cmd_args.tag_filters: # Convert dict(s) list to what describe_instances' Filters expects. for filter_elements in cmd_args.tag_filters: # Filter instances based on tag key-value(s). if len(filter_elements) > 1: tag_filter.append({ 'Name': f"tag:{filter_elements[0]}", 'Values': filter_elements[1:] }) # If no filter tag values given, filter by just the tag key. elif filter_elements: tag_filter.append({ 'Name': "tag-key", 'Values': [filter_elements[0]] }) if cmd_args.name_filter: tag_filter.append({'Name': "tag:Name", 'Values': cmd_args.name_filter}) if cmd_args.id_filter: tag_filter.append({ 'Name': "instance-id", 'Values': cmd_args.id_filter }) return (regions, tag_filter)
def main(self, cmd_args): """disassociate elastic IP address from its instance Args: cmd_args (namedtuple): See add_documentation method. """ address = find_addresses.main(cmd_args.ip) ec2_client = aws.ec2_client(address['region']) if 'association_id' not in address: halt.err("Elastic IP address not associated with anything.") with aws.ClientErrorHalt(): ec2_client.disassociate_address( AssociationId=address['association_id']) print("") print("Elastic IP address disassociated.")
def _request_specific_address(self, region, ipv4_ip): """request specific IPv4 elastic IP address from AWS""" for address in find_addresses.probe_regions(): if address['ip'] == ipv4_ip: if region is not None and region != address['region']: halt.err( "You already possess this elastic IP address.", f" It is located in the {address['region']} region.") halt.err("You already possess this elastic IP address.") try: return self._ec2_client.allocate_address(Domain="vpc", Address=ipv4_ip) except ClientError as e: if e.response['Error']['Code'] == "InvalidParameterValue": halt.err(f"\"{ipv4_ip}\" is not a valid IPv4 address.") if e.response['Error']['Code'] == "InvalidAddress.NotFound": halt.err(f"IP \"{ipv4_ip}\" not available.") halt.err(str(e))
def _validate_instance_templates(): """validate config aws_setup user_data YAML instance templates""" template_yaml_files = os2.dir_files(consts.USER_DATA_DIR, ext=".yaml") schema = os2.get_json_schema("instance_templates") for template_yaml_file in template_yaml_files: template_name = Path(template_yaml_file).stem user_data = os2.parse_yaml(consts.USER_DATA_DIR / template_yaml_file) os2.validate_dict(user_data, schema, template_yaml_file) template_info = user_data['ec2mc_template_info'] if 'write_directories' not in template_info: continue for write_dir in template_info['write_directories']: dir_path = consts.USER_DATA_DIR.joinpath(*write_dir['local_dir']) if not dir_path.is_dir(): halt.err(f"{dir_path} directory for the {template_name} " "template not found.")
def main(self, cmd_args): """create a new IAM user under an IAM group""" iam_client = aws.iam_client() path_prefix = f"/{consts.NAMESPACE}/" aws.validate_group_exists(path_prefix, cmd_args.group) # IAM user created and added to group (given the name is unique) try: iam_client.create_user(Path=path_prefix, UserName=cmd_args.name) except ClientError as e: if e.response['Error']['Code'] == "EntityAlreadyExists": halt.err(f"IAM user \"{cmd_args.name}\" already exists.") halt.err(str(e)) iam_client.add_user_to_group(GroupName=cmd_args.group, UserName=cmd_args.name) print("") print(f"IAM user \"{cmd_args.name}\" created on AWS.") # IAM user access key generated and saved to dictionary new_key = iam_client.create_access_key( UserName=cmd_args.name)['AccessKey'] new_key = {new_key['AccessKeyId']: new_key['SecretAccessKey']} self._access_key_usable_waiter(new_key) config_dict = os2.parse_json(consts.CONFIG_JSON) if 'backup_keys' not in config_dict: config_dict['backup_keys'] = {} if cmd_args.default: # Modify existing config instead of creating new one config_dict['backup_keys'].update(config_dict['access_key']) config_dict['access_key'] = new_key os2.save_json(config_dict, consts.CONFIG_JSON) print(" User's access key set as default in config.") else: # Back up new IAM user's access key in config file config_dict['backup_keys'].update(new_key) os2.save_json(config_dict, consts.CONFIG_JSON) os2.create_configuration_zip(cmd_args.name, new_key, cmd_args.ssh_key) print(" User's zipped configuration created in config.")
def upload_component(self, fingerprint_regions): """create namespace EC2 key pair in each whitelisted AWS region Args: fingerprint_regions (dict): See what check_component returns. """ aws_fingerprints = [ fp for fp in fingerprint_regions.values() if fp is not None ] if consts.RSA_KEY_PEM.is_file(): pub_key_bytes = pem.pem_to_public_key() print(f"Using existing {self._pem_file} file for EC2 key pair(s).") # If SSH key pair doesn't exist in any regions, create a new one elif not aws_fingerprints: pub_key_bytes = pem.generate_rsa_key_pair() print(f"Generating new {self._pem_file} file for EC2 key pair(s).") # No private key file, and there are existing EC2 key pairs else: halt.err( f"RSA private key file {self._pem_file} not found.", " Additional pairs must be created from same private key.") if len(set(aws_fingerprints)) > 1: print("Warning: Differing EC2 key pairs found.") local_key_fingerprint = pem.local_key_fingerprint() if local_key_fingerprint not in aws_fingerprints and aws_fingerprints: halt.err("Local key fingerprint doesn't match any EC2 key pair.") threader = Threader() for region in fingerprint_regions: if fingerprint_regions[region] is None: threader.add_thread(self._create_region_key_pair, (region, pub_key_bytes)) created_pair_fingerprints = threader.get_results() if created_pair_fingerprints: print(f"EC2 key pair {self._key_pair_name} created in " f"{len(created_pair_fingerprints)} AWS region(s).") else: print(f"EC2 key pair {self._key_pair_name} " "already present in whitelisted region(s).")
def _switch_access_key(user_name): """set access key stored in backup access keys list as default""" config_dict = os2.parse_json(consts.CONFIG_JSON) if 'backup_keys' not in config_dict: halt.err("No backup access keys stored in config.") for key_id, key_secret in config_dict['backup_keys'].items(): # TODO: Validate access key is active key_owner = aws.access_key_owner(key_id) if key_owner is None: continue if key_owner.lower() == user_name.lower(): # Swap default access key with requested IAM user's in config config_dict['backup_keys'].update(config_dict['access_key']) config_dict['access_key'] = {key_id: key_secret} del config_dict['backup_keys'][key_id] os2.save_json(config_dict, consts.CONFIG_JSON) return key_owner halt.err(f"Backup access key for IAM user \"{user_name}\" not found.")
def main(self, cmd_args): """list IAM groups and their IAM users""" iam_client = aws.iam_client() path_prefix = f"/{consts.NAMESPACE}/" iam_group_names = [iam_group['GroupName'] for iam_group in iam_client.list_groups(PathPrefix=path_prefix)['Groups']] if not iam_group_names: halt.err("No namespace IAM groups found from AWS.", " Have you uploaded the AWS setup?") print("") print(f"{len(iam_group_names)} IAM group(s) found from AWS:") for group_name in iam_group_names: group_users = iam_client.get_group(GroupName=group_name)['Users'] if group_users: print(f"{group_name}: {len(group_users)} user(s) in group:") for group_user in group_users: print(f" {group_user['UserName']}") else: print(f"{group_name}: 0 users in group.")
def _validate_iam_policies(config_aws_setup): """validate aws_setup.json reflects contents of iam_policies dir""" policy_dir = consts.AWS_SETUP_DIR / "iam_policies" # Policies described in aws_setup/aws_setup.json setup_policy_list = [ f"{policy}.json" for policy in config_aws_setup['IAM']['Policies'] ] # Actual policy JSON files located in aws_setup/iam_policies/ iam_policy_files = os2.dir_files(policy_dir, ext=".json") # Halt if any IAM policy file contains invalid JSON for iam_policy_file in iam_policy_files: os2.parse_json(policy_dir / iam_policy_file) # Halt if aws_setup.json describes policies not found in iam_policies if not set(setup_policy_list).issubset(set(iam_policy_files)): halt.err( "Following policy(s) not found from aws_setup/iam_policies/:", *[ policy for policy in setup_policy_list if policy not in iam_policy_files ])
def main(): """validate contents of user's config's aws_setup directory""" # Directory path for distribution's packaged aws_setup src_aws_setup_dir = consts.DIST_DIR / "aws_setup_src" # If consts.AWS_SETUP_DIR nonexistant, copy from ec2mc.aws_setup_src if not consts.AWS_SETUP_DIR.is_dir(): _cp_aws_setup_to_config(src_aws_setup_dir) config_aws_setup = _get_config_aws_setup_dict() # Config's aws_setup.json must contain the 'Modified' key if 'Modified' not in config_aws_setup: halt.err( "'Modified' key missing from aws_setup.json.", " Delete your config's aws_setup folder and it will regenerate.") # If 'Modified' key is True, prevent overwriting config's aws_setup if config_aws_setup['Modified'] is False: cmp_files = os2.recursive_dir_files(src_aws_setup_dir) diffs = filecmp.cmpfiles(src_aws_setup_dir, consts.AWS_SETUP_DIR, cmp_files, shallow=False) # If source and config aws_setup differ, overwrite config aws_setup # If config aws_setup missing files, overwrite config aws_setup if diffs[1] or diffs[2]: _cp_aws_setup_to_config(src_aws_setup_dir) print("Config's aws_setup directory updated.") config_aws_setup = _get_config_aws_setup_dict() consts.NAMESPACE = config_aws_setup['Namespace'] consts.IAM_PREFIX = f"/{consts.NAMESPACE}/" consts.RSA_KEY_PEM = consts.CONFIG_DIR / f"{consts.NAMESPACE}.pem" consts.RSA_KEY_PPK = consts.CONFIG_DIR / f"{consts.NAMESPACE}.ppk" _validate_iam_policies(config_aws_setup) _validate_iam_groups(config_aws_setup) _validate_vpc_security_groups(config_aws_setup) _validate_instance_templates()
def main(self, cmd_args): """delete an existing IAM user from AWS""" path_prefix = f"/{consts.NAMESPACE}/" user_name = cmd_args.name # IAM user names cannot differ only by case if user_name.lower() == consts.IAM_NAME.lower(): halt.err("You cannot delete yourself.") user_name = aws.validate_user_exists(path_prefix, user_name) self._delete_user_access_keys(user_name) self._remove_user_from_groups(user_name) self._detach_user_from_policies(user_name) self._iam_client.delete_user(UserName=user_name) print("") print(f"IAM user \"{user_name}\" deleted from AWS.") user_config_zip = consts.CONFIG_DIR / f"{user_name}_config.zip" if user_config_zip.is_file(): user_config_zip.unlink() print(" User's zipped configuration deleted from config.")
def _validate_vpc_security_groups(config_aws_setup): """validate aws_setup.json reflects contents of vpc_security_groups dir""" sg_dir = consts.AWS_SETUP_DIR / "vpc_security_groups" # SGs described in aws_setup/aws_setup.json setup_sg_list = [ f"{sg_name}.json" for sg_name in config_aws_setup['VPC']['SecurityGroups'] ] # Actual SG json files located in aws_setup/vpc_security_groups/ vpc_sg_json_files = os2.dir_files(sg_dir, ext=".json") # Halt if aws_setup.json describes SGs not found in sg_dir if not set(setup_sg_list).issubset(set(vpc_sg_json_files)): halt.err( "Following SG(s) not found from aws_setup/vpc_security_groups/:", *[sg for sg in setup_sg_list if sg not in vpc_sg_json_files]) # Halt if any security group missing Ingress key schema = os2.get_json_schema("vpc_security_groups") for sg_file in vpc_sg_json_files: sg_dict = os2.parse_json(sg_dir / sg_file) os2.validate_dict(sg_dict, schema, f"SG {sg_file}")
def main(self, cmd_args): """release elastic IP address (give up possession) Args: cmd_args (namedtuple): See add_documentation method. """ address = find_addresses.main(cmd_args.ip) ec2_client = aws.ec2_client(address['region']) if 'association_id' in address and cmd_args.force is False: halt.err(f"Elastic IP address {address['ip']} currently in use.", " Append the -f argument to force disassociation.") print("") if 'association_id' in address: ec2_client.disassociate_address( AssociationId=address['association_id']) ec2_client.release_address(AllocationId=address['allocation_id']) if 'association_id' in address: print(f"Elastic IP address {address['ip']} " "disassociated and released.") else: print(f"Elastic IP address {address['ip']} released.")
def _validate_region_whitelist(config_dict): """validate config's region whitelist and save to consts.REGIONS tuple Requires ec2:DescribeRegions permission. """ response = aws.ec2_client_no_validate("us-east-1").describe_regions() region_names = [region['RegionName'] for region in response['Regions']] if 'region_whitelist' in config_dict: whitelist = tuple(config_dict['region_whitelist']) if not set(whitelist).issubset(set(region_names)): halt.err("Following invalid region(s) in config whitelist:", *(set(whitelist) - set(region_names))) consts.REGIONS = sorted(whitelist) else: print("Searching for closest AWS EC2 region...") closest_region = find_closest_region.main(region_names) print(f" Region with lowest average latency is {closest_region}.") config_dict['region_whitelist'] = [closest_region] os2.save_json(config_dict, consts.CONFIG_JSON) print(f" {closest_region} configured as your region whitelist.") consts.REGIONS = (closest_region, )
def _validate_user(config_dict): """validate config's IAM user access key and minimal permissions iam:GetUser, iam:SimulatePrincipalPolicy, iam:GetAccessKeyLastUsed, and ec2:DescribeRegions permissions required for successful validation. Args: config_dict (dict): Should contain config's IAM user access key. 'access_key' (dict): IAM user's access key. Access key ID (str): Secret access key. """ consts.KEY_ID = next(iter(config_dict['access_key'])) consts.KEY_SECRET = config_dict['access_key'][consts.KEY_ID] # IAM User access key must be validated before validate_perms can be used. try: iam_user = aws.iam_client().get_user()['User'] except ClientError as e: # TODO: Use client exceptions instead once they're documented if e.response['Error']['Code'] == "InvalidClientTokenId": halt.err("Access key ID is invalid.") elif e.response['Error']['Code'] == "SignatureDoesNotMatch": halt.err("Access key ID is valid, but its secret is invalid.") elif e.response['Error']['Code'] == "AccessDenied": halt.assert_empty(["iam:GetUser"]) halt.err(str(e)) # This ARN is needed for iam:SimulatePrincipalPolicy action. consts.IAM_ARN = iam_user['Arn'] consts.IAM_NAME = iam_user['UserName'] # Validate IAM user can use iam:SimulatePrincipalPolicy action. try: validate_perms.blocked(actions=["iam:GetUser"]) except ClientError as e: if e.response['Error']['Code'] == "AccessDenied": halt.assert_empty(["iam:SimulatePrincipalPolicy"]) halt.err(str(e)) # Validate IAM user can use other basic permissions needed for the script halt.assert_empty( validate_perms.blocked( actions=["iam:GetAccessKeyLastUsed", "ec2:DescribeRegions"]))
def main(self, cmd_args): """SSH into an EC2 instance using its .pem/.ppk private key Attempts to open an interactive SSH session using either OpenSSH or PuTTY (OpenSSH is prioritized). A .pem/.ppk private key file is expected to exist within user's config. Instance's user@hostname is printed, for if an alternative SSH method is desired. Args: cmd_args (namedtuple): See find_instances:add_argparse_args """ instance = find_instances.main(cmd_args, single_instance=True) instance_state, instance_ip = find_instances.get_state_and_ip( instance['region'], instance['id']) if 'DefaultUser' not in instance['tags']: halt.err("Instance missing DefaultUser tag key-value pair.") if instance_state != "running": halt.err("Cannot SSH into an instance that isn't running.") user_and_hostname = f"{instance['tags']['DefaultUser']}@{instance_ip}" print("") print("Instance's user and hostname (seperated by \"@\"):") print(user_and_hostname) if shutil.which("ssh") is not None: self._open_openssh_session(user_and_hostname, consts.RSA_KEY_PEM) elif shutil.which("putty") is not None: self._open_putty_session(user_and_hostname, consts.RSA_KEY_PPK) else: if platform.system() == "Windows": halt.err("Neither OpenSSH for Windows nor PuTTY were found.", " Please install one and ensure it is in PATH.", " OpenSSH: https://www.mls-software.com/opensshd.html", " PuTTY: https://www.putty.org/") halt.err("Neither the OpenSSH client nor PuTTY were found.", " Please install one and ensure it is in PATH.")
def main(cmd_args, *, single_instance=False): """wrapper for probe_regions which prints found instances to the CLI Requires ec2:DescribeInstances permission. Halts if no instances found. This functionality is relied upon. Args: cmd_args (namedtuple): See argparse_args function. single_instance (bool): Halt if multiple instances are found. Returns: See what probe_regions returns. """ regions, tag_filter = _parse_filters(cmd_args) print("") print(f"Probing {len(regions)} AWS region(s) for instances...") all_instances = probe_regions(regions, tag_filter) if not all_instances: if (cmd_args.region_filter or cmd_args.tag_filters or cmd_args.name_filter or cmd_args.id_filter): halt.err("No namespace instances found.", " Remove specified filter(s) and try again.") halt.err("No namespace instances found.") for region in regions: region_instances = [ instance for instance in all_instances if instance['region'] == region ] if not region_instances: continue print(f"{region}: {len(region_instances)} instance(s) found:") for instance in region_instances: print(f" {instance['name']} ({instance['id']})") for tag_key, tag_value in instance['tags'].items(): print(f" {tag_key}: {tag_value}") if single_instance is True: if len(all_instances) > 1: halt.err("Instance query returned multiple results.", " Narrow filter(s) so that only one instance is found.") return all_instances[0] return all_instances
def _validate_ec2_key_pair(self): """validate EC2 key pair exists, and matches local RSA key file""" ec2_key_pairs = self._ec2_client.describe_key_pairs( Filters=[{ 'Name': "key-name", 'Values': [consts.NAMESPACE] }])['KeyPairs'] if not ec2_key_pairs: halt.err(f"EC2 key pair {consts.NAMESPACE} not found from AWS.", " Have you uploaded the AWS setup?") pem_path = consts.RSA_KEY_PEM.name if not consts.RSA_KEY_PEM.is_file(): halt.err(f"{pem_path} not found from config.") if pem.local_key_fingerprint() != ec2_key_pairs[0]['KeyFingerprint']: halt.err(f"Fingerprints of config's {pem_path} and EC2 key pair " "do not match.") return ec2_key_pairs[0]['KeyName'] # Should be same as namespace