def check_tag(key_value_tuple):
    key, value = key_value_tuple
    if not _TAG_KEY_REGEX.match(key):
        raise UsageError("invalid tag key: " + key)
    if not _TAG_VALUE_REGEX.match(value):
        raise UsageError("invalid tag value: " + value)
    return (key, value)
def parse_tag_filter(tagspec):
    """Parses a tag filter specification in the format NAME=VALUE 
    into a dict with 'Name' and 'Values' entries."""
    if sum([1 if c == '=' else 0 for c in tagspec]) != 1:
        raise UsageError("tag spec must contain exactly one '=' character")
    name, value = tagspec.split('=')
    return {'Name': 'tag:' + name, 'Values': [value]}
def act_on_rule(security_groups, cidrip, port, action, verbose, dry_run,
                ignore_not_found):
    """Adds or removes a rule. Use action='authorize_ingress' to 
    add a rule; use action='revoke_ingress' to remove a rule."""
    if action not in ('authorize_ingress', 'revoke_ingress'):
        raise ValueError("invalid action: " + action)
    if '/' not in cidrip:
        raise UsageError(
            'CIDR does not specify subnet; use /32 to restrict to only the explicit IP address'
        )
    cidrip_obj = ipcalc.Network(cidrip)
    from_port, to_port = parse_port_range(port)
    _log.debug(
        "executing %s to %d security groups allowing TCP traffic on port(s) %d-%d from %d addresses (%s)",
        action, len(security_groups), from_port, to_port, cidrip_obj.size(),
        cidrip)
    num_successes = 0
    for secgroup in security_groups:
        try:
            fn = type(secgroup).__dict__[action]
            fn(secgroup,
               DryRun=dry_run,
               FromPort=from_port,
               ToPort=to_port,
               CidrIp=cidrip,
               IpProtocol='tcp')
            num_successes += 1
        except botocore.exceptions.ClientError as e:
            error_code = e.response['Error']['Code']
            if error_code == 'DryRunOperation':
                num_successes += 1
            elif error_code == 'InvalidPermission.NotFound' and not ignore_not_found:
                raise
            else:
                raise
        _log.debug("%s '%s' on rule of security group %s (%s)",
                   "dry-ran" if dry_run else "executed", action,
                   secgroup.group_name, secgroup.group_id)
    _log.debug("%d successful actions executed on %d security groups",
               num_successes, len(security_groups))
    return num_successes
def is_suspended(config, instance_id):
    """Check the configuration to see if the limits defined are 
    temporarily suspended."""
    suspensions = get_instance_criterion(config,
                                         instance_id,
                                         'suspensions',
                                         default_value=[])
    now = datetime.datetime.now()
    for bounds in suspensions:
        try:
            earliest = dateparser.parse(
                '1996-08-29T02:14:00-04:00' if bounds['from'] ==
                '*' else bounds['from'])
            latest = dateparser.parse('2038-01-19T03:14:07' if bounds['to'] ==
                                      '*' else bounds['to'])
        except KeyError:
            raise UsageError(
                "suspensions objects in config must have 'from' and 'to' fields"
            )
        if now >= earliest and now <= latest:
            return True
    return False
