#!/usr/bin/python # Copyright (c) 2018, Milan Ilic # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later # Make coding more python3-ish from __future__ import annotations DOCUMENTATION = r""" module: one_image short_description: Manages OpenNebula images description: - Manages OpenNebula images. requirements: - pyone extends_documentation_fragment: - community.general.opennebula - community.general.attributes attributes: check_mode: support: full diff_mode: support: none options: id: description: - A O(id) of the image you would like to manage. type: int name: description: - A O(name) of the image you would like to manage. - Required if O(create=true). type: str state: description: - V(present) - state that is used to manage the image. - V(absent) - delete the image. - V(cloned) - clone the image. - V(renamed) - rename the image to the O(new_name). choices: ["present", "absent", "cloned", "renamed"] default: present type: str enabled: description: - Whether the image should be enabled or disabled. type: bool new_name: description: - A name that is assigned to the existing or new image. - In the case of cloning, by default O(new_name) is set to the name of the origin image with the prefix 'Copy of'. type: str persistent: description: - Whether the image should be persistent or non-persistent. type: bool version_added: 9.5.0 create: description: - Whether the image should be created if not present. - This is ignored if O(state=absent). type: bool version_added: 10.0.0 template: description: - Use with O(create=true) to specify image template. type: str version_added: 10.0.0 datastore_id: description: - Use with O(create=true) to specify datastore for image. type: int version_added: 10.0.0 wait_timeout: description: - Seconds to wait until image is ready, deleted or cloned. type: int default: 60 version_added: 10.0.0 author: - "Milan Ilic (@ilicmilan)" """ EXAMPLES = r""" - name: Fetch the IMAGE by id community.general.one_image: id: 45 register: result - name: Print the IMAGE properties ansible.builtin.debug: var: result - name: Rename existing IMAGE community.general.one_image: id: 34 state: renamed new_name: bar-image - name: Disable the IMAGE by id community.general.one_image: id: 37 enabled: false - name: Make the IMAGE persistent community.general.one_image: id: 37 persistent: true - name: Enable the IMAGE by name community.general.one_image: name: bar-image enabled: true - name: Clone the IMAGE by name community.general.one_image: name: bar-image state: cloned new_name: bar-image-clone register: result - name: Delete the IMAGE by id community.general.one_image: id: '{{ result.id }}' state: absent - name: Make sure IMAGE is present community.general.one_image: name: myyy-image state: present create: true datastore_id: 100 template: | PATH = "/var/tmp/image" TYPE = "OS" SIZE = 20512 FORMAT = "qcow2" PERSISTENT = "Yes" DEV_PREFIX = "vd" - name: Make sure IMAGE is present with a longer timeout community.general.one_image: name: big-image state: present create: true datastore_id: 100 wait_timeout: 900 template: |- PATH = "https://192.0.2.200/repo/tipa_image.raw" TYPE = "OS" SIZE = 82048 FORMAT = "raw" PERSISTENT = "Yes" DEV_PREFIX = "vd" """ RETURN = r""" id: description: Image ID. type: int returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: 153 name: description: Image name. type: str returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: app1 group_id: description: Image's group ID. type: int returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: 1 group_name: description: Image's group name. type: str returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: one-users owner_id: description: Image's owner ID. type: int returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: 143 owner_name: description: Image's owner name. type: str returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: ansible-test state: description: State of image instance. type: str returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: READY used: description: Is image in use. type: bool returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: true running_vms: description: Count of running vms that use this image. type: int returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: 7 permissions: description: The image's permissions. type: dict returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 contains: owner_u: description: The image's owner USAGE permissions. type: str sample: 1 owner_m: description: The image's owner MANAGE permissions. type: str sample: 0 owner_a: description: The image's owner ADMIN permissions. type: str sample: 0 group_u: description: The image's group USAGE permissions. type: str sample: 0 group_m: description: The image's group MANAGE permissions. type: str sample: 0 group_a: description: The image's group ADMIN permissions. type: str sample: 0 other_u: description: The image's other users USAGE permissions. type: str sample: 0 other_m: description: The image's other users MANAGE permissions. type: str sample: 0 other_a: description: The image's other users ADMIN permissions. type: str sample: 0 sample: owner_u: 1 owner_m: 0 owner_a: 0 group_u: 0 group_m: 0 group_a: 0 other_u: 0 other_m: 0 other_a: 0 type: description: The image's type. type: str sample: 0 returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 disk_type: description: The image's format type. type: str sample: 0 returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 persistent: description: The image's persistence status (1 means true, 0 means false). type: int sample: 1 returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 source: description: The image's source. type: str sample: /var/lib/one//datastores/100/somerandomstringxd returned: when O(state=present), O(state=cloned), or O(state=renamed) path: description: The image's filesystem path. type: str sample: /var/tmp/hello.qcow2 returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 fstype: description: The image's filesystem type. type: str sample: ext4 returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 size: description: The image's size in MegaBytes. type: int sample: 10000 returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 cloning_ops: description: The image's cloning operations per second. type: int sample: 0 returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 cloning_id: description: The image's cloning ID. type: int sample: -1 returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 target_snapshot: description: The image's target snapshot. type: int sample: 1 returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 datastore_id: description: The image's datastore ID. type: int sample: 100 returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 datastore: description: The image's datastore name. type: int sample: image_datastore returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 vms: description: The image's list of VM ID's. type: list elements: int returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: - 1 - 2 - 3 version_added: 9.5.0 clones: description: The image's list of clones ID's. type: list elements: int returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: - 1 - 2 - 3 version_added: 9.5.0 app_clones: description: The image's list of app_clones ID's. type: list elements: int returned: when O(state=present), O(state=cloned), or O(state=renamed) sample: - 1 - 2 - 3 version_added: 9.5.0 snapshots: description: The image's list of snapshots. type: list returned: when O(state=present), O(state=cloned), or O(state=renamed) version_added: 9.5.0 sample: - date: 123123 parent: 1 size: 10228 allow_orphans: 1 children: 0 active: 1 name: SampleName """ from ansible_collections.community.general.plugins.module_utils.opennebula import OpenNebulaModule IMAGE_STATES = [ "INIT", "READY", "USED", "DISABLED", "LOCKED", "ERROR", "CLONE", "DELETE", "USED_PERS", "LOCKED_USED", "LOCKED_USED_PERS", ] class ImageModule(OpenNebulaModule): def __init__(self): argument_spec = dict( id=dict(type="int"), name=dict(type="str"), state=dict(type="str", choices=["present", "absent", "cloned", "renamed"], default="present"), enabled=dict(type="bool"), new_name=dict(type="str"), persistent=dict(type="bool"), create=dict(type="bool"), template=dict(type="str"), datastore_id=dict(type="int"), wait_timeout=dict(type="int", default=60), ) required_if = [ ["state", "renamed", ["id"]], ["create", True, ["template", "datastore_id", "name"]], ] mutually_exclusive = [ ["id", "name"], ] OpenNebulaModule.__init__( self, argument_spec, supports_check_mode=True, mutually_exclusive=mutually_exclusive, required_if=required_if, ) def run(self, one, module, result): params = module.params id = params.get("id") name = params.get("name") desired_state = params.get("state") enabled = params.get("enabled") new_name = params.get("new_name") persistent = params.get("persistent") create = params.get("create") template = params.get("template") datastore_id = params.get("datastore_id") wait_timeout = params.get("wait_timeout") self.result = {} image = self.get_image_instance(id, name) if not image and desired_state != "absent": if create: self.result = self.create_image(name, template, datastore_id, wait_timeout) # Using 'if id:' doesn't work properly when id=0 elif id is not None: module.fail_json(msg=f"There is no image with id={id}") elif name is not None: module.fail_json(msg=f"There is no image with name={name}") if desired_state == "absent": self.result = self.delete_image(image, wait_timeout) else: if persistent is not None: self.result = self.change_persistence(image, persistent) if enabled is not None: self.result = self.enable_image(image, enabled) if desired_state == "cloned": self.result = self.clone_image(image, new_name, wait_timeout) elif desired_state == "renamed": self.result = self.rename_image(image, new_name) self.exit() def get_image(self, predicate): # Filter -2 means fetch all images user can Use pool = self.one.imagepool.info(-2, -1, -1, -1) for image in pool.IMAGE: if predicate(image): return image return None def get_image_by_name(self, image_name): return self.get_image(lambda image: (image_name == image.NAME)) def get_image_by_id(self, image_id): return self.get_image(lambda image: (image_id == image.ID)) def get_image_instance(self, requested_id, requested_name): # Using 'if requested_id:' doesn't work properly when requested_id=0 if requested_id is not None: return self.get_image_by_id(requested_id) else: return self.get_image_by_name(requested_name) def create_image(self, image_name, template, datastore_id, wait_timeout): if not self.module.check_mode: image_id = self.one.image.allocate(f'NAME = "{image_name}"\n{template}', datastore_id) self.wait_for_ready(image_id, wait_timeout) image = self.get_image_by_id(image_id) result = self.get_image_info(image) result["changed"] = True return result def wait_for_ready(self, image_id, wait_timeout=60): import time start_time = time.time() while (time.time() - start_time) < wait_timeout: image = self.one.image.info(image_id) state = image.STATE if state in [IMAGE_STATES.index("ERROR")]: self.module.fail_json(msg=f"Got an ERROR state: {image.TEMPLATE['ERROR']}") if state in [IMAGE_STATES.index("READY")]: return True time.sleep(1) self.module.fail_json(msg="Wait timeout has expired!") def wait_for_delete(self, image_id, wait_timeout=60): import time start_time = time.time() while (time.time() - start_time) < wait_timeout: # It might be already deleted by the time this function is called try: image = self.one.image.info(image_id) except Exception: check_image = self.get_image_instance(image_id) if not check_image: return True state = image.STATE if state in [IMAGE_STATES.index("DELETE")]: return True time.sleep(1) self.module.fail_json(msg="Wait timeout has expired!") def enable_image(self, image, enable): image = self.one.image.info(image.ID) changed = False state = image.STATE if state not in [IMAGE_STATES.index("READY"), IMAGE_STATES.index("DISABLED"), IMAGE_STATES.index("ERROR")]: if enable: self.module.fail_json(msg=f"Cannot enable {IMAGE_STATES[state]} image!") else: self.module.fail_json(msg=f"Cannot disable {IMAGE_STATES[state]} image!") if (enable and state != IMAGE_STATES.index("READY")) or ( not enable and state != IMAGE_STATES.index("DISABLED") ): changed = True if changed and not self.module.check_mode: self.one.image.enable(image.ID, enable) result = self.get_image_info(image) result["changed"] = changed return result def change_persistence(self, image, enable): image = self.one.image.info(image.ID) changed = False state = image.STATE if state not in [IMAGE_STATES.index("READY"), IMAGE_STATES.index("DISABLED"), IMAGE_STATES.index("ERROR")]: if enable: self.module.fail_json(msg=f"Cannot enable persistence for {IMAGE_STATES[state]} image!") else: self.module.fail_json(msg=f"Cannot disable persistence for {IMAGE_STATES[state]} image!") if (enable and state != IMAGE_STATES.index("READY")) or ( not enable and state != IMAGE_STATES.index("DISABLED") ): changed = True if changed and not self.module.check_mode: self.one.image.persistent(image.ID, enable) result = self.get_image_info(image) result["changed"] = changed return result def clone_image(self, image, new_name, wait_timeout): if new_name is None: new_name = f"Copy of {image.NAME}" tmp_image = self.get_image_by_name(new_name) if tmp_image: result = self.get_image_info(image) result["changed"] = False return result if IMAGE_STATES.index("DISABLED") == image.STATE: self.module.fail_json(msg="Cannot clone DISABLED image") if not self.module.check_mode: new_id = self.one.image.clone(image.ID, new_name) self.wait_for_ready(new_id, wait_timeout) image = self.one.image.info(new_id) result = self.get_image_info(image) result["changed"] = True return result def rename_image(self, image, new_name): if new_name is None: self.module.fail_json(msg="'new_name' option has to be specified when the state is 'renamed'") if new_name == image.NAME: result = self.get_image_info(image) result["changed"] = False return result tmp_image = self.get_image_by_name(new_name) if tmp_image: self.module.fail_json(msg=f"Name '{new_name}' is already taken by IMAGE with id={tmp_image.ID!s}") if not self.module.check_mode: self.one.image.rename(image.ID, new_name) result = self.get_image_info(image) result["changed"] = True return result def delete_image(self, image, wait_timeout): if not image: return {"changed": False} if image.RUNNING_VMS > 0: self.module.fail_json(msg=f"Cannot delete image. There are {image.RUNNING_VMS!s} VMs using it.") if not self.module.check_mode: self.one.image.delete(image.ID) self.wait_for_delete(image.ID, wait_timeout) return {"changed": True} def main(): ImageModule().run_module() if __name__ == "__main__": main()