Example #1
0
def test_cfn_main() -> None:
    class MyCFNMain(CFNMain):
        def create_stack(self) -> Stack:
            return Stack(name="teststack")

    aws_env = AWSEnv(regions=["us-east-1"], stub=True)
    with default_region("us-east-1"):
        aws_env.client("cloudformation", region="us-east-1")

        stubber = aws_env.stub("cloudformation")
        stubber.add_response("validate_template", {}, {"TemplateBody": ANY})
        stubber.add_response(
            "create_stack",
            {},
            {
                "Capabilities": ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
                "StackName": "teststack",
                "ClientRequestToken": ANY,
                "TemplateBody": ANY,
            },
        )
        stubber.add_response("describe_stacks", {}, {"StackName": "teststack"})
        with stubber:
            m = MyCFNMain(regions=["us-east-1"])
            m.execute(args=["push", "--no-wait"], aws_env=aws_env)
Example #2
0
def test_cfn_main_multiple_stacks():
    class MyCFNMain(CFNMain):
        def create_stack(self):
            return [Stack(name="first-stack"), Stack(name="second-stack")]

    aws_env = AWSEnv(regions=["us-east-1"], stub=True)
    with default_region("us-east-1"):
        aws_env.client("cloudformation", region="us-east-1")

        stubber = aws_env.stub("cloudformation")
        for stack_name in ("first-stack", "second-stack"):
            stubber.add_response("validate_template", {},
                                 {"TemplateBody": ANY})
            stubber.add_response(
                "create_stack",
                {},
                {
                    "Capabilities": ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
                    "StackName": f"{stack_name}",
                    "TemplateBody": ANY,
                },
            )
            stubber.add_response("describe_stacks", {},
                                 {"StackName": f"{stack_name}"})
        with stubber:
            m = MyCFNMain(regions=["us-east-1"])
            m.execute(args=["push", "--no-wait"], aws_env=aws_env)
Example #3
0
def test_cfn_main_s3():
    class MyCFNMain(CFNMain):
        def create_stack(self):
            return Stack(name='teststack')

    os.mkdir('data')
    aws_env = AWSEnv(regions=['us-east-1'], stub=True)
    with default_region('us-east-1'):
        aws_env.client('cloudformation', region='us-east-1')

        stubber = aws_env.stub('cloudformation')
        stubber.add_response('validate_template', {}, {'TemplateURL': ANY})
        stubber.add_response('create_stack', {}, {
            'Capabilities': ['CAPABILITY_IAM'],
            'StackName': 'teststack',
            'TemplateURL': ANY
        })
        stubber.add_response('describe_stacks', {}, {'StackName': 'teststack'})

        aws_env.client('s3', region='us-east-1')
        s3_stubber = aws_env.stub('s3')
        s3_stubber.add_response(
            'put_object', DEFAULT_S3_ANSWER, {
                'Bucket': 'superbucket',
                'Body': ANY,
                'Key': ANY,
                'ServerSideEncryption': 'AES256'
            })
        with stubber:
            with s3_stubber:
                m = MyCFNMain(regions=['us-east-1'],
                              data_dir='data',
                              s3_bucket='superbucket',
                              s3_key='test_key')
                m.execute(args=['push'], aws_env=aws_env)
Example #4
0
def test_create_change_set():
    s = Stack(name="teststack")

    aws_env = AWSEnv(regions=["us-east-1"])
    with default_region("us-east-1"):
        cfn_client = aws_env.client("cloudformation", region="us-east-1")

        stubber = Stubber(cfn_client)
        stubber.add_response(
            "create_change_set",
            {},
            {
                "Capabilities": ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
                "StackName": "teststack",
                "ChangeSetName": "name1",
                "TemplateBody": ANY,
            },
        )
        stubber.add_response(
            "create_change_set",
            {},
            {
                "Capabilities": ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
                "ChangeSetName": "name2",
                "StackName": "teststack",
                "TemplateURL": ANY,
            },
        )
        with stubber:
            s.create_change_set("name1")
            s.create_change_set("name2", url="noprotocol://nothing")