def main(argv):
    from argparse import ArgumentParser
    parser = ArgumentParser(description="""Creates security groups in 
multiple VPCs with common name and tags.""")
    myawscommon.add_log_level_option(parser)
    parser.add_argument("--verbose",
                        help="print more messages on stdout",
                        action='store_true',
                        default=False)
    myawscommon.add_credentials_options(parser)
    myawscommon.add_region_option(parser)
    parser.add_argument("sg_name",
                        metavar="NAME",
                        help="name for the security groups")
    parser.add_argument("vpcs",
                        nargs='+',
                        metavar="VPC_ID",
                        help="one or more VPC IDs")
    parser.add_argument("--dry-run",
                        action='store_true',
                        default=False,
                        help="execute in dry-run mode")
    parser.add_argument("--description",
                        metavar="TEXT",
                        help="specify description for security groups")
    parser.add_argument(
        "--tags",
        nargs='+',
        metavar="KEY=VALUE",
        default=(),
        help="one or more key=value pairs to add as tags to each group created"
    )
    args = parser.parse_args(argv[1:])
    myawscommon.configure_logging(_LOGGER_NAME, args.log_level)
    check_security_group_name(args.sg_name)
    tags = [check_tag(pair.split('=', 1)) for pair in args.tags]
    session = boto3.session.Session(
        aws_access_key_id=args.aws_access_key_id,
        aws_secret_access_key=args.aws_secret_access_key,
        profile_name=args.profile)
    regions = myawscommon.filter_regions(session, args.regions)
    vpcs_by_region = collections.defaultdict(list)
    region_by_vpc_id = {}
    vpc_filters = []  # [{'Name': 'isDefault', 'Values': ['False']}]
    for region in regions:
        _log.debug("gathering VPCs from region %s", region)
        ec2 = session.client('ec2', region_name=region)
        vpcs = ec2.describe_vpcs(Filters=vpc_filters)['Vpcs']
        vpcs = [vpc for vpc in vpcs if not vpc['IsDefault']
                ]  # the IsDefault query filter doesn't seem to work
        for vpc in vpcs:
            vpc_id = vpc['VpcId']
            region_by_vpc_id[vpc_id] = region
        if len(vpcs) > 0:
            vpcs_by_region[region] = vpcs
        if args.verbose:
            print("%d VPCs in %s%s%s" % (len(vpcs), region, ': ' if
                                         (len(vpcs) > 0) else '', '. '.join(
                                             [vpc['VpcId'] for vpc in vpcs])))
    _log.debug("%d VPCs across %d regions", len(region_by_vpc_id),
               len(vpcs_by_region))
    for vpc_id in args.vpcs:
        if vpc_id not in region_by_vpc_id:
            raise UsageError("vpc id not found in region scope: " + vpc_id)
    _log.debug("creating security group named '%s' in %d VPCs with %d tags",
               args.sg_name, len(args.vpcs), len(tags))
    client_cache = SessionProductCache(
        lambda region: session.client('ec2', region_name=region))
    resource_cache = SessionProductCache(
        lambda region: session.resource('ec2', region_name=region))
    for vpc_id in args.vpcs:
        region = region_by_vpc_id[vpc_id]
        ec2 = client_cache[region]
        ec2_resource = resource_cache[region]
        _log.debug("using client %s for region %s", ec2, region)
        try:
            group_id = ec2.create_security_group(
                DryRun=args.dry_run,
                GroupName=args.sg_name,
                Description=construct_description(args, vpc_id, region),
                VpcId=vpc_id)['GroupId']
        except botocore.exceptions.ClientError as e:
            if myawscommon.client_error_has_code(e, 'DryRunOperation'):
                group_id = "sg-%012x" % random.getrandbits(48)
            else:
                raise
        print(group_id,
              "%screated" % "(dry-run) " if args.dry_run else '',
              end="")
        if len(tags) > 0:
            security_group = ec2_resource.SecurityGroup(group_id)
            try:
                response = security_group.create_tags(DryRun=args.dry_run,
                                                      Tags=[{
                                                          'Key': key,
                                                          'Value': value
                                                      } for key, value in tags
                                                            ])
            except botocore.exceptions.ClientError as e:
                if myawscommon.client_error_has_code(e, 'DryRunOperation'):
                    response = [
                        ec2_resource.Tag(group_id, key, value)
                        for key, value in tags
                    ]
                else:
                    raise
            print('with', len(response), 'tag(s)', end="")
            _log.debug("created tags: %s", response)
        print()
    return 0
def check_security_group_name(sg_name):
    if len(sg_name) > _SG_DESC_LEN_MAX:
        raise UsageError(
            f"security group name is invalid; must be no more than {_SG_NAME_LEN_MAX} characters"
        )
    return sg_name
