New file |
| | |
| | | # Copyright: (c) 2020, Marcos Amorim <mamorim@redhat.com> |
| | | # 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: convert_blueprint |
| | | short_description: manage objects in IBM Cloud Object Store using S3 |
| | | version_added: "2.7" |
| | | description: |
| | | - This module allows the user to download QCOW2 images from IBM Cloud Storage, create a new image in Ceph and |
| | | add the image in OpenStack Glance |
| | | options: |
| | | TODO |
| | | extends_documentation_fragment: |
| | | - openstack |
| | | equirements: |
| | | - "python >= 2.7" |
| | | - "openstacksdk" |
| | | - "ibm-cos-sdk" |
| | | author: |
| | | - Marcos Amorim (@marcosmamorim) |
| | | ''' |
| | | |
| | | EXAMPLES = ''' |
| | | - name: Download images from project |
| | | environment: |
| | | OS_AUTH_URL: "{{ osp_auth_url }}" |
| | | OS_USERNAME: "{{ osp_auth_username }}" |
| | | OS_PASSWORD: "{{ osp_auth_password }}" |
| | | OS_PROJECT_NAME: "admin" |
| | | OS_PROJECT_DOMAIN_ID: "{{ osp_auth_project_domain }}" |
| | | OS_USER_DOMAIN_NAME: "{{ osp_auth_user_domain }}" |
| | | PATH: "/root/.local/bin:{{ ansible_env.PATH }}" |
| | | CEPH_CONF: "/etc/ceph/{{ ceph_cluster |default('red') }}.conf" |
| | | convert_blueprint: |
| | | ibm_endpoint: "{{ ibm_endpoint }}" |
| | | ibm_auth_endpoint: "{{ ibm_auth_endpoint }}" |
| | | ibm_api_key: "{{ ibm_api_key }}" |
| | | ibm_resource_id: "{{ ibm_resource_id }}" |
| | | bucket: "{{ ibm_bucket_name }}" |
| | | project: "{{ project }}" |
| | | output_dir: "{{ output_dir }}" |
| | | mode: "download" |
| | | glance_pool: "{{ ceph_cluster |default('red') }}-images" |
| | | overwrite: "{{ overwrite_image | default('false') }}" |
| | | ''' |
| | | RETURN = ''' |
| | | ''' |
| | | |
| | | from ansible.module_utils.basic import * |
| | | from ansible.module_utils.openstack import openstack_full_argument_spec, openstack_module_kwargs, \ |
| | | openstack_cloud_from_module |
| | | import ibm_boto3 |
| | | import os |
| | | from ibm_botocore.client import Config, ClientError |
| | | import uuid |
| | | |
| | | try: |
| | | import botocore |
| | | except ImportError: |
| | | pass # will be detected by imported AnsibleAWSModule |
| | | |
| | | |
| | | def get_connection(module): |
| | | endpoint = module.params['ibm_endpoint'] |
| | | api_key = module.params['ibm_api_key'] |
| | | auth_endpoint = module.params['ibm_auth_endpoint'] |
| | | resource_id = module.params['ibm_resource_id'] |
| | | |
| | | cos = ibm_boto3.resource("s3", |
| | | ibm_api_key_id=api_key, |
| | | ibm_service_instance_id=resource_id, |
| | | ibm_auth_endpoint=auth_endpoint, |
| | | config=Config(signature_version="oauth"), |
| | | endpoint_url=endpoint |
| | | ) |
| | | return cos |
| | | |
| | | |
| | | def glance_image_exists(module, image_name): |
| | | try: |
| | | module.log("Verifying if {} exists".format(image_name)) |
| | | sdk, cloud = openstack_cloud_from_module(module) |
| | | image = cloud.get_image(image_name) |
| | | return image |
| | | |
| | | except sdk.exceptions.OpenStackCloudException as e: |
| | | module.fail_json(msg=str(e)) |
| | | |
| | | |
| | | def multi_part_download(module, object, dest): |
| | | module.log(msg="Starting download %s to %s" % (object, dest)) |
| | | if module.check_mode: |
| | | module.exit_json(msg="PUT operation skipped - running in check mode", changed=True) |
| | | |
| | | try: |
| | | if os.path.exists(dest): |
| | | return True |
| | | s3 = get_connection(module) |
| | | bucket_name = module.params.get('bucket') |
| | | bucket = s3.Bucket(bucket_name) |
| | | obj = bucket.Object(object) |
| | | |
| | | file_size = module.params.get('chunk_file_size') |
| | | threshold_file = module.params.get('threshold_file_size') |
| | | |
| | | # set 5 MB chunks |
| | | part_size = 1024 * 1024 * file_size |
| | | |
| | | # set threadhold to 15 MB |
| | | file_threshold = 1024 * 1024 * threshold_file |
| | | |
| | | # set the transfer threshold and chunk size |
| | | transfer_config = ibm_boto3.s3.transfer.TransferConfig( |
| | | multipart_threshold=file_threshold, |
| | | multipart_chunksize=part_size |
| | | ) |
| | | |
| | | with open(dest, 'wb') as data: |
| | | obj.download_fileobj(Fileobj=data, Config=transfer_config) |
| | | module.log(msg="Transfer for {0} Complete!\n".format(dest)) |
| | | return True |
| | | except Exception as e: |
| | | module.exit_json(msg="Unable to complete multi-part download: {0}".format(e)) |
| | | |
| | | |
| | | def get_image_list(module, project): |
| | | endpoint = module.params['ibm_endpoint'] |
| | | api_key = module.params['ibm_api_key'] |
| | | auth_endpoint = module.params['ibm_auth_endpoint'] |
| | | resource_id = module.params['ibm_resource_id'] |
| | | bucket = module.params.get('bucket') |
| | | cos = ibm_boto3.client("s3", |
| | | ibm_api_key_id=api_key, |
| | | ibm_service_instance_id=resource_id, |
| | | ibm_auth_endpoint=auth_endpoint, |
| | | config=Config(signature_version="oauth"), |
| | | endpoint_url=endpoint |
| | | ) |
| | | |
| | | module.log("Getting list of images for the project {0}".format(project)) |
| | | try: |
| | | response = cos.list_objects_v2(Bucket=bucket, Prefix=project) |
| | | if 'Contents' in response: |
| | | return response["Contents"] |
| | | except Exception as e: |
| | | module.exit_json(msg="Error get bucket content. {0}".format(e)) |
| | | |
| | | |
| | | def check_img_ceph(module, img_id): |
| | | glance_pool = module.params.get("glance_pool") |
| | | |
| | | cmd = 'rbd info {0}/{1}'.format(glance_pool, img_id) |
| | | module.log("CHECK IMG: %s" % cmd) |
| | | rc, out, err = module.run_command(cmd) |
| | | module.log("CHECK IMG: %s - rc: %s - out: %s - %s" % (cmd, rc, out, err)) |
| | | |
| | | if rc == 0: |
| | | return True |
| | | else: |
| | | return False |
| | | |
| | | |
| | | def check_snapshot_ceph(module, img_id): |
| | | glance_pool = module.params.get("glance_pool") |
| | | |
| | | cmd = 'rbd snap list {0}/{1}'.format(glance_pool, img_id) |
| | | rc, out, err = module.run_command(cmd) |
| | | module.log("CHECK SNAP: %s - rc: %s - out: %s - %s" % (cmd, rc, out, err)) |
| | | |
| | | if len(out) == 0: |
| | | return True |
| | | else: |
| | | return False |
| | | |
| | | |
| | | def convert_to_ceph(module, disk_image, img_id): |
| | | glance_pool = module.params.get("glance_pool") |
| | | |
| | | if not check_img_ceph(module, img_id): |
| | | module.log("Converting image to ceph") |
| | | cmd = "qemu-img convert -f qcow2 -O raw '{0}' 'rbd:{1}/{2}'".format(disk_image, glance_pool, img_id) |
| | | module.log(cmd) |
| | | module.run_command(cmd, check_rc=True) |
| | | |
| | | if check_snapshot_ceph(module, img_id): |
| | | cmd = "rbd snap create {0}/{1}@snap".format(glance_pool, img_id) |
| | | module.log("Create snapshot {}".format(cmd)) |
| | | module.run_command(cmd, check_rc=True) |
| | | |
| | | cmd = "rbd snap protect {0}/{1}@snap".format(glance_pool, img_id) |
| | | module.log("Protect snapshot {}".format(cmd)) |
| | | module.run_command(cmd, check_rc=True) |
| | | |
| | | |
| | | def create_glance_image(module, image_name, img_id): |
| | | # glance image-create --disk-format raw --id $IMAGE_ID --container-format bare --name IMAGE_NAME |
| | | # TODO: Add virtio properties to the images |
| | | cmd = "glance image-create --disk-format raw --id {0} --container-format bare --visibility public --name '{1}'".format( |
| | | img_id, image_name) |
| | | module.log("run_command: glance image-create --disk-format raw --id {0} --container-format bare --visibility public --name '{1}'".format( |
| | | img_id, image_name)) |
| | | rc, out, err = module.run_command(cmd, check_rc=True) |
| | | |
| | | |
| | | def update_glance_location(module, img_id): |
| | | # glance --os-image-api-version 2 location-add --url "rbd://$CLUSTER_ID/$POOL/$IMAGE_ID/snap" $IMAGE_ID |
| | | result = module.run_command("ceph fsid", check_rc=True) |
| | | cluster_id = result[1].rstrip('\n') |
| | | module.log("CLUSTER ID: '{}'".format(cluster_id)) |
| | | glance_pool = module.params.get("glance_pool") |
| | | cmd = "glance location-add --url \'rbd://{cluster}/{pool}/{img_id}/snap\' {img_id}".format(cluster=cluster_id, |
| | | pool=glance_pool, |
| | | img_id=img_id) |
| | | module.log("UPDATE LOCATION: %s" % cmd) |
| | | module.run_command(cmd, check_rc=True) |
| | | |
| | | |
| | | def convert_to_raw(module): |
| | | output_dir = module.params.get('output_dir') |
| | | project = module.params.get('project') |
| | | overwrite = boolean(module.params.get('overwrite')) |
| | | |
| | | if not os.path.exists(output_dir): |
| | | os.makedirs(output_dir) |
| | | |
| | | image_list = get_image_list(module, project) |
| | | for img in image_list: |
| | | image_name = img["Key"].replace('/', '-').replace('.qcow2', '') |
| | | |
| | | check_image = glance_image_exists(module, image_name) |
| | | module.log("DEBUG check_image: %s" % check_image) |
| | | if check_image is not None and overwrite: |
| | | check_image = None |
| | | module.log("DELETE IMG %s" % image_name) |
| | | module.run_command("glance image-delete '{}'".format(image_name), check_rc=True) |
| | | |
| | | if check_image is not None and not overwrite: |
| | | module.log("Image %s already uploaded to glance. skipping..." % image_name) |
| | | continue |
| | | |
| | | module.log("Downloading image {} from IBM Cloud".format(image_name)) |
| | | dest_disk = "%s/%s.qcow2" % (output_dir, image_name.replace(' ', '_')) |
| | | |
| | | if not multi_part_download(module, img["Key"], dest_disk): |
| | | module.exit_json(msg="Error download images %s to %s " % (image_name, dest_disk)) |
| | | |
| | | if check_image is not None: |
| | | img_id = check_image.get('id') |
| | | else: |
| | | img_id = uuid.uuid4() |
| | | create_glance_image(module, image_name, img_id) |
| | | |
| | | convert_to_ceph(module, dest_disk, img_id) |
| | | os.remove(dest_disk) |
| | | update_glance_location(module, img_id) |
| | | module.exit_json(changed=True) |
| | | |
| | | |
| | | def run_module(): |
| | | module_args = dict( |
| | | ibm_endpoint=dict(type='str', required=True), |
| | | ibm_api_key=dict(type='str', required=True, no_log=True), |
| | | ibm_auth_endpoint=dict(type='str', default='https://iam.cloud.ibm.com/identity/token'), |
| | | ibm_resource_id=dict(type='str', required=True), |
| | | project=dict(type='str', required=True), |
| | | bucket=dict(required=True), |
| | | mode=dict(choices=['upload', 'download'], default='download'), |
| | | output_dir=dict(default="/images/import"), |
| | | glance_pool=dict(required=True), |
| | | overwrite=dict(aliases=['force'], default=False, choices=BOOLEANS), |
| | | chunk_file_size=dict(default=5, type='int'), |
| | | threshold_file_size=dict(default=15, type='int'), |
| | | retries=dict(default=5, type=int) |
| | | ) |
| | | |
| | | argument_spec = openstack_full_argument_spec( |
| | | image=dict(required=False), |
| | | ) |
| | | module_args.update(argument_spec) |
| | | module_kwargs = openstack_module_kwargs() |
| | | module = AnsibleModule(module_args, **module_kwargs) |
| | | |
| | | # if the user is working with this module in only check mode we do not |
| | | # want to make any changes to the environment, just return the current |
| | | # state with no modifications |
| | | if module.check_mode: |
| | | module.exit_json(msg="Operation skipped - running in check mode", changed=True) |
| | | |
| | | mode = module.params.get("mode") |
| | | |
| | | if mode == "download": |
| | | convert_to_raw(module) |
| | | |
| | | module.exit_json(failed=False) |
| | | |
| | | |
| | | def main(): |
| | | run_module() |
| | | |
| | | |
| | | if __name__ == '__main__': |
| | | main() |