Source code for saltext.boto3.states.boto3_cfn

"""
Manage CloudFormation stacks using boto3.
=========================================

    Renamed from ``boto_cfn`` to ``boto3_cfn`` and updated to call the
    refactored ``boto3_cfn`` execution module.

Create and destroy CloudFormation stacks. Be aware that this interacts with Amazon's
services, and so may incur charges.

:depends:
  - boto3 >= 1.28.0
  - botocore >= 1.31.0

This module uses boto3, which can be installed via package, or pip.

This module accepts explicit CloudFormation credentials but can also utilize
IAM roles assigned to the instance through Instance Profiles. Dynamic
credentials are then automatically obtained from AWS API and no further
configuration is necessary. More Information available at:

.. code-block:: text

    http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html

If IAM roles are not used you need to specify them either in the minion's config file
or as a profile. For example, to specify them in the minion's config file:

.. code-block:: yaml

    cfn.keyid: GKTADJGHEIQSXMKKRBJ08H
    cfn.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs

It's also possible to specify key, keyid and region via a profile, either
as a passed in dict, or as a string to pull from pillars or minion config:

.. code-block:: yaml

    myprofile:
        keyid: GKTADJGHEIQSXMKKRBJ08H
        key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
        region: us-east-1

.. code-block:: yaml

    stack-present:
      boto3_cfn.present:
        - name: mystack
        - template_body: salt://base/mytemplate.json
        - disable_rollback: true
        - region: eu-west-1
        - keyid: 'AKIAJHTMIQ2ASDFLASDF'
        - key: 'fdkjsafkljsASSADFalkfjasdf'

.. code-block:: yaml

    stack-absent:
      boto3_cfn.absent:
        - name: mystack

.. versionadded:: 1.0.0
"""

import logging

import salt.utils.compat
import salt.utils.json

log = logging.getLogger(__name__)

__virtualname__ = "boto3_cfn"


