Source code for troposphere_mate.core.mate

# -*- coding: utf-8 -*-

"""
This module aims to add more feature to Original troposphere Class.
"""

try:
    from typing import Union, List
except:  # pragma: no cover
    pass

import importlib
import warnings

import troposphere
from six import string_types
from troposphere import AWSObject, depends_on_helper, cloudformation
from troposphere.template_generator import TemplateGenerator

from . import metadata as mtdt
from .aws_object import Mixin
from .sentiel import NOTHING, REQUIRED
from .tagger import (
    update_tags_for_template,
)

DEFAULT_LABELS_FIELD = mtdt.ResourceLevelField.LABELS


def preprocess_init_kwargs(**kwargs):
    processed_kwargs = dict()
    for key, value in kwargs.items():
        if value is not NOTHING:
            processed_kwargs[key] = value
    return processed_kwargs


[docs]def convert_to_mate_resource(troposphere_resource, troposphere_mate_template): """ This method converts ``troposphere.AWSObject`` to ``troposphere_mate.AWSObject``. :type troposphere_resource: AWSObject :rtype: Mixin """ troposphere_mate_module_name = troposphere_resource.__class__.__module__ \ .replace("troposphere.", "troposphere_mate.") troposphere_mate_module = importlib.import_module(troposphere_mate_module_name) troposphere_mate_aws_resource_class = getattr( troposphere_mate_module, troposphere_resource.__class__.__name__ ) kwargs = { "title": troposphere_resource.title, "Metadata": troposphere_resource.resource.get("Metadata"), "Condition": troposphere_resource.resource.get("Condition"), "CreationPolicy": troposphere_resource.resource.get("CreationPolicy"), "DeletionPolicy": troposphere_resource.resource.get("DeletionPolicy"), "DependsOn": troposphere_resource.resource.get("DependsOn"), "UpdatePolicy": troposphere_resource.resource.get("UpdatePolicy"), "UpdateReplacePolicy": troposphere_resource.resource.get("UpdateReplacePolicy"), } kwargs = { k: v for k, v in kwargs.items() if v is not None } for key in troposphere_resource.props: value = troposphere_resource.resource.get("Properties", {}).get(key) if value is not None: kwargs[key] = value troposphere_mate_resource = troposphere_mate_aws_resource_class(**kwargs) return troposphere_mate_resource
[docs]def convert_to_mate_output(troposphere_output, troposphere_mate_template): """ This method converts ``troposphere.AWSObject`` to ``troposphere_mate.AWSObject``. :type troposphere_output: troposphere.Output :rtype: Output """ kwargs = { "title": troposphere_output.title, } kwargs = { k: v for k, v in kwargs.items() if v is not None } for key, value in troposphere_output.resource.items(): kwargs[key] = value mtdt.initiate_default_template_metadata(troposphere_mate_template) DependsOn = troposphere_mate_template.metadata[mtdt.TROPOSPHERE_METADATA_FIELD_NAME] \ [mtdt.TemplateLevelField.OUTPUTS] \ .get(troposphere_output.title, {}) \ .get(mtdt.TemplateLevelField.OUTPUTS_DEPENDS_ON, []) kwargs["DependsOn"] = DependsOn troposphere_mate_output = Output(**kwargs) return troposphere_mate_output
[docs]def is_x_depends_on_y(res_x, res_y): """ Returns a boolean value to indicte that whether Resource X depends on Resource Y. :type res_x: AWSObject :type res_y: AWSObject :rtype: bool """ try: _ = res_x.DependsOn except AttributeError: return False if isinstance(res_x.DependsOn, string_types): return res_y.title == res_x.DependsOn elif isinstance(res_x.DependsOn, (list, tuple)): return res_y.title in res_x.DependsOn else: return False
class Template(troposphere.Template): @classmethod def from_dict(cls, dct): """ Factory method to construct a Troposphere template from dictionary data. :type dct: dict :return: Template .. note:: troposphere provides a factory class TemplateGenerator to deserialize the dict data to troposphere.Template object. troposphere_mate.Template should be able to do the same things. To reuse ``TemplateGenerator`` code, we convert ``troposphere.Template`` to ``troposphere_mate.Template`` afterwards. """ tropo_tpl = TemplateGenerator(dct) tropo_mate_tpl = cls() tropo_mate_tpl.description = tropo_tpl.description tropo_mate_tpl.metadata = tropo_tpl.metadata tropo_mate_tpl.conditions = tropo_tpl.conditions tropo_mate_tpl.mappings = tropo_tpl.mappings tropo_mate_tpl.outputs = { k: convert_to_mate_output(v, tropo_mate_tpl) for k, v in tropo_tpl.outputs.items() } tropo_mate_tpl.parameters = tropo_tpl.parameters tropo_mate_tpl.resources = { k: convert_to_mate_resource(v, tropo_mate_tpl) for k, v in tropo_tpl.resources.items() } tropo_mate_tpl.version = tropo_tpl.version tropo_mate_tpl.transform = tropo_tpl.transform return tropo_mate_tpl def update_tags(self, tags_dct, overwrite=False): """ Batch Update Tags to all resource that support tags. :type tags_dct: Dict[str, Union[str,Callable]] :type overwrite: bool """ update_tags_for_template(self, tags_dct, overwrite=overwrite) def to_file(self, path, json_or_yml="json", **kwargs): # pragma: no cover """ Dump template to a json or yml file. """ self.set_version() if json_or_yml == "json": content = self.to_json(**kwargs) elif json_or_yml == "yml": content = self.to_yaml(**kwargs) else: raise Exception with open(path, "wb") as f: f.write(content.encode("utf8")) def pprint(self, json_or_yml="json"): # pragma: no cover if json_or_yml == "json": print(self.to_json()) else: print(self.to_yaml()) def add_parameter(self, parameter, ignore_duplicate=False): """ :type resource: Parameter :type ignore_duplicate: bool """ if ignore_duplicate: if parameter.title in self.parameters: return super(Template, self).add_parameter(parameter) def add_output(self, output, ignore_duplicate=False): """ :type resource: Output :type ignore_duplicate: bool """ if ignore_duplicate: if output.title in self.outputs: return if not isinstance(self.metadata, dict): self.metadata = {} mtdt.initiate_default_template_metadata(self) self.metadata[mtdt.TROPOSPHERE_METADATA_FIELD_NAME] \ [mtdt.TemplateLevelField.OUTPUTS] \ .setdefault(output.title, {}) self.metadata[mtdt.TROPOSPHERE_METADATA_FIELD_NAME] \ [mtdt.TemplateLevelField.OUTPUTS] \ [output.title] \ [mtdt.TemplateLevelField.OUTPUTS_DEPENDS_ON] \ = getattr(output, mtdt.TemplateLevelField.OUTPUTS_DEPENDS_ON) super(Template, self).add_output(output) def add_resource(self, resource, ignore_duplicate=False): """ :type resource: AWSObject :type ignore_duplicate: bool """ if ignore_duplicate: if resource.title in self.resources: return super(Template, self).add_resource(resource) def remove_parameter(self, parameter, ignore_not_exists=False): """ Remove a parameter. :type output: Union[Parameter, str] """ if isinstance(parameter, Parameter): parameter_logic_id = parameter.title else: parameter_logic_id = parameter if parameter_logic_id not in self.parameters: if not ignore_not_exists: raise ValueError("Can't remove, Template '{}' not found in the template!".format( parameter_logic_id)) del self.parameters[parameter_logic_id] def remove_output(self, output, ignore_not_exists=False): """ Remove a parameter. :type output: Union[Output, str] """ if isinstance(output, Output): output_logic_id = output.title else: output_logic_id = output if output_logic_id not in self.outputs: if ignore_not_exists: return else: raise ValueError( "Can't remove, Output '{}' not found in the template!".format(output_logic_id)) del self.outputs[output_logic_id] def remove_resource(self, resource, ignore_not_exists=False, remove_dependent=True, _to_delete_resources=None, _to_delete_outputs=None, _first_time_called=False): """ Remove AWS Resource Object from "Resources" and related "Outputs". Note: there's no need to worry that, supposed that X depends on Y, you removed Y, so the DependsOn field in X doesn't make sense. because it doesn't make sense that you remove Y but not remove X. :type resource: Union[AWSObject, str] :type ignore_not_exists: bool :type remove_dependent: bool :type _to_delete_resources: list :param _to_delete_resources: internal implementation variables :type _to_delete_outputs: list :param _to_delete_outputs: internal implementation variables :type _first_time_called: bool :param _first_time_called: internal implementation variables """ if not remove_dependent: # pragma: no cover warnings.warn("`remove_dependent=False` might leave invalid dependencies " "relationship. For example X depends on Y, you removed Y " "but forgot to remove X!") if isinstance(resource, AWSObject): resource_logic_id = resource.title else: resource_logic_id = resource if _to_delete_resources is None: _to_delete_resources = list() _first_time_called = True if _to_delete_outputs is None: _to_delete_outputs = list() if resource_logic_id not in self.resources: if ignore_not_exists: return else: raise ValueError( "Can't remove, Resource '{}' not found in the template!".format(resource_logic_id)) if not isinstance(resource, AWSObject): resource = self.resources[resource_logic_id] # type: AWSObject _to_delete_resources.append(resource_logic_id) # iterate all output, find all outputs depends on this resource for output_logic_id, output in list(self.outputs.items()): if resource_logic_id in output.depends_on_resources: _to_delete_outputs.append(output_logic_id) # recursively remove all resource that explicitly depends on this resource. if remove_dependent: for res_logic_id, res in list(self.resources.items()): if is_x_depends_on_y(res, resource): self.remove_resource( res, ignore_not_exists=True, remove_dependent=remove_dependent, _to_delete_resources=_to_delete_resources, _to_delete_outputs=_to_delete_outputs, ) if _first_time_called: for resource_logic_id in _to_delete_resources: if resource_logic_id in self.resources: del self.resources[resource_logic_id] for output_logic_id in _to_delete_outputs: if output_logic_id in self.outputs: del self.outputs[output_logic_id] def remove_resource_by_label(self, label, label_field_in_metadata=DEFAULT_LABELS_FIELD): """ If you specified Tags (a list of string) in Metadata field, you can batch remove resource by tag :type label: str :type label_field_in_metadata: str """ for resource_logic_id, resource in list(self.resources.items()): if label in resource.resource.get("Metadata", {}).get(label_field_in_metadata, []): self.remove_resource(resource) def create_resource_type_label(self, label_field_in_metadata=DEFAULT_LABELS_FIELD): """ Put resource type in Metadata. Allow you to use resource type to easily filter resources. .. versionchanged:: 0.0.13 no longer to create dependent resource type label to metadata. because it confuses users. """ for resource_logic_id, resource in self.resources.items(): try: metadata = resource.Metadata metadata.setdefault(label_field_in_metadata, []) except: metadata = {label_field_in_metadata: []} resource_type = resource.resource_type if resource_type not in metadata[label_field_in_metadata]: metadata[DEFAULT_LABELS_FIELD].append(resource_type) resource.Metadata = metadata @property def param_ids(self): l = list(self.parameters) l.sort() return l @property def n_param(self): return len(self.parameters) @property def resource_ids(self): l = list(self.resources) l.sort() return l @property def n_resource(self): return len(self.resources) @property def outputs_ids(self): l = list(self.outputs) l.sort() return l @property def n_output(self): return len(self.outputs) def iter_nested_template(self, depth_first=True, _templates=None, _first_time_called=False): """ Iterate nested template recursively. :type depth_first: bool :param _templates: an internal implementation variable. :param _first_time_called: an internal implementation variable. :rtype: List[Template] """ if _templates is None: _templates = list() _first_time_called = True _templates_this_level = list() # type: List[Template] for resource in self.resources.values(): if resource.resource_type == cloudformation.Stack.resource_type: template = resource._template _templates.append(template) # collect this level template, could be used in width first mode later _templates_this_level.append(resource._template) if depth_first: template.iter_nested_template( depth_first=depth_first, _templates=_templates, _first_time_called=False, ) if not depth_first: for template in _templates_this_level: template.iter_nested_template( depth_first=depth_first, _templates=_templates, _first_time_called=False, ) if _first_time_called: return _templates
[docs]class Parameter(troposphere.Parameter): def __init__(self, title, Type=REQUIRED, Default=NOTHING, NoEcho=NOTHING, AllowedValues=NOTHING, AllowedPattern=NOTHING, MaxLength=NOTHING, MinLength=NOTHING, MaxValue=NOTHING, MinValue=NOTHING, Description=NOTHING, ConstraintDescription=NOTHING, **kwargs): processed_kwargs = preprocess_init_kwargs( title=title, Type=Type, Default=Default, NoEcho=NoEcho, AllowedValues=AllowedValues, AllowedPattern=AllowedPattern, MaxLength=MaxLength, MinLength=MinLength, MaxValue=MaxValue, MinValue=MinValue, Description=Description, ConstraintDescription=ConstraintDescription, **kwargs ) super(Parameter, self).__init__(**processed_kwargs)
[docs]class Output(troposphere.Output): def __init__(self, title, Value=REQUIRED, Description=NOTHING, Export=NOTHING, DependsOn=NOTHING, **kwargs): processed_kwargs = preprocess_init_kwargs( title=title, Value=Value, Description=Description, Export=Export, **kwargs ) super(Output, self).__init__(**processed_kwargs) if DependsOn is NOTHING: object.__setattr__(self, mtdt.TemplateLevelField.OUTPUTS_DEPENDS_ON, []) else: depends_on = depends_on_helper(DependsOn) if not isinstance(depends_on, list): depends_on = [depends_on, ] object.__setattr__(self, mtdt.TemplateLevelField.OUTPUTS_DEPENDS_ON, depends_on)