Skip to content

Commit

Permalink
Merge pull request #403 from cloudify-cosmo/RD-3358-create-ami-from-ec2
Browse files Browse the repository at this point in the history
Rd 3358 create ami from ec2
  • Loading branch information
EarthmanT authored Dec 1, 2021
2 parents 5a0d800 + 02bce31 commit 770c425
Show file tree
Hide file tree
Showing 7 changed files with 514 additions and 31 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,5 @@
- RD-3369-Return Aarch64 Wagons to Bundle
2.12.13:
- RD-3565: Fix Populate Resource issue.
2.12.14:
- RD-3358: Added option to create an EC2 ami image.
10 changes: 7 additions & 3 deletions cloudify_aws/common/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,10 @@ def _aws_resource(function,
resource_config = kwargs['resource_config']
resource_id = utils.get_resource_id(node=ctx.node, instance=ctx.instance)
# Check if using external
if props.get('use_external_resource') and \
'cloudify.nodes.aws.ec2.Image' in ctx.node.type_hierarchy and \
operation_name == 'create':
pass
iface = kwargs.get('iface')
if ctx.node.properties.get('use_external_resource', False):
ctx.logger.info('{t} ID# {i} is user-provided.'.format(
Expand All @@ -362,12 +366,12 @@ def _aws_resource(function,
return
ctx.logger.warn('{t} ID# {i} has force_operation set.'.format(
t=resource_type, i=resource_id))
ctx.logger.debug('Executing: {} with params {}'.format(function, kwargs))
result = function(**kwargs)
if ctx.operation.name == 'cloudify.interfaces.lifecycle.configure' \
and iface:
if operation_name == 'configure' and iface:
iface.populate_resource(ctx)
kwargs['iface'] = iface
if ctx.operation.name == 'cloudify.interfaces.lifecycle.delete':
elif operation_name == 'delete':
# cleanup runtime after delete
keys = list(ctx.instance.runtime_properties.keys())
for key in keys:
Expand Down
20 changes: 20 additions & 0 deletions cloudify_aws/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ def check_availability_zone(zone):


def clean_params(p):
p = dict() if not p else p.copy()
if not isinstance(p, dict) or not p:
return {}
for _k, _v in list(p.items()):
Expand Down Expand Up @@ -904,3 +905,22 @@ def get_node_instances_by_type_related_to_node_name(node_name,
if node_type in node.type_hierarchy and node_name in rels:
nodes.append({'node_instance': ni, 'node': node})
return nodes


def clean_empty_vals(params):
if isinstance(params, dict):
new_params = {}
for key, val in params.items():
if isinstance(val, dict) or isinstance(val, list):
val = clean_empty_vals(val)
if val:
new_params[key] = val
return new_params
if isinstance(params, list):
new_params = []
for val in params:
if isinstance(val, dict) or isinstance(val, list):
val = clean_empty_vals(val)
if val:
new_params.append(val)
return new_params
95 changes: 75 additions & 20 deletions cloudify_aws/ec2/resources/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,24 @@ class EC2Image(EC2Base):
def __init__(self, ctx_node, resource_id=None, client=None, logger=None):
EC2Base.__init__(self, ctx_node, resource_id, client, logger)
self.type_name = RESOURCE_TYPE
self.describe_image_filters = {}
image_filters = ctx_node.properties["resource_config"].get(
"kwargs", {}).get("Filters")
self.describe_image_filters = None
if resource_id:
self.prepare_describe_image_filter({IMAGE_IDS: [resource_id]})
elif image_filters:
self.prepare_describe_image_filter({FILTERS: image_filters})

@property
def properties(self):
"""Gets the properties of an external resource"""
params = self.describe_image_filters
if not params:
return
try:
resources = \
self.client.describe_images(**params)
self.logger.info('Describe images returned: {}'.format(resources))
except (ClientError, ParamValidationError):
pass
else:
Expand All @@ -61,7 +70,13 @@ def properties(self):
@property
def status(self):
"""Gets the status of an external resource"""
props = self.properties
try:
props = self.properties
except NonRecoverableError as e:
if 'Found no AMIs matching provided filters' in str(e):
props = None
else:
raise e
if not props:
return None
return props['State']
Expand All @@ -77,28 +92,68 @@ def create(self, params):
return res

def delete(self, params=None):
return
self.logger.debug('Deleting %s' % self.type_name)
self.logger.debug('Deregistering ImageId %s' % params.get('ImageId'))
self.client.deregister_image(**params)


def prepare_describe_image_filter(params, iface):
iface.describe_image_filters = {
DRY_RUN: params.get(DRY_RUN, False),
IMAGE_IDS: params.get(IMAGE_IDS, []),
OWNERS: params.get(OWNERS, []),
EXECUTABLE_USERS: params.get(EXECUTABLE_USERS, []),
FILTERS: params.get(FILTERS, [])
}
return iface
def prepare_describe_image_filter(self, params):
dry_run = params.get(DRY_RUN, False)
image_ids = params.get(IMAGE_IDS, [])
owners = params.get(OWNERS, [])
executable_users = params.get(EXECUTABLE_USERS, [])
filters = params.get(FILTERS, [])
if any([dry_run, image_ids, owners, executable_users, filters]):
self.describe_image_filters = {
DRY_RUN: dry_run,
IMAGE_IDS: image_ids,
OWNERS: owners,
EXECUTABLE_USERS: executable_users,
FILTERS: filters}
self.logger.debug('Updated image filter: {}'.format(
self.describe_image_filters))


@decorators.aws_resource(EC2Image, resource_type=RESOURCE_TYPE)
def prepare(ctx, iface, resource_config, **_):
"""Prepares an AWS EC2 Image"""
# Save the parameters
ctx.instance.runtime_properties['resource_config'] = resource_config
iface = \
prepare_describe_image_filter(
resource_config.copy(),
iface)
ami = iface.properties
utils.update_resource_id(ctx.instance, ami.get(IMAGE_ID))
if ctx.node.properties.get('use_external_resource'):
ctx.instance.runtime_properties['resource_config'] = resource_config
iface.prepare_describe_image_filter(resource_config)
utils.update_resource_id(ctx.instance, iface.properties.get(IMAGE_ID))


@decorators.aws_resource(EC2Image, resource_type=RESOURCE_TYPE)
@decorators.wait_for_status(status_good=['available'], fail_on_missing=False)
def create(ctx, iface, resource_config, **_):
"""Create an AWS EC2 Image"""
if ctx.instance.runtime_properties.get('use_external_resource'):
# if use_external_resource there we are using an existing image
return

params = utils.clean_params(resource_config)
if 'InstanceId' not in params:
params['InstanceId'] = utils.find_resource_id_by_type(
ctx.instance, 'cloudify.nodes.aws.ec2.Instances')
params = utils.clean_empty_vals(params)

# Actually create the resource
create_response = iface.create(params)
ctx.instance.runtime_properties['create_response'] = \
utils.JsonCleanuper(create_response).to_dict()
iface.update_resource_id(create_response['ImageId'])
utils.update_resource_id(ctx.instance, create_response['ImageId'])


@decorators.aws_resource(EC2Image, resource_type=RESOURCE_TYPE)
@decorators.wait_for_delete(status_deleted=['deregistered'])
def delete(ctx, iface, resource_config, **_):
"""delete/deregister an AWS EC2 Image"""
if not ctx.instance.runtime_properties.get('use_external_resource'):
params = {'ImageId': iface.resource_id}
try:
iface.delete(params)
except ClientError as e:
if 'is no longer available' in str(e):
return
raise e
20 changes: 14 additions & 6 deletions cloudify_aws/ec2/tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,21 @@
class TestEC2Image(TestBase):

def setUp(self):
self.image = EC2Image("ctx_node", resource_id=True,
client=True, logger=None)
ctx_node_mock = MagicMock(
properties={'resource_config': {'Name': 'foo'}})
mock_client = MagicMock()
self.image = EC2Image(ctx_node_mock,
resource_id=True,
client=mock_client,
logger=None)
mock1 = patch('cloudify_aws.common.decorators.aws_resource',
mock_decorator)
mock1.start()
reload_module(image)
self.image.describe_image_filters = \
{'Filters': {
'name': 'CentOS 7.7.1908 x86_64 with cloud-init (HVM)',
'owner-id': '057448758665'}}

def test_class_properties(self):
effect = self.get_client_error_exception(name='EC2 Image')
Expand All @@ -67,12 +76,10 @@ def test_class_properties(self):

def test_class_status(self):
value = {}

self.image.client = self.make_client_function('describe_images',
return_value=value)
with self.assertRaises(NonRecoverableError) as e:
self.image.status
self.assertEqual(text_type(e.exception),
u"Found no AMIs matching provided filters.")
self.assertIsNone(self.image.status)

value = {IMAGES: [None]}
self.image.client = self.make_client_function('describe_images',
Expand All @@ -97,6 +104,7 @@ def test_prepare(self):
ctx = self.get_mock_ctx("Image")
config = {IMAGE_ID: 'image', OWNERS: 'owner'}
iface = MagicMock()
ctx.node.properties['use_external_resource'] = True
iface.create = self.mock_return(config)
image.prepare(ctx, iface, config)
self.assertEqual(ctx.instance.runtime_properties['resource_config'],
Expand Down
Loading

0 comments on commit 770c425

Please sign in to comment.