Example #5
0
def test_create_change_set():
    s = Stack(name='teststack')

    aws_env = AWSEnv(regions=['us-east-1'])
    with default_region('us-east-1'):
        cfn_client = aws_env.client('cloudformation', region='us-east-1')

        stubber = Stubber(cfn_client)
        stubber.add_response(
            'create_change_set', {}, {
                'Capabilities': ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                'StackName': 'teststack',
                'ChangeSetName': 'name1',
                'TemplateBody': ANY
            })
        stubber.add_response(
            'create_change_set', {}, {
                'Capabilities': ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                'ChangeSetName': 'name2',
                'StackName': 'teststack',
                'TemplateURL': ANY
            })
        with stubber:
            s.create_change_set('name1')
            s.create_change_set('name2', url='noprotocol://nothing')
Example #6
0
def test_cfn_main_s3() -> None:
    class MyCFNMain(CFNMain):
        def create_stack(self) -> Stack:
            return Stack(name="teststack")

    os.mkdir("data")
    aws_env = AWSEnv(regions=["us-east-1"], stub=True)
    with default_region("us-east-1"):
        aws_env.client("cloudformation", region="us-east-1")

        stubber = aws_env.stub("cloudformation")
        stubber.add_response("validate_template", {}, {"TemplateURL": ANY})
        stubber.add_response(
            "create_stack",
            {},
            {
                "Capabilities": ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
                "StackName": "teststack",
                "ClientRequestToken": ANY,
                "TemplateURL": ANY,
            },
        )
        stubber.add_response("describe_stacks", {}, {"StackName": "teststack"})

        aws_env.client("s3", region="us-east-1")
        s3_stubber = aws_env.stub("s3")
        s3_stubber.add_response(
            "put_object",
            DEFAULT_S3_ANSWER,
            {
                "Bucket": "superbucket",
                "Body": ANY,
                "Key": ANY,
                "ServerSideEncryption": "AES256",
            },
        )
        with stubber:
            with s3_stubber:
                m = MyCFNMain(
                    regions=["us-east-1"],
                    data_dir="data",
                    s3_bucket="superbucket",
                    s3_key="test_key",
                )
                m.execute(args=["push", "--no-wait"], aws_env=aws_env)
Example #7
0
def test_cfn_main():
    class MyCFNMain(CFNMain):
        def create_stack(self):
            return Stack(name='teststack')

    aws_env = AWSEnv(regions=['us-east-1'], stub=True)
    with default_region('us-east-1'):
        aws_env.client('cloudformation', region='us-east-1')

        stubber = aws_env.stub('cloudformation')
        stubber.add_response('validate_template', {}, {'TemplateBody': ANY})
        stubber.add_response('create_stack', {}, {
            'Capabilities': ['CAPABILITY_IAM'],
            'StackName': 'teststack',
            'TemplateBody': ANY
        })
        stubber.add_response('describe_stacks', {}, {'StackName': 'teststack'})
        with stubber:
            m = MyCFNMain(regions=['us-east-1'])
            m.execute(args=['push'], aws_env=aws_env)
Example #8
0
def test_validate():
    s = Stack(name='teststack')

    aws_env = AWSEnv(regions=['us-east-1'])
    with default_region('us-east-1'):
        cfn_client = aws_env.client('cloudformation', region='us-east-1')

        stubber = Stubber(cfn_client)
        stubber.add_response('validate_template', {}, {'TemplateBody': ANY})
        stubber.add_response('validate_template', {}, {'TemplateURL': ANY})
        with stubber:
            s.validate()
            s.validate(url='noprotocol://nothing')
Example #9
0
def test_validate():
    s = Stack(name="teststack")

    aws_env = AWSEnv(regions=["us-east-1"])
    with default_region("us-east-1"):
        cfn_client = aws_env.client("cloudformation", region="us-east-1")

        stubber = Stubber(cfn_client)
        stubber.add_response("validate_template", {}, {"TemplateBody": ANY})
        stubber.add_response("validate_template", {}, {"TemplateURL": ANY})
        with stubber:
            s.validate()
            s.validate(url="noprotocol://nothing")