def main(argv):
    parser = ArgumentParser(description="""\
Perform operations on security groups. Print security groups, 
check that they have identical ingress rules, and add/remove
ingress rules.""")
    myawscommon.add_log_level_option(parser)
    myawscommon.add_credentials_options(parser)
    myawscommon.add_region_option(parser)
    parser.add_argument("--group-ids",
                        nargs="+",
                        help="filter target groups by group id",
                        metavar="ID")
    parser.add_argument("--group-names",
                        nargs="+",
                        help="filter target groups by group name",
                        metavar="NAME")
    parser.add_argument("--tag",
                        metavar="NAME=VALUE",
                        help="filter target groups by tag value")
    parser.add_argument("--verbose",
                        help="print more messages on stdout",
                        action='store_true',
                        default=False)
    parser.add_argument(
        "--check-in-use",
        action='store_true',
        default=False,
        help=
        "check whether security groups are in use (and mark those not in use with * in list)"
    )
    parser.add_argument(
        "--dry-run",
        action='store_true',
        default=False,
        help=
        "set DryRun flag to true for actions that would modify security groups"
    )
    parser.add_argument(
        "--check",
        nargs="?",
        metavar="TAGNAME",
        const='ALL',
        help=
        "checks that security group subsets contain the same set of ingress IP permission rules; "
        +
        "security groups can be partitioned into subsets based on the value of the tag specified"
    )
    parser.add_argument(
        "--add-rule",
        nargs=2,
        action='append',
        metavar="ARG",
        help=
        "for each security group, adds a rule (formatted as two args, PORT SPEC) allowing ingress "
        +
        "via TCP on port PORT from IP addresses within the range CIDR; e.g. --add-rule 8080 10.0.0.0/16"
    )
    parser.add_argument(
        "--remove-rule",
        nargs=2,
        action='append',
        metavar="ARG",
        help=
        "each security group, removes the rule (formatted as two args, PORT SPEC) that allows "
        +
        "ingress via TCP on port PORT from IP addresses within the range CIDR")
    parser.add_argument(
        "--ignore-rule-not-found",
        action="store_true",
        default=False,
        help=
        "with --remove-rule, ignore errors that are due to absence of the specified rule in any security group"
    )
    parser.add_argument(
        "--allow-action-on-all",
        action='store_true',
        default=False,
        help=
        "allows --add-rule or --remove-rule to operate on all security groups; by default, as "
        +
        "a precaution, it is assumed that the user erred in requesting a rule be added to every "
        + "security group; this option overrides that assumption")
    parser.add_argument(
        "--delete-unused",
        action="store_true",
        default=False,
        help=
        "delete security groups that are not in use by any instances (running or stopped)"
    )
    parser.add_argument(
        "--ignore-empty-target-list",
        action='store_true',
        default=False,
        help=
        "do not exit dirty when an action is specified but the target list is empty"
    )
    args = parser.parse_args(argv[1:])
    myawscommon.configure_logging(_LOGGER_NAME, args.log_level)
    session = boto3.session.Session(
        aws_access_key_id=args.aws_access_key_id,
        aws_secret_access_key=args.aws_secret_access_key,
        profile_name=args.profile)
    try:
        regions = myawscommon.filter_regions(session, args.regions)
        _log.debug(
            "regions filtered to %s according to user specification %s; tasks: %s",
            regions, args.regions, _get_task_argument_values(args))
        filters = []
        if args.tag:
            filters.append(parse_tag_filter(args.tag))
        security_groups = fetch_security_groups(
            session, regions, args.group_ids, args.group_names, filters,
            print_security_group
            if args.verbose or not _has_task_argument(args) else _NOOP,
            args.check_in_use, args.delete_unused, args.dry_run)
        if len(security_groups) == 0:
            _log.info(
                "target security group list is empty (%d regions searched)",
                len(regions))
            if _has_task_argument(args) and not args.ignore_empty_target_list:
                return ERR_USAGE
        if args.check is not None:
            _log.debug(
                "checking synchronization of security groups based on partition spec %s",
                args.check)
            secgroups_by_key = defaultdict(list)
            if args.check == 'ALL':
                secgroups_by_key['ALL'] += security_groups
            else:
                for secgroup in security_groups:
                    for tag in secgroup.tags or ():
                        if args.check == tag['Key']:
                            secgroups_by_key[tag['Value']].append(secgroup)
                if len(secgroups_by_key) == 0:
                    raise UsageError(
                        "the set of partitions created from tag '%s' is empty (no security groups have this tag)"
                        % args.check)
            not_in_sync = list()
            for sync_key in secgroups_by_key:
                expect_in_sync = secgroups_by_key[sync_key]
                _log.debug(
                    "expecting %d security groups (annotated as %s) to be synchronized",
                    len(expect_in_sync), sync_key)
                if not are_synchronized(expect_in_sync,
                                        verbose=args.verbose,
                                        annotation=sync_key):
                    not_in_sync.append(sync_key)
            if len(not_in_sync) > 0:
                _log.info("not in sync: %s", ','.join(not_in_sync))
                return ERR_NOT_SYNCHRONIZED
        for option in ((args.add_rule, 'authorize_ingress'),
                       (args.remove_rule, 'revoke_ingress')):
            option_value, action = option
            if option_value is not None:
                for unexpanded_rule_spec in option_value:
                    if not is_any_restriction_specified(args):
                        _log.error(
                            "denying request to add or remove rule to/from all security groups; "
                            +
                            "use --group-ids, --group-names, or --tag to restrict the security "
                            +
                            "groups list, or use --allow-action-on-all to override this check"
                        )
                        return ERR_UNRESTRICTED_RULE_APPLICATION_REQUESTED
                    rule_specs = expand_rule_spec(unexpanded_rule_spec)
                    for rule_spec in rule_specs:
                        port, cidrip = rule_spec[0], rule_spec[1]
                        act_on_rule(security_groups, cidrip, port, action,
                                    args.verbose, args.dry_run,
                                    args.ignore_rule_not_found)
                        if args.verbose:
                            print("dry-ran" if args.dry_run else "executed",
                                  action, port, cidrip, "on",
                                  len(security_groups), "security groups")
    except UsageError as e:
        print(e, file=sys.stderr)
        return ERR_USAGE
    return 0
