Beispiel #1
0
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
                ])
Beispiel #2
0
    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.")
Beispiel #3
0
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)
Beispiel #4
0
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.")
Beispiel #5
0
    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).")
Beispiel #6
0
 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.")
Beispiel #7
0
    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.")
Beispiel #8
0
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
Beispiel #9
0
    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.")
Beispiel #10
0
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)
Beispiel #11
0
    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.")
Beispiel #12
0
    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
        ])
Beispiel #13
0
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)
Beispiel #14
0
    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.")
Beispiel #15
0
    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))
Beispiel #16
0
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.")
Beispiel #17
0
    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.")
Beispiel #18
0
    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).")
Beispiel #19
0
    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.")
Beispiel #20
0
    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.")
Beispiel #21
0
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
            ])
Beispiel #22
0
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()
Beispiel #23
0
    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.")
Beispiel #24
0
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}")
Beispiel #25
0
    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.")
Beispiel #26
0
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, )
Beispiel #27
0
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"]))
Beispiel #28
0
    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.")
Beispiel #29
0
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
Beispiel #30
0
    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