def service_client(self, region=None, method_names=None): """ Returns a client for the service using the session/role/region of the service class instance :param region: :param method_names: names of function to create wrapper methods for with retry logic :return: client for making the call to describe the resources """ if region is None: region = boto3.client(self.service_name).meta.config.region_name if self._service_client is None or self._service_client.meta.config.region_name != region: args = { "service_name": self.service_name, "region_name": region } used_session = self._session if self._session is not None else services.get_session(self.role_arn) self._service_client = used_session.client(**args) if self._service_retry_strategy is not None and method_names is not None: for method_name in method_names: if getattr(self._service_client, method_name + boto_retry.DEFAULT_SUFFIX, None) is None: boto_retry.make_method_with_retries(boto_client_or_resource=self._service_client, name=method_name, service_retry_strategy=self._service_retry_strategy) return self._service_client
def service_regions(self): """ Returns all regions in which a service is available :return: all regions in which the service is available """ return services.get_session().get_available_regions( service_name=self.service_name)
def action_validate_parameters(parameters, task_settings, logger): valid_regions = services.get_session().get_available_regions( "ec2", "aws") region = parameters.get(PARAM_DESTINATION_REGION) if region not in valid_regions: raise_value_error(ERR_INVALID_DESTINATION_REGION, region, ",".join(valid_regions)) if parameters.get(PARAM_DELETE_AFTER_COPY, False) and parameters.get( PARAM_COPIED_SNAPSHOTS) != COPIED_OWNED_BY_ACCOUNT: raise_value_error(ERR_CANNOT_DELETE_SHARED_SNAPSHOTS) kms_key_id = parameters.get(PARAM_KMS_KEY_ID, None) if not parameters[PARAM_ENCRYPTED] and kms_key_id not in ["", None]: raise_value_error(ERR_KMS_KEY_ONLY_IF_ENCRYPTED, PARAM_KMS_KEY_ID) if kms_key_id not in ["", None]: if re.match(KMS_KEY_ID_PATTERN, kms_key_id) is None: raise_value_error(ERR_INVALID_KMS_ID_ARN, kms_key_id) destination_region = parameters[PARAM_DESTINATION_REGION] if kms_key_id.split(":")[3] != destination_region: raise_value_error(ERR_KMS_KEY_NOT_IN_REGION, kms_key_id, destination_region) return parameters
def session(self): """ Returns a (cached) session for the service class instance, use role if an arf for that role was provided, otherwise the default boto3 session is used :return: Session """ if self._session is None: self._session = services.get_session(role_arn=self.role_arn, sts_client=self.sts_client if self.role_arn not in [None, ""] else None) return self._session
def get_task_session(account, task, this_account=False, logger=None): log_to_debug(logger, "Getting session for account \"{}\", task is \"{}\"", account, task[TASK_NAME]) role_arn = get_account_role(account, task, logger=logger) try: return services.get_session(role_arn=role_arn, logger=logger), role_arn except Exception as ex: if logger is not None: logger.error(ERR_CREATING_SESSION, ex) return None, role_arn
def __init__(self, event, context): """ Initializes handler. :param event: Event to handle :param context: Context if run within Lambda environment """ self._context = context self._event = event self.action_id = self._event[handlers.TASK_TR_ID] self.task = self._event[handlers.TASK_TR_NAME] self.task_timezone = self._event.get(handlers.TASK_TR_TIMEZONE, None) self.has_completion = self._event[handlers.TASK_TR_HAS_COMPLETION] self.action_parameters = self._event.get(handlers.TASK_TR_PARAMETERS, {}) self.dryrun = self._event.get(handlers.TASK_TR_DRYRUN) self.interval = self._event.get(handlers.TASK_TR_INTERVAL, None) self.metrics = self._event.get(handlers.TASK_TR_METRICS, False) self.debug = self._event.get(handlers.TASK_TR_DEBUG) self.started_at = int(self._event.get(handlers.TASK_TR_STARTED_TS, 0)) self.start_result = self._event.get(handlers.TASK_TR_START_RESULT, None) self.session = services.get_session( self._event.get(handlers.TASK_TR_ASSUMED_ROLE)) self.stack_name = os.getenv(handlers.ENV_STACK_NAME) self.stack_id = os.getenv(handlers.ENV_STACK_ID) self.action = event[handlers.TASK_TR_ACTION] self.tagfilter = event.get(handlers.TASK_TR_TAGFILTER, "") self.action_properties = actions.get_action_properties(self.action) self.action_class = actions.get_action_class(self.action) self._stack_resources = None self.timeout = int( self._event[handlers.TASK_TR_TIMEOUT]) * 60 if self._event.get( handlers.TASK_TR_TIMEOUT, None) not in [None, "None"] else 0 self.execution_log_stream = self._event.get( handlers.TASK_TR_EXECUTION_LOGSTREAM) self.assumed_role = self._event.get(handlers.TASK_TR_ASSUMED_ROLE, None) self.events = self._event.get(handlers.TASK_TR_EVENTS, {}) if isinstance(self.events, str): self.events = json.loads( self._event.get(handlers.TASK_TR_EVENTS, "{}").replace("u'", '"').replace("'", '"')) self._action_resources = None self._s3_client = None self._action_instance = None self._action_class = None self._action_arguments = None self._timer = None self._timeout_event = None self.__logger = None self.__action_tracking = None
def concurrency_table(self): """ Returns table to store last execution time for this handler. :return: table to store last execution time for this handler """ if self._concurrency_table is None: tablename = os.getenv(handlers.ENV_CONCURRENCY_TABLE) self._logger.debug("Using concurrency table {}", tablename) self._concurrency_table = services.get_session().resource( "dynamodb").Table(tablename) boto_retry.add_retry_methods_to_resource( self._concurrency_table, ["update_item", "get_item", "delete_item"], context=self._context) return self._concurrency_table
def describe(self, as_tuple=None, **kwargs): """ This method is to retrieve a pseudo UTC time resource, method parameters are only used signature compatibility :param as_tuple: Set to true to return results as immutable named dictionaries instead of dictionaries :return: Pseudo time resource """ def use_tuple(): return (as_tuple is not None and as_tuple) or (as_tuple is None and self._as_tuple) region = kwargs.get("region") result = { "Time": datetime.datetime.now(pytz.timezone("UTC")), "AwsAccount": self.aws_account, "Region": region if region else services.get_session().region_name } return [as_namedtuple("Time", result)] if use_tuple() else [result]
def get_client_with_retries(service_name, methods, context=None, region=None, session=None, wait_strategy=None, method_suffix=DEFAULT_SUFFIX, logger=None): args = { "service_name": service_name, } if region is not None: args["region_name"] = region user_agent = os.getenv(ENV_USER_AGENT, None) if user_agent is not None: session_config = botocore.config.Config(user_agent=user_agent) args["config"] = session_config if session is not None: aws_session = session else: aws_session = services.get_session() result = aws_session.client(**args) # get strategy for the service service_retry_strategy = get_default_retry_strategy( context=context, service=service_name, wait_strategy=wait_strategy, logger=logger) # add a new method to the client instance that wraps the original method with service specific retry logic for method in methods: make_method_with_retries(boto_client_or_resource=result, name=method, service_retry_strategy=service_retry_strategy, method_suffix=method_suffix) return result
def build_events_forward_template(template_filename, script_filename, stack, event_role_arn, ops_automator_topic_arn, version): with open(script_filename, "rt") as f: script_text = f.readlines() with open(template_filename, "rt") as f: template = json.loads("".join(f.readlines()), object_pairs_hook=OrderedDict) code = template["Resources"]["EventsForwardFunction"]["Properties"][ "Code"] code["ZipFile"]["Fn::Join"][1] = script_text return json.dumps(template, indent=3) \ .replace("%version%", version) \ .replace("%ops-automator-stack%", stack) \ .replace("%ops-automator-region%", services.get_session().region_name) \ .replace("%ops-automator-account%", services.get_aws_account()) \ .replace("%ops-automator-topic-arn%", ops_automator_topic_arn) \ .replace("%event-forward-role%", event_role_arn)
def get_action_session(self, account, param_name=None, logger=None): self._logger_.debug( "Getting action session for account \"{}\", task is \"{}\", parameter is \"{}\"", account, self._task_, param_name) try: role_name = self.get(param_name, None) if role_name is None: role_name = self.get(handlers.TASK_ROLE, None) if role_name is None: if account == os.getenv(handlers.ENV_OPS_AUTOMATOR_ACCOUNT): role_name = None else: role_name = handlers.default_rolename_for_stack() role_arn = handlers.ARN_ROLE_TEMPLATE.format( account, role_name) if role_name is not None else None self._logger_.debug("Role arn is \"{}\"", role_arn) return services.get_session(role_arn=role_arn, logger=logger) except Exception as ex: if logger is not None: logger.error(handlers.ERR_CREATING_SESSION, ex) return None
class Ec2CopySnapshotAction(ActionBase): """ Class implements action for copying EC2 Snapshots """ properties = { ACTION_TITLE: "EC2 Copy Snapshot", ACTION_VERSION: "1.3", ACTION_DESCRIPTION: "Copies EC2 snapshot", ACTION_AUTHOR: "AWS", ACTION_ID: "eb287af5-e5c0-41cb-832b-d218c075fa26", ACTION_SERVICE: "ec2", ACTION_RESOURCES: services.ec2_service.SNAPSHOTS, ACTION_AGGREGATION: ACTION_AGGREGATION_RESOURCE, ACTION_COMPLETION_TIMEOUT_MINUTES: 60, ACTION_MIN_INTERVAL_MIN: 60, ACTION_SELECT_SIZE: [ ACTION_SIZE_MEDIUM, ACTION_SIZE_LARGE, ACTION_SIZE_XLARGE, ACTION_SIZE_XXLARGE, ACTION_SIZE_XXXLARGE ] + [ACTION_USE_ECS], ACTION_EXECUTE_SIZE: [ACTION_SIZE_MEDIUM], ACTION_COMPLETION_SIZE: [ACTION_SIZE_MEDIUM], ACTION_SELECT_EXPRESSION: "Snapshots[?State=='completed'].{SnapshotId:SnapshotId, " "VolumeId:VolumeId, OwnerId:OwnerId, " "StartTime:StartTime," "Description:Description, " "Tags:Tags}", ACTION_KEEP_RESOURCE_TAGS: True, ACTION_SELECT_PARAMETERS: { 'RestorableByUserIds': ["self"], }, ACTION_EVENTS: { handlers.EC2_EVENT_SOURCE: { handlers.ebs_snapshot_event_handler.EBS_SNAPSHOT_NOTIFICATION: [ handlers.ebs_snapshot_event_handler.EBS_SNAPSHOT_CREATED, handlers.ebs_snapshot_event_handler.EBS_SNAPSHOT_SHARED ] } }, # Ec2 CopySnapshot only allows 5 concurrent copies per account to a destination region ACTION_MAX_CONCURRENCY: int( os.getenv(handlers.ENV_SERVICE_LIMIT_CONCURRENT_EBS_SNAPSHOT_COPY, 5)), ACTION_PARAMETERS: { PARAM_DESTINATION_REGION: { PARAM_DESCRIPTION: PARAM_DESC_DESTINATION_REGION, PARAM_LABEL: PARAM_LABEL_DESTINATION_REGION, PARAM_TYPE: str, PARAM_REQUIRED: True, PARAM_DEFAULT: services.get_session().region_name, PARAM_ALLOWED_VALUES: [ str(r) for r in services.get_session().get_available_regions( "ec2", "aws") ] }, PARAM_SNAPSHOT_DESCRIPTION: { PARAM_DESCRIPTION: PARAM_DESC_SNAPSHOT_DESCRIPTION, PARAM_LABEL: PARAM_LABEL_SNAPSHOT_DESCRIPTION, PARAM_TYPE: str, PARAM_REQUIRED: False, }, PARAM_COPIED_SNAPSHOT_TAGS: { PARAM_DESCRIPTION: PARAM_DESC_COPIED_SNAPSHOT_TAGS, PARAM_LABEL: PARAM_LABEL_COPIED_SNAPSHOT_TAGS, PARAM_TYPE: str, PARAM_REQUIRED: False, }, PARAM_SNAPSHOT_TAGS: { PARAM_DESCRIPTION: PARAM_DESC_SNAPSHOT_TAGS, PARAM_LABEL: PARAM_LABEL_SNAPSHOT_TAGS, PARAM_TYPE: str, PARAM_REQUIRED: False }, PARAM_COPIED_SNAPSHOTS: { PARAM_DESCRIPTION: PARAM_DESC_COPIED_SNAPSHOTS, PARAM_LABEL: PARAM_LABEL_COPIED_SNAPSHOTS, PARAM_TYPE: str, PARAM_ALLOWED_VALUES: [ COPIED_OWNED_BY_ACCOUNT, COPIED_SNAPSHOTS_SHARED_TO_ACCOUNT, COPIED_SNAPSHOTS_BOTH ], PARAM_DEFAULT: COPIED_OWNED_BY_ACCOUNT, PARAM_REQUIRED: False }, PARAM_SOURCE_TAGS: { PARAM_DESCRIPTION: PARAM_DESC_SOURCE_TAGS, PARAM_LABEL: PARAM_LABEL_SOURCE_TAGS, PARAM_TYPE: str, PARAM_REQUIRED: False }, PARAM_DELETE_AFTER_COPY: { PARAM_DESCRIPTION: PARAM_DESC_DELETE_AFTER_COPY, PARAM_LABEL: PARAM_LABEL_DELETE_AFTER_COPY, PARAM_TYPE: bool, PARAM_DEFAULT: False, PARAM_REQUIRED: False }, PARAM_ENCRYPTED: { PARAM_DESCRIPTION: PARAM_DESC_ENCRYPTED, PARAM_LABEL: PARAM_LABEL_ENCRYPTED, PARAM_TYPE: bool, PARAM_DEFAULT: False, PARAM_REQUIRED: False }, PARAM_ACCOUNTS_VOLUME_CREATE_PERMISSIONS: { PARAM_DESCRIPTION: PARAM_DESC_ACCOUNTS_VOLUME_CREATE_PERMISSIONS, PARAM_TYPE: list, PARAM_REQUIRED: False, PARAM_LABEL: PARAM_LABEL_ACCOUNTS_VOLUME_CREATE_PERMISSIONS }, PARAM_COPY_FROM_OWNER_ACCOUNTS: { PARAM_DESCRIPTION: PARAM_DESC_COPY_FROM_OWNER_ACCOUNTS, PARAM_LABEL: PARAM_LABEL_COPY_FROM_OWNER_ACCOUNTS, PARAM_TYPE: list, PARAM_REQUIRED: False }, PARAM_TAG_IN_DESTINATION_ACCOUNT: { PARAM_DESCRIPTION: PARAM_DESC_TAG_IN_DESTINATION_ACCOUNT, PARAM_TYPE: bool, PARAM_REQUIRED: False, PARAM_DEFAULT: False, PARAM_LABEL: PARAM_LABEL_TAG_IN_DESTINATION_ACCOUNT }, PARAM_DESTINATION_ACCOUNT_TAG_ROLENAME: { PARAM_DESCRIPTION: PARAM_DESC_DESTINATION_ACCOUNT_TAG_ROLENAME, PARAM_TYPE: str, PARAM_REQUIRED: False, PARAM_LABEL: PARAM_LABEL_DESTINATION_ACCOUNT_TAG_ROLENAME }, PARAM_TAG_IN_SOURCE_ACCOUNT: { PARAM_DESCRIPTION: PARAM_DESC_TAG_IN_SOURCE_ACCOUNT, PARAM_TYPE: bool, PARAM_REQUIRED: False, PARAM_DEFAULT: False, PARAM_LABEL: PARAM_LABEL_TAG_IN_SOURCE_ACCOUNT }, PARAM_SOURCE_SHARED_BY_TAGS: { PARAM_DESCRIPTION: PARAM_DESC_SOURCE_SHARED_BY_TAGS, PARAM_LABEL: PARAM_LABEL_SOURCE_SHARED_BY_TAGS, PARAM_TYPE: str, PARAM_REQUIRED: False }, PARAM_SOURCE_ACCOUNT_TAG_ROLE_NAME: { PARAM_DESCRIPTION: PARAM_DESC_SOURCE_ACCOUNT_TAG_ROLENAME, PARAM_TYPE: str, PARAM_REQUIRED: False, PARAM_LABEL: PARAM_LABEL_SOURCE_ACCOUNT_TAG_ROLENAME }, PARAM_KMS_KEY_ID: { PARAM_DESCRIPTION: PARAM_DESC_KMS_KEY_ID, PARAM_LABEL: PARAM_LABEL_KMS_KEY_ID, PARAM_TYPE: str, PARAM_REQUIRED: False } }, ACTION_PARAMETER_GROUPS: [{ ACTION_PARAMETER_GROUP_TITLE: GROUP_LABEL_SNAPSHOT_COPY_OPTIONS, ACTION_PARAMETER_GROUP_LIST: [ PARAM_DESTINATION_REGION, PARAM_COPIED_SNAPSHOTS, PARAM_COPY_FROM_OWNER_ACCOUNTS, PARAM_SNAPSHOT_DESCRIPTION, PARAM_DELETE_AFTER_COPY ], }, { ACTION_PARAMETER_GROUP_TITLE: GROUP_LABEL_TAGGING_OPTIONS, ACTION_PARAMETER_GROUP_LIST: [ PARAM_COPIED_SNAPSHOT_TAGS, PARAM_SNAPSHOT_TAGS, ], }, { ACTION_PARAMETER_GROUP_TITLE: GROUP_LABEL_SOURCE_SNAPSHOT_TAGGING, ACTION_PARAMETER_GROUP_LIST: [ PARAM_SOURCE_TAGS, PARAM_TAG_IN_SOURCE_ACCOUNT, PARAM_SOURCE_SHARED_BY_TAGS, PARAM_SOURCE_ACCOUNT_TAG_ROLE_NAME, ], }, { ACTION_PARAMETER_GROUP_TITLE: GROUP_LABEL_SNAPSHOT_SHARING, ACTION_PARAMETER_GROUP_LIST: [ PARAM_ACCOUNTS_VOLUME_CREATE_PERMISSIONS, PARAM_TAG_IN_DESTINATION_ACCOUNT, PARAM_DESTINATION_ACCOUNT_TAG_ROLENAME ], }, { ACTION_PARAMETER_GROUP_TITLE: GROUP_LABEL_ENCRYPTION_AND_PERMISSIONS, ACTION_PARAMETER_GROUP_LIST: [PARAM_ENCRYPTED, PARAM_KMS_KEY_ID], }], ACTION_PERMISSIONS: [ "ec2:CopySnapshot", "ec2:CreateTags", "ec2:DeleteTags", "ec2:DescribeSnapshots", "ec2:DeleteSnapshot", "ec2:ModifySnapshotAttribute" ] } @staticmethod def marker_tag_source_snapshot_id(): return MARKER_TAG_SOURCE_SNAPSHOT_ID_TEMPLATE.format( os.getenv(handlers.ENV_STACK_NAME)) @staticmethod def marker_tag_copied_to(taskname): return MARKER_TAG_COPIED_TO_TEMPLATE.format( os.getenv(handlers.ENV_STACK_NAME), taskname) # noinspection PyUnusedLocal @staticmethod def process_and_select_resource(service, logger, resource_name, resource, context, task, task_assumed_role): def get_snapshot_tags(client, snap_id): try: resp = client.describe_snapshots_with_retries( RestorableByUserIds=["self"], SnapshotIds=[snap_id]) list_of_tags = resp.get("Snapshots", [{}])[0].get("Tags", []) return { tag["Key"].strip(): tag.get("Value", "").strip() for tag in list_of_tags }, True except Exception as ex: if getattr(ex, "response", {}).get("Error", {}).get("Code", "") == "InvalidSnapshot.NotFound": return {}, False else: raise ex def mark_as_being_selected_for_copy(client, snapshot): try: tag_name = Ec2CopySnapshotAction.marker_tag_copied_to( task[handlers.TASK_NAME]) # Serial number for copy. This is stored in the tag of the snapshot anf the stored resources in the task # Before starting a copy there will be check if these match to avoid double copied of a snapshot copy_serial = str(uuid.uuid4()) tag_data = { tag_name: safe_json({ TAG_REGION: task.get(handlers.TASK_PARAMETERS, {}).get(PARAM_DESTINATION_REGION, ""), COPY_SERIAL_NUMBER: copy_serial, TAG_COPY_SNAPSHOT_ID: "" }) } client.create_tags_with_retries( Resources=[snapshot["SnapshotId"]], Tags=tag_key_value_list(tag_data)) # store the copy serial number as part of the selected resource resource[COPY_SERIAL_NUMBER] = copy_serial except Exception as ex: logger.warning(WARN_SETTING_COPIED_TAG, snapshot["SnapshotId"], ex) # source snapshot snapshot_id = resource["SnapshotId"] # owner of the snapshot snapshot_owner = resource["OwnerId"] parameters = task.get(handlers.TASK_PARAMETERS, {}) # copy owned, shared or both copied_snapshot_types = parameters[PARAM_COPIED_SNAPSHOTS] this_account = task.get(TASK_THIS_ACCOUNT, False) accounts = task.get(TASK_ACCOUNTS, []) if this_account and len(accounts) == 0: account = os.getenv(handlers.ENV_OPS_AUTOMATOR_ACCOUNT) elif not this_account and len(accounts) == 1: account = accounts[0] else: account = services.account_from_role_arn(task_assumed_role) if copied_snapshot_types == COPIED_OWNED_BY_ACCOUNT and account != snapshot_owner: logger.debug(DEBUG_ONLY_COPY_OWNED_SNAPSHOTS, snapshot_id, snapshot_owner, PARAM_COPIED_SNAPSHOTS, account) return None if copied_snapshot_types == COPIED_SNAPSHOTS_SHARED_TO_ACCOUNT and account == snapshot_owner: logger.debug(DEBUG_ONLY_COPY_SHARED_SNAPSHOTS, snapshot_id, snapshot_owner, PARAM_COPIED_SNAPSHOTS, account) return None copy_from_accounts = parameters.get(PARAM_COPY_FROM_OWNER_ACCOUNTS, None) if copy_from_accounts not in [None, []]: if copied_snapshot_types == COPIED_OWNED_BY_ACCOUNT: raise_value_error(ERR_ACCOUNTS_BUT_NOT_SHARED, PARAM_COPY_FROM_OWNER_ACCOUNTS, PARAM_COPIED_SNAPSHOTS) if snapshot_owner != account and snapshot_owner not in [ a.strip() for a in copy_from_accounts ]: logger.debug(DEBUG_SHARED_SNAPSHOT_OWNER_NOT_IN_LIST, snapshot_id, snapshot_owner, ",".join(copy_from_accounts)) return None # name of tag that is used to mark snapshots being copied copied_tag_name = Ec2CopySnapshotAction.marker_tag_copied_to( task[handlers.TASK_NAME]) if copied_tag_name in resource.get("Tags", {}): # noinspection PyBroadException try: logger.debug( "Snapshot already copied or being copied, copy data is:\n {}", safe_json( json.loads( resource.get("Tags", {}).get(copied_tag_name, {})))) except Exception: pass return None # ec2 client for getting most current tag values and setting tags ec2 = get_client_with_retries( service_name="ec2", methods=["create_tags", "describe_snapshots"], region=resource["Region"], context=context, session=service.session, logger=logger) # get the most current tags as they might be changed by overlapping copy tasks tags, snapshot_found = get_snapshot_tags(ec2, snapshot_id) # snapshot no longer there if not snapshot_found: logger.debug("Snapshot {} not longer available", snapshot_id) return None if copied_tag_name in tags: return None mark_as_being_selected_for_copy(ec2, resource) return resource # noinspection PyUnusedLocal @staticmethod def action_validate_parameters(parameters, task_settings, logger): valid_regions = services.get_session().get_available_regions( "ec2", "aws") region = parameters.get(PARAM_DESTINATION_REGION) if region not in valid_regions: raise_value_error(ERR_INVALID_DESTINATION_REGION, region, ",".join(valid_regions)) if parameters.get(PARAM_DELETE_AFTER_COPY, False) and parameters.get( PARAM_COPIED_SNAPSHOTS) != COPIED_OWNED_BY_ACCOUNT: raise_value_error(ERR_CANNOT_DELETE_SHARED_SNAPSHOTS) kms_key_id = parameters.get(PARAM_KMS_KEY_ID, None) if not parameters[PARAM_ENCRYPTED] and kms_key_id not in ["", None]: raise_value_error(ERR_KMS_KEY_ONLY_IF_ENCRYPTED, PARAM_KMS_KEY_ID) if kms_key_id not in ["", None]: if re.match(KMS_KEY_ID_PATTERN, kms_key_id) is None: raise_value_error(ERR_INVALID_KMS_ID_ARN, kms_key_id) destination_region = parameters[PARAM_DESTINATION_REGION] if kms_key_id.split(":")[3] != destination_region: raise_value_error(ERR_KMS_KEY_NOT_IN_REGION, kms_key_id, destination_region) return parameters @staticmethod def action_logging_subject(arguments, _): snapshot = arguments[ACTION_PARAM_RESOURCES] account = snapshot["AwsAccount"] snapshot_id = snapshot["SnapshotId"] region = snapshot["Region"] return "{}-{}-{}-{}".format(account, region, snapshot_id, log_stream_date()) @staticmethod def action_concurrency_key(arguments): # copies per account/destination return "ec2:CopySnapshot:{}:{}".format( arguments[ACTION_PARAM_ACCOUNT], arguments[PARAM_DESTINATION_REGION]) @property def ec2_destination_client(self): if self._ec2_destination_client is None: methods = [ "copy_snapshot", "create_tags", "delete_tags", "modify_snapshot_attribute" ] self._ec2_destination_client = get_client_with_retries( "ec2", methods=methods, region=self._destination_region_, context=self._context_, session=self._session_, logger=self._logger_) return self._ec2_destination_client @property def ec2_source_client(self): if self._ec2_source_client is None: methods = ["create_tags", "delete_tags", "delete_snapshot"] self._ec2_source_client = get_client_with_retries( "ec2", methods=methods, region=self.source_region, context=self._context_, session=self._session_, logger=self._logger_) return self._ec2_source_client def __init__(self, action_args, action_parameters): self._destination_region_ = None ActionBase.__init__(self, action_args, action_parameters) # debug and dryrun self.snapshot = self._resources_ # snapshot source and destination information self.source_snapshot_id = self.snapshot["SnapshotId"] self.source_region = self.snapshot["Region"] self.owner = self.snapshot.get("OwnerId", "") self.encrypted = self.get(PARAM_ENCRYPTED, False) self.kms_key_id = self.get(PARAM_KMS_KEY_ID, None) self.accounts_with_create_permissions = self.get( PARAM_ACCOUNTS_VOLUME_CREATE_PERMISSIONS, []) self.delete_after_copy = self.get(PARAM_DELETE_AFTER_COPY, False) # filter for copied tags from source snapshot self.copied_volume_tagfiter = TagFilterSet( self.get(PARAM_COPIED_SNAPSHOT_TAGS, "")) self.tag_snapshots_in_shared_accounts = self.get( PARAM_TAG_IN_DESTINATION_ACCOUNT, False) self.tag_snapshots_in_source_account = self.get( PARAM_TAG_IN_SOURCE_ACCOUNT, False) # tagging roles self.dest_account_snapshot_tagging_rolename = self.get( PARAM_DESTINATION_ACCOUNT_TAG_ROLENAME, "") self.source_account_tagging_role_name = self.get( PARAM_SOURCE_ACCOUNT_TAG_ROLE_NAME, "") volume_id = self.snapshot["VolumeId"] if volume_id == DUMMY_VOLUME_IF_FOR_COPIED_SNAPSHOT: volume_from_tag = self.snapshot.get("Tags", {}).get( actions.marker_snapshot_tag_source_source_volume_id(), None) if volume_from_tag is not None: volume_id = volume_from_tag self.source_volume_id = volume_id self._ec2_destination_client = None self._ec2_source_client = None # setup result with known values self.result = { "account": self._account_, "task": self._task_, "destination-region": self._destination_region_, "source-region": self.source_region, "source-snapshot-id": self.source_snapshot_id, "encrypted": self.encrypted, "kms-id": self.kms_key_id if self.kms_key_id is not None else "" } def is_completed(self, snapshot_create_data): def delete_source_after_copy(): self._logger_.info(INF_DELETING_SNAPSHOT, self.source_snapshot_id) self.ec2_source_client.delete_snapshot_with_retries( SnapshotId=self.source_snapshot_id) self._logger_.info(INF_SNAPSHOT_DELETED, self.source_snapshot_id, self.source_region) def source_tags(copy_id, source_tags_param): snapshot_tags = {} snapshot_tags.update( self.build_tags_from_template( parameter_name=source_tags_param, region=self.source_region, tag_variables={ TAG_PLACEHOLDER_COPIED_SNAPSHOT_ID: copy_id, TAG_PLACEHOLDER_COPIED_REGION: self._destination_region_ })) return snapshot_tags def set_source_snapshot_tags(copy_id): snapshot_tags = source_tags(copy_id, PARAM_SOURCE_TAGS) if len(snapshot_tags) == 0: return self._logger_.info(INF_CREATE_SOURCE_TAGS, snapshot_tags, self._account_) if len(snapshot_tags) > 0: tagging.set_ec2_tags(ec2_client=self.ec2_source_client, resource_ids=[self.source_snapshot_id], tags=snapshot_tags, logger=self._logger_) self._logger_.info(INF_TAGS_CREATED) def grant_create_volume_permissions(snap_id): if self.accounts_with_create_permissions is not None and len( self.accounts_with_create_permissions) > 0: args = { "CreateVolumePermission": { "Add": [{ "UserId": a.strip() } for a in self.accounts_with_create_permissions] }, "SnapshotId": snap_id } try: self.ec2_destination_client.modify_snapshot_attribute_with_retries( **args) self._logger_.info( INF_SETTING_CREATE_VOLUME_PERMISSIONS, ", ".join(self.accounts_with_create_permissions)) except Exception as ex: raise_exception(ERR_SETTING_CREATE_VOLUME_PERMISSIONS, self.accounts_with_create_permissions, ex) def tag_shared_snapshots(tags, snap_id): # creates tags for snapshots that have been shared in account the snapshots are shared with if len(tags) == 0 or not self.tag_snapshots_in_shared_accounts: return if self.accounts_with_create_permissions in ["", None]: return for account in self.accounts_with_create_permissions: session_for_tagging = self.get_action_session( account=account, param_name=PARAM_DESTINATION_ACCOUNT_TAG_ROLENAME, logger=self._logger_) if session_for_tagging is None: self._logger_.error(ERR_TAGS_NOT_SET_IN_ACCOUNT, account) continue try: ec2_client = get_client_with_retries( service_name="ec2", methods=["create_tags", "delete_tags"], context=self._context_, region=self.get(PARAM_DESTINATION_REGION), session=session_for_tagging, logger=self._logger_) tagging.set_ec2_tags(ec2_client=ec2_client, resource_ids=[snap_id], tags=tags, logger=self._logger_) self._logger_.info(INF_CREATE_SHARED_TAGS, tags, account) except Exception as ex: raise_exception(ERR_SETTING_SHARED_TAGS, account, str(ex)) def tag_shared_source_snapshot(copy_id): # created tags for snapshots for shared snapshots in the source account of the shares snapshots snapshot_tags = source_tags(copy_id, PARAM_SOURCE_SHARED_BY_TAGS) if len(snapshot_tags ) == 0 or not self.tag_snapshots_in_source_account: return # only for snapshots that have been shared by other account if self.owner == self.get_account_for_task(): self._logger_.debug( "Account {} is owner, no tags set for snapshot {} in account of owner", self._account_, self.source_snapshot_id) return session_for_tagging = self.get_action_session( account=self.owner, param_name=PARAM_SOURCE_ACCOUNT_TAG_ROLE_NAME, logger=self._logger_) if session_for_tagging is None: self._logger_.error(ERR_TAGS_NOT_SET_IN_ACCOUNT, self.owner) return try: self._logger_.info(INF_CREATE_SHARED_ACCOUNT_SNAPSHOT_TAGS, snapshot_tags, self.source_snapshot_id, self.owner) ec2_client = get_client_with_retries( service_name="ec2", methods=["create_tags", "delete_tags"], context=self._context_, region=self.source_region, session=session_for_tagging, logger=self._logger_) tagging.set_ec2_tags(ec2_client=ec2_client, resource_ids=[self.source_snapshot_id], tags=snapshot_tags, logger=self._logger_) except Exception as ex: raise_exception(ERR_SETTING_SOURCE_SHARED_TAGS, self.owner, str(ex)) if snapshot_create_data.get("already-copied", False): self._logger_.info(INF_COMPLETE_ALREADY_COPIED, self.source_snapshot_id) return self.result if snapshot_create_data.get("not-longer-available", False): self._logger_.info(INF_COMPLETED_NOT_LONGER_AVAILABLE, self.source_snapshot_id) return self.result # create service instance to test if snapshot exists ec2 = services.create_service( "ec2", session=self._session_, service_retry_strategy=get_default_retry_strategy( "ec2", context=self._context_)) copy_snapshot_id = snapshot_create_data["copy-snapshot-id"] # test if the snapshot with the id that was returned from the CopySnapshot API call exists and is completed copied_snapshot = ec2.get(services.ec2_service.SNAPSHOTS, region=self._destination_region_, OwnerIds=["self"], Filters=[{ "Name": "snapshot-id", "Values": [copy_snapshot_id] }]) if copied_snapshot is not None: self._logger_.debug(INF_CHECK_COMPLETED_RESULT, copied_snapshot) state = copied_snapshot[ "State"] if copied_snapshot is not None else None if copied_snapshot is None or state == SNAPSHOT_STATE_PENDING: self._logger_.info(INF_COPY_PENDING, copy_snapshot_id, self._destination_region_) return None if state == SNAPSHOT_STATE_ERROR: copied_tag_name = Ec2CopySnapshotAction.marker_tag_copied_to( self._task_) self.ec2_source_client.delete_tags_with_retries( Resources=[self.source_snapshot_id], Tags=[{ "Key": copied_tag_name }]) raise_exception(ERR_COPY_SNAPSHOT) if state == SNAPSHOT_STATE_COMPLETED: self._logger_.info(INF_COPY_COMPLETED, self.source_snapshot_id, self.source_region, copy_snapshot_id, self._destination_region_) grant_create_volume_permissions(copy_snapshot_id) tag_shared_snapshots(snapshot_create_data.get("tags", {}), copy_snapshot_id) tag_shared_source_snapshot(copy_snapshot_id) if self.delete_after_copy: delete_source_after_copy() else: set_source_snapshot_tags(copy_snapshot_id) # wait there for 15 seconds as count the limit for max number of concurrent snapshot copies # by the EC2 service is sometimes delayed time.sleep(5) return copied_snapshot return None def execute(self): def get_tags_for_copied_snapshot(): snapshot_tags = ( self.copied_volume_tagfiter.pairs_matching_any_filter( self.snapshot.get("Tags", {}))) snapshot_tags[actions.marker_snapshot_tag_source_source_volume_id( )] = self.source_volume_id snapshot_tags.update( self.build_tags_from_template( parameter_name=PARAM_SNAPSHOT_TAGS, region=self.source_region, tag_variables={ TAG_PLACEHOLDER_SOURCE_SNAPSHOT_ID: self.source_snapshot_id, TAG_PLACEHOLDER_SOURCE_REGION: self.source_region, TAG_PLACEHOLDER_OWNER_ACCOUNT: self.owner, TAG_PLACEHOLDER_SOURCE_VOLUME: self.source_volume_id })) snapshot_tags[Ec2CopySnapshotAction.marker_tag_source_snapshot_id( )] = self.source_snapshot_id snapshot_tags[actions.marker_snapshot_tag_source_source_volume_id( )] = self.source_volume_id return snapshot_tags def get_source_snapshot(): ec2 = services.create_service( "ec2", session=self._session_, service_retry_strategy=get_default_retry_strategy( "ec2", context=self._context_)) snapshot = ec2.get(services.ec2_service.SNAPSHOTS, region=self.source_region, RestorableByUserIds=["self"], Filters=[{ "Name": "snapshot-id", "Values": [self.source_snapshot_id] }]) return snapshot def should_copy_snapshot(): snapshot = get_source_snapshot() # source snapshot was already deleted by tasks that were in wait for execution list if snapshot is None: self.result["not-longer-available"] = True self._logger_.info(INF_WARNING_NO_SNAPSHOT, self.source_snapshot_id) return False # get tags from the snapshot, these must have contain the mark_as_copied tag and this tag must contain the same # copy serial number as the snapshot that was in the selected resource for this task instance source_snapshot_tags = snapshot.get( "Tags", {}) if snapshot is not None else {} marked_as_copied_tag = Ec2CopySnapshotAction.marker_tag_copied_to( self._task_) if marked_as_copied_tag in source_snapshot_tags: snapshot_copy_data = json.loads( source_snapshot_tags[marked_as_copied_tag]) else: snapshot_copy_data = {} if snapshot_copy_data.get( COPY_SERIAL_NUMBER, "") != self.snapshot.get(COPY_SERIAL_NUMBER): self._logger_.info(INF_COPIED_BY_OTHER, snapshot_copy_data.get(TAG_REGION, ""), snapshot_copy_data(COPY_SERIAL_NUMBER, "")) self.result["already-copied"] = True self.result["copied-data"] = snapshot_copy_data return False return True # logged information self._logger_.info("{}, version {}", self.properties[ACTION_TITLE], self.properties[ACTION_VERSION]) self._logger_.info(INF_ACCOUNT_SNAPSHOT, self.source_snapshot_id, self._account_, self.source_region, self._destination_region_) self._logger_.debug("Snapshot : {}", self.snapshot) boto_call = "copy_snapshot" try: # setup argument for CopySnapshot call args = { "SourceRegion": self.source_region, "SourceSnapshotId": self.source_snapshot_id } if not should_copy_snapshot(): return self.result if self.encrypted: args["Encrypted"] = True self.result["encrypted"] = True if self.kms_key_id not in ["", None]: args["KmsKeyId"] = self.kms_key_id if self._dryrun_: args["DryRun"] = True source_description = self.snapshot.get("Description", "") description_variables = { TAG_PLACEHOLDER_SOURCE_SNAPSHOT_ID: self.source_snapshot_id, TAG_PLACEHOLDER_SOURCE_REGION: self.source_region, TAG_PLACEHOLDER_OWNER_ACCOUNT: self.owner, TAG_PLACEHOLDER_SOURCE_VOLUME: self.source_volume_id, TAG_PLACEHOLDER_SOURCE_DESCRIPTION: source_description } args["Description"] = self.build_str_from_template( parameter_name=PARAM_SNAPSHOT_DESCRIPTION, region=self.source_region, tag_variables=description_variables) if args["Description"] == "": args["Description"] = source_description # start the copy resp = self.ec2_destination_client.copy_snapshot_with_retries( **args) # id of the copy copy_snapshot_id = resp.get("SnapshotId") self._logger_.info(INF_SNAPSHOT_COPIED, self.source_snapshot_id, self._destination_region_, copy_snapshot_id) self.result[boto_call] = resp self.result["copy-snapshot-id"] = copy_snapshot_id # update the tag that marks the snapshot as being copied boto_call = "create_tags (source)" copied_tag_name = Ec2CopySnapshotAction.marker_tag_copied_to( self._task_) copy_data_tag = { copied_tag_name: safe_json({ TAG_REGION: self._destination_region_, COPY_SERIAL_NUMBER: self.snapshot.get(COPY_SERIAL_NUMBER, ""), TAG_COPIED_BY_TASK: self.get(ACTION_PARAM_TASK_ID, ""), TAG_COPY_SNAPSHOT_ID: copy_snapshot_id }) } self.ec2_source_client.create_tags_with_retries( Resources=[self.source_snapshot_id], Tags=tag_key_value_list(copy_data_tag)) # set tags on the copy boto_call = "create_tags (target)" tags = get_tags_for_copied_snapshot() self._logger_.info(INF_CREATE_COPIED_TAGS, tags, self._account_) if len(tags) > 0: tagging.set_ec2_tags(ec2_client=self.ec2_destination_client, resource_ids=[copy_snapshot_id], tags=tags, logger=self._logger_) self.result["tags"] = tags self._logger_.info(INF_TAGS_CREATED) except Exception as ex: if self._dryrun_: self._logger_.debug(str(ex)) self.result[boto_call] = str(ex) return self.result else: raise ex self.result[METRICS_DATA] = build_action_metrics(self, CopiedSnapshots=1) return self.result
def service_regions(self): """ Regions that can be used for this service, return all AWS regions (assuming they all support EC2) :return: Service regions """ return services.get_session().get_available_regions(service_name="ec2")
def _get_tags_for_resource(self, client, resource): """ Returns the tags for specific resources that require additional boto calls to retrieve their tags. :param client: Client that can be used to make the boto call to retrieve the tags :param resource: The resource for which to retrieve the tags :return: Tags """ # get the name of the proprty that holds the arn of the resource arn_property_name = "{}Arn".format(self._resource_name[0:-1]) if arn_property_name[0:2].lower() == "db": arn_property_name = "DB{}".format(arn_property_name[2:]) # get the arn of the resource resource_arn = resource[arn_property_name] # owner of the resource (could be other account for shared sbapshots) resource_owner_account = resource_arn.split(":")[4] resource_region = resource_arn.split(":")[3] if resource_owner_account == self.aws_account: # sane account, can use same session as used to retrieve the resource if self._use_cached_tags: self._tag_session = self.session # make sure the client has retries if getattr(self._service_client, "list_tags_for_resource" + boto_retry.DEFAULT_SUFFIX, None) is None: boto_retry.make_method_with_retries(boto_client_or_resource=client, name="list_tags_for_resource", service_retry_strategy=self._service_retry_strategy) self._tag_rds_client = client else: # resource is from other account, get a session to get the tags from that account as these are not # visible for shared rds resources if self._tag_account != resource_owner_account or self._tag_session is None: self._tag_account = resource_owner_account used_tag_role = None if self._tag_roles is not None: # see if there is a role for the owner account for role in self._tag_roles: if role is not None and services.account_from_role_arn(role) == resource_owner_account: used_tag_role = role break else: # if there is no role and the account is the ops automator account use the default role # in other cases it is not possible to retrieve the tags if resource_owner_account != os.getenv(handlers.ENV_OPS_AUTOMATOR_ACCOUNT): return {} self._tag_session = services.get_session(role_arn=used_tag_role) if not self._use_cached_tags: self._tag_rds_client = boto_retry.get_client_with_retries("rds", methods=["list_tags_for_resource"], context=self._context, region=resource_region) if self._use_cached_tags: return self.cached_tags(session=self._tag_session, resource_name=RESOURCES_WITH_TAGS[resource["ResourceTypeName"]], region=resource_region).get(resource_arn, {}) try: resp = self._tag_rds_client.list_tags_for_resource_with_retries(ResourceName=resource_arn) return resp.get("TagList", []) except botocore.exceptions.ClientError as ex: if getattr(ex, "response", {}).get("Error", {}).get("Code", "") == "InvalidParameterValue": return [] raise_exception("Can not list rds tags for resource {}, {}", resource_arn, ex)
def handle_request(self, use_custom_select=True): """ Handled the cloudwatch rule timer event :return: Started tasks, if any, information """ try: self._logger.info("Handling CloudWatch event {}", safe_json(self._event, indent=3)) result = [] start = datetime.now() dt = self._event_time() config_task = None source_resource_tags = None try: # for all events tasks in configuration for config_task in TaskConfiguration( context=self._context, logger=self._logger).get_tasks(): self._logger.debug_enabled = config_task.get( handlers.TASK_DEBUG, False) if not self._event_triggers_task(task=config_task): continue # tasks that can react to events with a wider resource scope than the actual resource causing the event may # have a filter that can is used to filter based on the tags of the resource event_source_tag_filter = config_task.get( handlers.TASK_EVENT_SOURCE_TAG_FILTER, None) if event_source_tag_filter is not None: if source_resource_tags is None: # get the tags for the source resource of the event session = services.get_session( self._role_executing_triggered_task, logger=self._logger) if session is None: self._logger.error( ERR_NO_SESSION_FOR_GETTING_TAGS) continue try: source_resource_tags = self._source_resource_tags( session, config_task) except Exception as ex: self._logger.error( ERR_GETTING_EVENT_SOURCE_RESOURCE_TAGS, ex) continue self._logger.debug( "Tags for event source resource are {}", source_resource_tags) # apply filter to source resource tags if not TagFilterExpression( event_source_tag_filter).is_match( source_resource_tags): self._logger.debug( "Tags of source resource do not match tag filter {}", event_source_tag_filter) continue task_name = config_task[handlers.TASK_NAME] result.append(task_name) select_parameters = self._select_parameters( self._event_name(), config_task) if select_parameters is None: continue self._logger.debug(DEBUG_EVENT, task_name, self._event_name(), select_parameters, self._event_account(), self._event_region(), safe_json(config_task, indent=3)) # create an event for lambda function that scans for resources for this task lambda_event = { handlers.HANDLER_EVENT_ACTION: handlers.HANDLER_ACTION_SELECT_RESOURCES, handlers.HANDLER_EVENT_CUSTOM_SELECT: use_custom_select, handlers.HANDLER_SELECT_ARGUMENTS: { handlers.HANDLER_EVENT_REGIONS: [self._event_region()], handlers.HANDLER_EVENT_ACCOUNT: self._event_account(), handlers.HANDLER_EVENT_RESOURCE_NAME: config_task[handlers.TASK_RESOURCE_TYPE], }, handlers.HANDLER_EVENT_SOURCE: "{}:{}:{}".format(self._handled_event_source, self._handled_detail_type, self._event_name()), handlers.HANDLER_EVENT_TASK: config_task, handlers.HANDLER_EVENT_TASK_DT: dt } for i in select_parameters: lambda_event[handlers.HANDLER_SELECT_ARGUMENTS][ i] = select_parameters[i] if self._event_resources() is not None: self._logger.debug( DEBUG_EVENT_RESOURCES, safe_json(self._event_resources(), indent=3)) lambda_event[ handlers. HANDLER_SELECT_RESOURCES] = self._event_resources( ) if not handlers.running_local(self._context): # start lambda function to scan for task resources payload = str.encode(safe_json(lambda_event)) client = get_client_with_retries("lambda", ["invoke"], context=self._context, logger=self._logger) client.invoke_with_retries( FunctionName=self._context.function_name, InvocationType="Event", LogType="None", Payload=payload) else: # or if not running in lambda environment pass event to main task handler lambda_handler(lambda_event, None) return safe_dict({ "datetime": datetime.now().isoformat(), "running-time": (datetime.now() - start).total_seconds(), "event-datetime": dt, "started-tasks": result }) except ValueError as ex: self._logger.error(ERR_HANDLING_EVENT_IN_BASE_HANDLER, ex, safe_json(config_task, indent=2)) finally: self._logger.flush()
def sqs_client(self): if self._sqs_client is None: self._sqs_client = services.get_session().client("sqs") return self._sqs_client
def region(self): return self._args[ "region"] if "region" in self._args else services.get_session( ).region_name
def _get_tasks_to_execute(self): task_items = self._build_tasks_for_selected_resources() for item in task_items: event = {i: item.get(i) for i in item} event[handlers. HANDLER_EVENT_ACTION] = handlers.HANDLER_ACTION_EXECUTE event[actions.ACTION_SERVICE] = actions.get_action_properties( item[handlers.TASK_TR_ACTION]).get(actions.ACTION_SERVICE) action_argument = { actions.ACTION_PARAM_CONTEXT: self.context, actions.ACTION_PARAM_EVENT: event, actions.ACTION_PARAM_SESSION: services.get_session( role_arn=item.get(handlers.TASK_TR_ASSUMED_ROLE, None)), actions.ACTION_PARAM_RESOURCES: handlers.get_item_resource_data(item, context=self.context), actions.ACTION_PARAM_DEBUG: item[handlers.TASK_TR_DEBUG], actions.ACTION_PARAM_DRYRUN: item[handlers.TASK_TR_DRYRUN], actions.ACTION_PARAM_TASK_ID: item[handlers.TASK_TR_ID], actions.ACTION_PARAM_TASK: item[handlers.TASK_TR_NAME], actions.ACTION_PARAM_TASK_TIMEZONE: item[handlers.TASK_TR_TIMEZONE], actions.ACTION_PARAM_STACK: self.action_stack.stack_name if self.action_stack is not None else None, actions.ACTION_PARAM_STACK_ID: self.action_stack.stack_id if self.action_stack is not None else None, actions.ACTION_PARAM_STACK_RESOURCES: self.action_stack_resources, actions.ACTION_PARAM_ASSUMED_ROLE: item.get(handlers.TASK_TR_ASSUMED_ROLE), actions.ACTION_PARAM_STARTED_AT: item[handlers.TASK_TR_STARTED_TS], actions.ACTION_PARAM_TAGFILTER: item[handlers.TASK_TR_TAGFILTER], actions.ACTION_PARAM_TIMEOUT: item[handlers.TASK_TR_TIMEOUT], actions.ACTION_PARAM_LOGGER: self.logger, actions.ACTION_PARAM_EVENTS: self._events, actions.ACTION_PARAM_HAS_COMPLETION: item[handlers.TASK_TR_HAS_COMPLETION], actions.ACTION_PARAM_INTERVAL: item[handlers.TASK_INTERVAL] } if self._assumed_role is None: self._assumed_role = action_argument[ actions.ACTION_PARAM_ASSUMED_ROLE] action_instance = self.action_class( action_argument, item.get(handlers.TASK_TR_PARAMETERS, {})) self.verify_log_subject(action_argument) self.verify_concurrency(action_argument, item) yield action_instance