def fetch_security_groups(session, regions, group_ids, group_names, filters,
                          foreach, check_in_use, delete_unused, dry_run):
    """Fetches a list of security groups, compiled from multiple 
    regions and filtered by group IDs and other optional filters."""
    if delete_unused and not check_in_use:
        raise UsageError("--delete-unused requires --check-in-use")
    if not callable(foreach):
        raise ValueError("'foreach' parameter must be a function")
    secgroups = []
    group_ids, group_names = group_ids or [], group_names or []
    for region in regions:
        ec2 = session.resource('ec2', region_name=region)
        try:
            secgroups_in_region = list(
                ec2.security_groups.filter(GroupIds=group_ids,
                                           GroupNames=group_names,
                                           Filters=filters))
        except botocore.exceptions.ClientError as e:
            error_code = e.response['Error']['Code']
            if error_code == 'InvalidGroup.NotFound':
                secgroups_in_region = []
            elif error_code == 'InvalidParameterValue' and 'You may not reference Amazon VPC security groups by name.' in str(
                    e):
                _log.info(
                    "adjusting fetch strategy for inconsistent server behavior ('%s' client error)",
                    error_code)
                secgroups_in_region = [
                    sg for sg in ec2.security_groups.filter(
                        GroupIds=group_ids, GroupNames=[], Filters=filters)
                    if sg.group_name in group_names
                ]
            else:
                raise
        groups_in_use = None
        if check_in_use:
            groups_in_use = fetch_security_groups_in_use(ec2)
        for sg in sorted(secgroups_in_region,
                         key=operator.attrgetter('vpc_id', 'group_name')):
            foreach(sg, region, groups_in_use)
        if delete_unused:
            not_deleted = []
            for sg in secgroups_in_region:
                if sg.group_name != 'default' and sg.group_id not in groups_in_use:
                    try:
                        sg.delete(DryRun=dry_run, GroupId=sg.group_id)
                    except botocore.exceptions.ClientError as e:
                        error_code = e.response['Error']['Code']
                        if error_code == 'DryRunOperation':
                            _log.debug("error_code=%s operation_name=%s",
                                       error_code, e.operation_name)
                        elif error_code == 'DependencyViolation':
                            _log.warning(
                                "failed to delete %s (%s) due to dependency violation",
                                sg.group_id, sg.group_name)
                        else:
                            raise
                else:
                    not_deleted.append(sg)
            _log.info("%sdeleted %d unused security groups (out of %d)",
                      "(dry run) " if dry_run else "",
                      len(secgroups_in_region) - len(not_deleted),
                      len(secgroups_in_region))
            secgroups_in_region = secgroups_in_region if dry_run else not_deleted
        _log.debug("fetched %d security group(s) in region %s",
                   len(secgroups_in_region), region)
        secgroups += secgroups_in_region
        if len(group_ids) > 0 and (len(secgroups) >= len(group_ids)):
            _log.debug(
                "already found %d security groups, and %d group IDs were specified; not searching any more regions",
                len(secgroups), len(group_ids))
            break
    return secgroups