Esempio n. 1
0
 def test_get_autoscaling_group_properties_valid_tag_name(self):
     """Test method returns valid parameters file"""
     mock_asg_resource = Mock(name="Mock Autoscaling Client")
     mock_asg_resource.describe_auto_scaling_groups.return_value = \
     {
       "AutoScalingGroups": [
       ]
     }
     mock_asg_resource.describe_tags.return_value = \
     {
       "Tags": [
         {
           "ResourceType": "auto-scaling-group",
           "ResourceId": "alpha0-test-instance-ServerGroup",
           "PropagateAtLaunch": "true",
           "Value": "alpha0-test-instance",
           "Key": "Name"
         }
       ]
     }
     result = ef_utils.get_autoscaling_group_properties(
         mock_asg_resource, "alpha0", "test-instance")
     mock_asg_resource.describe_tags.assert_called_once_with(
         Filters=[{
             "Name": "Key",
             "Values": ["Name"]
         }, {
             "Name": "Value",
             "Values": ["alpha0-test-instance"]
         }])
     mock_asg_resource.describe_auto_scaling_groups.assert_called_with(
         AutoScalingGroupNames=["alpha0-test-instance-ServerGroup"])
Esempio n. 2
0
 def test_get_autoscaling_group_properties_valid_asg_name(self):
     """Test method returns valid parameters file"""
     mock_asg_resource = Mock(name="Mock Autoscaling Client")
     mock_asg_resource.describe_auto_scaling_groups.return_value = \
     {
       "AutoScalingGroups": [
         {
           "DesiredCapacity": 2,
           "Tags": [
             {
               "ResourceType": "auto-scaling-group",
               "ResourceId": "alpha0-test-instance-ServerGroup",
               "PropagateAtLaunch": "true",
               "Value": "alpha0-test-instance",
               "Key": "Name"
             }
           ],
           "AutoScalingGroupName": "alpha0-test-instance-ServerGroup"
         }
       ]
     }
     result = ef_utils.get_autoscaling_group_properties(
         mock_asg_resource, "alpha0", "test-instance")
     self.assertEquals(result[0]["DesiredCapacity"], 2)
     self.assertEquals(result[0]["AutoScalingGroupName"],
                       "alpha0-test-instance-ServerGroup")
     self.assertEquals(result[0]["Tags"][0]["ResourceId"],
                       "alpha0-test-instance-ServerGroup")
Esempio n. 3
0
def calculate_max_batch_size(asg_client, service, percent):
  autoscaling_group_properties = get_autoscaling_group_properties(asg_client, service.split("-")[0], "-".join(service.split("-")[1:]))
  if not autoscaling_group_properties:
      # safe default
      return 1
  current_desired = autoscaling_group_properties[0]["DesiredCapacity"]
  new_batch_size = int(math.ceil(current_desired * (percent * 0.01)))
  # max batch size must be a minimum of 1, otherwise cloudformation gives an error.
  return max(new_batch_size, 1)
Esempio n. 4
0
def calculate_max_batch_size(asg_client, service, percent):
    autoscaling_group_properties = get_autoscaling_group_properties(
        asg_client,
        service.split("-")[0], "-".join(service.split("-")[1:]))
    if not autoscaling_group_properties:
        # safe default
        return 1
    current_desired = autoscaling_group_properties[0]["DesiredCapacity"]
    new_batch_size = int(math.ceil(current_desired * (percent * 0.01)))
    return new_batch_size
