Example #1
0
 def _validator_execute(self, validator_class, validator_args, suppressors):
     validator = validator_class()
     if any(
             suppressor.suppress_validator(validator)
             for suppressor in (suppressors or [])):
         LOGGER.debug("Suppressing validator %s", validator_class.__name__)
         return []
     LOGGER.debug("Executing validator %s", validator_class.__name__)
     try:
         return validator.execute(**validator_args)
     except Exception as e:
         return [
             ValidationResult(str(e), FailureLevel.ERROR, validator.type)
         ]
Example #2
0
 def _validate_config(self, validator_suppressors, validation_failure_level):
     """Validate the configuration, throwing an exception for failures above a given failure level."""
     try:
         validation_failures = self.config.validate(validator_suppressors)
     except ValidationError as e:
         # syntactic failure
         data = str(sorted(e.messages.items()) if isinstance(e.messages, dict) else e)
         validation_failures = [ValidationResult(data, FailureLevel.ERROR, validator_type="ImageSchemaValidator")]
         raise ConfigValidationError("Invalid image configuration.", validation_failures=validation_failures)
     for failure in validation_failures:
         if failure.level.value >= FailureLevel(validation_failure_level).value:
             raise BadRequestImageBuilderActionError(
                 message="Configuration is invalid", validation_failures=validation_failures
             )
     return validation_failures
Example #3
0
 def _load_config(self, cluster_config: dict) -> BaseClusterConfig:
     """Load the config and catch / translate any errors that occur during loading."""
     try:
         return ClusterSchema(cluster_name=self.name).load(cluster_config)
     except ValidationError as e:
         # syntactic failure
         data = str(
             sorted(e.messages.items()) if isinstance(e.messages, dict
                                                      ) else e)
         validation_failures = [
             ValidationResult(data,
                              FailureLevel.ERROR,
                              validator_type="ConfigSchemaValidator")
         ]
         raise ConfigValidationError(
             "Invalid cluster configuration.",
             validation_failures=validation_failures)
Example #4
0
    def validate(
        self,
        validator_suppressor_list: List[ValidatorSuppressor] = None
    ) -> List[ValidationResult]:
        """Execute registered validators."""
        # Cleanup failures and validators
        self._validation_failures.clear()

        # Call validators for nested resources
        for attr, value in self.__dict__.items():
            if isinstance(value, Resource):
                # Validate nested Resources
                self._validation_failures.extend(
                    value.validate(validator_suppressor_list))
            if isinstance(value, list) and value:
                # Validate nested lists of Resources
                for item in self.__getattribute__(attr):
                    if isinstance(item, Resource):
                        self._validation_failures.extend(
                            item.validate(validator_suppressor_list))

        # Update validators to be executed according to current status of the model and order by priority
        self._validators.clear()
        self._register_validators()
        for validator_class, validator_args in self._validators:
            validator = validator_class()
            if validator_suppressor_list and any(
                    suppressor.suppress_validator(validator)
                    for suppressor in validator_suppressor_list):
                LOGGER.debug("Suppressing validator %s",
                             validator_class.__name__)
                continue
            LOGGER.debug("Executing validator %s", validator_class.__name__)
            try:
                validation_failures = validator.execute(**validator_args)
                self._validation_failures.extend(validation_failures)
            except Exception as e:
                self._validation_failures.append(
                    ValidationResult(str(e), FailureLevel.ERROR,
                                     validator.type))
        return self._validation_failures
