def on_update(event, context):
    log.info("Handle resource update event %s" % json.dumps(event, indent=2))
    # get parameters passed in
    props = event["ResourceProperties"]
    nodes_per_zone = int(props["NodesPerZone"])
    zone_count = int(props["NumberOfZones"])
    parent_stack_name = props["ParentStackName"]
    parent_stack_id = props["ParentStackId"]
    subnets = props["Subnets"]
    security_group_id = props["SecurityGroup"]
    # old properties
    old_props = event["OldResourceProperties"]
    old_nodes_per_zone = int(old_props["NodesPerZone"])
    old_zone_count = int(old_props["NumberOfZones"])

    # validate diff and handle special case
    if old_zone_count != zone_count:
        reason = "Updating number of zones is not supported"
        log.error(reason)
        return cfn_failure_response(event, reason)
    if old_nodes_per_zone == nodes_per_zone:
        return cfn_success_response(event, reuse_physical_id=True)
    if old_nodes_per_zone > nodes_per_zone and nodes_per_zone != 0:
        reason = "Scaling down the number of nodes per zone by updating the stack is not recommended. " \
                 "Please manually remove unused network interface."
        log.warning(reason)
        return cfn_failure_response(event, reason)
    if nodes_per_zone == 0:
        log.info("Hibernating the cluster, retain network interfaces")
        return cfn_success_response(event, reuse_physical_id=True)

    # prepare ENI meta information
    id_hash = hashlib.md5(parent_stack_id.encode()).hexdigest()
    eni_tag_prefix = parent_stack_name + "-" + id_hash + "_"

    eni_idx = old_zone_count * old_nodes_per_zone
    for i in range(zone_count):
        for j in range(nodes_per_zone):
            if j < old_nodes_per_zone:
                continue
            tag = eni_tag_prefix + str(eni_idx)
            eni_idx += 1
            eni_id = create_eni(subnets[i], security_group_id, tag)
            if not eni_id:
                reason = "Failed to create network interface with tag %s" % tag
                log.warning(reason)
                continue

    return cfn_success_response(event, reuse_physical_id=True)
def on_create(event, context):
    log.info("Handle resource create event %s" % json.dumps(event, indent=2))
    # get parameters passed in
    props = event["ResourceProperties"]
    vpc = props["Vpc"]
    service_name = props["ServiceName"]
    security_group = props["SecurityGroup"]
    subnets = props["Subnets"]

    # create endpoint
    try:
        response = ec2_client.create_vpc_endpoint(
            VpcEndpointType='Interface',
            VpcId=vpc,
            ServiceName=service_name,
            SubnetIds=subnets,
            SecurityGroupIds=[security_group]
        )
    except ClientError as e:
        reason="Failed to create vpc endpoint for service %s" % service_name
        log.exception(reason)
        time.sleep(5) # sleep for 5 seconds to allow exception info being sent to CloudWatch
        return cfn_failure_response(event, reason)

    endpoint_id = response["VpcEndpoint"]["VpcEndpointId"]
    log.info(
        "Creating vpc endpoint %s for service %s" % (
            endpoint_id, service_name
        )
    )
    log.info("Waiting for vpc endpoint %s creation finish" % endpoint_id)
    endpoint_wait_for_creation(endpoint_id)

    return cfn_success_response(event)
def on_create(event, context):
    log.info("Handle resource create event %s" % json.dumps(event, indent=2))
    # get parameters passed in
    props = event["ResourceProperties"]
    nodes_per_zone = int(props["NodesPerZone"])
    zone_count = int(props["NumberOfZones"])
    parent_stack_name = props["ParentStackName"]
    parent_stack_id = props["ParentStackId"]
    subnets = props["Subnets"]
    security_group_id = props["SecurityGroup"]

    # prepare ENI meta information
    id_hash = hashlib.md5(parent_stack_id.encode()).hexdigest()
    eni_tag_prefix = parent_stack_name + "-" + id_hash + "_"

    dns = []
    # craete ENIs
    for i in range(0, zone_count):
        for j in range(0, nodes_per_zone):
            eni_idx = i * nodes_per_zone + j
            tag = eni_tag_prefix + str(eni_idx)
            eni_info = create_eni(subnets[i], security_group_id, tag)
            if not eni_info:
                reason = "Failed to create network interface with tag %s" % tag
                log.warning(reason)
                continue

            eni_id = eni_info["NetworkInterfaceId"]
            eni_dns = eni_info["PrivateDnsName"]
            eni_assign_tag(eni_id=eni_id, tag=tag)
            dns.append(eni_dns)

    return cfn_success_response(event, data={"Addresses": ",".join(dns)})