[docs] def __virtual__(): """ Only load if the boto3_cfn execution module is available. """ if "boto3_cfn.exists" in __salt__: return __virtualname__ return ( False, f"Cannot load {__virtualname__} state: boto3_cfn module unavailable", )
[docs] def present( name, template_body=None, template_url=None, parameters=None, notification_arns=None, disable_rollback=None, timeout_in_minutes=None, capabilities=None, tags=None, on_failure=None, stack_policy_body=None, stack_policy_url=None, use_previous_template=None, stack_policy_during_update_body=None, stack_policy_during_update_url=None, region=None, key=None, keyid=None, profile=None, ): """ Ensure cloud formation stack is present. name (string) - Name of the stack. template_body (string) - Structure containing the template body. Can also be loaded from a file by using ``salt://``. template_url (string) - Location of file containing the template body. The URL must point to a template located in an S3 bucket in the same region as the stack. parameters (list) - A list of ``(key, value)`` tuples or ``{"ParameterKey": ..., "ParameterValue": ...}`` dicts that specify input parameters for the stack. A 3-tuple ``(key, value, use_previous_value)`` may be used to specify the ``UsePreviousValue`` option. notification_arns (list) - The Simple Notification Service (SNS) topic ARNs to publish stack related events. disable_rollback (bool) - Indicates whether or not to rollback on failure. timeout_in_minutes (integer) - The amount of time that can pass before the stack status becomes ``CREATE_FAILED``. capabilities (list) - The list of capabilities you want to allow in the stack. tags (dict or list) - Tags to associate with this stack. A dict is converted to the boto3 ``[{"Key": ..., "Value": ...}]`` form. on_failure (string) - One of ``DO_NOTHING``, ``ROLLBACK``, or ``DELETE``. stack_policy_body (string) - Structure containing the stack policy body. Can also be loaded from a file by using ``salt://``. stack_policy_url (string) - Location of a file containing the stack policy. use_previous_template (boolean) - Set to True to use the previous template instead of uploading a new one via ``template_body`` or ``template_url``. stack_policy_during_update_body (string) - Temporary overriding stack policy body used during an update. Can also be loaded from a file by using ``salt://``. stack_policy_during_update_url (string) - Location of a file containing the temporary overriding stack policy. region (string) - Region to connect to. key (string) - Secret key to be used. keyid (string) - Access key to be used. profile (dict) - A dict with region, key and keyid, or a pillar key (string) that contains a dict with region, key and keyid. Example: .. code-block:: yaml ensure-present: boto3_cfn.present: - name: example """ ret = {"name": name, "result": True, "comment": "", "changes": {}} template_body = _get_template(template_body, name) stack_policy_body = _get_template(stack_policy_body, name) stack_policy_during_update_body = _get_template(stack_policy_during_update_body, name) for i in [template_body, stack_policy_body, stack_policy_during_update_body]: if isinstance(i, dict): return i _valid = _validate(template_body, template_url, region, key, keyid, profile) log.debug("Validate is : %s.", _valid) if _valid is not True: ret["result"] = False ret["comment"] = f"Template could not be validated.\n{_valid}" return ret log.debug("Template %s is valid.", name) if __salt__["boto3_cfn.exists"](name, region, key, keyid, profile): template = __salt__["boto3_cfn.get_template"](name, region, key, keyid, profile) if isinstance(template, str): ret["result"] = False ret["comment"] = f"Could not retrieve stack template.\n{template}" return ret current_body = template["TemplateBody"] if isinstance(current_body, str): current_body = salt.utils.json.loads(current_body) _template_body = salt.utils.json.loads(template_body) compare = salt.utils.compat.cmp(current_body, _template_body) if compare != 0: log.debug("Templates are not the same. Compare value is %s", compare) if __opts__["test"]: ret["comment"] = f"Stack {name} is set to be updated." ret["result"] = None return ret updated = __salt__["boto3_cfn.update_stack"]( name, template_body, template_url, parameters, notification_arns, disable_rollback, timeout_in_minutes, capabilities, tags, use_previous_template, stack_policy_during_update_body, stack_policy_during_update_url, stack_policy_body, stack_policy_url, region, key, keyid, profile, ) if isinstance(updated, str): ret["result"] = False ret["comment"] = f"Stack {name} could not be updated.\n{updated}" return ret ret["comment"] = f"Cloud formation template {name} has been updated." ret["changes"]["new"] = updated return ret ret["comment"] = f"Stack {name} exists." ret["changes"] = {} return ret if __opts__["test"]: ret["comment"] = f"Stack {name} is set to be created." ret["result"] = None return ret created = __salt__["boto3_cfn.create"]( name, template_body, template_url, parameters, notification_arns, disable_rollback, timeout_in_minutes, capabilities, tags, on_failure, stack_policy_body, stack_policy_url, region, key, keyid, profile, ) if created: ret["comment"] = f"Stack {name} was created." ret["changes"]["new"] = created return ret ret["result"] = False return ret
[docs] def absent(name, region=None, key=None, keyid=None, profile=None): """ Ensure cloud formation stack is absent. name (string) - The name of the stack to delete. region (string) - Region to connect to. key (string) - Secret key to be used. keyid (string) - Access key to be used. profile (dict) - A dict with region, key and keyid, or a pillar key (string) that contains a dict with region, key and keyid. Example: .. code-block:: yaml ensure-absent: boto3_cfn.absent: - name: example """ ret = {"name": name, "result": True, "comment": "", "changes": {}} if not __salt__["boto3_cfn.exists"](name, region, key, keyid, profile): ret["comment"] = f"Stack {name} does not exist." ret["changes"] = {} return ret if __opts__["test"]: ret["comment"] = f"Stack {name} is set to be deleted." ret["result"] = None return ret deleted = __salt__["boto3_cfn.delete"](name, region, key, keyid, profile) if isinstance(deleted, str): ret["comment"] = f"Stack {name} could not be deleted.\n{deleted}" ret["result"] = False ret["changes"] = {} return ret if deleted: ret["comment"] = f"Stack {name} was deleted." ret["changes"]["deleted"] = name return ret ret["result"] = False ret["comment"] = f"Stack {name} could not be deleted." return ret
def _get_template(template, name): # Checks if template is a file in salt defined by salt://. ret = {"name": name, "result": True, "comment": "", "changes": {}} if template is not None and "salt://" in template: try: return __salt__["cp.get_file_str"](template) except OSError as e: log.debug(e) ret["comment"] = f"File {template} not found." ret["result"] = False return ret return template def _validate( template_body=None, template_url=None, region=None, key=None, keyid=None, profile=None, ): # Validates template. Returns True if syntax is correct. validate = __salt__["boto3_cfn.validate_template"]( template_body, template_url, region, key, keyid, profile ) log.debug("Validate result is %s.", validate) if isinstance(validate, str): return validate return True