Esempio n. 5
0
def main():
    context = handle_args_and_set_context(sys.argv[1:])

    if context.changeset:
        print(
            "=== CHANGESET ===\nCreating changeset only. See AWS GUI for changeset\n=== CHANGESET ==="
        )
    elif not context.commit:
        print(
            "=== DRY RUN ===\nValidation only. Use --commit to push template to CF\n=== DRY RUN ==="
        )

    service_name = os.path.basename(os.path.splitext(context.template_file)[0])
    template_file_dir = os.path.dirname(context.template_file)
    # parameter file may not exist, but compute the name it would have if it did
    parameter_file_dir = template_file_dir + "/../parameters"
    parameter_file = parameter_file_dir + "/" + service_name + ".parameters." + context.env_full + ".json"

    # If running in EC2, use instance credentials (i.e. profile = None)
    # otherwise, use local credentials with profile name in .aws/credentials == account alias name
    if context.whereami == "ec2" and not os.getenv("JENKINS_URL", False):
        profile = None
    else:
        profile = context.account_alias

    # Get service registry and refresh repo if appropriate
    try:
        if not (context.devel or os.getenv("JENKINS_URL", False)):
            pull_repo()
        else:
            print(
                "not refreshing repo because --devel was set or running on Jenkins"
            )
    except Exception as error:
        fail("Error: ", error)

    # Service must exist in service registry
    if context.service_registry.service_record(service_name) is None:
        fail("service: {} not found in service registry: {}".format(
            service_name, context.service_registry.filespec))

    if not context.env_full in context.service_registry.valid_envs(
            service_name):
        fail("Invalid environment: {} for service_name: {}\nValid environments are: {}" \
             .format(context.env_full, service_name, ", ".join(context.service_registry.valid_envs(service_name))))

    if context.percent and (context.percent <= 0 or context.percent > 100):
        fail(
            "Percent value cannot be less than or equal to 0 and greater than 100"
        )

    # Set the region found in the service_registry. Default is EFConfig.DEFAULT_REGION if region key not found
    region = context.service_registry.service_region(service_name)

    if context.verbose:
        print("service_name: {}".format(service_name))
        print("env: {}".format(context.env))
        print("env_full: {}".format(context.env_full))
        print("env_short: {}".format(context.env_short))
        print("region: {}".format(region))
        print("template_file: {}".format(context.template_file))
        print("parameter_file: {}".format(parameter_file))
        if profile:
            print("profile: {}".format(profile))
        print("whereami: {}".format(context.whereami))
        print("service type: {}".format(
            context.service_registry.service_record(service_name)["type"]))

    template = resolve_template(template=context.template_file,
                                profile=profile,
                                env=context.env,
                                region=region,
                                service=service_name,
                                verbose=context.verbose)

    # Create clients - if accessing by role, profile should be None
    try:
        clients = create_aws_clients(region, profile, "cloudformation",
                                     "autoscaling")
    except RuntimeError as error:
        fail(
            "Exception creating clients in region {} with profile {}".format(
                region, profile), error)

    stack_name = context.env + "-" + service_name
    try:
        stack_exists = clients["cloudformation"].describe_stacks(
            StackName=stack_name)
    except botocore.exceptions.ClientError:
        stack_exists = False

    # Load parameters from file
    if os.path.isfile(parameter_file):
        parameters_template = resolve_template(template=parameter_file,
                                               profile=profile,
                                               env=context.env,
                                               region=region,
                                               service=service_name,
                                               verbose=context.verbose)
        try:
            parameters = json.loads(parameters_template)
        except ValueError as error:
            fail("JSON error in parameter file: {}".format(
                parameter_file, error))
    else:
        parameters = []

    if context.percent:
        print("Modifying deploy rate to {}%".format(context.percent))
        modify_template = json.loads(template)
        for key in modify_template["Resources"]:
            if modify_template["Resources"][key][
                    "Type"] == "AWS::AutoScaling::AutoScalingGroup":
                if modify_template["Resources"][key]["UpdatePolicy"]:
                    autoscaling_group = modify_template["Resources"][key][
                        "Properties"]
                    service = autoscaling_group["Tags"][0]["Value"]
                    autoscaling_group_properties = get_autoscaling_group_properties(
                        clients["autoscaling"],
                        service.split("-")[0],
                        "-".join(service.split("-")[1:]))
                    new_max_batch_size = calculate_max_batch_size(
                        clients["autoscaling"], service, context.percent)
                    modify_template["Resources"][key]["UpdatePolicy"][
                        "AutoScalingRollingUpdate"][
                            "MaxBatchSize"] = new_max_batch_size
                    current_desired = autoscaling_group_properties[0][
                        "DesiredCapacity"] if autoscaling_group_properties else "missing"
                    print(
                        "Service {} [current desired: {}, calculated max batch size: {}]"
                        .format(service, current_desired, new_max_batch_size))
        template = json.dumps(modify_template)

    # Detect if the template exceeds the maximum size that is allowed by Cloudformation
    if len(template) > CLOUDFORMATION_SIZE_LIMIT:
        # Compress the generated template by removing whitespaces
        print(
            "Template exceeds the max allowed length that Cloudformation will accept. Compressing template..."
        )
        print("Uncompressed size of template: {}".format(len(template)))
        unpacked = json.loads(template)
        template = json.dumps(unpacked, separators=(",", ":"))
        print("Compressed size of template: {}".format(len(template)))

    # Validate rendered template before trying the stack operation
    if context.verbose:
        print("Validating template")
    try:
        clients["cloudformation"].validate_template(TemplateBody=template)
        json.loads(
            template)  # Tests for valid JSON syntax, oddly not handled above
    except botocore.exceptions.ClientError as error:
        fail("Template did not pass validation", error)
    except ValueError as e:  # includes simplejson.decoder.JSONDecodeError
        fail('Failed to decode JSON', e)

    print("Template passed validation")

    # DO IT
    try:
        if context.changeset:
            print("Creating changeset: {}".format(stack_name))
            results = clients["cloudformation"].create_change_set(
                StackName=stack_name,
                TemplateBody=template,
                Parameters=parameters,
                Capabilities=[
                    'CAPABILITY_AUTO_EXPAND', 'CAPABILITY_IAM',
                    'CAPABILITY_NAMED_IAM'
                ],
                ChangeSetName=stack_name,
                ClientToken=stack_name)
            if is_stack_termination_protected_env(context.env):
                enable_stack_termination_protection(clients, stack_name)
            results_ids = {
                key: value
                for key, value in results.iteritems()
                if key in ('Id', 'StackId')
            }
            print("Changeset Info: {}".format(json.dumps(results_ids)))
        elif context.commit:
            if stack_exists:
                print("Updating stack: {}".format(stack_name))
                clients["cloudformation"].update_stack(
                    StackName=stack_name,
                    TemplateBody=template,
                    Parameters=parameters,
                    Capabilities=[
                        'CAPABILITY_AUTO_EXPAND', 'CAPABILITY_IAM',
                        'CAPABILITY_NAMED_IAM'
                    ])
                if is_stack_termination_protected_env(context.env):
                    enable_stack_termination_protection(clients, stack_name)
            else:
                print("Creating stack: {}".format(stack_name))
                clients["cloudformation"].create_stack(
                    StackName=stack_name,
                    TemplateBody=template,
                    Parameters=parameters,
                    Capabilities=[
                        'CAPABILITY_AUTO_EXPAND', 'CAPABILITY_IAM',
                        'CAPABILITY_NAMED_IAM'
                    ])
                if is_stack_termination_protected_env(context.env):
                    enable_stack_termination_protection(clients, stack_name)
            if context.poll_status:
                while True:
                    stack_status = clients["cloudformation"].describe_stacks(
                        StackName=stack_name)["Stacks"][0]["StackStatus"]
                    if context.verbose:
                        print("{}".format(stack_status))
                    if stack_status.endswith('ROLLBACK_COMPLETE'):
                        print(
                            "Stack went into rollback with status: {}".format(
                                stack_status))
                        sys.exit(1)
                    elif re.match(r".*_COMPLETE(?!.)",
                                  stack_status) is not None:
                        break
                    elif re.match(r".*_FAILED(?!.)", stack_status) is not None:
                        print("Stack failed with status: {}".format(
                            stack_status))
                        sys.exit(1)
                    elif re.match(r".*_IN_PROGRESS(?!.)",
                                  stack_status) is not None:
                        time.sleep(EFConfig.EF_CF_POLL_PERIOD)
        elif context.lint:
            tester = CFTemplateLinter(template)
            tester.run_tests()
            exit(tester.exit_code)

    except botocore.exceptions.ClientError as error:
        if error.response["Error"][
                "Message"] in "No updates are to be performed.":
            # Don't fail when there is no update to the stack
            print("No updates are to be performed.")
        else:
            fail("Error occurred when creating or updating stack", error)