lifted: Add support for AWS upload
This uses a new Ansible module, ec2_snapshot_import, which is included here until it is available from upstream. It will upload the AMI to s3, convert it to a snapshot, and then register the snapshot as an AMI. The s3 object is deleted when it has been successfully uploaded.
This commit is contained in:
		
							parent
							
								
									a59c0241c4
								
							
						
					
					
						commit
						c2620b0c85
					
				| @ -153,6 +153,8 @@ Requires: git | ||||
| Requires: xz | ||||
| Requires: createrepo_c | ||||
| Requires: python3-ansible-runner | ||||
| # For AWS playbook support | ||||
| Requires: python3-boto3 | ||||
| 
 | ||||
| %{?systemd_requires} | ||||
| BuildRequires: systemd | ||||
| @ -237,11 +239,13 @@ getent passwd weldr >/dev/null 2>&1 || useradd -r -g weldr -d / -s /sbin/nologin | ||||
| %files composer | ||||
| %config(noreplace) %{_sysconfdir}/lorax/composer.conf | ||||
| %{python3_sitelib}/pylorax/api/* | ||||
| %{python3_sitelib}/lifted/* | ||||
| %{_sbindir}/lorax-composer | ||||
| %{_unitdir}/lorax-composer.service | ||||
| %{_unitdir}/lorax-composer.socket | ||||
| %dir %{_datadir}/lorax/composer | ||||
| %{_datadir}/lorax/composer/* | ||||
| %{_datadir}/lorax/lifted/* | ||||
| %{_tmpfilesdir}/lorax-composer.conf | ||||
| %dir %attr(0771, root, weldr) %{_sharedstatedir}/lorax/composer/ | ||||
| %dir %attr(0771, root, weldr) %{_sharedstatedir}/lorax/composer/blueprints/ | ||||
|  | ||||
							
								
								
									
										258
									
								
								share/lifted/providers/aws/library/ec2_snapshot_import.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								share/lifted/providers/aws/library/ec2_snapshot_import.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,258 @@ | ||||
| #!/usr/bin/python | ||||
| # Copyright (C) 2019 Red Hat, Inc. | ||||
| # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | ||||
| 
 | ||||
| from __future__ import absolute_import, division, print_function | ||||
| __metaclass__ = type | ||||
| 
 | ||||
| ANSIBLE_METADATA = {'metadata_version': '1.1', | ||||
|                     'status': ['preview'], | ||||
|                     'supported_by': 'community'} | ||||
| 
 | ||||
| 
 | ||||
| DOCUMENTATION = ''' | ||||
| --- | ||||
| module: ec2_snapshot_import | ||||
| short_description: Imports a disk into an EBS snapshot | ||||
| description: | ||||
|     - Imports a disk into an EBS snapshot | ||||
| version_added: "2.10" | ||||
| options: | ||||
|   description: | ||||
|     description: | ||||
|       - description of the import snapshot task | ||||
|     required: false | ||||
|     type: str | ||||
|   format: | ||||
|     description: | ||||
|       - The format of the disk image being imported. | ||||
|     required: true | ||||
|     type: str | ||||
|   url: | ||||
|     description: | ||||
|       - The URL to the Amazon S3-based disk image being imported. It can either be a https URL (https://..) or an Amazon S3 URL (s3://..). | ||||
|         Either C(url) or C(s3_bucket) and C(s3_key) are required. | ||||
|     required: false | ||||
|     type: str | ||||
|   s3_bucket: | ||||
|     description: | ||||
|       - The name of the S3 bucket where the disk image is located. | ||||
|       - C(s3_bucket) and C(s3_key) are required together if C(url) is not used. | ||||
|     required: false | ||||
|     type: str | ||||
|   s3_key: | ||||
|     description: | ||||
|       - The file name of the disk image. | ||||
|       - C(s3_bucket) and C(s3_key) are required together if C(url) is not used. | ||||
|     required: false | ||||
|     type: str | ||||
|   encrypted: | ||||
|     description: | ||||
|       - Whether or not the destination Snapshot should be encrypted. | ||||
|     type: bool | ||||
|     default: 'no' | ||||
|   kms_key_id: | ||||
|     description: | ||||
|       - KMS key id used to encrypt snapshot. If not specified, defaults to EBS Customer Master Key (CMK) for that account. | ||||
|     required: false | ||||
|     type: str | ||||
|   role_name: | ||||
|     description: | ||||
|       - The name of the role to use when not using the default role, 'vmimport'. | ||||
|     required: false | ||||
|     type: str | ||||
|   wait: | ||||
|     description: | ||||
|       - wait for the snapshot to be ready | ||||
|     type: bool | ||||
|     required: false | ||||
|     default: yes | ||||
|   wait_timeout: | ||||
|     description: | ||||
|       - how long before wait gives up, in seconds | ||||
|       - specify 0 to wait forever | ||||
|     required: false | ||||
|     type: int | ||||
|     default: 900 | ||||
|   tags: | ||||
|     description: | ||||
|       - A hash/dictionary of tags to add to the new Snapshot; '{"key":"value"}' and '{"key":"value","key":"value"}' | ||||
|     required: false | ||||
|     type: dict | ||||
| 
 | ||||
| author: "Brian C. Lane (@bcl)" | ||||
| extends_documentation_fragment: | ||||
|     - aws | ||||
|     - ec2 | ||||
| ''' | ||||
| 
 | ||||
| EXAMPLES = ''' | ||||
| # Import an S3 object as a snapshot | ||||
| ec2_snapshot_import: | ||||
|   description: simple-http-server | ||||
|   format: raw | ||||
|   s3_bucket: mybucket | ||||
|   s3_key: server-image.ami | ||||
|   wait: yes | ||||
|   tags: | ||||
|     Name: Snapshot-Name | ||||
| ''' | ||||
| 
 | ||||
| RETURN = ''' | ||||
| snapshot_id: | ||||
|     description: id of the created snapshot | ||||
|     returned: when snapshot is created | ||||
|     type: str | ||||
|     sample: "snap-1234abcd" | ||||
| description: | ||||
|     description: description of snapshot | ||||
|     returned: when snapshot is created | ||||
|     type: str | ||||
|     sample: "simple-http-server" | ||||
| format: | ||||
|     description: format of the disk image being imported | ||||
|     returned: when snapshot is created | ||||
|     type: str | ||||
|     sample: "raw" | ||||
| disk_image_size: | ||||
|     description: size of the disk image being imported, in bytes. | ||||
|     returned: when snapshot is created | ||||
|     type: float | ||||
|     sample: 3836739584.0 | ||||
| user_bucket: | ||||
|     description: S3 bucket with the image to import | ||||
|     returned: when snapshot is created | ||||
|     type: dict | ||||
|     sample: { | ||||
|         "s3_bucket": "mybucket", | ||||
|         "s3_key": "server-image.ami" | ||||
|     } | ||||
| status: | ||||
|     description: status of the import operation | ||||
|     returned: when snapshot is created | ||||
|     type: str | ||||
|     sample: "completed" | ||||
| ''' | ||||
| 
 | ||||
| 
 | ||||
| import time | ||||
| 
 | ||||
| from ansible.module_utils.aws.core import AnsibleAWSModule | ||||
| from ansible.module_utils.ec2 import camel_dict_to_snake_dict | ||||
| 
 | ||||
| try: | ||||
|     import botocore | ||||
| except ImportError: | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| def wait_for_import_snapshot(connection, wait_timeout, import_task_id): | ||||
| 
 | ||||
|     params = { | ||||
|         'ImportTaskIds': [import_task_id] | ||||
|     } | ||||
|     start_time = time.time() | ||||
|     while True: | ||||
|         status = connection.describe_import_snapshot_tasks(**params) | ||||
| 
 | ||||
|         # What are the valid status values? | ||||
|         if len(status['ImportSnapshotTasks']) > 1: | ||||
|             raise RuntimeError("Should only be 1 Import Snapshot Task with this id.") | ||||
| 
 | ||||
|         task = status['ImportSnapshotTasks'][0] | ||||
|         if task['SnapshotTaskDetail']['Status'] in ['completed']: | ||||
|             return status | ||||
| 
 | ||||
|         if time.time() - start_time > wait_timeout: | ||||
|             raise RuntimeError('Wait timeout exceeded (%s sec)' % wait_timeout) | ||||
| 
 | ||||
|         time.sleep(5) | ||||
| 
 | ||||
| 
 | ||||
| def import_snapshot(module, connection): | ||||
|     description = module.params.get('description') | ||||
|     image_format = module.params.get('format') | ||||
|     url = module.params.get('url') | ||||
|     s3_bucket = module.params.get('s3_bucket') | ||||
|     s3_key = module.params.get('s3_key') | ||||
|     encrypted = module.params.get('encrypted') | ||||
|     kms_key_id = module.params.get('kms_key_id') | ||||
|     role_name = module.params.get('role_name') | ||||
|     wait = module.params.get('wait') | ||||
|     wait_timeout = module.params.get('wait_timeout') | ||||
|     tags = module.params.get('tags') | ||||
| 
 | ||||
|     if module.check_mode: | ||||
|         module.exit_json(changed=True, msg="IMPORT operation skipped - running in check mode") | ||||
| 
 | ||||
|     try: | ||||
|         params = { | ||||
|             'Description': description, | ||||
|             'DiskContainer': { | ||||
|                 'Description': description, | ||||
|                 'Format': image_format, | ||||
|             }, | ||||
|             'Encrypted': encrypted | ||||
|         } | ||||
|         if url: | ||||
|             params['DiskContainer']['Url'] = url | ||||
|         else: | ||||
|             params['DiskContainer']['UserBucket'] = { | ||||
|                 'S3Bucket': s3_bucket, | ||||
|                 'S3Key': s3_key | ||||
|             } | ||||
|         if kms_key_id: | ||||
|             params['KmsKeyId'] = kms_key_id | ||||
|         if role_name: | ||||
|             params['RoleName'] = role_name | ||||
| 
 | ||||
|         task = connection.import_snapshot(**params) | ||||
|         import_task_id = task['ImportTaskId'] | ||||
|         detail = task['SnapshotTaskDetail'] | ||||
| 
 | ||||
|         if wait: | ||||
|             status = wait_for_import_snapshot(connection, wait_timeout, import_task_id) | ||||
|             detail = status['ImportSnapshotTasks'][0]['SnapshotTaskDetail'] | ||||
| 
 | ||||
|         if tags: | ||||
|             connection.create_tags( | ||||
|                 Resources=[detail["SnapshotId"]], | ||||
|                 Tags=[{'Key': k, 'Value': v} for k, v in tags.items()] | ||||
|             ) | ||||
|     except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError, RuntimeError) as e: | ||||
|         module.fail_json_aws(e, msg="Error importing image") | ||||
| 
 | ||||
|     module.exit_json(changed=True, **camel_dict_to_snake_dict(detail)) | ||||
| 
 | ||||
| 
 | ||||
| def snapshot_import_ansible_module(): | ||||
|     argument_spec = dict( | ||||
|         description=dict(default=''), | ||||
|         wait=dict(type='bool', default=True), | ||||
|         wait_timeout=dict(type='int', default=900), | ||||
|         format=dict(required=True), | ||||
|         url=dict(), | ||||
|         s3_bucket=dict(), | ||||
|         s3_key=dict(), | ||||
|         encrypted=dict(type='bool', default=False), | ||||
|         kms_key_id=dict(), | ||||
|         role_name=dict(), | ||||
|         tags=dict(type='dict') | ||||
|     ) | ||||
|     return AnsibleAWSModule( | ||||
|         argument_spec=argument_spec, | ||||
|         supports_check_mode=True, | ||||
|         mutually_exclusive=[['s3_bucket', 'url']], | ||||
|         required_one_of=[['s3_bucket', 'url']], | ||||
|         required_together=[['s3_bucket', 's3_key']] | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     module = snapshot_import_ansible_module() | ||||
|     connection = module.client('ec2') | ||||
|     import_snapshot(module, connection) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
							
								
								
									
										94
									
								
								share/lifted/providers/aws/playbook.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								share/lifted/providers/aws/playbook.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,94 @@ | ||||
| - hosts: localhost | ||||
|   tasks: | ||||
|   - name: Make sure bucket exists | ||||
|     aws_s3: | ||||
|       bucket: "{{ aws_bucket }}" | ||||
|       mode: create | ||||
|       aws_access_key: "{{ aws_access_key }}" | ||||
|       aws_secret_key: "{{ aws_secret_key }}" | ||||
|       region: "{{ aws_region }}" | ||||
|     register: bucket_facts | ||||
|   - fail: | ||||
|       msg: "Bucket creation failed" | ||||
|     when: | ||||
|       - bucket_facts.msg != "Bucket created successfully" | ||||
|       - bucket_facts.msg != "Bucket already exists." | ||||
|   - name: Make sure vmimport role exists | ||||
|     iam_role_facts: | ||||
|       name: vmimport | ||||
|       aws_access_key: "{{ aws_access_key }}" | ||||
|       aws_secret_key: "{{ aws_secret_key }}" | ||||
|       region: "{{ aws_region }}" | ||||
|     register: role_facts | ||||
|   - fail: | ||||
|       msg: "Role vmimport doesn't exist" | ||||
|     when: role_facts.iam_roles | length < 1 | ||||
|   - name: Make sure the AMI name isn't already in use | ||||
|     ec2_ami_facts: | ||||
|       filters: | ||||
|         name: "{{ image_name }}" | ||||
|       aws_access_key: "{{ aws_access_key }}" | ||||
|       aws_secret_key: "{{ aws_secret_key }}" | ||||
|       region: "{{ aws_region }}" | ||||
|     register: ami_facts | ||||
|   - fail: | ||||
|       msg: "An AMI named {{ image_name }} already exists" | ||||
|     when: ami_facts.images | length > 0 | ||||
|   - stat: | ||||
|       path: "{{ image_path }}" | ||||
|     register: image_stat | ||||
|   - set_fact: | ||||
|       image_id: "{{ image_name }}-{{ image_stat['stat']['checksum'] }}.ami" | ||||
|   - name: Upload the .ami image to an s3 bucket | ||||
|     aws_s3: | ||||
|       bucket: "{{ aws_bucket }}" | ||||
|       src: "{{ image_path }}" | ||||
|       object: "{{ image_id }}" | ||||
|       mode: put | ||||
|       overwrite: different | ||||
|       aws_access_key: "{{ aws_access_key }}" | ||||
|       aws_secret_key: "{{ aws_secret_key }}" | ||||
|       region: "{{ aws_region }}" | ||||
|   - name: Import a snapshot from an AMI stored as an s3 object | ||||
|     ec2_snapshot_import: | ||||
|       description: "{{ image_name }}" | ||||
|       format: raw | ||||
|       s3_bucket: "{{ aws_bucket }}" | ||||
|       s3_key: "{{ image_id }}" | ||||
|       aws_access_key: "{{ aws_access_key }}" | ||||
|       aws_secret_key: "{{ aws_secret_key }}" | ||||
|       region: "{{ aws_region }}" | ||||
|       wait: yes | ||||
|       tags: | ||||
|         Name: "{{ image_name }}" | ||||
|     register: import_facts | ||||
|   - fail: | ||||
|       msg: "Import of image from s3 failed" | ||||
|     when: | ||||
|       - import_facts.status != "completed" | ||||
|   - name: Register the snapshot as an AMI | ||||
|     ec2_ami: | ||||
|       name: "{{ image_name }}" | ||||
|       state: present | ||||
|       virtualization_type: hvm | ||||
|       root_device_name: /dev/sda1 | ||||
|       device_mapping: | ||||
|         - device_name: /dev/sda1 | ||||
|           snapshot_id: "{{ import_facts.snapshot_id }}" | ||||
|       aws_access_key: "{{ aws_access_key }}" | ||||
|       aws_secret_key: "{{ aws_secret_key }}" | ||||
|       region: "{{ aws_region }}" | ||||
|       wait: yes | ||||
|     register: register_facts | ||||
|   - fail: | ||||
|       msg: "Registering snapshot as an AMI failed" | ||||
|     when: | ||||
|         - register_facts.msg != "AMI creation operation complete." | ||||
|   - name: Delete the s3 object used for the snapshot/AMI | ||||
|     aws_s3: | ||||
|       bucket: "{{ aws_bucket }}" | ||||
|       object: "{{ image_id }}" | ||||
|       mode: delobj | ||||
|       aws_access_key: "{{ aws_access_key }}" | ||||
|       aws_secret_key: "{{ aws_secret_key }}" | ||||
|       region: "{{ aws_region }}" | ||||
							
								
								
									
										29
									
								
								share/lifted/providers/aws/provider.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								share/lifted/providers/aws/provider.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| display = "AWS" | ||||
| 
 | ||||
| supported_types = [ | ||||
|     "ami", | ||||
| ] | ||||
| 
 | ||||
| [settings-info.aws_access_key] | ||||
| display = "AWS Access Key" | ||||
| type = "string" | ||||
| placeholder = "" | ||||
| regex = '' | ||||
| 
 | ||||
| [settings-info.aws_secret_key] | ||||
| display = "AWS Secret Key" | ||||
| type = "string" | ||||
| placeholder = "" | ||||
| regex = '' | ||||
| 
 | ||||
| [settings-info.aws_region] | ||||
| display = "AWS Region" | ||||
| type = "string" | ||||
| placeholder = "" | ||||
| regex = '' | ||||
| 
 | ||||
| [settings-info.aws_bucket] | ||||
| display = "AWS Bucket" | ||||
| type = "string" | ||||
| placeholder = "" | ||||
| regex = '' | ||||
| @ -17,6 +17,12 @@ | ||||
| 
 | ||||
| # test profile settings for each provider | ||||
| test_profiles = { | ||||
|     "aws": ["aws-profile", { | ||||
|         "aws_access_key": "theaccesskey", | ||||
|         "aws_secret_key": "thesecretkey", | ||||
|         "aws_region":     "us-east-1", | ||||
|         "aws_bucket":     "composer-mops" | ||||
|         }], | ||||
|     "azure": ["azure-profile", { | ||||
|         "resource_group":       "production", | ||||
|         "storage_account_name": "HomerSimpson", | ||||
|  | ||||
| @ -50,7 +50,7 @@ class ProvidersTestCase(unittest.TestCase): | ||||
| 
 | ||||
|     def test_list_providers(self): | ||||
|         p = list_providers(self.config["upload"]) | ||||
|         self.assertEqual(p, ['azure', 'dummy', 'openstack', 'vsphere']) | ||||
|         self.assertEqual(p, ['aws', 'azure', 'dummy', 'openstack', 'vsphere']) | ||||
| 
 | ||||
|     def test_resolve_provider(self): | ||||
|         for p in list_providers(self.config["upload"]): | ||||
|  | ||||
| @ -3496,7 +3496,7 @@ class ServerAPIV1TestCase(unittest.TestCase): | ||||
|         self.assertNotEqual(data, None) | ||||
|         self.assertTrue("providers" in data) | ||||
|         providers = sorted(data["providers"].keys()) | ||||
|         self.assertEqual(providers, ["azure", "dummy", "openstack", "vsphere"]) | ||||
|         self.assertEqual(providers, ["aws", "azure", "dummy", "openstack", "vsphere"]) | ||||
| 
 | ||||
|     def test_upload_01_providers_save(self): | ||||
|         """Save settings for a provider""" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user