mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
New module: Proxmox disk management (#5101)
* New module: Proxmox disk management * Remove misplaced option * Type missed * Fixed docs, quotes, 2.7 syntax * Forgotten comma * Version added 5.5.0 Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Italic options Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Missed dot Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Pythonify python Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Shorten command Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Action parameter drop. General improvements. * Add proxmox_disk integration testing * Shorten getting vmid Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Code tag for value Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Italic tag for option Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Definite ID of the VM Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Docs edit and loop condition * Simplify conditions Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Dropped bps options, added idempotency checks * Documentaion edit * Rewrite create/import condition * Trainling comma Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Added type field to all choosable arguments * Description of disk bus ranges * Fix imports * Update version Co-authored-by: Felix Fontein <felix@fontein.de> * Lowercase YAML boolean * Rename grown to resized and update documentation * Documentation updated before actual changes * Added 'update' flag for 'present' state * Traling space * YAML indentation * Merged 'updated' option into 'present'. * Doc update. * Exclude 'import_from' on update * Version bump * yaml boolean lowercase Co-authored-by: Felix Fontein <felix@fontein.de> * yaml boolean lowercase Co-authored-by: Felix Fontein <felix@fontein.de> * More detailed description Co-authored-by: Felix Fontein <felix@fontein.de> Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
35e3a9615a
commit
7777b48c99
4 changed files with 944 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
@ -378,6 +378,8 @@ files:
|
|||
$modules/cloud/misc/proxmox_template.py:
|
||||
maintainers: UnderGreen
|
||||
ignore: skvidal
|
||||
$modules/cloud/misc/proxmox_disk.py:
|
||||
maintainers: castorsky
|
||||
$modules/cloud/misc/rhevm.py:
|
||||
maintainers: $team_virt TimothyVandenbrande
|
||||
labels: rhevm virt
|
||||
|
|
|
@ -1213,6 +1213,8 @@ plugin_routing:
|
|||
redirect: community.general.cloud.profitbricks.profitbricks_volume_attachments
|
||||
proxmox:
|
||||
redirect: community.general.cloud.misc.proxmox
|
||||
proxmox_disk:
|
||||
redirect: community.general.cloud.misc.proxmox_disk
|
||||
proxmox_domain_info:
|
||||
redirect: community.general.cloud.misc.proxmox_domain_info
|
||||
proxmox_group_info:
|
||||
|
|
744
plugins/modules/cloud/misc/proxmox_disk.py
Normal file
744
plugins/modules/cloud/misc/proxmox_disk.py
Normal file
|
@ -0,0 +1,744 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2022, Castor Sky (@castorsky) <csky57@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: proxmox_disk
|
||||
short_description: Management of a disk of a Qemu(KVM) VM in a Proxmox VE cluster.
|
||||
version_added: 5.7.0
|
||||
description:
|
||||
- Allows you to perform some supported operations on a disk in Qemu(KVM) Virtual Machines in a Proxmox VE cluster.
|
||||
author: "Castor Sky (@castorsky) <csky57@gmail.com>"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- The unique name of the VM.
|
||||
- You can specify either I(name) or I(vmid) or both of them.
|
||||
type: str
|
||||
vmid:
|
||||
description:
|
||||
- The unique ID of the VM.
|
||||
- You can specify either I(vmid) or I(name) or both of them.
|
||||
type: int
|
||||
disk:
|
||||
description:
|
||||
- The disk key (C(unused[n]), C(ide[n]), C(sata[n]), C(scsi[n]) or C(virtio[n])) you want to operate on.
|
||||
- Disk buses (IDE, SATA and so on) have fixed ranges of C(n) that accepted by Proxmox API.
|
||||
- >
|
||||
For IDE: 0-3;
|
||||
for SCSI: 0-30;
|
||||
for SATA: 0-5;
|
||||
for VirtIO: 0-15;
|
||||
for Unused: 0-255.
|
||||
type: str
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Indicates desired state of the disk.
|
||||
- >
|
||||
I(state=present) can be used to create, replace disk or update options in existing disk. It will create missing
|
||||
disk or update options in existing one by default. See the I(create) parameter description to control behavior
|
||||
of this option.
|
||||
- Some updates on options (like I(cache)) are not being applied instantly and require VM restart.
|
||||
- >
|
||||
Use I(state=detached) to detach existing disk from VM but do not remove it entirely.
|
||||
When I(state=detached) and disk is C(unused[n]) it will be left in same state (not removed).
|
||||
- >
|
||||
I(state=moved) may be used to change backing storage for the disk in bounds of the same VM
|
||||
or to send the disk to another VM (using the same backing storage).
|
||||
- >
|
||||
I(state=resized) intended to change the disk size. As of Proxmox 7.2 you can only increase the disk size
|
||||
because shrinking disks is not supported by the PVE API and has to be done manually.
|
||||
- To entirely remove the disk from backing storage use I(state=absent).
|
||||
type: str
|
||||
choices: ['present', 'resized', 'detached', 'moved', 'absent']
|
||||
default: present
|
||||
create:
|
||||
description:
|
||||
- With I(create) flag you can control behavior of I(state=present).
|
||||
- When I(create=disabled) it will not create new disk (if not exists) but will update options in existing disk.
|
||||
- When I(create=regular) it will either create new disk (if not exists) or update options in existing disk.
|
||||
- When I(create=forced) it will always create new disk (if disk exists it will be detached and left unused).
|
||||
type: str
|
||||
choices: ['disabled', 'regular', 'forced']
|
||||
default: regular
|
||||
storage:
|
||||
description:
|
||||
- The drive's backing storage.
|
||||
- Used only when I(state) is C(present).
|
||||
type: str
|
||||
size:
|
||||
description:
|
||||
- Desired volume size in GB to allocate when I(state=present) (specify I(size) without suffix).
|
||||
- >
|
||||
New (or additional) size of volume when I(state=resized). With the C(+) sign
|
||||
the value is added to the actual size of the volume
|
||||
and without it, the value is taken as an absolute one.
|
||||
type: str
|
||||
bwlimit:
|
||||
description:
|
||||
- Override I/O bandwidth limit (in KB/s).
|
||||
- Used only when I(state=moved).
|
||||
type: int
|
||||
delete_moved:
|
||||
description:
|
||||
- Delete the original disk after successful copy.
|
||||
- By default the original disk is kept as unused disk.
|
||||
- Used only when I(state=moved).
|
||||
type: bool
|
||||
target_disk:
|
||||
description:
|
||||
- The config key the disk will be moved to on the target VM (for example, C(ide0) or C(scsi1)).
|
||||
- Default is the source disk key.
|
||||
- Used only when I(state=moved).
|
||||
type: str
|
||||
target_storage:
|
||||
description:
|
||||
- Move the disk to this storage when I(state=moved).
|
||||
- You can move between storages only in scope of one VM.
|
||||
- Mutually exclusive with I(target_vmid).
|
||||
type: str
|
||||
target_vmid:
|
||||
description:
|
||||
- The (unique) ID of the VM where disk will be placed when I(state=moved).
|
||||
- You can move disk between VMs only when the same storage is used.
|
||||
- Mutually exclusive with I(target_vmid).
|
||||
type: int
|
||||
timeout:
|
||||
description:
|
||||
- Timeout in seconds to wait when moving disk.
|
||||
- Used only when I(state=moved).
|
||||
type: int
|
||||
default: 600
|
||||
aio:
|
||||
description:
|
||||
- AIO type to use.
|
||||
type: str
|
||||
choices: ['native', 'threads', 'io_uring']
|
||||
backup:
|
||||
description:
|
||||
- Whether the drive should be included when making backups.
|
||||
type: bool
|
||||
bps_max_length:
|
||||
description:
|
||||
- Maximum length of total r/w I/O bursts in seconds.
|
||||
type: int
|
||||
bps_rd_max_length:
|
||||
description:
|
||||
- Maximum length of read I/O bursts in seconds.
|
||||
type: int
|
||||
bps_wr_max_length:
|
||||
description:
|
||||
- Maximum length of write I/O bursts in seconds.
|
||||
type: int
|
||||
cache:
|
||||
description:
|
||||
- The drive's cache mode.
|
||||
type: str
|
||||
choices: ['none', 'writethrough', 'writeback', 'unsafe', 'directsync']
|
||||
cyls:
|
||||
description:
|
||||
- Force the drive's physical geometry to have a specific cylinder count.
|
||||
type: int
|
||||
detect_zeroes:
|
||||
description:
|
||||
- Control whether to detect and try to optimize writes of zeroes.
|
||||
type: bool
|
||||
discard:
|
||||
description:
|
||||
- Control whether to pass discard/trim requests to the underlying storage.
|
||||
type: str
|
||||
choices: ['ignore', 'on']
|
||||
format:
|
||||
description:
|
||||
- The drive's backing file's data format.
|
||||
type: str
|
||||
choices: ['raw', 'cow', 'qcow', 'qed', 'qcow2', 'vmdk', 'cloop']
|
||||
heads:
|
||||
description:
|
||||
- Force the drive's physical geometry to have a specific head count.
|
||||
type: int
|
||||
import_from:
|
||||
description:
|
||||
- Import volume from this existing one.
|
||||
- Volume string format
|
||||
- C(<STORAGE>:<VMID>/<FULL_NAME>) or C(<ABSOLUTE_PATH>/<FULL_NAME>)
|
||||
- Attention! Only root can use absolute paths.
|
||||
- This parameter is mutually exclusive with I(size).
|
||||
type: str
|
||||
iops:
|
||||
description:
|
||||
- Maximum total r/w I/O in operations per second.
|
||||
- You can specify either total limit or per operation (mutually exclusive with I(iops_rd) and I(iops_wr)).
|
||||
type: int
|
||||
iops_max:
|
||||
description:
|
||||
- Maximum unthrottled total r/w I/O pool in operations per second.
|
||||
type: int
|
||||
iops_max_length:
|
||||
description:
|
||||
- Maximum length of total r/w I/O bursts in seconds.
|
||||
type: int
|
||||
iops_rd:
|
||||
description:
|
||||
- Maximum read I/O in operations per second.
|
||||
- You can specify either read or total limit (mutually exclusive with I(iops)).
|
||||
type: int
|
||||
iops_rd_max:
|
||||
description:
|
||||
- Maximum unthrottled read I/O pool in operations per second.
|
||||
type: int
|
||||
iops_rd_max_length:
|
||||
description:
|
||||
- Maximum length of read I/O bursts in seconds.
|
||||
type: int
|
||||
iops_wr:
|
||||
description:
|
||||
- Maximum write I/O in operations per second.
|
||||
- You can specify either write or total limit (mutually exclusive with I(iops)).
|
||||
type: int
|
||||
iops_wr_max:
|
||||
description:
|
||||
- Maximum unthrottled write I/O pool in operations per second.
|
||||
type: int
|
||||
iops_wr_max_length:
|
||||
description:
|
||||
- Maximum length of write I/O bursts in seconds.
|
||||
type: int
|
||||
iothread:
|
||||
description:
|
||||
- Whether to use iothreads for this drive (only for SCSI and VirtIO)
|
||||
type: bool
|
||||
mbps:
|
||||
description:
|
||||
- Maximum total r/w speed in megabytes per second.
|
||||
- Can be fractional but use with caution - fractionals less than 1 are not supported officially.
|
||||
- You can specify either total limit or per operation (mutually exclusive with I(mbps_rd) and I(mbps_wr)).
|
||||
type: float
|
||||
mbps_max:
|
||||
description:
|
||||
- Maximum unthrottled total r/w pool in megabytes per second.
|
||||
type: float
|
||||
mbps_rd:
|
||||
description:
|
||||
- Maximum read speed in megabytes per second.
|
||||
- You can specify either read or total limit (mutually exclusive with I(mbps)).
|
||||
type: float
|
||||
mbps_rd_max:
|
||||
description:
|
||||
- Maximum unthrottled read pool in megabytes per second.
|
||||
type: float
|
||||
mbps_wr:
|
||||
description:
|
||||
- Maximum write speed in megabytes per second.
|
||||
- You can specify either write or total limit (mutually exclusive with I(mbps)).
|
||||
type: float
|
||||
mbps_wr_max:
|
||||
description:
|
||||
- Maximum unthrottled write pool in megabytes per second.
|
||||
type: float
|
||||
media:
|
||||
description:
|
||||
- The drive's media type.
|
||||
type: str
|
||||
choices: ['cdrom', 'disk']
|
||||
queues:
|
||||
description:
|
||||
- Number of queues (SCSI only).
|
||||
type: int
|
||||
replicate:
|
||||
description:
|
||||
- Whether the drive should considered for replication jobs.
|
||||
type: bool
|
||||
rerror:
|
||||
description:
|
||||
- Read error action.
|
||||
type: str
|
||||
choices: ['ignore', 'report', 'stop']
|
||||
ro:
|
||||
description:
|
||||
- Whether the drive is read-only.
|
||||
type: bool
|
||||
scsiblock:
|
||||
description:
|
||||
- Whether to use scsi-block for full passthrough of host block device.
|
||||
- Can lead to I/O errors in combination with low memory or high memory fragmentation on host.
|
||||
type: bool
|
||||
secs:
|
||||
description:
|
||||
- Force the drive's physical geometry to have a specific sector count.
|
||||
type: int
|
||||
serial:
|
||||
description:
|
||||
- The drive's reported serial number, url-encoded, up to 20 bytes long.
|
||||
type: str
|
||||
shared:
|
||||
description:
|
||||
- Mark this locally-managed volume as available on all nodes.
|
||||
- This option does not share the volume automatically, it assumes it is shared already!
|
||||
type: bool
|
||||
snapshot:
|
||||
description:
|
||||
- Control qemu's snapshot mode feature.
|
||||
- If activated, changes made to the disk are temporary and will be discarded when the VM is shutdown.
|
||||
type: bool
|
||||
ssd:
|
||||
description:
|
||||
- Whether to expose this drive as an SSD, rather than a rotational hard disk.
|
||||
type: bool
|
||||
trans:
|
||||
description:
|
||||
- Force disk geometry bios translation mode.
|
||||
type: str
|
||||
choices: ['auto', 'lba', 'none']
|
||||
werror:
|
||||
description:
|
||||
- Write error action.
|
||||
type: str
|
||||
choices: ['enospc', 'ignore', 'report', 'stop']
|
||||
wwn:
|
||||
description:
|
||||
- The drive's worldwide name, encoded as 16 bytes hex string, prefixed by C(0x).
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.documentation
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create new disk in VM (do not rewrite in case it exists already)
|
||||
community.general.proxmox_disk:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_token_id: token1
|
||||
api_token_secret: some-token-data
|
||||
name: vm-name
|
||||
disk: scsi3
|
||||
backup: true
|
||||
cache: none
|
||||
storage: local-zfs
|
||||
size: 5
|
||||
state: present
|
||||
|
||||
- name: Create new disk in VM (force rewrite in case it exists already)
|
||||
community.general.proxmox_disk:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_token_id: token1
|
||||
api_token_secret: some-token-data
|
||||
vmid: 101
|
||||
disk: scsi3
|
||||
format: qcow2
|
||||
storage: local
|
||||
size: 16
|
||||
create: forced
|
||||
state: present
|
||||
|
||||
- name: Update existing disk
|
||||
community.general.proxmox_disk:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_token_id: token1
|
||||
api_token_secret: some-token-data
|
||||
vmid: 101
|
||||
disk: ide0
|
||||
backup: false
|
||||
ro: true
|
||||
aio: native
|
||||
state: present
|
||||
|
||||
- name: Grow existing disk
|
||||
community.general.proxmox_disk:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_token_id: token1
|
||||
api_token_secret: some-token-data
|
||||
vmid: 101
|
||||
disk: sata4
|
||||
size: +5G
|
||||
state: resized
|
||||
|
||||
- name: Detach disk (leave it unused)
|
||||
community.general.proxmox_disk:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_token_id: token1
|
||||
api_token_secret: some-token-data
|
||||
name: vm-name
|
||||
disk: virtio0
|
||||
state: detached
|
||||
|
||||
- name: Move disk to another storage
|
||||
community.general.proxmox_disk:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_password: secret
|
||||
vmid: 101
|
||||
disk: scsi7
|
||||
target_storage: local
|
||||
format: qcow2
|
||||
state: moved
|
||||
|
||||
- name: Move disk from one VM to another
|
||||
community.general.proxmox_disk:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_token_id: token1
|
||||
api_token_secret: some-token-data
|
||||
vmid: 101
|
||||
disk: scsi7
|
||||
target_vmid: 201
|
||||
state: moved
|
||||
|
||||
- name: Remove disk permanently
|
||||
community.general.proxmox_disk:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_password: secret
|
||||
vmid: 101
|
||||
disk: scsi4
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
vmid:
|
||||
description: The VM vmid.
|
||||
returned: success
|
||||
type: int
|
||||
sample: 101
|
||||
msg:
|
||||
description: A short message on what the module did.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Disk scsi3 created in VM 101"
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (proxmox_auth_argument_spec,
|
||||
ProxmoxAnsible)
|
||||
from re import compile, match, sub
|
||||
from time import sleep
|
||||
|
||||
|
||||
def disk_conf_str_to_dict(config_string):
|
||||
config = config_string.split(',')
|
||||
storage_volume = config.pop(0).split(':')
|
||||
config.sort()
|
||||
storage_name = storage_volume[0]
|
||||
volume_name = storage_volume[1]
|
||||
config_current = dict(
|
||||
volume='%s:%s' % (storage_name, volume_name),
|
||||
storage_name=storage_name,
|
||||
volume_name=volume_name
|
||||
)
|
||||
|
||||
for option in config:
|
||||
k, v = option.split('=')
|
||||
config_current[k] = v
|
||||
|
||||
return config_current
|
||||
|
||||
|
||||
class ProxmoxDiskAnsible(ProxmoxAnsible):
|
||||
create_update_fields = [
|
||||
'aio', 'backup', 'bps_max_length', 'bps_rd_max_length', 'bps_wr_max_length',
|
||||
'cache', 'cyls', 'detect_zeroes', 'discard', 'format', 'heads', 'import_from', 'iops', 'iops_max',
|
||||
'iops_max_length', 'iops_rd', 'iops_rd_max', 'iops_rd_max_length', 'iops_wr', 'iops_wr_max',
|
||||
'iops_wr_max_length', 'iothread', 'mbps', 'mbps_max', 'mbps_rd', 'mbps_rd_max', 'mbps_wr', 'mbps_wr_max',
|
||||
'media', 'queues', 'replicate', 'rerror', 'ro', 'scsiblock', 'secs', 'serial', 'shared', 'snapshot',
|
||||
'ssd', 'trans', 'werror', 'wwn'
|
||||
]
|
||||
supported_bus_num_ranges = dict(
|
||||
ide=range(0, 4),
|
||||
scsi=range(0, 31),
|
||||
sata=range(0, 6),
|
||||
virtio=range(0, 16),
|
||||
unused=range(0, 256)
|
||||
)
|
||||
|
||||
def get_create_attributes(self):
|
||||
# Sanitize parameters dictionary:
|
||||
# - Remove not defined args
|
||||
# - Ensure True and False converted to int.
|
||||
# - Remove unnecessary parameters
|
||||
params = dict((k, v) for k, v in self.module.params.items() if v is not None and k in self.create_update_fields)
|
||||
params.update(dict((k, int(v)) for k, v in params.items() if isinstance(v, bool)))
|
||||
return params
|
||||
|
||||
def create_disk(self, disk, vmid, vm, vm_config):
|
||||
create = self.module.params['create']
|
||||
if create == 'disabled' and disk not in vm_config:
|
||||
# NOOP
|
||||
return False, "Disk %s not found in VM %s and creation was disabled in parameters." % (disk, vmid)
|
||||
|
||||
if (create == 'regular' and disk not in vm_config) or (create == 'forced'):
|
||||
# CREATE
|
||||
attributes = self.get_create_attributes()
|
||||
import_string = attributes.pop('import_from', None)
|
||||
|
||||
if import_string:
|
||||
config_str = "%s:%s,import-from=%s" % (self.module.params["storage"], "0", import_string)
|
||||
else:
|
||||
config_str = "%s:%s" % (self.module.params["storage"], self.module.params["size"])
|
||||
|
||||
for k, v in attributes.items():
|
||||
config_str += ',%s=%s' % (k, v)
|
||||
|
||||
create_disk = {self.module.params["disk"]: config_str}
|
||||
self.proxmox_api.nodes(vm['node']).qemu(vmid).config.set(**create_disk)
|
||||
return True, "Disk %s created in VM %s" % (disk, vmid)
|
||||
|
||||
if create in ['disabled', 'regular'] and disk in vm_config:
|
||||
# UPDATE
|
||||
disk_config = disk_conf_str_to_dict(vm_config[disk])
|
||||
config_str = disk_config["volume"]
|
||||
attributes = self.get_create_attributes()
|
||||
# 'import_from' fails on disk updates
|
||||
attributes.pop('import_from', None)
|
||||
|
||||
for k, v in attributes.items():
|
||||
config_str += ',%s=%s' % (k, v)
|
||||
|
||||
# Now compare old and new config to detect if changes are needed
|
||||
for option in ['size', 'storage_name', 'volume', 'volume_name']:
|
||||
attributes.update({option: disk_config[option]})
|
||||
# Values in params are numbers, but strings are needed to compare with disk_config
|
||||
attributes = dict((k, str(v)) for k, v in attributes.items())
|
||||
if disk_config == attributes:
|
||||
return False, "Disk %s is up to date in VM %s" % (disk, vmid)
|
||||
|
||||
update_disk = {self.module.params["disk"]: config_str}
|
||||
self.proxmox_api.nodes(vm['node']).qemu(vmid).config.set(**update_disk)
|
||||
return True, "Disk %s updated in VM %s" % (disk, vmid)
|
||||
|
||||
def move_disk(self, disk, vmid, vm, vm_config):
|
||||
params = dict()
|
||||
params['disk'] = disk
|
||||
params['vmid'] = vmid
|
||||
params['bwlimit'] = self.module.params['bwlimit']
|
||||
params['storage'] = self.module.params['target_storage']
|
||||
params['target-disk'] = self.module.params['target_disk']
|
||||
params['target-vmid'] = self.module.params['target_vmid']
|
||||
params['format'] = self.module.params['format']
|
||||
params['delete'] = 1 if self.module.params.get('delete_moved', False) else 0
|
||||
# Remove not defined args
|
||||
params = dict((k, v) for k, v in params.items() if v is not None)
|
||||
|
||||
if params.get('storage', False):
|
||||
disk_config = disk_conf_str_to_dict(vm_config[disk])
|
||||
if params['storage'] == disk_config['storage_name']:
|
||||
return False
|
||||
|
||||
taskid = self.proxmox_api.nodes(vm['node']).qemu(vmid).move_disk.post(**params)
|
||||
timeout = self.module.params['timeout']
|
||||
while timeout:
|
||||
status_data = self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()
|
||||
if status_data['status'] == 'stopped' and status_data['exitstatus'] == 'OK':
|
||||
return True
|
||||
if timeout <= 0:
|
||||
self.module.fail_json(
|
||||
msg='Reached timeout while waiting for moving VM disk. Last line in task before timeout: %s' %
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
|
||||
|
||||
sleep(1)
|
||||
timeout -= 1
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
disk_args = dict(
|
||||
# Proxmox native parameters
|
||||
aio=dict(type='str', choices=['native', 'threads', 'io_uring']),
|
||||
backup=dict(type='bool'),
|
||||
bps_max_length=dict(type='int'),
|
||||
bps_rd_max_length=dict(type='int'),
|
||||
bps_wr_max_length=dict(type='int'),
|
||||
cache=dict(type='str', choices=['none', 'writethrough', 'writeback', 'unsafe', 'directsync']),
|
||||
cyls=dict(type='int'),
|
||||
detect_zeroes=dict(type='bool'),
|
||||
discard=dict(type='str', choices=['ignore', 'on']),
|
||||
format=dict(type='str', choices=['raw', 'cow', 'qcow', 'qed', 'qcow2', 'vmdk', 'cloop']),
|
||||
heads=dict(type='int'),
|
||||
import_from=dict(type='str'),
|
||||
iops=dict(type='int'),
|
||||
iops_max=dict(type='int'),
|
||||
iops_max_length=dict(type='int'),
|
||||
iops_rd=dict(type='int'),
|
||||
iops_rd_max=dict(type='int'),
|
||||
iops_rd_max_length=dict(type='int'),
|
||||
iops_wr=dict(type='int'),
|
||||
iops_wr_max=dict(type='int'),
|
||||
iops_wr_max_length=dict(type='int'),
|
||||
iothread=dict(type='bool'),
|
||||
mbps=dict(type='float'),
|
||||
mbps_max=dict(type='float'),
|
||||
mbps_rd=dict(type='float'),
|
||||
mbps_rd_max=dict(type='float'),
|
||||
mbps_wr=dict(type='float'),
|
||||
mbps_wr_max=dict(type='float'),
|
||||
media=dict(type='str', choices=['cdrom', 'disk']),
|
||||
queues=dict(type='int'),
|
||||
replicate=dict(type='bool'),
|
||||
rerror=dict(type='str', choices=['ignore', 'report', 'stop']),
|
||||
ro=dict(type='bool'),
|
||||
scsiblock=dict(type='bool'),
|
||||
secs=dict(type='int'),
|
||||
serial=dict(type='str'),
|
||||
shared=dict(type='bool'),
|
||||
snapshot=dict(type='bool'),
|
||||
ssd=dict(type='bool'),
|
||||
trans=dict(type='str', choices=['auto', 'lba', 'none']),
|
||||
werror=dict(type='str', choices=['enospc', 'ignore', 'report', 'stop']),
|
||||
wwn=dict(type='str'),
|
||||
|
||||
# Disk moving relates parameters
|
||||
bwlimit=dict(type='int'),
|
||||
target_storage=dict(type='str'),
|
||||
target_disk=dict(type='str'),
|
||||
target_vmid=dict(type='int'),
|
||||
delete_moved=dict(type='bool'),
|
||||
timeout=dict(type='int', default='600'),
|
||||
|
||||
# Module related parameters
|
||||
name=dict(type='str'),
|
||||
vmid=dict(type='int'),
|
||||
disk=dict(type='str', required=True),
|
||||
storage=dict(type='str'),
|
||||
size=dict(type='str'),
|
||||
state=dict(type='str', choices=['present', 'resized', 'detached', 'moved', 'absent'],
|
||||
default='present'),
|
||||
create=dict(type='str', choices=['disabled', 'regular', 'forced'], default='regular'),
|
||||
)
|
||||
|
||||
module_args.update(disk_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_together=[('api_token_id', 'api_token_secret')],
|
||||
required_one_of=[('name', 'vmid'), ('api_password', 'api_token_id')],
|
||||
required_if=[
|
||||
('create', 'forced', ['storage']),
|
||||
('state', 'resized', ['size']),
|
||||
],
|
||||
required_by={
|
||||
'target_disk': 'target_vmid',
|
||||
'mbps_max': 'mbps',
|
||||
'mbps_rd_max': 'mbps_rd',
|
||||
'mbps_wr_max': 'mbps_wr',
|
||||
'bps_max_length': 'mbps_max',
|
||||
'bps_rd_max_length': 'mbps_rd_max',
|
||||
'bps_wr_max_length': 'mbps_wr_max',
|
||||
'iops_max': 'iops',
|
||||
'iops_rd_max': 'iops_rd',
|
||||
'iops_wr_max': 'iops_wr',
|
||||
'iops_max_length': 'iops_max',
|
||||
'iops_rd_max_length': 'iops_rd_max',
|
||||
'iops_wr_max_length': 'iops_wr_max',
|
||||
},
|
||||
supports_check_mode=False,
|
||||
mutually_exclusive=[
|
||||
('target_vmid', 'target_storage'),
|
||||
('mbps', 'mbps_rd'),
|
||||
('mbps', 'mbps_wr'),
|
||||
('iops', 'iops_rd'),
|
||||
('iops', 'iops_wr'),
|
||||
('import_from', 'size'),
|
||||
]
|
||||
)
|
||||
|
||||
proxmox = ProxmoxDiskAnsible(module)
|
||||
|
||||
disk = module.params['disk']
|
||||
# Verify disk name has appropriate name
|
||||
disk_regex = compile(r'^([a-z]+)([0-9]+)$')
|
||||
disk_bus = sub(disk_regex, r'\1', disk)
|
||||
disk_number = int(sub(disk_regex, r'\2', disk))
|
||||
if disk_bus not in proxmox.supported_bus_num_ranges:
|
||||
proxmox.module.fail_json(msg='Unsupported disk bus: %s' % disk_bus)
|
||||
elif disk_number not in proxmox.supported_bus_num_ranges[disk_bus]:
|
||||
bus_range = proxmox.supported_bus_num_ranges[disk_bus]
|
||||
proxmox.module.fail_json(msg='Disk %s number not in range %s..%s ' % (disk, bus_range[0], bus_range[-1]))
|
||||
|
||||
name = module.params['name']
|
||||
state = module.params['state']
|
||||
vmid = module.params['vmid'] or proxmox.get_vmid(name)
|
||||
|
||||
# Ensure VM id exists and retrieve its config
|
||||
vm = None
|
||||
vm_config = None
|
||||
try:
|
||||
vm = proxmox.get_vm(vmid)
|
||||
vm_config = proxmox.proxmox_api.nodes(vm['node']).qemu(vmid).config.get()
|
||||
except Exception as e:
|
||||
proxmox.module.fail_json(msg='Getting information for VM %s failed with exception: %s' % (vmid, str(e)))
|
||||
|
||||
# Do not try to perform actions on missing disk
|
||||
if disk not in vm_config and state in ['resized', 'moved']:
|
||||
module.fail_json(vmid=vmid, msg='Unable to process missing disk %s in VM %s' % (disk, vmid))
|
||||
|
||||
if state == 'present':
|
||||
try:
|
||||
success, message = proxmox.create_disk(disk, vmid, vm, vm_config)
|
||||
if success:
|
||||
module.exit_json(changed=True, vmid=vmid, msg=message)
|
||||
else:
|
||||
module.exit_json(changed=False, vmid=vmid, msg=message)
|
||||
except Exception as e:
|
||||
module.fail_json(vmid=vmid, msg='Unable to create/update disk %s in VM %s: %s' % (disk, vmid, str(e)))
|
||||
|
||||
elif state == 'detached':
|
||||
try:
|
||||
if disk_bus == 'unused':
|
||||
module.exit_json(changed=False, vmid=vmid, msg='Disk %s already detached in VM %s' % (disk, vmid))
|
||||
if disk not in vm_config:
|
||||
module.exit_json(changed=False, vmid=vmid, msg="Disk %s not present in VM %s config" % (disk, vmid))
|
||||
proxmox.proxmox_api.nodes(vm['node']).qemu(vmid).unlink.put(vmid=vmid, idlist=disk, force=0)
|
||||
module.exit_json(changed=True, vmid=vmid, msg="Disk %s detached from VM %s" % (disk, vmid))
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to detach disk %s from VM %s with exception: %s" % (disk, vmid, str(e)))
|
||||
|
||||
elif state == 'moved':
|
||||
try:
|
||||
disk_config = disk_conf_str_to_dict(vm_config[disk])
|
||||
disk_storage = disk_config["storage_name"]
|
||||
if proxmox.move_disk(disk, vmid, vm, vm_config):
|
||||
module.exit_json(changed=True, vmid=vmid,
|
||||
msg="Disk %s moved from VM %s storage %s" % (disk, vmid, disk_storage))
|
||||
else:
|
||||
module.exit_json(changed=False, vmid=vmid, msg="Disk %s already at %s storage" % (disk, disk_storage))
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to move disk %s in VM %s with exception: %s" % (disk, vmid, str(e)))
|
||||
|
||||
elif state == 'resized':
|
||||
try:
|
||||
size = module.params['size']
|
||||
if not match(r'^\+?\d+(\.\d+)?[KMGT]?$', size):
|
||||
module.fail_json(msg="Unrecognized size pattern for disk %s: %s" % (disk, size))
|
||||
disk_config = disk_conf_str_to_dict(vm_config[disk])
|
||||
actual_size = disk_config['size']
|
||||
if size == actual_size:
|
||||
module.exit_json(changed=False, vmid=vmid, msg="Disk %s is already %s size" % (disk, size))
|
||||
proxmox.proxmox_api.nodes(vm['node']).qemu(vmid).resize.set(vmid=vmid, disk=disk, size=size)
|
||||
module.exit_json(changed=True, vmid=vmid, msg="Disk %s resized in VM %s" % (disk, vmid))
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to resize disk %s in VM %s with exception: %s" % (disk, vmid, str(e)))
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
if disk not in vm_config:
|
||||
module.exit_json(changed=False, vmid=vmid, msg="Disk %s is already absent in VM %s" % (disk, vmid))
|
||||
proxmox.proxmox_api.nodes(vm['node']).qemu(vmid).unlink.put(vmid=vmid, idlist=disk, force=1)
|
||||
module.exit_json(changed=True, vmid=vmid, msg="Disk %s removed from VM %s" % (disk, vmid))
|
||||
except Exception as e:
|
||||
module.fail_json(vmid=vmid, msg='Unable to remove disk %s from VM %s: %s' % (disk, vmid, str(e)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -313,6 +313,202 @@
|
|||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Nic net5 deleted on VM with vmid {{ vmid }}"
|
||||
|
||||
- name: Create new disk in VM
|
||||
tags: ['create_disk']
|
||||
block:
|
||||
- name: Add new disk (without force) to VM
|
||||
proxmox_disk:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ user }}@{{ domain }}"
|
||||
api_password: "{{ api_password | default(omit) }}"
|
||||
api_token_id: "{{ api_token_id | default(omit) }}"
|
||||
api_token_secret: "{{ api_token_secret | default(omit) }}"
|
||||
vmid: "{{ vmid }}"
|
||||
disk: "{{ disk }}"
|
||||
storage: "{{ storage }}"
|
||||
size: 1
|
||||
state: present
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Disk {{ disk }} created in VM {{ vmid }}"
|
||||
|
||||
- name: Try add disk again with same options (expect no-op)
|
||||
proxmox_disk:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ user }}@{{ domain }}"
|
||||
api_password: "{{ api_password | default(omit) }}"
|
||||
api_token_id: "{{ api_token_id | default(omit) }}"
|
||||
api_token_secret: "{{ api_token_secret | default(omit) }}"
|
||||
vmid: "{{ vmid }}"
|
||||
disk: "{{ disk }}"
|
||||
storage: "{{ storage }}"
|
||||
size: 1
|
||||
state: present
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Disk {{ disk }} is up to date in VM {{ vmid }}"
|
||||
|
||||
- name: Add new disk replacing existing disk (detach old and leave unused)
|
||||
proxmox_disk:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ user }}@{{ domain }}"
|
||||
api_password: "{{ api_password | default(omit) }}"
|
||||
api_token_id: "{{ api_token_id | default(omit) }}"
|
||||
api_token_secret: "{{ api_token_secret | default(omit) }}"
|
||||
vmid: "{{ vmid }}"
|
||||
disk: "{{ disk }}"
|
||||
storage: "{{ storage }}"
|
||||
size: 2
|
||||
create: forced
|
||||
state: present
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Disk {{ disk }} created in VM {{ vmid }}"
|
||||
|
||||
- name: Update existing disk in VM
|
||||
tags: ['update_disk']
|
||||
block:
|
||||
- name: Update disk configuration
|
||||
proxmox_disk:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ user }}@{{ domain }}"
|
||||
api_password: "{{ api_password | default(omit) }}"
|
||||
api_token_id: "{{ api_token_id | default(omit) }}"
|
||||
api_token_secret: "{{ api_token_secret | default(omit) }}"
|
||||
vmid: "{{ vmid }}"
|
||||
disk: "{{ disk }}"
|
||||
backup: false
|
||||
ro: true
|
||||
aio: native
|
||||
state: present
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Disk {{ disk }} updated in VM {{ vmid }}"
|
||||
|
||||
- name: Grow existing disk in VM
|
||||
tags: ['grow_disk']
|
||||
block:
|
||||
- name: Increase disk size
|
||||
proxmox_disk:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ user }}@{{ domain }}"
|
||||
api_password: "{{ api_password | default(omit) }}"
|
||||
api_token_id: "{{ api_token_id | default(omit) }}"
|
||||
api_token_secret: "{{ api_token_secret | default(omit) }}"
|
||||
vmid: "{{ vmid }}"
|
||||
disk: "{{ disk }}"
|
||||
size: +1G
|
||||
state: resized
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Disk {{ disk }} resized in VM {{ vmid }}"
|
||||
|
||||
- name: Detach disk and leave it unused
|
||||
tags: ['detach_disk']
|
||||
block:
|
||||
- name: Detach disk
|
||||
proxmox_disk:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ user }}@{{ domain }}"
|
||||
api_password: "{{ api_password | default(omit) }}"
|
||||
api_token_id: "{{ api_token_id | default(omit) }}"
|
||||
api_token_secret: "{{ api_token_secret | default(omit) }}"
|
||||
vmid: "{{ vmid }}"
|
||||
disk: "{{ disk }}"
|
||||
state: detached
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Disk {{ disk }} detached from VM {{ vmid }}"
|
||||
|
||||
- name: Move disk to another storage or another VM
|
||||
tags: ['move_disk']
|
||||
block:
|
||||
- name: Move disk to another storage inside same VM
|
||||
proxmox_disk:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ user }}@{{ domain }}"
|
||||
api_password: "{{ api_password | default(omit) }}"
|
||||
api_token_id: "{{ api_token_id | default(omit) }}"
|
||||
api_token_secret: "{{ api_token_secret | default(omit) }}"
|
||||
vmid: "{{ vmid }}"
|
||||
disk: "{{ disk }}"
|
||||
target_storage: "{{ target_storage }}"
|
||||
format: "{{ target_format }}"
|
||||
state: moved
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Disk {{ disk }} moved from VM {{ vmid }} storage {{ results.storage }}"
|
||||
|
||||
- name: Move disk to another VM (same storage)
|
||||
proxmox_disk:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ user }}@{{ domain }}"
|
||||
api_password: "{{ api_password | default(omit) }}"
|
||||
api_token_id: "{{ api_token_id | default(omit) }}"
|
||||
api_token_secret: "{{ api_token_secret | default(omit) }}"
|
||||
vmid: "{{ vmid }}"
|
||||
disk: "{{ disk }}"
|
||||
target_vmid: "{{ target_vm }}"
|
||||
target_disk: "{{ target_disk }}"
|
||||
state: moved
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Disk {{ disk }} moved from VM {{ vmid }} storage {{ results.storage }}"
|
||||
|
||||
|
||||
- name: Remove disk permanently
|
||||
tags: ['remove_disk']
|
||||
block:
|
||||
- name: Remove disk
|
||||
proxmox_disk:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ user }}@{{ domain }}"
|
||||
api_password: "{{ api_password | default(omit) }}"
|
||||
api_token_id: "{{ api_token_id | default(omit) }}"
|
||||
api_token_secret: "{{ api_token_secret | default(omit) }}"
|
||||
vmid: "{{ target_vm }}"
|
||||
disk: "{{ target_disk }}"
|
||||
state: absent
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results.vmid == {{ target_vm }}
|
||||
- results.msg == "Disk {{ target_disk }} removed from VM {{ target_vm }}"
|
||||
|
||||
- name: VM stop
|
||||
tags: [ 'stop' ]
|
||||
block:
|
||||
|
|
Loading…
Reference in a new issue