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: xz
|
||||||
Requires: createrepo_c
|
Requires: createrepo_c
|
||||||
Requires: python3-ansible-runner
|
Requires: python3-ansible-runner
|
||||||
|
# For AWS playbook support
|
||||||
|
Requires: python3-boto3
|
||||||
|
|
||||||
%{?systemd_requires}
|
%{?systemd_requires}
|
||||||
BuildRequires: systemd
|
BuildRequires: systemd
|
||||||
@ -237,11 +239,13 @@ getent passwd weldr >/dev/null 2>&1 || useradd -r -g weldr -d / -s /sbin/nologin
|
|||||||
%files composer
|
%files composer
|
||||||
%config(noreplace) %{_sysconfdir}/lorax/composer.conf
|
%config(noreplace) %{_sysconfdir}/lorax/composer.conf
|
||||||
%{python3_sitelib}/pylorax/api/*
|
%{python3_sitelib}/pylorax/api/*
|
||||||
|
%{python3_sitelib}/lifted/*
|
||||||
%{_sbindir}/lorax-composer
|
%{_sbindir}/lorax-composer
|
||||||
%{_unitdir}/lorax-composer.service
|
%{_unitdir}/lorax-composer.service
|
||||||
%{_unitdir}/lorax-composer.socket
|
%{_unitdir}/lorax-composer.socket
|
||||||
%dir %{_datadir}/lorax/composer
|
%dir %{_datadir}/lorax/composer
|
||||||
%{_datadir}/lorax/composer/*
|
%{_datadir}/lorax/composer/*
|
||||||
|
%{_datadir}/lorax/lifted/*
|
||||||
%{_tmpfilesdir}/lorax-composer.conf
|
%{_tmpfilesdir}/lorax-composer.conf
|
||||||
%dir %attr(0771, root, weldr) %{_sharedstatedir}/lorax/composer/
|
%dir %attr(0771, root, weldr) %{_sharedstatedir}/lorax/composer/
|
||||||
%dir %attr(0771, root, weldr) %{_sharedstatedir}/lorax/composer/blueprints/
|
%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 profile settings for each provider
|
||||||
test_profiles = {
|
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", {
|
"azure": ["azure-profile", {
|
||||||
"resource_group": "production",
|
"resource_group": "production",
|
||||||
"storage_account_name": "HomerSimpson",
|
"storage_account_name": "HomerSimpson",
|
||||||
|
@ -50,7 +50,7 @@ class ProvidersTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
def test_list_providers(self):
|
def test_list_providers(self):
|
||||||
p = list_providers(self.config["upload"])
|
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):
|
def test_resolve_provider(self):
|
||||||
for p in list_providers(self.config["upload"]):
|
for p in list_providers(self.config["upload"]):
|
||||||
|
@ -3496,7 +3496,7 @@ class ServerAPIV1TestCase(unittest.TestCase):
|
|||||||
self.assertNotEqual(data, None)
|
self.assertNotEqual(data, None)
|
||||||
self.assertTrue("providers" in data)
|
self.assertTrue("providers" in data)
|
||||||
providers = sorted(data["providers"].keys())
|
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):
|
def test_upload_01_providers_save(self):
|
||||||
"""Save settings for a provider"""
|
"""Save settings for a provider"""
|
||||||
|
Loading…
Reference in New Issue
Block a user