Example #10
0
def test_create_stack():
    s = Stack(name='teststack')

    aws_env = AWSEnv(regions=['us-east-1'])
    with default_region('us-east-1'):
        cfn_client = aws_env.client('cloudformation', region='us-east-1')

        stubber = Stubber(cfn_client)
        stubber.add_response('create_stack', {}, {
            'Capabilities': ['CAPABILITY_IAM'],
            'StackName': 'teststack',
            'TemplateBody': ANY
        })
        with stubber:
            s.create()
Example #11
0
class CFNMain(Main, metaclass=abc.ABCMeta):
    """Main to handle CloudFormation stack from command line."""
    def __init__(self,
                 regions,
                 force_profile=None,
                 data_dir=None,
                 s3_bucket=None,
                 s3_key=''):
        """Initialize main.

        :param regions: list of regions on which we can operate
        :type regions: list[str]
        :param force_profile: if None then add ability to select a credential
            profile to use. Otherwise profile is set
        :type force_profile: str | None
        :param data_dir: directory containing files used by cfn-init
        :type data_dir: str | None
        :param s3_bucket: if defined S3 will be used as a proxy for resources.
            Template body will be uploaded to S3 before calling operation on
            it. This change the body limit from 50Ko to 500Ko. Additionally if
            data_dir is defined, the directory will be uploaded to the
            specified S3 bucket.
        :param s3_key: if s3_bucket is defined, then all uploaded resources
            will be stored under a subkey of s3_key. If not defined the root
            of the bucket is used.
        :type: str
        """
        super(CFNMain, self).__init__(platform_args=False)
        self.argument_parser.add_argument('--profile',
                                          help='choose AWS profile',
                                          default='default')

        if len(regions) > 1:
            self.argument_parser.add_argument(
                '--region',
                help='choose region (default: %s)' % regions[0],
                default=regions[0])
        else:
            self.argument_parser.set_defaults(region=regions[0])

        subs = self.argument_parser.add_subparsers(
            title='commands', description='available commands')

        create_args = subs.add_parser('push', help='push a stack')
        create_args.add_argument(
            '--wait',
            action='store_true',
            default=False,
            help='if used then wait for stack creation completion')
        create_args.set_defaults(command='push')

        self.regions = regions

        self.data_dir = data_dir
        self.s3_bucket = s3_bucket
        self.s3_data_key = None
        self.s3_data_url = None
        self.s3_template_key = None
        self.s3_template_url = None

        self.timestamp = str(int(time.time()))

        if s3_bucket is not None:
            s3_root_key = '/'.join([s3_key.rstrip('/'), self.timestamp
                                    ]).strip('/') + '/'
            self.s3_data_key = s3_root_key + 'data/'
            self.s3_data_url = 'https://%s.s3.amazonaws.com/%s' % \
                (self.s3_bucket, self.s3_data_key)
            self.s3_template_key = s3_root_key + 'template'
            self.s3_template_url = 'https://%s.s3.amazonaws.com/%s' % \
                (self.s3_bucket, self.s3_template_key)

    def execute(self, args=None, known_args_only=False, aws_env=None):
        """Execute application and return exit status.

        See parse_args arguments.
        """
        super(CFNMain, self).parse_args(args, known_args_only)
        if aws_env is not None:
            self.aws_env = aws_env
        else:
            self.aws_env = AWSEnv(regions=self.regions,
                                  profile=self.args.profile)
            self.aws_env.default_region = self.args.region

        try:
            if self.args.command == 'push':
                if self.data_dir is not None and self.s3_data_key is not None:
                    s3 = self.aws_env.client('s3')

                    # synchronize data to the bucket before creating the stack
                    for f in find(self.data_dir):
                        with open(f, 'rb') as fd:
                            subkey = os.path.relpath(f, self.data_dir).replace(
                                '\\', '/')
                            logging.info('Upload %s to %s:%s%s', subkey,
                                         self.s3_bucket, self.s3_data_key,
                                         subkey)
                            s3.put_object(Bucket=self.s3_bucket,
                                          Body=fd,
                                          ServerSideEncryption='AES256',
                                          Key=self.s3_data_key + subkey)

                s = self.create_stack()

                if self.s3_template_key is not None:
                    logging.info('Upload template to %s:%s', self.s3_bucket,
                                 self.s3_template_key)
                    s3.put_object(Bucket=self.s3_bucket,
                                  Body=s.body.encode('utf-8'),
                                  ServerSideEncryption='AES256',
                                  Key=self.s3_template_key)

                logging.info('Validate template for stack %s' % s.name)
                s.validate(url=self.s3_template_url)

                if s.exists():
                    changeset_name = 'changeset%s' % int(time.time())
                    logging.info('Push changeset: %s' % changeset_name)
                    s.create_change_set(changeset_name,
                                        url=self.s3_template_url)
                    result = s.describe_change_set(changeset_name)
                    while result['Status'] in ('CREATE_PENDING',
                                               'CREATE_IN_PROGRESS'):
                        time.sleep(1.0)
                        result = s.describe_change_set(changeset_name)

                    if result['Status'] == 'FAILED':
                        logging.error(result['StatusReason'])
                        s.delete_change_set(changeset_name)
                        return 1
                    else:
                        print(yaml.safe_dump(result['Changes']))
                        return 0
                else:
                    logging.info('Create new stack')
                    s.create(url=self.s3_template_url)
                    state = s.state()
                    if self.args.wait:
                        while 'PROGRESS' in state['Stacks'][0]['StackStatus']:
                            result = s.resource_status(in_progress_only=False)
                            print(result)
                            time.sleep(10.0)
                            state = s.state()
            else:
                return 1
            return 0
        except botocore.exceptions.ClientError as e:
            logging.error(str(e))
            return 1

    @abc.abstractmethod
    def create_stack(self):
        """Create a stack.
Example #12
0
def test_cfn_main_push_existing_stack(status: Tuple[str, str, int],
                                      monkeypatch: MonkeyPatch) -> None:
    """Test pushing an already existing stack.

    :param status: Tuple with status and status reason from describe_change_set
        response and associated expected execute return value.
    """
    class MyCFNMain(CFNMain):
        def create_stack(self):
            return [Stack(name="existing-stack")]

    aws_env = AWSEnv(regions=["us-east-1"], stub=True)

    with default_region("us-east-1"):
        aws_env.client("cloudformation", region="us-east-1")

        stack_name = "existing-stack"
        stubber = aws_env.stub("cloudformation")
        stubber.add_response("validate_template", {}, {"TemplateBody": ANY})
        stubber.add_response(
            "describe_stacks",
            service_response={
                "Stacks": [{
                    "StackName": stack_name,
                    "CreationTime": datetime(2016, 1, 20, 22, 9),
                    "StackStatus": "CREATE_COMPLETE",
                    "StackId": stack_name + "1",
                }]
            },
            expected_params={"StackName": stack_name},
        )
        stubber.add_response(
            "create_change_set",
            {},
            {
                "Capabilities": ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
                "ChangeSetName": ANY,
                "StackName": stack_name,
                "TemplateBody": ANY,
            },
        )
        stubber = stubber
        stubber.add_response(
            "describe_change_set",
            {
                "StackName": stack_name,
                "Status": status[0],
                "StatusReason": status[1],
                "Changes": [],
            },
            {
                "ChangeSetName": ANY,
                "StackName": stack_name
            },
        )

        if status[0] == "FAILED":
            stubber.add_response("delete_change_set", {}, {
                "ChangeSetName": ANY,
                "StackName": stack_name
            })
        else:
            stubber.add_response(
                "execute_change_set",
                {},
                {
                    "ChangeSetName": ANY,
                    "StackName": ANY,
                    "ClientRequestToken": ANY
                },
            )
            stubber.add_response(
                "describe_stacks",
                service_response={
                    "Stacks": [{
                        "StackName": stack_name,
                        "CreationTime": datetime(2016, 1, 20, 22, 9),
                        "StackStatus": "UPDATE_COMPLETE",
                        "StackId": stack_name + "1",
                    }]
                },
                expected_params={"StackName": ANY},
            )
            monkeypatch.setattr("builtins.input", lambda _: "Y")

        with stubber:
            m = MyCFNMain(regions=["us-east-1"])
            assert m.execute(args=["update", "--no-wait"],
                             aws_env=aws_env) == status[2]
Example #13
0
class CFNMain(Main, metaclass=abc.ABCMeta):
    """Main to handle CloudFormation stack from command line."""
    def __init__(
        self,
        regions,
        default_profile="default",
        data_dir=None,
        s3_bucket=None,
        s3_key="",
        assume_role=None,
    ):
        """Initialize main.

        :param regions: list of regions on which we can operate
        :type regions: list[str]
        :param default_profile: default AWS profile to use to create the stack
        :type default_region: str
        :param data_dir: directory containing files used by cfn-init
        :type data_dir: str | None
        :param s3_bucket: if defined S3 will be used as a proxy for resources.
            Template body will be uploaded to S3 before calling operation on
            it. This change the body limit from 50Ko to 500Ko. Additionally if
            data_dir is defined, the directory will be uploaded to the
            specified S3 bucket.
        :param s3_key: if s3_bucket is defined, then all uploaded resources
            will be stored under a subkey of s3_key. If not defined the root
            of the bucket is used.
        :type: str
        :param assume_role: tuple containing the two values that are passed
            to Session.assume_role()
        :type assume_role: str
        """
        super(CFNMain, self).__init__(platform_args=False)
        self.argument_parser.add_argument(
            "--profile",
            help="choose AWS profile, default is {}".format(default_profile),
            default=default_profile,
        )

        if len(regions) > 1:
            self.argument_parser.add_argument(
                "--region",
                help="choose region (default: %s)" % regions[0],
                default=regions[0],
            )
        else:
            self.argument_parser.set_defaults(region=regions[0])

        subs = self.argument_parser.add_subparsers(
            title="commands", description="available commands", dest="command")
        subs.required = True

        create_args = subs.add_parser("push", help="push a stack")
        create_args.add_argument(
            "--no-wait",
            action="store_false",
            default=True,
            dest="wait_stack_creation",
            help="do not wait for stack creation completion",
        )
        create_args.set_defaults(command="push")

        update_args = subs.add_parser("update", help="update a stack")
        update_args.add_argument(
            "--no-apply",
            action="store_false",
            default=True,
            dest="apply_changeset",
            help="do not ask whether to apply the changeset",
        )
        update_args.set_defaults(command="update")

        show_args = subs.add_parser("show", help="show the changeset content")
        show_args.set_defaults(command="show")

        protect_args = subs.add_parser(
            "protect", help="protect the stack against deletion")
        protect_args.set_defaults(command="protect")

        self.regions = regions

        self.data_dir = data_dir
        self.s3_bucket = s3_bucket
        self.s3_data_key = None
        self.s3_data_url = None
        self.s3_template_key = None
        self.s3_template_url = None
        self.assume_role = assume_role

        self.timestamp = str(int(time.time()))

        if s3_bucket is not None:
            s3_root_key = (
                "/".join([s3_key.rstrip("/"), self.timestamp]).strip("/") +
                "/")
            self.s3_data_key = s3_root_key + "data/"
            self.s3_data_url = "https://%s.s3.amazonaws.com/%s" % (
                self.s3_bucket,
                self.s3_data_key,
            )
            self.s3_template_key = s3_root_key + "template"
            self.s3_template_url = "https://%s.s3.amazonaws.com/%s" % (
                self.s3_bucket,
                self.s3_template_key,
            )

    def execute(self, args=None, known_args_only=False, aws_env=None):
        """Execute application and return exit status.

        See parse_args arguments.
        """
        super(CFNMain, self).parse_args(args, known_args_only)
        if aws_env is not None:
            self.aws_env = aws_env
        else:

            if self.assume_role:
                main_session = Session(regions=self.regions,
                                       profile=self.args.profile)
                self.aws_env = main_session.assume_role(
                    self.assume_role[0], self.assume_role[1])
                # ??? needed since we still use a global variable for AWSEnv
                Env().aws_env = self.aws_env
            else:
                self.aws_env = AWSEnv(regions=self.regions,
                                      profile=self.args.profile)
            self.aws_env.default_region = self.args.region

        try:
            if self.args.command in ("push", "update"):
                if self.data_dir is not None and self.s3_data_key is not None:
                    s3 = self.aws_env.client("s3")

                    # synchronize data to the bucket before creating the stack
                    for f in find(self.data_dir):
                        with open(f, "rb") as fd:
                            subkey = os.path.relpath(f, self.data_dir).replace(
                                "\\", "/")
                            logging.info(
                                "Upload %s to %s:%s%s",
                                subkey,
                                self.s3_bucket,
                                self.s3_data_key,
                                subkey,
                            )
                            s3.put_object(
                                Bucket=self.s3_bucket,
                                Body=fd,
                                ServerSideEncryption="AES256",
                                Key=self.s3_data_key + subkey,
                            )

                s = self.create_stack()

                if self.s3_template_key is not None:
                    logging.info("Upload template to %s:%s", self.s3_bucket,
                                 self.s3_template_key)
                    s3.put_object(
                        Bucket=self.s3_bucket,
                        Body=s.body.encode("utf-8"),
                        ServerSideEncryption="AES256",
                        Key=self.s3_template_key,
                    )

                logging.info("Validate template for stack %s" % s.name)
                try:
                    s.validate(url=self.s3_template_url)
                except Exception:
                    logging.error("Invalid cloud formation template")
                    logging.error(s.body)
                    raise

                if s.exists():
                    changeset_name = "changeset%s" % int(time.time())
                    logging.info("Push changeset: %s" % changeset_name)
                    s.create_change_set(changeset_name,
                                        url=self.s3_template_url)
                    result = s.describe_change_set(changeset_name)
                    while result["Status"] in ("CREATE_PENDING",
                                               "CREATE_IN_PROGRESS"):
                        time.sleep(1.0)
                        result = s.describe_change_set(changeset_name)

                    if result["Status"] == "FAILED":
                        logging.error(result["StatusReason"])
                        s.delete_change_set(changeset_name)
                        return 1
                    else:
                        for el in result["Changes"]:
                            if "ResourceChange" not in el:
                                continue
                            logging.info(
                                "%-8s %-32s: (replacement:%s)",
                                el["ResourceChange"].get("Action"),
                                el["ResourceChange"].get("LogicalResourceId"),
                                el["ResourceChange"].get("Replacement", "n/a"),
                            )

                        if self.args.apply_changeset:
                            ask = input("Apply change (y/N): ")
                            if ask[0] in "Yy":
                                return s.execute_change_set(
                                    changeset_name=changeset_name, wait=True)
                        return 0
                else:
                    logging.info("Create new stack")
                    s.create(url=self.s3_template_url)
                    state = s.state()
                    if self.args.wait_stack_creation:
                        logging.info("waiting for stack creation...")
                        while "PROGRESS" in state["Stacks"][0]["StackStatus"]:
                            result = s.resource_status(in_progress_only=False)
                            time.sleep(10.0)
                            state = s.state()
                        logging.info("done")
            elif self.args.command == "show":
                s = self.create_stack()
                print(s.body)
            elif self.args.command == "protect":
                s = self.create_stack()

                # Enable termination protection
                result = s.enable_termination_protection()

                if self.stack_policy_body is not None:
                    s.set_stack_policy(self.stack_policy_body)
                else:
                    print("No stack policy to set")

            return 0
        except botocore.exceptions.ClientError as e:
            logging.error(str(e))
            return 1

    @abc.abstractmethod
    def create_stack(self):
        """Create a stack.

        :return: Stack on which the application will operate
        :rtype: Stack
        """
        pass

    @property
    def stack_policy_body(self):
        """Stack Policy that can be set by calling the command ``protect``.

        :return: the inline stack policy
        :rtype: str
        """
        return None