diff --git a/CHANGELOG.txt b/CHANGELOG.txt index f8cbb0ea..e2fd503f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,6 +2,7 @@ - RD-4434 Nics for Spot Fleet Requests - RD-2009 EC2 base properties improvement - Fix for EFS + - Fix for ECS 3.0.1: - RD-3458 fix iam blueprint & iam status 3.0.0: diff --git a/cloudify_aws/autoscaling/resources/autoscaling_group.py b/cloudify_aws/autoscaling/resources/autoscaling_group.py index de5ce7d6..ed81946e 100644 --- a/cloudify_aws/autoscaling/resources/autoscaling_group.py +++ b/cloudify_aws/autoscaling/resources/autoscaling_group.py @@ -17,11 +17,10 @@ AWS Autoscaling Group interface """ -# Standard imports -from re import sub # Third party imports -from cloudify.exceptions import OperationRetry +from cloudify.exceptions import OperationRetry, NonRecoverableError + from cloudify_aws.common import decorators, utils from cloudify_aws.autoscaling import AutoscalingBase @@ -148,26 +147,7 @@ def create(ctx, iface, resource_config, params, **_): INSTANCE_TYPE) params[INSTANCE_ID] = instance_id - subnet_list_string = params.get(SUBNET_LIST) - subnet_list = \ - sub("[^\w]", " ", subnet_list_string).split() if \ - subnet_list_string else [] - - subnet_list = \ - utils.add_resources_from_rels( - ctx.instance, - SUBNET_TYPE, - subnet_list) - subnet_list = \ - utils.add_resources_from_rels( - ctx.instance, - SUBNET_TYPE_DEPRECATED, - subnet_list) - - if subnet_list: - # Remove any duplicate items from subnet list - subnet_list = list(set(subnet_list)) - params[SUBNET_LIST] = ', '.join(subnet_list) + get_subnet_list(ctx.instance, params) # Actually create the resource if not iface.resource_id: @@ -181,9 +161,7 @@ def create(ctx, iface, resource_config, params, **_): @decorators.aws_resource(AutoscalingGroup, RESOURCE_TYPE) -def stop(iface, - resource_config, - **_): +def stop(iface, resource_config, **_): """Stops all instances associated with Autoscaling group.""" autoscaling_group = iface.properties @@ -232,3 +210,28 @@ def delete(iface, resource_config, **_): [instance.get(INSTANCE_ID) for instance in instances]}) iface.delete(params) + + +def get_subnet_list(ctx_instance, params): + subnet_list = params.get(SUBNET_LIST) + subnet_list = subnet_list or [] + if not isinstance(subnet_list, list): + raise NonRecoverableError( + 'The provided {} is not a list, ' + 'please reformat. Provided value: {}'.format( + SUBNET_LIST, subnet_list)) + + subnet_list = \ + utils.add_resources_from_rels( + ctx_instance, + SUBNET_TYPE, + subnet_list) + subnet_list = \ + utils.add_resources_from_rels( + ctx_instance, + SUBNET_TYPE_DEPRECATED, + subnet_list) + if subnet_list: + # Remove any duplicate items from subnet list + subnet_list = list(set(subnet_list)) + params[SUBNET_LIST] = ', '.join(subnet_list) diff --git a/cloudify_aws/autoscaling/resources/launch_configuration.py b/cloudify_aws/autoscaling/resources/launch_configuration.py index 31b5e962..edcc01b1 100644 --- a/cloudify_aws/autoscaling/resources/launch_configuration.py +++ b/cloudify_aws/autoscaling/resources/launch_configuration.py @@ -168,4 +168,4 @@ def delete(iface, resource_config, **_): params = dict() if not resource_config else resource_config.copy() if RESOURCE_NAME not in params: params.update({RESOURCE_NAME: iface.resource_id}) - iface.delete(params) + utils.handle_response(iface, 'delete', params, ['not found']) diff --git a/cloudify_aws/common/decorators.py b/cloudify_aws/common/decorators.py index 2617637d..b9ac3dab 100644 --- a/cloudify_aws/common/decorators.py +++ b/cloudify_aws/common/decorators.py @@ -425,12 +425,19 @@ def _aws_resource(function, aws_config = ctx.instance.runtime_properties.get('aws_config') aws_config_kwargs = kwargs.get('aws_config') + resource_name = None + if 'cloudify.nodes.aws.elb.LoadBalancer' in ctx.node.type_hierarchy: + resource_name = ctx.node.properties.get( + 'resource_config', {}).get('Name') + # Attribute needed for AWS resource class class_decl_attr = { 'ctx_node': ctx.node, 'logger': ctx.logger, 'resource_id': utils.get_resource_id( - node=ctx.node, instance=ctx.instance), + node=ctx.node, + instance=ctx.instance, + resource_name=resource_name), } _put_aws_config_in_class_decl( diff --git a/cloudify_aws/common/utils.py b/cloudify_aws/common/utils.py index c9256037..28d3eca0 100644 --- a/cloudify_aws/common/utils.py +++ b/cloudify_aws/common/utils.py @@ -622,9 +622,11 @@ def handle_response(iface, :param raisable: The exception to raise if substrings are found. :return: """ + exit_substrings = exit_substrings or [] if isinstance(exit_substrings, text_type): exit_substrings = [exit_substrings] + raise_substrings = raise_substrings or [] if isinstance(raise_substrings, text_type): raise_substrings = [raise_substrings] diff --git a/cloudify_aws/ec2/resources/routetable.py b/cloudify_aws/ec2/resources/routetable.py index 243420a0..f401a31d 100644 --- a/cloudify_aws/ec2/resources/routetable.py +++ b/cloudify_aws/ec2/resources/routetable.py @@ -19,6 +19,7 @@ from time import sleep # Boto +from botocore.exceptions import ClientError from cloudify.exceptions import OperationRetry # Cloudify @@ -76,13 +77,26 @@ def create(self, params): ''' return self.make_client_call('create_route_table', params) - def delete(self, params=None): + def delete(self, params=None, recurse=True): ''' Deletes an existing AWS EC2 Route Table. ''' self.logger.debug('Deleting %s with parameters: %s' % (self.type_name, params)) - res = self.client.delete_route_table(**params) + try: + res = self.client.delete_route_table(**params) + except ClientError: + if not recurse: + raise + self.logger.error( + 'Failed to delete route table because of dependencies. ' + 'Attempting cleanup.') + for a in self.properties.get('Associations', []): + if not a.get('Main'): + self.logger.info('Disassociating: {}'.format(a)) + self.client.disassociate_route_table( + AssociationId=a.get('RouteTableAssociationId')) + res = self.delete(params, False) self.logger.debug('Response: %s' % res) return res diff --git a/cloudify_aws/ec2/resources/securitygroup.py b/cloudify_aws/ec2/resources/securitygroup.py index a7435b0e..d62cb28a 100644 --- a/cloudify_aws/ec2/resources/securitygroup.py +++ b/cloudify_aws/ec2/resources/securitygroup.py @@ -122,7 +122,7 @@ def wait(self): max_wait = 5 counter = 0 while not self.properties: - self.logger.debug('Waiting for Route Table to be created.') + self.logger.debug('Waiting for Security Group to be created.') sleep(5) if max_wait > counter: break @@ -136,7 +136,9 @@ def prepare(ctx, iface, resource_config, **_): ctx.instance.runtime_properties['resource_config'] = resource_config -@decorators.aws_resource(EC2SecurityGroup, RESOURCE_TYPE) +@decorators.aws_resource(EC2SecurityGroup, + RESOURCE_TYPE, + waits_for_status=False) @decorators.tag_resources def create(ctx, iface, resource_config, **_): '''Creates an AWS EC2 Security Group''' @@ -147,8 +149,6 @@ def create(ctx, iface, resource_config, **_): # Actually create the resource iface.create(params) utils.update_resource_id(ctx.instance, iface.resource_id) - utils.assign_create_response( - iface, ctx.instance.runtime_properties, iface.create_response) iface.wait() diff --git a/cloudify_aws/ecs/resources/cluster.py b/cloudify_aws/ecs/resources/cluster.py index f04cbdc1..56f77289 100644 --- a/cloudify_aws/ecs/resources/cluster.py +++ b/cloudify_aws/ecs/resources/cluster.py @@ -20,10 +20,7 @@ from __future__ import unicode_literals -# Boto - -from botocore.exceptions import ClientError, ParamValidationError - +from cloudify.exceptions import NonRecoverableError # Cloudify from cloudify_aws.common import decorators, utils from cloudify_aws.ecs import ECSBase @@ -41,29 +38,30 @@ class ECSCluster(ECSBase): """ def __init__(self, ctx_node, resource_id=None, client=None, logger=None): ECSBase.__init__(self, ctx_node, resource_id, client, logger) + self._properties = {} self.type_name = RESOURCE_TYPE self.describe_cluster_filter = {} @property def properties(self): """Gets the properties of an external resource""" - try: - resources = \ - self.client.describe_clusters( - **self.describe_cluster_filter - ) - except (ParamValidationError, ClientError): - pass - else: - return None if not resources else resources.get(CLUSTERS)[0] + if not self._properties: + try: + resources = self.make_client_call( + 'describe_clusters', self.describe_cluster_filter) + except NonRecoverableError: + return + if CLUSTERS in resources: + for resource in resources[CLUSTERS]: + if resource.get(CLUSTER_RESOURCE_NAME) == self.resource_id: + self._properties = resource + return self._properties @property def status(self): """Gets the status of an external resource""" - props = self.properties - if not props: - return None - return props.get('status') + if self.properties: + return self.properties.get('status') def create(self, params): """ diff --git a/cloudify_aws/ecs/tests/test_cluster.py b/cloudify_aws/ecs/tests/test_cluster.py index 6c8a3c70..5267d257 100644 --- a/cloudify_aws/ecs/tests/test_cluster.py +++ b/cloudify_aws/ecs/tests/test_cluster.py @@ -34,7 +34,7 @@ class TestECSCluster(TestBase): def setUp(self): super(TestECSCluster, self).setUp() - self.cluster = ECSCluster("ctx_node", resource_id=True, + self.cluster = ECSCluster("ctx_node", resource_id='test_cluster_name', client=True, logger=None) self.mock_resource = patch( 'cloudify_aws.common.decorators.aws_resource', mock_decorator @@ -49,8 +49,8 @@ def tearDown(self): def test_class_properties(self): effect = self.get_client_error_exception(name=cluster.RESOURCE_TYPE) - self.cluster.client = self.make_client_function('describe_clusters', - side_effect=effect) + self.cluster.client = self.make_client_function( + 'describe_clusters', side_effect=effect) self.assertIsNone(self.cluster.properties) response = \ diff --git a/cloudify_aws/efs/resources/file_system.py b/cloudify_aws/efs/resources/file_system.py index cd64e8a0..2364b49a 100644 --- a/cloudify_aws/efs/resources/file_system.py +++ b/cloudify_aws/efs/resources/file_system.py @@ -115,4 +115,4 @@ def delete(ctx, iface, resource_config, **_): params[FILESYSTEM_ID] = iface.resource_id # Actually delete the resource - utils.handle_response(iface, 'delete', params, raise_substrings='') + utils.handle_response(iface, 'delete', params, raise_substrings=['']) diff --git a/cloudify_aws/elb/resources/listener.py b/cloudify_aws/elb/resources/listener.py index 65ccc0cf..d743e788 100644 --- a/cloudify_aws/elb/resources/listener.py +++ b/cloudify_aws/elb/resources/listener.py @@ -50,6 +50,7 @@ def __init__(self, ctx_node, resource_id=None, client=None, logger=None): resource_id, client or Boto3Connection(ctx_node).client('elbv2'), logger) + self._properties = {} self.type_name = RESOURCE_TYPE @property @@ -57,22 +58,23 @@ def properties(self): '''Gets the properties of an external resource''' if not self.resource_id: return - try: - resources = self.client.describe_listeners( - ListenerArns=[self.resource_id]) - except (ParamValidationError, ClientError): - pass - else: - return None \ - if not resources else resources['Listeners'][0] + if not self._properties: + try: + resources = self.client.describe_listeners( + ListenerArns=[self.resource_id]) + except (ParamValidationError, ClientError): + pass + else: + if 'Listeners' in resources: + for listener in resources['Listeners']: + if listener.get('ListenerArn') == self.resource_id: + self._properties = listener + return self._properties @property def status(self): '''Gets the status of an external resource''' - props = self.properties - if not props: - return None - return props['State']['Code'] + return self.properties def create(self, params): ''' diff --git a/cloudify_aws/elb/resources/load_balancer.py b/cloudify_aws/elb/resources/load_balancer.py index 183087dd..8f6643f5 100644 --- a/cloudify_aws/elb/resources/load_balancer.py +++ b/cloudify_aws/elb/resources/load_balancer.py @@ -62,13 +62,13 @@ def properties(self): if not self.resource_id: return if not self._properties: - res = self.get_describe_result( - {'LoadBalancerNames': [self.resource_id]}) + res = self.get_describe_result({'Names': [self.resource_id]}) + self.logger.info('Describe LB response: {}'.format(res)) + names = [] if 'LoadBalancers' in res: for elb in res['LoadBalancers']: lb_name = elb.get('LoadBalancerName') name = elb.get('Name') - names = [] if lb_name: names.append(lb_name) if name: @@ -195,6 +195,8 @@ def modify(ctx, iface, resource_config, **_): modify_params[LB_ATTR] = modify_params_attributes # Actually modify the resource attributes = iface.modify_attribute(modify_params) + if 'resource_config' not in ctx.instance.runtime_properties: + ctx.instance.runtime_properties['resource_config'] = {} ctx.instance.runtime_properties['resource_config'][LB_ATTR] = \ attributes diff --git a/cloudify_aws/elb/resources/rule.py b/cloudify_aws/elb/resources/rule.py index c049c778..82ad7695 100644 --- a/cloudify_aws/elb/resources/rule.py +++ b/cloudify_aws/elb/resources/rule.py @@ -49,6 +49,7 @@ def __init__(self, ctx_node, resource_id=None, client=None, logger=None): resource_id, client or Boto3Connection(ctx_node).client('elbv2'), logger) + self._properties = {} self.type_name = RESOURCE_TYPE @property @@ -56,22 +57,23 @@ def properties(self): '''Gets the properties of an external resource''' if not self.resource_id: return - try: - resources = self.client.describe_rules( - RuleArns=[self.resource_id]) - except (ParamValidationError, ClientError): - pass - else: - return None \ - if not resources else resources['Rules'][0] + if not self._properties: + try: + resources = self.client.describe_rules( + RuleArns=[self.resource_id]) + except (ParamValidationError, ClientError): + pass + else: + if 'Rules' in resources: + for rule in resources['Rules']: + if rule.get('RuleArn') == self.resource_id: + self._properties = rule + return self._properties @property def status(self): '''Gets the status of an external resource''' - props = self.properties - if not props: - return None - return props['State']['Code'] + return self.properties def create(self, params): ''' @@ -149,8 +151,8 @@ def create(ctx, iface, resource_config, **_): @decorators.aws_resource(ELBRule, RESOURCE_TYPE, - ignore_properties=True) -@decorators.wait_for_delete(status_pending=[]) + ignore_properties=True, + waits_for_status=False) def delete(iface, resource_config, **_): '''Deletes an AWS ELB rule''' iface.delete(resource_config) diff --git a/cloudify_aws/elb/resources/target_group.py b/cloudify_aws/elb/resources/target_group.py index 5c2593f8..b3f0849c 100644 --- a/cloudify_aws/elb/resources/target_group.py +++ b/cloudify_aws/elb/resources/target_group.py @@ -17,7 +17,7 @@ AWS ELB target group ''' # Third Party imports -from botocore.exceptions import ClientError, ParamValidationError +from cloudify.exceptions import NonRecoverableError # Local imports from cloudify_aws.common import decorators, utils @@ -45,28 +45,34 @@ def __init__(self, ctx_node, resource_id=None, client=None, logger=None): client or Boto3Connection(ctx_node).client('elbv2'), logger) self.type_name = RESOURCE_TYPE + self._properties = {} @property def properties(self): '''Gets the properties of an external resource''' - if not self.resource_id: - return - try: - resources = self.client.describe_target_groups( - TargetGroupArns=[self.resource_id]) - except (ParamValidationError, ClientError): - pass - else: - return None \ - if not resources else resources['TargetGroups'][0] + if not self._properties: + if not self.resource_id: + return + params = {'TargetGroupArns': [self.resource_id]} + try: + resources = self.make_client_call( + 'describe_target_groups', params) + except NonRecoverableError: + return + if 'TargetGroups' in resources: + for resource in resources['TargetGroups']: + if resource.get('TargetGroupArn', '') == self.resource_id: + self._properties = resource + elif resource.get('TargetGroupName', + '') == self.resource_id: + self._properties = resource + return self._properties @property def status(self): '''Gets the status of an external resource''' - props = self.properties - if not props: - return None - return props['State']['Code'] + if self.properties: + return self.properties.get('State', {}).get('Code') def create(self, params): ''' @@ -151,7 +157,7 @@ def modify(ctx, iface, resource_config, **_): '''modify an AWS ELB target group attributes''' # Build API params params = \ - ctx.instance.runtime_properties['resource_config'] \ + ctx.instance.runtime_properties.get('resource_config') \ or resource_config if TARGETGROUP_ARN not in params: params.update( diff --git a/cloudify_aws/elb/tests/test_listener.py b/cloudify_aws/elb/tests/test_listener.py index ba474955..0080932e 100644 --- a/cloudify_aws/elb/tests/test_listener.py +++ b/cloudify_aws/elb/tests/test_listener.py @@ -52,32 +52,34 @@ def test_class_properties(self): self.listener.client = self.make_client_function('describe_listeners', side_effect=effect) res = self.listener.properties - self.assertIsNone(res) + self.assertEqual(res, {}) value = [] self.listener.client = self.make_client_function('describe_listeners', return_value=value) res = self.listener.properties - self.assertIsNone(res) + self.assertEqual(res, {}) - value = {'Listeners': ['test']} + self.listener.resource_id = True + value = {'Listeners': [{'ListenerArn': True}]} self.listener.client = self.make_client_function('describe_listeners', return_value=value) res = self.listener.properties - self.assertEqual(res, 'test') + self.assertEqual(res, {'ListenerArn': True}) def test_class_status(self): value = [] self.listener.client = self.make_client_function('describe_listeners', return_value=value) res = self.listener.status - self.assertIsNone(res) + self.assertEqual(res, {}) - value = {'Listeners': [{'State': {'Code': 'ok'}}]} + value = {'Listeners': [{'ListenerArn': True, 'State': {'Code': 'ok'}}]} self.listener.client = self.make_client_function('describe_listeners', return_value=value) + self.listener.resource_id = True res = self.listener.status - self.assertEqual(res, 'ok') + self.assertEqual(res, {'ListenerArn': True, 'State': {'Code': 'ok'}}) def test_class_create(self): value = {'Listeners': [{LISTENER_ARN: 'id'}]} diff --git a/cloudify_aws/elb/tests/test_load_balancer.py b/cloudify_aws/elb/tests/test_load_balancer.py index 099b6835..66bb0899 100644 --- a/cloudify_aws/elb/tests/test_load_balancer.py +++ b/cloudify_aws/elb/tests/test_load_balancer.py @@ -231,7 +231,7 @@ def test_create(self): Name='aws_resource', SecurityGroups=['sec_id'], Subnets=['subnet_id']) self.fake_client.describe_load_balancers.assert_called_with( - LoadBalancerNames=['abc']) + Names=['abc']) # This is just because I'm not interested in the content # of remote_configuration right now. @@ -418,7 +418,7 @@ def test_delete(self): LoadBalancerArn='def' ) self.fake_client.describe_load_balancers.assert_called_with( - LoadBalancerNames=['abc']) + Names=['abc']) if __name__ == '__main__': diff --git a/cloudify_aws/elb/tests/test_rule.py b/cloudify_aws/elb/tests/test_rule.py index 33577339..74cc278f 100644 --- a/cloudify_aws/elb/tests/test_rule.py +++ b/cloudify_aws/elb/tests/test_rule.py @@ -52,32 +52,34 @@ def test_class_properties(self): self.rule.client = self.make_client_function('describe_rules', side_effect=effect) res = self.rule.properties - self.assertIsNone(res) + self.assertEqual(res, {}) value = [] self.rule.client = self.make_client_function('describe_rules', return_value=value) res = self.rule.properties - self.assertIsNone(res) + self.assertEqual(res, {}) - value = {'Rules': ['test']} + value = {'Rules': [{'RuleArn': True}]} + self.rule.resource_id = True self.rule.client = self.make_client_function('describe_rules', return_value=value) res = self.rule.properties - self.assertEqual(res, 'test') + self.assertEqual(res, {'RuleArn': True}) def test_class_status(self): value = [] self.rule.client = self.make_client_function('describe_rules', return_value=value) res = self.rule.status - self.assertIsNone(res) + self.assertEqual(res, {}) - value = {'Rules': [{'State': {'Code': 'ok'}}]} + value = {'Rules': [{'RuleArn': True, 'State': {'Code': 'ok'}}]} + self.rule.resource_id = True self.rule.client = self.make_client_function('describe_rules', return_value=value) res = self.rule.status - self.assertEqual(res, 'ok') + self.assertEqual(res, {'RuleArn': True, 'State': {'Code': 'ok'}}) def test_class_create(self): value = {'Rules': [{RULE_ARN: 'id'}]} diff --git a/cloudify_aws/elb/tests/test_target_group.py b/cloudify_aws/elb/tests/test_target_group.py index 051ccbaf..48928433 100644 --- a/cloudify_aws/elb/tests/test_target_group.py +++ b/cloudify_aws/elb/tests/test_target_group.py @@ -41,7 +41,7 @@ class TestELBTargetGroup(TestBase): def setUp(self): super(TestELBTargetGroup, self).setUp() - self.target_group = ELBTargetGroup("ctx_node", resource_id=True, + self.target_group = ELBTargetGroup("ctx_node", resource_id='test', client=MagicMock(), logger=None) mock1 = patch('cloudify_aws.common.decorators.aws_resource', mock_decorator) @@ -67,14 +67,14 @@ def test_class_properties(self): 'describe_target_groups', return_value=value) res = self.target_group.properties - self.assertIsNone(res) + self.assertEqual(res, {}) - value = {'TargetGroups': ['test']} + value = {'TargetGroups': [{'TargetGroupArn': 'test'}]} self.target_group.client = self.make_client_function( 'describe_target_groups', return_value=value) res = self.target_group.properties - self.assertEqual(res, 'test') + self.assertEqual(res, {'TargetGroupArn': 'test'}) def test_class_status(self): value = [] @@ -84,7 +84,13 @@ def test_class_status(self): res = self.target_group.status self.assertIsNone(res) - value = {'TargetGroups': [{'State': {'Code': 'ok'}}]} + value = { + 'TargetGroups': [ + {'TargetGroupArn': 'test', + 'State': {'Code': 'ok'} + } + ] + } self.target_group.client = self.make_client_function( 'describe_target_groups', return_value=value) diff --git a/cloudify_aws/iam/resources/instance_profile.py b/cloudify_aws/iam/resources/instance_profile.py index 157097a8..b9720f44 100644 --- a/cloudify_aws/iam/resources/instance_profile.py +++ b/cloudify_aws/iam/resources/instance_profile.py @@ -156,6 +156,10 @@ def delete(ctx, iface, resource_config, **_): RESOURCE_NAME: instance_profile_name, 'RoleName': role_name } - iface.remove_role_from_instance_profile(remove_role_params) + utils.handle_response( + iface, + 'remove_role_from_instance_profile', + remove_role_params, + ['NoSuchEntity']) - iface.delete(params) + utils.handle_response(iface, 'delete', params, ['NoSuchEntity']) diff --git a/cloudify_aws/iam/resources/policy.py b/cloudify_aws/iam/resources/policy.py index 61b0f6f5..3c229360 100644 --- a/cloudify_aws/iam/resources/policy.py +++ b/cloudify_aws/iam/resources/policy.py @@ -21,6 +21,8 @@ # Boto from botocore.exceptions import ClientError, ParamValidationError +from cloudify.exceptions import NonRecoverableError + # Cloudify from cloudify_aws.common import decorators, utils from cloudify_aws.iam import IAMBase @@ -98,7 +100,10 @@ def create(ctx, iface, resource_config, **_): isinstance(params['PolicyDocument'], dict): params['PolicyDocument'] = json_dumps(params['PolicyDocument']) # Actually create the resource - create_response = iface.create(params) + create_response = utils.handle_response( + iface, 'create', params, + raise_substrings='EntityAlreadyExists', + raisable=NonRecoverableError) resource_id = create_response['Policy']['PolicyName'] iface.update_resource_id(resource_id) utils.update_resource_id(ctx.instance, resource_id) @@ -112,4 +117,4 @@ def create(ctx, iface, resource_config, **_): def delete(iface, resource_config, **_): '''Deletes an AWS IAM Policy''' iface.update_resource_id(utils.get_resource_arn()) - iface.delete(resource_config) + utils.handle_response(iface, 'delete', resource_config, ['NoSuchEntity']) diff --git a/cloudify_aws/iam/resources/role.py b/cloudify_aws/iam/resources/role.py index 4646291c..79f11c85 100644 --- a/cloudify_aws/iam/resources/role.py +++ b/cloudify_aws/iam/resources/role.py @@ -18,8 +18,7 @@ ''' from json import dumps as json_dumps -# Boto -# from botocore.exceptions import ClientError +from botocore.exceptions import ClientError # Cloudify from cloudify.exceptions import NonRecoverableError @@ -74,7 +73,23 @@ def delete(self, params=None): params.update(dict(RoleName=self.resource_id)) self.logger.debug('Deleting %s with parameters: %s' % (self.type_name, params)) - self.client.delete_role(**params) + try: + self.client.delete_role(**params) + except ClientError as e: + if 'DeleteConflict' in str(e): + instance_profiles_list = self.client.list_instance_profiles() + instance_profiles = instance_profiles_list.get( + 'InstanceProfiles') + for instance_profile in instance_profiles: + for role in instance_profile.get('Roles', []): + if self.resource_id in role.get('RoleName'): + pm = { + 'InstanceProfileName': instance_profile.get( + 'InstanceProfileName'), + 'RoleName': role.get('RoleName') + } + self.client.remove_role_from_instance_profile(**pm) + self._properties = {} def attach_policy(self, params): ''' @@ -123,7 +138,10 @@ def create(ctx, iface, resource_config, params, **_): ctx.node.properties.get('policy_arns', [])) # Actually create the resource - create_response = iface.create(params) + create_response = utils.handle_response( + iface, 'create', params, + raise_substrings='EntityAlreadyExists', + raisable=NonRecoverableError) resource_id = create_response['Role']['RoleName'] iface.update_resource_id(resource_id) utils.update_resource_id(ctx.instance, resource_id) @@ -147,8 +165,8 @@ def create(ctx, iface, resource_config, params, **_): @decorators.aws_resource(IAMRole, RESOURCE_TYPE, - ignore_properties=True) -@decorators.wait_for_delete() + ignore_properties=True, + waits_for_status=False) def delete(ctx, iface, resource_config, **_): '''Deletes an AWS IAM Role''' @@ -158,8 +176,7 @@ def delete(ctx, iface, resource_config, **_): payload = dict() payload['PolicyArn'] = policy iface.detach_policy(payload) - - iface.delete(resource_config) + utils.handle_response(iface, 'delete', resource_config, ['NoSuchEntity']) @decorators.aws_relationship(IAMRole, RESOURCE_TYPE) @@ -183,4 +200,5 @@ def detach_from(ctx, iface, resource_config, **_): node=ctx.target.node, instance=ctx.target.instance, raise_on_missing=True) - iface.detach_policy(resource_config) + utils.handle_response( + iface, 'detach_policy', resource_config, ['NoSuchEntity']) diff --git a/cloudify_aws/iam/tests/test_role.py b/cloudify_aws/iam/tests/test_role.py index dfb82222..71ea91a4 100644 --- a/cloudify_aws/iam/tests/test_role.py +++ b/cloudify_aws/iam/tests/test_role.py @@ -249,13 +249,6 @@ def test_delete(self): RoleName='role_name_id' ) - self.assertEqual( - _ctx.instance.runtime_properties, - { - '__deleted': True, - } - ) - def test_IAMRoleClass_properties(self): self.fake_client.get_role = MagicMock(return_value={ 'Role': { diff --git a/examples/ecs-feature-demo/blueprint.yaml b/examples/ecs-feature-demo/blueprint.yaml index 60ad15d8..0fac2238 100644 --- a/examples/ecs-feature-demo/blueprint.yaml +++ b/examples/ecs-feature-demo/blueprint.yaml @@ -117,7 +117,7 @@ node_templates: type: cloudify.nodes.aws.iam.InstanceProfile properties: client_config: *client_config - resource_id: ecs_cfy_instance_profile + resource_id: { concat: [ 'cfy_ecs_instance_profile', '-', { get_input: ecs_cluster_name } ] } resource_config: InstanceProfileName: ecs_cfy_instance_profile Path: '/ecs_cfy_instance_profile/' @@ -183,7 +183,7 @@ node_templates: target: ecs_service_iam_role interfaces: cloudify.interfaces.lifecycle: - configure: + create: inputs: resource_config: serviceName: 'service_name_1' @@ -207,8 +207,9 @@ node_templates: DesiredCapacity: 1 DefaultCooldown: 20 AvailabilityZones: - - { get_property: [ subnet1, resource_config, kwargs, AvailabilityZone ] } - VPCZoneIdentifier: { concat: [ { get_attribute: [ subnet1, aws_resource_id ] } ] } + - { get_property: [ subnet1, resource_config, kwargs, AvailabilityZone ] } + VPCZoneIdentifier: + - { get_attribute: [ subnet1, aws_resource_id ] } relationships: - type: cloudify.relationships.depends_on @@ -232,7 +233,8 @@ node_templates: LaunchConfigurationName: container_instance KeyName: { get_property: [ key, resource_config, KeyName] } kwargs: - IamInstanceProfile: { get_attribute: [ ecs_instance_iam_role_instance_profile, aws_resource_arn ] } + IamInstanceProfile: { concat: [ 'cfy_ecs_instance_profile', '-', { get_input: ecs_cluster_name } ] } + # IamInstanceProfile: { get_attribute: [ ecs_instance_iam_role_instance_profile, aws_resource_arn ] } AssociatePublicIpAddress: True SecurityGroups: - { get_attribute: [ securitygroup1, aws_resource_id ] } @@ -254,6 +256,8 @@ node_templates: target: ecs_instance_iam_role_instance_profile - type: cloudify.relationships.depends_on target: key + - type: cloudify.relationships.depends_on + target: igw forward_rule: type: cloudify.nodes.aws.elb.Rule @@ -263,7 +267,7 @@ node_templates: Priority: 101 interfaces: cloudify.interfaces.lifecycle: - configure: + create: inputs: resource_config: Priority: 101 @@ -399,7 +403,7 @@ node_templates: type: cloudify.nodes.aws.ec2.SecurityGroup properties: resource_config: - GroupName: SecurityGroup1 + GroupName: SecurityGroup2 Description: Example Security Group 1 kwargs: client_config: *client_config