#!/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: list
  elements: dict
  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: list
  elements: dict
  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 copy
import os
import re
import time
from dataclasses import dataclass

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


@dataclass
class AuthParams:
    url: str
    username: str
    password: str


# 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", ""),
                }
            )

    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

    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):
    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"):
        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) as fp:
                    authstring = fp.read().rstrip()
                username = authstring.split(":")[0]
                password = authstring.split(":")[1]
            except OSError:
                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")

    return AuthParams(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:
            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:
        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()