def on_delete(event, context):
    log.info("Handle resource delete event %s " % json.dumps(event, indent=2))
    # get parameters passed in
    props = event["ResourceProperties"]
    nodes_per_zone = int(props["NodesPerZone"])
    zone_count = int(props["NumberOfZones"])
    parent_stack_name = props["ParentStackName"]
    parent_stack_id = props["ParentStackId"]

    # prepare ENI meta information
    id_hash = hashlib.md5(parent_stack_id.encode()).hexdigest()
    eni_tag_prefix = parent_stack_name + "-" + id_hash + "_"

    # delete ENIs
    for i in range(0, zone_count):
        for j in range(0, nodes_per_zone):
            eni_idx = i * nodes_per_zone + j
            tag = eni_tag_prefix + str(eni_idx)
            log.info("Querying EC2 for ENI with tag %s" % tag)
            # query
            response = None
            try:
                response = ec2_client.describe_network_interfaces(
                    # TODO AWS SDK bug #1450
                    # Filters=[{
                    #     "Name": "tag:cluster-eni-id",
                    #     "Values": [tag]
                    # }]
                    Filters=[{
                        "Name": "description",
                        "Values": [tag]
                    }])
            except ClientError as e:
                reason = "Failed to describe network interface with tag %s" % tag
                log.exception(reason)
                time.sleep(5)
                continue

            for eni_info in response["NetworkInterfaces"]:
                eni_id = eni_info["NetworkInterfaceId"]
                log.info("Found network interface %s " % eni_id)

                # detach
                if "Attachment" in eni_info and (
                        eni_info["Attachment"]["Status"] == "attached"
                        or eni_info["Attachment"]["Status"] == "attaching"):
                    attachment_id = eni_info["Attachment"]["AttachmentId"]
                    if not detach_eni(eni_id, attachment_id):
                        reason = "Failed to detach network interface %s" % eni_id
                        log.error(reason)
                try:
                    ec2_client.delete_network_interface(
                        NetworkInterfaceId=eni_id)
                    log.info("Deleting network interface %s" % eni_id)
                except ClientError as e:
                    reason = "Failed to delete network interface %s" % eni_id
                    log.exception(reason)

    return cfn_success_response(event)
def on_delete(event, context):
    log.info("Handle resource delete event %s" % json.dumps(event, indent=2))
    # get parameters passed in
    props = event["ResourceProperties"]
    service_name = props["ServiceName"]
    vpc = props["Vpc"]

    # find endpoint
    response = None
    try:
        response = ec2_client.describe_vpc_endpoints(
            Filters=[
                {
                    "Name": "service-name",
                    "Values": [service_name]
                },
                {
                    "Name": "vpc-id",
                    "Values": [vpc]
                }
            ]
        )
    except ClientError as e:
        reason = "Failed to describe vpc endpoint for service %s" % service_name
        log.exception(reason)
        time.sleep(5) # sleep for 5 seconds to allow exception info being sent to CloudWatch
        return cfn_failure_response(event, reason)

    if len(response["VpcEndpoints"]) <= 0:
        log.info("No endpoint found for service %s" % service_name)
    else:
        endpoint_id = response["VpcEndpoints"][0]["VpcEndpointId"]
        try:
            response = ec2_client.delete_vpc_endpoints(
                VpcEndpointIds=[endpoint_id]
            )
            log.info("Deleting endpoint %s" % endpoint_id)
        except ClientError as e:
            reason = "Failed to delete vpc endpoint %s" % endpoint_id
            log.exception(reason)
            time.sleep(5)
            return cfn_failure_response(event, reason)

        if "Unsuccessful" in response and len(response["Unsuccessful"]) > 0:
            reason = "Failed to delete vpc endpoint %s: %s" % (
                endpoint_id,
                response["Unsuccessful"][0]["Error"]["Message"]
            )
            log.error(reason)
            time.sleep(5)
            return cfn_failure_response(event, reason)

    return cfn_success_response(event)