mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-02-04 07:51:50 +00:00
* Address F841 (unused variable). * Reformat. * Add changelog fragment. * More cleanup. * Remove trailing whitespace. * Readd removed code as a comment with TODO.
1958 lines
60 KiB
Python
1958 lines
60 KiB
Python
#!/usr/bin/python
|
|
# Copyright (c) 2017, Milan Ilic <milani@nordeus.com>
|
|
# Copyright (c) 2019, Jan Meerkamp <meerkamp@dvv.de>
|
|
# Copyright (c) 2025, Tom Paine <github@aioue.net>
|
|
# 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_vm
|
|
short_description: Creates or terminates OpenNebula instances
|
|
description:
|
|
- Manages OpenNebula instances.
|
|
requirements:
|
|
- pyone
|
|
extends_documentation_fragment:
|
|
- community.general.attributes
|
|
attributes:
|
|
check_mode:
|
|
support: full
|
|
diff_mode:
|
|
support: none
|
|
options:
|
|
api_url:
|
|
description:
|
|
- URL of the OpenNebula RPC server.
|
|
- It is recommended to use HTTPS so that the username/password are not transferred over the network unencrypted.
|
|
- If not set then the value of the E(ONE_URL) environment variable is used.
|
|
type: str
|
|
api_username:
|
|
description:
|
|
- Name of the user to login into the OpenNebula RPC server. If not set then the value of the E(ONE_USERNAME) environment
|
|
variable is used.
|
|
type: str
|
|
api_password:
|
|
description:
|
|
- Password of the user to login into OpenNebula RPC server. If not set then the value of the E(ONE_PASSWORD) environment
|
|
variable is used. if both O(api_username) or O(api_password) are not set, then it tries to authenticate with ONE auth
|
|
file. Default path is C(~/.one/one_auth).
|
|
- Set environment variable E(ONE_AUTH) to override this path.
|
|
type: str
|
|
template_name:
|
|
description:
|
|
- Name of VM template to use to create a new instance.
|
|
type: str
|
|
template_id:
|
|
description:
|
|
- ID of a VM template to use to create a new instance.
|
|
type: int
|
|
vm_start_on_hold:
|
|
description:
|
|
- Set to true to put VM on hold while creating.
|
|
default: false
|
|
type: bool
|
|
instance_ids:
|
|
description:
|
|
- 'A list of instance IDs used for states: V(absent), V(running), V(rebooted), V(poweredoff).'
|
|
aliases: ['ids']
|
|
type: list
|
|
elements: int
|
|
state:
|
|
description:
|
|
- V(present) - create instances from a template specified with C(template_id)/C(template_name).
|
|
- V(running) - run instances.
|
|
- V(poweredoff) - power-off instances.
|
|
- V(rebooted) - reboot instances.
|
|
- V(absent) - terminate instances.
|
|
choices: ["present", "absent", "running", "rebooted", "poweredoff"]
|
|
default: present
|
|
type: str
|
|
hard:
|
|
description:
|
|
- Reboot, power-off or terminate instances C(hard).
|
|
default: false
|
|
type: bool
|
|
wait:
|
|
description:
|
|
- Wait for the instance to reach its desired state before returning. Keep in mind if you are waiting for instance to
|
|
be in running state it does not mean that you are able to SSH on that machine only that boot process have started
|
|
on that instance. See the example using the M(ansible.builtin.wait_for) module for details.
|
|
default: true
|
|
type: bool
|
|
wait_timeout:
|
|
description:
|
|
- How long before wait gives up, in seconds.
|
|
default: 300
|
|
type: int
|
|
attributes:
|
|
description:
|
|
- A dictionary of key/value attributes to add to new instances, or for setting C(state) of instances with these attributes.
|
|
- Keys are case insensitive and OpenNebula automatically converts them to upper case.
|
|
- Be aware V(NAME) is a special attribute which sets the name of the VM when it is deployed.
|
|
- C(#) character(s) can be appended to the C(NAME) and the module automatically adds indexes to the names of VMs.
|
|
- 'For example: V(NAME: foo-###) would create VMs with names V(foo-000), V(foo-001),...'
|
|
- When used with O(count_attributes) and O(exact_count) the module matches the base name without the index part.
|
|
default: {}
|
|
type: dict
|
|
labels:
|
|
description:
|
|
- A list of labels to associate with new instances, or for setting C(state) of instances with these labels.
|
|
default: []
|
|
type: list
|
|
elements: str
|
|
count_attributes:
|
|
description:
|
|
- A dictionary of key/value attributes that can only be used with O(exact_count) to determine how many nodes based on
|
|
a specific attributes criteria should be deployed. This can be expressed in multiple ways and is shown in the EXAMPLES
|
|
section.
|
|
type: dict
|
|
count_labels:
|
|
description:
|
|
- A list of labels that can only be used with O(exact_count) to determine how many nodes based on a specific labels
|
|
criteria should be deployed. This can be expressed in multiple ways and is shown in the EXAMPLES section.
|
|
type: list
|
|
elements: str
|
|
count:
|
|
description:
|
|
- Number of instances to launch.
|
|
default: 1
|
|
type: int
|
|
exact_count:
|
|
description:
|
|
- Indicates how many instances that match O(count_attributes) and O(count_labels) parameters should be deployed. Instances
|
|
are either created or terminated based on this value.
|
|
- B(NOTE:) Instances with the least IDs are terminated first.
|
|
type: int
|
|
mode:
|
|
description:
|
|
- Set permission mode of the instance in octet format, for example V(0600) to give owner C(use) and C(manage) and nothing
|
|
to group and others.
|
|
type: str
|
|
owner_id:
|
|
description:
|
|
- ID of the user which is set as the owner of the instance.
|
|
type: int
|
|
group_id:
|
|
description:
|
|
- ID of the group which is set as the group of the instance.
|
|
type: int
|
|
memory:
|
|
description:
|
|
- The size of the memory for new instances (in MB, GB, ..).
|
|
type: str
|
|
disk_size:
|
|
description:
|
|
- The size of the disk created for new instances (in MB, GB, TB,...).
|
|
- B(NOTE:) If The Template hats Multiple Disks the Order of the Sizes is matched against the order specified in O(template_id)/O(template_name).
|
|
type: list
|
|
elements: str
|
|
cpu:
|
|
description:
|
|
- Percentage of CPU divided by 100 required for the new instance. Half a processor is written 0.5.
|
|
type: float
|
|
vcpu:
|
|
description:
|
|
- Number of CPUs (cores) the new VM uses.
|
|
type: int
|
|
networks:
|
|
description:
|
|
- A list of dictionaries with network parameters. See examples for more details.
|
|
default: []
|
|
type: list
|
|
elements: dict
|
|
disk_saveas:
|
|
description:
|
|
- Creates an image from a VM disk.
|
|
- It is a dictionary where you have to specify C(name) of the new image.
|
|
- Optionally you can specify C(disk_id) of the disk you want to save. By default C(disk_id) is 0.
|
|
- B(NOTE:) This operation is only performed on the first VM (if more than one VM ID is passed) and the VM has to be
|
|
in the C(poweredoff) state.
|
|
- Also this operation fails if an image with specified C(name) already exists.
|
|
type: dict
|
|
persistent:
|
|
description:
|
|
- Create a private persistent copy of the template plus any image defined in DISK, and instantiate that copy.
|
|
default: false
|
|
type: bool
|
|
version_added: '0.2.0'
|
|
datastore_id:
|
|
description:
|
|
- Name of Datastore to use to create a new instance.
|
|
version_added: '0.2.0'
|
|
type: int
|
|
datastore_name:
|
|
description:
|
|
- Name of Datastore to use to create a new instance.
|
|
version_added: '0.2.0'
|
|
type: str
|
|
updateconf:
|
|
description:
|
|
- When O(instance_ids) is provided, updates running VMs with the C(updateconf) API call.
|
|
- When new VMs are being created, emulates the C(updateconf) API call using direct template merge.
|
|
- Allows for complete modifications of the C(CONTEXT) attribute.
|
|
- 'Supported attributes include:'
|
|
- B(BACKUP_CONFIG:) V(BACKUP_VOLATILE), V(FS_FREEZE), V(INCREMENT_MODE), V(KEEP_LAST), V(MODE);
|
|
- B(CONTEXT:) (Any value, except V(ETH*). Variable substitutions are made);
|
|
- B(CPU_MODEL:) V(FEATURES), V(MODEL);
|
|
- B(FEATURES:) V(ACPI), V(APIC), V(GUEST_AGENT), V(HYPERV), V(IOTHREADS), V(LOCALTIME), V(PAE), V(VIRTIO_BLK_QUEUES),
|
|
V(VIRTIO_SCSI_QUEUES);
|
|
- B(GRAPHICS:) V(COMMAND), V(KEYMAP), V(LISTEN), V(PASSWD), V(PORT), V(TYPE);
|
|
- B(INPUT:) V(BUS), V(TYPE);
|
|
- B(OS:) V(ARCH), V(BOOT), V(BOOTLOADER), V(FIRMWARE), V(INITRD), V(KERNEL), V(KERNEL_CMD), V(MACHINE), V(ROOT), V(SD_DISK_BUS),
|
|
V(UUID);
|
|
- B(RAW:) V(DATA), V(DATA_VMX), V(TYPE), V(VALIDATE);
|
|
- B(VIDEO:) V(ATS), V(IOMMU), V(RESOLUTION), V(TYPE), V(VRAM).
|
|
type: dict
|
|
version_added: 6.3.0
|
|
author:
|
|
- "Milan Ilic (@ilicmilan)"
|
|
- "Jan Meerkamp (@meerkampdvv)"
|
|
"""
|
|
|
|
|
|
EXAMPLES = r"""
|
|
- name: Create a new instance
|
|
community.general.one_vm:
|
|
template_id: 90
|
|
register: result
|
|
|
|
- name: Print VM properties
|
|
ansible.builtin.debug:
|
|
msg: result
|
|
|
|
- name: Deploy a new VM on hold
|
|
community.general.one_vm:
|
|
template_name: 'app1_template'
|
|
vm_start_on_hold: 'True'
|
|
|
|
- name: Deploy a new VM and set its name to 'foo'
|
|
community.general.one_vm:
|
|
template_name: 'app1_template'
|
|
attributes:
|
|
name: foo
|
|
|
|
- name: Deploy a new VM and set its group_id and mode
|
|
community.general.one_vm:
|
|
template_id: 90
|
|
group_id: 16
|
|
mode: 660
|
|
|
|
- name: Deploy a new VM as persistent
|
|
community.general.one_vm:
|
|
template_id: 90
|
|
persistent: true
|
|
|
|
- name: Change VM's permissions to 640
|
|
community.general.one_vm:
|
|
instance_ids: 5
|
|
mode: 640
|
|
|
|
- name: Deploy 2 new instances and set memory, vcpu, disk_size and 3 networks
|
|
community.general.one_vm:
|
|
template_id: 15
|
|
disk_size: 35.2 GB
|
|
memory: 4 GB
|
|
vcpu: 4
|
|
count: 2
|
|
networks:
|
|
- NETWORK_ID: 27
|
|
- NETWORK: "default-network"
|
|
NETWORK_UNAME: "app-user"
|
|
SECURITY_GROUPS: "120,124"
|
|
- NETWORK_ID: 27
|
|
SECURITY_GROUPS: "10"
|
|
|
|
- name: Deploy a new instance which uses a Template with two Disks
|
|
community.general.one_vm:
|
|
template_id: 42
|
|
disk_size:
|
|
- 35.2 GB
|
|
- 50 GB
|
|
memory: 4 GB
|
|
vcpu: 4
|
|
count: 1
|
|
networks:
|
|
- NETWORK_ID: 27
|
|
|
|
- name: "Deploy an new instance with attribute 'bar: bar1' and set its name to 'foo'"
|
|
community.general.one_vm:
|
|
template_id: 53
|
|
attributes:
|
|
name: foo
|
|
bar: bar1
|
|
|
|
- name: "Enforce that 2 instances with attributes 'foo1: app1' and 'foo2: app2' are deployed"
|
|
community.general.one_vm:
|
|
template_id: 53
|
|
attributes:
|
|
foo1: app1
|
|
foo2: app2
|
|
exact_count: 2
|
|
count_attributes:
|
|
foo1: app1
|
|
foo2: app2
|
|
|
|
- name: Enforce that 4 instances with an attribute 'bar' are deployed
|
|
community.general.one_vm:
|
|
template_id: 53
|
|
attributes:
|
|
name: app
|
|
bar: bar2
|
|
exact_count: 4
|
|
count_attributes:
|
|
bar:
|
|
|
|
# Deploy 2 new instances with attribute 'foo: bar' and labels 'app1' and 'app2' and names in format 'fooapp-##'
|
|
# Names will be: fooapp-00 and fooapp-01
|
|
- name: Deploy 2 new instances
|
|
community.general.one_vm:
|
|
template_id: 53
|
|
attributes:
|
|
name: fooapp-##
|
|
foo: bar
|
|
labels:
|
|
- app1
|
|
- app2
|
|
count: 2
|
|
|
|
# Deploy 2 new instances with attribute 'app: app1' and names in format 'fooapp-###'
|
|
# Names will be: fooapp-002 and fooapp-003
|
|
- name: Deploy 2 new instances
|
|
community.general.one_vm:
|
|
template_id: 53
|
|
attributes:
|
|
name: fooapp-###
|
|
app: app1
|
|
count: 2
|
|
|
|
# Reboot all instances with name in format 'fooapp-#'
|
|
# Instances 'fooapp-00', 'fooapp-01', 'fooapp-002' and 'fooapp-003' will be rebooted
|
|
- name: Reboot all instances with names in a certain format
|
|
community.general.one_vm:
|
|
attributes:
|
|
name: fooapp-#
|
|
state: rebooted
|
|
|
|
# Enforce that only 1 instance with name in format 'fooapp-#' is deployed
|
|
# The task will delete oldest instances, so only the 'fooapp-003' will remain
|
|
- name: Enforce that only 1 instance with name in a certain format is deployed
|
|
community.general.one_vm:
|
|
template_id: 53
|
|
exact_count: 1
|
|
count_attributes:
|
|
name: fooapp-#
|
|
|
|
- name: Deploy an new instance with a network
|
|
community.general.one_vm:
|
|
template_id: 53
|
|
networks:
|
|
- NETWORK_ID: 27
|
|
register: vm
|
|
|
|
- name: Wait for SSH to come up
|
|
ansible.builtin.wait_for:
|
|
port: 22
|
|
host: '{{ vm.instances[0].networks[0].ip }}'
|
|
|
|
- name: Terminate VMs by ids
|
|
community.general.one_vm:
|
|
instance_ids:
|
|
- 153
|
|
- 160
|
|
state: absent
|
|
|
|
- name: Reboot all VMs that have labels 'foo' and 'app1'
|
|
community.general.one_vm:
|
|
labels:
|
|
- foo
|
|
- app1
|
|
state: rebooted
|
|
|
|
- name: "Fetch all VMs that have name 'foo' and attribute 'app: bar'"
|
|
community.general.one_vm:
|
|
attributes:
|
|
name: foo
|
|
app: bar
|
|
register: results
|
|
|
|
- name: Deploy 2 new instances with labels 'foo1' and 'foo2'
|
|
community.general.one_vm:
|
|
template_name: app_template
|
|
labels:
|
|
- foo1
|
|
- foo2
|
|
count: 2
|
|
|
|
- name: Enforce that only 1 instance with label 'foo1' will be running
|
|
community.general.one_vm:
|
|
template_name: app_template
|
|
labels:
|
|
- foo1
|
|
exact_count: 1
|
|
count_labels:
|
|
- foo1
|
|
|
|
- name: Terminate all instances that have attribute foo
|
|
community.general.one_vm:
|
|
template_id: 53
|
|
exact_count: 0
|
|
count_attributes:
|
|
foo:
|
|
|
|
- name: "Power-off the VM and save VM's disk with id=0 to the image with name 'foo-image'"
|
|
community.general.one_vm:
|
|
instance_ids: 351
|
|
state: poweredoff
|
|
disk_saveas:
|
|
name: foo-image
|
|
|
|
- name: "Save VM's disk with id=1 to the image with name 'bar-image'"
|
|
community.general.one_vm:
|
|
instance_ids: 351
|
|
disk_saveas:
|
|
name: bar-image
|
|
disk_id: 1
|
|
|
|
- name: "Deploy 2 new instances with a custom 'start script'"
|
|
community.general.one_vm:
|
|
template_name: app_template
|
|
count: 2
|
|
updateconf:
|
|
CONTEXT:
|
|
START_SCRIPT: ip r r 169.254.16.86/32 dev eth0
|
|
|
|
- name: "Add a custom 'start script' to a running VM"
|
|
community.general.one_vm:
|
|
instance_ids: 351
|
|
updateconf:
|
|
CONTEXT:
|
|
START_SCRIPT: ip r r 169.254.16.86/32 dev eth0
|
|
|
|
- name: "Update SSH public keys inside the VM's context"
|
|
community.general.one_vm:
|
|
instance_ids: 351
|
|
updateconf:
|
|
CONTEXT:
|
|
SSH_PUBLIC_KEY: |-
|
|
ssh-rsa ...
|
|
ssh-ed25519 ...
|
|
"""
|
|
|
|
RETURN = r"""
|
|
instances_ids:
|
|
description: A list of instances IDs whose state is changed or which are fetched with O(instance_ids) option.
|
|
type: list
|
|
returned: success
|
|
sample: [1234, 1235]
|
|
instances:
|
|
description: A list of instances info whose state is changed or which are fetched with O(instance_ids) option.
|
|
type: complex
|
|
returned: success
|
|
contains:
|
|
vm_id:
|
|
description: VM ID.
|
|
type: int
|
|
sample: 153
|
|
vm_name:
|
|
description: VM name.
|
|
type: str
|
|
sample: foo
|
|
template_id:
|
|
description: VM's template ID.
|
|
type: int
|
|
sample: 153
|
|
group_id:
|
|
description: VM's group ID.
|
|
type: int
|
|
sample: 1
|
|
group_name:
|
|
description: VM's group name.
|
|
type: str
|
|
sample: one-users
|
|
owner_id:
|
|
description: VM's owner ID.
|
|
type: int
|
|
sample: 143
|
|
owner_name:
|
|
description: VM's owner name.
|
|
type: str
|
|
sample: app-user
|
|
mode:
|
|
description: VM's mode.
|
|
type: str
|
|
returned: success
|
|
sample: 660
|
|
state:
|
|
description: State of an instance.
|
|
type: str
|
|
sample: ACTIVE
|
|
lcm_state:
|
|
description: Lcm state of an instance that is only relevant when the state is ACTIVE.
|
|
type: str
|
|
sample: RUNNING
|
|
cpu:
|
|
description: Percentage of CPU divided by 100.
|
|
type: float
|
|
sample: 0.2
|
|
vcpu:
|
|
description: Number of CPUs (cores).
|
|
type: int
|
|
sample: 2
|
|
memory:
|
|
description: The size of the memory in MB.
|
|
type: str
|
|
sample: 4096 MB
|
|
disk_size:
|
|
description: The size of the disk in MB.
|
|
type: str
|
|
sample: 20480 MB
|
|
networks:
|
|
description: A list of dictionaries with info about IP, NAME, MAC, SECURITY_GROUPS for each NIC.
|
|
type: list
|
|
sample:
|
|
[
|
|
{
|
|
"ip": "10.120.5.33",
|
|
"mac": "02:00:0a:78:05:21",
|
|
"name": "default-test-private",
|
|
"security_groups": "0,10"
|
|
},
|
|
{
|
|
"ip": "10.120.5.34",
|
|
"mac": "02:00:0a:78:05:22",
|
|
"name": "default-test-private",
|
|
"security_groups": "0"
|
|
}
|
|
]
|
|
uptime_h:
|
|
description: Uptime of the instance in hours.
|
|
type: int
|
|
sample: 35
|
|
labels:
|
|
description: A list of string labels that are associated with the instance.
|
|
type: list
|
|
sample: ["foo", "spec-label"]
|
|
attributes:
|
|
description: A dictionary of key/values attributes that are associated with the instance.
|
|
type: dict
|
|
sample:
|
|
{
|
|
"HYPERVISOR": "kvm",
|
|
"LOGO": "images/logos/centos.png",
|
|
"TE_GALAXY": "bar",
|
|
"USER_INPUTS": null
|
|
}
|
|
updateconf:
|
|
description: A dictionary of key/values attributes that are set with the updateconf API call.
|
|
type: dict
|
|
version_added: 6.3.0
|
|
sample:
|
|
{
|
|
"OS": {
|
|
"ARCH": "x86_64"
|
|
},
|
|
"CONTEXT": {
|
|
"START_SCRIPT": "ip r r 169.254.16.86/32 dev eth0",
|
|
"SSH_PUBLIC_KEY": "ssh-rsa ...\\nssh-ed25519 ..."
|
|
}
|
|
}
|
|
tagged_instances:
|
|
description:
|
|
- A list of instances info based on a specific attributes and/or labels that are specified with O(count_attributes) and
|
|
O(count_labels) options.
|
|
type: complex
|
|
returned: success
|
|
contains:
|
|
vm_id:
|
|
description: VM ID.
|
|
type: int
|
|
sample: 153
|
|
vm_name:
|
|
description: VM name.
|
|
type: str
|
|
sample: foo
|
|
template_id:
|
|
description: VM's template ID.
|
|
type: int
|
|
sample: 153
|
|
group_id:
|
|
description: VM's group ID.
|
|
type: int
|
|
sample: 1
|
|
group_name:
|
|
description: VM's group name.
|
|
type: str
|
|
sample: one-users
|
|
owner_id:
|
|
description: VM's user ID.
|
|
type: int
|
|
sample: 143
|
|
owner_name:
|
|
description: VM's user name.
|
|
type: str
|
|
sample: app-user
|
|
mode:
|
|
description: VM's mode.
|
|
type: str
|
|
returned: success
|
|
sample: 660
|
|
state:
|
|
description: State of an instance.
|
|
type: str
|
|
sample: ACTIVE
|
|
lcm_state:
|
|
description: Lcm state of an instance that is only relevant when the state is ACTIVE.
|
|
type: str
|
|
sample: RUNNING
|
|
cpu:
|
|
description: Percentage of CPU divided by 100.
|
|
type: float
|
|
sample: 0.2
|
|
vcpu:
|
|
description: Number of CPUs (cores).
|
|
type: int
|
|
sample: 2
|
|
memory:
|
|
description: The size of the memory in MB.
|
|
type: str
|
|
sample: 4096 MB
|
|
disk_size:
|
|
description: The size of the disk in MB.
|
|
type: list
|
|
sample: ["20480 MB", "10240 MB"]
|
|
networks:
|
|
description: A list of dictionaries with info about IP, NAME, MAC, SECURITY_GROUPS for each NIC.
|
|
type: list
|
|
sample:
|
|
[
|
|
{
|
|
"ip": "10.120.5.33",
|
|
"mac": "02:00:0a:78:05:21",
|
|
"name": "default-test-private",
|
|
"security_groups": "0,10"
|
|
},
|
|
{
|
|
"ip": "10.120.5.34",
|
|
"mac": "02:00:0a:78:05:22",
|
|
"name": "default-test-private",
|
|
"security_groups": "0"
|
|
}
|
|
]
|
|
uptime_h:
|
|
description: Uptime of the instance in hours.
|
|
type: int
|
|
sample: 35
|
|
labels:
|
|
description: A list of string labels that are associated with the instance.
|
|
type: list
|
|
sample: ["foo", "spec-label"]
|
|
attributes:
|
|
description: A dictionary of key/values attributes that are associated with the instance.
|
|
type: dict
|
|
sample:
|
|
{
|
|
"HYPERVISOR": "kvm",
|
|
"LOGO": "images/logos/centos.png",
|
|
"TE_GALAXY": "bar",
|
|
"USER_INPUTS": null
|
|
}
|
|
updateconf:
|
|
description: A dictionary of key/values attributes that are set with the updateconf API call.
|
|
type: dict
|
|
version_added: 6.3.0
|
|
sample:
|
|
{
|
|
"OS": {
|
|
"ARCH": "x86_64"
|
|
},
|
|
"CONTEXT": {
|
|
"START_SCRIPT": "ip r r 169.254.16.86/32 dev eth0",
|
|
"SSH_PUBLIC_KEY": "ssh-rsa ...\\nssh-ed25519 ..."
|
|
}
|
|
}
|
|
"""
|
|
|
|
try:
|
|
import pyone
|
|
|
|
HAS_PYONE = True
|
|
except ImportError:
|
|
HAS_PYONE = False
|
|
|
|
|
|
import os
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils.common.dict_transformations import dict_merge
|
|
|
|
from ansible_collections.community.general.plugins.module_utils.opennebula import flatten, render
|
|
|
|
|
|
# Updateconf attributes documentation: https://docs.opennebula.io/6.10/integration_and_development/system_interfaces/api.html#one-vm-updateconf
|
|
UPDATECONF_ATTRIBUTES = {
|
|
"OS": ["ARCH", "MACHINE", "KERNEL", "INITRD", "BOOTLOADER", "BOOT", "SD_DISK_BUS", "UUID", "FIRMWARE"],
|
|
"CPU_MODEL": ["MODEL", "FEATURES"],
|
|
"FEATURES": [
|
|
"ACPI",
|
|
"PAE",
|
|
"APIC",
|
|
"LOCALTIME",
|
|
"HYPERV",
|
|
"GUEST_AGENT",
|
|
"VIRTIO_BLK_QUEUES",
|
|
"VIRTIO_SCSI_QUEUES",
|
|
"IOTHREADS",
|
|
],
|
|
"INPUT": ["TYPE", "BUS"],
|
|
"GRAPHICS": ["TYPE", "LISTEN", "PORT", "PASSWD", "KEYMAP", "COMMAND"],
|
|
"VIDEO": ["ATS", "IOMMU", "RESOLUTION", "TYPE", "VRAM"],
|
|
"RAW": ["DATA", "DATA_VMX", "TYPE", "VALIDATE"],
|
|
"CONTEXT": [],
|
|
"BACKUP_CONFIG": ["FS_FREEZE", "KEEP_LAST", "BACKUP_VOLATILE", "MODE", "INCREMENT_MODE"],
|
|
}
|
|
|
|
|
|
def check_updateconf(module, to_check):
|
|
"""Checks if attributes are compatible with one.vm.updateconf API call."""
|
|
for attr, subattributes in to_check.items():
|
|
if attr not in UPDATECONF_ATTRIBUTES:
|
|
module.fail_json(msg=f"'{attr}' is not a valid VM attribute.")
|
|
if not UPDATECONF_ATTRIBUTES[attr]:
|
|
continue
|
|
for subattr in subattributes:
|
|
if subattr not in UPDATECONF_ATTRIBUTES[attr]:
|
|
module.fail_json(msg=f"'{subattr}' is not a valid VM subattribute of '{attr}'")
|
|
|
|
|
|
def parse_updateconf(vm_template):
|
|
"""Extracts 'updateconf' attributes from a VM template."""
|
|
updateconf = {}
|
|
for attr, subattributes in vm_template.items():
|
|
if attr not in UPDATECONF_ATTRIBUTES:
|
|
continue
|
|
tmp = {}
|
|
for subattr, value in subattributes.items():
|
|
if UPDATECONF_ATTRIBUTES[attr] and subattr not in UPDATECONF_ATTRIBUTES[attr]:
|
|
continue
|
|
tmp[subattr] = value
|
|
if tmp:
|
|
updateconf[attr] = tmp
|
|
return updateconf
|
|
|
|
|
|
def get_template(module, client, predicate):
|
|
pool = client.templatepool.info(-2, -1, -1, -1)
|
|
# Filter -2 means fetch all templates user can Use
|
|
found = 0
|
|
found_template = None
|
|
template_name = ""
|
|
|
|
for template in pool.VMTEMPLATE:
|
|
if predicate(template):
|
|
found = found + 1
|
|
found_template = template
|
|
template_name = template.NAME
|
|
|
|
if found == 0:
|
|
return None
|
|
elif found > 1:
|
|
module.fail_json(msg=f"There are more templates with name: {template_name}")
|
|
return found_template
|
|
|
|
|
|
def get_template_by_name(module, client, template_name):
|
|
return get_template(module, client, lambda template: (template_name == template.NAME))
|
|
|
|
|
|
def get_template_by_id(module, client, template_id):
|
|
return get_template(module, client, lambda template: (template_id == template.ID))
|
|
|
|
|
|
def get_template_id(module, client, requested_id, requested_name):
|
|
template = (
|
|
get_template_by_id(module, client, requested_id)
|
|
if requested_id is not None
|
|
else get_template_by_name(module, client, requested_name)
|
|
)
|
|
if template:
|
|
return template.ID
|
|
else:
|
|
return None
|
|
|
|
|
|
def get_datastore(module, client, predicate):
|
|
pool = client.datastorepool.info()
|
|
found = 0
|
|
found_datastore = None
|
|
datastore_name = ""
|
|
|
|
for datastore in pool.DATASTORE:
|
|
if predicate(datastore):
|
|
found = found + 1
|
|
found_datastore = datastore
|
|
datastore_name = datastore.NAME
|
|
|
|
if found == 0:
|
|
return None
|
|
elif found > 1:
|
|
module.fail_json(msg=f"There are more datastores with name: {datastore_name}")
|
|
return found_datastore
|
|
|
|
|
|
def get_datastore_by_name(module, client, datastore_name):
|
|
return get_datastore(module, client, lambda datastore: (datastore_name == datastore.NAME))
|
|
|
|
|
|
def get_datastore_by_id(module, client, datastore_id):
|
|
return get_datastore(module, client, lambda datastore: (datastore_id == datastore.ID))
|
|
|
|
|
|
def get_datastore_id(module, client, requested_id, requested_name):
|
|
datastore = (
|
|
get_datastore_by_id(module, client, requested_id)
|
|
if requested_id
|
|
else get_datastore_by_name(module, client, requested_name)
|
|
)
|
|
if datastore:
|
|
return datastore.ID
|
|
else:
|
|
return None
|
|
|
|
|
|
def get_vm_by_id(client, vm_id):
|
|
try:
|
|
vm = client.vm.info(int(vm_id))
|
|
except BaseException:
|
|
return None
|
|
return vm
|
|
|
|
|
|
def get_vms_by_ids(module, client, state, ids):
|
|
vms = []
|
|
|
|
for vm_id in ids:
|
|
vm = get_vm_by_id(client, vm_id)
|
|
if vm is None and state != "absent":
|
|
module.fail_json(msg=f"There is no VM with id={vm_id}")
|
|
vms.append(vm)
|
|
|
|
return vms
|
|
|
|
|
|
def get_vm_info(client, vm):
|
|
vm = client.vm.info(vm.ID)
|
|
|
|
networks_info = []
|
|
|
|
disk_size = []
|
|
if "DISK" in vm.TEMPLATE:
|
|
if isinstance(vm.TEMPLATE["DISK"], list):
|
|
for disk in vm.TEMPLATE["DISK"]:
|
|
disk_size.append(f"{disk['SIZE']} MB")
|
|
else:
|
|
disk_size.append(f"{vm.TEMPLATE['DISK']['SIZE']} MB")
|
|
|
|
if "NIC" in vm.TEMPLATE:
|
|
if isinstance(vm.TEMPLATE["NIC"], list):
|
|
for nic in vm.TEMPLATE["NIC"]:
|
|
networks_info.append(
|
|
{
|
|
"ip": nic.get("IP", ""),
|
|
"mac": nic.get("MAC", ""),
|
|
"name": nic.get("NETWORK", ""),
|
|
"security_groups": nic.get("SECURITY_GROUPS", ""),
|
|
}
|
|
)
|
|
else:
|
|
networks_info.append(
|
|
{
|
|
"ip": vm.TEMPLATE["NIC"].get("IP", ""),
|
|
"mac": vm.TEMPLATE["NIC"].get("MAC", ""),
|
|
"name": vm.TEMPLATE["NIC"].get("NETWORK", ""),
|
|
"security_groups": vm.TEMPLATE["NIC"].get("SECURITY_GROUPS", ""),
|
|
}
|
|
)
|
|
import time
|
|
|
|
current_time = time.localtime()
|
|
vm_start_time = time.localtime(vm.STIME)
|
|
|
|
vm_uptime = time.mktime(current_time) - time.mktime(vm_start_time)
|
|
vm_uptime /= 60 * 60
|
|
|
|
permissions_str = parse_vm_permissions(client, vm)
|
|
|
|
# LCM_STATE is VM's sub-state that is relevant only when STATE is ACTIVE
|
|
vm_lcm_state = None
|
|
if VM_STATES.index("ACTIVE") == vm.STATE:
|
|
vm_lcm_state = LCM_STATES[vm.LCM_STATE]
|
|
|
|
vm_labels, vm_attributes = get_vm_labels_and_attributes_dict(client, vm.ID)
|
|
|
|
updateconf = parse_updateconf(vm.TEMPLATE)
|
|
|
|
info = {
|
|
"template_id": int(vm.TEMPLATE["TEMPLATE_ID"]),
|
|
"vm_id": vm.ID,
|
|
"vm_name": vm.NAME,
|
|
"state": VM_STATES[vm.STATE],
|
|
"lcm_state": vm_lcm_state,
|
|
"owner_name": vm.UNAME,
|
|
"owner_id": vm.UID,
|
|
"networks": networks_info,
|
|
"disk_size": disk_size,
|
|
"memory": f"{vm.TEMPLATE['MEMORY']} MB",
|
|
"vcpu": vm.TEMPLATE["VCPU"],
|
|
"cpu": vm.TEMPLATE["CPU"],
|
|
"group_name": vm.GNAME,
|
|
"group_id": vm.GID,
|
|
"uptime_h": int(vm_uptime),
|
|
"attributes": vm_attributes,
|
|
"mode": permissions_str,
|
|
"labels": vm_labels,
|
|
"updateconf": updateconf,
|
|
}
|
|
|
|
return info
|
|
|
|
|
|
def parse_vm_permissions(client, vm):
|
|
vm_PERMISSIONS = client.vm.info(vm.ID).PERMISSIONS
|
|
|
|
owner_octal = int(vm_PERMISSIONS.OWNER_U) * 4 + int(vm_PERMISSIONS.OWNER_M) * 2 + int(vm_PERMISSIONS.OWNER_A)
|
|
group_octal = int(vm_PERMISSIONS.GROUP_U) * 4 + int(vm_PERMISSIONS.GROUP_M) * 2 + int(vm_PERMISSIONS.GROUP_A)
|
|
other_octal = int(vm_PERMISSIONS.OTHER_U) * 4 + int(vm_PERMISSIONS.OTHER_M) * 2 + int(vm_PERMISSIONS.OTHER_A)
|
|
|
|
permissions = str(owner_octal) + str(group_octal) + str(other_octal)
|
|
|
|
return permissions
|
|
|
|
|
|
def set_vm_permissions(module, client, vms, permissions):
|
|
changed = False
|
|
|
|
for vm in vms:
|
|
vm = client.vm.info(vm.ID)
|
|
old_permissions = parse_vm_permissions(client, vm)
|
|
changed = changed or old_permissions != permissions
|
|
|
|
if not module.check_mode and old_permissions != permissions:
|
|
permissions_str = bin(int(permissions, base=8))[2:] # 600 -> 110000000
|
|
mode_bits = [int(d) for d in permissions_str]
|
|
try:
|
|
client.vm.chmod(
|
|
vm.ID,
|
|
mode_bits[0],
|
|
mode_bits[1],
|
|
mode_bits[2],
|
|
mode_bits[3],
|
|
mode_bits[4],
|
|
mode_bits[5],
|
|
mode_bits[6],
|
|
mode_bits[7],
|
|
mode_bits[8],
|
|
)
|
|
except pyone.OneAuthorizationException:
|
|
module.fail_json(
|
|
msg="Permissions changing is unsuccessful, but instances are present if you deployed them."
|
|
)
|
|
|
|
return changed
|
|
|
|
|
|
def set_vm_ownership(module, client, vms, owner_id, group_id):
|
|
changed = False
|
|
|
|
for vm in vms:
|
|
vm = client.vm.info(vm.ID)
|
|
if owner_id is None:
|
|
owner_id = vm.UID
|
|
if group_id is None:
|
|
group_id = vm.GID
|
|
|
|
changed = changed or owner_id != vm.UID or group_id != vm.GID
|
|
|
|
if not module.check_mode and (owner_id != vm.UID or group_id != vm.GID):
|
|
try:
|
|
client.vm.chown(vm.ID, owner_id, group_id)
|
|
except pyone.OneAuthorizationException:
|
|
module.fail_json(
|
|
msg="Ownership changing is unsuccessful, but instances are present if you deployed them."
|
|
)
|
|
|
|
return changed
|
|
|
|
|
|
def update_vm(module, client, vm, updateconf_dict):
|
|
changed = False
|
|
if not updateconf_dict:
|
|
return changed
|
|
|
|
before = client.vm.info(vm.ID).TEMPLATE
|
|
|
|
client.vm.updateconf(vm.ID, render(updateconf_dict), 1) # 1: Merge new template with the existing one.
|
|
|
|
after = client.vm.info(vm.ID).TEMPLATE
|
|
|
|
changed = before != after
|
|
return changed
|
|
|
|
|
|
def update_vms(module, client, vms, *args):
|
|
changed = False
|
|
for vm in vms:
|
|
changed = update_vm(module, client, vm, *args) or changed
|
|
return changed
|
|
|
|
|
|
def get_size_in_MB(module, size_str):
|
|
SYMBOLS = ["B", "KB", "MB", "GB", "TB"]
|
|
|
|
s = size_str
|
|
init = size_str
|
|
num = ""
|
|
while s and s[0:1].isdigit() or s[0:1] == ".":
|
|
num += s[0]
|
|
s = s[1:]
|
|
num = float(num)
|
|
symbol = s.strip()
|
|
|
|
if symbol not in SYMBOLS:
|
|
module.fail_json(msg=f"Cannot interpret {init!r} {symbol!r} {num}")
|
|
|
|
prefix = {"B": 1}
|
|
|
|
for i, s in enumerate(SYMBOLS[1:]):
|
|
prefix[s] = 1 << (i + 1) * 10
|
|
|
|
size_in_bytes = int(num * prefix[symbol])
|
|
size_in_MB = size_in_bytes / (1024 * 1024)
|
|
|
|
return size_in_MB
|
|
|
|
|
|
def create_vm(
|
|
module,
|
|
client,
|
|
template_id,
|
|
attributes_dict,
|
|
labels_list,
|
|
disk_size,
|
|
network_attrs_list,
|
|
vm_start_on_hold,
|
|
vm_persistent,
|
|
updateconf_dict,
|
|
):
|
|
if attributes_dict:
|
|
vm_name = attributes_dict.get("NAME", "")
|
|
|
|
template = client.template.info(template_id).TEMPLATE
|
|
|
|
disk_count = len(flatten(template.get("DISK", [])))
|
|
if disk_size:
|
|
size_count = len(flatten(disk_size))
|
|
# check if the number of disks is correct
|
|
if disk_count != size_count:
|
|
module.fail_json(msg=f"This template has {disk_count} disks but you defined {size_count}")
|
|
|
|
vm_extra_template = dict_merge(template or {}, attributes_dict or {})
|
|
vm_extra_template = dict_merge(
|
|
vm_extra_template,
|
|
{
|
|
"LABELS": ",".join(labels_list),
|
|
"NIC": flatten(network_attrs_list, extract=True),
|
|
"DISK": flatten(
|
|
[
|
|
disk
|
|
if not size
|
|
else dict_merge(
|
|
disk,
|
|
{
|
|
"SIZE": str(int(get_size_in_MB(module, size))),
|
|
},
|
|
)
|
|
for disk, size in zip(
|
|
flatten(template.get("DISK", [])),
|
|
flatten(disk_size or [None] * disk_count),
|
|
)
|
|
if disk is not None
|
|
],
|
|
extract=True,
|
|
),
|
|
},
|
|
)
|
|
vm_extra_template = dict_merge(vm_extra_template, updateconf_dict or {})
|
|
|
|
try:
|
|
vm_id = client.template.instantiate(
|
|
template_id, vm_name, vm_start_on_hold, render(vm_extra_template), vm_persistent
|
|
)
|
|
except pyone.OneException as e:
|
|
module.fail_json(msg=str(e))
|
|
|
|
vm = get_vm_by_id(client, vm_id)
|
|
return get_vm_info(client, vm)
|
|
|
|
|
|
def generate_next_index(vm_filled_indexes_list, num_sign_cnt):
|
|
counter = 0
|
|
cnt_str = str(counter).zfill(num_sign_cnt)
|
|
|
|
while cnt_str in vm_filled_indexes_list:
|
|
counter = counter + 1
|
|
cnt_str = str(counter).zfill(num_sign_cnt)
|
|
|
|
return cnt_str
|
|
|
|
|
|
def get_vm_labels_and_attributes_dict(client, vm_id):
|
|
vm_USER_TEMPLATE = client.vm.info(vm_id).USER_TEMPLATE
|
|
|
|
attrs_dict = {}
|
|
labels_list = []
|
|
|
|
for key, value in vm_USER_TEMPLATE.items():
|
|
if key != "LABELS":
|
|
attrs_dict[key] = value
|
|
else:
|
|
if key is not None and value is not None:
|
|
labels_list = value.split(",")
|
|
|
|
return labels_list, attrs_dict
|
|
|
|
|
|
def get_all_vms_by_attributes(client, attributes_dict, labels_list):
|
|
pool = client.vmpool.info(-2, -1, -1, -1).VM
|
|
vm_list = []
|
|
name = ""
|
|
if attributes_dict:
|
|
name = attributes_dict.pop("NAME", "")
|
|
|
|
if name != "":
|
|
base_name = name[: len(name) - name.count("#")]
|
|
# Check does the name have indexed format
|
|
with_hash = name.endswith("#")
|
|
|
|
for vm in pool:
|
|
if vm.NAME.startswith(base_name):
|
|
if with_hash and vm.NAME[len(base_name) :].isdigit():
|
|
# If the name has indexed format and after base_name it has only digits it'll be matched
|
|
vm_list.append(vm)
|
|
elif not with_hash and name == vm.NAME:
|
|
# If the name is not indexed it has to be same
|
|
vm_list.append(vm)
|
|
pool = vm_list
|
|
|
|
import copy
|
|
|
|
vm_list = copy.copy(pool)
|
|
|
|
for vm in pool:
|
|
remove_list = []
|
|
vm_labels_list, vm_attributes_dict = get_vm_labels_and_attributes_dict(client, vm.ID)
|
|
|
|
if attributes_dict and len(attributes_dict) > 0:
|
|
for key, val in attributes_dict.items():
|
|
if key in vm_attributes_dict:
|
|
if val and vm_attributes_dict[key] != val:
|
|
remove_list.append(vm)
|
|
break
|
|
else:
|
|
remove_list.append(vm)
|
|
break
|
|
vm_list = list(set(vm_list).difference(set(remove_list)))
|
|
|
|
remove_list = []
|
|
if labels_list and len(labels_list) > 0:
|
|
for label in labels_list:
|
|
if label not in vm_labels_list:
|
|
remove_list.append(vm)
|
|
break
|
|
vm_list = list(set(vm_list).difference(set(remove_list)))
|
|
|
|
return vm_list
|
|
|
|
|
|
def create_count_of_vms(
|
|
module,
|
|
client,
|
|
template_id,
|
|
count,
|
|
attributes_dict,
|
|
labels_list,
|
|
disk_size,
|
|
network_attrs_list,
|
|
wait,
|
|
wait_timeout,
|
|
vm_start_on_hold,
|
|
vm_persistent,
|
|
updateconf_dict,
|
|
):
|
|
new_vms_list = []
|
|
|
|
vm_name = ""
|
|
if attributes_dict:
|
|
vm_name = attributes_dict.get("NAME", "")
|
|
|
|
if module.check_mode:
|
|
return True, [], []
|
|
|
|
# Create list of used indexes
|
|
vm_filled_indexes_list = None
|
|
num_sign_cnt = vm_name.count("#")
|
|
if vm_name != "" and num_sign_cnt > 0:
|
|
vm_list = get_all_vms_by_attributes(client, {"NAME": vm_name}, None)
|
|
base_name = vm_name[: len(vm_name) - num_sign_cnt]
|
|
vm_name = base_name
|
|
# Make list which contains used indexes in format ['000', '001',...]
|
|
vm_filled_indexes_list = [vm.NAME[len(base_name) :].zfill(num_sign_cnt) for vm in vm_list]
|
|
|
|
while count > 0:
|
|
new_vm_name = vm_name
|
|
# Create indexed name
|
|
if vm_filled_indexes_list is not None:
|
|
next_index = generate_next_index(vm_filled_indexes_list, num_sign_cnt)
|
|
vm_filled_indexes_list.append(next_index)
|
|
new_vm_name += next_index
|
|
# Update NAME value in the attributes in case there is index
|
|
attributes_dict["NAME"] = new_vm_name
|
|
new_vm_dict = create_vm(
|
|
module,
|
|
client,
|
|
template_id,
|
|
attributes_dict,
|
|
labels_list,
|
|
disk_size,
|
|
network_attrs_list,
|
|
vm_start_on_hold,
|
|
vm_persistent,
|
|
updateconf_dict,
|
|
)
|
|
new_vm_id = new_vm_dict.get("vm_id")
|
|
new_vm = get_vm_by_id(client, new_vm_id)
|
|
new_vms_list.append(new_vm)
|
|
count -= 1
|
|
|
|
if vm_start_on_hold:
|
|
if wait:
|
|
for vm in new_vms_list:
|
|
wait_for_hold(module, client, vm, wait_timeout)
|
|
else:
|
|
if wait:
|
|
for vm in new_vms_list:
|
|
wait_for_running(module, client, vm, wait_timeout)
|
|
|
|
return True, new_vms_list, []
|
|
|
|
|
|
def create_exact_count_of_vms(
|
|
module,
|
|
client,
|
|
template_id,
|
|
exact_count,
|
|
attributes_dict,
|
|
count_attributes_dict,
|
|
labels_list,
|
|
count_labels_list,
|
|
disk_size,
|
|
network_attrs_list,
|
|
hard,
|
|
wait,
|
|
wait_timeout,
|
|
vm_start_on_hold,
|
|
vm_persistent,
|
|
updateconf_dict,
|
|
):
|
|
vm_list = get_all_vms_by_attributes(client, count_attributes_dict, count_labels_list)
|
|
|
|
vm_count_diff = exact_count - len(vm_list)
|
|
changed = vm_count_diff != 0
|
|
|
|
instances_list = []
|
|
tagged_instances_list = vm_list
|
|
|
|
if module.check_mode:
|
|
return changed, instances_list, tagged_instances_list
|
|
|
|
if vm_count_diff > 0:
|
|
# Add more VMs
|
|
changed, instances_list, tagged_instances = create_count_of_vms(
|
|
module,
|
|
client,
|
|
template_id,
|
|
vm_count_diff,
|
|
attributes_dict,
|
|
labels_list,
|
|
disk_size,
|
|
network_attrs_list,
|
|
wait,
|
|
wait_timeout,
|
|
vm_start_on_hold,
|
|
vm_persistent,
|
|
updateconf_dict,
|
|
)
|
|
|
|
tagged_instances_list += instances_list
|
|
elif vm_count_diff < 0:
|
|
# Delete surplus VMs
|
|
old_vms_list = []
|
|
|
|
while vm_count_diff < 0:
|
|
old_vm = vm_list.pop(0)
|
|
old_vms_list.append(old_vm)
|
|
terminate_vm(module, client, old_vm, hard)
|
|
vm_count_diff += 1
|
|
|
|
if wait:
|
|
for vm in old_vms_list:
|
|
wait_for_done(module, client, vm, wait_timeout)
|
|
|
|
instances_list = old_vms_list
|
|
# store only the remaining instances
|
|
old_vms_set = set(old_vms_list)
|
|
tagged_instances_list = [vm for vm in vm_list if vm not in old_vms_set]
|
|
|
|
return changed, instances_list, tagged_instances_list
|
|
|
|
|
|
VM_STATES = [
|
|
"INIT",
|
|
"PENDING",
|
|
"HOLD",
|
|
"ACTIVE",
|
|
"STOPPED",
|
|
"SUSPENDED",
|
|
"DONE",
|
|
"",
|
|
"POWEROFF",
|
|
"UNDEPLOYED",
|
|
"CLONING",
|
|
"CLONING_FAILURE",
|
|
]
|
|
LCM_STATES = [
|
|
"LCM_INIT",
|
|
"PROLOG",
|
|
"BOOT",
|
|
"RUNNING",
|
|
"MIGRATE",
|
|
"SAVE_STOP",
|
|
"SAVE_SUSPEND",
|
|
"SAVE_MIGRATE",
|
|
"PROLOG_MIGRATE",
|
|
"PROLOG_RESUME",
|
|
"EPILOG_STOP",
|
|
"EPILOG",
|
|
"SHUTDOWN",
|
|
"STATE13",
|
|
"STATE14",
|
|
"CLEANUP_RESUBMIT",
|
|
"UNKNOWN",
|
|
"HOTPLUG",
|
|
"SHUTDOWN_POWEROFF",
|
|
"BOOT_UNKNOWN",
|
|
"BOOT_POWEROFF",
|
|
"BOOT_SUSPENDED",
|
|
"BOOT_STOPPED",
|
|
"CLEANUP_DELETE",
|
|
"HOTPLUG_SNAPSHOT",
|
|
"HOTPLUG_NIC",
|
|
"HOTPLUG_SAVEAS",
|
|
"HOTPLUG_SAVEAS_POWEROFF",
|
|
"HOTPULG_SAVEAS_SUSPENDED",
|
|
"SHUTDOWN_UNDEPLOY",
|
|
]
|
|
|
|
|
|
def wait_for_state(module, client, vm, wait_timeout, state_predicate):
|
|
import time
|
|
|
|
start_time = time.time()
|
|
|
|
while (time.time() - start_time) < wait_timeout:
|
|
vm = client.vm.info(vm.ID)
|
|
state = vm.STATE
|
|
lcm_state = vm.LCM_STATE
|
|
|
|
if state_predicate(state, lcm_state):
|
|
return vm
|
|
elif state not in [
|
|
VM_STATES.index("INIT"),
|
|
VM_STATES.index("PENDING"),
|
|
VM_STATES.index("HOLD"),
|
|
VM_STATES.index("ACTIVE"),
|
|
VM_STATES.index("CLONING"),
|
|
VM_STATES.index("POWEROFF"),
|
|
]:
|
|
module.fail_json(msg=f"Action is unsuccessful. VM state: {VM_STATES[state]}")
|
|
|
|
time.sleep(1)
|
|
|
|
module.fail_json(msg="Wait timeout has expired!")
|
|
|
|
|
|
def wait_for_running(module, client, vm, wait_timeout):
|
|
return wait_for_state(
|
|
module,
|
|
client,
|
|
vm,
|
|
wait_timeout,
|
|
lambda state, lcm_state: (state in [VM_STATES.index("ACTIVE")] and lcm_state in [LCM_STATES.index("RUNNING")]),
|
|
)
|
|
|
|
|
|
def wait_for_done(module, client, vm, wait_timeout):
|
|
return wait_for_state(
|
|
module, client, vm, wait_timeout, lambda state, lcm_state: (state in [VM_STATES.index("DONE")])
|
|
)
|
|
|
|
|
|
def wait_for_hold(module, client, vm, wait_timeout):
|
|
return wait_for_state(
|
|
module, client, vm, wait_timeout, lambda state, lcm_state: (state in [VM_STATES.index("HOLD")])
|
|
)
|
|
|
|
|
|
def wait_for_poweroff(module, client, vm, wait_timeout):
|
|
return wait_for_state(
|
|
module, client, vm, wait_timeout, lambda state, lcm_state: (state in [VM_STATES.index("POWEROFF")])
|
|
)
|
|
|
|
|
|
def terminate_vm(module, client, vm, hard=False):
|
|
changed = False
|
|
|
|
if not vm:
|
|
return changed
|
|
|
|
changed = True
|
|
|
|
if not module.check_mode:
|
|
if hard:
|
|
client.vm.action("terminate-hard", vm.ID)
|
|
else:
|
|
client.vm.action("terminate", vm.ID)
|
|
|
|
return changed
|
|
|
|
|
|
def terminate_vms(module, client, vms, hard):
|
|
changed = False
|
|
|
|
for vm in vms:
|
|
changed = terminate_vm(module, client, vm, hard) or changed
|
|
|
|
return changed
|
|
|
|
|
|
def poweroff_vm(module, client, vm, hard):
|
|
vm = client.vm.info(vm.ID)
|
|
changed = False
|
|
|
|
lcm_state = vm.LCM_STATE
|
|
state = vm.STATE
|
|
|
|
if lcm_state not in [LCM_STATES.index("SHUTDOWN"), LCM_STATES.index("SHUTDOWN_POWEROFF")] and state not in [
|
|
VM_STATES.index("POWEROFF")
|
|
]:
|
|
changed = True
|
|
|
|
if changed and not module.check_mode:
|
|
if not hard:
|
|
client.vm.action("poweroff", vm.ID)
|
|
else:
|
|
client.vm.action("poweroff-hard", vm.ID)
|
|
|
|
return changed
|
|
|
|
|
|
def poweroff_vms(module, client, vms, hard):
|
|
changed = False
|
|
|
|
for vm in vms:
|
|
changed = poweroff_vm(module, client, vm, hard) or changed
|
|
|
|
return changed
|
|
|
|
|
|
def reboot_vms(module, client, vms, wait_timeout, hard):
|
|
if not module.check_mode:
|
|
# Firstly, power-off all instances
|
|
for vm in vms:
|
|
vm = client.vm.info(vm.ID)
|
|
lcm_state = vm.LCM_STATE
|
|
state = vm.STATE
|
|
if lcm_state not in [LCM_STATES.index("SHUTDOWN_POWEROFF")] and state not in [VM_STATES.index("POWEROFF")]:
|
|
poweroff_vm(module, client, vm, hard)
|
|
|
|
# Wait for all to be power-off
|
|
for vm in vms:
|
|
wait_for_poweroff(module, client, vm, wait_timeout)
|
|
|
|
for vm in vms:
|
|
resume_vm(module, client, vm)
|
|
|
|
return True
|
|
|
|
|
|
def resume_vm(module, client, vm):
|
|
vm = client.vm.info(vm.ID)
|
|
changed = False
|
|
|
|
state = vm.STATE
|
|
if state in [VM_STATES.index("HOLD")]:
|
|
changed = release_vm(module, client, vm)
|
|
return changed
|
|
|
|
lcm_state = vm.LCM_STATE
|
|
if lcm_state == LCM_STATES.index("SHUTDOWN_POWEROFF"):
|
|
module.fail_json(
|
|
msg="Cannot perform action 'resume' because this action is not available "
|
|
"for LCM_STATE: 'SHUTDOWN_POWEROFF'. Wait for the VM to shutdown properly"
|
|
)
|
|
if lcm_state not in [LCM_STATES.index("RUNNING")]:
|
|
changed = True
|
|
|
|
if changed and not module.check_mode:
|
|
client.vm.action("resume", vm.ID)
|
|
|
|
return changed
|
|
|
|
|
|
def resume_vms(module, client, vms):
|
|
changed = False
|
|
|
|
for vm in vms:
|
|
changed = resume_vm(module, client, vm) or changed
|
|
|
|
return changed
|
|
|
|
|
|
def release_vm(module, client, vm):
|
|
vm = client.vm.info(vm.ID)
|
|
changed = False
|
|
|
|
state = vm.STATE
|
|
if state != VM_STATES.index("HOLD"):
|
|
module.fail_json(
|
|
msg="Cannot perform action 'release' because this action is not available "
|
|
"because VM is not in state 'HOLD'."
|
|
)
|
|
else:
|
|
changed = True
|
|
|
|
if changed and not module.check_mode:
|
|
client.vm.action("release", vm.ID)
|
|
|
|
return changed
|
|
|
|
|
|
def check_name_attribute(module, attributes):
|
|
if attributes.get("NAME"):
|
|
import re
|
|
|
|
if re.match(r"^[^#]+#*$", attributes.get("NAME")) is None:
|
|
module.fail_json(
|
|
msg=f"Illegal 'NAME' attribute: '{attributes.get('NAME')}"
|
|
"' .Signs '#' are allowed only at the end of the name and the name cannot contain only '#'."
|
|
)
|
|
|
|
|
|
TEMPLATE_RESTRICTED_ATTRIBUTES = [
|
|
"CPU",
|
|
"VCPU",
|
|
"OS",
|
|
"FEATURES",
|
|
"MEMORY",
|
|
"DISK",
|
|
"NIC",
|
|
"INPUT",
|
|
"GRAPHICS",
|
|
"CONTEXT",
|
|
"CREATED_BY",
|
|
"CPU_COST",
|
|
"DISK_COST",
|
|
"MEMORY_COST",
|
|
"TEMPLATE_ID",
|
|
"VMID",
|
|
"AUTOMATIC_DS_REQUIREMENTS",
|
|
"DEPLOY_FOLDER",
|
|
"LABELS",
|
|
]
|
|
|
|
|
|
def check_attributes(module, attributes):
|
|
for key in attributes.keys():
|
|
if key in TEMPLATE_RESTRICTED_ATTRIBUTES:
|
|
module.fail_json(msg=f"Restricted attribute `{key}` cannot be used when filtering VMs.")
|
|
# Check the format of the name attribute
|
|
check_name_attribute(module, attributes)
|
|
|
|
|
|
def disk_save_as(module, client, vm, disk_saveas, wait_timeout):
|
|
if not disk_saveas.get("name"):
|
|
module.fail_json(msg="Key 'name' is required for 'disk_saveas' option")
|
|
|
|
image_name = disk_saveas.get("name")
|
|
disk_id = disk_saveas.get("disk_id", 0)
|
|
|
|
if not module.check_mode:
|
|
if VM_STATES.index("POWEROFF") != vm.STATE:
|
|
module.fail_json(msg="'disksaveas' option can be used only when the VM is in 'POWEROFF' state")
|
|
try:
|
|
client.vm.disksaveas(vm.ID, disk_id, image_name, "OS", -1)
|
|
except pyone.OneException as e:
|
|
module.fail_json(msg=str(e))
|
|
wait_for_poweroff(module, client, vm, wait_timeout) # wait for VM to leave the hotplug_saveas_poweroff state
|
|
|
|
|
|
def get_connection_info(module):
|
|
url = module.params.get("api_url")
|
|
username = module.params.get("api_username")
|
|
password = module.params.get("api_password")
|
|
|
|
if not url:
|
|
url = os.environ.get("ONE_URL")
|
|
|
|
if not username:
|
|
username = os.environ.get("ONE_USERNAME")
|
|
|
|
if not password:
|
|
password = os.environ.get("ONE_PASSWORD")
|
|
|
|
if not username:
|
|
if not password:
|
|
authfile = os.environ.get("ONE_AUTH")
|
|
if authfile is None:
|
|
authfile = os.path.join(os.environ.get("HOME"), ".one", "one_auth")
|
|
try:
|
|
with open(authfile, "r") as fp:
|
|
authstring = fp.read().rstrip()
|
|
username = authstring.split(":")[0]
|
|
password = authstring.split(":")[1]
|
|
except (OSError, IOError):
|
|
module.fail_json(msg=f"Could not find or read ONE_AUTH file at '{authfile}'")
|
|
except Exception:
|
|
module.fail_json(msg=f"Error occurs when read ONE_AUTH file at '{authfile}'")
|
|
if not url:
|
|
module.fail_json(msg="Opennebula API url (api_url) is not specified")
|
|
from collections import namedtuple
|
|
|
|
auth_params = namedtuple("auth", ("url", "username", "password"))
|
|
|
|
return auth_params(url=url, username=username, password=password)
|
|
|
|
|
|
def main():
|
|
fields = {
|
|
"api_url": {"required": False, "type": "str"},
|
|
"api_username": {"required": False, "type": "str"},
|
|
"api_password": {"required": False, "type": "str", "no_log": True},
|
|
"instance_ids": {"required": False, "aliases": ["ids"], "type": "list", "elements": "int"},
|
|
"template_name": {"required": False, "type": "str"},
|
|
"template_id": {"required": False, "type": "int"},
|
|
"vm_start_on_hold": {"default": False, "type": "bool"},
|
|
"state": {
|
|
"default": "present",
|
|
"choices": ["present", "absent", "rebooted", "poweredoff", "running"],
|
|
"type": "str",
|
|
},
|
|
"mode": {"required": False, "type": "str"},
|
|
"owner_id": {"required": False, "type": "int"},
|
|
"group_id": {"required": False, "type": "int"},
|
|
"wait": {"default": True, "type": "bool"},
|
|
"wait_timeout": {"default": 300, "type": "int"},
|
|
"hard": {"default": False, "type": "bool"},
|
|
"memory": {"required": False, "type": "str"},
|
|
"cpu": {"required": False, "type": "float"},
|
|
"vcpu": {"required": False, "type": "int"},
|
|
"disk_size": {"required": False, "type": "list", "elements": "str"},
|
|
"datastore_name": {"required": False, "type": "str"},
|
|
"datastore_id": {"required": False, "type": "int"},
|
|
"networks": {"default": [], "type": "list", "elements": "dict"},
|
|
"count": {"default": 1, "type": "int"},
|
|
"exact_count": {"required": False, "type": "int"},
|
|
"attributes": {"default": {}, "type": "dict"},
|
|
"count_attributes": {"required": False, "type": "dict"},
|
|
"labels": {"default": [], "type": "list", "elements": "str"},
|
|
"count_labels": {"required": False, "type": "list", "elements": "str"},
|
|
"disk_saveas": {"type": "dict"},
|
|
"persistent": {"default": False, "type": "bool"},
|
|
"updateconf": {"type": "dict"},
|
|
}
|
|
|
|
module = AnsibleModule(
|
|
argument_spec=fields,
|
|
mutually_exclusive=[
|
|
["template_id", "template_name", "instance_ids"],
|
|
["template_id", "template_name", "disk_saveas"],
|
|
["instance_ids", "count_attributes", "count"],
|
|
["instance_ids", "count_labels", "count"],
|
|
["instance_ids", "exact_count"],
|
|
["instance_ids", "attributes"],
|
|
["instance_ids", "labels"],
|
|
["disk_saveas", "attributes"],
|
|
["disk_saveas", "labels"],
|
|
["exact_count", "count"],
|
|
["count", "hard"],
|
|
["instance_ids", "cpu"],
|
|
["instance_ids", "vcpu"],
|
|
["instance_ids", "memory"],
|
|
["instance_ids", "disk_size"],
|
|
["instance_ids", "networks"],
|
|
["persistent", "disk_size"],
|
|
],
|
|
supports_check_mode=True,
|
|
)
|
|
|
|
if not HAS_PYONE:
|
|
module.fail_json(msg="This module requires pyone to work!")
|
|
|
|
auth = get_connection_info(module)
|
|
params = module.params
|
|
instance_ids = params.get("instance_ids")
|
|
requested_template_name = params.get("template_name")
|
|
requested_template_id = params.get("template_id")
|
|
put_vm_on_hold = params.get("vm_start_on_hold")
|
|
state = params.get("state")
|
|
permissions = params.get("mode")
|
|
owner_id = params.get("owner_id")
|
|
group_id = params.get("group_id")
|
|
wait = params.get("wait")
|
|
wait_timeout = params.get("wait_timeout")
|
|
hard = params.get("hard")
|
|
memory = params.get("memory")
|
|
cpu = params.get("cpu")
|
|
vcpu = params.get("vcpu")
|
|
disk_size = params.get("disk_size")
|
|
requested_datastore_id = params.get("datastore_id")
|
|
requested_datastore_name = params.get("datastore_name")
|
|
networks = params.get("networks")
|
|
count = params.get("count")
|
|
exact_count = params.get("exact_count")
|
|
attributes = params.get("attributes")
|
|
count_attributes = params.get("count_attributes")
|
|
labels = params.get("labels")
|
|
count_labels = params.get("count_labels")
|
|
disk_saveas = params.get("disk_saveas")
|
|
persistent = params.get("persistent")
|
|
updateconf = params.get("updateconf")
|
|
|
|
if not (auth.username and auth.password):
|
|
module.warn("Credentials missing")
|
|
else:
|
|
one_client = pyone.OneServer(auth.url, session=f"{auth.username}:{auth.password}")
|
|
|
|
if attributes:
|
|
attributes = {key.upper(): value for key, value in attributes.items()}
|
|
check_attributes(module, attributes)
|
|
|
|
if count_attributes:
|
|
count_attributes = {key.upper(): value for key, value in count_attributes.items()}
|
|
if not attributes:
|
|
import copy
|
|
|
|
module.warn(
|
|
"When you pass `count_attributes` without `attributes` option when deploying, `attributes` option will have same values implicitly."
|
|
)
|
|
attributes = copy.copy(count_attributes)
|
|
check_attributes(module, count_attributes)
|
|
|
|
if updateconf:
|
|
check_updateconf(module, updateconf)
|
|
|
|
if count_labels and not labels:
|
|
module.warn(
|
|
"When you pass `count_labels` without `labels` option when deploying, `labels` option will have same values implicitly."
|
|
)
|
|
labels = count_labels
|
|
|
|
# Fetch template
|
|
template_id = None
|
|
if requested_template_id is not None or requested_template_name:
|
|
template_id = get_template_id(module, one_client, requested_template_id, requested_template_name)
|
|
if template_id is None:
|
|
if requested_template_id is not None:
|
|
module.fail_json(msg=f"There is no template with template_id: {requested_template_id}")
|
|
elif requested_template_name:
|
|
module.fail_json(msg=f"There is no template with name: {requested_template_name}")
|
|
|
|
# Fetch datastore
|
|
datastore_id = None
|
|
if requested_datastore_id or requested_datastore_name:
|
|
datastore_id = get_datastore_id(module, one_client, requested_datastore_id, requested_datastore_name)
|
|
if datastore_id is None:
|
|
if requested_datastore_id:
|
|
module.fail_json(msg=f"There is no datastore with datastore_id: {requested_datastore_id}")
|
|
elif requested_datastore_name:
|
|
module.fail_json(msg=f"There is no datastore with name: {requested_datastore_name}")
|
|
else:
|
|
attributes["SCHED_DS_REQUIREMENTS"] = f"ID={datastore_id}"
|
|
|
|
if exact_count and template_id is None:
|
|
module.fail_json(msg="Option `exact_count` needs template_id or template_name")
|
|
|
|
if exact_count is not None and not (count_attributes or count_labels):
|
|
module.fail_json(
|
|
msg="Either `count_attributes` or `count_labels` has to be specified with option `exact_count`."
|
|
)
|
|
if (count_attributes or count_labels) and exact_count is None:
|
|
module.fail_json(
|
|
msg="Option `exact_count` has to be specified when either `count_attributes` or `count_labels` is used."
|
|
)
|
|
if template_id is not None and state != "present":
|
|
module.fail_json(msg="Only state 'present' is valid for the template")
|
|
|
|
if memory:
|
|
attributes["MEMORY"] = str(int(get_size_in_MB(module, memory)))
|
|
if cpu:
|
|
attributes["CPU"] = str(cpu)
|
|
if vcpu:
|
|
attributes["VCPU"] = str(vcpu)
|
|
|
|
if exact_count is not None and state != "present":
|
|
module.fail_json(msg="The `exact_count` option is valid only for the `present` state")
|
|
if exact_count is not None and exact_count < 0:
|
|
module.fail_json(msg="`exact_count` cannot be less than 0")
|
|
if count <= 0:
|
|
module.fail_json(msg="`count` has to be greater than 0")
|
|
|
|
if permissions is not None:
|
|
import re
|
|
|
|
if re.match("^[0-7]{3}$", permissions) is None:
|
|
module.fail_json(msg="Option `mode` has to have exactly 3 digits and be in the octet format e.g. 600")
|
|
|
|
if exact_count is not None:
|
|
# Deploy an exact count of VMs
|
|
changed, instances_list, tagged_instances_list = create_exact_count_of_vms(
|
|
module,
|
|
one_client,
|
|
template_id,
|
|
exact_count,
|
|
attributes,
|
|
count_attributes,
|
|
labels,
|
|
count_labels,
|
|
disk_size,
|
|
networks,
|
|
hard,
|
|
wait,
|
|
wait_timeout,
|
|
put_vm_on_hold,
|
|
persistent,
|
|
updateconf,
|
|
)
|
|
vms = tagged_instances_list
|
|
elif template_id is not None and state == "present":
|
|
# Deploy count VMs
|
|
changed, instances_list, tagged_instances_list = create_count_of_vms(
|
|
module,
|
|
one_client,
|
|
template_id,
|
|
count,
|
|
attributes,
|
|
labels,
|
|
disk_size,
|
|
networks,
|
|
wait,
|
|
wait_timeout,
|
|
put_vm_on_hold,
|
|
persistent,
|
|
updateconf,
|
|
)
|
|
# instances_list - new instances
|
|
# tagged_instances_list - all instances with specified `count_attributes` and `count_labels`
|
|
vms = instances_list
|
|
else:
|
|
# Fetch data of instances, or change their state
|
|
if not (instance_ids or attributes or labels):
|
|
module.fail_json(msg="At least one of `instance_ids`,`attributes`,`labels` must be passed!")
|
|
|
|
if memory or cpu or vcpu or disk_size or networks:
|
|
module.fail_json(
|
|
msg="Parameters as `memory`, `cpu`, `vcpu`, `disk_size` and `networks` you can only set when deploying a VM!"
|
|
)
|
|
|
|
if hard and state not in ["rebooted", "poweredoff", "absent", "present"]:
|
|
module.fail_json(
|
|
msg="The 'hard' option can be used only for one of these states: 'rebooted', 'poweredoff', 'absent' and 'present'"
|
|
)
|
|
|
|
vms = []
|
|
tagged = False
|
|
changed = False
|
|
|
|
if instance_ids:
|
|
vms = get_vms_by_ids(module, one_client, state, instance_ids)
|
|
else:
|
|
tagged = True
|
|
vms = get_all_vms_by_attributes(one_client, attributes, labels)
|
|
|
|
if len(vms) == 0 and state != "absent" and state != "present":
|
|
module.fail_json(msg="There are no instances with specified `instance_ids`, `attributes` and/or `labels`")
|
|
|
|
if len(vms) == 0 and state == "present" and not tagged:
|
|
module.fail_json(msg="There are no instances with specified `instance_ids`.")
|
|
|
|
if tagged and state == "absent":
|
|
module.fail_json(msg="Option `instance_ids` is required when state is `absent`.")
|
|
|
|
if state == "absent":
|
|
changed = terminate_vms(module, one_client, vms, hard)
|
|
elif state == "rebooted":
|
|
changed = reboot_vms(module, one_client, vms, wait_timeout, hard)
|
|
elif state == "poweredoff":
|
|
changed = poweroff_vms(module, one_client, vms, hard)
|
|
elif state == "running":
|
|
changed = resume_vms(module, one_client, vms)
|
|
|
|
instances_list = vms
|
|
tagged_instances_list = []
|
|
|
|
if permissions is not None:
|
|
changed = set_vm_permissions(module, one_client, vms, permissions) or changed
|
|
|
|
if owner_id is not None or group_id is not None:
|
|
changed = set_vm_ownership(module, one_client, vms, owner_id, group_id) or changed
|
|
|
|
if template_id is None and updateconf is not None:
|
|
changed = update_vms(module, one_client, vms, updateconf) or changed
|
|
|
|
if wait and not module.check_mode and state != "present":
|
|
wait_for = {
|
|
"absent": wait_for_done,
|
|
"rebooted": wait_for_running,
|
|
"poweredoff": wait_for_poweroff,
|
|
"running": wait_for_running,
|
|
}
|
|
for vm in vms:
|
|
if vm is not None:
|
|
wait_for[state](module, one_client, vm, wait_timeout)
|
|
|
|
if disk_saveas is not None:
|
|
if len(vms) == 0:
|
|
module.fail_json(msg="There is no VM whose disk will be saved.")
|
|
disk_save_as(module, one_client, vms[0], disk_saveas, wait_timeout)
|
|
changed = True
|
|
|
|
# instances - a list of instances info whose state is changed or which are fetched with C(instance_ids) option
|
|
instances = list(get_vm_info(one_client, vm) for vm in instances_list if vm is not None)
|
|
instances_ids = list(vm.ID for vm in instances_list if vm is not None)
|
|
# tagged_instances - A list of instances info based on a specific attributes and/or labels that are specified with C(count_attributes) and C(count_labels)
|
|
tagged_instances = list(get_vm_info(one_client, vm) for vm in tagged_instances_list if vm is not None)
|
|
|
|
result = {
|
|
"changed": changed,
|
|
"instances": instances,
|
|
"instances_ids": instances_ids,
|
|
"tagged_instances": tagged_instances,
|
|
}
|
|
|
|
module.exit_json(**result)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|