class TestBuildImage:
    url = "/v3/images/custom"
    method = "POST"
    config = (
        "Build:\n  InstanceType: c5.xlarge\n  ParentImage: arn:aws:imagebuilder:us-east-1:aws:image/amazon-"
        "linux-2-x86/x.x.x"
        "\n\nDevSettings:\n  Cookbook:\n    ChefCookbook: https://github.com/aws/aws-par"
        "allelcluster-cookbook/tarball/26ab8423b84de1a098bc26e8ff1768e930fc7707\n  NodePackage: https://git"
        "hub.com/aws/aws-parallelcluster-node/tarball/875ef93986a86ea3267835a813d38eaa05e575f3\n  AwsBatchC"
        "liPackage: https://github.com/aws/aws-parallelcluster/tarball/d5c2a1ec267a865cff3cf350af30d44e68f0"
        "ef18")

    def _send_test_request(self,
                           client,
                           dryrun=None,
                           suppress_validators=None,
                           rollback_on_failure=None):
        build_image_request_content = {
            "imageConfiguration": self.config,
            "imageId": "imageid"
        }
        query_string = [("validationFailureLevel", ValidationLevel.INFO),
                        ("region", "eu-west-1")]

        if dryrun is not None:
            query_string.append(("dryrun", dryrun))

        if rollback_on_failure is not None:
            query_string.append(("rollbackOnFailure", rollback_on_failure))

        if suppress_validators:
            query_string.extend([("suppressValidators", validator)
                                 for validator in suppress_validators])

        headers = {
            "Accept": "application/json",
            "Content-Type": "application/json"
        }
        return client.open(
            self.url,
            method=self.method,
            data=json.dumps(build_image_request_content),
            headers=headers,
            query_string=query_string,
            content_type="application/json",
        )

    @pytest.mark.parametrize(
        "suppress_validators, suppressed_validation_errors, rollback_on_failure",
        [
            (None, None, None),
            (["type:type1", "type:type2"], [
                ValidationResult("suppressed failure", FailureLevel.INFO,
                                 "type1")
            ], None),
            (None, None, False),
        ],
        ids=[
            "test with no validation errors",
            "test with suppressed validators", "rollback on failure"
        ],
    )
    def test_build_image_success(self, client, mocker, suppress_validators,
                                 suppressed_validation_errors,
                                 rollback_on_failure):
        mocked_call = mocker.patch(
            "pcluster.models.imagebuilder.ImageBuilder.create",
            auto_spec=True,
            return_value=suppressed_validation_errors,
        )
        mocker.patch(
            "pcluster.aws.cfn.CfnClient.describe_stack",
            return_value=_create_stack(
                "image1", CloudFormationStackStatus.CREATE_IN_PROGRESS),
        )
        mocker.patch("pcluster.aws.cfn.CfnClient.describe_stack_resource",
                     return_value=None)

        expected_response = {
            "image": {
                "cloudformationStackArn": "arn:image1",
                "cloudformationStackStatus": "CREATE_IN_PROGRESS",
                "imageBuildStatus": "BUILD_IN_PROGRESS",
                "imageId": "image1",
                "region": "eu-west-1",
                "version": "3.0.0",
            }
        }

        if suppressed_validation_errors:
            expected_response["validationMessages"] = [{
                "level":
                "INFO",
                "type":
                "type1",
                "message":
                "suppressed failure"
            }]

        response = self._send_test_request(
            client,
            dryrun=False,
            suppress_validators=suppress_validators,
            rollback_on_failure=rollback_on_failure)

        with soft_assertions():
            assert_that(response.status_code).is_equal_to(202)
            assert_that(response.get_json()).is_equal_to(expected_response)

        mocked_call.assert_called_with(
            disable_rollback=not rollback_on_failure
            if rollback_on_failure is not None else True,
            validator_suppressors=mocker.ANY,
            validation_failure_level=FailureLevel[ValidationLevel.INFO],
        )
        mocked_call.assert_called_once()
        if suppress_validators:
            _, kwargs = mocked_call.call_args
            assert_that(kwargs["validator_suppressors"].pop(
            )._validators_to_suppress).is_equal_to({"type1", "type2"})

        mocked_call.assert_called_once()

    @pytest.mark.parametrize(
        "validation_errors, error_code, expected_response",
        [
            pytest.param(
                None,
                412, {
                    "message":
                    "Request would have succeeded, but DryRun flag is set."
                },
                id="test success"),
            pytest.param(
                BadRequestImageBuilderActionError("test validation error", [
                    ValidationResult("test failure", FailureLevel.ERROR,
                                     "dummy validator")
                ]),
                400,
                {
                    "configurationValidationErrors": [{
                        "level": "ERROR",
                        "type": "dummy validator",
                        "message": "test failure"
                    }],
                    "message":
                    "test validation error",
                },
                id="test validation failure",
            ),
            pytest.param(ConflictImageBuilderActionError("test error"),
                         409, {"message": "test error"},
                         id="test conflict error"),
        ],
    )
    def test_dryrun(self, client, mocker, validation_errors, error_code,
                    expected_response):
        if validation_errors:
            mocker.patch(
                "pcluster.models.imagebuilder.ImageBuilder.validate_create_request",
                side_effect=validation_errors)
        else:
            mocker.patch(
                "pcluster.models.imagebuilder.ImageBuilder.validate_create_request",
                return_value=None)

        response = self._send_test_request(client, dryrun=True)

        with soft_assertions():
            assert_that(response.status_code).is_equal_to(error_code)
            assert_that(response.get_json()).is_equal_to(expected_response)

    @pytest.mark.parametrize(
        "error, error_code",
        [
            (LimitExceededImageError("test error"), 429),
            (LimitExceededStackError("test error"), 429),
            (LimitExceededImageBuilderActionError("test error"), 429),
            (BadRequestImageError("test error"), 400),
            (BadRequestStackError("test error"), 400),
            (BadRequestImageBuilderActionError("test error", []), 400),
            (
                BadRequestImageBuilderActionError("test error", [
                    ValidationResult("message", FailureLevel.WARNING, "type")
                ]),
                400,
            ),
            (ConflictImageBuilderActionError("test error"), 409),
        ],
    )
    def test_that_errors_are_converted(self, client, mocker, error,
                                       error_code):
        mocker.patch("pcluster.models.imagebuilder.ImageBuilder.create",
                     side_effect=error)
        expected_error = {"message": "test error"}

        if isinstance(error, (BadRequestImageError, BadRequestStackError)):
            expected_error[
                "message"] = "Bad Request: " + expected_error["message"]

        if isinstance(error, BadRequestImageBuilderActionError
                      ) and error.validation_failures:
            expected_error["configurationValidationErrors"] = [{
                "level": "WARNING",
                "message": "message",
                "type": "type"
            }]

        response = self._send_test_request(client, dryrun=False)

        with soft_assertions():
            assert_that(response.status_code).is_equal_to(error_code)
            assert_that(response.get_json()).is_equal_to(expected_error)

    def test_parse_config_error(self, client, mocker):
        mocker.patch("pcluster.aws.ec2.Ec2Client.image_exists",
                     return_value=False)
        mocker.patch("pcluster.aws.cfn.CfnClient.stack_exists",
                     return_value=False)
        mocker.patch("marshmallow.Schema.load",
                     side_effect=ValidationError(message={"Error": "error"}))
        response = self._send_test_request(client)
        assert_that(response.status_code).is_equal_to(400)
        assert_that(response.get_json()["message"]).matches(
            "Invalid image configuration.")