629 lines
18 KiB
Python
629 lines
18 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2018, Milan Ilic <milani@nordeus.com>
|
|
# 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 (absolute_import, division, print_function)
|
|
__metaclass__ = type
|
|
|
|
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 will be assigned to the existing or new image.
|
|
- In the case of cloning, by default O(new_name) will take 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="There is no image with id=" + str(id))
|
|
elif name is not None:
|
|
module.fail_json(msg="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("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="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="Cannot enable " + IMAGE_STATES[state] + " image!")
|
|
else:
|
|
self.module.fail_json(msg="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="Cannot enable persistence for " + IMAGE_STATES[state] + " image!")
|
|
else:
|
|
self.module.fail_json(msg="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 = "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.STATE == IMAGE_STATES.index('DISABLED'):
|
|
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="Name '" + new_name + "' is already taken by IMAGE with id=" + str(tmp_image.ID))
|
|
|
|
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="Cannot delete image. There are " + str(image.RUNNING_VMS) + " 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()
|