mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-06-28 03:00:23 -07:00
Remove proxmox content (#10110)
Some checks failed
EOL CI / EOL Sanity (Ⓐ2.16) (push) Has been cancelled
EOL CI / EOL Units (Ⓐ2.16+py2.7) (push) Has been cancelled
EOL CI / EOL Units (Ⓐ2.16+py3.11) (push) Has been cancelled
EOL CI / EOL Units (Ⓐ2.16+py3.6) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/1/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/2/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/3/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/1/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/2/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/3/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/1/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/2/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/3/) (push) Has been cancelled
nox / Run extra sanity tests (push) Has been cancelled
Some checks failed
EOL CI / EOL Sanity (Ⓐ2.16) (push) Has been cancelled
EOL CI / EOL Units (Ⓐ2.16+py2.7) (push) Has been cancelled
EOL CI / EOL Units (Ⓐ2.16+py3.11) (push) Has been cancelled
EOL CI / EOL Units (Ⓐ2.16+py3.6) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/1/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/2/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/3/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/1/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/2/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/3/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/1/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/2/) (push) Has been cancelled
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/3/) (push) Has been cancelled
nox / Run extra sanity tests (push) Has been cancelled
Remove proxmox content.
This commit is contained in:
parent
f63fdceb23
commit
f2b7bdf293
76 changed files with 124 additions and 14629 deletions
33
.github/BOTMETA.yml
vendored
33
.github/BOTMETA.yml
vendored
|
@ -113,9 +113,6 @@ files:
|
|||
$connections/lxd.py:
|
||||
labels: lxd
|
||||
maintainers: mattclay
|
||||
$connections/proxmox_pct_remote.py:
|
||||
labels: proxmox
|
||||
maintainers: mietzen
|
||||
$connections/qubes.py:
|
||||
maintainers: kushaldas
|
||||
$connections/saltstack.py:
|
||||
|
@ -247,8 +244,6 @@ files:
|
|||
keywords: opennebula dynamic inventory script
|
||||
labels: cloud opennebula
|
||||
maintainers: feldsam
|
||||
$inventories/proxmox.py:
|
||||
maintainers: $team_virt ilijamt krauthosting
|
||||
$inventories/scaleway.py:
|
||||
labels: cloud scaleway
|
||||
maintainers: $team_scaleway
|
||||
|
@ -1136,34 +1131,6 @@ files:
|
|||
maintainers: $team_bsd berenddeboer
|
||||
$modules/pritunl_:
|
||||
maintainers: Lowess
|
||||
$modules/proxmox:
|
||||
keywords: kvm libvirt proxmox qemu
|
||||
labels: proxmox virt
|
||||
maintainers: $team_virt UnderGreen krauthosting
|
||||
ignore: tleguern
|
||||
$modules/proxmox.py:
|
||||
ignore: skvidal
|
||||
maintainers: UnderGreen krauthosting
|
||||
$modules/proxmox_disk.py:
|
||||
maintainers: castorsky krauthosting
|
||||
$modules/proxmox_kvm.py:
|
||||
ignore: skvidal
|
||||
maintainers: helldorado krauthosting
|
||||
$modules/proxmox_backup.py:
|
||||
maintainers: IamLunchbox
|
||||
$modules/proxmox_backup_info.py:
|
||||
maintainers: raoufnezhad mmayabi
|
||||
$modules/proxmox_nic.py:
|
||||
maintainers: Kogelvis krauthosting
|
||||
$modules/proxmox_node_info.py:
|
||||
maintainers: jwbernin krauthosting
|
||||
$modules/proxmox_storage_contents_info.py:
|
||||
maintainers: l00ptr krauthosting
|
||||
$modules/proxmox_tasks_info:
|
||||
maintainers: paginabianca krauthosting
|
||||
$modules/proxmox_template.py:
|
||||
ignore: skvidal
|
||||
maintainers: UnderGreen krauthosting
|
||||
$modules/pubnub_blocks.py:
|
||||
maintainers: parfeon pubnub
|
||||
$modules/pulp_repo.py:
|
||||
|
|
|
@ -54,12 +54,6 @@ exclusions = [
|
|||
]
|
||||
doc_fragment = "community.general.keycloak.actiongroup_keycloak"
|
||||
|
||||
[[sessions.extra_checks.action_groups_config]]
|
||||
name = "proxmox"
|
||||
pattern = "^proxmox(_.*)?$"
|
||||
exclusions = []
|
||||
doc_fragment = "community.general.proxmox.actiongroup_proxmox"
|
||||
|
||||
[sessions.build_import_check]
|
||||
run_galaxy_importer = true
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
bugfixes:
|
||||
- "proxmox - fix crash in module when the used on an existing LXC container with ``state=present`` and ``force=true`` (https://github.com/ansible-collections/community.proxmox/pull/91, https://github.com/ansible-collections/community.general/pull/10155)."
|
|
@ -1,3 +0,0 @@
|
|||
---
|
||||
minor_changes:
|
||||
- proxmox_snap - correctly handle proxmox_snap timeout parameter (https://github.com/ansible-collections/community.proxmox/issues/73, https://github.com/ansible-collections/community.proxmox/issues/95, https://github.com/ansible-collections/community.general/pull/10176).
|
|
@ -1,2 +0,0 @@
|
|||
minor_changes:
|
||||
- proxmox_template - add server side artifact fetching support (https://github.com/ansible-collections/community.general/pull/9113).
|
|
@ -1,2 +0,0 @@
|
|||
bugfixes:
|
||||
- proxmox_backup - fix incorrect key lookup in vmid permission check (https://github.com/ansible-collections/community.general/pull/9223).
|
|
@ -1,11 +0,0 @@
|
|||
minor_changes:
|
||||
- proxmox - refactors the proxmox module (https://github.com/ansible-collections/community.general/pull/9225).
|
||||
|
||||
bugfixes:
|
||||
- proxmox - fixes idempotency of template conversions (https://github.com/ansible-collections/community.general/pull/9225, https://github.com/ansible-collections/community.general/issues/8811).
|
||||
- proxmox - fixes issues with disk_volume variable (https://github.com/ansible-collections/community.general/pull/9225, https://github.com/ansible-collections/community.general/issues/9065).
|
||||
- proxmox - fixes incorrect parsing for bind-only mounts (https://github.com/ansible-collections/community.general/pull/9225, https://github.com/ansible-collections/community.general/issues/8982).
|
||||
- proxmox module utils - fixes ignoring of ``choose_first_if_multiple`` argument in ``get_vmid`` (https://github.com/ansible-collections/community.general/pull/9225).
|
||||
|
||||
deprecated_features:
|
||||
- proxmox - removes default value ``false`` of ``update`` parameter. This will be changed to a default of ``true`` in community.general 11.0.0 (https://github.com/ansible-collections/community.general/pull/9225).
|
|
@ -1,2 +0,0 @@
|
|||
minor_changes:
|
||||
- proxmox inventory plugin - strip whitespace from ``user``, ``token_id``, and ``token_secret`` (https://github.com/ansible-collections/community.general/issues/9227, https://github.com/ansible-collections/community.general/pull/9228/).
|
|
@ -1,2 +0,0 @@
|
|||
minor_changes:
|
||||
- proxmox_backup - refactor permission checking to improve code readability and maintainability (https://github.com/ansible-collections/community.general/pull/9239).
|
|
@ -1,4 +0,0 @@
|
|||
bugfixes:
|
||||
- proxmox_disk - fix async method and make ``resize_disk`` method handle errors correctly (https://github.com/ansible-collections/community.general/pull/9256).
|
||||
minor_changes:
|
||||
- proxmox module utils - add method ``api_task_complete`` that can wait for task completion and return error message (https://github.com/ansible-collections/community.general/pull/9256).
|
|
@ -1,2 +0,0 @@
|
|||
bugfixes:
|
||||
- proxmox_template - fix the wrong path called on ``proxmox_template.task_status`` (https://github.com/ansible-collections/community.general/issues/9276, https://github.com/ansible-collections/community.general/pull/9277).
|
|
@ -7,7 +7,6 @@ minor_changes:
|
|||
- nmap inventory plugin - use f-strings instead of interpolations or ``format`` (https://github.com/ansible-collections/community.general/pull/9323).
|
||||
- online inventory plugin - use f-strings instead of interpolations or ``format`` (https://github.com/ansible-collections/community.general/pull/9323).
|
||||
- opennebula inventory plugin - use f-strings instead of interpolations or ``format`` (https://github.com/ansible-collections/community.general/pull/9323).
|
||||
- proxmox inventory plugin - use f-strings instead of interpolations or ``format`` (https://github.com/ansible-collections/community.general/pull/9323).
|
||||
- scaleway inventory plugin - use f-strings instead of interpolations or ``format`` (https://github.com/ansible-collections/community.general/pull/9323).
|
||||
- stackpath_compute inventory plugin - use f-strings instead of interpolations or ``format`` (https://github.com/ansible-collections/community.general/pull/9323).
|
||||
- virtualbox inventory plugin - use f-strings instead of interpolations or ``format`` (https://github.com/ansible-collections/community.general/pull/9323).
|
||||
|
|
|
@ -13,7 +13,6 @@ minor_changes:
|
|||
- "lxd inventory plugin - clean up string conversions (https://github.com/ansible-collections/community.general/pull/9379)."
|
||||
- "nmap inventory plugin - clean up string conversions (https://github.com/ansible-collections/community.general/pull/9379)."
|
||||
- "opennebula inventory plugin - clean up string conversions (https://github.com/ansible-collections/community.general/pull/9379)."
|
||||
- "proxmox inventory plugin - clean up string conversions (https://github.com/ansible-collections/community.general/pull/9379)."
|
||||
- "scaleway inventory plugin - clean up string conversions (https://github.com/ansible-collections/community.general/pull/9379)."
|
||||
- "virtualbox inventory plugin - clean up string conversions (https://github.com/ansible-collections/community.general/pull/9379)."
|
||||
- "cyberarkpassword lookup plugin - clean up string conversions (https://github.com/ansible-collections/community.general/pull/9379)."
|
||||
|
|
|
@ -6,7 +6,6 @@ minor_changes:
|
|||
- jail connection plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- lxc connection plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- lxd connection plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- proxmox_pct_remote connection plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- qubes connection plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- saltstack connection plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- zone connection plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
|
@ -19,7 +18,6 @@ minor_changes:
|
|||
- nmap inventory plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- online inventory plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- opennebula inventory plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- proxmox inventory plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- scaleway inventory plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- stackpath_compute inventory plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
- virtualbox inventory plugin - adjust standard preamble for Python 3 (https://github.com/ansible-collections/community.general/pull/9584).
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
minor_changes:
|
||||
- proxmox_template - add support for checksum validation with new options ``checksum_algorithm`` and ``checksum`` (https://github.com/ansible-collections/community.general/issues/9553, https://github.com/ansible-collections/community.general/pull/9601).
|
|
@ -1,3 +0,0 @@
|
|||
bugfixes:
|
||||
- proxmox - fixes a typo in the translation of the ``pubkey`` parameter to proxmox' ``ssh-public-keys`` (https://github.com/ansible-collections/community.general/issues/9642, https://github.com/ansible-collections/community.general/pull/9645).
|
||||
- proxmox - adds the ``pubkey`` parameter (back to) the ``update`` state (https://github.com/ansible-collections/community.general/issues/9642, https://github.com/ansible-collections/community.general/pull/9645).
|
|
@ -1,2 +0,0 @@
|
|||
minor_changes:
|
||||
- proxmox_kvm - allow hibernation and suspending of VMs (https://github.com/ansible-collections/community.general/issues/9620, https://github.com/ansible-collections/community.general/pull/9653).
|
|
@ -1,2 +0,0 @@
|
|||
bugfixes:
|
||||
- "proxmox inventory plugin - plugin did not update cache correctly after ``meta: refresh_inventory`` (https://github.com/ansible-collections/community.general/issues/9710, https://github.com/ansible-collections/community.general/pull/9760)."
|
|
@ -1,2 +0,0 @@
|
|||
bugfixes:
|
||||
- proxmox - add missing key selection of ``'status'`` key to ``get_lxc_status`` (https://github.com/ansible-collections/community.general/issues/9696, https://github.com/ansible-collections/community.general/pull/9809).
|
|
@ -1,2 +0,0 @@
|
|||
minor_changes:
|
||||
- proxmox_kvm - add missing audio hardware device handling (https://github.com/ansible-collections/community.general/issues/5192, https://github.com/ansible-collections/community.general/pull/9847).
|
|
@ -1,2 +0,0 @@
|
|||
bugfixes:
|
||||
- proxmox_vm_info - the module no longer expects that the key ``template`` exists in a dictionary returned by Proxmox (https://github.com/ansible-collections/community.general/issues/9875, https://github.com/ansible-collections/community.general/pull/9910).
|
|
@ -1,2 +0,0 @@
|
|||
minor_changes:
|
||||
- proxmox and proxmox_kvm modules - allow uppercase characters in VM/container tags (https://github.com/ansible-collections/community.general/issues/9895, https://github.com/ansible-collections/community.general/pull/10024).
|
|
@ -1,2 +0,0 @@
|
|||
bugfixes:
|
||||
- proxmox inventory plugin - fix ``ansible_host`` staying empty for certain Proxmox nodes (https://github.com/ansible-collections/community.general/issues/5906, https://github.com/ansible-collections/community.general/pull/9952).
|
|
@ -1,2 +0,0 @@
|
|||
bugfixes:
|
||||
- "proxmox_disk - fail gracefully if ``storage`` is required but not provided by the user (https://github.com/ansible-collections/community.general/issues/9941, https://github.com/ansible-collections/community.general/pull/9963)."
|
|
@ -15,5 +15,3 @@ removed_features:
|
|||
- "mh.module_helper module utils - ``AnsibleModule`` and ``VarsMixin`` are no longer provided (https://github.com/ansible-collections/community.general/pull/10126)."
|
||||
- "mh.module_helper module utils - the attributes ``use_old_vardict`` and ``mute_vardict_deprecation`` from ``ModuleHelper`` have been removed. We suggest to remove them from your modules if you no longer support community.general < 11.0.0 (https://github.com/ansible-collections/community.general/pull/10126)."
|
||||
- "module_helper module utils - ``StateMixin``, ``DependencyCtxMgr``, ``VarMeta``, ``VarDict``, and ``VarsMixin`` are no longer provided (https://github.com/ansible-collections/community.general/pull/10126)."
|
||||
breaking_changes:
|
||||
- "proxmox - the default of ``update`` changed from ``false`` to ``true`` (https://github.com/ansible-collections/community.general/pull/10126)."
|
||||
|
|
7
changelogs/fragments/proxmox-removal.yml
Normal file
7
changelogs/fragments/proxmox-removal.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
removed_features:
|
||||
- "The Proxmox content (modules and plugins) has been moved to the `new collection community.proxmox <https://github.com/ansible-collections/community.proxmox>`__.
|
||||
Since community.general 11.0.0, these modules and plugins have been replaced by deprecated redirections to community.proxmox.
|
||||
You need to explicitly install community.proxmox, for example with ``ansible-galaxy collection install community.proxmox``,
|
||||
or by installing a new enough version of the Ansible community package.
|
||||
We suggest to update your roles and playbooks to use the new FQCNs as soon as possible to avoid getting deprecation messages
|
||||
(https://github.com/ansible-collections/community.general/pull/10110)."
|
131
meta/runtime.yml
131
meta/runtime.yml
|
@ -15,24 +15,9 @@ action_groups:
|
|||
- consul_session
|
||||
- consul_token
|
||||
proxmox:
|
||||
- proxmox
|
||||
- proxmox_backup
|
||||
- proxmox_backup_info
|
||||
- proxmox_disk
|
||||
- proxmox_domain_info
|
||||
- proxmox_group_info
|
||||
- proxmox_kvm
|
||||
- proxmox_nic
|
||||
- proxmox_node_info
|
||||
- proxmox_pool
|
||||
- proxmox_pool_member
|
||||
- proxmox_snap
|
||||
- proxmox_storage_contents_info
|
||||
- proxmox_storage_info
|
||||
- proxmox_tasks_info
|
||||
- proxmox_template
|
||||
- proxmox_user_info
|
||||
- proxmox_vm_info
|
||||
- metadata:
|
||||
extend_group:
|
||||
- community.proxmox.proxmox
|
||||
keycloak:
|
||||
- keycloak_authentication
|
||||
- keycloak_authentication_required_actions
|
||||
|
@ -94,6 +79,11 @@ plugin_routing:
|
|||
redirect: community.docker.docker
|
||||
oc:
|
||||
redirect: community.okd.oc
|
||||
proxmox_pct_remote:
|
||||
redirect: community.proxmox.proxmox_pct_remote
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
lookup:
|
||||
gcp_storage_file:
|
||||
redirect: community.google.gcp_storage_file
|
||||
|
@ -664,6 +654,96 @@ plugin_routing:
|
|||
tombstone:
|
||||
removal_version: 11.0.0
|
||||
warning_text: Supporting library is unsupported since 2021.
|
||||
proxmox:
|
||||
redirect: community.proxmox.proxmox
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_backup:
|
||||
redirect: community.proxmox.proxmox_backup
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_backup_info:
|
||||
redirect: community.proxmox.proxmox_backup_info
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_disk:
|
||||
redirect: community.proxmox.proxmox_disk
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_domain_info:
|
||||
redirect: community.proxmox.proxmox_domain_info
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_group_info:
|
||||
redirect: community.proxmox.proxmox_group_info
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_kvm:
|
||||
redirect: community.proxmox.proxmox_kvm
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_nic:
|
||||
redirect: community.proxmox.proxmox_nic
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_node_info:
|
||||
redirect: community.proxmox.proxmox_node_info
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_pool:
|
||||
redirect: community.proxmox.proxmox_pool
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_pool_member:
|
||||
redirect: community.proxmox.proxmox_pool_member
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_snap:
|
||||
redirect: community.proxmox.proxmox_snap
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_storage_contents_info:
|
||||
redirect: community.proxmox.proxmox_storage_contents_info
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_storage_info:
|
||||
redirect: community.proxmox.proxmox_storage_info
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_tasks_info:
|
||||
redirect: community.proxmox.proxmox_tasks_info
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_template:
|
||||
redirect: community.proxmox.proxmox_template
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_user_info:
|
||||
redirect: community.proxmox.proxmox_user_info
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
proxmox_vm_info:
|
||||
redirect: community.proxmox.proxmox_vm_info
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
purefa_facts:
|
||||
tombstone:
|
||||
removal_version: 3.0.0
|
||||
|
@ -922,6 +1002,11 @@ plugin_routing:
|
|||
redirect: infoblox.nios_modules.nios
|
||||
postgresql:
|
||||
redirect: community.postgresql.postgresql
|
||||
proxmox:
|
||||
redirect: community.proxmox.proxmox
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
purestorage:
|
||||
deprecation:
|
||||
removal_version: 12.0.0
|
||||
|
@ -950,6 +1035,11 @@ plugin_routing:
|
|||
redirect: infoblox.nios_modules.api
|
||||
postgresql:
|
||||
redirect: community.postgresql.postgresql
|
||||
proxmox:
|
||||
redirect: community.proxmox.proxmox
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
pure:
|
||||
deprecation:
|
||||
removal_version: 12.0.0
|
||||
|
@ -967,6 +1057,11 @@ plugin_routing:
|
|||
redirect: community.docker.docker_machine
|
||||
docker_swarm:
|
||||
redirect: community.docker.docker_swarm
|
||||
proxmox:
|
||||
redirect: community.proxmox.proxmox
|
||||
deprecation:
|
||||
removal_version: 15.0.0
|
||||
warning_text: The proxmox content has been moved to community.proxmox.
|
||||
kubevirt:
|
||||
redirect: community.kubevirt.kubevirt
|
||||
stackpath_compute:
|
||||
|
|
|
@ -1,857 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# Copyright (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
|
||||
# Copyright (c) 2024 Ansible Project
|
||||
# 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
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
author: Nils Stein (@mietzen) <github.nstein@mailbox.org>
|
||||
name: proxmox_pct_remote
|
||||
short_description: Run tasks in Proxmox LXC container instances using pct CLI via SSH
|
||||
requirements:
|
||||
- paramiko
|
||||
description:
|
||||
- Run commands or put/fetch files to an existing Proxmox LXC container using pct CLI via SSH.
|
||||
- Uses the Python SSH implementation (Paramiko) to connect to the Proxmox host.
|
||||
version_added: "10.3.0"
|
||||
options:
|
||||
remote_addr:
|
||||
description:
|
||||
- Address of the remote target.
|
||||
default: inventory_hostname
|
||||
type: string
|
||||
vars:
|
||||
- name: inventory_hostname
|
||||
- name: ansible_host
|
||||
- name: ansible_ssh_host
|
||||
- name: ansible_paramiko_host
|
||||
port:
|
||||
description: Remote port to connect to.
|
||||
type: int
|
||||
default: 22
|
||||
ini:
|
||||
- section: defaults
|
||||
key: remote_port
|
||||
- section: paramiko_connection
|
||||
key: remote_port
|
||||
env:
|
||||
- name: ANSIBLE_REMOTE_PORT
|
||||
- name: ANSIBLE_REMOTE_PARAMIKO_PORT
|
||||
vars:
|
||||
- name: ansible_port
|
||||
- name: ansible_ssh_port
|
||||
- name: ansible_paramiko_port
|
||||
keyword:
|
||||
- name: port
|
||||
remote_user:
|
||||
description:
|
||||
- User to login/authenticate as.
|
||||
- Can be set from the CLI via the C(--user) or C(-u) options.
|
||||
type: string
|
||||
vars:
|
||||
- name: ansible_user
|
||||
- name: ansible_ssh_user
|
||||
- name: ansible_paramiko_user
|
||||
env:
|
||||
- name: ANSIBLE_REMOTE_USER
|
||||
- name: ANSIBLE_PARAMIKO_REMOTE_USER
|
||||
ini:
|
||||
- section: defaults
|
||||
key: remote_user
|
||||
- section: paramiko_connection
|
||||
key: remote_user
|
||||
keyword:
|
||||
- name: remote_user
|
||||
password:
|
||||
description:
|
||||
- Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
|
||||
- Can be set from the CLI via the C(--ask-pass) option.
|
||||
type: string
|
||||
vars:
|
||||
- name: ansible_password
|
||||
- name: ansible_ssh_pass
|
||||
- name: ansible_ssh_password
|
||||
- name: ansible_paramiko_pass
|
||||
- name: ansible_paramiko_password
|
||||
use_rsa_sha2_algorithms:
|
||||
description:
|
||||
- Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
|
||||
- On paramiko versions older than 2.9, this only affects hostkeys.
|
||||
- For behavior matching paramiko<2.9 set this to V(false).
|
||||
vars:
|
||||
- name: ansible_paramiko_use_rsa_sha2_algorithms
|
||||
ini:
|
||||
- {key: use_rsa_sha2_algorithms, section: paramiko_connection}
|
||||
env:
|
||||
- {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
|
||||
default: true
|
||||
type: boolean
|
||||
host_key_auto_add:
|
||||
description: "Automatically add host keys to C(~/.ssh/known_hosts)."
|
||||
env:
|
||||
- name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
|
||||
ini:
|
||||
- key: host_key_auto_add
|
||||
section: paramiko_connection
|
||||
type: boolean
|
||||
look_for_keys:
|
||||
default: True
|
||||
description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
|
||||
env:
|
||||
- name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
|
||||
ini:
|
||||
- {key: look_for_keys, section: paramiko_connection}
|
||||
type: boolean
|
||||
proxy_command:
|
||||
default: ""
|
||||
description:
|
||||
- Proxy information for running the connection via a jumphost.
|
||||
type: string
|
||||
env:
|
||||
- name: ANSIBLE_PARAMIKO_PROXY_COMMAND
|
||||
ini:
|
||||
- {key: proxy_command, section: paramiko_connection}
|
||||
vars:
|
||||
- name: ansible_paramiko_proxy_command
|
||||
pty:
|
||||
default: True
|
||||
description: "C(sudo) usually requires a PTY, V(true) to give a PTY and V(false) to not give a PTY."
|
||||
env:
|
||||
- name: ANSIBLE_PARAMIKO_PTY
|
||||
ini:
|
||||
- section: paramiko_connection
|
||||
key: pty
|
||||
type: boolean
|
||||
record_host_keys:
|
||||
default: True
|
||||
description: "Save the host keys to a file."
|
||||
env:
|
||||
- name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
|
||||
ini:
|
||||
- section: paramiko_connection
|
||||
key: record_host_keys
|
||||
type: boolean
|
||||
host_key_checking:
|
||||
description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
|
||||
type: boolean
|
||||
default: true
|
||||
env:
|
||||
- name: ANSIBLE_HOST_KEY_CHECKING
|
||||
- name: ANSIBLE_SSH_HOST_KEY_CHECKING
|
||||
- name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
|
||||
ini:
|
||||
- section: defaults
|
||||
key: host_key_checking
|
||||
- section: paramiko_connection
|
||||
key: host_key_checking
|
||||
vars:
|
||||
- name: ansible_host_key_checking
|
||||
- name: ansible_ssh_host_key_checking
|
||||
- name: ansible_paramiko_host_key_checking
|
||||
use_persistent_connections:
|
||||
description: "Toggles the use of persistence for connections."
|
||||
type: boolean
|
||||
default: False
|
||||
env:
|
||||
- name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
|
||||
ini:
|
||||
- section: defaults
|
||||
key: use_persistent_connections
|
||||
banner_timeout:
|
||||
type: float
|
||||
default: 30
|
||||
description:
|
||||
- Configures, in seconds, the amount of time to wait for the SSH
|
||||
banner to be presented. This option is supported by paramiko
|
||||
version 1.15.0 or newer.
|
||||
ini:
|
||||
- section: paramiko_connection
|
||||
key: banner_timeout
|
||||
env:
|
||||
- name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
|
||||
timeout:
|
||||
type: int
|
||||
default: 10
|
||||
description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
|
||||
ini:
|
||||
- section: defaults
|
||||
key: timeout
|
||||
- section: ssh_connection
|
||||
key: timeout
|
||||
- section: paramiko_connection
|
||||
key: timeout
|
||||
env:
|
||||
- name: ANSIBLE_TIMEOUT
|
||||
- name: ANSIBLE_SSH_TIMEOUT
|
||||
- name: ANSIBLE_PARAMIKO_TIMEOUT
|
||||
vars:
|
||||
- name: ansible_ssh_timeout
|
||||
- name: ansible_paramiko_timeout
|
||||
cli:
|
||||
- name: timeout
|
||||
lock_file_timeout:
|
||||
type: int
|
||||
default: 60
|
||||
description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
|
||||
vars:
|
||||
- name: ansible_lock_file_timeout
|
||||
env:
|
||||
- name: ANSIBLE_LOCK_FILE_TIMEOUT
|
||||
private_key_file:
|
||||
description:
|
||||
- Path to private key file to use for authentication.
|
||||
type: string
|
||||
ini:
|
||||
- section: defaults
|
||||
key: private_key_file
|
||||
- section: paramiko_connection
|
||||
key: private_key_file
|
||||
env:
|
||||
- name: ANSIBLE_PRIVATE_KEY_FILE
|
||||
- name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
|
||||
vars:
|
||||
- name: ansible_private_key_file
|
||||
- name: ansible_ssh_private_key_file
|
||||
- name: ansible_paramiko_private_key_file
|
||||
cli:
|
||||
- name: private_key_file
|
||||
option: "--private-key"
|
||||
vmid:
|
||||
description:
|
||||
- LXC Container ID
|
||||
type: int
|
||||
vars:
|
||||
- name: proxmox_vmid
|
||||
proxmox_become_method:
|
||||
description:
|
||||
- Become command used in proxmox
|
||||
type: str
|
||||
default: sudo
|
||||
vars:
|
||||
- name: proxmox_become_method
|
||||
notes:
|
||||
- >
|
||||
When NOT using this plugin as root, you need to have a become mechanism,
|
||||
e.g. C(sudo), installed on Proxmox and setup so we can run it without prompting for the password.
|
||||
Inside the container, we need a shell, for example C(sh) and the C(cat) command to be available in the C(PATH) for this plugin to work.
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
# --------------------------------------------------------------
|
||||
# Setup sudo with password less access to pct for user 'ansible':
|
||||
# --------------------------------------------------------------
|
||||
#
|
||||
# Open a Proxmox root shell and execute:
|
||||
# $ useradd -d /opt/ansible-pct -r -m -s /bin/sh ansible
|
||||
# $ mkdir -p /opt/ansible-pct/.ssh
|
||||
# $ ssh-keygen -t ed25519 -C 'ansible' -N "" -f /opt/ansible-pct/.ssh/ansible <<< y > /dev/null
|
||||
# $ cat /opt/ansible-pct/.ssh/ansible
|
||||
# $ mv /opt/ansible-pct/.ssh/ansible.pub /opt/ansible-pct/.ssh/authorized-keys
|
||||
# $ rm /opt/ansible-pct/.ssh/ansible*
|
||||
# $ chown -R ansible:ansible /opt/ansible-pct/.ssh
|
||||
# $ chmod 700 /opt/ansible-pct/.ssh
|
||||
# $ chmod 600 /opt/ansible-pct/.ssh/authorized-keys
|
||||
# $ echo 'ansible ALL = (root) NOPASSWD: /usr/sbin/pct' > /etc/sudoers.d/ansible_pct
|
||||
#
|
||||
# Save the displayed private key and add it to your ssh-agent
|
||||
#
|
||||
# Or use ansible:
|
||||
# ---
|
||||
# - name: Setup ansible-pct user and configure environment on Proxmox host
|
||||
# hosts: proxmox
|
||||
# become: true
|
||||
# gather_facts: false
|
||||
#
|
||||
# tasks:
|
||||
# - name: Create ansible user
|
||||
# ansible.builtin.user:
|
||||
# name: ansible
|
||||
# comment: Ansible User
|
||||
# home: /opt/ansible-pct
|
||||
# shell: /bin/sh
|
||||
# create_home: true
|
||||
# system: true
|
||||
#
|
||||
# - name: Create .ssh directory
|
||||
# ansible.builtin.file:
|
||||
# path: /opt/ansible-pct/.ssh
|
||||
# state: directory
|
||||
# owner: ansible
|
||||
# group: ansible
|
||||
# mode: '0700'
|
||||
#
|
||||
# - name: Generate SSH key for ansible user
|
||||
# community.crypto.openssh_keypair:
|
||||
# path: /opt/ansible-pct/.ssh/ansible
|
||||
# type: ed25519
|
||||
# comment: 'ansible'
|
||||
# force: true
|
||||
# mode: '0600'
|
||||
# owner: ansible
|
||||
# group: ansible
|
||||
#
|
||||
# - name: Set public key as authorized key
|
||||
# ansible.builtin.copy:
|
||||
# src: /opt/ansible-pct/.ssh/ansible.pub
|
||||
# dest: /opt/ansible-pct/.ssh/authorized-keys
|
||||
# remote_src: yes
|
||||
# owner: ansible
|
||||
# group: ansible
|
||||
# mode: '0600'
|
||||
#
|
||||
# - name: Add sudoers entry for ansible user
|
||||
# ansible.builtin.copy:
|
||||
# content: 'ansible ALL = (root) NOPASSWD: /usr/sbin/pct'
|
||||
# dest: /etc/sudoers.d/ansible_pct
|
||||
# owner: root
|
||||
# group: root
|
||||
# mode: '0440'
|
||||
#
|
||||
# - name: Fetch private SSH key to localhost
|
||||
# ansible.builtin.fetch:
|
||||
# src: /opt/ansible-pct/.ssh/ansible
|
||||
# dest: ~/.ssh/proxmox_ansible_private_key
|
||||
# flat: yes
|
||||
# fail_on_missing: true
|
||||
#
|
||||
# - name: Clean up generated SSH keys
|
||||
# ansible.builtin.file:
|
||||
# path: /opt/ansible-pct/.ssh/ansible*
|
||||
# state: absent
|
||||
#
|
||||
# - name: Configure private key permissions on localhost
|
||||
# hosts: localhost
|
||||
# tasks:
|
||||
# - name: Set permissions for fetched private key
|
||||
# ansible.builtin.file:
|
||||
# path: ~/.ssh/proxmox_ansible_private_key
|
||||
# mode: '0600'
|
||||
#
|
||||
# --------------------------------
|
||||
# Static inventory file: hosts.yml
|
||||
# --------------------------------
|
||||
# all:
|
||||
# children:
|
||||
# lxc:
|
||||
# hosts:
|
||||
# container-1:
|
||||
# ansible_host: 10.0.0.10
|
||||
# proxmox_vmid: 100
|
||||
# ansible_connection: community.general.proxmox_pct_remote
|
||||
# ansible_user: ansible
|
||||
# container-2:
|
||||
# ansible_host: 10.0.0.10
|
||||
# proxmox_vmid: 200
|
||||
# ansible_connection: community.general.proxmox_pct_remote
|
||||
# ansible_user: ansible
|
||||
# proxmox:
|
||||
# hosts:
|
||||
# proxmox-1:
|
||||
# ansible_host: 10.0.0.10
|
||||
#
|
||||
#
|
||||
# ---------------------------------------------
|
||||
# Dynamic inventory file: inventory.proxmox.yml
|
||||
# ---------------------------------------------
|
||||
# plugin: community.general.proxmox
|
||||
# url: https://10.0.0.10:8006
|
||||
# validate_certs: false
|
||||
# user: ansible@pam
|
||||
# token_id: ansible
|
||||
# token_secret: !vault |
|
||||
# $ANSIBLE_VAULT;1.1;AES256
|
||||
# ...
|
||||
|
||||
# want_facts: true
|
||||
# exclude_nodes: true
|
||||
# filters:
|
||||
# - proxmox_vmtype == "lxc"
|
||||
# want_proxmox_nodes_ansible_host: false
|
||||
# compose:
|
||||
# ansible_host: "'10.0.0.10'"
|
||||
# ansible_connection: "'community.general.proxmox_pct_remote'"
|
||||
# ansible_user: "'ansible'"
|
||||
#
|
||||
#
|
||||
# ----------------------
|
||||
# Playbook: playbook.yml
|
||||
# ----------------------
|
||||
---
|
||||
- hosts: lxc
|
||||
# On nodes with many containers you might want to deactivate the devices facts
|
||||
# or set `gather_facts: false` if you don't need them.
|
||||
# More info on gathering fact subsets:
|
||||
# https://docs.ansible.com/ansible/latest/collections/ansible/builtin/setup_module.html
|
||||
#
|
||||
# gather_facts: true
|
||||
# gather_subset:
|
||||
# - "!devices"
|
||||
tasks:
|
||||
- name: Ping LXC container
|
||||
ansible.builtin.ping:
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import socket
|
||||
import tempfile
|
||||
import typing as t
|
||||
|
||||
from ansible.errors import (
|
||||
AnsibleAuthenticationFailure,
|
||||
AnsibleConnectionFailure,
|
||||
AnsibleError,
|
||||
)
|
||||
from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
|
||||
from ansible.module_utils.compat.version import LooseVersion
|
||||
from ansible.plugins.connection import ConnectionBase
|
||||
from ansible.utils.display import Display
|
||||
from ansible.utils.path import makedirs_safe
|
||||
from binascii import hexlify
|
||||
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
|
||||
msg = f"""
|
||||
paramiko: The authenticity of host '{hostname}' can't be established.
|
||||
The {ktype} key fingerprint is {fingerprint}.
|
||||
Are you sure you want to continue connecting (yes/no)?
|
||||
"""
|
||||
return msg
|
||||
|
||||
|
||||
MissingHostKeyPolicy: type = object
|
||||
if paramiko:
|
||||
MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
|
||||
|
||||
|
||||
class MyAddPolicy(MissingHostKeyPolicy):
|
||||
"""
|
||||
Based on AutoAddPolicy in paramiko so we can determine when keys are added
|
||||
|
||||
and also prompt for input.
|
||||
|
||||
Policy for automatically adding the hostname and new host key to the
|
||||
local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
|
||||
"""
|
||||
|
||||
def __init__(self, connection: Connection) -> None:
|
||||
self.connection = connection
|
||||
self._options = connection._options
|
||||
|
||||
def missing_host_key(self, client, hostname, key) -> None:
|
||||
|
||||
if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
|
||||
|
||||
fingerprint = hexlify(key.get_fingerprint())
|
||||
ktype = key.get_name()
|
||||
|
||||
if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
|
||||
# don't print the prompt string since the user cannot respond
|
||||
# to the question anyway
|
||||
raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
|
||||
|
||||
inp = to_text(
|
||||
display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
|
||||
errors='surrogate_or_strict'
|
||||
)
|
||||
|
||||
if inp.lower() not in ['yes', 'y', '']:
|
||||
raise AnsibleError('host connection rejected by user')
|
||||
|
||||
key._added_by_ansible_this_time = True
|
||||
|
||||
# existing implementation below:
|
||||
client._host_keys.add(hostname, key.get_name(), key)
|
||||
|
||||
# host keys are actually saved in close() function below
|
||||
# in order to control ordering.
|
||||
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
""" SSH based connections (paramiko) to Proxmox pct """
|
||||
|
||||
transport = 'community.general.proxmox_pct_remote'
|
||||
_log_channel: str | None = None
|
||||
|
||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||
|
||||
def _set_log_channel(self, name: str) -> None:
|
||||
""" Mimic paramiko.SSHClient.set_log_channel """
|
||||
self._log_channel = name
|
||||
|
||||
def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
|
||||
proxy_command = self.get_option('proxy_command') or None
|
||||
|
||||
sock_kwarg = {}
|
||||
if proxy_command:
|
||||
replacers = {
|
||||
'%h': self.get_option('remote_addr'),
|
||||
'%p': port,
|
||||
'%r': self.get_option('remote_user')
|
||||
}
|
||||
for find, replace in replacers.items():
|
||||
proxy_command = proxy_command.replace(find, str(replace))
|
||||
try:
|
||||
sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
|
||||
display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
|
||||
except AttributeError:
|
||||
display.warning('Paramiko ProxyCommand support unavailable. '
|
||||
'Please upgrade to Paramiko 1.9.0 or newer. '
|
||||
'Not using configured ProxyCommand')
|
||||
|
||||
return sock_kwarg
|
||||
|
||||
def _connect(self) -> Connection:
|
||||
""" activates the connection object """
|
||||
|
||||
if paramiko is None:
|
||||
raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
|
||||
|
||||
port = self.get_option('port')
|
||||
display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
|
||||
host=self.get_option('remote_addr'))
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
|
||||
# Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
|
||||
# is keeping or omitting rsa-sha2 algorithms
|
||||
# default_keys: t.Tuple[str] = ()
|
||||
paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
|
||||
paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
|
||||
use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
|
||||
disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
|
||||
if not use_rsa_sha2_algorithms:
|
||||
if paramiko_preferred_pubkeys:
|
||||
disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
|
||||
if paramiko_preferred_hostkeys:
|
||||
disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
|
||||
|
||||
# override paramiko's default logger name
|
||||
if self._log_channel is not None:
|
||||
ssh.set_log_channel(self._log_channel)
|
||||
|
||||
self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
|
||||
|
||||
if self.get_option('host_key_checking'):
|
||||
for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
|
||||
try:
|
||||
ssh.load_system_host_keys(ssh_known_hosts)
|
||||
break
|
||||
except IOError:
|
||||
pass # file was not found, but not required to function
|
||||
except paramiko.hostkeys.InvalidHostKey as e:
|
||||
raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
|
||||
try:
|
||||
ssh.load_system_host_keys()
|
||||
except paramiko.hostkeys.InvalidHostKey as e:
|
||||
raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
|
||||
|
||||
ssh_connect_kwargs = self._parse_proxy_command(port)
|
||||
ssh.set_missing_host_key_policy(MyAddPolicy(self))
|
||||
conn_password = self.get_option('password')
|
||||
allow_agent = True
|
||||
|
||||
if conn_password is not None:
|
||||
allow_agent = False
|
||||
|
||||
try:
|
||||
key_filename = None
|
||||
if self.get_option('private_key_file'):
|
||||
key_filename = os.path.expanduser(self.get_option('private_key_file'))
|
||||
|
||||
# paramiko 2.2 introduced auth_timeout parameter
|
||||
if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
|
||||
ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
|
||||
|
||||
# paramiko 1.15 introduced banner timeout parameter
|
||||
if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
|
||||
ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
|
||||
|
||||
ssh.connect(
|
||||
self.get_option('remote_addr').lower(),
|
||||
username=self.get_option('remote_user'),
|
||||
allow_agent=allow_agent,
|
||||
look_for_keys=self.get_option('look_for_keys'),
|
||||
key_filename=key_filename,
|
||||
password=conn_password,
|
||||
timeout=self.get_option('timeout'),
|
||||
port=port,
|
||||
disabled_algorithms=disabled_algorithms,
|
||||
**ssh_connect_kwargs,
|
||||
)
|
||||
except paramiko.ssh_exception.BadHostKeyException as e:
|
||||
raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
|
||||
except paramiko.ssh_exception.AuthenticationException as e:
|
||||
msg = f'Failed to authenticate: {e}'
|
||||
raise AnsibleAuthenticationFailure(msg)
|
||||
except Exception as e:
|
||||
msg = to_text(e)
|
||||
if u'PID check failed' in msg:
|
||||
raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
|
||||
elif u'Private key file is encrypted' in msg:
|
||||
msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
|
||||
f'{msg}\nTo connect as a different user, use -u <username>.'
|
||||
raise AnsibleConnectionFailure(msg)
|
||||
else:
|
||||
raise AnsibleConnectionFailure(msg)
|
||||
self.ssh = ssh
|
||||
self._connected = True
|
||||
return self
|
||||
|
||||
def _any_keys_added(self) -> bool:
|
||||
for hostname, keys in self.ssh._host_keys.items():
|
||||
for keytype, key in keys.items():
|
||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||
if added_this_time:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _save_ssh_host_keys(self, filename: str) -> None:
|
||||
"""
|
||||
not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
|
||||
don't complain about it :)
|
||||
"""
|
||||
|
||||
if not self._any_keys_added():
|
||||
return
|
||||
|
||||
path = os.path.expanduser('~/.ssh')
|
||||
makedirs_safe(path)
|
||||
|
||||
with open(filename, 'w') as f:
|
||||
for hostname, keys in self.ssh._host_keys.items():
|
||||
for keytype, key in keys.items():
|
||||
# was f.write
|
||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||
if not added_this_time:
|
||||
f.write(f'{hostname} {keytype} {key.get_base64()}\n')
|
||||
|
||||
for hostname, keys in self.ssh._host_keys.items():
|
||||
for keytype, key in keys.items():
|
||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||
if added_this_time:
|
||||
f.write(f'{hostname} {keytype} {key.get_base64()}\n')
|
||||
|
||||
def _build_pct_command(self, cmd: str) -> str:
|
||||
cmd = ['/usr/sbin/pct', 'exec', str(self.get_option('vmid')), '--', cmd]
|
||||
if self.get_option('remote_user') != 'root':
|
||||
cmd = [self.get_option('proxmox_become_method')] + cmd
|
||||
display.vvv(f'INFO Running as non root user: {self.get_option("remote_user")}, trying to run pct with become method: ' +
|
||||
f'{self.get_option("proxmox_become_method")}',
|
||||
host=self.get_option('remote_addr'))
|
||||
return ' '.join(cmd)
|
||||
|
||||
def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
|
||||
""" run a command on inside the LXC container """
|
||||
|
||||
cmd = self._build_pct_command(cmd)
|
||||
|
||||
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||
|
||||
bufsize = 4096
|
||||
|
||||
try:
|
||||
self.ssh.get_transport().set_keepalive(5)
|
||||
chan = self.ssh.get_transport().open_session()
|
||||
except Exception as e:
|
||||
text_e = to_text(e)
|
||||
msg = 'Failed to open session'
|
||||
if text_e:
|
||||
msg += f': {text_e}'
|
||||
raise AnsibleConnectionFailure(to_native(msg))
|
||||
|
||||
# sudo usually requires a PTY (cf. requiretty option), therefore
|
||||
# we give it one by default (pty=True in ansible.cfg), and we try
|
||||
# to initialise from the calling environment when sudoable is enabled
|
||||
if self.get_option('pty') and sudoable:
|
||||
chan.get_pty(term=os.getenv('TERM', 'vt100'), width=int(os.getenv('COLUMNS', 0)), height=int(os.getenv('LINES', 0)))
|
||||
|
||||
display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
|
||||
|
||||
cmd = to_bytes(cmd, errors='surrogate_or_strict')
|
||||
|
||||
no_prompt_out = b''
|
||||
no_prompt_err = b''
|
||||
become_output = b''
|
||||
|
||||
try:
|
||||
chan.exec_command(cmd)
|
||||
if self.become and self.become.expect_prompt():
|
||||
password_prompt = False
|
||||
become_success = False
|
||||
while not (become_success or password_prompt):
|
||||
display.debug('Waiting for Privilege Escalation input')
|
||||
|
||||
chunk = chan.recv(bufsize)
|
||||
display.debug(f'chunk is: {to_text(chunk)}')
|
||||
if not chunk:
|
||||
if b'unknown user' in become_output:
|
||||
n_become_user = to_native(self.become.get_option('become_user'))
|
||||
raise AnsibleError(f'user {n_become_user} does not exist')
|
||||
else:
|
||||
break
|
||||
# raise AnsibleError('ssh connection closed waiting for password prompt')
|
||||
become_output += chunk
|
||||
|
||||
# need to check every line because we might get lectured
|
||||
# and we might get the middle of a line in a chunk
|
||||
for line in become_output.splitlines(True):
|
||||
if self.become.check_success(line):
|
||||
become_success = True
|
||||
break
|
||||
elif self.become.check_password_prompt(line):
|
||||
password_prompt = True
|
||||
break
|
||||
|
||||
if password_prompt:
|
||||
if self.become:
|
||||
become_pass = self.become.get_option('become_pass')
|
||||
chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
else:
|
||||
raise AnsibleError('A password is required but none was supplied')
|
||||
else:
|
||||
no_prompt_out += become_output
|
||||
no_prompt_err += become_output
|
||||
|
||||
if in_data:
|
||||
for i in range(0, len(in_data), bufsize):
|
||||
chan.send(in_data[i:i + bufsize])
|
||||
chan.shutdown_write()
|
||||
elif in_data == b'':
|
||||
chan.shutdown_write()
|
||||
|
||||
except socket.timeout:
|
||||
raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
|
||||
|
||||
stdout = b''.join(chan.makefile('rb', bufsize))
|
||||
stderr = b''.join(chan.makefile_stderr('rb', bufsize))
|
||||
returncode = chan.recv_exit_status()
|
||||
|
||||
if 'pct: not found' in stderr.decode('utf-8'):
|
||||
raise AnsibleError(
|
||||
f'pct not found in path of host: {to_text(self.get_option("remote_addr"))}')
|
||||
|
||||
return (returncode, no_prompt_out + stdout, no_prompt_out + stderr)
|
||||
|
||||
def put_file(self, in_path: str, out_path: str) -> None:
|
||||
""" transfer a file from local to remote """
|
||||
|
||||
display.vvv(f'PUT {in_path} TO {out_path}', host=self.get_option('remote_addr'))
|
||||
try:
|
||||
with open(in_path, 'rb') as f:
|
||||
data = f.read()
|
||||
returncode, stdout, stderr = self.exec_command(
|
||||
' '.join([
|
||||
self._shell.executable, '-c',
|
||||
self._shell.quote(f'cat > {out_path}')]),
|
||||
in_data=data,
|
||||
sudoable=False)
|
||||
if returncode != 0:
|
||||
if 'cat: not found' in stderr.decode('utf-8'):
|
||||
raise AnsibleError(
|
||||
f'cat not found in path of container: {to_text(self.get_option("vmid"))}')
|
||||
raise AnsibleError(
|
||||
f'{to_text(stdout)}\n{to_text(stderr)}')
|
||||
except Exception as e:
|
||||
raise AnsibleError(
|
||||
f'error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}')
|
||||
|
||||
def fetch_file(self, in_path: str, out_path: str) -> None:
|
||||
""" save a remote file to the specified path """
|
||||
|
||||
display.vvv(f'FETCH {in_path} TO {out_path}', host=self.get_option('remote_addr'))
|
||||
try:
|
||||
returncode, stdout, stderr = self.exec_command(
|
||||
' '.join([
|
||||
self._shell.executable, '-c',
|
||||
self._shell.quote(f'cat {in_path}')]),
|
||||
sudoable=False)
|
||||
if returncode != 0:
|
||||
if 'cat: not found' in stderr.decode('utf-8'):
|
||||
raise AnsibleError(
|
||||
f'cat not found in path of container: {to_text(self.get_option("vmid"))}')
|
||||
raise AnsibleError(
|
||||
f'{to_text(stdout)}\n{to_text(stderr)}')
|
||||
with open(out_path, 'wb') as f:
|
||||
f.write(stdout)
|
||||
except Exception as e:
|
||||
raise AnsibleError(
|
||||
f'error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}')
|
||||
|
||||
def reset(self) -> None:
|
||||
""" reset the connection """
|
||||
|
||||
if not self._connected:
|
||||
return
|
||||
self.close()
|
||||
self._connect()
|
||||
|
||||
def close(self) -> None:
|
||||
""" terminate the connection """
|
||||
|
||||
if self.get_option('host_key_checking') and self.get_option('record_host_keys') and self._any_keys_added():
|
||||
# add any new SSH host keys -- warning -- this could be slow
|
||||
# (This doesn't acquire the connection lock because it needs
|
||||
# to exclude only other known_hosts writers, not connections
|
||||
# that are starting up.)
|
||||
lockfile = os.path.basename(self.keyfile)
|
||||
dirname = os.path.dirname(self.keyfile)
|
||||
makedirs_safe(dirname)
|
||||
tmp_keyfile_name = None
|
||||
try:
|
||||
with FileLock().lock_file(lockfile, dirname, self.get_option('lock_file_timeout')):
|
||||
# just in case any were added recently
|
||||
|
||||
self.ssh.load_system_host_keys()
|
||||
self.ssh._host_keys.update(self.ssh._system_host_keys)
|
||||
|
||||
# gather information about the current key file, so
|
||||
# we can ensure the new file has the correct mode/owner
|
||||
|
||||
key_dir = os.path.dirname(self.keyfile)
|
||||
if os.path.exists(self.keyfile):
|
||||
key_stat = os.stat(self.keyfile)
|
||||
mode = key_stat.st_mode & 0o777
|
||||
uid = key_stat.st_uid
|
||||
gid = key_stat.st_gid
|
||||
else:
|
||||
mode = 0o644
|
||||
uid = os.getuid()
|
||||
gid = os.getgid()
|
||||
|
||||
# Save the new keys to a temporary file and move it into place
|
||||
# rather than rewriting the file. We set delete=False because
|
||||
# the file will be moved into place rather than cleaned up.
|
||||
|
||||
with tempfile.NamedTemporaryFile(dir=key_dir, delete=False) as tmp_keyfile:
|
||||
tmp_keyfile_name = tmp_keyfile.name
|
||||
os.chmod(tmp_keyfile_name, mode)
|
||||
os.chown(tmp_keyfile_name, uid, gid)
|
||||
self._save_ssh_host_keys(tmp_keyfile_name)
|
||||
|
||||
os.rename(tmp_keyfile_name, self.keyfile)
|
||||
except LockTimeout:
|
||||
raise AnsibleError(
|
||||
f'writing lock file for {self.keyfile} ran in to the timeout of {self.get_option("lock_file_timeout")}s')
|
||||
except paramiko.hostkeys.InvalidHostKey as e:
|
||||
raise AnsibleConnectionFailure(f'Invalid host key: {e.line}')
|
||||
except Exception as e:
|
||||
# unable to save keys, including scenario when key was invalid
|
||||
# and caught earlier
|
||||
raise AnsibleError(
|
||||
f'error occurred while writing SSH host keys!\n{to_text(e)}')
|
||||
finally:
|
||||
if tmp_keyfile_name is not None:
|
||||
pathlib.Path(tmp_keyfile_name).unlink(missing_ok=True)
|
||||
|
||||
self.ssh.close()
|
||||
self._connected = False
|
|
@ -1,84 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) Ansible project
|
||||
# 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
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
# Common parameters for Proxmox VE modules
|
||||
DOCUMENTATION = r"""
|
||||
options:
|
||||
api_host:
|
||||
description:
|
||||
- Specify the target host of the Proxmox VE cluster.
|
||||
type: str
|
||||
required: true
|
||||
api_port:
|
||||
description:
|
||||
- Specify the target port of the Proxmox VE cluster.
|
||||
- Uses the E(PROXMOX_PORT) environment variable if not specified.
|
||||
type: int
|
||||
required: false
|
||||
version_added: 9.1.0
|
||||
api_user:
|
||||
description:
|
||||
- Specify the user to authenticate with.
|
||||
type: str
|
||||
required: true
|
||||
api_password:
|
||||
description:
|
||||
- Specify the password to authenticate with.
|
||||
- You can use E(PROXMOX_PASSWORD) environment variable.
|
||||
type: str
|
||||
api_token_id:
|
||||
description:
|
||||
- Specify the token ID.
|
||||
- Requires C(proxmoxer>=1.1.0) to work.
|
||||
type: str
|
||||
version_added: 1.3.0
|
||||
api_token_secret:
|
||||
description:
|
||||
- Specify the token secret.
|
||||
- Requires C(proxmoxer>=1.1.0) to work.
|
||||
type: str
|
||||
version_added: 1.3.0
|
||||
validate_certs:
|
||||
description:
|
||||
- If V(false), SSL certificates will not be validated.
|
||||
- This should only be used on personally controlled sites using self-signed certificates.
|
||||
type: bool
|
||||
default: false
|
||||
requirements: ["proxmoxer", "requests"]
|
||||
"""
|
||||
|
||||
SELECTION = r"""
|
||||
options:
|
||||
vmid:
|
||||
description:
|
||||
- Specifies the instance ID.
|
||||
- If not set the next available ID will be fetched from ProxmoxAPI.
|
||||
type: int
|
||||
node:
|
||||
description:
|
||||
- Proxmox VE node on which to operate.
|
||||
- Only required for O(state=present).
|
||||
- For every other states it will be autodiscovered.
|
||||
type: str
|
||||
pool:
|
||||
description:
|
||||
- Add the new VM to the specified pool.
|
||||
type: str
|
||||
"""
|
||||
|
||||
ACTIONGROUP_PROXMOX = r"""
|
||||
options: {}
|
||||
attributes:
|
||||
action_group:
|
||||
description: Use C(group/community.general.proxmox) in C(module_defaults) to set defaults for this module.
|
||||
support: full
|
||||
membership:
|
||||
- community.general.proxmox
|
||||
"""
|
|
@ -1,715 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>, Daniel Lobato Garcia <dlobatog@redhat.com>
|
||||
# Copyright (c) 2018 Ansible Project
|
||||
# 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
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
name: proxmox
|
||||
short_description: Proxmox inventory source
|
||||
version_added: "1.2.0"
|
||||
author:
|
||||
- Jeffrey van Pelt (@Thulium-Drake) <jeff@vanpelt.one>
|
||||
requirements:
|
||||
- requests >= 1.1
|
||||
description:
|
||||
- Get inventory hosts from a Proxmox PVE cluster.
|
||||
- "Uses a configuration file as an inventory source, it must end in C(.proxmox.yml) or C(.proxmox.yaml)"
|
||||
- Will retrieve the first network interface with an IP for Proxmox nodes.
|
||||
- Can retrieve LXC/QEMU configuration as facts.
|
||||
extends_documentation_fragment:
|
||||
- constructed
|
||||
- inventory_cache
|
||||
options:
|
||||
plugin:
|
||||
description: The name of this plugin, it should always be set to V(community.general.proxmox) for this plugin to recognize it as its own.
|
||||
required: true
|
||||
choices: ['community.general.proxmox']
|
||||
type: str
|
||||
url:
|
||||
description:
|
||||
- URL to Proxmox cluster.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable E(PROXMOX_URL) will be used instead.
|
||||
- Since community.general 4.7.0 you can also use templating to specify the value of the O(url).
|
||||
default: 'http://localhost:8006'
|
||||
type: str
|
||||
env:
|
||||
- name: PROXMOX_URL
|
||||
version_added: 2.0.0
|
||||
user:
|
||||
description:
|
||||
- Proxmox authentication user.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable E(PROXMOX_USER) will be used instead.
|
||||
- Since community.general 4.7.0 you can also use templating to specify the value of the O(user).
|
||||
required: true
|
||||
type: str
|
||||
env:
|
||||
- name: PROXMOX_USER
|
||||
version_added: 2.0.0
|
||||
password:
|
||||
description:
|
||||
- Proxmox authentication password.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable E(PROXMOX_PASSWORD) will be used instead.
|
||||
- Since community.general 4.7.0 you can also use templating to specify the value of the O(password).
|
||||
- If you do not specify a password, you must set O(token_id) and O(token_secret) instead.
|
||||
type: str
|
||||
env:
|
||||
- name: PROXMOX_PASSWORD
|
||||
version_added: 2.0.0
|
||||
token_id:
|
||||
description:
|
||||
- Proxmox authentication token ID.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable E(PROXMOX_TOKEN_ID) will be used instead.
|
||||
- To use token authentication, you must also specify O(token_secret). If you do not specify O(token_id) and O(token_secret),
|
||||
you must set a password instead.
|
||||
- Make sure to grant explicit pve permissions to the token or disable 'privilege separation' to use the users' privileges instead.
|
||||
version_added: 4.8.0
|
||||
type: str
|
||||
env:
|
||||
- name: PROXMOX_TOKEN_ID
|
||||
token_secret:
|
||||
description:
|
||||
- Proxmox authentication token secret.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable E(PROXMOX_TOKEN_SECRET) will be used instead.
|
||||
- To use token authentication, you must also specify O(token_id). If you do not specify O(token_id) and O(token_secret),
|
||||
you must set a password instead.
|
||||
version_added: 4.8.0
|
||||
type: str
|
||||
env:
|
||||
- name: PROXMOX_TOKEN_SECRET
|
||||
validate_certs:
|
||||
description: Verify SSL certificate if using HTTPS.
|
||||
type: boolean
|
||||
default: true
|
||||
group_prefix:
|
||||
description: Prefix to apply to Proxmox groups.
|
||||
default: proxmox_
|
||||
type: str
|
||||
facts_prefix:
|
||||
description: Prefix to apply to LXC/QEMU config facts.
|
||||
default: proxmox_
|
||||
type: str
|
||||
want_facts:
|
||||
description:
|
||||
- Gather LXC/QEMU configuration facts.
|
||||
- When O(want_facts) is set to V(true) more details about QEMU VM status are possible, besides the running and stopped states.
|
||||
Currently if the VM is running and it is suspended, the status will be running and the machine will be in C(running) group,
|
||||
but its actual state will be paused. See O(qemu_extended_statuses) for how to retrieve the real status.
|
||||
default: false
|
||||
type: bool
|
||||
qemu_extended_statuses:
|
||||
description:
|
||||
- Requires O(want_facts) to be set to V(true) to function. This will allow you to differentiate between C(paused) and C(prelaunch)
|
||||
statuses of the QEMU VMs.
|
||||
- This introduces multiple groups [prefixed with O(group_prefix)] C(prelaunch) and C(paused).
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 5.1.0
|
||||
want_proxmox_nodes_ansible_host:
|
||||
version_added: 3.0.0
|
||||
description:
|
||||
- Whether to set C(ansible_host) for proxmox nodes.
|
||||
- When set to V(true) (default), will use the first available interface. This can be different from what you expect.
|
||||
- The default of this option changed from V(true) to V(false) in community.general 6.0.0.
|
||||
type: bool
|
||||
default: false
|
||||
exclude_nodes:
|
||||
description: Exclude proxmox nodes and the nodes-group from the inventory output.
|
||||
type: bool
|
||||
default: false
|
||||
version_added: 8.1.0
|
||||
filters:
|
||||
version_added: 4.6.0
|
||||
description: A list of Jinja templates that allow filtering hosts.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
strict:
|
||||
version_added: 2.5.0
|
||||
compose:
|
||||
version_added: 2.5.0
|
||||
groups:
|
||||
version_added: 2.5.0
|
||||
keyed_groups:
|
||||
version_added: 2.5.0
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
---
|
||||
# Minimal example which will not gather additional facts for QEMU/LXC guests
|
||||
# By not specifying a URL the plugin will attempt to connect to the controller host on port 8006
|
||||
# my.proxmox.yml
|
||||
plugin: community.general.proxmox
|
||||
user: ansible@pve
|
||||
password: secure
|
||||
# Note that this can easily give you wrong values as ansible_host. See further below for
|
||||
# an example where this is set to `false` and where ansible_host is set with `compose`.
|
||||
want_proxmox_nodes_ansible_host: true
|
||||
|
||||
---
|
||||
# Instead of login with password, proxmox supports api token authentication since release 6.2.
|
||||
plugin: community.general.proxmox
|
||||
user: ci@pve
|
||||
token_id: gitlab-1
|
||||
token_secret: fa256e9c-26ab-41ec-82da-707a2c079829
|
||||
|
||||
---
|
||||
# The secret can also be a vault string or passed via the environment variable TOKEN_SECRET.
|
||||
plugin: community.general.proxmox
|
||||
user: ci@pve
|
||||
token_id: gitlab-1
|
||||
token_secret: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
62353634333163633336343265623632626339313032653563653165313262343931643431656138
|
||||
6134333736323265656466646539663134306166666237630a653363623262636663333762316136
|
||||
34616361326263383766366663393837626437316462313332663736623066656237386531663731
|
||||
3037646432383064630a663165303564623338666131353366373630656661333437393937343331
|
||||
32643131386134396336623736393634373936356332623632306561356361323737313663633633
|
||||
6231313333666361656537343562333337323030623732323833
|
||||
|
||||
---
|
||||
# More complete example demonstrating the use of 'want_facts' and the constructed options
|
||||
# Note that using facts returned by 'want_facts' in constructed options requires 'want_facts=true'
|
||||
# my.proxmox.yml
|
||||
plugin: community.general.proxmox
|
||||
url: http://pve.domain.com:8006
|
||||
user: ansible@pve
|
||||
password: secure
|
||||
want_facts: true
|
||||
keyed_groups:
|
||||
# proxmox_tags_parsed is an example of a fact only returned when 'want_facts=true'
|
||||
- key: proxmox_tags_parsed
|
||||
separator: ""
|
||||
prefix: group
|
||||
groups:
|
||||
webservers: "'web' in (proxmox_tags_parsed|list)"
|
||||
mailservers: "'mail' in (proxmox_tags_parsed|list)"
|
||||
compose:
|
||||
ansible_port: 2222
|
||||
# Note that this can easily give you wrong values as ansible_host. See further below for
|
||||
# an example where this is set to `false` and where ansible_host is set with `compose`.
|
||||
want_proxmox_nodes_ansible_host: true
|
||||
|
||||
---
|
||||
# Using the inventory to allow ansible to connect via the first IP address of the VM / Container
|
||||
# (Default is connection by name of QEMU/LXC guests)
|
||||
# Note: my_inv_var demonstrates how to add a string variable to every host used by the inventory.
|
||||
# my.proxmox.yml
|
||||
plugin: community.general.proxmox
|
||||
url: http://192.168.1.2:8006
|
||||
user: ansible@pve
|
||||
password: secure
|
||||
validate_certs: false # only do this when you trust the network!
|
||||
want_facts: true
|
||||
want_proxmox_nodes_ansible_host: false
|
||||
compose:
|
||||
ansible_host: proxmox_ipconfig0.ip | default(proxmox_net0.ip) | ipaddr('address')
|
||||
my_inv_var_1: "'my_var1_value'"
|
||||
my_inv_var_2: >
|
||||
"my_var_2_value"
|
||||
|
||||
---
|
||||
# Specify the url, user and password using templating
|
||||
# my.proxmox.yml
|
||||
plugin: community.general.proxmox
|
||||
url: "{{ lookup('ansible.builtin.ini', 'url', section='proxmox', file='file.ini') }}"
|
||||
user: "{{ lookup('ansible.builtin.env','PM_USER') | default('ansible@pve') }}"
|
||||
password: "{{ lookup('community.general.random_string', base64=True) }}"
|
||||
# Note that this can easily give you wrong values as ansible_host. See further up for
|
||||
# an example where this is set to `false` and where ansible_host is set with `compose`.
|
||||
want_proxmox_nodes_ansible_host: true
|
||||
'''
|
||||
|
||||
import itertools
|
||||
import re
|
||||
|
||||
from ansible.module_utils.common._collections_compat import MutableMapping
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.utils.display import Display
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
|
||||
|
||||
# 3rd party imports
|
||||
try:
|
||||
import requests
|
||||
if LooseVersion(requests.__version__) < LooseVersion('1.1.0'):
|
||||
raise ImportError
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
''' Host inventory parser for ansible using Proxmox as source. '''
|
||||
|
||||
NAME = 'community.general.proxmox'
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super(InventoryModule, self).__init__()
|
||||
|
||||
# from config
|
||||
self.proxmox_url = None
|
||||
|
||||
self.session = None
|
||||
self.cache_key = None
|
||||
self.use_cache = None
|
||||
|
||||
def verify_file(self, path):
|
||||
|
||||
valid = False
|
||||
if super(InventoryModule, self).verify_file(path):
|
||||
if path.endswith(('proxmox.yaml', 'proxmox.yml')):
|
||||
valid = True
|
||||
else:
|
||||
self.display.vvv('Skipping due to inventory source not ending in "proxmox.yaml" nor "proxmox.yml"')
|
||||
return valid
|
||||
|
||||
def _get_session(self):
|
||||
if not self.session:
|
||||
self.session = requests.session()
|
||||
self.session.verify = self.get_option('validate_certs')
|
||||
return self.session
|
||||
|
||||
def _get_auth(self):
|
||||
validate_certs = self.get_option('validate_certs')
|
||||
|
||||
if validate_certs is False:
|
||||
from requests.packages.urllib3 import disable_warnings
|
||||
disable_warnings()
|
||||
|
||||
if self.proxmox_password:
|
||||
credentials = urlencode({'username': self.proxmox_user, 'password': self.proxmox_password})
|
||||
a = self._get_session()
|
||||
ret = a.post(f'{self.proxmox_url}/api2/json/access/ticket', data=credentials)
|
||||
json = ret.json()
|
||||
self.headers = {
|
||||
# only required for POST/PUT/DELETE methods, which we are not using currently
|
||||
# 'CSRFPreventionToken': json['data']['CSRFPreventionToken'],
|
||||
'Cookie': f"PVEAuthCookie={json['data']['ticket']}"
|
||||
}
|
||||
else:
|
||||
# Clean and format token components
|
||||
user = self.proxmox_user.strip()
|
||||
token_id = self.proxmox_token_id.strip()
|
||||
token_secret = self.proxmox_token_secret.strip()
|
||||
|
||||
# Build token string without newlines
|
||||
token = f'{user}!{token_id}={token_secret}'
|
||||
|
||||
# Set headers with clean token
|
||||
self.headers = {'Authorization': f'PVEAPIToken={token}'}
|
||||
|
||||
def _get_json(self, url, ignore_errors=None):
|
||||
|
||||
data = []
|
||||
has_data = False
|
||||
|
||||
if self.use_cache:
|
||||
try:
|
||||
data = self._cache[self.cache_key][url]
|
||||
has_data = True
|
||||
except KeyError:
|
||||
self.update_cache = True
|
||||
|
||||
if not has_data:
|
||||
s = self._get_session()
|
||||
while True:
|
||||
ret = s.get(url, headers=self.headers)
|
||||
if ignore_errors and ret.status_code in ignore_errors:
|
||||
break
|
||||
ret.raise_for_status()
|
||||
json = ret.json()
|
||||
|
||||
# process results
|
||||
# FIXME: This assumes 'return type' matches a specific query,
|
||||
# it will break if we expand the queries and they dont have different types
|
||||
if 'data' not in json:
|
||||
# /hosts/:id does not have a 'data' key
|
||||
data = json
|
||||
break
|
||||
elif isinstance(json['data'], MutableMapping):
|
||||
# /facts are returned as dict in 'data'
|
||||
data = json['data']
|
||||
break
|
||||
else:
|
||||
if json['data']:
|
||||
# /hosts 's 'results' is a list of all hosts, returned is paginated
|
||||
data = data + json['data']
|
||||
break
|
||||
|
||||
self._results[url] = data
|
||||
return make_unsafe(data)
|
||||
|
||||
def _get_nodes(self):
|
||||
return self._get_json(f"{self.proxmox_url}/api2/json/nodes")
|
||||
|
||||
def _get_pools(self):
|
||||
return self._get_json(f"{self.proxmox_url}/api2/json/pools")
|
||||
|
||||
def _get_lxc_per_node(self, node):
|
||||
return self._get_json(f"{self.proxmox_url}/api2/json/nodes/{node}/lxc")
|
||||
|
||||
def _get_qemu_per_node(self, node):
|
||||
return self._get_json(f"{self.proxmox_url}/api2/json/nodes/{node}/qemu")
|
||||
|
||||
def _get_members_per_pool(self, pool):
|
||||
ret = self._get_json(f"{self.proxmox_url}/api2/json/pools/{pool}")
|
||||
return ret['members']
|
||||
|
||||
def _get_node_ip(self, node):
|
||||
ret = self._get_json(f"{self.proxmox_url}/api2/json/nodes/{node}/network")
|
||||
|
||||
# sort interface by iface name to make selection as stable as possible
|
||||
ret.sort(key=lambda x: x['iface'])
|
||||
|
||||
for iface in ret:
|
||||
try:
|
||||
# only process interfaces adhering to these rules
|
||||
if 'active' not in iface:
|
||||
self.display.vvv(f"Interface {iface['iface']} on node {node} does not have an active state")
|
||||
continue
|
||||
if 'address' not in iface:
|
||||
self.display.vvv(f"Interface {iface['iface']} on node {node} does not have an address")
|
||||
continue
|
||||
if 'gateway' not in iface:
|
||||
self.display.vvv(f"Interface {iface['iface']} on node {node} does not have a gateway")
|
||||
continue
|
||||
self.display.vv(f"Using interface {iface['iface']} on node {node} with address {iface['address']} as node ip for ansible_host")
|
||||
return iface['address']
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
def _get_lxc_interfaces(self, properties, node, vmid):
|
||||
status_key = self._fact('status')
|
||||
|
||||
if status_key not in properties or not properties[status_key] == 'running':
|
||||
return
|
||||
|
||||
ret = self._get_json(f"{self.proxmox_url}/api2/json/nodes/{node}/lxc/{vmid}/interfaces", ignore_errors=[501])
|
||||
if not ret:
|
||||
return
|
||||
|
||||
result = []
|
||||
|
||||
for iface in ret:
|
||||
result_iface = {
|
||||
'name': iface['name'],
|
||||
'hwaddr': iface['hwaddr']
|
||||
}
|
||||
|
||||
if 'inet' in iface:
|
||||
result_iface['inet'] = iface['inet']
|
||||
|
||||
if 'inet6' in iface:
|
||||
result_iface['inet6'] = iface['inet6']
|
||||
|
||||
result.append(result_iface)
|
||||
|
||||
properties[self._fact('lxc_interfaces')] = result
|
||||
|
||||
def _get_agent_network_interfaces(self, node, vmid, vmtype):
|
||||
result = []
|
||||
|
||||
try:
|
||||
ifaces = self._get_json(
|
||||
f"{self.proxmox_url}/api2/json/nodes/{node}/{vmtype}/{vmid}/agent/network-get-interfaces"
|
||||
)['result']
|
||||
|
||||
if "error" in ifaces:
|
||||
if "class" in ifaces["error"]:
|
||||
# This happens on Windows, even though qemu agent is running, the IP address
|
||||
# cannot be fetched, as it is unsupported, also a command disabled can happen.
|
||||
errorClass = ifaces["error"]["class"]
|
||||
if errorClass in ["Unsupported"]:
|
||||
self.display.v("Retrieving network interfaces from guest agents on windows with older qemu-guest-agents is not supported")
|
||||
elif errorClass in ["CommandDisabled"]:
|
||||
self.display.v("Retrieving network interfaces from guest agents has been disabled")
|
||||
return result
|
||||
|
||||
for iface in ifaces:
|
||||
result.append({
|
||||
'name': iface['name'],
|
||||
'mac-address': iface['hardware-address'] if 'hardware-address' in iface else '',
|
||||
'ip-addresses': [f"{ip['ip-address']}/{ip['prefix']}" for ip in iface['ip-addresses']] if 'ip-addresses' in iface else []
|
||||
})
|
||||
except requests.HTTPError:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
def _get_vm_config(self, properties, node, vmid, vmtype, name):
|
||||
ret = self._get_json(f"{self.proxmox_url}/api2/json/nodes/{node}/{vmtype}/{vmid}/config")
|
||||
|
||||
properties[self._fact('node')] = node
|
||||
properties[self._fact('vmid')] = vmid
|
||||
properties[self._fact('vmtype')] = vmtype
|
||||
|
||||
plaintext_configs = [
|
||||
'description',
|
||||
]
|
||||
|
||||
for config in ret:
|
||||
key = self._fact(config)
|
||||
value = ret[config]
|
||||
try:
|
||||
# fixup disk images as they have no key
|
||||
if config == 'rootfs' or config.startswith(('virtio', 'sata', 'ide', 'scsi')):
|
||||
value = f"disk_image={value}"
|
||||
|
||||
# Additional field containing parsed tags as list
|
||||
if config == 'tags':
|
||||
stripped_value = value.strip()
|
||||
if stripped_value:
|
||||
parsed_key = f"{key}_parsed"
|
||||
properties[parsed_key] = [tag.strip() for tag in stripped_value.replace(',', ';').split(";")]
|
||||
|
||||
# The first field in the agent string tells you whether the agent is enabled
|
||||
# the rest of the comma separated string is extra config for the agent.
|
||||
# In some (newer versions of proxmox) instances it can be 'enabled=1'.
|
||||
if config == 'agent':
|
||||
agent_enabled = 0
|
||||
try:
|
||||
agent_enabled = int(value.split(',')[0])
|
||||
except ValueError:
|
||||
if value.split(',')[0] == "enabled=1":
|
||||
agent_enabled = 1
|
||||
if agent_enabled:
|
||||
agent_iface_value = self._get_agent_network_interfaces(node, vmid, vmtype)
|
||||
if agent_iface_value:
|
||||
agent_iface_key = self.to_safe(f'{key}_interfaces')
|
||||
properties[agent_iface_key] = agent_iface_value
|
||||
|
||||
if config == 'lxc':
|
||||
out_val = {}
|
||||
for k, v in value:
|
||||
if k.startswith('lxc.'):
|
||||
k = k[len('lxc.'):]
|
||||
out_val[k] = v
|
||||
value = out_val
|
||||
|
||||
if config not in plaintext_configs and isinstance(value, string_types) \
|
||||
and all("=" in v for v in value.split(",")):
|
||||
# split off strings with commas to a dict
|
||||
# skip over any keys that cannot be processed
|
||||
try:
|
||||
value = dict(key.split("=", 1) for key in value.split(","))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
properties[key] = value
|
||||
except NameError:
|
||||
return None
|
||||
|
||||
def _get_vm_status(self, properties, node, vmid, vmtype, name):
|
||||
ret = self._get_json(f"{self.proxmox_url}/api2/json/nodes/{node}/{vmtype}/{vmid}/status/current")
|
||||
properties[self._fact('status')] = ret['status']
|
||||
if vmtype == 'qemu':
|
||||
properties[self._fact('qmpstatus')] = ret['qmpstatus']
|
||||
|
||||
def _get_vm_snapshots(self, properties, node, vmid, vmtype, name):
|
||||
ret = self._get_json(f"{self.proxmox_url}/api2/json/nodes/{node}/{vmtype}/{vmid}/snapshot")
|
||||
snapshots = [snapshot['name'] for snapshot in ret if snapshot['name'] != 'current']
|
||||
properties[self._fact('snapshots')] = snapshots
|
||||
|
||||
def to_safe(self, word):
|
||||
'''Converts 'bad' characters in a string to underscores so they can be used as Ansible groups
|
||||
#> ProxmoxInventory.to_safe("foo-bar baz")
|
||||
'foo_barbaz'
|
||||
'''
|
||||
regex = r"[^A-Za-z0-9\_]"
|
||||
return re.sub(regex, "_", word.replace(" ", ""))
|
||||
|
||||
def _fact(self, name):
|
||||
'''Generate a fact's full name from the common prefix and a name.'''
|
||||
return self.to_safe(f'{self.facts_prefix}{name.lower()}')
|
||||
|
||||
def _group(self, name):
|
||||
'''Generate a group's full name from the common prefix and a name.'''
|
||||
return self.to_safe(f'{self.group_prefix}{name.lower()}')
|
||||
|
||||
def _can_add_host(self, name, properties):
|
||||
'''Ensure that a host satisfies all defined hosts filters. If strict mode is
|
||||
enabled, any error during host filter compositing will lead to an AnsibleError
|
||||
being raised, otherwise the filter will be ignored.
|
||||
'''
|
||||
for host_filter in self.host_filters:
|
||||
try:
|
||||
if not self._compose(host_filter, properties):
|
||||
return False
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
message = f"Could not evaluate host filter {host_filter} for host {name} - {e}"
|
||||
if self.strict:
|
||||
raise AnsibleError(message)
|
||||
display.warning(message)
|
||||
return True
|
||||
|
||||
def _add_host(self, name, variables):
|
||||
self.inventory.add_host(name)
|
||||
for k, v in variables.items():
|
||||
self.inventory.set_variable(name, k, v)
|
||||
variables = self.inventory.get_host(name).get_vars()
|
||||
self._set_composite_vars(self.get_option('compose'), variables, name, strict=self.strict)
|
||||
self._add_host_to_composed_groups(self.get_option('groups'), variables, name, strict=self.strict)
|
||||
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), variables, name, strict=self.strict)
|
||||
|
||||
def _handle_item(self, node, ittype, item):
|
||||
'''Handle an item from the list of LXC containers and Qemu VM. The
|
||||
return value will be either None if the item was skipped or the name of
|
||||
the item if it was added to the inventory.'''
|
||||
if item.get('template'):
|
||||
return None
|
||||
|
||||
properties = dict()
|
||||
name, vmid = item['name'], item['vmid']
|
||||
|
||||
# get status, config and snapshots if want_facts == True
|
||||
want_facts = self.get_option('want_facts')
|
||||
if want_facts:
|
||||
self._get_vm_status(properties, node, vmid, ittype, name)
|
||||
self._get_vm_config(properties, node, vmid, ittype, name)
|
||||
self._get_vm_snapshots(properties, node, vmid, ittype, name)
|
||||
|
||||
if ittype == 'lxc':
|
||||
self._get_lxc_interfaces(properties, node, vmid)
|
||||
|
||||
# ensure the host satisfies filters
|
||||
if not self._can_add_host(name, properties):
|
||||
return None
|
||||
|
||||
# add the host to the inventory
|
||||
self._add_host(name, properties)
|
||||
node_type_group = self._group(f'{node}_{ittype}')
|
||||
self.inventory.add_child(self._group(f"all_{ittype}"), name)
|
||||
self.inventory.add_child(node_type_group, name)
|
||||
|
||||
item_status = item['status']
|
||||
if item_status == 'running':
|
||||
if want_facts and ittype == 'qemu' and self.get_option('qemu_extended_statuses'):
|
||||
# get more details about the status of the qemu VM
|
||||
item_status = properties.get(self._fact('qmpstatus'), item_status)
|
||||
self.inventory.add_child(self._group(f'all_{item_status}'), name)
|
||||
|
||||
return name
|
||||
|
||||
def _populate_pool_groups(self, added_hosts):
|
||||
'''Generate groups from Proxmox resource pools, ignoring VMs and
|
||||
containers that were skipped.'''
|
||||
for pool in self._get_pools():
|
||||
poolid = pool.get('poolid')
|
||||
if not poolid:
|
||||
continue
|
||||
pool_group = self._group(f"pool_{poolid}")
|
||||
self.inventory.add_group(pool_group)
|
||||
|
||||
for member in self._get_members_per_pool(poolid):
|
||||
name = member.get('name')
|
||||
if name and name in added_hosts:
|
||||
self.inventory.add_child(pool_group, name)
|
||||
|
||||
def _populate(self):
|
||||
|
||||
# create common groups
|
||||
default_groups = ['lxc', 'qemu', 'running', 'stopped']
|
||||
|
||||
if self.get_option('qemu_extended_statuses'):
|
||||
default_groups.extend(['prelaunch', 'paused'])
|
||||
|
||||
for group in default_groups:
|
||||
self.inventory.add_group(self._group(f'all_{group}'))
|
||||
nodes_group = self._group('nodes')
|
||||
if not self.exclude_nodes:
|
||||
self.inventory.add_group(nodes_group)
|
||||
|
||||
want_proxmox_nodes_ansible_host = self.get_option("want_proxmox_nodes_ansible_host")
|
||||
|
||||
# gather vm's on nodes
|
||||
self._get_auth()
|
||||
hosts = []
|
||||
for node in self._get_nodes():
|
||||
if not node.get('node'):
|
||||
continue
|
||||
if not self.exclude_nodes:
|
||||
self.inventory.add_host(node['node'])
|
||||
if node['type'] == 'node' and not self.exclude_nodes:
|
||||
self.inventory.add_child(nodes_group, node['node'])
|
||||
|
||||
if node['status'] == 'offline':
|
||||
continue
|
||||
|
||||
# get node IP address
|
||||
if want_proxmox_nodes_ansible_host and not self.exclude_nodes:
|
||||
ip = self._get_node_ip(node['node'])
|
||||
self.inventory.set_variable(node['node'], 'ansible_host', ip)
|
||||
|
||||
# Setting composite variables
|
||||
if not self.exclude_nodes:
|
||||
variables = self.inventory.get_host(node['node']).get_vars()
|
||||
self._set_composite_vars(self.get_option('compose'), variables, node['node'], strict=self.strict)
|
||||
|
||||
# add LXC/Qemu groups for the node
|
||||
for ittype in ('lxc', 'qemu'):
|
||||
node_type_group = self._group(f"{node['node']}_{ittype}")
|
||||
self.inventory.add_group(node_type_group)
|
||||
|
||||
# get LXC containers and Qemu VMs for this node
|
||||
lxc_objects = zip(itertools.repeat('lxc'), self._get_lxc_per_node(node['node']))
|
||||
qemu_objects = zip(itertools.repeat('qemu'), self._get_qemu_per_node(node['node']))
|
||||
for ittype, item in itertools.chain(lxc_objects, qemu_objects):
|
||||
name = self._handle_item(node['node'], ittype, item)
|
||||
if name is not None:
|
||||
hosts.append(name)
|
||||
|
||||
# gather vm's in pools
|
||||
self._populate_pool_groups(hosts)
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
if not HAS_REQUESTS:
|
||||
raise AnsibleError('This module requires Python Requests 1.1.0 or higher: '
|
||||
'https://github.com/psf/requests.')
|
||||
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
|
||||
# read config from file, this sets 'options'
|
||||
self._read_config_data(path)
|
||||
|
||||
# read and template auth options
|
||||
for o in ('url', 'user', 'password', 'token_id', 'token_secret'):
|
||||
v = self.get_option(o)
|
||||
if self.templar.is_template(v):
|
||||
v = self.templar.template(v, disable_lookups=False)
|
||||
setattr(self, f'proxmox_{o}', v)
|
||||
|
||||
# some more cleanup and validation
|
||||
self.proxmox_url = self.proxmox_url.rstrip('/')
|
||||
|
||||
if self.proxmox_password is None and (self.proxmox_token_id is None or self.proxmox_token_secret is None):
|
||||
raise AnsibleError('You must specify either a password or both token_id and token_secret.')
|
||||
|
||||
if self.get_option('qemu_extended_statuses') and not self.get_option('want_facts'):
|
||||
raise AnsibleError('You must set want_facts to True if you want to use qemu_extended_statuses.')
|
||||
# read rest of options
|
||||
self.exclude_nodes = self.get_option('exclude_nodes')
|
||||
self.cache_key = self.get_cache_key(path)
|
||||
self.use_cache = cache and self.get_option('cache')
|
||||
self.update_cache = not cache and self.get_option('cache')
|
||||
self.host_filters = self.get_option('filters')
|
||||
self.group_prefix = self.get_option('group_prefix')
|
||||
self.facts_prefix = self.get_option('facts_prefix')
|
||||
self.strict = self.get_option('strict')
|
||||
|
||||
# actually populate inventory
|
||||
self._results = {}
|
||||
self._populate()
|
||||
if self.update_cache:
|
||||
self._cache[self.cache_key] = self._results
|
|
@ -1,242 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2020, Tristan Le Guern <tleguern at bouledef.eu>
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import traceback
|
||||
from time import sleep
|
||||
|
||||
PROXMOXER_IMP_ERR = None
|
||||
try:
|
||||
from proxmoxer import ProxmoxAPI
|
||||
from proxmoxer import __version__ as proxmoxer_version
|
||||
HAS_PROXMOXER = True
|
||||
except ImportError:
|
||||
HAS_PROXMOXER = False
|
||||
PROXMOXER_IMP_ERR = traceback.format_exc()
|
||||
|
||||
|
||||
from ansible.module_utils.basic import env_fallback, missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
|
||||
def proxmox_auth_argument_spec():
|
||||
return dict(
|
||||
api_host=dict(type='str',
|
||||
required=True,
|
||||
fallback=(env_fallback, ['PROXMOX_HOST'])
|
||||
),
|
||||
api_port=dict(type='int',
|
||||
fallback=(env_fallback, ['PROXMOX_PORT'])
|
||||
),
|
||||
api_user=dict(type='str',
|
||||
required=True,
|
||||
fallback=(env_fallback, ['PROXMOX_USER'])
|
||||
),
|
||||
api_password=dict(type='str',
|
||||
no_log=True,
|
||||
fallback=(env_fallback, ['PROXMOX_PASSWORD'])
|
||||
),
|
||||
api_token_id=dict(type='str',
|
||||
no_log=False
|
||||
),
|
||||
api_token_secret=dict(type='str',
|
||||
no_log=True
|
||||
),
|
||||
validate_certs=dict(type='bool',
|
||||
default=False
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def proxmox_to_ansible_bool(value):
|
||||
'''Convert Proxmox representation of a boolean to be ansible-friendly'''
|
||||
return True if value == 1 else False
|
||||
|
||||
|
||||
def ansible_to_proxmox_bool(value):
|
||||
'''Convert Ansible representation of a boolean to be proxmox-friendly'''
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError("%s must be of type bool not %s" % (value, type(value)))
|
||||
|
||||
return 1 if value else 0
|
||||
|
||||
|
||||
class ProxmoxAnsible(object):
|
||||
"""Base class for Proxmox modules"""
|
||||
TASK_TIMED_OUT = 'timeout expired'
|
||||
|
||||
def __init__(self, module):
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
self.module = module
|
||||
self.proxmoxer_version = proxmoxer_version
|
||||
self.proxmox_api = self._connect()
|
||||
# Test token validity
|
||||
try:
|
||||
self.proxmox_api.version.get()
|
||||
except Exception as e:
|
||||
module.fail_json(msg='%s' % e, exception=traceback.format_exc())
|
||||
|
||||
def _connect(self):
|
||||
api_host = self.module.params['api_host']
|
||||
api_port = self.module.params['api_port']
|
||||
api_user = self.module.params['api_user']
|
||||
api_password = self.module.params['api_password']
|
||||
api_token_id = self.module.params['api_token_id']
|
||||
api_token_secret = self.module.params['api_token_secret']
|
||||
validate_certs = self.module.params['validate_certs']
|
||||
|
||||
auth_args = {'user': api_user}
|
||||
|
||||
if api_port:
|
||||
auth_args['port'] = api_port
|
||||
|
||||
if api_password:
|
||||
auth_args['password'] = api_password
|
||||
else:
|
||||
if self.proxmoxer_version < LooseVersion('1.1.0'):
|
||||
self.module.fail_json('Using "token_name" and "token_value" require proxmoxer>=1.1.0')
|
||||
auth_args['token_name'] = api_token_id
|
||||
auth_args['token_value'] = api_token_secret
|
||||
|
||||
try:
|
||||
return ProxmoxAPI(api_host, verify_ssl=validate_certs, **auth_args)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='%s' % e, exception=traceback.format_exc())
|
||||
|
||||
def version(self):
|
||||
try:
|
||||
apiversion = self.proxmox_api.version.get()
|
||||
return LooseVersion(apiversion['version'])
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to retrieve Proxmox VE version: %s' % e)
|
||||
|
||||
def get_node(self, node):
|
||||
try:
|
||||
nodes = [n for n in self.proxmox_api.nodes.get() if n['node'] == node]
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to retrieve Proxmox VE node: %s' % e)
|
||||
return nodes[0] if nodes else None
|
||||
|
||||
def get_nextvmid(self):
|
||||
try:
|
||||
return self.proxmox_api.cluster.nextid.get()
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to retrieve next free vmid: %s' % e)
|
||||
|
||||
def get_vmid(self, name, ignore_missing=False, choose_first_if_multiple=False):
|
||||
try:
|
||||
vms = [vm['vmid'] for vm in self.proxmox_api.cluster.resources.get(type='vm') if vm.get('name') == name]
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to retrieve list of VMs filtered by name %s: %s' % (name, e))
|
||||
|
||||
if not vms:
|
||||
if ignore_missing:
|
||||
return None
|
||||
|
||||
self.module.fail_json(msg='No VM with name %s found' % name)
|
||||
elif len(vms) > 1 and not choose_first_if_multiple:
|
||||
self.module.fail_json(msg='Multiple VMs with name %s found, provide vmid instead' % name)
|
||||
|
||||
return vms[0]
|
||||
|
||||
def get_vm(self, vmid, ignore_missing=False):
|
||||
try:
|
||||
vms = [vm for vm in self.proxmox_api.cluster.resources.get(type='vm') if vm['vmid'] == int(vmid)]
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to retrieve list of VMs filtered by vmid %s: %s' % (vmid, e))
|
||||
|
||||
if vms:
|
||||
return vms[0]
|
||||
else:
|
||||
if ignore_missing:
|
||||
return None
|
||||
|
||||
self.module.fail_json(msg='VM with vmid %s does not exist in cluster' % vmid)
|
||||
|
||||
def api_task_ok(self, node, taskid):
|
||||
try:
|
||||
status = self.proxmox_api.nodes(node).tasks(taskid).status.get()
|
||||
return status['status'] == 'stopped' and status['exitstatus'] == 'OK'
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to retrieve API task ID from node %s: %s' % (node, e))
|
||||
|
||||
def api_task_failed(self, node, taskid):
|
||||
""" Explicitly check if the task stops but exits with a failed status
|
||||
"""
|
||||
try:
|
||||
status = self.proxmox_api.nodes(node).tasks(taskid).status.get()
|
||||
return status['status'] == 'stopped' and status['exitstatus'] != 'OK'
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to retrieve API task ID from node %s: %s' % (node, e))
|
||||
|
||||
def api_task_complete(self, node_name, task_id, timeout):
|
||||
"""Wait until the task stops or times out.
|
||||
|
||||
:param node_name: Proxmox node name where the task is running.
|
||||
:param task_id: ID of the running task.
|
||||
:param timeout: Timeout in seconds to wait for the task to complete.
|
||||
:return: Task completion status (True/False) and ``exitstatus`` message when status=False.
|
||||
"""
|
||||
status = {}
|
||||
while timeout:
|
||||
try:
|
||||
status = self.proxmox_api.nodes(node_name).tasks(task_id).status.get()
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to retrieve API task ID from node %s: %s' % (node_name, e))
|
||||
|
||||
if status['status'] == 'stopped':
|
||||
if status['exitstatus'] == 'OK':
|
||||
return True, None
|
||||
else:
|
||||
return False, status['exitstatus']
|
||||
else:
|
||||
timeout -= 1
|
||||
if timeout <= 0:
|
||||
return False, ProxmoxAnsible.TASK_TIMED_OUT
|
||||
sleep(1)
|
||||
|
||||
def get_pool(self, poolid):
|
||||
"""Retrieve pool information
|
||||
|
||||
:param poolid: str - name of the pool
|
||||
:return: dict - pool information
|
||||
"""
|
||||
try:
|
||||
return self.proxmox_api.pools(poolid).get()
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to retrieve pool %s information: %s" % (poolid, e))
|
||||
|
||||
def get_storages(self, type):
|
||||
"""Retrieve storages information
|
||||
|
||||
:param type: str, optional - type of storages
|
||||
:return: list of dicts - array of storages
|
||||
"""
|
||||
try:
|
||||
return self.proxmox_api.storage.get(type=type)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to retrieve storages information with type %s: %s" % (type, e))
|
||||
|
||||
def get_storage_content(self, node, storage, content=None, vmid=None):
|
||||
try:
|
||||
return (
|
||||
self.proxmox_api.nodes(node)
|
||||
.storage(storage)
|
||||
.content()
|
||||
.get(content=content, vmid=vmid)
|
||||
)
|
||||
except Exception as e:
|
||||
self.module.fail_json(
|
||||
msg="Unable to list content on %s, %s for %s and %s: %s"
|
||||
% (node, storage, content, vmid, e)
|
||||
)
|
File diff suppressed because it is too large
Load diff
|
@ -1,570 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2024, IamLunchbox <r.grieger@hotmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_backup
|
||||
author: "Raphael Grieger (@IamLunchbox) <r.grieger@hotmail.com>"
|
||||
short_description: Start a VM backup in Proxmox VE cluster
|
||||
version_added: 10.1.0
|
||||
description:
|
||||
- Allows you to create backups of KVM and LXC guests in Proxmox VE cluster.
|
||||
- Offers the GUI functionality of creating a single backup as well as using the run-now functionality from the cluster backup
|
||||
schedule.
|
||||
- The mininum required privileges to use this module are C(VM.Backup) and C(Datastore.AllocateSpace) for the respective
|
||||
VMs and storage.
|
||||
- Most options are optional and if unspecified will be chosen by the Cluster and its default values.
|
||||
- Note that this module B(is not idempotent). It always starts a new backup (when not in check mode).
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
options:
|
||||
backup_mode:
|
||||
description:
|
||||
- The mode how Proxmox performs backups. The default is, to create a runtime snapshot including memory.
|
||||
- Check U(https://pve.proxmox.com/pve-docs/chapter-vzdump.html#_backup_modes) for an explanation of the differences.
|
||||
type: str
|
||||
choices: ["snapshot", "suspend", "stop"]
|
||||
default: snapshot
|
||||
bandwidth:
|
||||
description:
|
||||
- Limit the I/O bandwidth (in KiB/s) to write backup. V(0) is unlimited.
|
||||
type: int
|
||||
change_detection_mode:
|
||||
description:
|
||||
- Set the change detection mode (available from Proxmox VE 8.3).
|
||||
- It is only used when backing up containers, Proxmox silently ignores this option when applied to kvm guests.
|
||||
type: str
|
||||
choices: ["legacy", "data", "metadata"]
|
||||
compress:
|
||||
description:
|
||||
- Enable additional compression of the backup archive.
|
||||
- V(0) will use the Proxmox recommended value, depending on your storage target.
|
||||
type: str
|
||||
choices: ["0", "1", "gzip", "lzo", "zstd"]
|
||||
compression_threads:
|
||||
description:
|
||||
- The number of threads zstd will use to compress the backup.
|
||||
- V(0) uses 50% of the available cores, anything larger than V(0) will use exactly as many threads.
|
||||
- Is ignored if you specify O(compress=gzip) or O(compress=lzo).
|
||||
type: int
|
||||
description:
|
||||
description:
|
||||
- Specify the description of the backup.
|
||||
- Needs to be a single line, newline and backslash need to be escaped as V(\\n) and V(\\\\) respectively.
|
||||
- If you need variable interpolation, you can set the content as usual through ansible jinja templating and/or let Proxmox
|
||||
substitute templates.
|
||||
- Proxmox currently supports V({{cluster}}), V({{guestname}}), V({{node}}), and V({{vmid}}) as templating variables.
|
||||
Since this is also a jinja delimiter, you need to set these values as raw jinja.
|
||||
default: "{{guestname}}"
|
||||
type: str
|
||||
fleecing:
|
||||
description:
|
||||
- Enable backup fleecing. Works only for virtual machines and their disks.
|
||||
- Must be entered as a string, containing key-value pairs in a list.
|
||||
type: str
|
||||
mode:
|
||||
description:
|
||||
- Specifices the mode to select backup targets.
|
||||
choices: ["include", "all", "pool"]
|
||||
required: true
|
||||
type: str
|
||||
node:
|
||||
description:
|
||||
- Only execute the backup job for the given node.
|
||||
- This option is usually used if O(mode=all).
|
||||
- If you specify a node ID and your vmids or pool do not reside there, they will not be backed up!
|
||||
type: str
|
||||
notification_mode:
|
||||
description:
|
||||
- Determine which notification system to use.
|
||||
type: str
|
||||
choices: ["auto", "legacy-sendmail", "notification-system"]
|
||||
default: auto
|
||||
performance_tweaks:
|
||||
description:
|
||||
- Enable other performance-related settings.
|
||||
- Must be entered as a string, containing comma separated key-value pairs.
|
||||
- 'For example: V(max-workers=2,pbs-entries-max=2).'
|
||||
type: str
|
||||
pool:
|
||||
description:
|
||||
- Specify a pool name to limit backups to guests to the given pool.
|
||||
- Required, when O(mode=pool).
|
||||
- Also required, when your user only has VM.Backup permission for this single pool.
|
||||
type: str
|
||||
protected:
|
||||
description:
|
||||
- Marks backups as protected.
|
||||
- '"Might fail, when the PBS backend has verify enabled due to this bug: U(https://bugzilla.proxmox.com/show_bug.cgi?id=4289)".'
|
||||
type: bool
|
||||
retention:
|
||||
description:
|
||||
- Use custom retention options instead of those from the default cluster configuration (which is usually V("keep-all=1")).
|
||||
- Always requires Datastore.Allocate permission at the storage endpoint.
|
||||
- Specifying a retention time other than V(keep-all=1) might trigger pruning on the datastore, if an existing backup
|
||||
should be deleted due to your specified timeframe.
|
||||
- Deleting requires C(Datastore.Modify) or C(Datastore.Prune) permissions on the backup storage.
|
||||
type: str
|
||||
storage:
|
||||
description:
|
||||
- Store the backup archive on this storage.
|
||||
type: str
|
||||
required: true
|
||||
vmids:
|
||||
description:
|
||||
- The instance IDs to be backed up.
|
||||
- Only valid, if O(mode=include).
|
||||
type: list
|
||||
elements: int
|
||||
wait:
|
||||
description:
|
||||
- Wait for the backup to be finished.
|
||||
- Fails, if job does not succeed successfully within the given timeout.
|
||||
type: bool
|
||||
default: false
|
||||
wait_timeout:
|
||||
description:
|
||||
- Seconds to wait for the backup to be finished.
|
||||
- Will only be evaluated, if O(wait=true).
|
||||
type: int
|
||||
default: 10
|
||||
requirements: ["proxmoxer", "requests"]
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Backup all vms in the Proxmox cluster to storage mypbs
|
||||
community.general.proxmox_backup:
|
||||
api_user: root@pam
|
||||
api_password: secret
|
||||
api_host: node1
|
||||
storage: mypbs
|
||||
mode: all
|
||||
|
||||
- name: Backup VMID 100 by stopping it and set an individual retention
|
||||
community.general.proxmox_backup:
|
||||
api_user: root@pam
|
||||
api_password: secret
|
||||
api_host: node1
|
||||
backup-mode: stop
|
||||
mode: include
|
||||
retention: keep-daily=5, keep-last=14, keep-monthly=4, keep-weekly=4, keep-yearly=0
|
||||
storage: mypbs
|
||||
vmid: [100]
|
||||
|
||||
- name: Backup all vms on node node2 to storage mypbs and wait for the task to finish
|
||||
community.general.proxmox_backup:
|
||||
api_user: test@pve
|
||||
api_password: 1q2w3e
|
||||
api_host: node2
|
||||
storage: mypbs
|
||||
mode: all
|
||||
node: node2
|
||||
wait: true
|
||||
wait_timeout: 30
|
||||
|
||||
- name: Use all the options
|
||||
community.general.proxmox_backup:
|
||||
api_user: root@pam
|
||||
api_password: secret
|
||||
api_host: node1
|
||||
bandwidth: 1000
|
||||
backup_mode: suspend
|
||||
compress: zstd
|
||||
compression_threads: 0
|
||||
description: A single backup for {% raw %}{{ guestname }}{% endraw %}
|
||||
mode: include
|
||||
notification_mode: notification-system
|
||||
protected: true
|
||||
retention: keep-monthly=1, keep-weekly=1
|
||||
storage: mypbs
|
||||
vmids:
|
||||
- 100
|
||||
- 101
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
backups:
|
||||
description: List of nodes and their task IDs.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
node:
|
||||
description: Node ID.
|
||||
returned: on success
|
||||
type: str
|
||||
status:
|
||||
description: Last known task status. Will be unknown, if O(wait=false).
|
||||
returned: on success
|
||||
type: str
|
||||
choices: ["unknown", "success", "failed"]
|
||||
upid:
|
||||
description: >-
|
||||
Proxmox cluster UPID, which is needed to lookup task info. Returns OK, when a cluster node did not create a task after
|
||||
being called, for example due to no matching targets.
|
||||
returned: on success
|
||||
type: str
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import ProxmoxAnsible, proxmox_auth_argument_spec
|
||||
|
||||
|
||||
def has_permission(permission_tree, permission, search_scopes, default=0, expected=1):
|
||||
return any(permission_tree.get(scope, {}).get(permission, default) == expected for scope in search_scopes)
|
||||
|
||||
|
||||
class ProxmoxBackupAnsible(ProxmoxAnsible):
|
||||
|
||||
def _get_permissions(self):
|
||||
return self.proxmox_api.access.permissions.get()
|
||||
|
||||
def _get_resources(self, resource_type=None):
|
||||
return self.proxmox_api.cluster.resources.get(type=resource_type)
|
||||
|
||||
def _get_tasklog(self, node, upid):
|
||||
return self.proxmox_api.nodes(node).tasks(upid).log.get()
|
||||
|
||||
def _get_taskok(self, node, upid):
|
||||
return self.proxmox_api.nodes(node).tasks(upid).status.get()
|
||||
|
||||
def _post_vzdump(self, node, request_body):
|
||||
return self.proxmox_api.nodes(node).vzdump.post(**request_body)
|
||||
|
||||
def request_backup(
|
||||
self,
|
||||
request_body,
|
||||
node_endpoints):
|
||||
task_ids = []
|
||||
|
||||
for node in node_endpoints:
|
||||
upid = self._post_vzdump(node, request_body)
|
||||
if upid != "OK":
|
||||
tasklog = ", ".join(logentry["t"] for logentry in self._get_tasklog(node, upid))
|
||||
else:
|
||||
tasklog = ""
|
||||
task_ids.extend([{"node": node, "upid": upid, "status": "unknown", "log": "%s" % tasklog}])
|
||||
return task_ids
|
||||
|
||||
def check_relevant_nodes(self, node):
|
||||
nodes = [
|
||||
item["node"]
|
||||
for item in self._get_resources("node")
|
||||
if item["status"] == "online"
|
||||
]
|
||||
if node and node not in nodes:
|
||||
self.module.fail_json(msg="Node %s was specified, but does not exist on the cluster" % node)
|
||||
elif node:
|
||||
return [node]
|
||||
return nodes
|
||||
|
||||
def check_storage_permissions(
|
||||
self,
|
||||
permissions,
|
||||
storage,
|
||||
bandwidth,
|
||||
performance,
|
||||
retention):
|
||||
# Check for Datastore.AllocateSpace in the permission tree
|
||||
if not has_permission(permissions, "Datastore.AllocateSpace", search_scopes=["/", "/storage/", "/storage/" + storage]):
|
||||
self.module.fail_json(changed=False, msg="Insufficient permission: Datastore.AllocateSpace is missing")
|
||||
|
||||
if (bandwidth or performance) and has_permission(permissions, "Sys.Modify", search_scopes=["/"], expected=0):
|
||||
self.module.fail_json(changed=False, msg="Insufficient permission: Performance_tweaks and bandwidth require 'Sys.Modify' permission for '/'")
|
||||
|
||||
if retention:
|
||||
if not has_permission(permissions, "Datastore.Allocate", search_scopes=["/", "/storage", "/storage/" + storage]):
|
||||
self.module.fail_json(changed=False, msg="Insufficient permissions: Custom retention was requested, but Datastore.Allocate is missing")
|
||||
|
||||
def check_vmid_backup_permission(self, permissions, vmids, pool):
|
||||
sufficient_permissions = has_permission(permissions, "VM.Backup", search_scopes=["/", "/vms"])
|
||||
if pool and not sufficient_permissions:
|
||||
sufficient_permissions = has_permission(permissions, "VM.Backup", search_scopes=["/pool/" + pool, "/pool/" + pool + "/vms"])
|
||||
|
||||
if not sufficient_permissions:
|
||||
# Since VM.Backup can be given for each vmid at a time, iterate through all of them
|
||||
# and check, if the permission is set
|
||||
failed_vmids = []
|
||||
for vm in vmids:
|
||||
vm_path = "/vms/" + str(vm)
|
||||
if has_permission(permissions, "VM.Backup", search_scopes=[vm_path], default=1, expected=0):
|
||||
failed_vmids.append(str(vm))
|
||||
if failed_vmids:
|
||||
self.module.fail_json(
|
||||
changed=False, msg="Insufficient permissions: "
|
||||
"You dont have the VM.Backup permission for VMID %s" %
|
||||
", ".join(failed_vmids))
|
||||
sufficient_permissions = True
|
||||
# Finally, when no check succeeded, fail
|
||||
if not sufficient_permissions:
|
||||
self.module.fail_json(changed=False, msg="Insufficient permissions: You do not have the VM.Backup permission")
|
||||
|
||||
def check_general_backup_permission(self, permissions, pool):
|
||||
if not has_permission(permissions, "VM.Backup", search_scopes=["/", "/vms"] + (["/pool/" + pool] if pool else [])):
|
||||
self.module.fail_json(changed=False, msg="Insufficient permissions: You dont have the VM.Backup permission")
|
||||
|
||||
def check_if_storage_exists(self, storage, node):
|
||||
storages = self.get_storages(type=None)
|
||||
# Loop through all cluster storages and get all matching storages
|
||||
validated_storagepath = [storageentry for storageentry in storages if storageentry["storage"] == storage]
|
||||
if not validated_storagepath:
|
||||
self.module.fail_json(
|
||||
changed=False,
|
||||
msg="Storage %s does not exist in the cluster" %
|
||||
storage)
|
||||
|
||||
def check_vmids(self, vmids):
|
||||
cluster_vmids = [vm["vmid"] for vm in self._get_resources("vm")]
|
||||
if not cluster_vmids:
|
||||
self.module.warn(
|
||||
"VM.Audit permission is missing or there are no VMs. This task might fail if one VMID does not exist")
|
||||
return
|
||||
vmids_not_found = [str(vm) for vm in vmids if vm not in cluster_vmids]
|
||||
if vmids_not_found:
|
||||
self.module.warn(
|
||||
"VMIDs %s not found. This task will fail if one VMID does not exist" %
|
||||
", ".join(vmids_not_found))
|
||||
|
||||
def wait_for_timeout(self, timeout, raw_tasks):
|
||||
|
||||
# filter all entries, which did not get a task id from the Cluster
|
||||
tasks = []
|
||||
ok_tasks = []
|
||||
for node in raw_tasks:
|
||||
if node["upid"] != "OK":
|
||||
tasks.append(node)
|
||||
else:
|
||||
ok_tasks.append(node)
|
||||
|
||||
start_time = time.time()
|
||||
# iterate through the task ids and check their values
|
||||
while True:
|
||||
for node in tasks:
|
||||
if node["status"] == "unknown":
|
||||
try:
|
||||
# proxmox.api_task_ok does not suffice, since it only
|
||||
# is true at `stopped` and `ok`
|
||||
status = self._get_taskok(node["node"], node["upid"])
|
||||
if status["status"] == "stopped" and status["exitstatus"] == "OK":
|
||||
node["status"] = "success"
|
||||
if status["status"] == "stopped" and status["exitstatus"] == "job errors":
|
||||
node["status"] = "failed"
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to retrieve API task ID from node %s: %s" % (node["node"], e))
|
||||
if len([item for item in tasks if item["status"] != "unknown"]) == len(tasks):
|
||||
break
|
||||
if time.time() > start_time + timeout:
|
||||
timeouted_nodes = [
|
||||
node["node"]
|
||||
for node in tasks
|
||||
if node["status"] == "unknown"
|
||||
]
|
||||
failed_nodes = [node["node"] for node in tasks if node["status"] == "failed"]
|
||||
if failed_nodes:
|
||||
self.module.fail_json(
|
||||
msg="Reached timeout while waiting for backup task. "
|
||||
"Nodes, who reached the timeout: %s. "
|
||||
"Nodes, which failed: %s" %
|
||||
(", ".join(timeouted_nodes), ", ".join(failed_nodes)))
|
||||
self.module.fail_json(
|
||||
msg="Reached timeout while waiting for creating VM snapshot. "
|
||||
"Nodes who reached the timeout: %s" %
|
||||
", ".join(timeouted_nodes))
|
||||
time.sleep(1)
|
||||
|
||||
error_logs = []
|
||||
for node in tasks:
|
||||
if node["status"] == "failed":
|
||||
tasklog = ", ".join([logentry["t"] for logentry in self._get_tasklog(node["node"], node["upid"])])
|
||||
error_logs.append("%s: %s" % (node, tasklog))
|
||||
if error_logs:
|
||||
self.module.fail_json(
|
||||
msg="An error occured creating the backups. "
|
||||
"These are the last log lines from the failed nodes: %s" %
|
||||
", ".join(error_logs))
|
||||
|
||||
for node in tasks:
|
||||
tasklog = ", ".join([logentry["t"] for logentry in self._get_tasklog(node["node"], node["upid"])])
|
||||
node["log"] = tasklog
|
||||
|
||||
# Finally, reattach ok tasks to show, that all nodes were contacted
|
||||
tasks.extend(ok_tasks)
|
||||
return tasks
|
||||
|
||||
def permission_check(
|
||||
self,
|
||||
storage,
|
||||
mode,
|
||||
node,
|
||||
bandwidth,
|
||||
performance_tweaks,
|
||||
retention,
|
||||
pool,
|
||||
vmids):
|
||||
permissions = self._get_permissions()
|
||||
self.check_if_storage_exists(storage, node)
|
||||
self.check_storage_permissions(
|
||||
permissions, storage, bandwidth, performance_tweaks, retention)
|
||||
if mode == "include":
|
||||
self.check_vmid_backup_permission(permissions, vmids, pool)
|
||||
else:
|
||||
self.check_general_backup_permission(permissions, pool)
|
||||
|
||||
def prepare_request_parameters(self, module_arguments):
|
||||
# ensure only valid post parameters are passed to proxmox
|
||||
# list of dict items to replace with (new_val, old_val)
|
||||
post_params = [("bwlimit", "bandwidth"),
|
||||
("compress", "compress"),
|
||||
("fleecing", "fleecing"),
|
||||
("mode", "backup_mode"),
|
||||
("notes-template", "description"),
|
||||
("notification-mode", "notification_mode"),
|
||||
("pbs-change-detection-mode", "change_detection_mode"),
|
||||
("performance", "performance_tweaks"),
|
||||
("pool", "pool"),
|
||||
("protected", "protected"),
|
||||
("prune-backups", "retention"),
|
||||
("storage", "storage"),
|
||||
("zstd", "compression_threads"),
|
||||
("vmid", "vmids")]
|
||||
request_body = {}
|
||||
for new, old in post_params:
|
||||
if module_arguments.get(old):
|
||||
request_body.update({new: module_arguments[old]})
|
||||
|
||||
# Set mode specific values
|
||||
if module_arguments["mode"] == "include":
|
||||
request_body.pop("pool", None)
|
||||
request_body["all"] = 0
|
||||
elif module_arguments["mode"] == "all":
|
||||
request_body.pop("vmid", None)
|
||||
request_body.pop("pool", None)
|
||||
request_body["all"] = 1
|
||||
elif module_arguments["mode"] == "pool":
|
||||
request_body.pop("vmid", None)
|
||||
request_body["all"] = 0
|
||||
|
||||
# Create comma separated list from vmids, the API expects so
|
||||
if request_body.get("vmid"):
|
||||
request_body.update({"vmid": ",".join(str(vmid) for vmid in request_body["vmid"])})
|
||||
|
||||
# remove whitespaces from option strings
|
||||
for key in ("prune-backups", "performance"):
|
||||
if request_body.get(key):
|
||||
request_body[key] = request_body[key].replace(" ", "")
|
||||
# convert booleans to 0/1
|
||||
for key in ("protected",):
|
||||
if request_body.get(key):
|
||||
request_body[key] = 1
|
||||
return request_body
|
||||
|
||||
def backup_create(
|
||||
self,
|
||||
module_arguments,
|
||||
check_mode,
|
||||
node_endpoints):
|
||||
request_body = self.prepare_request_parameters(module_arguments)
|
||||
# stop here, before anything gets changed
|
||||
if check_mode:
|
||||
return []
|
||||
|
||||
task_ids = self.request_backup(request_body, node_endpoints)
|
||||
updated_task_ids = []
|
||||
if module_arguments["wait"]:
|
||||
updated_task_ids = self.wait_for_timeout(
|
||||
module_arguments["wait_timeout"], task_ids)
|
||||
return updated_task_ids if updated_task_ids else task_ids
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
backup_args = {
|
||||
"backup_mode": {"type": "str", "default": "snapshot", "choices": ["snapshot", "suspend", "stop"]},
|
||||
"bandwidth": {"type": "int"},
|
||||
"change_detection_mode": {"type": "str", "choices": ["legacy", "data", "metadata"]},
|
||||
"compress": {"type": "str", "choices": ["0", "1", "gzip", "lzo", "zstd"]},
|
||||
"compression_threads": {"type": "int"},
|
||||
"description": {"type": "str", "default": "{{guestname}}"},
|
||||
"fleecing": {"type": "str"},
|
||||
"mode": {"type": "str", "required": True, "choices": ["include", "all", "pool"]},
|
||||
"node": {"type": "str"},
|
||||
"notification_mode": {"type": "str", "default": "auto", "choices": ["auto", "legacy-sendmail", "notification-system"]},
|
||||
"performance_tweaks": {"type": "str"},
|
||||
"pool": {"type": "str"},
|
||||
"protected": {"type": "bool"},
|
||||
"retention": {"type": "str"},
|
||||
"storage": {"type": "str", "required": True},
|
||||
"vmids": {"type": "list", "elements": "int"},
|
||||
"wait": {"type": "bool", "default": False},
|
||||
"wait_timeout": {"type": "int", "default": 10}}
|
||||
module_args.update(backup_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
supports_check_mode=True,
|
||||
required_if=[
|
||||
("mode", "include", ("vmids",), True),
|
||||
("mode", "pool", ("pool",))
|
||||
]
|
||||
)
|
||||
proxmox = ProxmoxBackupAnsible(module)
|
||||
bandwidth = module.params["bandwidth"]
|
||||
mode = module.params["mode"]
|
||||
node = module.params["node"]
|
||||
performance_tweaks = module.params["performance_tweaks"]
|
||||
pool = module.params["pool"]
|
||||
retention = module.params["retention"]
|
||||
storage = module.params["storage"]
|
||||
vmids = module.params["vmids"]
|
||||
|
||||
proxmox.permission_check(
|
||||
storage,
|
||||
mode,
|
||||
node,
|
||||
bandwidth,
|
||||
performance_tweaks,
|
||||
retention,
|
||||
pool,
|
||||
vmids)
|
||||
if module.params["mode"] == "include":
|
||||
proxmox.check_vmids(module.params["vmids"])
|
||||
node_endpoints = proxmox.check_relevant_nodes(module.params["node"])
|
||||
try:
|
||||
result = proxmox.backup_create(module.params, module.check_mode, node_endpoints)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Creating backups failed with exception: %s" % to_native(e))
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(backups=result, changed=True, msg="Backups would be created")
|
||||
|
||||
elif len([entry for entry in result if entry["upid"] == "OK"]) == len(result):
|
||||
module.exit_json(backups=result, changed=False, msg="Backup request sent to proxmox, no tasks created")
|
||||
|
||||
elif module.params["wait"]:
|
||||
module.exit_json(backups=result, changed=True, msg="Backups succeeded")
|
||||
|
||||
else:
|
||||
module.exit_json(backups=result, changed=True,
|
||||
msg="Backup tasks created")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,244 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2024 Marzieh Raoufnezhad <raoufnezhad at gmail.com>
|
||||
# Copyright (c) 2024 Maryam Mayabi <mayabi.ahm at gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
module: proxmox_backup_info
|
||||
|
||||
short_description: Retrieve information on Proxmox scheduled backups
|
||||
|
||||
version_added: 10.3.0
|
||||
|
||||
description:
|
||||
- Retrieve information such as backup times, VM name, VM ID, mode, backup type, and backup schedule using the Proxmox Server API.
|
||||
|
||||
author:
|
||||
- "Marzieh Raoufnezhad (@raoufnezhad) <raoufnezhad@gmail.com>"
|
||||
- "Maryam Mayabi (@mmayabi) <mayabi.ahm@gmail.com>"
|
||||
|
||||
options:
|
||||
vm_name:
|
||||
description:
|
||||
- The name of the Proxmox VM.
|
||||
- If defined, the returned list will contain backup jobs that have been parsed and filtered based on O(vm_name) value.
|
||||
- Mutually exclusive with O(vm_id) and O(backup_jobs).
|
||||
type: str
|
||||
vm_id:
|
||||
description:
|
||||
- The ID of the Proxmox VM.
|
||||
- If defined, the returned list will contain backup jobs that have been parsed and filtered based on O(vm_id) value.
|
||||
- Mutually exclusive with O(vm_name) and O(backup_jobs).
|
||||
type: str
|
||||
backup_jobs:
|
||||
description:
|
||||
- If V(true), the module will return all backup jobs information.
|
||||
- If V(false), the module will parse all backup jobs based on VM IDs and return a list of VMs' backup information.
|
||||
- Mutually exclusive with O(vm_id) and O(vm_name).
|
||||
default: false
|
||||
type: bool
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
- community.general.attributes.info_module
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Print all backup information by VM ID and VM name
|
||||
community.general.proxmox_backup_info:
|
||||
api_user: 'myUser@pam'
|
||||
api_password: '*******'
|
||||
api_host: '192.168.20.20'
|
||||
|
||||
- name: Print Proxmox backup information for a specific VM based on its name
|
||||
community.general.proxmox_backup_info:
|
||||
api_user: 'myUser@pam'
|
||||
api_password: '*******'
|
||||
api_host: '192.168.20.20'
|
||||
vm_name: 'mailsrv'
|
||||
|
||||
- name: Print Proxmox backup information for a specific VM based on its VM ID
|
||||
community.general.proxmox_backup_info:
|
||||
api_user: 'myUser@pam'
|
||||
api_password: '*******'
|
||||
api_host: '192.168.20.20'
|
||||
vm_id: '150'
|
||||
|
||||
- name: Print Proxmox all backup job information
|
||||
community.general.proxmox_backup_info:
|
||||
api_user: 'myUser@pam'
|
||||
api_password: '*******'
|
||||
api_host: '192.168.20.20'
|
||||
backup_jobs: true
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
---
|
||||
backup_info:
|
||||
description: The return value provides backup job information based on VM ID or VM name, or total backup job information.
|
||||
returned: on success, but can be empty
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
bktype:
|
||||
description: The type of the backup.
|
||||
returned: on success
|
||||
type: str
|
||||
sample: vzdump
|
||||
enabled:
|
||||
description: V(1) if backup is enabled else V(0).
|
||||
returned: on success
|
||||
type: int
|
||||
sample: 1
|
||||
id:
|
||||
description: The backup job ID.
|
||||
returned: on success
|
||||
type: str
|
||||
sample: backup-83831498-c631
|
||||
mode:
|
||||
description: The backup job mode such as snapshot.
|
||||
returned: on success
|
||||
type: str
|
||||
sample: snapshot
|
||||
next-run:
|
||||
description: The next backup time.
|
||||
returned: on success
|
||||
type: str
|
||||
sample: "2024-12-28 11:30:00"
|
||||
schedule:
|
||||
description: The backup job schedule.
|
||||
returned: on success
|
||||
type: str
|
||||
sample: "sat 15:00"
|
||||
storage:
|
||||
description: The backup storage location.
|
||||
returned: on success
|
||||
type: str
|
||||
sample: local
|
||||
vm_name:
|
||||
description: The VM name.
|
||||
returned: on success
|
||||
type: str
|
||||
sample: test01
|
||||
vmid:
|
||||
description: The VM ID.
|
||||
returned: on success
|
||||
type: str
|
||||
sample: "100"
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, HAS_PROXMOXER, PROXMOXER_IMP_ERR)
|
||||
|
||||
|
||||
class ProxmoxBackupInfoAnsible(ProxmoxAnsible):
|
||||
|
||||
# Get all backup information
|
||||
def get_jobs_list(self):
|
||||
try:
|
||||
backupJobs = self.proxmox_api.cluster.backup.get()
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Getting backup jobs failed: %s" % e)
|
||||
return backupJobs
|
||||
|
||||
# Get VM information
|
||||
def get_vms_list(self):
|
||||
try:
|
||||
vms = self.proxmox_api.cluster.resources.get(type='vm')
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Getting VMs info from cluster failed: %s" % e)
|
||||
return vms
|
||||
|
||||
# Get all backup information by VM ID and VM name
|
||||
def vms_backup_info(self):
|
||||
backupList = self.get_jobs_list()
|
||||
vmInfo = self.get_vms_list()
|
||||
bkInfo = []
|
||||
for backupItem in backupList:
|
||||
nextrun = datetime.fromtimestamp(backupItem['next-run'])
|
||||
vmids = backupItem['vmid'].split(',')
|
||||
for vmid in vmids:
|
||||
for vm in vmInfo:
|
||||
if vm['vmid'] == int(vmid):
|
||||
vmName = vm['name']
|
||||
break
|
||||
bkInfoData = {'id': backupItem['id'],
|
||||
'schedule': backupItem['schedule'],
|
||||
'storage': backupItem['storage'],
|
||||
'mode': backupItem['mode'],
|
||||
'next-run': nextrun.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'enabled': backupItem['enabled'],
|
||||
'bktype': backupItem['type'],
|
||||
'vmid': vmid,
|
||||
'vm_name': vmName}
|
||||
bkInfo.append(bkInfoData)
|
||||
return bkInfo
|
||||
|
||||
# Get proxmox backup information for a specific VM based on its VM ID or VM name
|
||||
def specific_vmbackup_info(self, vm_name_id):
|
||||
fullBackupInfo = self.vms_backup_info()
|
||||
vmBackupJobs = []
|
||||
for vm in fullBackupInfo:
|
||||
if (vm["vm_name"] == vm_name_id or vm["vmid"] == vm_name_id):
|
||||
vmBackupJobs.append(vm)
|
||||
return vmBackupJobs
|
||||
|
||||
|
||||
def main():
|
||||
# Define module args
|
||||
args = proxmox_auth_argument_spec()
|
||||
backup_info_args = dict(
|
||||
vm_id=dict(type='str'),
|
||||
vm_name=dict(type='str'),
|
||||
backup_jobs=dict(type='bool', default=False)
|
||||
)
|
||||
args.update(backup_info_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=args,
|
||||
mutually_exclusive=[('backup_jobs', 'vm_id', 'vm_name')],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
# Define (init) result value
|
||||
result = dict(
|
||||
changed=False
|
||||
)
|
||||
|
||||
# Check if proxmoxer exist
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
# Start to connect to proxmox to get backup data
|
||||
proxmox = ProxmoxBackupInfoAnsible(module)
|
||||
vm_id = module.params['vm_id']
|
||||
vm_name = module.params['vm_name']
|
||||
backup_jobs = module.params['backup_jobs']
|
||||
|
||||
# Update result value based on what requested (module args)
|
||||
if backup_jobs:
|
||||
result['backup_info'] = proxmox.get_jobs_list()
|
||||
elif vm_id:
|
||||
result['backup_info'] = proxmox.specific_vmbackup_info(vm_id)
|
||||
elif vm_name:
|
||||
result['backup_info'] = proxmox.specific_vmbackup_info(vm_name)
|
||||
else:
|
||||
result['backup_info'] = proxmox.vms_backup_info()
|
||||
|
||||
# Return result value
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,877 +0,0 @@
|
|||
#!/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>"
|
||||
attributes:
|
||||
check_mode:
|
||||
support: none
|
||||
diff_mode:
|
||||
support: none
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- The unique name of the VM.
|
||||
- You can specify either O(name) or O(vmid) or both of them.
|
||||
type: str
|
||||
vmid:
|
||||
description:
|
||||
- The unique ID of the VM.
|
||||
- You can specify either O(vmid) or O(name) or both of them.
|
||||
type: int
|
||||
disk:
|
||||
description:
|
||||
- The disk key (V(unused[n]), V(ide[n]), V(sata[n]), V(scsi[n]) or V(virtio[n])) you want to operate on.
|
||||
- Disk buses (IDE, SATA and so on) have fixed ranges of V(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.
|
||||
- O(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 O(create) parameter description to control behavior of this
|
||||
option.
|
||||
- Some updates on options (like O(cache)) are not being applied instantly and require VM restart.
|
||||
- Use O(state=detached) to detach existing disk from VM but do not remove it entirely. When O(state=detached) and disk
|
||||
is V(unused[n]) it will be left in same state (not removed).
|
||||
- O(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).
|
||||
- O(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 O(state=absent).
|
||||
type: str
|
||||
choices: ['present', 'resized', 'detached', 'moved', 'absent']
|
||||
default: present
|
||||
create:
|
||||
description:
|
||||
- With O(create) flag you can control behavior of O(state=present).
|
||||
- When O(create=disabled) it will not create new disk (if not exists) but will update options in existing disk.
|
||||
- When O(create=regular) it will either create new disk (if not exists) or update options in existing disk.
|
||||
- When O(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 O(state) is V(present).
|
||||
type: str
|
||||
size:
|
||||
description:
|
||||
- Desired volume size in GB to allocate when O(state=present) (specify O(size) without suffix).
|
||||
- New (or additional) size of volume when O(state=resized). With the V(+) 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 O(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 O(state=moved).
|
||||
type: bool
|
||||
target_disk:
|
||||
description:
|
||||
- The config key the disk will be moved to on the target VM (for example, V(ide0) or V(scsi1)).
|
||||
- Default is the source disk key.
|
||||
- Used only when O(state=moved).
|
||||
type: str
|
||||
target_storage:
|
||||
description:
|
||||
- Move the disk to this storage when O(state=moved).
|
||||
- You can move between storages only in scope of one VM.
|
||||
- Mutually exclusive with O(target_vmid).
|
||||
- Consider increasing O(timeout) in case of large disk images or slow storage backend.
|
||||
type: str
|
||||
target_vmid:
|
||||
description:
|
||||
- The (unique) ID of the VM where disk will be placed when O(state=moved).
|
||||
- You can move disk between VMs only when the same storage is used.
|
||||
- Mutually exclusive with O(target_vmid).
|
||||
type: int
|
||||
timeout:
|
||||
description:
|
||||
- Timeout in seconds to wait for slow operations such as importing disk or moving disk between storages.
|
||||
- Used only when O(state) is V(present) or V(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.
|
||||
- V(<STORAGE>:<VMID>/<FULL_NAME>) or V(<ABSOLUTE_PATH>/<FULL_NAME>).
|
||||
- Attention! Only root can use absolute paths.
|
||||
- This parameter is mutually exclusive with O(size).
|
||||
- Increase O(timeout) parameter when importing large disk images or using slow storage.
|
||||
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 O(iops_rd) and O(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 O(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 O(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 O(mbps_rd) and O(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 O(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 O(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']
|
||||
iso_image:
|
||||
description:
|
||||
- The ISO image to be mounted on the specified in O(disk) CD-ROM.
|
||||
- O(media=cdrom) needs to be specified for this option to work.
|
||||
- Use V(<STORAGE>:iso/<ISO_NAME>) to mount ISO.
|
||||
- Use V(cdrom) to access the physical CD/DVD drive.
|
||||
- Use V(none) to unmount image from existent CD-ROM or create empty CD-ROM drive.
|
||||
type: str
|
||||
version_added: 8.1.0
|
||||
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 V(0x).
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- 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
|
||||
|
||||
- name: Mount ISO image on CD-ROM (create drive if missing)
|
||||
community.general.proxmox_disk:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_token_id: token1
|
||||
api_token_secret: some-token-data
|
||||
vmid: 101
|
||||
disk: ide2
|
||||
media: cdrom
|
||||
iso_image: local:iso/favorite_distro_amd64.iso
|
||||
state: present
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
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.version import LooseVersion
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (proxmox_auth_argument_spec,
|
||||
ProxmoxAnsible)
|
||||
from re import compile, match, sub
|
||||
|
||||
|
||||
def disk_conf_str_to_dict(config_string):
|
||||
"""
|
||||
Transform Proxmox configuration string for disk element into dictionary which has
|
||||
volume option parsed in '{ storage }:{ volume }' format and other options parsed
|
||||
in '{ option }={ value }' format. This dictionary will be compared afterward with
|
||||
attributes that user passed to this module in playbook.\n
|
||||
config_string examples:
|
||||
- local-lvm:vm-100-disk-0,ssd=1,discard=on,size=25G
|
||||
- local:iso/new-vm-ignition.iso,media=cdrom,size=70k
|
||||
- none,media=cdrom
|
||||
:param config_string: Retrieved from Proxmox API configuration string
|
||||
:return: Dictionary with volume option divided into parts ('volume_name', 'storage_name', 'volume') \n
|
||||
and other options as key:value.
|
||||
"""
|
||||
config = config_string.split(',')
|
||||
|
||||
# When empty CD-ROM drive present, the volume part of config string is "none".
|
||||
storage_volume = config.pop(0)
|
||||
if storage_volume in ["none", "cdrom"]:
|
||||
config_current = dict(
|
||||
volume=storage_volume,
|
||||
storage_name=None,
|
||||
volume_name=None,
|
||||
size=None,
|
||||
)
|
||||
else:
|
||||
storage_volume = storage_volume.split(':')
|
||||
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,
|
||||
)
|
||||
|
||||
config.sort()
|
||||
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 = {
|
||||
k: int(v) if isinstance(v, bool) else v
|
||||
for k, v in self.module.params.items()
|
||||
if v is not None and k in self.create_update_fields
|
||||
}
|
||||
return params
|
||||
|
||||
def create_disk(self, disk, vmid, vm, vm_config):
|
||||
"""Create a disk in the specified virtual machine. Check if creation is required,
|
||||
and if so, compile the disk configuration and create it by updating the virtual
|
||||
machine configuration. After calling the API function, wait for the result.
|
||||
|
||||
:param disk: ID of the disk in format "<bus><number>".
|
||||
:param vmid: ID of the virtual machine where the disk will be created.
|
||||
:param vm: Name of the virtual machine where the disk will be created.
|
||||
:param vm_config: Configuration of the virtual machine.
|
||||
:return: (bool, string) Whether the task was successful or not
|
||||
and the message to return to Ansible.
|
||||
"""
|
||||
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)
|
||||
|
||||
timeout_str = "Reached timeout. Last line in task before timeout: %s"
|
||||
if (create == 'regular' and disk not in vm_config) or (create == 'forced'):
|
||||
# CREATE
|
||||
playbook_config = self.get_create_attributes()
|
||||
import_string = playbook_config.pop('import_from', None)
|
||||
iso_image = self.module.params.get('iso_image', None)
|
||||
|
||||
if import_string:
|
||||
# When 'import_from' option is present in task options.
|
||||
config_str = "%s:%s,import-from=%s" % (self.module.params["storage"], "0", import_string)
|
||||
timeout_str = "Reached timeout while importing VM disk. Last line in task before timeout: %s"
|
||||
ok_str = "Disk %s imported into VM %s"
|
||||
elif iso_image is not None:
|
||||
# disk=<busN>, media=cdrom, iso_image=<ISO_NAME>
|
||||
config_str = iso_image
|
||||
ok_str = "CD-ROM was created on %s bus in VM %s"
|
||||
else:
|
||||
config_str = self.module.params["storage"]
|
||||
if not config_str:
|
||||
self.module.fail_json(msg="The storage option must be specified.")
|
||||
if self.module.params.get("media") != "cdrom":
|
||||
config_str += ":%s" % (self.module.params["size"])
|
||||
ok_str = "Disk %s created in VM %s"
|
||||
timeout_str = "Reached timeout while creating VM disk. Last line in task before timeout: %s"
|
||||
|
||||
for k, v in playbook_config.items():
|
||||
config_str += ',%s=%s' % (k, v)
|
||||
|
||||
disk_config_to_apply = {self.module.params["disk"]: config_str}
|
||||
|
||||
if create in ['disabled', 'regular'] and disk in vm_config:
|
||||
# UPDATE
|
||||
ok_str = "Disk %s updated in VM %s"
|
||||
iso_image = self.module.params.get('iso_image', None)
|
||||
|
||||
proxmox_config = disk_conf_str_to_dict(vm_config[disk])
|
||||
# 'import_from' fails on disk updates
|
||||
playbook_config = self.get_create_attributes()
|
||||
playbook_config.pop('import_from', None)
|
||||
|
||||
# Begin composing configuration string
|
||||
if iso_image is not None:
|
||||
config_str = iso_image
|
||||
else:
|
||||
config_str = proxmox_config["volume"]
|
||||
# Append all mandatory fields from playbook_config
|
||||
for k, v in playbook_config.items():
|
||||
config_str += ',%s=%s' % (k, v)
|
||||
|
||||
# Append to playbook_config fields which are constants for disk images
|
||||
for option in ['size', 'storage_name', 'volume', 'volume_name']:
|
||||
playbook_config.update({option: proxmox_config[option]})
|
||||
# CD-ROM is special disk device and its disk image is subject to change
|
||||
if iso_image is not None:
|
||||
playbook_config['volume'] = iso_image
|
||||
# Values in params are numbers, but strings are needed to compare with disk_config
|
||||
playbook_config = {k: str(v) for k, v in playbook_config.items()}
|
||||
|
||||
# Now compare old and new config to detect if changes are needed
|
||||
if proxmox_config == playbook_config:
|
||||
return False, "Disk %s is up to date in VM %s" % (disk, vmid)
|
||||
|
||||
disk_config_to_apply = {self.module.params["disk"]: config_str}
|
||||
|
||||
current_task_id = self.proxmox_api.nodes(vm['node']).qemu(vmid).config.post(**disk_config_to_apply)
|
||||
task_success, fail_reason = self.api_task_complete(vm['node'], current_task_id, self.module.params['timeout'])
|
||||
|
||||
if task_success:
|
||||
return True, ok_str % (disk, vmid)
|
||||
else:
|
||||
if fail_reason == ProxmoxAnsible.TASK_TIMED_OUT:
|
||||
self.module.fail_json(
|
||||
msg=timeout_str % self.proxmox_api.nodes(vm['node']).tasks(current_task_id).log.get()[:1]
|
||||
)
|
||||
else:
|
||||
self.module.fail_json(msg="Error occurred on task execution: %s" % fail_reason)
|
||||
|
||||
def move_disk(self, disk, vmid, vm, vm_config):
|
||||
"""Call the `move_disk` API function that moves the disk to another storage and wait for the result.
|
||||
|
||||
:param disk: ID of disk in format "<bus><number>".
|
||||
:param vmid: ID of virtual machine which disk will be moved.
|
||||
:param vm: Name of virtual machine which disk will be moved.
|
||||
:param vm_config: Virtual machine configuration.
|
||||
:return: (bool, string) Whether the task was successful or not
|
||||
and the message to return to Ansible.
|
||||
"""
|
||||
disk_config = disk_conf_str_to_dict(vm_config[disk])
|
||||
disk_storage = disk_config["storage_name"]
|
||||
|
||||
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 = {k: v for k, v in params.items() if v is not None}
|
||||
|
||||
if params.get('storage', False):
|
||||
# Check if the disk is already in the target storage.
|
||||
disk_config = disk_conf_str_to_dict(vm_config[disk])
|
||||
if params['storage'] == disk_config['storage_name']:
|
||||
return False, "Disk %s already at %s storage" % (disk, disk_storage)
|
||||
|
||||
current_task_id = self.proxmox_api.nodes(vm['node']).qemu(vmid).move_disk.post(**params)
|
||||
task_success, fail_reason = self.api_task_complete(vm['node'], current_task_id, self.module.params['timeout'])
|
||||
|
||||
if task_success:
|
||||
return True, "Disk %s moved from VM %s storage %s" % (disk, vmid, disk_storage)
|
||||
else:
|
||||
if fail_reason == ProxmoxAnsible.TASK_TIMED_OUT:
|
||||
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(current_task_id).log.get()[:1]
|
||||
)
|
||||
else:
|
||||
self.module.fail_json(msg="Error occurred on task execution: %s" % fail_reason)
|
||||
|
||||
def resize_disk(self, disk, vmid, vm, vm_config):
|
||||
"""Call the `resize` API function to change the disk size and wait for the result.
|
||||
|
||||
:param disk: ID of disk in format "<bus><number>".
|
||||
:param vmid: ID of virtual machine which disk will be resized.
|
||||
:param vm: Name of virtual machine which disk will be resized.
|
||||
:param vm_config: Virtual machine configuration.
|
||||
:return: (Bool, string) Whether the task was successful or not
|
||||
and the message to return to Ansible.
|
||||
"""
|
||||
size = self.module.params['size']
|
||||
if not match(r'^\+?\d+(\.\d+)?[KMGT]?$', size):
|
||||
self.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:
|
||||
return False, "Disk %s is already %s size" % (disk, size)
|
||||
|
||||
# Resize disk API endpoint has changed at v8.0: PUT method become async.
|
||||
version = self.version()
|
||||
pve_major_version = 3 if version < LooseVersion('4.0') else version.version[0]
|
||||
if pve_major_version >= 8:
|
||||
current_task_id = self.proxmox_api.nodes(vm['node']).qemu(vmid).resize.set(disk=disk, size=size)
|
||||
task_success, fail_reason = self.api_task_complete(vm['node'], current_task_id, self.module.params['timeout'])
|
||||
if task_success:
|
||||
return True, "Disk %s resized in VM %s" % (disk, vmid)
|
||||
else:
|
||||
if fail_reason == ProxmoxAnsible.TASK_TIMED_OUT:
|
||||
self.module.fail_json(
|
||||
msg="Reached timeout while resizing disk. Last line in task before timeout: %s" %
|
||||
self.proxmox_api.nodes(vm['node']).tasks(current_task_id).log.get()[:1]
|
||||
)
|
||||
else:
|
||||
self.module.fail_json(msg="Error occurred on task execution: %s" % fail_reason)
|
||||
else:
|
||||
self.proxmox_api.nodes(vm['node']).qemu(vmid).resize.set(disk=disk, size=size)
|
||||
return True, "Disk %s resized in VM %s" % (disk, vmid)
|
||||
|
||||
|
||||
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'),
|
||||
iso_image=dict(type='str'),
|
||||
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',
|
||||
'iso_image': 'media',
|
||||
},
|
||||
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:
|
||||
changed, message = proxmox.create_disk(disk, vmid, vm, vm_config)
|
||||
module.exit_json(changed=changed, 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(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:
|
||||
changed, message = proxmox.move_disk(disk, vmid, vm, vm_config)
|
||||
module.exit_json(changed=changed, vmid=vmid, msg=message)
|
||||
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:
|
||||
changed, message = proxmox.resize_disk(disk, vmid, vm, vm_config)
|
||||
module.exit_json(changed=changed, vmid=vmid, msg=message)
|
||||
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(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()
|
|
@ -1,137 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright Tristan Le Guern (@tleguern) <tleguern at bouledef.eu>
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_domain_info
|
||||
short_description: Retrieve information about one or more Proxmox VE domains
|
||||
version_added: 1.3.0
|
||||
description:
|
||||
- Retrieve information about one or more Proxmox VE domains.
|
||||
attributes:
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
domain:
|
||||
description:
|
||||
- Restrict results to a specific authentication realm.
|
||||
aliases: ['realm', 'name']
|
||||
type: str
|
||||
author: Tristan Le Guern (@tleguern)
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
- community.general.attributes.info_module
|
||||
"""
|
||||
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: List existing domains
|
||||
community.general.proxmox_domain_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
register: proxmox_domains
|
||||
|
||||
- name: Retrieve information about the pve domain
|
||||
community.general.proxmox_domain_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
domain: pve
|
||||
register: proxmox_domain_pve
|
||||
"""
|
||||
|
||||
|
||||
RETURN = r"""
|
||||
proxmox_domains:
|
||||
description: List of authentication domains.
|
||||
returned: always, but can be empty
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
comment:
|
||||
description: Short description of the realm.
|
||||
returned: on success
|
||||
type: str
|
||||
realm:
|
||||
description: Realm name.
|
||||
returned: on success
|
||||
type: str
|
||||
type:
|
||||
description: Realm type.
|
||||
returned: on success
|
||||
type: str
|
||||
digest:
|
||||
description: Realm hash.
|
||||
returned: on success, can be absent
|
||||
type: str
|
||||
"""
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxDomainInfoAnsible(ProxmoxAnsible):
|
||||
def get_domain(self, realm):
|
||||
try:
|
||||
domain = self.proxmox_api.access.domains.get(realm)
|
||||
except Exception:
|
||||
self.module.fail_json(msg="Domain '%s' does not exist" % realm)
|
||||
domain['realm'] = realm
|
||||
return domain
|
||||
|
||||
def get_domains(self):
|
||||
domains = self.proxmox_api.access.domains.get()
|
||||
return domains
|
||||
|
||||
|
||||
def proxmox_domain_info_argument_spec():
|
||||
return dict(
|
||||
domain=dict(type='str', aliases=['realm', 'name']),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
domain_info_args = proxmox_domain_info_argument_spec()
|
||||
module_args.update(domain_info_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_one_of=[('api_password', 'api_token_id')],
|
||||
required_together=[('api_token_id', 'api_token_secret')],
|
||||
supports_check_mode=True
|
||||
)
|
||||
result = dict(
|
||||
changed=False
|
||||
)
|
||||
|
||||
proxmox = ProxmoxDomainInfoAnsible(module)
|
||||
domain = module.params['domain']
|
||||
|
||||
if domain:
|
||||
domains = [proxmox.get_domain(realm=domain)]
|
||||
else:
|
||||
domains = proxmox.get_domains()
|
||||
result['proxmox_domains'] = domains
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,147 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright Tristan Le Guern <tleguern at bouledef.eu>
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_group_info
|
||||
short_description: Retrieve information about one or more Proxmox VE groups
|
||||
version_added: 1.3.0
|
||||
description:
|
||||
- Retrieve information about one or more Proxmox VE groups.
|
||||
attributes:
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
group:
|
||||
description:
|
||||
- Restrict results to a specific group.
|
||||
aliases: ['groupid', 'name']
|
||||
type: str
|
||||
author: Tristan Le Guern (@tleguern)
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
- community.general.attributes.info_module
|
||||
"""
|
||||
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: List existing groups
|
||||
community.general.proxmox_group_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
register: proxmox_groups
|
||||
|
||||
- name: Retrieve information about the admin group
|
||||
community.general.proxmox_group_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
group: admin
|
||||
register: proxmox_group_admin
|
||||
"""
|
||||
|
||||
|
||||
RETURN = r"""
|
||||
proxmox_groups:
|
||||
description: List of groups.
|
||||
returned: always, but can be empty
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
comment:
|
||||
description: Short description of the group.
|
||||
returned: on success, can be absent
|
||||
type: str
|
||||
groupid:
|
||||
description: Group name.
|
||||
returned: on success
|
||||
type: str
|
||||
users:
|
||||
description: List of users in the group.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxGroupInfoAnsible(ProxmoxAnsible):
|
||||
def get_group(self, groupid):
|
||||
try:
|
||||
group = self.proxmox_api.access.groups.get(groupid)
|
||||
except Exception:
|
||||
self.module.fail_json(msg="Group '%s' does not exist" % groupid)
|
||||
group['groupid'] = groupid
|
||||
return ProxmoxGroup(group)
|
||||
|
||||
def get_groups(self):
|
||||
groups = self.proxmox_api.access.groups.get()
|
||||
return [ProxmoxGroup(group) for group in groups]
|
||||
|
||||
|
||||
class ProxmoxGroup:
|
||||
def __init__(self, group):
|
||||
self.group = dict()
|
||||
# Data representation is not the same depending on API calls
|
||||
for k, v in group.items():
|
||||
if k == 'users' and isinstance(v, str):
|
||||
self.group['users'] = v.split(',')
|
||||
elif k == 'members':
|
||||
self.group['users'] = group['members']
|
||||
else:
|
||||
self.group[k] = v
|
||||
|
||||
|
||||
def proxmox_group_info_argument_spec():
|
||||
return dict(
|
||||
group=dict(type='str', aliases=['groupid', 'name']),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
group_info_args = proxmox_group_info_argument_spec()
|
||||
module_args.update(group_info_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_one_of=[('api_password', 'api_token_id')],
|
||||
required_together=[('api_token_id', 'api_token_secret')],
|
||||
supports_check_mode=True
|
||||
)
|
||||
result = dict(
|
||||
changed=False
|
||||
)
|
||||
|
||||
proxmox = ProxmoxGroupInfoAnsible(module)
|
||||
group = module.params['group']
|
||||
|
||||
if group:
|
||||
groups = [proxmox.get_group(groupid=group)]
|
||||
else:
|
||||
groups = proxmox.get_groups()
|
||||
result['proxmox_groups'] = [group.group for group in groups]
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
File diff suppressed because it is too large
Load diff
|
@ -1,313 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2021, Lammert Hellinga (@Kogelvis) <lammert@hellinga.it>
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_nic
|
||||
short_description: Management of a NIC of a Qemu(KVM) VM in a Proxmox VE cluster
|
||||
version_added: 3.1.0
|
||||
description:
|
||||
- Allows you to create/update/delete a NIC on Qemu(KVM) Virtual Machines in a Proxmox VE cluster.
|
||||
author: "Lammert Hellinga (@Kogelvis) <lammert@hellinga.it>"
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
bridge:
|
||||
description:
|
||||
- Add this interface to the specified bridge device. The Proxmox VE default bridge is called V(vmbr0).
|
||||
type: str
|
||||
firewall:
|
||||
description:
|
||||
- Whether this interface should be protected by the firewall.
|
||||
type: bool
|
||||
default: false
|
||||
interface:
|
||||
description:
|
||||
- Name of the interface, should be V(net[n]) where C(1 ≤ n ≤ 31).
|
||||
type: str
|
||||
required: true
|
||||
link_down:
|
||||
description:
|
||||
- Whether this interface should be disconnected (like pulling the plug).
|
||||
type: bool
|
||||
default: false
|
||||
mac:
|
||||
description:
|
||||
- V(XX:XX:XX:XX:XX:XX) should be a unique MAC address. This is automatically generated if not specified.
|
||||
- When not specified this module will keep the MAC address the same when changing an existing interface.
|
||||
type: str
|
||||
model:
|
||||
description:
|
||||
- The NIC emulator model.
|
||||
type: str
|
||||
choices: ['e1000', 'e1000-82540em', 'e1000-82544gc', 'e1000-82545em', 'i82551', 'i82557b', 'i82559er', 'ne2k_isa', 'ne2k_pci',
|
||||
'pcnet', 'rtl8139', 'virtio', 'vmxnet3']
|
||||
default: virtio
|
||||
mtu:
|
||||
description:
|
||||
- Force MTU, for C(virtio) model only, setting will be ignored otherwise.
|
||||
- Set to V(1) to use the bridge MTU.
|
||||
- Value should be C(1 ≤ n ≤ 65520).
|
||||
type: int
|
||||
name:
|
||||
description:
|
||||
- Specifies the VM name. Only used on the configuration web interface.
|
||||
- Required only for O(state=present).
|
||||
type: str
|
||||
queues:
|
||||
description:
|
||||
- Number of packet queues to be used on the device.
|
||||
- Value should be C(0 ≤ n ≤ 16).
|
||||
type: int
|
||||
rate:
|
||||
description:
|
||||
- Rate limit in MBps (MegaBytes per second) as floating point number.
|
||||
type: float
|
||||
state:
|
||||
description:
|
||||
- Indicates desired state of the NIC.
|
||||
type: str
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
tag:
|
||||
description:
|
||||
- VLAN tag to apply to packets on this interface.
|
||||
- Value should be C(1 ≤ n ≤ 4094).
|
||||
type: int
|
||||
trunks:
|
||||
description:
|
||||
- List of VLAN trunks to pass through this interface.
|
||||
type: list
|
||||
elements: int
|
||||
vmid:
|
||||
description:
|
||||
- Specifies the instance ID.
|
||||
type: int
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create NIC net0 targeting the vm by name
|
||||
community.general.proxmox_nic:
|
||||
api_user: root@pam
|
||||
api_password: secret
|
||||
api_host: proxmoxhost
|
||||
name: my_vm
|
||||
interface: net0
|
||||
bridge: vmbr0
|
||||
tag: 3
|
||||
|
||||
- name: Create NIC net0 targeting the vm by id
|
||||
community.general.proxmox_nic:
|
||||
api_user: root@pam
|
||||
api_password: secret
|
||||
api_host: proxmoxhost
|
||||
vmid: 103
|
||||
interface: net0
|
||||
bridge: vmbr0
|
||||
mac: "12:34:56:C0:FF:EE"
|
||||
firewall: true
|
||||
|
||||
- name: Delete NIC net0 targeting the vm by name
|
||||
community.general.proxmox_nic:
|
||||
api_user: root@pam
|
||||
api_password: secret
|
||||
api_host: proxmoxhost
|
||||
name: my_vm
|
||||
interface: net0
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
vmid:
|
||||
description: The VM vmid.
|
||||
returned: success
|
||||
type: int
|
||||
sample: 115
|
||||
msg:
|
||||
description: A short message.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Nic net0 unchanged on VM with vmid 103"
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxNicAnsible(ProxmoxAnsible):
|
||||
def update_nic(self, vmid, interface, model, **kwargs):
|
||||
vm = self.get_vm(vmid)
|
||||
|
||||
try:
|
||||
vminfo = self.proxmox_api.nodes(vm['node']).qemu(vmid).config.get()
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Getting information for VM with vmid = %s failed with exception: %s' % (vmid, e))
|
||||
|
||||
if interface in vminfo:
|
||||
# Convert the current config to a dictionary
|
||||
config = vminfo[interface].split(',')
|
||||
config.sort()
|
||||
|
||||
config_current = {}
|
||||
|
||||
for i in config:
|
||||
kv = i.split('=')
|
||||
try:
|
||||
config_current[kv[0]] = kv[1]
|
||||
except IndexError:
|
||||
config_current[kv[0]] = ''
|
||||
|
||||
# determine the current model nic and mac-address
|
||||
models = ['e1000', 'e1000-82540em', 'e1000-82544gc', 'e1000-82545em', 'i82551', 'i82557b',
|
||||
'i82559er', 'ne2k_isa', 'ne2k_pci', 'pcnet', 'rtl8139', 'virtio', 'vmxnet3']
|
||||
current_model = set(models) & set(config_current.keys())
|
||||
current_model = current_model.pop()
|
||||
current_mac = config_current[current_model]
|
||||
|
||||
# build nic config string
|
||||
config_provided = "{0}={1}".format(model, current_mac)
|
||||
else:
|
||||
config_provided = model
|
||||
|
||||
if kwargs['mac']:
|
||||
config_provided = "{0}={1}".format(model, kwargs['mac'])
|
||||
|
||||
if kwargs['bridge']:
|
||||
config_provided += ",bridge={0}".format(kwargs['bridge'])
|
||||
|
||||
if kwargs['firewall']:
|
||||
config_provided += ",firewall=1"
|
||||
|
||||
if kwargs['link_down']:
|
||||
config_provided += ',link_down=1'
|
||||
|
||||
if kwargs['mtu']:
|
||||
config_provided += ",mtu={0}".format(kwargs['mtu'])
|
||||
if model != 'virtio':
|
||||
self.module.warn(
|
||||
'Ignoring MTU for nic {0} on VM with vmid {1}, '
|
||||
'model should be set to \'virtio\': '.format(interface, vmid))
|
||||
|
||||
if kwargs['queues']:
|
||||
config_provided += ",queues={0}".format(kwargs['queues'])
|
||||
|
||||
if kwargs['rate']:
|
||||
config_provided += ",rate={0}".format(kwargs['rate'])
|
||||
|
||||
if kwargs['tag']:
|
||||
config_provided += ",tag={0}".format(kwargs['tag'])
|
||||
|
||||
if kwargs['trunks']:
|
||||
config_provided += ",trunks={0}".format(';'.join(str(x) for x in kwargs['trunks']))
|
||||
|
||||
net = {interface: config_provided}
|
||||
vm = self.get_vm(vmid)
|
||||
|
||||
if ((interface not in vminfo) or (vminfo[interface] != config_provided)):
|
||||
if not self.module.check_mode:
|
||||
self.proxmox_api.nodes(vm['node']).qemu(vmid).config.set(**net)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def delete_nic(self, vmid, interface):
|
||||
vm = self.get_vm(vmid)
|
||||
vminfo = self.proxmox_api.nodes(vm['node']).qemu(vmid).config.get()
|
||||
|
||||
if interface in vminfo:
|
||||
if not self.module.check_mode:
|
||||
self.proxmox_api.nodes(vm['node']).qemu(vmid).config.set(delete=interface)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
nic_args = dict(
|
||||
bridge=dict(type='str'),
|
||||
firewall=dict(type='bool', default=False),
|
||||
interface=dict(type='str', required=True),
|
||||
link_down=dict(type='bool', default=False),
|
||||
mac=dict(type='str'),
|
||||
model=dict(choices=['e1000', 'e1000-82540em', 'e1000-82544gc', 'e1000-82545em',
|
||||
'i82551', 'i82557b', 'i82559er', 'ne2k_isa', 'ne2k_pci', 'pcnet',
|
||||
'rtl8139', 'virtio', 'vmxnet3'], default='virtio'),
|
||||
mtu=dict(type='int'),
|
||||
name=dict(type='str'),
|
||||
queues=dict(type='int'),
|
||||
rate=dict(type='float'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
tag=dict(type='int'),
|
||||
trunks=dict(type='list', elements='int'),
|
||||
vmid=dict(type='int'),
|
||||
)
|
||||
module_args.update(nic_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')],
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
proxmox = ProxmoxNicAnsible(module)
|
||||
|
||||
interface = module.params['interface']
|
||||
model = module.params['model']
|
||||
name = module.params['name']
|
||||
state = module.params['state']
|
||||
vmid = module.params['vmid']
|
||||
|
||||
# If vmid is not defined then retrieve its value from the vm name,
|
||||
if not vmid:
|
||||
vmid = proxmox.get_vmid(name)
|
||||
|
||||
# Ensure VM id exists
|
||||
proxmox.get_vm(vmid)
|
||||
|
||||
if state == 'present':
|
||||
try:
|
||||
if proxmox.update_nic(vmid, interface, model,
|
||||
bridge=module.params['bridge'],
|
||||
firewall=module.params['firewall'],
|
||||
link_down=module.params['link_down'],
|
||||
mac=module.params['mac'],
|
||||
mtu=module.params['mtu'],
|
||||
queues=module.params['queues'],
|
||||
rate=module.params['rate'],
|
||||
tag=module.params['tag'],
|
||||
trunks=module.params['trunks']):
|
||||
module.exit_json(changed=True, vmid=vmid, msg="Nic {0} updated on VM with vmid {1}".format(interface, vmid))
|
||||
else:
|
||||
module.exit_json(vmid=vmid, msg="Nic {0} unchanged on VM with vmid {1}".format(interface, vmid))
|
||||
except Exception as e:
|
||||
module.fail_json(vmid=vmid, msg='Unable to change nic {0} on VM with vmid {1}: '.format(interface, vmid) + str(e))
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
if proxmox.delete_nic(vmid, interface):
|
||||
module.exit_json(changed=True, vmid=vmid, msg="Nic {0} deleted on VM with vmid {1}".format(interface, vmid))
|
||||
else:
|
||||
module.exit_json(vmid=vmid, msg="Nic {0} does not exist on VM with vmid {1}".format(interface, vmid))
|
||||
except Exception as e:
|
||||
module.fail_json(vmid=vmid, msg='Unable to delete nic {0} on VM with vmid {1}: '.format(interface, vmid) + str(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,143 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright John Berninger (@jberning) <john.berninger at gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_node_info
|
||||
short_description: Retrieve information about one or more Proxmox VE nodes
|
||||
version_added: 8.2.0
|
||||
description:
|
||||
- Retrieve information about one or more Proxmox VE nodes.
|
||||
author: John Berninger (@jwbernin)
|
||||
attributes:
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
- community.general.attributes.info_module
|
||||
"""
|
||||
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: List existing nodes
|
||||
community.general.proxmox_node_info:
|
||||
api_host: proxmox1
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
register: proxmox_nodes
|
||||
"""
|
||||
|
||||
|
||||
RETURN = r"""
|
||||
proxmox_nodes:
|
||||
description: List of Proxmox VE nodes.
|
||||
returned: always, but can be empty
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
cpu:
|
||||
description: Current CPU usage in fractional shares of this host's total available CPU.
|
||||
returned: on success
|
||||
type: float
|
||||
disk:
|
||||
description: Current local disk usage of this host.
|
||||
returned: on success
|
||||
type: int
|
||||
id:
|
||||
description: Identity of the node.
|
||||
returned: on success
|
||||
type: str
|
||||
level:
|
||||
description: Support level. Can be blank if not under a paid support contract.
|
||||
returned: on success
|
||||
type: str
|
||||
maxcpu:
|
||||
description: Total number of available CPUs on this host.
|
||||
returned: on success
|
||||
type: int
|
||||
maxdisk:
|
||||
description: Size of local disk in bytes.
|
||||
returned: on success
|
||||
type: int
|
||||
maxmem:
|
||||
description: Memory size in bytes.
|
||||
returned: on success
|
||||
type: int
|
||||
mem:
|
||||
description: Used memory in bytes.
|
||||
returned: on success
|
||||
type: int
|
||||
node:
|
||||
description: Short hostname of this node.
|
||||
returned: on success
|
||||
type: str
|
||||
ssl_fingerprint:
|
||||
description: SSL fingerprint of the node certificate.
|
||||
returned: on success
|
||||
type: str
|
||||
status:
|
||||
description: Node status.
|
||||
returned: on success
|
||||
type: str
|
||||
type:
|
||||
description: Object type being returned.
|
||||
returned: on success
|
||||
type: str
|
||||
uptime:
|
||||
description: Node uptime in seconds.
|
||||
returned: on success
|
||||
type: int
|
||||
"""
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxNodeInfoAnsible(ProxmoxAnsible):
|
||||
def get_nodes(self):
|
||||
nodes = self.proxmox_api.nodes.get()
|
||||
return nodes
|
||||
|
||||
|
||||
def proxmox_node_info_argument_spec():
|
||||
return dict()
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
node_info_args = proxmox_node_info_argument_spec()
|
||||
module_args.update(node_info_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_one_of=[('api_password', 'api_token_id')],
|
||||
required_together=[('api_token_id', 'api_token_secret')],
|
||||
supports_check_mode=True,
|
||||
)
|
||||
result = dict(
|
||||
changed=False
|
||||
)
|
||||
|
||||
proxmox = ProxmoxNodeInfoAnsible(module)
|
||||
|
||||
nodes = proxmox.get_nodes()
|
||||
result['proxmox_nodes'] = nodes
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,182 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2023, Sergei Antipov (UnderGreen) <greendayonfire@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_pool
|
||||
short_description: Pool management for Proxmox VE cluster
|
||||
description:
|
||||
- Create or delete a pool for Proxmox VE clusters.
|
||||
- For pool members management please consult M(community.general.proxmox_pool_member) module.
|
||||
version_added: 7.1.0
|
||||
author: "Sergei Antipov (@UnderGreen) <greendayonfire@gmail.com>"
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
poolid:
|
||||
description:
|
||||
- The pool ID.
|
||||
type: str
|
||||
aliases: ["name"]
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the pool.
|
||||
- The pool must be empty prior deleting it with O(state=absent).
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type: str
|
||||
comment:
|
||||
description:
|
||||
- Specify the description for the pool.
|
||||
- Parameter is ignored when pool already exists or O(state=absent).
|
||||
type: str
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create new Proxmox VE pool
|
||||
community.general.proxmox_pool:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_password: password
|
||||
poolid: test
|
||||
comment: 'New pool'
|
||||
|
||||
- name: Delete the Proxmox VE pool
|
||||
community.general.proxmox_pool:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_password: password
|
||||
poolid: test
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
poolid:
|
||||
description: The pool ID.
|
||||
returned: success
|
||||
type: str
|
||||
sample: test
|
||||
msg:
|
||||
description: A short message on what the module did.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Pool test successfully created"
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxPoolAnsible(ProxmoxAnsible):
|
||||
|
||||
def is_pool_existing(self, poolid):
|
||||
"""Check whether pool already exist
|
||||
|
||||
:param poolid: str - name of the pool
|
||||
:return: bool - is pool exists?
|
||||
"""
|
||||
try:
|
||||
pools = self.proxmox_api.pools.get()
|
||||
for pool in pools:
|
||||
if pool['poolid'] == poolid:
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to retrieve pools: {0}".format(e))
|
||||
|
||||
def is_pool_empty(self, poolid):
|
||||
"""Check whether pool has members
|
||||
|
||||
:param poolid: str - name of the pool
|
||||
:return: bool - is pool empty?
|
||||
"""
|
||||
return True if not self.get_pool(poolid)['members'] else False
|
||||
|
||||
def create_pool(self, poolid, comment=None):
|
||||
"""Create Proxmox VE pool
|
||||
|
||||
:param poolid: str - name of the pool
|
||||
:param comment: str, optional - Description of a pool
|
||||
:return: None
|
||||
"""
|
||||
if self.is_pool_existing(poolid):
|
||||
self.module.exit_json(changed=False, poolid=poolid, msg="Pool {0} already exists".format(poolid))
|
||||
|
||||
if self.module.check_mode:
|
||||
return
|
||||
|
||||
try:
|
||||
self.proxmox_api.pools.post(poolid=poolid, comment=comment)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Failed to create pool with ID {0}: {1}".format(poolid, e))
|
||||
|
||||
def delete_pool(self, poolid):
|
||||
"""Delete Proxmox VE pool
|
||||
|
||||
:param poolid: str - name of the pool
|
||||
:return: None
|
||||
"""
|
||||
if not self.is_pool_existing(poolid):
|
||||
self.module.exit_json(changed=False, poolid=poolid, msg="Pool {0} doesn't exist".format(poolid))
|
||||
|
||||
if self.is_pool_empty(poolid):
|
||||
if self.module.check_mode:
|
||||
return
|
||||
|
||||
try:
|
||||
self.proxmox_api.pools(poolid).delete()
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Failed to delete pool with ID {0}: {1}".format(poolid, e))
|
||||
else:
|
||||
self.module.fail_json(msg="Can't delete pool {0} with members. Please remove members from pool first.".format(poolid))
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
pools_args = dict(
|
||||
poolid=dict(type="str", aliases=["name"], required=True),
|
||||
comment=dict(type="str"),
|
||||
state=dict(default="present", choices=["present", "absent"]),
|
||||
)
|
||||
|
||||
module_args.update(pools_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_together=[("api_token_id", "api_token_secret")],
|
||||
required_one_of=[("api_password", "api_token_id")],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
poolid = module.params["poolid"]
|
||||
comment = module.params["comment"]
|
||||
state = module.params["state"]
|
||||
|
||||
proxmox = ProxmoxPoolAnsible(module)
|
||||
|
||||
if state == "present":
|
||||
proxmox.create_pool(poolid, comment)
|
||||
module.exit_json(changed=True, poolid=poolid, msg="Pool {0} successfully created".format(poolid))
|
||||
else:
|
||||
proxmox.delete_pool(poolid)
|
||||
module.exit_json(changed=True, poolid=poolid, msg="Pool {0} successfully deleted".format(poolid))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,240 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2023, Sergei Antipov (UnderGreen) <greendayonfire@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_pool_member
|
||||
short_description: Add or delete members from Proxmox VE cluster pools
|
||||
description:
|
||||
- Create or delete a pool member in Proxmox VE clusters.
|
||||
version_added: 7.1.0
|
||||
author: "Sergei Antipov (@UnderGreen) <greendayonfire@gmail.com>"
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
poolid:
|
||||
description:
|
||||
- The pool ID.
|
||||
type: str
|
||||
aliases: ["name"]
|
||||
required: true
|
||||
member:
|
||||
description:
|
||||
- Specify the member name.
|
||||
- For O(type=storage) it is a storage name.
|
||||
- For O(type=vm) either vmid or vm name could be used.
|
||||
type: str
|
||||
required: true
|
||||
type:
|
||||
description:
|
||||
- Member type to add/remove from the pool.
|
||||
choices: ["vm", "storage"]
|
||||
default: vm
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the pool member.
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type: str
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Add new VM to Proxmox VE pool
|
||||
community.general.proxmox_pool_member:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_password: password
|
||||
poolid: test
|
||||
member: 101
|
||||
|
||||
- name: Add new storage to Proxmox VE pool
|
||||
community.general.proxmox_pool_member:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_password: password
|
||||
poolid: test
|
||||
member: zfs-data
|
||||
type: storage
|
||||
|
||||
- name: Remove VM from the Proxmox VE pool using VM name
|
||||
community.general.proxmox_pool_member:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_password: password
|
||||
poolid: test
|
||||
member: pxe.home.arpa
|
||||
state: absent
|
||||
|
||||
- name: Remove storage from the Proxmox VE pool
|
||||
community.general.proxmox_pool_member:
|
||||
api_host: node1
|
||||
api_user: root@pam
|
||||
api_password: password
|
||||
poolid: test
|
||||
member: zfs-storage
|
||||
type: storage
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
poolid:
|
||||
description: The pool ID.
|
||||
returned: success
|
||||
type: str
|
||||
sample: test
|
||||
member:
|
||||
description: Member name.
|
||||
returned: success
|
||||
type: str
|
||||
sample: 101
|
||||
msg:
|
||||
description: A short message on what the module did.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Member 101 deleted from the pool test"
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxPoolMemberAnsible(ProxmoxAnsible):
|
||||
|
||||
def pool_members(self, poolid):
|
||||
vms = []
|
||||
storage = []
|
||||
for member in self.get_pool(poolid)["members"]:
|
||||
if member["type"] == "storage":
|
||||
storage.append(member["storage"])
|
||||
else:
|
||||
vms.append(member["vmid"])
|
||||
|
||||
return (vms, storage)
|
||||
|
||||
def add_pool_member(self, poolid, member, member_type):
|
||||
current_vms_members, current_storage_members = self.pool_members(poolid)
|
||||
all_members_before = current_storage_members + current_vms_members
|
||||
all_members_after = all_members_before.copy()
|
||||
diff = {"before": {"members": all_members_before}, "after": {"members": all_members_after}}
|
||||
|
||||
try:
|
||||
if member_type == "storage":
|
||||
storages = self.get_storages(type=None)
|
||||
if member not in [storage["storage"] for storage in storages]:
|
||||
self.module.fail_json(msg="Storage {0} doesn't exist in the cluster".format(member))
|
||||
if member in current_storage_members:
|
||||
self.module.exit_json(changed=False, poolid=poolid, member=member,
|
||||
diff=diff, msg="Member {0} is already part of the pool {1}".format(member, poolid))
|
||||
|
||||
all_members_after.append(member)
|
||||
if self.module.check_mode:
|
||||
return diff
|
||||
|
||||
self.proxmox_api.pools(poolid).put(storage=[member])
|
||||
return diff
|
||||
else:
|
||||
try:
|
||||
vmid = int(member)
|
||||
except ValueError:
|
||||
vmid = self.get_vmid(member)
|
||||
|
||||
if vmid in current_vms_members:
|
||||
self.module.exit_json(changed=False, poolid=poolid, member=member,
|
||||
diff=diff, msg="VM {0} is already part of the pool {1}".format(member, poolid))
|
||||
|
||||
all_members_after.append(member)
|
||||
|
||||
if not self.module.check_mode:
|
||||
self.proxmox_api.pools(poolid).put(vms=[vmid])
|
||||
return diff
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Failed to add a new member ({0}) to the pool {1}: {2}".format(member, poolid, e))
|
||||
|
||||
def delete_pool_member(self, poolid, member, member_type):
|
||||
current_vms_members, current_storage_members = self.pool_members(poolid)
|
||||
all_members_before = current_storage_members + current_vms_members
|
||||
all_members_after = all_members_before.copy()
|
||||
diff = {"before": {"members": all_members_before}, "after": {"members": all_members_after}}
|
||||
|
||||
try:
|
||||
if member_type == "storage":
|
||||
if member not in current_storage_members:
|
||||
self.module.exit_json(changed=False, poolid=poolid, member=member,
|
||||
diff=diff, msg="Member {0} is not part of the pool {1}".format(member, poolid))
|
||||
|
||||
all_members_after.remove(member)
|
||||
if self.module.check_mode:
|
||||
return diff
|
||||
|
||||
self.proxmox_api.pools(poolid).put(storage=[member], delete=1)
|
||||
return diff
|
||||
else:
|
||||
try:
|
||||
vmid = int(member)
|
||||
except ValueError:
|
||||
vmid = self.get_vmid(member)
|
||||
|
||||
if vmid not in current_vms_members:
|
||||
self.module.exit_json(changed=False, poolid=poolid, member=member,
|
||||
diff=diff, msg="VM {0} is not part of the pool {1}".format(member, poolid))
|
||||
|
||||
all_members_after.remove(vmid)
|
||||
|
||||
if not self.module.check_mode:
|
||||
self.proxmox_api.pools(poolid).put(vms=[vmid], delete=1)
|
||||
return diff
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Failed to delete a member ({0}) from the pool {1}: {2}".format(member, poolid, e))
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
pool_members_args = dict(
|
||||
poolid=dict(type="str", aliases=["name"], required=True),
|
||||
member=dict(type="str", required=True),
|
||||
type=dict(default="vm", choices=["vm", "storage"]),
|
||||
state=dict(default="present", choices=["present", "absent"]),
|
||||
)
|
||||
|
||||
module_args.update(pool_members_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_together=[("api_token_id", "api_token_secret")],
|
||||
required_one_of=[("api_password", "api_token_id")],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
poolid = module.params["poolid"]
|
||||
member = module.params["member"]
|
||||
member_type = module.params["type"]
|
||||
state = module.params["state"]
|
||||
|
||||
proxmox = ProxmoxPoolMemberAnsible(module)
|
||||
|
||||
if state == "present":
|
||||
diff = proxmox.add_pool_member(poolid, member, member_type)
|
||||
module.exit_json(changed=True, poolid=poolid, member=member, diff=diff, msg="New member {0} added to the pool {1}".format(member, poolid))
|
||||
else:
|
||||
diff = proxmox.delete_pool_member(poolid, member, member_type)
|
||||
module.exit_json(changed=True, poolid=poolid, member=member, diff=diff, msg="Member {0} deleted from the pool {1}".format(member, poolid))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,395 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2020, Jeffrey van Pelt (@Thulium-Drake) <jeff@vanpelt.one>
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_snap
|
||||
short_description: Snapshot management of instances in Proxmox VE cluster
|
||||
version_added: 2.0.0
|
||||
description:
|
||||
- Allows you to create/delete/restore snapshots from instances in Proxmox VE cluster.
|
||||
- Supports both KVM and LXC, OpenVZ has not been tested, as it is no longer supported on Proxmox VE.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
hostname:
|
||||
description:
|
||||
- The instance name.
|
||||
type: str
|
||||
vmid:
|
||||
description:
|
||||
- The instance ID.
|
||||
- If not set, will be fetched from PromoxAPI based on the hostname.
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the instance snapshot.
|
||||
- The V(rollback) value was added in community.general 4.8.0.
|
||||
choices: ['present', 'absent', 'rollback']
|
||||
default: present
|
||||
type: str
|
||||
force:
|
||||
description:
|
||||
- For removal from config file, even if removing disk snapshot fails.
|
||||
default: false
|
||||
type: bool
|
||||
unbind:
|
||||
description:
|
||||
- This option only applies to LXC containers.
|
||||
- Allows to snapshot a container even if it has configured mountpoints.
|
||||
- Temporarily disables all configured mountpoints, takes snapshot, and finally restores original configuration.
|
||||
- If running, the container will be stopped and restarted to apply config changes.
|
||||
- Due to restrictions in the Proxmox API this option can only be used authenticating as V(root@pam) with O(api_password),
|
||||
API tokens do not work either.
|
||||
- See U(https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/lxc/{vmid}/config) (PUT tab) for more details.
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 5.7.0
|
||||
vmstate:
|
||||
description:
|
||||
- Snapshot includes RAM.
|
||||
default: false
|
||||
type: bool
|
||||
description:
|
||||
description:
|
||||
- Specify the description for the snapshot. Only used on the configuration web interface.
|
||||
- This is saved as a comment inside the configuration file.
|
||||
type: str
|
||||
timeout:
|
||||
description:
|
||||
- Timeout for operations.
|
||||
default: 30
|
||||
type: int
|
||||
snapname:
|
||||
description:
|
||||
- Name of the snapshot that has to be created/deleted/restored.
|
||||
default: 'ansible_snap'
|
||||
type: str
|
||||
retention:
|
||||
description:
|
||||
- Remove old snapshots if there are more than O(retention) snapshots.
|
||||
- If O(retention) is set to V(0), all snapshots will be kept.
|
||||
- This is only used when O(state=present) and when an actual snapshot is created. If no snapshot is created, all existing
|
||||
snapshots will be kept.
|
||||
default: 0
|
||||
type: int
|
||||
version_added: 7.1.0
|
||||
|
||||
notes:
|
||||
- Requires proxmoxer and requests modules on host. These modules can be installed with pip.
|
||||
requirements: ["proxmoxer", "requests"]
|
||||
author: Jeffrey van Pelt (@Thulium-Drake)
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create new container snapshot
|
||||
community.general.proxmox_snap:
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
vmid: 100
|
||||
state: present
|
||||
snapname: pre-updates
|
||||
|
||||
- name: Create new container snapshot and keep only the 2 newest snapshots
|
||||
community.general.proxmox_snap:
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
vmid: 100
|
||||
state: present
|
||||
snapname: snapshot-42
|
||||
retention: 2
|
||||
|
||||
- name: Create new snapshot for a container with configured mountpoints
|
||||
community.general.proxmox_snap:
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
vmid: 100
|
||||
state: present
|
||||
unbind: true # requires root@pam+password auth, API tokens are not supported
|
||||
snapname: pre-updates
|
||||
|
||||
- name: Remove container snapshot
|
||||
community.general.proxmox_snap:
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
vmid: 100
|
||||
state: absent
|
||||
snapname: pre-updates
|
||||
|
||||
- name: Rollback container snapshot
|
||||
community.general.proxmox_snap:
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
vmid: 100
|
||||
state: rollback
|
||||
snapname: pre-updates
|
||||
"""
|
||||
|
||||
RETURN = r"""#"""
|
||||
|
||||
import time
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxSnapAnsible(ProxmoxAnsible):
|
||||
def snapshot(self, vm, vmid):
|
||||
return getattr(self.proxmox_api.nodes(vm['node']), vm['type'])(vmid).snapshot
|
||||
|
||||
def vmconfig(self, vm, vmid):
|
||||
return getattr(self.proxmox_api.nodes(vm['node']), vm['type'])(vmid).config
|
||||
|
||||
def vmstatus(self, vm, vmid):
|
||||
return getattr(self.proxmox_api.nodes(vm['node']), vm['type'])(vmid).status
|
||||
|
||||
def _container_mp_get(self, vm, vmid):
|
||||
cfg = self.vmconfig(vm, vmid).get()
|
||||
mountpoints = {}
|
||||
for key, value in cfg.items():
|
||||
if key.startswith('mp'):
|
||||
mountpoints[key] = value
|
||||
return mountpoints
|
||||
|
||||
def _container_mp_disable(self, vm, vmid, timeout, unbind, mountpoints, vmstatus):
|
||||
# shutdown container if running
|
||||
if vmstatus == 'running':
|
||||
self.shutdown_instance(vm, vmid, timeout)
|
||||
# delete all mountpoints configs
|
||||
self.vmconfig(vm, vmid).put(delete=' '.join(mountpoints))
|
||||
|
||||
def _container_mp_restore(self, vm, vmid, timeout, unbind, mountpoints, vmstatus):
|
||||
# NOTE: requires auth as `root@pam`, API tokens are not supported
|
||||
# see https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/lxc/{vmid}/config
|
||||
# restore original config
|
||||
self.vmconfig(vm, vmid).put(**mountpoints)
|
||||
# start container (if was running before snap)
|
||||
if vmstatus == 'running':
|
||||
self.start_instance(vm, vmid, timeout)
|
||||
|
||||
def start_instance(self, vm, vmid, timeout):
|
||||
taskid = self.vmstatus(vm, vmid).start.post()
|
||||
while timeout >= 0:
|
||||
if self.api_task_ok(vm['node'], taskid):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for VM to start. Last line in task before timeout: %s' %
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def shutdown_instance(self, vm, vmid, timeout):
|
||||
taskid = self.vmstatus(vm, vmid).shutdown.post()
|
||||
while timeout >= 0:
|
||||
if self.api_task_ok(vm['node'], taskid):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for VM to stop. Last line in task before timeout: %s' %
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def snapshot_retention(self, vm, vmid, retention):
|
||||
# ignore the last snapshot, which is the current state
|
||||
snapshots = self.snapshot(vm, vmid).get()[:-1]
|
||||
if retention > 0 and len(snapshots) > retention:
|
||||
# sort by age, oldest first
|
||||
for snap in sorted(snapshots, key=lambda x: x['snaptime'])[:len(snapshots) - retention]:
|
||||
self.snapshot(vm, vmid)(snap['name']).delete()
|
||||
|
||||
def snapshot_create(self, vm, vmid, timeout, snapname, description, vmstate, unbind, retention):
|
||||
if self.module.check_mode:
|
||||
return True
|
||||
|
||||
if vm['type'] == 'lxc':
|
||||
if unbind is True:
|
||||
# check if credentials will work
|
||||
# WARN: it is crucial this check runs here!
|
||||
# The correct permissions are required only to reconfig mounts.
|
||||
# Not checking now would allow to remove the configuration BUT
|
||||
# fail later, leaving the container in a misconfigured state.
|
||||
if (
|
||||
self.module.params['api_user'] != 'root@pam'
|
||||
or not self.module.params['api_password']
|
||||
):
|
||||
self.module.fail_json(msg='`unbind=True` requires authentication as `root@pam` with `api_password`, API tokens are not supported.')
|
||||
return False
|
||||
mountpoints = self._container_mp_get(vm, vmid)
|
||||
vmstatus = self.vmstatus(vm, vmid).current().get()['status']
|
||||
if mountpoints:
|
||||
self._container_mp_disable(vm, vmid, timeout, unbind, mountpoints, vmstatus)
|
||||
taskid = self.snapshot(vm, vmid).post(snapname=snapname, description=description)
|
||||
else:
|
||||
taskid = self.snapshot(vm, vmid).post(snapname=snapname, description=description, vmstate=int(vmstate))
|
||||
|
||||
while timeout >= 0:
|
||||
if self.api_task_ok(vm['node'], taskid):
|
||||
break
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for creating VM snapshot. Last line in task before timeout: %s' %
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
if vm['type'] == 'lxc' and unbind is True and mountpoints:
|
||||
self._container_mp_restore(vm, vmid, timeout, unbind, mountpoints, vmstatus)
|
||||
|
||||
self.snapshot_retention(vm, vmid, retention)
|
||||
return timeout > 0
|
||||
|
||||
def snapshot_remove(self, vm, vmid, timeout, snapname, force):
|
||||
if self.module.check_mode:
|
||||
return True
|
||||
|
||||
taskid = self.snapshot(vm, vmid).delete(snapname, force=int(force))
|
||||
while timeout >= 0:
|
||||
if self.api_task_ok(vm['node'], taskid):
|
||||
return True
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for removing VM snapshot. Last line in task before timeout: %s' %
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
return False
|
||||
|
||||
def snapshot_rollback(self, vm, vmid, timeout, snapname):
|
||||
if self.module.check_mode:
|
||||
return True
|
||||
|
||||
taskid = self.snapshot(vm, vmid)(snapname).post("rollback")
|
||||
while timeout >= 0:
|
||||
if self.api_task_ok(vm['node'], taskid):
|
||||
return True
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for rolling back VM snapshot. Last line in task before timeout: %s' %
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
snap_args = dict(
|
||||
vmid=dict(required=False),
|
||||
hostname=dict(),
|
||||
timeout=dict(type='int', default=30),
|
||||
state=dict(default='present', choices=['present', 'absent', 'rollback']),
|
||||
description=dict(type='str'),
|
||||
snapname=dict(type='str', default='ansible_snap'),
|
||||
force=dict(type='bool', default=False),
|
||||
unbind=dict(type='bool', default=False),
|
||||
vmstate=dict(type='bool', default=False),
|
||||
retention=dict(type='int', default=0),
|
||||
)
|
||||
module_args.update(snap_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
proxmox = ProxmoxSnapAnsible(module)
|
||||
|
||||
state = module.params['state']
|
||||
vmid = module.params['vmid']
|
||||
hostname = module.params['hostname']
|
||||
description = module.params['description']
|
||||
snapname = module.params['snapname']
|
||||
timeout = module.params['timeout']
|
||||
force = module.params['force']
|
||||
unbind = module.params['unbind']
|
||||
vmstate = module.params['vmstate']
|
||||
retention = module.params['retention']
|
||||
|
||||
# If hostname is set get the VM id from ProxmoxAPI
|
||||
if not vmid and hostname:
|
||||
vmid = proxmox.get_vmid(hostname)
|
||||
elif not vmid:
|
||||
module.exit_json(changed=False, msg="Vmid could not be fetched for the following action: %s" % state)
|
||||
|
||||
vm = proxmox.get_vm(vmid)
|
||||
|
||||
if state == 'present':
|
||||
try:
|
||||
for i in proxmox.snapshot(vm, vmid).get():
|
||||
if i['name'] == snapname:
|
||||
module.exit_json(changed=False, msg="Snapshot %s is already present" % snapname)
|
||||
|
||||
if proxmox.snapshot_create(vm, vmid, timeout, snapname, description, vmstate, unbind, retention):
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=False, msg="Snapshot %s would be created" % snapname)
|
||||
else:
|
||||
module.exit_json(changed=True, msg="Snapshot %s created" % snapname)
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Creating snapshot %s of VM %s failed with exception: %s" % (snapname, vmid, to_native(e)))
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
snap_exist = False
|
||||
|
||||
for i in proxmox.snapshot(vm, vmid).get():
|
||||
if i['name'] == snapname:
|
||||
snap_exist = True
|
||||
continue
|
||||
|
||||
if not snap_exist:
|
||||
module.exit_json(changed=False, msg="Snapshot %s does not exist" % snapname)
|
||||
else:
|
||||
if proxmox.snapshot_remove(vm, vmid, timeout, snapname, force):
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=False, msg="Snapshot %s would be removed" % snapname)
|
||||
else:
|
||||
module.exit_json(changed=True, msg="Snapshot %s removed" % snapname)
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Removing snapshot %s of VM %s failed with exception: %s" % (snapname, vmid, to_native(e)))
|
||||
elif state == 'rollback':
|
||||
try:
|
||||
snap_exist = False
|
||||
|
||||
for i in proxmox.snapshot(vm, vmid).get():
|
||||
if i['name'] == snapname:
|
||||
snap_exist = True
|
||||
continue
|
||||
|
||||
if not snap_exist:
|
||||
module.exit_json(changed=False, msg="Snapshot %s does not exist" % snapname)
|
||||
if proxmox.snapshot_rollback(vm, vmid, timeout, snapname):
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True, msg="Snapshot %s would be rolled back" % snapname)
|
||||
else:
|
||||
module.exit_json(changed=True, msg="Snapshot %s rolled back" % snapname)
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Rollback of snapshot %s of VM %s failed with exception: %s" % (snapname, vmid, to_native(e)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,147 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright Julian Vanden Broeck (@l00ptr) <julian.vandenbroeck at dalibo.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_storage_contents_info
|
||||
short_description: List content from a Proxmox VE storage
|
||||
version_added: 8.2.0
|
||||
description:
|
||||
- Retrieves information about stored objects on a specific storage attached to a node.
|
||||
attributes:
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
storage:
|
||||
description:
|
||||
- Only return content stored on that specific storage.
|
||||
aliases: ['name']
|
||||
type: str
|
||||
required: true
|
||||
node:
|
||||
description:
|
||||
- Proxmox node to which the storage is attached.
|
||||
type: str
|
||||
required: true
|
||||
content:
|
||||
description:
|
||||
- Filter on a specific content type.
|
||||
type: str
|
||||
choices: ["all", "backup", "rootdir", "images", "iso"]
|
||||
default: "all"
|
||||
vmid:
|
||||
description:
|
||||
- Filter on a specific VMID.
|
||||
type: int
|
||||
author: Julian Vanden Broeck (@l00ptr)
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
- community.general.attributes.info_module
|
||||
"""
|
||||
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: List existing storages
|
||||
community.general.proxmox_storage_contents_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
storage: lvm2
|
||||
content: backup
|
||||
vmid: 130
|
||||
"""
|
||||
|
||||
|
||||
RETURN = r"""
|
||||
proxmox_storage_content:
|
||||
description: Content of of storage attached to a node.
|
||||
type: list
|
||||
returned: success
|
||||
elements: dict
|
||||
contains:
|
||||
content:
|
||||
description: Proxmox content of listed objects on this storage.
|
||||
type: str
|
||||
returned: success
|
||||
ctime:
|
||||
description: Creation time of the listed objects.
|
||||
type: str
|
||||
returned: success
|
||||
format:
|
||||
description: Format of the listed objects (can be V(raw), V(pbs-vm), V(iso),...).
|
||||
type: str
|
||||
returned: success
|
||||
size:
|
||||
description: Size of the listed objects.
|
||||
type: int
|
||||
returned: success
|
||||
subtype:
|
||||
description: Subtype of the listed objects (can be V(qemu) or V(lxc)).
|
||||
type: str
|
||||
returned: When storage is dedicated to backup, typically on PBS storage.
|
||||
verification:
|
||||
description: Backup verification status of the listed objects.
|
||||
type: dict
|
||||
returned: When storage is dedicated to backup, typically on PBS storage.
|
||||
sample: {
|
||||
"state": "ok",
|
||||
"upid": "UPID:backup-srv:00130F49:1A12D8375:00001CD7:657A2258:verificationjob:daily\\x3av\\x2dd0cc18c5\\x2d8707:root@pam:"
|
||||
}
|
||||
volid:
|
||||
description: Volume identifier of the listed objects.
|
||||
type: str
|
||||
returned: success
|
||||
"""
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
ProxmoxAnsible, proxmox_auth_argument_spec)
|
||||
|
||||
|
||||
def proxmox_storage_info_argument_spec():
|
||||
return dict(
|
||||
storage=dict(type="str", required=True, aliases=["name"]),
|
||||
content=dict(type="str", required=False, default="all", choices=["all", "backup", "rootdir", "images", "iso"]),
|
||||
vmid=dict(type="int"),
|
||||
node=dict(required=True, type="str"),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
storage_info_args = proxmox_storage_info_argument_spec()
|
||||
module_args.update(storage_info_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_one_of=[("api_password", "api_token_id")],
|
||||
required_together=[("api_token_id", "api_token_secret")],
|
||||
supports_check_mode=True,
|
||||
)
|
||||
result = dict(changed=False)
|
||||
proxmox = ProxmoxAnsible(module)
|
||||
res = proxmox.get_storage_content(
|
||||
node=module.params["node"],
|
||||
storage=module.params["storage"],
|
||||
content=None if module.params["content"] == "all" else module.params["content"],
|
||||
vmid=module.params["vmid"],
|
||||
)
|
||||
result["proxmox_storage_content"] = res
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,194 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright Tristan Le Guern (@tleguern) <tleguern at bouledef.eu>
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_storage_info
|
||||
short_description: Retrieve information about one or more Proxmox VE storages
|
||||
version_added: 2.2.0
|
||||
description:
|
||||
- Retrieve information about one or more Proxmox VE storages.
|
||||
attributes:
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
storage:
|
||||
description:
|
||||
- Only return information on a specific storage.
|
||||
aliases: ['name']
|
||||
type: str
|
||||
type:
|
||||
description:
|
||||
- Filter on a specific storage type.
|
||||
type: str
|
||||
author: Tristan Le Guern (@tleguern)
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
- community.general.attributes.info_module
|
||||
notes:
|
||||
- Storage specific options can be returned by this module, please look at the documentation at U(https://pve.proxmox.com/wiki/Storage).
|
||||
"""
|
||||
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: List existing storages
|
||||
community.general.proxmox_storage_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
register: proxmox_storages
|
||||
|
||||
- name: List NFS storages only
|
||||
community.general.proxmox_storage_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
type: nfs
|
||||
register: proxmox_storages_nfs
|
||||
|
||||
- name: Retrieve information about the lvm2 storage
|
||||
community.general.proxmox_storage_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
storage: lvm2
|
||||
register: proxmox_storage_lvm
|
||||
"""
|
||||
|
||||
|
||||
RETURN = r"""
|
||||
proxmox_storages:
|
||||
description: List of storage pools.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
content:
|
||||
description: Proxmox content types available in this storage.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: str
|
||||
digest:
|
||||
description: Storage's digest.
|
||||
returned: on success
|
||||
type: str
|
||||
nodes:
|
||||
description: List of nodes associated to this storage.
|
||||
returned: on success, if storage is not local
|
||||
type: list
|
||||
elements: str
|
||||
path:
|
||||
description: Physical path to this storage.
|
||||
returned: on success
|
||||
type: str
|
||||
prune-backups:
|
||||
description: Backup retention options.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: dict
|
||||
shared:
|
||||
description: Is this storage shared.
|
||||
returned: on success
|
||||
type: bool
|
||||
storage:
|
||||
description: Storage name.
|
||||
returned: on success
|
||||
type: str
|
||||
type:
|
||||
description: Storage type.
|
||||
returned: on success
|
||||
type: str
|
||||
"""
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, proxmox_to_ansible_bool)
|
||||
|
||||
|
||||
class ProxmoxStorageInfoAnsible(ProxmoxAnsible):
|
||||
def get_storage(self, storage):
|
||||
try:
|
||||
storage = self.proxmox_api.storage.get(storage)
|
||||
except Exception:
|
||||
self.module.fail_json(msg="Storage '%s' does not exist" % storage)
|
||||
return ProxmoxStorage(storage)
|
||||
|
||||
def get_storages(self, type=None):
|
||||
storages = self.proxmox_api.storage.get(type=type)
|
||||
storages = [ProxmoxStorage(storage) for storage in storages]
|
||||
return storages
|
||||
|
||||
|
||||
class ProxmoxStorage:
|
||||
def __init__(self, storage):
|
||||
self.storage = storage
|
||||
# Convert proxmox representation of lists, dicts and boolean for easier
|
||||
# manipulation within ansible.
|
||||
if 'shared' in self.storage:
|
||||
self.storage['shared'] = proxmox_to_ansible_bool(self.storage['shared'])
|
||||
if 'content' in self.storage:
|
||||
self.storage['content'] = self.storage['content'].split(',')
|
||||
if 'nodes' in self.storage:
|
||||
self.storage['nodes'] = self.storage['nodes'].split(',')
|
||||
if 'prune-backups' in storage:
|
||||
options = storage['prune-backups'].split(',')
|
||||
self.storage['prune-backups'] = dict()
|
||||
for option in options:
|
||||
k, v = option.split('=')
|
||||
self.storage['prune-backups'][k] = v
|
||||
|
||||
|
||||
def proxmox_storage_info_argument_spec():
|
||||
return dict(
|
||||
storage=dict(type='str', aliases=['name']),
|
||||
type=dict(type='str'),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
storage_info_args = proxmox_storage_info_argument_spec()
|
||||
module_args.update(storage_info_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_one_of=[('api_password', 'api_token_id')],
|
||||
required_together=[('api_token_id', 'api_token_secret')],
|
||||
mutually_exclusive=[('storage', 'type')],
|
||||
supports_check_mode=True
|
||||
)
|
||||
result = dict(
|
||||
changed=False
|
||||
)
|
||||
|
||||
proxmox = ProxmoxStorageInfoAnsible(module)
|
||||
storage = module.params['storage']
|
||||
storagetype = module.params['type']
|
||||
|
||||
if storage:
|
||||
storages = [proxmox.get_storage(storage)]
|
||||
else:
|
||||
storages = proxmox.get_storages(type=storagetype)
|
||||
result['proxmox_storages'] = [storage.storage for storage in storages]
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,188 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2021, Andreas Botzner (@paginabianca) <andreas at botzner dot com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_tasks_info
|
||||
short_description: Retrieve information about one or more Proxmox VE tasks
|
||||
version_added: 3.8.0
|
||||
description:
|
||||
- Retrieve information about one or more Proxmox VE tasks.
|
||||
author: 'Andreas Botzner (@paginabianca) <andreas at botzner dot com>'
|
||||
attributes:
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
node:
|
||||
description:
|
||||
- Node where to get tasks.
|
||||
required: true
|
||||
type: str
|
||||
task:
|
||||
description:
|
||||
- Return specific task.
|
||||
aliases: ['upid', 'name']
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
- community.general.attributes.info_module
|
||||
"""
|
||||
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: List tasks on node01
|
||||
community.general.proxmox_tasks_info:
|
||||
api_host: proxmoxhost
|
||||
api_user: root@pam
|
||||
api_password: '{{ password | default(omit) }}'
|
||||
api_token_id: '{{ token_id | default(omit) }}'
|
||||
api_token_secret: '{{ token_secret | default(omit) }}'
|
||||
node: node01
|
||||
register: result
|
||||
|
||||
- name: Retrieve information about specific tasks on node01
|
||||
community.general.proxmox_tasks_info:
|
||||
api_host: proxmoxhost
|
||||
api_user: root@pam
|
||||
api_password: '{{ password | default(omit) }}'
|
||||
api_token_id: '{{ token_id | default(omit) }}'
|
||||
api_token_secret: '{{ token_secret | default(omit) }}'
|
||||
task: 'UPID:node01:00003263:16167ACE:621EE230:srvreload:networking:root@pam:'
|
||||
node: node01
|
||||
register: proxmox_tasks
|
||||
"""
|
||||
|
||||
|
||||
RETURN = r"""
|
||||
proxmox_tasks:
|
||||
description: List of tasks.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
id:
|
||||
description: ID of the task.
|
||||
returned: on success
|
||||
type: str
|
||||
node:
|
||||
description: Node name.
|
||||
returned: on success
|
||||
type: str
|
||||
pid:
|
||||
description: PID of the task.
|
||||
returned: on success
|
||||
type: int
|
||||
pstart:
|
||||
description: Pastart of the task.
|
||||
returned: on success
|
||||
type: int
|
||||
starttime:
|
||||
description: Starting time of the task.
|
||||
returned: on success
|
||||
type: int
|
||||
type:
|
||||
description: Type of the task.
|
||||
returned: on success
|
||||
type: str
|
||||
upid:
|
||||
description: UPID of the task.
|
||||
returned: on success
|
||||
type: str
|
||||
user:
|
||||
description: User that owns the task.
|
||||
returned: on success
|
||||
type: str
|
||||
endtime:
|
||||
description: Endtime of the task.
|
||||
returned: on success, can be absent
|
||||
type: int
|
||||
status:
|
||||
description: Status of the task.
|
||||
returned: on success, can be absent
|
||||
type: str
|
||||
failed:
|
||||
description: If the task failed.
|
||||
returned: when status is defined
|
||||
type: bool
|
||||
msg:
|
||||
description: Short message.
|
||||
returned: on failure
|
||||
type: str
|
||||
sample: 'Task: UPID:xyz:xyz does not exist on node: proxmoxnode'
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxTaskInfoAnsible(ProxmoxAnsible):
|
||||
def get_task(self, upid, node):
|
||||
tasks = self.get_tasks(node)
|
||||
for task in tasks:
|
||||
if task.info['upid'] == upid:
|
||||
return [task]
|
||||
|
||||
def get_tasks(self, node):
|
||||
tasks = self.proxmox_api.nodes(node).tasks.get()
|
||||
return [ProxmoxTask(task) for task in tasks]
|
||||
|
||||
|
||||
class ProxmoxTask:
|
||||
def __init__(self, task):
|
||||
self.info = dict()
|
||||
for k, v in task.items():
|
||||
if k == 'status' and isinstance(v, str):
|
||||
self.info[k] = v
|
||||
if v != 'OK':
|
||||
self.info['failed'] = True
|
||||
else:
|
||||
self.info[k] = v
|
||||
|
||||
|
||||
def proxmox_task_info_argument_spec():
|
||||
return dict(
|
||||
task=dict(type='str', aliases=['upid', 'name'], required=False),
|
||||
node=dict(type='str', required=True),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
task_info_args = proxmox_task_info_argument_spec()
|
||||
module_args.update(task_info_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_together=[('api_token_id', 'api_token_secret')],
|
||||
required_one_of=[('api_password', 'api_token_id')],
|
||||
supports_check_mode=True)
|
||||
result = dict(changed=False)
|
||||
|
||||
proxmox = ProxmoxTaskInfoAnsible(module)
|
||||
upid = module.params['task']
|
||||
node = module.params['node']
|
||||
if upid:
|
||||
tasks = proxmox.get_task(upid=upid, node=node)
|
||||
else:
|
||||
tasks = proxmox.get_tasks(node=node)
|
||||
if tasks is not None:
|
||||
result['proxmox_tasks'] = [task.info for task in tasks]
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
result['msg'] = 'Task: {0} does not exist on node: {1}.'.format(
|
||||
upid, node)
|
||||
module.fail_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,374 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright Ansible Project
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_template
|
||||
short_description: Management of OS templates in Proxmox VE cluster
|
||||
description:
|
||||
- Allows you to upload/delete templates in Proxmox VE cluster.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: none
|
||||
diff_mode:
|
||||
support: none
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
node:
|
||||
description:
|
||||
- Proxmox VE node on which to operate.
|
||||
type: str
|
||||
src:
|
||||
description:
|
||||
- Path to uploaded file.
|
||||
- Exactly one of O(src) or O(url) is required for O(state=present).
|
||||
type: path
|
||||
url:
|
||||
description:
|
||||
- URL to file to download.
|
||||
- Exactly one of O(src) or O(url) is required for O(state=present).
|
||||
type: str
|
||||
version_added: 10.1.0
|
||||
template:
|
||||
description:
|
||||
- The template name.
|
||||
- Required for O(state=absent) to delete a template.
|
||||
- Required for O(state=present) to download an appliance container template (pveam).
|
||||
type: str
|
||||
content_type:
|
||||
description:
|
||||
- Content type.
|
||||
- Required only for O(state=present).
|
||||
type: str
|
||||
default: 'vztmpl'
|
||||
choices: ['vztmpl', 'iso']
|
||||
storage:
|
||||
description:
|
||||
- Target storage.
|
||||
type: str
|
||||
default: 'local'
|
||||
timeout:
|
||||
description:
|
||||
- Timeout for operations.
|
||||
type: int
|
||||
default: 30
|
||||
force:
|
||||
description:
|
||||
- It can only be used with O(state=present), existing template will be overwritten.
|
||||
type: bool
|
||||
default: false
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the template.
|
||||
type: str
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
checksum_algorithm:
|
||||
description:
|
||||
- Algorithm used to verify the checksum.
|
||||
- If specified, O(checksum) must also be specified.
|
||||
type: str
|
||||
choices: ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']
|
||||
version_added: 10.3.0
|
||||
checksum:
|
||||
description:
|
||||
- The checksum to validate against.
|
||||
- Checksums are often provided by software distributors to verify that a download is not corrupted.
|
||||
- Checksums can usually be found on the distributors download page in the form of a file or string.
|
||||
- If specified, O(checksum_algorithm) must also be specified.
|
||||
type: str
|
||||
version_added: 10.3.0
|
||||
notes:
|
||||
- Requires C(proxmoxer) and C(requests) modules on host. Those modules can be installed with M(ansible.builtin.pip).
|
||||
- C(proxmoxer) >= 1.2.0 requires C(requests_toolbelt) to upload files larger than 256 MB.
|
||||
author: Sergei Antipov (@UnderGreen)
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
---
|
||||
- name: Upload new openvz template with minimal options
|
||||
community.general.proxmox_template:
|
||||
node: uk-mc02
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
src: ~/ubuntu-14.04-x86_64.tar.gz
|
||||
|
||||
- name: Pull new openvz template with minimal options
|
||||
community.general.proxmox_template:
|
||||
node: uk-mc02
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
url: https://ubuntu-mirror/ubuntu-14.04-x86_64.tar.gz
|
||||
|
||||
- name: >
|
||||
Upload new openvz template with minimal options use environment
|
||||
PROXMOX_PASSWORD variable(you should export it before)
|
||||
community.general.proxmox_template:
|
||||
node: uk-mc02
|
||||
api_user: root@pam
|
||||
api_host: node1
|
||||
src: ~/ubuntu-14.04-x86_64.tar.gz
|
||||
|
||||
- name: Upload new openvz template with all options and force overwrite
|
||||
community.general.proxmox_template:
|
||||
node: uk-mc02
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
storage: local
|
||||
content_type: vztmpl
|
||||
src: ~/ubuntu-14.04-x86_64.tar.gz
|
||||
force: true
|
||||
|
||||
- name: Pull new openvz template with all options and force overwrite
|
||||
community.general.proxmox_template:
|
||||
node: uk-mc02
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
storage: local
|
||||
content_type: vztmpl
|
||||
url: https://ubuntu-mirror/ubuntu-14.04-x86_64.tar.gz
|
||||
force: true
|
||||
|
||||
- name: Delete template with minimal options
|
||||
community.general.proxmox_template:
|
||||
node: uk-mc02
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
template: ubuntu-14.04-x86_64.tar.gz
|
||||
state: absent
|
||||
|
||||
- name: Download proxmox appliance container template
|
||||
community.general.proxmox_template:
|
||||
node: uk-mc02
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
storage: local
|
||||
content_type: vztmpl
|
||||
template: ubuntu-20.04-standard_20.04-1_amd64.tar.gz
|
||||
|
||||
- name: Download and verify a template's checksum
|
||||
community.general.proxmox_template:
|
||||
node: uk-mc02
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
url: ubuntu-20.04-standard_20.04-1_amd64.tar.gz
|
||||
checksum_algorithm: sha256
|
||||
checksum: 65d860160bdc9b98abf72407e14ca40b609417de7939897d3b58d55787aaef69
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
|
||||
|
||||
REQUESTS_TOOLBELT_ERR = None
|
||||
try:
|
||||
# requests_toolbelt is used internally by proxmoxer module
|
||||
import requests_toolbelt # noqa: F401, pylint: disable=unused-import
|
||||
HAS_REQUESTS_TOOLBELT = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS_TOOLBELT = False
|
||||
REQUESTS_TOOLBELT_ERR = traceback.format_exc()
|
||||
|
||||
|
||||
class ProxmoxTemplateAnsible(ProxmoxAnsible):
|
||||
def has_template(self, node, storage, content_type, template):
|
||||
volid = '%s:%s/%s' % (storage, content_type, template)
|
||||
try:
|
||||
return any(tmpl['volid'] == volid for tmpl in self.proxmox_api.nodes(node).storage(storage).content.get())
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Failed to retrieve template '%s': %s" % (volid, e))
|
||||
|
||||
def task_status(self, node, taskid, timeout):
|
||||
"""
|
||||
Check the task status and wait until the task is completed or the timeout is reached.
|
||||
"""
|
||||
while timeout:
|
||||
if self.api_task_ok(node, taskid):
|
||||
return True
|
||||
elif self.api_task_failed(node, taskid):
|
||||
self.module.fail_json(msg="Task error: %s" % self.proxmox_api.nodes(node).tasks(taskid).status.get()['exitstatus'])
|
||||
timeout = timeout - 1
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for uploading/downloading template. Last line in task before timeout: %s' %
|
||||
self.proxmox_api.nodes(node).tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def upload_template(self, node, storage, content_type, realpath, timeout):
|
||||
stats = os.stat(realpath)
|
||||
if (LooseVersion(self.proxmoxer_version) >= LooseVersion('1.2.0') and
|
||||
stats.st_size > 268435456 and not HAS_REQUESTS_TOOLBELT):
|
||||
self.module.fail_json(msg="'requests_toolbelt' module is required to upload files larger than 256MB",
|
||||
exception=missing_required_lib('requests_toolbelt'))
|
||||
|
||||
try:
|
||||
taskid = self.proxmox_api.nodes(node).storage(storage).upload.post(content=content_type, filename=open(realpath, 'rb'))
|
||||
return self.task_status(node, taskid, timeout)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Uploading template %s failed with error: %s" % (realpath, e))
|
||||
|
||||
def fetch_template(self, node, storage, content_type, url, timeout):
|
||||
"""Fetch a template from a web url source using the proxmox download-url endpoint
|
||||
"""
|
||||
try:
|
||||
taskid = self.proxmox_api.nodes(node).storage(storage)("download-url").post(
|
||||
url=url, content=content_type, filename=os.path.basename(url)
|
||||
)
|
||||
return self.task_status(node, taskid, timeout)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Fetching template from url %s failed with error: %s" % (url, e))
|
||||
|
||||
def download_template(self, node, storage, template, timeout):
|
||||
try:
|
||||
taskid = self.proxmox_api.nodes(node).aplinfo.post(storage=storage, template=template)
|
||||
return self.task_status(node, taskid, timeout)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Downloading template %s failed with error: %s" % (template, e))
|
||||
|
||||
def delete_template(self, node, storage, content_type, template, timeout):
|
||||
volid = '%s:%s/%s' % (storage, content_type, template)
|
||||
self.proxmox_api.nodes(node).storage(storage).content.delete(volid)
|
||||
while timeout:
|
||||
if not self.has_template(node, storage, content_type, template):
|
||||
return True
|
||||
timeout = timeout - 1
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for deleting template.')
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def fetch_and_verify(self, node, storage, url, content_type, timeout, checksum, checksum_algorithm):
|
||||
""" Fetch a template from a web url, then verify it using a checksum.
|
||||
"""
|
||||
data = {
|
||||
'url': url,
|
||||
'content': content_type,
|
||||
'filename': os.path.basename(url),
|
||||
'checksum': checksum,
|
||||
'checksum-algorithm': checksum_algorithm}
|
||||
try:
|
||||
taskid = self.proxmox_api.nodes(node).storage(storage).post("download-url?{}".format(urlencode(data)))
|
||||
return self.task_status(node, taskid, timeout)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Checksum mismatch: %s" % (e))
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
template_args = dict(
|
||||
node=dict(),
|
||||
src=dict(type='path'),
|
||||
url=dict(),
|
||||
template=dict(),
|
||||
content_type=dict(default='vztmpl', choices=['vztmpl', 'iso']),
|
||||
storage=dict(default='local'),
|
||||
timeout=dict(type='int', default=30),
|
||||
force=dict(type='bool', default=False),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
checksum_algorithm=dict(choices=['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']),
|
||||
checksum=dict(type='str'),
|
||||
)
|
||||
module_args.update(template_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_together=[('api_token_id', 'api_token_secret'), ('checksum', 'checksum_algorithm')],
|
||||
required_one_of=[('api_password', 'api_token_id')],
|
||||
required_if=[('state', 'absent', ['template'])],
|
||||
mutually_exclusive=[("src", "url")],
|
||||
)
|
||||
|
||||
proxmox = ProxmoxTemplateAnsible(module)
|
||||
|
||||
state = module.params['state']
|
||||
node = module.params['node']
|
||||
storage = module.params['storage']
|
||||
timeout = module.params['timeout']
|
||||
checksum = module.params['checksum']
|
||||
checksum_algorithm = module.params['checksum_algorithm']
|
||||
|
||||
if state == 'present':
|
||||
content_type = module.params['content_type']
|
||||
src = module.params['src']
|
||||
url = module.params['url']
|
||||
|
||||
# download appliance template
|
||||
if content_type == 'vztmpl' and not (src or url):
|
||||
template = module.params['template']
|
||||
|
||||
if not template:
|
||||
module.fail_json(msg='template param for downloading appliance template is mandatory')
|
||||
|
||||
if proxmox.has_template(node, storage, content_type, template) and not module.params['force']:
|
||||
module.exit_json(changed=False, msg='template with volid=%s:%s/%s already exists' % (storage, content_type, template))
|
||||
|
||||
if proxmox.download_template(node, storage, template, timeout):
|
||||
module.exit_json(changed=True, msg='template with volid=%s:%s/%s downloaded' % (storage, content_type, template))
|
||||
|
||||
if not src and not url:
|
||||
module.fail_json(msg='src or url param for uploading template file is mandatory')
|
||||
elif not url:
|
||||
template = os.path.basename(src)
|
||||
if proxmox.has_template(node, storage, content_type, template) and not module.params['force']:
|
||||
module.exit_json(changed=False, msg='template with volid=%s:%s/%s already exists' % (storage, content_type, template))
|
||||
elif not (os.path.exists(src) and os.path.isfile(src)):
|
||||
module.fail_json(msg='template file on path %s not exists' % src)
|
||||
|
||||
if proxmox.upload_template(node, storage, content_type, src, timeout):
|
||||
module.exit_json(changed=True, msg='template with volid=%s:%s/%s uploaded' % (storage, content_type, template))
|
||||
elif not src:
|
||||
template = os.path.basename(urlparse(url).path)
|
||||
if proxmox.has_template(node, storage, content_type, template):
|
||||
if not module.params['force']:
|
||||
module.exit_json(changed=False, msg='template with volid=%s:%s/%s already exists' % (storage, content_type, template))
|
||||
elif not proxmox.delete_template(node, storage, content_type, template, timeout):
|
||||
module.fail_json(changed=False, msg='failed to delete template with volid=%s:%s/%s' % (storage, content_type, template))
|
||||
|
||||
if checksum:
|
||||
if proxmox.fetch_and_verify(node, storage, url, content_type, timeout, checksum, checksum_algorithm):
|
||||
module.exit_json(changed=True, msg="Checksum verified, template with volid=%s:%s/%s uploaded" % (storage, content_type, template))
|
||||
if proxmox.fetch_template(node, storage, content_type, url, timeout):
|
||||
module.exit_json(changed=True, msg='template with volid=%s:%s/%s uploaded' % (storage, content_type, template))
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
content_type = module.params['content_type']
|
||||
template = module.params['template']
|
||||
|
||||
if not proxmox.has_template(node, storage, content_type, template):
|
||||
module.exit_json(changed=False, msg='template with volid=%s:%s/%s is already deleted' % (storage, content_type, template))
|
||||
|
||||
if proxmox.delete_template(node, storage, content_type, template, timeout):
|
||||
module.exit_json(changed=True, msg='template with volid=%s:%s/%s deleted' % (storage, content_type, template))
|
||||
except Exception as e:
|
||||
module.fail_json(msg="deleting of template %s failed with exception: %s" % (template, e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,260 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright Tristan Le Guern <tleguern at bouledef.eu>
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_user_info
|
||||
short_description: Retrieve information about one or more Proxmox VE users
|
||||
version_added: 1.3.0
|
||||
description:
|
||||
- Retrieve information about one or more Proxmox VE users.
|
||||
attributes:
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
domain:
|
||||
description:
|
||||
- Restrict results to a specific authentication realm.
|
||||
aliases: ['realm']
|
||||
type: str
|
||||
user:
|
||||
description:
|
||||
- Restrict results to a specific user.
|
||||
aliases: ['name']
|
||||
type: str
|
||||
userid:
|
||||
description:
|
||||
- Restrict results to a specific user ID, which is a concatenation of a user and domain parts.
|
||||
type: str
|
||||
author: Tristan Le Guern (@tleguern)
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
- community.general.attributes.info_module
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: List existing users
|
||||
community.general.proxmox_user_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
register: proxmox_users
|
||||
|
||||
- name: List existing users in the pve authentication realm
|
||||
community.general.proxmox_user_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
domain: pve
|
||||
register: proxmox_users_pve
|
||||
|
||||
- name: Retrieve information about admin@pve
|
||||
community.general.proxmox_user_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
userid: admin@pve
|
||||
register: proxmox_user_admin
|
||||
|
||||
- name: Alternative way to retrieve information about admin@pve
|
||||
community.general.proxmox_user_info:
|
||||
api_host: helldorado
|
||||
api_user: root@pam
|
||||
api_password: "{{ password | default(omit) }}"
|
||||
api_token_id: "{{ token_id | default(omit) }}"
|
||||
api_token_secret: "{{ token_secret | default(omit) }}"
|
||||
user: admin
|
||||
domain: pve
|
||||
register: proxmox_user_admin
|
||||
"""
|
||||
|
||||
|
||||
RETURN = r"""
|
||||
proxmox_users:
|
||||
description: List of users.
|
||||
returned: always, but can be empty
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
comment:
|
||||
description: Short description of the user.
|
||||
returned: on success
|
||||
type: str
|
||||
domain:
|
||||
description: User's authentication realm, also the right part of the user ID.
|
||||
returned: on success
|
||||
type: str
|
||||
email:
|
||||
description: User's email address.
|
||||
returned: on success
|
||||
type: str
|
||||
enabled:
|
||||
description: User's account state.
|
||||
returned: on success
|
||||
type: bool
|
||||
expire:
|
||||
description: Expiration date in seconds since EPOCH. Zero means no expiration.
|
||||
returned: on success
|
||||
type: int
|
||||
firstname:
|
||||
description: User's first name.
|
||||
returned: on success
|
||||
type: str
|
||||
groups:
|
||||
description: List of groups which the user is a member of.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: str
|
||||
keys:
|
||||
description: User's two factor authentication keys.
|
||||
returned: on success
|
||||
type: str
|
||||
lastname:
|
||||
description: User's last name.
|
||||
returned: on success
|
||||
type: str
|
||||
tokens:
|
||||
description: List of API tokens associated to the user.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
comment:
|
||||
description: Short description of the token.
|
||||
returned: on success
|
||||
type: str
|
||||
expire:
|
||||
description: Expiration date in seconds since EPOCH. Zero means no expiration.
|
||||
returned: on success
|
||||
type: int
|
||||
privsep:
|
||||
description: Describe if the API token is further restricted with ACLs or is fully privileged.
|
||||
returned: on success
|
||||
type: bool
|
||||
tokenid:
|
||||
description: Token name.
|
||||
returned: on success
|
||||
type: str
|
||||
user:
|
||||
description: User's login name, also the left part of the user ID.
|
||||
returned: on success
|
||||
type: str
|
||||
userid:
|
||||
description: Proxmox user ID, represented as user@realm.
|
||||
returned: on success
|
||||
type: str
|
||||
"""
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, proxmox_to_ansible_bool)
|
||||
|
||||
|
||||
class ProxmoxUserInfoAnsible(ProxmoxAnsible):
|
||||
def get_user(self, userid):
|
||||
try:
|
||||
user = self.proxmox_api.access.users.get(userid)
|
||||
except Exception:
|
||||
self.module.fail_json(msg="User '%s' does not exist" % userid)
|
||||
user['userid'] = userid
|
||||
return ProxmoxUser(user)
|
||||
|
||||
def get_users(self, domain=None):
|
||||
users = self.proxmox_api.access.users.get(full=1)
|
||||
users = [ProxmoxUser(user) for user in users]
|
||||
if domain:
|
||||
return [user for user in users if user.user['domain'] == domain]
|
||||
return users
|
||||
|
||||
|
||||
class ProxmoxUser:
|
||||
def __init__(self, user):
|
||||
self.user = dict()
|
||||
# Data representation is not the same depending on API calls
|
||||
for k, v in user.items():
|
||||
if k == 'enable':
|
||||
self.user['enabled'] = proxmox_to_ansible_bool(user['enable'])
|
||||
elif k == 'userid':
|
||||
self.user['user'] = user['userid'].split('@')[0]
|
||||
self.user['domain'] = user['userid'].split('@')[1]
|
||||
self.user[k] = v
|
||||
elif k in ['groups', 'tokens'] and (v == '' or v is None):
|
||||
self.user[k] = []
|
||||
elif k == 'groups' and isinstance(v, str):
|
||||
self.user['groups'] = v.split(',')
|
||||
elif k == 'tokens' and isinstance(v, list):
|
||||
for token in v:
|
||||
if 'privsep' in token:
|
||||
token['privsep'] = proxmox_to_ansible_bool(token['privsep'])
|
||||
self.user['tokens'] = v
|
||||
elif k == 'tokens' and isinstance(v, dict):
|
||||
self.user['tokens'] = list()
|
||||
for tokenid, tokenvalues in v.items():
|
||||
t = tokenvalues
|
||||
t['tokenid'] = tokenid
|
||||
if 'privsep' in tokenvalues:
|
||||
t['privsep'] = proxmox_to_ansible_bool(tokenvalues['privsep'])
|
||||
self.user['tokens'].append(t)
|
||||
else:
|
||||
self.user[k] = v
|
||||
|
||||
|
||||
def proxmox_user_info_argument_spec():
|
||||
return dict(
|
||||
domain=dict(type='str', aliases=['realm']),
|
||||
user=dict(type='str', aliases=['name']),
|
||||
userid=dict(type='str'),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
user_info_args = proxmox_user_info_argument_spec()
|
||||
module_args.update(user_info_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_one_of=[('api_password', 'api_token_id')],
|
||||
required_together=[('api_token_id', 'api_token_secret')],
|
||||
mutually_exclusive=[('user', 'userid'), ('domain', 'userid')],
|
||||
supports_check_mode=True
|
||||
)
|
||||
result = dict(
|
||||
changed=False
|
||||
)
|
||||
|
||||
proxmox = ProxmoxUserInfoAnsible(module)
|
||||
domain = module.params['domain']
|
||||
user = module.params['user']
|
||||
if user and domain:
|
||||
userid = user + '@' + domain
|
||||
else:
|
||||
userid = module.params['userid']
|
||||
|
||||
if userid:
|
||||
users = [proxmox.get_user(userid=userid)]
|
||||
else:
|
||||
users = proxmox.get_users(domain=domain)
|
||||
result['proxmox_users'] = [user.user for user in users]
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,285 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2023, Sergei Antipov <greendayonfire at gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: proxmox_vm_info
|
||||
short_description: Retrieve information about one or more Proxmox VE virtual machines
|
||||
version_added: 7.2.0
|
||||
description:
|
||||
- Retrieve information about one or more Proxmox VE virtual machines.
|
||||
author: 'Sergei Antipov (@UnderGreen) <greendayonfire at gmail dot com>'
|
||||
attributes:
|
||||
action_group:
|
||||
version_added: 9.0.0
|
||||
options:
|
||||
node:
|
||||
description:
|
||||
- Restrict results to a specific Proxmox VE node.
|
||||
type: str
|
||||
type:
|
||||
description:
|
||||
- Restrict results to a specific virtual machine(s) type.
|
||||
type: str
|
||||
choices:
|
||||
- all
|
||||
- qemu
|
||||
- lxc
|
||||
default: all
|
||||
vmid:
|
||||
description:
|
||||
- Restrict results to a specific virtual machine by using its ID.
|
||||
- If VM with the specified vmid does not exist in a cluster then resulting list will be empty.
|
||||
type: int
|
||||
name:
|
||||
description:
|
||||
- Restrict results to a specific virtual machine(s) by using their name.
|
||||
- If VM(s) with the specified name do not exist in a cluster then the resulting list will be empty.
|
||||
type: str
|
||||
config:
|
||||
description:
|
||||
- Whether to retrieve the VM configuration along with VM status.
|
||||
- If set to V(none) (default), no configuration will be returned.
|
||||
- If set to V(current), the current running configuration will be returned.
|
||||
- If set to V(pending), the configuration with pending changes applied will be returned.
|
||||
type: str
|
||||
choices:
|
||||
- none
|
||||
- current
|
||||
- pending
|
||||
default: none
|
||||
version_added: 8.1.0
|
||||
network:
|
||||
description:
|
||||
- Whether to retrieve the current network status.
|
||||
- Requires enabled/running qemu-guest-agent on qemu VMs.
|
||||
type: bool
|
||||
default: false
|
||||
version_added: 9.1.0
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.actiongroup_proxmox
|
||||
- community.general.proxmox.documentation
|
||||
- community.general.attributes
|
||||
- community.general.attributes.info_module
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: List all existing virtual machines on node
|
||||
community.general.proxmox_vm_info:
|
||||
api_host: proxmoxhost
|
||||
api_user: root@pam
|
||||
api_token_id: '{{ token_id | default(omit) }}'
|
||||
api_token_secret: '{{ token_secret | default(omit) }}'
|
||||
node: node01
|
||||
|
||||
- name: List all QEMU virtual machines on node
|
||||
community.general.proxmox_vm_info:
|
||||
api_host: proxmoxhost
|
||||
api_user: root@pam
|
||||
api_password: '{{ password | default(omit) }}'
|
||||
node: node01
|
||||
type: qemu
|
||||
|
||||
- name: Retrieve information about specific VM by ID
|
||||
community.general.proxmox_vm_info:
|
||||
api_host: proxmoxhost
|
||||
api_user: root@pam
|
||||
api_password: '{{ password | default(omit) }}'
|
||||
node: node01
|
||||
type: qemu
|
||||
vmid: 101
|
||||
|
||||
- name: Retrieve information about specific VM by name and get current configuration
|
||||
community.general.proxmox_vm_info:
|
||||
api_host: proxmoxhost
|
||||
api_user: root@pam
|
||||
api_password: '{{ password | default(omit) }}'
|
||||
node: node01
|
||||
type: lxc
|
||||
name: lxc05.home.arpa
|
||||
config: current
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
proxmox_vms:
|
||||
description: List of virtual machines.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: dict
|
||||
sample:
|
||||
[
|
||||
{
|
||||
"cpu": 0.258944410905281,
|
||||
"cpus": 1,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "qemu/100",
|
||||
"maxcpu": 1,
|
||||
"maxdisk": 34359738368,
|
||||
"maxmem": 4294967296,
|
||||
"mem": 35158379,
|
||||
"name": "pxe.home.arpa",
|
||||
"netin": 99715803,
|
||||
"netout": 14237835,
|
||||
"node": "pve",
|
||||
"pid": 1947197,
|
||||
"status": "running",
|
||||
"template": False,
|
||||
"type": "qemu",
|
||||
"uptime": 135530,
|
||||
"vmid": 100
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 1,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "qemu/101",
|
||||
"maxcpu": 1,
|
||||
"maxdisk": 0,
|
||||
"maxmem": 536870912,
|
||||
"mem": 0,
|
||||
"name": "test1",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"node": "pve",
|
||||
"status": "stopped",
|
||||
"template": False,
|
||||
"type": "qemu",
|
||||
"uptime": 0,
|
||||
"vmid": 101
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec,
|
||||
ProxmoxAnsible,
|
||||
proxmox_to_ansible_bool,
|
||||
)
|
||||
|
||||
|
||||
class ProxmoxVmInfoAnsible(ProxmoxAnsible):
|
||||
def get_vms_from_cluster_resources(self):
|
||||
try:
|
||||
return self.proxmox_api.cluster().resources().get(type="vm")
|
||||
except Exception as e:
|
||||
self.module.fail_json(
|
||||
msg="Failed to retrieve VMs information from cluster resources: %s" % e
|
||||
)
|
||||
|
||||
def get_vms_from_nodes(self, cluster_machines, type, vmid=None, name=None, node=None, config=None, network=False):
|
||||
# Leave in dict only machines that user wants to know about
|
||||
filtered_vms = {
|
||||
vm: info for vm, info in cluster_machines.items() if not (
|
||||
type != info["type"]
|
||||
or (node and info["node"] != node)
|
||||
or (vmid and int(info["vmid"]) != vmid)
|
||||
or (name is not None and info["name"] != name)
|
||||
)
|
||||
}
|
||||
# Get list of unique node names and loop through it to get info about machines.
|
||||
nodes = frozenset([info["node"] for vm, info in filtered_vms.items()])
|
||||
for this_node in nodes:
|
||||
# "type" is mandatory and can have only values of "qemu" or "lxc". Seems that use of reflection is safe.
|
||||
call_vm_getter = getattr(self.proxmox_api.nodes(this_node), type)
|
||||
vms_from_this_node = call_vm_getter().get()
|
||||
for detected_vm in vms_from_this_node:
|
||||
this_vm_id = int(detected_vm["vmid"])
|
||||
desired_vm = filtered_vms.get(this_vm_id, None)
|
||||
if desired_vm:
|
||||
desired_vm.update(detected_vm)
|
||||
desired_vm["vmid"] = this_vm_id
|
||||
desired_vm["template"] = proxmox_to_ansible_bool(desired_vm.get("template", 0))
|
||||
# When user wants to retrieve the VM configuration
|
||||
if config != "none":
|
||||
# pending = 0, current = 1
|
||||
config_type = 0 if config == "pending" else 1
|
||||
# GET /nodes/{node}/qemu/{vmid}/config current=[0/1]
|
||||
desired_vm["config"] = call_vm_getter(this_vm_id).config().get(current=config_type)
|
||||
if network:
|
||||
if type == "qemu":
|
||||
desired_vm["network"] = call_vm_getter(this_vm_id).agent("network-get-interfaces").get()['result']
|
||||
elif type == "lxc":
|
||||
desired_vm["network"] = call_vm_getter(this_vm_id).interfaces.get()
|
||||
|
||||
return filtered_vms
|
||||
|
||||
def get_qemu_vms(self, cluster_machines, vmid=None, name=None, node=None, config=None, network=False):
|
||||
try:
|
||||
return self.get_vms_from_nodes(cluster_machines, "qemu", vmid, name, node, config, network)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Failed to retrieve QEMU VMs information: %s" % e)
|
||||
|
||||
def get_lxc_vms(self, cluster_machines, vmid=None, name=None, node=None, config=None, network=False):
|
||||
try:
|
||||
return self.get_vms_from_nodes(cluster_machines, "lxc", vmid, name, node, config, network)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Failed to retrieve LXC VMs information: %s" % e)
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
vm_info_args = dict(
|
||||
node=dict(type="str", required=False),
|
||||
type=dict(
|
||||
type="str", choices=["lxc", "qemu", "all"], default="all", required=False
|
||||
),
|
||||
vmid=dict(type="int", required=False),
|
||||
name=dict(type="str", required=False),
|
||||
config=dict(
|
||||
type="str", choices=["none", "current", "pending"],
|
||||
default="none", required=False
|
||||
),
|
||||
network=dict(type="bool", default=False, required=False),
|
||||
)
|
||||
module_args.update(vm_info_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_together=[("api_token_id", "api_token_secret")],
|
||||
required_one_of=[("api_password", "api_token_id")],
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
proxmox = ProxmoxVmInfoAnsible(module)
|
||||
node = module.params["node"]
|
||||
type = module.params["type"]
|
||||
vmid = module.params["vmid"]
|
||||
name = module.params["name"]
|
||||
config = module.params["config"]
|
||||
network = module.params["network"]
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if node and proxmox.get_node(node) is None:
|
||||
module.fail_json(msg="Node %s doesn't exist in PVE cluster" % node)
|
||||
|
||||
vms_cluster_resources = proxmox.get_vms_from_cluster_resources()
|
||||
cluster_machines = {int(machine["vmid"]): machine for machine in vms_cluster_resources}
|
||||
vms = {}
|
||||
|
||||
if type == "lxc":
|
||||
vms = proxmox.get_lxc_vms(cluster_machines, vmid, name, node, config, network)
|
||||
elif type == "qemu":
|
||||
vms = proxmox.get_qemu_vms(cluster_machines, vmid, name, node, config, network)
|
||||
else:
|
||||
vms = proxmox.get_qemu_vms(cluster_machines, vmid, name, node, config, network)
|
||||
vms.update(proxmox.get_lxc_vms(cluster_machines, vmid, name, node, config, network))
|
||||
|
||||
result["proxmox_vms"] = [info for vm, info in sorted(vms.items())]
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,12 +0,0 @@
|
|||
# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
|
||||
# Copyright (c) 2025 Ansible Project
|
||||
# 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
|
||||
|
||||
azp/posix/3
|
||||
destructive
|
||||
needs/root
|
||||
needs/target/connection
|
||||
skip/docker
|
||||
skip/alpine
|
||||
skip/macos
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
|
||||
# Copyright (c) 2025 Ansible Project
|
||||
# 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
|
||||
|
||||
- hosts: localhost
|
||||
gather_facts: true
|
||||
serial: 1
|
||||
tasks:
|
||||
- name: Copy pct mock
|
||||
copy:
|
||||
src: files/pct
|
||||
dest: /usr/sbin/pct
|
||||
mode: '0755'
|
||||
- name: Install paramiko
|
||||
pip:
|
||||
name: "paramiko>=3.0.0"
|
|
@ -1,33 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
|
||||
# Copyright (c) 2025 Ansible Project
|
||||
# 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
|
||||
|
||||
# Shell script to mock proxmox pct behaviour
|
||||
|
||||
>&2 echo "[DEBUG] INPUT: $@"
|
||||
|
||||
pwd="$(pwd)"
|
||||
|
||||
# Get quoted parts and restore quotes
|
||||
declare -a cmd=()
|
||||
for arg in "$@"; do
|
||||
if [[ $arg =~ [[:space:]] ]]; then
|
||||
arg="'$arg'"
|
||||
fi
|
||||
cmd+=("$arg")
|
||||
done
|
||||
|
||||
cmd="${cmd[@]:3}"
|
||||
vmid="${@:2:1}"
|
||||
>&2 echo "[INFO] MOCKING: pct ${@:1:3} ${cmd}"
|
||||
tmp_dir="/tmp/ansible-remote/proxmox_pct_remote/integration_test/ct_${vmid}"
|
||||
mkdir -p "$tmp_dir"
|
||||
>&2 echo "[INFO] PWD: $tmp_dir"
|
||||
>&2 echo "[INFO] CMD: ${cmd}"
|
||||
cd "$tmp_dir"
|
||||
|
||||
eval "${cmd}"
|
||||
|
||||
cd "$pwd"
|
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# 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
|
||||
|
||||
- hosts: "{{ target_hosts }}"
|
||||
gather_facts: false
|
||||
serial: 1
|
||||
tasks:
|
||||
- name: create file without content
|
||||
copy:
|
||||
content: ""
|
||||
dest: "{{ remote_tmp }}/test_empty.txt"
|
||||
force: no
|
||||
mode: '0644'
|
||||
|
||||
- name: assert file without content exists
|
||||
stat:
|
||||
path: "{{ remote_tmp }}/test_empty.txt"
|
||||
register: empty_file_stat
|
||||
|
||||
- name: verify file without content exists
|
||||
assert:
|
||||
that:
|
||||
- empty_file_stat.stat.exists
|
||||
fail_msg: "The file {{ remote_tmp }}/test_empty.txt does not exist."
|
||||
|
||||
- name: verify file without content is empty
|
||||
assert:
|
||||
that:
|
||||
- empty_file_stat.stat.size == 0
|
||||
fail_msg: "The file {{ remote_tmp }}/test_empty.txt is not empty."
|
|
@ -1,19 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
|
||||
# Copyright (c) 2025 Ansible Project
|
||||
# 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
|
||||
|
||||
set -eux
|
||||
|
||||
ANSIBLE_ROLES_PATH=../ \
|
||||
ansible-playbook dependencies.yml -v "$@"
|
||||
|
||||
./test.sh "$@"
|
||||
|
||||
ansible-playbook plugin-specific-tests.yml -i "./test_connection.inventory" \
|
||||
-e target_hosts="proxmox_pct_remote" \
|
||||
-e action_prefix= \
|
||||
-e local_tmp=/tmp/ansible-local \
|
||||
-e remote_tmp=/tmp/ansible-remote \
|
||||
"$@"
|
|
@ -1 +0,0 @@
|
|||
../connection_posix/test.sh
|
|
@ -1,14 +0,0 @@
|
|||
# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
|
||||
# Copyright (c) 2025 Ansible Project
|
||||
# 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
|
||||
|
||||
[proxmox_pct_remote]
|
||||
proxmox_pct_remote-pipelining ansible_ssh_pipelining=true
|
||||
proxmox_pct_remote-no-pipelining ansible_ssh_pipelining=false
|
||||
[proxmox_pct_remote:vars]
|
||||
ansible_host=localhost
|
||||
ansible_user=root
|
||||
ansible_python_interpreter="{{ ansible_playbook_python }}"
|
||||
ansible_connection=community.general.proxmox_pct_remote
|
||||
proxmox_vmid=123
|
|
@ -1,9 +0,0 @@
|
|||
# Copyright (c) Ansible Project
|
||||
# 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
|
||||
|
||||
unsupported
|
||||
proxmox_domain_info
|
||||
proxmox_group_info
|
||||
proxmox_user_info
|
||||
proxmox_storage_info
|
|
@ -1,616 +0,0 @@
|
|||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# Copyright (c) 2020, Tristan Le Guern <tleguern at bouledef.eu>
|
||||
# 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
|
||||
|
||||
- name: List domains
|
||||
proxmox_domain_info:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- results.proxmox_domains is defined
|
||||
|
||||
- name: Retrieve info about pve
|
||||
proxmox_domain_info:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
domain: pve
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- results.proxmox_domains is defined
|
||||
- results.proxmox_domains|length == 1
|
||||
- results.proxmox_domains[0].type == 'pve'
|
||||
|
||||
- name: List groups
|
||||
proxmox_group_info:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- results.proxmox_groups is defined
|
||||
|
||||
- name: List users
|
||||
proxmox_user_info:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- results.proxmox_users is defined
|
||||
|
||||
- name: Retrieve info about api_user using name and domain
|
||||
proxmox_user_info:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
user: "{{ user }}"
|
||||
domain: "{{ domain }}"
|
||||
register: results_user_domain
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_user_domain is not changed
|
||||
- results_user_domain.proxmox_users is defined
|
||||
- results_user_domain.proxmox_users|length == 1
|
||||
- results_user_domain.proxmox_users[0].domain == "{{ domain }}"
|
||||
- results_user_domain.proxmox_users[0].user == "{{ user }}"
|
||||
- results_user_domain.proxmox_users[0].userid == "{{ user }}@{{ domain }}"
|
||||
|
||||
- name: Retrieve info about api_user using userid
|
||||
proxmox_user_info:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
userid: "{{ user }}@{{ domain }}"
|
||||
register: results_userid
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_userid is not changed
|
||||
- results_userid.proxmox_users is defined
|
||||
- results_userid.proxmox_users|length == 1
|
||||
- results_userid.proxmox_users[0].domain == "{{ domain }}"
|
||||
- results_userid.proxmox_users[0].user == "{{ user }}"
|
||||
- results_userid.proxmox_users[0].userid == "{{ user }}@{{ domain }}"
|
||||
|
||||
- name: Retrieve info about storage
|
||||
proxmox_storage_info:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
storage: "{{ storage }}"
|
||||
register: results_storage
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_storage is not changed
|
||||
- results_storage.proxmox_storages is defined
|
||||
- results_storage.proxmox_storages|length == 1
|
||||
- results_storage.proxmox_storages[0].storage == "{{ storage }}"
|
||||
|
||||
- name: List content on storage
|
||||
proxmox_storage_contents_info:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
storage: "{{ storage }}"
|
||||
node: "{{ node }}"
|
||||
content: images
|
||||
register: results_list_storage
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_storage is not changed
|
||||
- results_storage.proxmox_storage_content is defined
|
||||
- results_storage.proxmox_storage_content |length == 1
|
||||
|
||||
- name: VM creation
|
||||
tags: [ 'create' ]
|
||||
block:
|
||||
- name: Create test vm test-instance
|
||||
proxmox_kvm:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
node: "{{ node }}"
|
||||
storage: "{{ storage }}"
|
||||
vmid: "{{ from_vmid }}"
|
||||
name: test-instance
|
||||
clone: 'yes'
|
||||
state: present
|
||||
tags:
|
||||
- TagWithUppercaseChars
|
||||
timeout: 500
|
||||
register: results_kvm
|
||||
|
||||
- set_fact:
|
||||
vmid: "{{ results_kvm.msg.split(' ')[-7] }}"
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_kvm is changed
|
||||
- results_kvm.vmid == from_vmid
|
||||
- results_kvm.msg == "VM test-instance with newid {{ vmid }} cloned from vm with vmid {{ from_vmid }}"
|
||||
|
||||
- pause:
|
||||
seconds: 30
|
||||
|
||||
- name: VM start
|
||||
tags: [ 'start' ]
|
||||
block:
|
||||
- name: Start test VM
|
||||
proxmox_kvm:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
node: "{{ node }}"
|
||||
vmid: "{{ vmid }}"
|
||||
state: started
|
||||
register: results_action_start
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_action_start is changed
|
||||
- results_action_start.status == 'stopped'
|
||||
- results_action_start.vmid == {{ vmid }}
|
||||
- results_action_start.msg == "VM {{ vmid }} started"
|
||||
|
||||
- pause:
|
||||
seconds: 90
|
||||
|
||||
- name: Try to start test VM again
|
||||
proxmox_kvm:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
node: "{{ node }}"
|
||||
vmid: "{{ vmid }}"
|
||||
state: started
|
||||
register: results_action_start_again
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_action_start_again is not changed
|
||||
- results_action_start_again.status == 'running'
|
||||
- results_action_start_again.vmid == {{ vmid }}
|
||||
- results_action_start_again.msg == "VM {{ vmid }} is already running"
|
||||
|
||||
- name: Check current status
|
||||
proxmox_kvm:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
node: "{{ node }}"
|
||||
vmid: "{{ vmid }}"
|
||||
state: current
|
||||
register: results_action_current
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_action_current is not changed
|
||||
- results_action_current.status == 'running'
|
||||
- results_action_current.vmid == {{ vmid }}
|
||||
- results_action_current.msg == "VM test-instance with vmid = {{ vmid }} is running"
|
||||
|
||||
- name: VM add/change/delete NIC
|
||||
tags: [ 'nic' ]
|
||||
block:
|
||||
- name: Add NIC to test VM
|
||||
proxmox_nic:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
vmid: "{{ vmid }}"
|
||||
state: present
|
||||
interface: net5
|
||||
bridge: vmbr0
|
||||
tag: 42
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Nic net5 updated on VM with vmid {{ vmid }}"
|
||||
|
||||
- name: Update NIC no changes
|
||||
proxmox_nic:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
vmid: "{{ vmid }}"
|
||||
state: present
|
||||
interface: net5
|
||||
bridge: vmbr0
|
||||
tag: 42
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Nic net5 unchanged on VM with vmid {{ vmid }}"
|
||||
|
||||
- name: Update NIC with changes
|
||||
proxmox_nic:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
vmid: "{{ vmid }}"
|
||||
state: present
|
||||
interface: net5
|
||||
bridge: vmbr0
|
||||
tag: 24
|
||||
firewall: true
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- results.vmid == {{ vmid }}
|
||||
- results.msg == "Nic net5 updated on VM with vmid {{ vmid }}"
|
||||
|
||||
- name: Delete NIC
|
||||
proxmox_nic:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
vmid: "{{ vmid }}"
|
||||
state: absent
|
||||
interface: net5
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is changed
|
||||
- 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:
|
||||
- name: Stop test VM
|
||||
proxmox_kvm:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
node: "{{ node }}"
|
||||
vmid: "{{ vmid }}"
|
||||
state: stopped
|
||||
register: results_action_stop
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_action_stop is changed
|
||||
- results_action_stop.status == 'running'
|
||||
- results_action_stop.vmid == {{ vmid }}
|
||||
- results_action_stop.msg == "VM {{ vmid }} is shutting down"
|
||||
|
||||
- pause:
|
||||
seconds: 5
|
||||
|
||||
- name: Check current status again
|
||||
proxmox_kvm:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
node: "{{ node }}"
|
||||
vmid: "{{ vmid }}"
|
||||
state: current
|
||||
register: results_action_current
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_action_current is not changed
|
||||
- results_action_current.status == 'stopped'
|
||||
- results_action_current.vmid == {{ vmid }}
|
||||
- results_action_current.msg == "VM test-instance with vmid = {{ vmid }} is stopped"
|
||||
|
||||
- name: VM destroy
|
||||
tags: [ 'destroy' ]
|
||||
block:
|
||||
- name: Destroy test VM
|
||||
proxmox_kvm:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
node: "{{ node }}"
|
||||
vmid: "{{ vmid }}"
|
||||
state: absent
|
||||
register: results_kvm_destroy
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results_kvm_destroy is changed
|
||||
- results_kvm_destroy.vmid == {{ vmid }}
|
||||
- results_kvm_destroy.msg == "VM {{ vmid }} removed"
|
||||
|
||||
- name: Retrieve information about nodes
|
||||
proxmox_node_info:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
register: results
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- results.proxmox_nodes is defined
|
||||
- results.proxmox_nodes|length >= 1
|
||||
- results.proxmox_nodes[0].type == 'node'
|
|
@ -1,7 +0,0 @@
|
|||
# Copyright (c) Ansible Project
|
||||
# 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
|
||||
|
||||
unsupported
|
||||
proxmox_pool
|
||||
proxmox_pool_member
|
|
@ -1,7 +0,0 @@
|
|||
# Copyright (c) 2023, Sergei Antipov <greendayonfire at gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
poolid: test
|
||||
member: local
|
||||
member_type: storage
|
|
@ -1,220 +0,0 @@
|
|||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# Copyright (c) 2023, Sergei Antipov <greendayonfire at gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
- name: Proxmox VE pool and pool membership management
|
||||
tags: ["pool"]
|
||||
block:
|
||||
- name: Make sure poolid parameter is not missing
|
||||
proxmox_pool:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
ignore_errors: true
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is failed
|
||||
- "'missing required arguments: poolid' in result.msg"
|
||||
|
||||
- name: Create pool (Check)
|
||||
proxmox_pool:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "{{ poolid }}"
|
||||
check_mode: true
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is success
|
||||
|
||||
- name: Create pool
|
||||
proxmox_pool:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "{{ poolid }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is success
|
||||
- result.poolid == "{{ poolid }}"
|
||||
|
||||
- name: Delete pool (Check)
|
||||
proxmox_pool:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "{{ poolid }}"
|
||||
state: absent
|
||||
check_mode: true
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is success
|
||||
|
||||
- name: Delete non-existing pool should do nothing
|
||||
proxmox_pool:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "non-existing-poolid"
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is not changed
|
||||
- result is success
|
||||
|
||||
- name: Deletion of non-empty pool fails
|
||||
block:
|
||||
- name: Add storage into pool
|
||||
proxmox_pool_member:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "{{ poolid }}"
|
||||
member: "{{ member }}"
|
||||
type: "{{ member_type }}"
|
||||
diff: true
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is success
|
||||
- "'{{ member }}' in result.diff.after.members"
|
||||
|
||||
- name: Add non-existing storage into pool should fail
|
||||
proxmox_pool_member:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "{{ poolid }}"
|
||||
member: "non-existing-storage"
|
||||
type: "{{ member_type }}"
|
||||
ignore_errors: true
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is failed
|
||||
- "'Storage non-existing-storage doesn\\'t exist in the cluster' in result.msg"
|
||||
|
||||
- name: Delete non-empty pool
|
||||
proxmox_pool:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "{{ poolid }}"
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is failed
|
||||
- "'Please remove members from pool first.' in result.msg"
|
||||
|
||||
- name: Delete storage from the pool
|
||||
proxmox_pool_member:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "{{ poolid }}"
|
||||
member: "{{ member }}"
|
||||
type: "{{ member_type }}"
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is success
|
||||
- result is changed
|
||||
|
||||
rescue:
|
||||
- name: Delete storage from the pool if it is added
|
||||
proxmox_pool_member:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "{{ poolid }}"
|
||||
member: "{{ member }}"
|
||||
type: "{{ member_type }}"
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
|
||||
- name: Delete pool
|
||||
proxmox_pool:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "{{ poolid }}"
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is success
|
||||
- result.poolid == "{{ poolid }}"
|
||||
|
||||
rescue:
|
||||
- name: Delete test pool if it is created
|
||||
proxmox_pool:
|
||||
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) }}"
|
||||
validate_certs: "{{ validate_certs }}"
|
||||
poolid: "{{ poolid }}"
|
||||
state: absent
|
||||
ignore_errors: true
|
|
@ -1,6 +0,0 @@
|
|||
# Copyright (c) Ansible Project
|
||||
# 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
|
||||
|
||||
unsupported
|
||||
proxmox_template
|
|
@ -1,136 +0,0 @@
|
|||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# Copyright (c) 2023, Sergei Antipov <greendayonfire at gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
- name: Proxmox VE virtual machines templates management
|
||||
tags: ['template']
|
||||
vars:
|
||||
filename: /tmp/dummy.iso
|
||||
block:
|
||||
- name: Create dummy ISO file
|
||||
ansible.builtin.command:
|
||||
cmd: 'truncate -s 300M {{ filename }}'
|
||||
|
||||
- name: Delete requests_toolbelt module if it is installed
|
||||
ansible.builtin.pip:
|
||||
name: requests_toolbelt
|
||||
state: absent
|
||||
|
||||
- name: Install latest proxmoxer
|
||||
ansible.builtin.pip:
|
||||
name: proxmoxer
|
||||
state: latest
|
||||
|
||||
- name: Upload ISO as template to Proxmox VE cluster should fail
|
||||
proxmox_template:
|
||||
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) }}'
|
||||
validate_certs: '{{ validate_certs }}'
|
||||
node: '{{ node }}'
|
||||
src: '{{ filename }}'
|
||||
content_type: iso
|
||||
force: true
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is failed
|
||||
- result.msg is match('\'requests_toolbelt\' module is required to upload files larger than 256MB')
|
||||
|
||||
- name: Install old (1.1.2) version of proxmoxer
|
||||
ansible.builtin.pip:
|
||||
name: proxmoxer==1.1.1
|
||||
state: present
|
||||
|
||||
- name: Upload ISO as template to Proxmox VE cluster should be successful
|
||||
proxmox_template:
|
||||
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) }}'
|
||||
validate_certs: '{{ validate_certs }}'
|
||||
node: '{{ node }}'
|
||||
src: '{{ filename }}'
|
||||
content_type: iso
|
||||
force: true
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is success
|
||||
- result.msg is match('template with volid=local:iso/dummy.iso uploaded')
|
||||
|
||||
- name: Install latest proxmoxer
|
||||
ansible.builtin.pip:
|
||||
name: proxmoxer
|
||||
state: latest
|
||||
|
||||
- name: Make smaller dummy file
|
||||
ansible.builtin.command:
|
||||
cmd: 'truncate -s 128M {{ filename }}'
|
||||
|
||||
- name: Upload ISO as template to Proxmox VE cluster should be successful
|
||||
proxmox_template:
|
||||
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) }}'
|
||||
validate_certs: '{{ validate_certs }}'
|
||||
node: '{{ node }}'
|
||||
src: '{{ filename }}'
|
||||
content_type: iso
|
||||
force: true
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is success
|
||||
- result.msg is match('template with volid=local:iso/dummy.iso uploaded')
|
||||
|
||||
- name: Install requests_toolbelt
|
||||
ansible.builtin.pip:
|
||||
name: requests_toolbelt
|
||||
state: present
|
||||
|
||||
- name: Make big dummy file
|
||||
ansible.builtin.command:
|
||||
cmd: 'truncate -s 300M {{ filename }}'
|
||||
|
||||
- name: Upload ISO as template to Proxmox VE cluster should be successful
|
||||
proxmox_template:
|
||||
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) }}'
|
||||
validate_certs: '{{ validate_certs }}'
|
||||
node: '{{ node }}'
|
||||
src: '{{ filename }}'
|
||||
content_type: iso
|
||||
force: true
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is success
|
||||
- result.msg is match('template with volid=local:iso/dummy.iso uploaded')
|
||||
|
||||
always:
|
||||
- name: Delete ISO file from host
|
||||
ansible.builtin.file:
|
||||
path: '{{ filename }}'
|
||||
state: absent
|
|
@ -5,7 +5,6 @@ plugins/inventory/iocage.py yamllint:unparsable-with-libyaml
|
|||
plugins/inventory/linode.py yamllint:unparsable-with-libyaml
|
||||
plugins/inventory/lxd.py yamllint:unparsable-with-libyaml
|
||||
plugins/inventory/nmap.py yamllint:unparsable-with-libyaml
|
||||
plugins/inventory/proxmox.py yamllint:unparsable-with-libyaml
|
||||
plugins/inventory/scaleway.py yamllint:unparsable-with-libyaml
|
||||
plugins/inventory/virtualbox.py yamllint:unparsable-with-libyaml
|
||||
plugins/lookup/dependent.py validate-modules:unidiomatic-typecheck
|
||||
|
|
|
@ -4,7 +4,6 @@ plugins/inventory/iocage.py yamllint:unparsable-with-libyaml
|
|||
plugins/inventory/linode.py yamllint:unparsable-with-libyaml
|
||||
plugins/inventory/lxd.py yamllint:unparsable-with-libyaml
|
||||
plugins/inventory/nmap.py yamllint:unparsable-with-libyaml
|
||||
plugins/inventory/proxmox.py yamllint:unparsable-with-libyaml
|
||||
plugins/inventory/scaleway.py yamllint:unparsable-with-libyaml
|
||||
plugins/inventory/virtualbox.py yamllint:unparsable-with-libyaml
|
||||
plugins/lookup/dependent.py validate-modules:unidiomatic-typecheck
|
||||
|
|
|
@ -1,585 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
|
||||
# Copyright (c) 2024 Ansible Project
|
||||
# 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
|
||||
|
||||
from __future__ import (annotations, absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.general.plugins.connection.proxmox_pct_remote import authenticity_msg, MyAddPolicy
|
||||
from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
|
||||
from ansible.errors import AnsibleError, AnsibleAuthenticationFailure, AnsibleConnectionFailure
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible.module_utils.compat.paramiko import paramiko
|
||||
from ansible.playbook.play_context import PlayContext
|
||||
from ansible.plugins.loader import connection_loader
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock, mock_open
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def connection():
|
||||
play_context = PlayContext()
|
||||
in_stream = StringIO()
|
||||
conn = connection_loader.get('community.general.proxmox_pct_remote', play_context, in_stream)
|
||||
conn.set_option('remote_addr', '192.168.1.100')
|
||||
conn.set_option('remote_user', 'root')
|
||||
conn.set_option('password', 'password')
|
||||
return conn
|
||||
|
||||
|
||||
def test_connection_options(connection):
|
||||
""" Test that connection options are properly set """
|
||||
assert connection.get_option('remote_addr') == '192.168.1.100'
|
||||
assert connection.get_option('remote_user') == 'root'
|
||||
assert connection.get_option('password') == 'password'
|
||||
|
||||
|
||||
def test_authenticity_msg():
|
||||
""" Test authenticity message formatting """
|
||||
msg = authenticity_msg('test.host', 'ssh-rsa', 'AA:BB:CC:DD')
|
||||
assert 'test.host' in msg
|
||||
assert 'ssh-rsa' in msg
|
||||
assert 'AA:BB:CC:DD' in msg
|
||||
|
||||
|
||||
def test_missing_host_key(connection):
|
||||
""" Test MyAddPolicy missing_host_key method """
|
||||
|
||||
client = MagicMock()
|
||||
key = MagicMock()
|
||||
key.get_fingerprint.return_value = b'fingerprint'
|
||||
key.get_name.return_value = 'ssh-rsa'
|
||||
|
||||
policy = MyAddPolicy(connection)
|
||||
|
||||
connection.set_option('host_key_auto_add', True)
|
||||
policy.missing_host_key(client, 'test.host', key)
|
||||
assert hasattr(key, '_added_by_ansible_this_time')
|
||||
|
||||
connection.set_option('host_key_auto_add', False)
|
||||
connection.set_option('host_key_checking', False)
|
||||
policy.missing_host_key(client, 'test.host', key)
|
||||
|
||||
connection.set_option('host_key_checking', True)
|
||||
connection.set_option('host_key_auto_add', False)
|
||||
connection.set_option('use_persistent_connections', False)
|
||||
|
||||
with patch('ansible.utils.display.Display.prompt_until', return_value='yes'):
|
||||
policy.missing_host_key(client, 'test.host', key)
|
||||
|
||||
with patch('ansible.utils.display.Display.prompt_until', return_value='no'):
|
||||
with pytest.raises(AnsibleError, match='host connection rejected by user'):
|
||||
policy.missing_host_key(client, 'test.host', key)
|
||||
|
||||
|
||||
def test_set_log_channel(connection):
|
||||
""" Test setting log channel """
|
||||
connection._set_log_channel('test_channel')
|
||||
assert connection._log_channel == 'test_channel'
|
||||
|
||||
|
||||
def test_parse_proxy_command(connection):
|
||||
""" Test proxy command parsing """
|
||||
connection.set_option('proxy_command', 'ssh -W %h:%p proxy.example.com')
|
||||
connection.set_option('remote_addr', 'target.example.com')
|
||||
connection.set_option('remote_user', 'testuser')
|
||||
|
||||
result = connection._parse_proxy_command(port=2222)
|
||||
assert 'sock' in result
|
||||
assert isinstance(result['sock'], paramiko.ProxyCommand)
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_connect_with_rsa_sha2_disabled(mock_ssh, connection):
|
||||
""" Test connection with RSA SHA2 algorithms disabled """
|
||||
connection.set_option('use_rsa_sha2_algorithms', False)
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
|
||||
connection._connect()
|
||||
|
||||
call_kwargs = mock_client.connect.call_args[1]
|
||||
assert 'disabled_algorithms' in call_kwargs
|
||||
assert 'pubkeys' in call_kwargs['disabled_algorithms']
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_connect_with_bad_host_key(mock_ssh, connection):
|
||||
""" Test connection with bad host key """
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
mock_client.connect.side_effect = paramiko.ssh_exception.BadHostKeyException(
|
||||
'hostname', MagicMock(), MagicMock())
|
||||
|
||||
with pytest.raises(AnsibleConnectionFailure, match='host key mismatch'):
|
||||
connection._connect()
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_connect_with_invalid_host_key(mock_ssh, connection):
|
||||
""" Test connection with bad host key """
|
||||
connection.set_option('host_key_checking', True)
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
mock_client.load_system_host_keys.side_effect = paramiko.hostkeys.InvalidHostKey(
|
||||
"Bad Line!", Exception('Something crashed!'))
|
||||
|
||||
with pytest.raises(AnsibleConnectionFailure, match="Invalid host key: Bad Line!"):
|
||||
connection._connect()
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_connect_success(mock_ssh, connection):
|
||||
""" Test successful SSH connection establishment """
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
|
||||
connection._connect()
|
||||
|
||||
assert mock_client.connect.called
|
||||
assert connection._connected
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_connect_authentication_failure(mock_ssh, connection):
|
||||
""" Test SSH connection with authentication failure """
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
mock_client.connect.side_effect = paramiko.ssh_exception.AuthenticationException('Auth failed')
|
||||
|
||||
with pytest.raises(AnsibleAuthenticationFailure):
|
||||
connection._connect()
|
||||
|
||||
|
||||
def test_any_keys_added(connection):
|
||||
""" Test checking for added host keys """
|
||||
connection.ssh = MagicMock()
|
||||
connection.ssh._host_keys = {
|
||||
'host1': {
|
||||
'ssh-rsa': MagicMock(_added_by_ansible_this_time=True),
|
||||
'ssh-ed25519': MagicMock(_added_by_ansible_this_time=False)
|
||||
}
|
||||
}
|
||||
|
||||
assert connection._any_keys_added() is True
|
||||
|
||||
connection.ssh._host_keys = {
|
||||
'host1': {
|
||||
'ssh-rsa': MagicMock(_added_by_ansible_this_time=False)
|
||||
}
|
||||
}
|
||||
assert connection._any_keys_added() is False
|
||||
|
||||
|
||||
@patch('os.path.exists')
|
||||
@patch('os.stat')
|
||||
@patch('tempfile.NamedTemporaryFile')
|
||||
def test_save_ssh_host_keys(mock_tempfile, mock_stat, mock_exists, connection):
|
||||
""" Test saving SSH host keys """
|
||||
mock_exists.return_value = True
|
||||
mock_stat.return_value = MagicMock(st_mode=0o644, st_uid=1000, st_gid=1000)
|
||||
mock_tempfile.return_value.__enter__.return_value.name = '/tmp/test_keys'
|
||||
|
||||
connection.ssh = MagicMock()
|
||||
connection.ssh._host_keys = {
|
||||
'host1': {
|
||||
'ssh-rsa': MagicMock(
|
||||
get_base64=lambda: 'KEY1',
|
||||
_added_by_ansible_this_time=True
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
mock_open_obj = mock_open()
|
||||
with patch('builtins.open', mock_open_obj):
|
||||
connection._save_ssh_host_keys('/tmp/test_keys')
|
||||
|
||||
mock_open_obj().write.assert_called_with('host1 ssh-rsa KEY1\n')
|
||||
|
||||
|
||||
def test_build_pct_command(connection):
|
||||
""" Test PCT command building with different users """
|
||||
connection.set_option('vmid', '100')
|
||||
|
||||
cmd = connection._build_pct_command('/bin/sh -c "ls -la"')
|
||||
assert cmd == '/usr/sbin/pct exec 100 -- /bin/sh -c "ls -la"'
|
||||
|
||||
connection.set_option('remote_user', 'user')
|
||||
connection.set_option('proxmox_become_method', 'sudo')
|
||||
cmd = connection._build_pct_command('/bin/sh -c "ls -la"')
|
||||
assert cmd == 'sudo /usr/sbin/pct exec 100 -- /bin/sh -c "ls -la"'
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_exec_command_success(mock_ssh, connection):
|
||||
""" Test successful command execution """
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
mock_channel = MagicMock()
|
||||
mock_transport = MagicMock()
|
||||
|
||||
mock_client.get_transport.return_value = mock_transport
|
||||
mock_transport.open_session.return_value = mock_channel
|
||||
mock_channel.recv_exit_status.return_value = 0
|
||||
mock_channel.makefile.return_value = [to_bytes('stdout')]
|
||||
mock_channel.makefile_stderr.return_value = [to_bytes("")]
|
||||
|
||||
connection._connected = True
|
||||
connection.ssh = mock_client
|
||||
|
||||
returncode, stdout, stderr = connection.exec_command('ls -la')
|
||||
|
||||
mock_transport.open_session.assert_called_once()
|
||||
mock_channel.get_pty.assert_called_once()
|
||||
mock_transport.set_keepalive.assert_called_once_with(5)
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_exec_command_pct_not_found(mock_ssh, connection):
|
||||
""" Test command execution when PCT is not found """
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
mock_channel = MagicMock()
|
||||
mock_transport = MagicMock()
|
||||
|
||||
mock_client.get_transport.return_value = mock_transport
|
||||
mock_transport.open_session.return_value = mock_channel
|
||||
mock_channel.recv_exit_status.return_value = 1
|
||||
mock_channel.makefile.return_value = [to_bytes("")]
|
||||
mock_channel.makefile_stderr.return_value = [to_bytes('pct: not found')]
|
||||
|
||||
connection._connected = True
|
||||
connection.ssh = mock_client
|
||||
|
||||
with pytest.raises(AnsibleError, match='pct not found in path of host'):
|
||||
connection.exec_command('ls -la')
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_exec_command_session_open_failure(mock_ssh, connection):
|
||||
""" Test exec_command when session opening fails """
|
||||
mock_client = MagicMock()
|
||||
mock_transport = MagicMock()
|
||||
mock_transport.open_session.side_effect = Exception('Failed to open session')
|
||||
mock_client.get_transport.return_value = mock_transport
|
||||
|
||||
connection._connected = True
|
||||
connection.ssh = mock_client
|
||||
|
||||
with pytest.raises(AnsibleConnectionFailure, match='Failed to open session'):
|
||||
connection.exec_command('test command')
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_exec_command_with_privilege_escalation(mock_ssh, connection):
|
||||
""" Test exec_command with privilege escalation """
|
||||
mock_client = MagicMock()
|
||||
mock_channel = MagicMock()
|
||||
mock_transport = MagicMock()
|
||||
|
||||
mock_client.get_transport.return_value = mock_transport
|
||||
mock_transport.open_session.return_value = mock_channel
|
||||
connection._connected = True
|
||||
connection.ssh = mock_client
|
||||
|
||||
connection.become = MagicMock()
|
||||
connection.become.expect_prompt.return_value = True
|
||||
connection.become.check_success.return_value = False
|
||||
connection.become.check_password_prompt.return_value = True
|
||||
connection.become.get_option.return_value = 'sudo_password'
|
||||
|
||||
mock_channel.recv.return_value = b'[sudo] password:'
|
||||
mock_channel.recv_exit_status.return_value = 0
|
||||
mock_channel.makefile.return_value = [b""]
|
||||
mock_channel.makefile_stderr.return_value = [b""]
|
||||
|
||||
returncode, stdout, stderr = connection.exec_command('sudo test command')
|
||||
|
||||
mock_channel.sendall.assert_called_once_with(b'sudo_password\n')
|
||||
|
||||
|
||||
def test_put_file(connection):
|
||||
""" Test putting a file to the remote system """
|
||||
connection.exec_command = MagicMock()
|
||||
connection.exec_command.return_value = (0, b"", b"")
|
||||
|
||||
with patch('builtins.open', create=True) as mock_open:
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = b'test content'
|
||||
connection.put_file('/local/path', '/remote/path')
|
||||
|
||||
connection.exec_command.assert_called_once_with("/bin/sh -c 'cat > /remote/path'", in_data=b'test content', sudoable=False)
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_put_file_general_error(mock_ssh, connection):
|
||||
""" Test put_file with general error """
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
mock_channel = MagicMock()
|
||||
mock_transport = MagicMock()
|
||||
|
||||
mock_client.get_transport.return_value = mock_transport
|
||||
mock_transport.open_session.return_value = mock_channel
|
||||
mock_channel.recv_exit_status.return_value = 1
|
||||
mock_channel.makefile.return_value = [to_bytes("")]
|
||||
mock_channel.makefile_stderr.return_value = [to_bytes('Some error')]
|
||||
|
||||
connection._connected = True
|
||||
connection.ssh = mock_client
|
||||
|
||||
with pytest.raises(AnsibleError, match='error occurred while putting file from /remote/path to /local/path'):
|
||||
connection.put_file('/remote/path', '/local/path')
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_put_file_cat_not_found(mock_ssh, connection):
|
||||
""" Test command execution when cat is not found """
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
mock_channel = MagicMock()
|
||||
mock_transport = MagicMock()
|
||||
|
||||
mock_client.get_transport.return_value = mock_transport
|
||||
mock_transport.open_session.return_value = mock_channel
|
||||
mock_channel.recv_exit_status.return_value = 1
|
||||
mock_channel.makefile.return_value = [to_bytes("")]
|
||||
mock_channel.makefile_stderr.return_value = [to_bytes('cat: not found')]
|
||||
|
||||
connection._connected = True
|
||||
connection.ssh = mock_client
|
||||
|
||||
with pytest.raises(AnsibleError, match='cat not found in path of container:'):
|
||||
connection.fetch_file('/remote/path', '/local/path')
|
||||
|
||||
|
||||
def test_fetch_file(connection):
|
||||
""" Test fetching a file from the remote system """
|
||||
connection.exec_command = MagicMock()
|
||||
connection.exec_command.return_value = (0, b'test content', b"")
|
||||
|
||||
with patch('builtins.open', create=True) as mock_open:
|
||||
connection.fetch_file('/remote/path', '/local/path')
|
||||
|
||||
connection.exec_command.assert_called_once_with("/bin/sh -c 'cat /remote/path'", sudoable=False)
|
||||
mock_open.assert_called_with('/local/path', 'wb')
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_fetch_file_general_error(mock_ssh, connection):
|
||||
""" Test fetch_file with general error """
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
mock_channel = MagicMock()
|
||||
mock_transport = MagicMock()
|
||||
|
||||
mock_client.get_transport.return_value = mock_transport
|
||||
mock_transport.open_session.return_value = mock_channel
|
||||
mock_channel.recv_exit_status.return_value = 1
|
||||
mock_channel.makefile.return_value = [to_bytes("")]
|
||||
mock_channel.makefile_stderr.return_value = [to_bytes('Some error')]
|
||||
|
||||
connection._connected = True
|
||||
connection.ssh = mock_client
|
||||
|
||||
with pytest.raises(AnsibleError, match='error occurred while fetching file from /remote/path to /local/path'):
|
||||
connection.fetch_file('/remote/path', '/local/path')
|
||||
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_fetch_file_cat_not_found(mock_ssh, connection):
|
||||
""" Test command execution when cat is not found """
|
||||
mock_client = MagicMock()
|
||||
mock_ssh.return_value = mock_client
|
||||
mock_channel = MagicMock()
|
||||
mock_transport = MagicMock()
|
||||
|
||||
mock_client.get_transport.return_value = mock_transport
|
||||
mock_transport.open_session.return_value = mock_channel
|
||||
mock_channel.recv_exit_status.return_value = 1
|
||||
mock_channel.makefile.return_value = [to_bytes("")]
|
||||
mock_channel.makefile_stderr.return_value = [to_bytes('cat: not found')]
|
||||
|
||||
connection._connected = True
|
||||
connection.ssh = mock_client
|
||||
|
||||
with pytest.raises(AnsibleError, match='cat not found in path of container:'):
|
||||
connection.fetch_file('/remote/path', '/local/path')
|
||||
|
||||
|
||||
def test_close(connection):
|
||||
""" Test connection close """
|
||||
mock_ssh = MagicMock()
|
||||
connection.ssh = mock_ssh
|
||||
connection._connected = True
|
||||
|
||||
connection.close()
|
||||
|
||||
assert mock_ssh.close.called, 'ssh.close was not called'
|
||||
assert not connection._connected, 'self._connected is still True'
|
||||
|
||||
|
||||
def test_close_with_lock_file(connection):
|
||||
""" Test close method with lock file creation """
|
||||
connection._any_keys_added = MagicMock(return_value=True)
|
||||
connection._connected = True
|
||||
connection.keyfile = '/tmp/pct-remote-known_hosts-test'
|
||||
connection.set_option('host_key_checking', True)
|
||||
connection.set_option('lock_file_timeout', 5)
|
||||
connection.set_option('record_host_keys', True)
|
||||
connection.ssh = MagicMock()
|
||||
|
||||
lock_file_path = os.path.join(os.path.dirname(connection.keyfile),
|
||||
f'ansible-{os.path.basename(connection.keyfile)}.lock')
|
||||
|
||||
try:
|
||||
connection.close()
|
||||
assert os.path.exists(lock_file_path), 'Lock file was not created'
|
||||
|
||||
lock_stat = os.stat(lock_file_path)
|
||||
assert lock_stat.st_mode & 0o777 == 0o600, 'Incorrect lock file permissions'
|
||||
finally:
|
||||
Path(lock_file_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
@patch('pathlib.Path.unlink')
|
||||
@patch('os.path.exists')
|
||||
def test_close_lock_file_time_out_error_handling(mock_exists, mock_unlink, connection):
|
||||
""" Test close method with lock file timeout error """
|
||||
connection._any_keys_added = MagicMock(return_value=True)
|
||||
connection._connected = True
|
||||
connection._save_ssh_host_keys = MagicMock()
|
||||
connection.keyfile = '/tmp/pct-remote-known_hosts-test'
|
||||
connection.set_option('host_key_checking', True)
|
||||
connection.set_option('lock_file_timeout', 5)
|
||||
connection.set_option('record_host_keys', True)
|
||||
connection.ssh = MagicMock()
|
||||
|
||||
mock_exists.return_value = False
|
||||
matcher = f'writing lock file for {connection.keyfile} ran in to the timeout of {connection.get_option("lock_file_timeout")}s'
|
||||
with pytest.raises(AnsibleError, match=matcher):
|
||||
with patch('os.getuid', return_value=1000), \
|
||||
patch('os.getgid', return_value=1000), \
|
||||
patch('os.chmod'), patch('os.chown'), \
|
||||
patch('os.rename'), \
|
||||
patch.object(FileLock, 'lock_file', side_effect=LockTimeout()):
|
||||
connection.close()
|
||||
|
||||
|
||||
@patch('ansible_collections.community.general.plugins.module_utils._filelock.FileLock.lock_file')
|
||||
@patch('tempfile.NamedTemporaryFile')
|
||||
@patch('os.chmod')
|
||||
@patch('os.chown')
|
||||
@patch('os.rename')
|
||||
@patch('os.path.exists')
|
||||
def test_tempfile_creation_and_move(mock_exists, mock_rename, mock_chown, mock_chmod, mock_tempfile, mock_lock_file, connection):
|
||||
""" Test tempfile creation and move during close """
|
||||
connection._any_keys_added = MagicMock(return_value=True)
|
||||
connection._connected = True
|
||||
connection._save_ssh_host_keys = MagicMock()
|
||||
connection.keyfile = '/tmp/pct-remote-known_hosts-test'
|
||||
connection.set_option('host_key_checking', True)
|
||||
connection.set_option('lock_file_timeout', 5)
|
||||
connection.set_option('record_host_keys', True)
|
||||
connection.ssh = MagicMock()
|
||||
|
||||
mock_exists.return_value = False
|
||||
|
||||
mock_lock_file_instance = MagicMock()
|
||||
mock_lock_file.return_value = mock_lock_file_instance
|
||||
mock_lock_file_instance.__enter__.return_value = None
|
||||
|
||||
mock_tempfile_instance = MagicMock()
|
||||
mock_tempfile_instance.name = '/tmp/mock_tempfile'
|
||||
mock_tempfile.return_value.__enter__.return_value = mock_tempfile_instance
|
||||
|
||||
mode = 0o644
|
||||
uid = 1000
|
||||
gid = 1000
|
||||
key_dir = os.path.dirname(connection.keyfile)
|
||||
|
||||
with patch('os.getuid', return_value=uid), patch('os.getgid', return_value=gid):
|
||||
connection.close()
|
||||
|
||||
connection._save_ssh_host_keys.assert_called_once_with('/tmp/mock_tempfile')
|
||||
mock_chmod.assert_called_once_with('/tmp/mock_tempfile', mode)
|
||||
mock_chown.assert_called_once_with('/tmp/mock_tempfile', uid, gid)
|
||||
mock_rename.assert_called_once_with('/tmp/mock_tempfile', connection.keyfile)
|
||||
mock_tempfile.assert_called_once_with(dir=key_dir, delete=False)
|
||||
|
||||
|
||||
@patch('pathlib.Path.unlink')
|
||||
@patch('tempfile.NamedTemporaryFile')
|
||||
@patch('ansible_collections.community.general.plugins.module_utils._filelock.FileLock.lock_file')
|
||||
@patch('os.path.exists')
|
||||
def test_close_tempfile_error_handling(mock_exists, mock_lock_file, mock_tempfile, mock_unlink, connection):
|
||||
""" Test tempfile creation error """
|
||||
connection._any_keys_added = MagicMock(return_value=True)
|
||||
connection._connected = True
|
||||
connection._save_ssh_host_keys = MagicMock()
|
||||
connection.keyfile = '/tmp/pct-remote-known_hosts-test'
|
||||
connection.set_option('host_key_checking', True)
|
||||
connection.set_option('lock_file_timeout', 5)
|
||||
connection.set_option('record_host_keys', True)
|
||||
connection.ssh = MagicMock()
|
||||
|
||||
mock_exists.return_value = False
|
||||
|
||||
mock_lock_file_instance = MagicMock()
|
||||
mock_lock_file.return_value = mock_lock_file_instance
|
||||
mock_lock_file_instance.__enter__.return_value = None
|
||||
|
||||
mock_tempfile_instance = MagicMock()
|
||||
mock_tempfile_instance.name = '/tmp/mock_tempfile'
|
||||
mock_tempfile.return_value.__enter__.return_value = mock_tempfile_instance
|
||||
|
||||
with pytest.raises(AnsibleError, match='error occurred while writing SSH host keys!'):
|
||||
with patch.object(os, 'chmod', side_effect=Exception()):
|
||||
connection.close()
|
||||
mock_unlink.assert_called_with(missing_ok=True)
|
||||
|
||||
|
||||
@patch('ansible_collections.community.general.plugins.module_utils._filelock.FileLock.lock_file')
|
||||
@patch('os.path.exists')
|
||||
def test_close_with_invalid_host_key(mock_exists, mock_lock_file, connection):
|
||||
""" Test load_system_host_keys on close with InvalidHostKey error """
|
||||
connection._any_keys_added = MagicMock(return_value=True)
|
||||
connection._connected = True
|
||||
connection._save_ssh_host_keys = MagicMock()
|
||||
connection.keyfile = '/tmp/pct-remote-known_hosts-test'
|
||||
connection.set_option('host_key_checking', True)
|
||||
connection.set_option('lock_file_timeout', 5)
|
||||
connection.set_option('record_host_keys', True)
|
||||
connection.ssh = MagicMock()
|
||||
connection.ssh.load_system_host_keys.side_effect = paramiko.hostkeys.InvalidHostKey(
|
||||
"Bad Line!", Exception('Something crashed!'))
|
||||
|
||||
mock_exists.return_value = False
|
||||
|
||||
mock_lock_file_instance = MagicMock()
|
||||
mock_lock_file.return_value = mock_lock_file_instance
|
||||
mock_lock_file_instance.__enter__.return_value = None
|
||||
|
||||
with pytest.raises(AnsibleConnectionFailure, match="Invalid host key: Bad Line!"):
|
||||
connection.close()
|
||||
|
||||
|
||||
def test_reset(connection):
|
||||
""" Test connection reset """
|
||||
connection._connected = True
|
||||
connection.close = MagicMock()
|
||||
connection._connect = MagicMock()
|
||||
|
||||
connection.reset()
|
||||
|
||||
connection.close.assert_called_once()
|
||||
connection._connect.assert_called_once()
|
||||
|
||||
connection._connected = False
|
||||
connection.reset()
|
||||
assert connection.close.call_count == 1
|
|
@ -1,786 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Jeffrey van Pelt <jeff@vanpelt.one>
|
||||
# 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
|
||||
#
|
||||
# The API responses used in these tests were recorded from PVE version 6.2.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.inventory.data import InventoryData
|
||||
from ansible_collections.community.general.plugins.inventory.proxmox import InventoryModule
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def inventory():
|
||||
r = InventoryModule()
|
||||
r.inventory = InventoryData()
|
||||
return r
|
||||
|
||||
|
||||
def test_verify_file(tmp_path, inventory):
|
||||
file = tmp_path / "foobar.proxmox.yml"
|
||||
file.touch()
|
||||
assert inventory.verify_file(str(file)) is True
|
||||
|
||||
|
||||
def test_verify_file_bad_config(inventory):
|
||||
assert inventory.verify_file('foobar.proxmox.yml') is False
|
||||
|
||||
|
||||
def get_auth():
|
||||
return True
|
||||
|
||||
|
||||
# NOTE: when updating/adding replies to this function,
|
||||
# be sure to only add only the _contents_ of the 'data' dict in the API reply
|
||||
def get_json(url, ignore_errors=None):
|
||||
if url == "https://localhost:8006/api2/json/nodes":
|
||||
# _get_nodes
|
||||
return [{"type": "node",
|
||||
"cpu": 0.01,
|
||||
"maxdisk": 500,
|
||||
"mem": 500,
|
||||
"node": "testnode",
|
||||
"id": "node/testnode",
|
||||
"maxcpu": 1,
|
||||
"status": "online",
|
||||
"ssl_fingerprint": "xx",
|
||||
"disk": 1000,
|
||||
"maxmem": 1000,
|
||||
"uptime": 10000,
|
||||
"level": ""},
|
||||
{"type": "node",
|
||||
"node": "testnode2",
|
||||
"id": "node/testnode2",
|
||||
"status": "offline",
|
||||
"ssl_fingerprint": "yy"}]
|
||||
elif url == "https://localhost:8006/api2/json/pools":
|
||||
# _get_pools
|
||||
return [{"poolid": "test"}]
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/lxc":
|
||||
# _get_lxc_per_node
|
||||
return [{"cpus": 1,
|
||||
"name": "test-lxc",
|
||||
"cpu": 0.01,
|
||||
"diskwrite": 0,
|
||||
"lock": "",
|
||||
"maxmem": 1000,
|
||||
"template": "",
|
||||
"diskread": 0,
|
||||
"mem": 1000,
|
||||
"swap": 0,
|
||||
"type": "lxc",
|
||||
"maxswap": 0,
|
||||
"maxdisk": "1000",
|
||||
"netout": 1000,
|
||||
"pid": "1000",
|
||||
"netin": 1000,
|
||||
"status": "running",
|
||||
"vmid": "100",
|
||||
"disk": "1000",
|
||||
"uptime": 1000}]
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu":
|
||||
# _get_qemu_per_node
|
||||
return [{"name": "test-qemu",
|
||||
"cpus": 1,
|
||||
"mem": 1000,
|
||||
"template": "",
|
||||
"diskread": 0,
|
||||
"cpu": 0.01,
|
||||
"maxmem": 1000,
|
||||
"diskwrite": 0,
|
||||
"netout": 1000,
|
||||
"pid": "1001",
|
||||
"netin": 1000,
|
||||
"maxdisk": 1000,
|
||||
"vmid": "101",
|
||||
"uptime": 1000,
|
||||
"disk": 0,
|
||||
"status": "running"},
|
||||
{"name": "test-qemu-windows",
|
||||
"cpus": 1,
|
||||
"mem": 1000,
|
||||
"template": "",
|
||||
"diskread": 0,
|
||||
"cpu": 0.01,
|
||||
"maxmem": 1000,
|
||||
"diskwrite": 0,
|
||||
"netout": 1000,
|
||||
"pid": "1001",
|
||||
"netin": 1000,
|
||||
"maxdisk": 1000,
|
||||
"vmid": "102",
|
||||
"uptime": 1000,
|
||||
"disk": 0,
|
||||
"status": "running"},
|
||||
{"name": "test-qemu-multi-nic",
|
||||
"cpus": 1,
|
||||
"mem": 1000,
|
||||
"template": "",
|
||||
"diskread": 0,
|
||||
"cpu": 0.01,
|
||||
"maxmem": 1000,
|
||||
"diskwrite": 0,
|
||||
"netout": 1000,
|
||||
"pid": "1001",
|
||||
"netin": 1000,
|
||||
"maxdisk": 1000,
|
||||
"vmid": "103",
|
||||
"uptime": 1000,
|
||||
"disk": 0,
|
||||
"status": "running"},
|
||||
{"name": "test-qemu-template",
|
||||
"cpus": 1,
|
||||
"mem": 0,
|
||||
"template": 1,
|
||||
"diskread": 0,
|
||||
"cpu": 0,
|
||||
"maxmem": 1000,
|
||||
"diskwrite": 0,
|
||||
"netout": 0,
|
||||
"pid": "1001",
|
||||
"netin": 0,
|
||||
"maxdisk": 1000,
|
||||
"vmid": "9001",
|
||||
"uptime": 0,
|
||||
"disk": 0,
|
||||
"status": "stopped"}]
|
||||
elif url == "https://localhost:8006/api2/json/pools/test":
|
||||
# _get_members_per_pool
|
||||
return {"members": [{"uptime": 1000,
|
||||
"template": 0,
|
||||
"id": "qemu/101",
|
||||
"mem": 1000,
|
||||
"status": "running",
|
||||
"cpu": 0.01,
|
||||
"maxmem": 1000,
|
||||
"diskwrite": 1000,
|
||||
"name": "test-qemu",
|
||||
"netout": 1000,
|
||||
"netin": 1000,
|
||||
"vmid": 101,
|
||||
"node": "testnode",
|
||||
"maxcpu": 1,
|
||||
"type": "qemu",
|
||||
"maxdisk": 1000,
|
||||
"disk": 0,
|
||||
"diskread": 1000}]}
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/network":
|
||||
# _get_node_ip
|
||||
return [{"families": ["inet"],
|
||||
"priority": 3,
|
||||
"active": 1,
|
||||
"cidr": "10.1.1.2/24",
|
||||
"iface": "eth0",
|
||||
"method": "static",
|
||||
"exists": 1,
|
||||
"type": "eth",
|
||||
"netmask": "24",
|
||||
"gateway": "10.1.1.1",
|
||||
"address": "10.1.1.2",
|
||||
"method6": "manual",
|
||||
"autostart": 1},
|
||||
{"method6": "manual",
|
||||
"autostart": 1,
|
||||
"type": "OVSPort",
|
||||
"exists": 1,
|
||||
"method": "manual",
|
||||
"iface": "eth1",
|
||||
"ovs_bridge": "vmbr0",
|
||||
"active": 1,
|
||||
"families": ["inet"],
|
||||
"priority": 5,
|
||||
"ovs_type": "OVSPort"},
|
||||
{"type": "OVSBridge",
|
||||
"method": "manual",
|
||||
"iface": "vmbr0",
|
||||
"families": ["inet"],
|
||||
"priority": 4,
|
||||
"ovs_ports": "eth1",
|
||||
"ovs_type": "OVSBridge",
|
||||
"method6": "manual",
|
||||
"autostart": 1,
|
||||
"active": 1}]
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/lxc/100/config":
|
||||
# _get_vm_config (lxc)
|
||||
return {
|
||||
"console": 1,
|
||||
"rootfs": "local-lvm:vm-100-disk-0,size=4G",
|
||||
"cmode": "tty",
|
||||
"description": "A testnode",
|
||||
"cores": 1,
|
||||
"hostname": "test-lxc",
|
||||
"arch": "amd64",
|
||||
"tty": 2,
|
||||
"swap": 0,
|
||||
"cpulimit": "0",
|
||||
"net0": "name=eth0,bridge=vmbr0,gw=10.1.1.1,hwaddr=FF:FF:FF:FF:FF:FF,ip=10.1.1.3/24,type=veth",
|
||||
"ostype": "ubuntu",
|
||||
"digest": "123456789abcdef0123456789abcdef01234567890",
|
||||
"protection": 0,
|
||||
"memory": 1000,
|
||||
"onboot": 0,
|
||||
"cpuunits": 1024,
|
||||
"tags": "one, two, three",
|
||||
}
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/101/config":
|
||||
# _get_vm_config (qemu)
|
||||
return {
|
||||
"tags": "one, two, three",
|
||||
"cores": 1,
|
||||
"ide2": "none,media=cdrom",
|
||||
"memory": 1000,
|
||||
"kvm": 1,
|
||||
"digest": "0123456789abcdef0123456789abcdef0123456789",
|
||||
"description": "A test qemu",
|
||||
"sockets": 1,
|
||||
"onboot": 1,
|
||||
"vmgenid": "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
||||
"numa": 0,
|
||||
"bootdisk": "scsi0",
|
||||
"cpu": "host",
|
||||
"name": "test-qemu",
|
||||
"ostype": "l26",
|
||||
"hotplug": "network,disk,usb",
|
||||
"scsi0": "local-lvm:vm-101-disk-0,size=8G",
|
||||
"net0": "virtio=ff:ff:ff:ff:ff:ff,bridge=vmbr0,firewall=1",
|
||||
"agent": "1,fstrim_cloned_disks=1",
|
||||
"bios": "seabios",
|
||||
"ide0": "local-lvm:vm-101-cloudinit,media=cdrom,size=4M",
|
||||
"boot": "cdn",
|
||||
"scsihw": "virtio-scsi-pci",
|
||||
"smbios1": "uuid=ffffffff-ffff-ffff-ffff-ffffffffffff"
|
||||
}
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/102/config":
|
||||
# _get_vm_config (qemu)
|
||||
return {
|
||||
"numa": 0,
|
||||
"digest": "460add1531a7068d2ae62d54f67e8fb9493dece9",
|
||||
"ide2": "none,media=cdrom",
|
||||
"bootdisk": "sata0",
|
||||
"name": "test-qemu-windows",
|
||||
"balloon": 0,
|
||||
"cpulimit": "4",
|
||||
"agent": "1",
|
||||
"cores": 6,
|
||||
"sata0": "storage:vm-102-disk-0,size=100G",
|
||||
"memory": 10240,
|
||||
"smbios1": "uuid=127301fc-0122-48d5-8fc5-c04fa78d8146",
|
||||
"scsihw": "virtio-scsi-pci",
|
||||
"sockets": 1,
|
||||
"ostype": "win8",
|
||||
"net0": "virtio=ff:ff:ff:ff:ff:ff,bridge=vmbr0",
|
||||
"onboot": 1
|
||||
}
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/103/config":
|
||||
# _get_vm_config (qemu)
|
||||
return {
|
||||
'scsi1': 'storage:vm-103-disk-3,size=30G',
|
||||
'sockets': 1,
|
||||
'memory': 8192,
|
||||
'ostype': 'l26',
|
||||
'scsihw': 'virtio-scsi-pci',
|
||||
"net0": "virtio=ff:ff:ff:ff:ff:ff,bridge=vmbr0",
|
||||
"net1": "virtio=ff:ff:ff:ff:ff:ff,bridge=vmbr1",
|
||||
'bootdisk': 'scsi0',
|
||||
'scsi0': 'storage:vm-103-disk-0,size=10G',
|
||||
'name': 'test-qemu-multi-nic',
|
||||
'cores': 4,
|
||||
'digest': '51b7599f869b9a3f564804a0aed290f3de803292',
|
||||
'smbios1': 'uuid=863b31c3-42ca-4a92-aed7-4111f342f70a',
|
||||
'agent': '1,type=virtio',
|
||||
'ide2': 'none,media=cdrom',
|
||||
'balloon': 0,
|
||||
'numa': 0,
|
||||
'scsi2': 'storage:vm-103-disk-2,size=10G',
|
||||
'serial0': 'socket',
|
||||
'vmgenid': 'ddfb79b2-b484-4d66-88e7-6e76f2d1be77',
|
||||
'onboot': 1,
|
||||
'tablet': 0
|
||||
}
|
||||
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/101/agent/network-get-interfaces":
|
||||
# _get_agent_network_interfaces
|
||||
return {"result": [
|
||||
{
|
||||
"hardware-address": "00:00:00:00:00:00",
|
||||
"ip-addresses": [
|
||||
{
|
||||
"prefix": 8,
|
||||
"ip-address-type": "ipv4",
|
||||
"ip-address": "127.0.0.1"
|
||||
},
|
||||
{
|
||||
"ip-address-type": "ipv6",
|
||||
"ip-address": "::1",
|
||||
"prefix": 128
|
||||
}],
|
||||
"statistics": {
|
||||
"rx-errs": 0,
|
||||
"rx-bytes": 163244,
|
||||
"rx-packets": 1623,
|
||||
"rx-dropped": 0,
|
||||
"tx-dropped": 0,
|
||||
"tx-packets": 1623,
|
||||
"tx-bytes": 163244,
|
||||
"tx-errs": 0},
|
||||
"name": "lo"},
|
||||
{
|
||||
"statistics": {
|
||||
"rx-packets": 4025,
|
||||
"rx-dropped": 12,
|
||||
"rx-bytes": 324105,
|
||||
"rx-errs": 0,
|
||||
"tx-errs": 0,
|
||||
"tx-bytes": 368860,
|
||||
"tx-packets": 3479,
|
||||
"tx-dropped": 0},
|
||||
"name": "eth0",
|
||||
"ip-addresses": [
|
||||
{
|
||||
"prefix": 24,
|
||||
"ip-address-type": "ipv4",
|
||||
"ip-address": "10.1.2.3"
|
||||
},
|
||||
{
|
||||
"prefix": 64,
|
||||
"ip-address": "fd8c:4687:e88d:1be3:5b70:7b88:c79c:293",
|
||||
"ip-address-type": "ipv6"
|
||||
}],
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff"
|
||||
},
|
||||
{
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff",
|
||||
"ip-addresses": [
|
||||
{
|
||||
"prefix": 16,
|
||||
"ip-address": "10.10.2.3",
|
||||
"ip-address-type": "ipv4"
|
||||
}],
|
||||
"name": "docker0",
|
||||
"statistics": {
|
||||
"rx-bytes": 0,
|
||||
"rx-errs": 0,
|
||||
"rx-dropped": 0,
|
||||
"rx-packets": 0,
|
||||
"tx-packets": 0,
|
||||
"tx-dropped": 0,
|
||||
"tx-errs": 0,
|
||||
"tx-bytes": 0
|
||||
}}]}
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/102/agent/network-get-interfaces":
|
||||
# _get_agent_network_interfaces
|
||||
return {"result": {'error': {'desc': 'this feature or command is not currently supported', 'class': 'Unsupported'}}}
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/103/agent/network-get-interfaces":
|
||||
# _get_agent_network_interfaces
|
||||
return {
|
||||
"result": [
|
||||
{
|
||||
"statistics": {
|
||||
"tx-errs": 0,
|
||||
"rx-errs": 0,
|
||||
"rx-dropped": 0,
|
||||
"tx-bytes": 48132932372,
|
||||
"tx-dropped": 0,
|
||||
"rx-bytes": 48132932372,
|
||||
"tx-packets": 178578980,
|
||||
"rx-packets": 178578980
|
||||
},
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff",
|
||||
"ip-addresses": [
|
||||
{
|
||||
"ip-address-type": "ipv4",
|
||||
"prefix": 8,
|
||||
"ip-address": "127.0.0.1"
|
||||
}
|
||||
],
|
||||
"name": "lo"
|
||||
},
|
||||
{
|
||||
"name": "eth0",
|
||||
"ip-addresses": [
|
||||
{
|
||||
"ip-address-type": "ipv4",
|
||||
"prefix": 24,
|
||||
"ip-address": "172.16.0.143"
|
||||
}
|
||||
],
|
||||
"statistics": {
|
||||
"rx-errs": 0,
|
||||
"tx-errs": 0,
|
||||
"rx-packets": 660028,
|
||||
"tx-packets": 304599,
|
||||
"tx-dropped": 0,
|
||||
"rx-bytes": 1846743499,
|
||||
"tx-bytes": 1287844926,
|
||||
"rx-dropped": 0
|
||||
},
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff"
|
||||
},
|
||||
{
|
||||
"name": "eth1",
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff",
|
||||
"statistics": {
|
||||
"rx-bytes": 235717091946,
|
||||
"tx-dropped": 0,
|
||||
"rx-dropped": 0,
|
||||
"tx-bytes": 123411636251,
|
||||
"rx-packets": 540431277,
|
||||
"tx-packets": 468411864,
|
||||
"rx-errs": 0,
|
||||
"tx-errs": 0
|
||||
},
|
||||
"ip-addresses": [
|
||||
{
|
||||
"ip-address": "10.0.0.133",
|
||||
"prefix": 24,
|
||||
"ip-address-type": "ipv4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "docker0",
|
||||
"ip-addresses": [
|
||||
{
|
||||
"ip-address": "172.17.0.1",
|
||||
"prefix": 16,
|
||||
"ip-address-type": "ipv4"
|
||||
}
|
||||
],
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff",
|
||||
"statistics": {
|
||||
"rx-errs": 0,
|
||||
"tx-errs": 0,
|
||||
"rx-packets": 0,
|
||||
"tx-packets": 0,
|
||||
"tx-dropped": 0,
|
||||
"rx-bytes": 0,
|
||||
"rx-dropped": 0,
|
||||
"tx-bytes": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff",
|
||||
"name": "datapath"
|
||||
},
|
||||
{
|
||||
"name": "weave",
|
||||
"ip-addresses": [
|
||||
{
|
||||
"ip-address": "10.42.0.1",
|
||||
"ip-address-type": "ipv4",
|
||||
"prefix": 16
|
||||
}
|
||||
],
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff",
|
||||
"statistics": {
|
||||
"rx-bytes": 127289123306,
|
||||
"tx-dropped": 0,
|
||||
"rx-dropped": 0,
|
||||
"tx-bytes": 43827573343,
|
||||
"rx-packets": 132750542,
|
||||
"tx-packets": 74218762,
|
||||
"rx-errs": 0,
|
||||
"tx-errs": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "vethwe-datapath",
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff"
|
||||
},
|
||||
{
|
||||
"name": "vethwe-bridge",
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff"
|
||||
},
|
||||
{
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff",
|
||||
"name": "vxlan-6784"
|
||||
},
|
||||
{
|
||||
"name": "vethwepl0dfe1fe",
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff"
|
||||
},
|
||||
{
|
||||
"name": "vethweplf1e7715",
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff"
|
||||
},
|
||||
{
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff",
|
||||
"name": "vethwepl9d244a1"
|
||||
},
|
||||
{
|
||||
"hardware-address": "ff:ff:ff:ff:ff:ff",
|
||||
"name": "vethwepl2ca477b"
|
||||
},
|
||||
{
|
||||
"name": "nomacorip",
|
||||
}
|
||||
]
|
||||
}
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/lxc/100/status/current":
|
||||
# _get_vm_status (lxc)
|
||||
return {
|
||||
"swap": 0,
|
||||
"name": "test-lxc",
|
||||
"diskread": 0,
|
||||
"vmid": 100,
|
||||
"diskwrite": 0,
|
||||
"pid": 9000,
|
||||
"mem": 89980928,
|
||||
"netin": 1950776396424,
|
||||
"disk": 4998168576,
|
||||
"cpu": 0.00163430613110039,
|
||||
"type": "lxc",
|
||||
"uptime": 6793736,
|
||||
"maxmem": 1073741824,
|
||||
"status": "running",
|
||||
"cpus": "1",
|
||||
"ha": {
|
||||
"group": 'null',
|
||||
"state": "started",
|
||||
"managed": 1
|
||||
},
|
||||
"maxdisk": 3348329267200,
|
||||
"netout": 1947793356037,
|
||||
"maxswap": 1073741824
|
||||
}
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/101/status/current":
|
||||
# _get_vm_status (qemu)
|
||||
return {
|
||||
"status": "stopped",
|
||||
"uptime": 0,
|
||||
"maxmem": 5364514816,
|
||||
"maxdisk": 34359738368,
|
||||
"netout": 0,
|
||||
"cpus": 2,
|
||||
"ha": {
|
||||
"managed": 0
|
||||
},
|
||||
"diskread": 0,
|
||||
"vmid": 101,
|
||||
"diskwrite": 0,
|
||||
"name": "test-qemu",
|
||||
"cpu": 0,
|
||||
"disk": 0,
|
||||
"netin": 0,
|
||||
"mem": 0,
|
||||
"qmpstatus": "stopped"
|
||||
}
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/102/status/current":
|
||||
# _get_vm_status (qemu)
|
||||
return {
|
||||
"status": "stopped",
|
||||
"uptime": 0,
|
||||
"maxmem": 5364514816,
|
||||
"maxdisk": 34359738368,
|
||||
"netout": 0,
|
||||
"cpus": 2,
|
||||
"ha": {
|
||||
"managed": 0
|
||||
},
|
||||
"diskread": 0,
|
||||
"vmid": 102,
|
||||
"diskwrite": 0,
|
||||
"name": "test-qemu-windows",
|
||||
"cpu": 0,
|
||||
"disk": 0,
|
||||
"netin": 0,
|
||||
"mem": 0,
|
||||
"qmpstatus": "prelaunch"
|
||||
}
|
||||
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/103/status/current":
|
||||
# _get_vm_status (qemu)
|
||||
return {
|
||||
"status": "stopped",
|
||||
"uptime": 0,
|
||||
"maxmem": 5364514816,
|
||||
"maxdisk": 34359738368,
|
||||
"netout": 0,
|
||||
"cpus": 2,
|
||||
"ha": {
|
||||
"managed": 0
|
||||
},
|
||||
"diskread": 0,
|
||||
"vmid": 103,
|
||||
"diskwrite": 0,
|
||||
"name": "test-qemu-multi-nic",
|
||||
"cpu": 0,
|
||||
"disk": 0,
|
||||
"netin": 0,
|
||||
"mem": 0,
|
||||
"qmpstatus": "paused"
|
||||
}
|
||||
|
||||
|
||||
def get_vm_snapshots(node, properties, vmtype, vmid, name):
|
||||
return [
|
||||
{"description": "",
|
||||
"name": "clean",
|
||||
"snaptime": 1000,
|
||||
"vmstate": 0
|
||||
},
|
||||
{"name": "current",
|
||||
"digest": "1234689abcdf",
|
||||
"running": 0,
|
||||
"description": "You are here!",
|
||||
"parent": "clean"
|
||||
}]
|
||||
|
||||
|
||||
def get_option(opts):
|
||||
def fn(option):
|
||||
default = opts.get('default', False)
|
||||
return opts.get(option, default)
|
||||
return fn
|
||||
|
||||
|
||||
def test_populate(inventory, mocker):
|
||||
# module settings
|
||||
inventory.proxmox_user = 'root@pam'
|
||||
inventory.proxmox_password = 'password'
|
||||
inventory.proxmox_url = 'https://localhost:8006'
|
||||
inventory.group_prefix = 'proxmox_'
|
||||
inventory.facts_prefix = 'proxmox_'
|
||||
inventory.strict = False
|
||||
inventory.exclude_nodes = False
|
||||
|
||||
opts = {
|
||||
'group_prefix': 'proxmox_',
|
||||
'facts_prefix': 'proxmox_',
|
||||
'want_facts': True,
|
||||
'want_proxmox_nodes_ansible_host': True,
|
||||
'qemu_extended_statuses': True,
|
||||
'exclude_nodes': False
|
||||
}
|
||||
|
||||
# bypass authentication and API fetch calls
|
||||
inventory._get_auth = mocker.MagicMock(side_effect=get_auth)
|
||||
inventory._get_json = mocker.MagicMock(side_effect=get_json)
|
||||
inventory._get_vm_snapshots = mocker.MagicMock(side_effect=get_vm_snapshots)
|
||||
inventory.get_option = mocker.MagicMock(side_effect=get_option(opts))
|
||||
inventory._can_add_host = mocker.MagicMock(return_value=True)
|
||||
inventory._populate()
|
||||
|
||||
# get different hosts
|
||||
host_qemu = inventory.inventory.get_host('test-qemu')
|
||||
host_qemu_windows = inventory.inventory.get_host('test-qemu-windows')
|
||||
host_qemu_multi_nic = inventory.inventory.get_host('test-qemu-multi-nic')
|
||||
host_qemu_template = inventory.inventory.get_host('test-qemu-template')
|
||||
host_lxc = inventory.inventory.get_host('test-lxc')
|
||||
|
||||
# check if qemu-test is in the proxmox_pool_test group
|
||||
assert 'proxmox_pool_test' in inventory.inventory.groups
|
||||
group_qemu = inventory.inventory.groups['proxmox_pool_test']
|
||||
assert group_qemu.hosts == [host_qemu]
|
||||
|
||||
# check if qemu-test has eth0 interface in agent_interfaces fact
|
||||
assert 'eth0' in [d['name'] for d in host_qemu.get_vars()['proxmox_agent_interfaces']]
|
||||
|
||||
# check if qemu-multi-nic has multiple network interfaces
|
||||
for iface_name in ['eth0', 'eth1', 'weave']:
|
||||
assert iface_name in [d['name'] for d in host_qemu_multi_nic.get_vars()['proxmox_agent_interfaces']]
|
||||
|
||||
# check if interface with no mac-address or ip-address defaults correctly
|
||||
assert [iface for iface in host_qemu_multi_nic.get_vars()['proxmox_agent_interfaces']
|
||||
if iface['name'] == 'nomacorip'
|
||||
and iface['mac-address'] == ''
|
||||
and iface['ip-addresses'] == []
|
||||
]
|
||||
|
||||
# check to make sure qemu-windows doesn't have proxmox_agent_interfaces
|
||||
assert "proxmox_agent_interfaces" not in host_qemu_windows.get_vars()
|
||||
|
||||
# check if lxc-test has been discovered correctly
|
||||
group_lxc = inventory.inventory.groups['proxmox_all_lxc']
|
||||
assert group_lxc.hosts == [host_lxc]
|
||||
|
||||
# check if qemu template is not present
|
||||
assert host_qemu_template is None
|
||||
|
||||
# check that offline node is in inventory
|
||||
assert inventory.inventory.get_host('testnode2')
|
||||
|
||||
# make sure that ['prelaunch', 'paused'] are in the group list
|
||||
for group in ['paused', 'prelaunch']:
|
||||
assert ('%sall_%s' % (inventory.group_prefix, group)) in inventory.inventory.groups
|
||||
|
||||
# check if qemu-windows is in the prelaunch group
|
||||
group_prelaunch = inventory.inventory.groups['proxmox_all_prelaunch']
|
||||
assert group_prelaunch.hosts == [host_qemu_windows]
|
||||
|
||||
# check if qemu-multi-nic is in the paused group
|
||||
group_paused = inventory.inventory.groups['proxmox_all_paused']
|
||||
assert group_paused.hosts == [host_qemu_multi_nic]
|
||||
|
||||
|
||||
def test_populate_missing_qemu_extended_groups(inventory, mocker):
|
||||
# module settings
|
||||
inventory.proxmox_user = 'root@pam'
|
||||
inventory.proxmox_password = 'password'
|
||||
inventory.proxmox_url = 'https://localhost:8006'
|
||||
inventory.group_prefix = 'proxmox_'
|
||||
inventory.facts_prefix = 'proxmox_'
|
||||
inventory.strict = False
|
||||
inventory.exclude_nodes = False
|
||||
|
||||
opts = {
|
||||
'group_prefix': 'proxmox_',
|
||||
'facts_prefix': 'proxmox_',
|
||||
'want_facts': True,
|
||||
'want_proxmox_nodes_ansible_host': True,
|
||||
'qemu_extended_statuses': False,
|
||||
'exclude_nodes': False
|
||||
}
|
||||
|
||||
# bypass authentication and API fetch calls
|
||||
inventory._get_auth = mocker.MagicMock(side_effect=get_auth)
|
||||
inventory._get_json = mocker.MagicMock(side_effect=get_json)
|
||||
inventory._get_vm_snapshots = mocker.MagicMock(side_effect=get_vm_snapshots)
|
||||
inventory.get_option = mocker.MagicMock(side_effect=get_option(opts))
|
||||
inventory._can_add_host = mocker.MagicMock(return_value=True)
|
||||
inventory._populate()
|
||||
|
||||
# make sure that ['prelaunch', 'paused'] are not in the group list
|
||||
for group in ['paused', 'prelaunch']:
|
||||
assert ('%sall_%s' % (inventory.group_prefix, group)) not in inventory.inventory.groups
|
||||
|
||||
|
||||
def test_populate_exclude_nodes(inventory, mocker):
|
||||
# module settings
|
||||
inventory.proxmox_user = 'root@pam'
|
||||
inventory.proxmox_password = 'password'
|
||||
inventory.proxmox_url = 'https://localhost:8006'
|
||||
inventory.group_prefix = 'proxmox_'
|
||||
inventory.facts_prefix = 'proxmox_'
|
||||
inventory.strict = False
|
||||
inventory.exclude_nodes = True
|
||||
|
||||
opts = {
|
||||
'group_prefix': 'proxmox_',
|
||||
'facts_prefix': 'proxmox_',
|
||||
'want_facts': True,
|
||||
'want_proxmox_nodes_ansible_host': True,
|
||||
'qemu_extended_statuses': False,
|
||||
'exclude_nodes': True
|
||||
}
|
||||
|
||||
# bypass authentication and API fetch calls
|
||||
inventory._get_auth = mocker.MagicMock(side_effect=get_auth)
|
||||
inventory._get_json = mocker.MagicMock(side_effect=get_json)
|
||||
inventory._get_vm_snapshots = mocker.MagicMock(side_effect=get_vm_snapshots)
|
||||
inventory.get_option = mocker.MagicMock(side_effect=get_option(opts))
|
||||
inventory._can_add_host = mocker.MagicMock(return_value=True)
|
||||
inventory._populate()
|
||||
|
||||
# make sure that nodes are not in the inventory
|
||||
for node in ['testnode', 'testnode2']:
|
||||
assert node not in inventory.inventory.hosts
|
||||
# make sure that nodes group is absent
|
||||
assert ('%s_nodes' % (inventory.group_prefix)) not in inventory.inventory.groups
|
||||
# make sure that nodes are not in the "ungrouped" group
|
||||
for node in ['testnode', 'testnode2']:
|
||||
assert node not in inventory.inventory.get_groups_dict()["ungrouped"]
|
|
@ -1,374 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2019, Ansible Project
|
||||
# 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
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
import \
|
||||
ansible_collections.community.general.plugins.module_utils.proxmox as proxmox_utils
|
||||
from ansible_collections.community.general.plugins.modules import proxmox_backup
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
|
||||
AnsibleExitJson, AnsibleFailJson, set_module_args, ModuleTestCase)
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
proxmoxer = pytest.importorskip('proxmoxer')
|
||||
|
||||
|
||||
MINIMAL_PERMISSIONS = {
|
||||
'/sdn/zones': {'Datastore.AllocateSpace': 1, 'Datastore.Audit': 1},
|
||||
'/nodes': {'Datastore.AllocateSpace': 1, 'Datastore.Audit': 1},
|
||||
'/sdn': {'Datastore.AllocateSpace': 1, 'Datastore.Audit': 1},
|
||||
'/vms': {'VM.Audit': 1,
|
||||
'Sys.Audit': 1,
|
||||
'Mapping.Audit': 1,
|
||||
'VM.Backup': 1,
|
||||
'Datastore.Audit': 1,
|
||||
'SDN.Audit': 1,
|
||||
'Pool.Audit': 1},
|
||||
'/': {'Datastore.Audit': 1, 'Datastore.AllocateSpace': 1},
|
||||
'/storage/local-zfs': {'Datastore.AllocateSpace': 1,
|
||||
'Datastore.Audit': 1},
|
||||
'/storage': {'Datastore.AllocateSpace': 1, 'Datastore.Audit': 1},
|
||||
'/access': {'Datastore.AllocateSpace': 1, 'Datastore.Audit': 1},
|
||||
'/vms/101': {'VM.Backup': 1,
|
||||
'Mapping.Audit': 1,
|
||||
'Datastore.AllocateSpace': 0,
|
||||
'Sys.Audit': 1,
|
||||
'VM.Audit': 1,
|
||||
'SDN.Audit': 1,
|
||||
'Pool.Audit': 1,
|
||||
'Datastore.Audit': 1},
|
||||
'/vms/100': {'VM.Backup': 1,
|
||||
'Mapping.Audit': 1,
|
||||
'Datastore.AllocateSpace': 0,
|
||||
'Sys.Audit': 1,
|
||||
'VM.Audit': 1,
|
||||
'SDN.Audit': 1,
|
||||
'Pool.Audit': 1,
|
||||
'Datastore.Audit': 1},
|
||||
'/pool': {'Datastore.Audit': 1, 'Datastore.AllocateSpace': 1}, }
|
||||
|
||||
STORAGE = [{'type': 'pbs',
|
||||
'username': 'test@pbs',
|
||||
'datastore': 'Backup-Pool',
|
||||
'server': '10.0.0.1',
|
||||
'shared': 1,
|
||||
'fingerprint': '94:fd:ac:e7:d5:36:0e:11:5b:23:05:40:d2:a4:e1:8a:c1:52:41:01:07:28:c0:4d:c5:ee:df:7f:7c:03:ab:41',
|
||||
'prune-backups': 'keep-all=1',
|
||||
'storage': 'backup',
|
||||
'content': 'backup',
|
||||
'digest': 'ca46a68d7699de061c139d714892682ea7c9d681'},
|
||||
{'nodes': 'node1,node2,node3',
|
||||
'sparse': 1,
|
||||
'type': 'zfspool',
|
||||
'content': 'rootdir,images',
|
||||
'digest': 'ca46a68d7699de061c139d714892682ea7c9d681',
|
||||
'pool': 'rpool/data',
|
||||
'storage': 'local-zfs'}]
|
||||
|
||||
|
||||
VMS = [{"diskwrite": 0,
|
||||
"vmid": 100,
|
||||
"node": "node1",
|
||||
"id": "lxc/100",
|
||||
"maxdisk": 10000,
|
||||
"template": 0,
|
||||
"disk": 10000,
|
||||
"uptime": 10000,
|
||||
"maxmem": 10000,
|
||||
"maxcpu": 1,
|
||||
"netin": 10000,
|
||||
"type": "lxc",
|
||||
"netout": 10000,
|
||||
"mem": 10000,
|
||||
"diskread": 10000,
|
||||
"cpu": 0.01,
|
||||
"name": "test-lxc",
|
||||
"status": "running"},
|
||||
{"diskwrite": 0,
|
||||
"vmid": 101,
|
||||
"node": "node2",
|
||||
"id": "kvm/101",
|
||||
"maxdisk": 10000,
|
||||
"template": 0,
|
||||
"disk": 10000,
|
||||
"uptime": 10000,
|
||||
"maxmem": 10000,
|
||||
"maxcpu": 1,
|
||||
"netin": 10000,
|
||||
"type": "lxc",
|
||||
"netout": 10000,
|
||||
"mem": 10000,
|
||||
"diskread": 10000,
|
||||
"cpu": 0.01,
|
||||
"name": "test-kvm",
|
||||
"status": "running"}
|
||||
]
|
||||
|
||||
NODES = [{'level': '',
|
||||
'type': 'node',
|
||||
'node': 'node1',
|
||||
'status': 'online',
|
||||
'id': 'node/node1',
|
||||
'cgroup-mode': 2},
|
||||
{'status': 'online',
|
||||
'id': 'node/node2',
|
||||
'cgroup-mode': 2,
|
||||
'level': '',
|
||||
'node': 'node2',
|
||||
'type': 'node'},
|
||||
{'status': 'online',
|
||||
'id': 'node/node3',
|
||||
'cgroup-mode': 2,
|
||||
'level': '',
|
||||
'node': 'node3',
|
||||
'type': 'node'},
|
||||
]
|
||||
|
||||
TASK_API_RETURN = {
|
||||
"node1": {
|
||||
'starttime': 1732606253,
|
||||
'status': 'stopped',
|
||||
'type': 'vzdump',
|
||||
'pstart': 517463911,
|
||||
'upid': 'UPID:node1:003F8C63:1E7FB79C:67449780:vzdump:100:root@pam:',
|
||||
'id': '100',
|
||||
'node': 'hypervisor',
|
||||
'pid': 541669,
|
||||
'user': 'test@pve',
|
||||
'exitstatus': 'OK'},
|
||||
"node2": {
|
||||
'starttime': 1732606253,
|
||||
'status': 'stopped',
|
||||
'type': 'vzdump',
|
||||
'pstart': 517463911,
|
||||
'upid': 'UPID:node2:000029DD:1599528B:6108F068:vzdump:101:root@pam:',
|
||||
'id': '101',
|
||||
'node': 'hypervisor',
|
||||
'pid': 541669,
|
||||
'user': 'test@pve',
|
||||
'exitstatus': 'OK'},
|
||||
}
|
||||
|
||||
|
||||
VZDUMP_API_RETURN = {
|
||||
"node1": "UPID:node1:003F8C63:1E7FB79C:67449780:vzdump:100:root@pam:",
|
||||
"node2": "UPID:node2:000029DD:1599528B:6108F068:vzdump:101:root@pam:",
|
||||
"node3": "OK",
|
||||
}
|
||||
|
||||
|
||||
TASKLOG_API_RETURN = {"node1": [{'n': 1,
|
||||
't': "INFO: starting new backup job: vzdump 100 --mode snapshot --node node1 "
|
||||
"--notes-template '{{guestname}}' --storage backup --notification-mode auto"},
|
||||
{'t': 'INFO: Starting Backup of VM 100 (lxc)',
|
||||
'n': 2},
|
||||
{'n': 23, 't': 'INFO: adding notes to backup'},
|
||||
{'n': 24,
|
||||
't': 'INFO: Finished Backup of VM 100 (00:00:03)'},
|
||||
{'n': 25,
|
||||
't': 'INFO: Backup finished at 2024-11-25 16:28:03'},
|
||||
{'t': 'INFO: Backup job finished successfully',
|
||||
'n': 26},
|
||||
{'n': 27, 't': 'TASK OK'}],
|
||||
"node2": [{'n': 1,
|
||||
't': "INFO: starting new backup job: vzdump 101 --mode snapshot --node node2 "
|
||||
"--notes-template '{{guestname}}' --storage backup --notification-mode auto"},
|
||||
{'t': 'INFO: Starting Backup of VM 101 (kvm)',
|
||||
'n': 2},
|
||||
{'n': 24,
|
||||
't': 'INFO: Finished Backup of VM 100 (00:00:03)'},
|
||||
{'n': 25,
|
||||
't': 'INFO: Backup finished at 2024-11-25 16:28:03'},
|
||||
{'t': 'INFO: Backup job finished successfully',
|
||||
'n': 26},
|
||||
{'n': 27, 't': 'TASK OK'}],
|
||||
}
|
||||
|
||||
|
||||
def return_valid_resources(resource_type, *args, **kwargs):
|
||||
if resource_type == "vm":
|
||||
return VMS
|
||||
if resource_type == "node":
|
||||
return NODES
|
||||
|
||||
|
||||
def return_vzdump_api(node, *args, **kwargs):
|
||||
if node in ("node1", "node2", "node3"):
|
||||
return VZDUMP_API_RETURN[node]
|
||||
|
||||
|
||||
def return_logs_api(node, *args, **kwargs):
|
||||
if node in ("node1", "node2"):
|
||||
return TASKLOG_API_RETURN[node]
|
||||
|
||||
|
||||
def return_task_status_api(node, *args, **kwargs):
|
||||
if node in ("node1", "node2"):
|
||||
return TASK_API_RETURN[node]
|
||||
|
||||
|
||||
class TestProxmoxBackup(ModuleTestCase):
|
||||
def setUp(self):
|
||||
super(TestProxmoxBackup, self).setUp()
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
self.module = proxmox_backup
|
||||
self.connect_mock = patch(
|
||||
"ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect",
|
||||
).start()
|
||||
self.mock_get_permissions = patch.object(
|
||||
proxmox_backup.ProxmoxBackupAnsible, "_get_permissions").start()
|
||||
self.mock_get_storages = patch.object(proxmox_utils.ProxmoxAnsible,
|
||||
"get_storages").start()
|
||||
self.mock_get_resources = patch.object(
|
||||
proxmox_backup.ProxmoxBackupAnsible, "_get_resources").start()
|
||||
self.mock_get_tasklog = patch.object(
|
||||
proxmox_backup.ProxmoxBackupAnsible, "_get_tasklog").start()
|
||||
self.mock_post_vzdump = patch.object(
|
||||
proxmox_backup.ProxmoxBackupAnsible, "_post_vzdump").start()
|
||||
self.mock_get_taskok = patch.object(
|
||||
proxmox_backup.ProxmoxBackupAnsible, "_get_taskok").start()
|
||||
self.mock_get_permissions.return_value = MINIMAL_PERMISSIONS
|
||||
self.mock_get_storages.return_value = STORAGE
|
||||
self.mock_get_resources.side_effect = return_valid_resources
|
||||
self.mock_get_taskok.side_effect = return_task_status_api
|
||||
self.mock_get_tasklog.side_effect = return_logs_api
|
||||
self.mock_post_vzdump.side_effect = return_vzdump_api
|
||||
|
||||
def tearDown(self):
|
||||
self.connect_mock.stop()
|
||||
self.mock_get_permissions.stop()
|
||||
self.mock_get_storages.stop()
|
||||
self.mock_get_resources.stop()
|
||||
super(TestProxmoxBackup, self).tearDown()
|
||||
|
||||
def test_proxmox_backup_without_argument(self):
|
||||
with set_module_args({}):
|
||||
with pytest.raises(AnsibleFailJson):
|
||||
proxmox_backup.main()
|
||||
|
||||
def test_create_backup_check_mode(self):
|
||||
with set_module_args(
|
||||
{
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"mode": "all",
|
||||
"storage": "backup",
|
||||
"_ansible_check_mode": True,
|
||||
}
|
||||
):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
proxmox_backup.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
|
||||
assert result["changed"] is True
|
||||
assert result["msg"] == "Backups would be created"
|
||||
assert len(result["backups"]) == 0
|
||||
assert self.mock_get_taskok.call_count == 0
|
||||
assert self.mock_get_tasklog.call_count == 0
|
||||
assert self.mock_post_vzdump.call_count == 0
|
||||
|
||||
def test_create_backup_all_mode(self):
|
||||
with set_module_args({
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"mode": "all",
|
||||
"storage": "backup",
|
||||
}):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
proxmox_backup.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["changed"] is True
|
||||
assert result["msg"] == "Backup tasks created"
|
||||
for backup_result in result["backups"]:
|
||||
assert backup_result["upid"] in {
|
||||
VZDUMP_API_RETURN[key] for key in VZDUMP_API_RETURN}
|
||||
assert self.mock_get_taskok.call_count == 0
|
||||
assert self.mock_post_vzdump.call_count == 3
|
||||
|
||||
def test_create_backup_include_mode_with_wait(self):
|
||||
with set_module_args({
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"mode": "include",
|
||||
"node": "node1",
|
||||
"storage": "backup",
|
||||
"vmids": [100],
|
||||
"wait": True
|
||||
}):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
proxmox_backup.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["changed"] is True
|
||||
assert result["msg"] == "Backups succeeded"
|
||||
for backup_result in result["backups"]:
|
||||
assert backup_result["upid"] in {
|
||||
VZDUMP_API_RETURN[key] for key in VZDUMP_API_RETURN}
|
||||
assert self.mock_get_taskok.call_count == 1
|
||||
assert self.mock_post_vzdump.call_count == 1
|
||||
|
||||
def test_fail_insufficient_permissions(self):
|
||||
with set_module_args({
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"mode": "include",
|
||||
"storage": "backup",
|
||||
"performance_tweaks": "max-workers=2",
|
||||
"vmids": [100],
|
||||
"wait": True
|
||||
}):
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
proxmox_backup.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["msg"] == "Insufficient permission: Performance_tweaks and bandwidth require 'Sys.Modify' permission for '/'"
|
||||
assert self.mock_get_taskok.call_count == 0
|
||||
assert self.mock_post_vzdump.call_count == 0
|
||||
|
||||
def test_fail_missing_node(self):
|
||||
with set_module_args({
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"mode": "include",
|
||||
"storage": "backup",
|
||||
"node": "nonexistingnode",
|
||||
"vmids": [100],
|
||||
"wait": True
|
||||
}):
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
proxmox_backup.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["msg"] == "Node nonexistingnode was specified, but does not exist on the cluster"
|
||||
assert self.mock_get_taskok.call_count == 0
|
||||
assert self.mock_post_vzdump.call_count == 0
|
||||
|
||||
def test_fail_missing_storage(self):
|
||||
with set_module_args({
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"mode": "include",
|
||||
"storage": "nonexistingstorage",
|
||||
"vmids": [100],
|
||||
"wait": True
|
||||
}):
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
proxmox_backup.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["msg"] == "Storage nonexistingstorage does not exist in the cluster"
|
||||
assert self.mock_get_taskok.call_count == 0
|
||||
assert self.mock_post_vzdump.call_count == 0
|
|
@ -1,275 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2024 Marzieh Raoufnezhad <raoufnezhad at gmail.com>
|
||||
# Copyright (c) 2024 Maryam Mayabi <mayabi.ahm at gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
proxmoxer = pytest.importorskip("proxmoxer")
|
||||
|
||||
from ansible_collections.community.general.plugins.modules import proxmox_backup_info
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
|
||||
AnsibleExitJson,
|
||||
AnsibleFailJson,
|
||||
ModuleTestCase,
|
||||
set_module_args,
|
||||
)
|
||||
import ansible_collections.community.general.plugins.module_utils.proxmox as proxmox_utils
|
||||
|
||||
RESOURCE_LIST = [
|
||||
{
|
||||
"uptime": 0,
|
||||
"diskwrite": 0,
|
||||
"name": "test01",
|
||||
"maxcpu": 0,
|
||||
"node": "NODE1",
|
||||
"mem": 0,
|
||||
"netout": 0,
|
||||
"netin": 0,
|
||||
"maxmem": 0,
|
||||
"diskread": 0,
|
||||
"disk": 0,
|
||||
"maxdisk": 0,
|
||||
"status": "running",
|
||||
"cpu": 0,
|
||||
"id": "qemu/100",
|
||||
"template": 0,
|
||||
"vmid": 100,
|
||||
"type": "qemu"
|
||||
},
|
||||
{
|
||||
"uptime": 0,
|
||||
"diskwrite": 0,
|
||||
"name": "test02",
|
||||
"maxcpu": 0,
|
||||
"node": "NODE1",
|
||||
"mem": 0,
|
||||
"netout": 0,
|
||||
"netin": 0,
|
||||
"maxmem": 0,
|
||||
"diskread": 0,
|
||||
"disk": 0,
|
||||
"maxdisk": 0,
|
||||
"status": "running",
|
||||
"cpu": 0,
|
||||
"id": "qemu/101",
|
||||
"template": 0,
|
||||
"vmid": 101,
|
||||
"type": "qemu"
|
||||
},
|
||||
{
|
||||
"uptime": 0,
|
||||
"diskwrite": 0,
|
||||
"name": "test03",
|
||||
"maxcpu": 0,
|
||||
"node": "NODE2",
|
||||
"mem": 0,
|
||||
"netout": 0,
|
||||
"netin": 0,
|
||||
"maxmem": 0,
|
||||
"diskread": 0,
|
||||
"disk": 0,
|
||||
"maxdisk": 0,
|
||||
"status": "running",
|
||||
"cpu": 0,
|
||||
"id": "qemu/102",
|
||||
"template": 0,
|
||||
"vmid": 102,
|
||||
"type": "qemu"
|
||||
}
|
||||
]
|
||||
BACKUP_JOBS = [
|
||||
{
|
||||
"type": "vzdump",
|
||||
"id": "backup-83831498-c631",
|
||||
"storage": "local",
|
||||
"vmid": "100",
|
||||
"enabled": 1,
|
||||
"next-run": 1735138800,
|
||||
"mailnotification": "always",
|
||||
"schedule": "06,18:30",
|
||||
"mode": "snapshot",
|
||||
"notes-template": "guestname"
|
||||
},
|
||||
{
|
||||
"schedule": "sat 15:00",
|
||||
"notes-template": "guestname",
|
||||
"mode": "snapshot",
|
||||
"mailnotification": "always",
|
||||
"next-run": 1735385400,
|
||||
"type": "vzdump",
|
||||
"enabled": 1,
|
||||
"vmid": "100,101,102",
|
||||
"storage": "local",
|
||||
"id": "backup-70025700-2302",
|
||||
}
|
||||
]
|
||||
|
||||
EXPECTED_BACKUP_OUTPUT = [
|
||||
{
|
||||
"bktype": "vzdump",
|
||||
"enabled": 1,
|
||||
"id": "backup-83831498-c631",
|
||||
"mode": "snapshot",
|
||||
"next-run": "2024-12-25 15:00:00",
|
||||
"schedule": "06,18:30",
|
||||
"storage": "local",
|
||||
"vm_name": "test01",
|
||||
"vmid": "100"
|
||||
},
|
||||
{
|
||||
"bktype": "vzdump",
|
||||
"enabled": 1,
|
||||
"id": "backup-70025700-2302",
|
||||
"mode": "snapshot",
|
||||
"next-run": "2024-12-28 11:30:00",
|
||||
"schedule": "sat 15:00",
|
||||
"storage": "local",
|
||||
"vm_name": "test01",
|
||||
"vmid": "100"
|
||||
},
|
||||
{
|
||||
"bktype": "vzdump",
|
||||
"enabled": 1,
|
||||
"id": "backup-70025700-2302",
|
||||
"mode": "snapshot",
|
||||
"next-run": "2024-12-28 11:30:00",
|
||||
"schedule": "sat 15:00",
|
||||
"storage": "local",
|
||||
"vm_name": "test02",
|
||||
"vmid": "101"
|
||||
},
|
||||
{
|
||||
"bktype": "vzdump",
|
||||
"enabled": 1,
|
||||
"id": "backup-70025700-2302",
|
||||
"mode": "snapshot",
|
||||
"next-run": "2024-12-28 11:30:00",
|
||||
"schedule": "sat 15:00",
|
||||
"storage": "local",
|
||||
"vm_name": "test03",
|
||||
"vmid": "102"
|
||||
}
|
||||
]
|
||||
EXPECTED_BACKUP_JOBS_OUTPUT = [
|
||||
{
|
||||
"enabled": 1,
|
||||
"id": "backup-83831498-c631",
|
||||
"mailnotification": "always",
|
||||
"mode": "snapshot",
|
||||
"next-run": 1735138800,
|
||||
"notes-template": "guestname",
|
||||
"schedule": "06,18:30",
|
||||
"storage": "local",
|
||||
"type": "vzdump",
|
||||
"vmid": "100"
|
||||
},
|
||||
{
|
||||
"enabled": 1,
|
||||
"id": "backup-70025700-2302",
|
||||
"mailnotification": "always",
|
||||
"mode": "snapshot",
|
||||
"next-run": 1735385400,
|
||||
"notes-template": "guestname",
|
||||
"schedule": "sat 15:00",
|
||||
"storage": "local",
|
||||
"type": "vzdump",
|
||||
"vmid": "100,101,102"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class TestProxmoxBackupInfoModule(ModuleTestCase):
|
||||
def setUp(self):
|
||||
super(TestProxmoxBackupInfoModule, self).setUp()
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
self.module = proxmox_backup_info
|
||||
self.connect_mock = patch(
|
||||
"ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect",
|
||||
).start()
|
||||
self.connect_mock.return_value.cluster.resources.get.return_value = (
|
||||
RESOURCE_LIST
|
||||
)
|
||||
self.connect_mock.return_value.cluster.backup.get.return_value = (
|
||||
BACKUP_JOBS
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.connect_mock.stop()
|
||||
super(TestProxmoxBackupInfoModule, self).tearDown()
|
||||
|
||||
def test_module_fail_when_required_args_missing(self):
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
with set_module_args({}):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["msg"] == "missing required arguments: api_host, api_user"
|
||||
|
||||
def test_get_all_backups_information(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
with set_module_args({
|
||||
'api_host': 'proxmoxhost',
|
||||
'api_user': 'root@pam',
|
||||
'api_password': 'supersecret'
|
||||
}):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["backup_info"] == EXPECTED_BACKUP_OUTPUT
|
||||
|
||||
def test_get_specific_backup_information_by_vmname(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
vmname = 'test01'
|
||||
expected_output = [
|
||||
backup for backup in EXPECTED_BACKUP_OUTPUT if backup["vm_name"] == vmname
|
||||
]
|
||||
with set_module_args({
|
||||
'api_host': 'proxmoxhost',
|
||||
'api_user': 'root@pam',
|
||||
'api_password': 'supersecret',
|
||||
'vm_name': vmname
|
||||
}):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["backup_info"] == expected_output
|
||||
assert len(result["backup_info"]) == 2
|
||||
|
||||
def test_get_specific_backup_information_by_vmid(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
vmid = "101"
|
||||
expected_output = [
|
||||
backup for backup in EXPECTED_BACKUP_OUTPUT if backup["vmid"] == vmid
|
||||
]
|
||||
with set_module_args({
|
||||
'api_host': 'proxmoxhost',
|
||||
'api_user': 'root@pam',
|
||||
'api_password': 'supersecret',
|
||||
'vm_id': vmid
|
||||
}):
|
||||
self.module.main()
|
||||
result = exc_info.value.args[0]
|
||||
assert result["backup_info"] == expected_output
|
||||
assert len(result["backup_info"]) == 1
|
||||
|
||||
def test_get_specific_backup_information_by_backupjobs(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
backupjobs = True
|
||||
with set_module_args({
|
||||
'api_host': 'proxmoxhost',
|
||||
'api_user': 'root@pam',
|
||||
'api_password': 'supersecret',
|
||||
'backup_jobs': backupjobs
|
||||
}):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["backup_info"] == EXPECTED_BACKUP_JOBS_OUTPUT
|
|
@ -1,168 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2021, Ansible Project
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
proxmoxer = pytest.importorskip("proxmoxer")
|
||||
mandatory_py_version = pytest.mark.skipif(
|
||||
sys.version_info < (2, 7),
|
||||
reason="The proxmoxer dependency requires python2.7 or higher",
|
||||
)
|
||||
|
||||
from ansible_collections.community.general.plugins.modules import proxmox_kvm
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import (
|
||||
patch,
|
||||
DEFAULT,
|
||||
)
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
|
||||
AnsibleExitJson,
|
||||
AnsibleFailJson,
|
||||
ModuleTestCase,
|
||||
set_module_args,
|
||||
)
|
||||
import ansible_collections.community.general.plugins.module_utils.proxmox as proxmox_utils
|
||||
|
||||
|
||||
class TestProxmoxKvmModule(ModuleTestCase):
|
||||
def setUp(self):
|
||||
super(TestProxmoxKvmModule, self).setUp()
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
self.module = proxmox_kvm
|
||||
self.connect_mock = patch(
|
||||
"ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect"
|
||||
).start()
|
||||
self.get_node_mock = patch.object(
|
||||
proxmox_utils.ProxmoxAnsible, "get_node"
|
||||
).start()
|
||||
self.get_vm_mock = patch.object(proxmox_utils.ProxmoxAnsible, "get_vm").start()
|
||||
self.create_vm_mock = patch.object(
|
||||
proxmox_kvm.ProxmoxKvmAnsible, "create_vm"
|
||||
).start()
|
||||
|
||||
def tearDown(self):
|
||||
self.create_vm_mock.stop()
|
||||
self.get_vm_mock.stop()
|
||||
self.get_node_mock.stop()
|
||||
self.connect_mock.stop()
|
||||
super(TestProxmoxKvmModule, self).tearDown()
|
||||
|
||||
def test_module_fail_when_required_args_missing(self):
|
||||
with self.assertRaises(AnsibleFailJson):
|
||||
with set_module_args({}):
|
||||
self.module.main()
|
||||
|
||||
def test_module_exits_unchaged_when_provided_vmid_exists(self):
|
||||
with set_module_args(
|
||||
{
|
||||
"api_host": "host",
|
||||
"api_user": "user",
|
||||
"api_password": "password",
|
||||
"vmid": "100",
|
||||
"node": "pve",
|
||||
}
|
||||
):
|
||||
self.get_vm_mock.return_value = [{"vmid": "100"}]
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
self.module.main()
|
||||
|
||||
assert self.get_vm_mock.call_count == 1
|
||||
result = exc_info.value.args[0]
|
||||
assert result["changed"] is False
|
||||
assert result["msg"] == "VM with vmid <100> already exists"
|
||||
|
||||
def test_vm_created_when_vmid_not_exist_but_name_already_exist(self):
|
||||
with set_module_args(
|
||||
{
|
||||
"api_host": "host",
|
||||
"api_user": "user",
|
||||
"api_password": "password",
|
||||
"vmid": "100",
|
||||
"name": "existing.vm.local",
|
||||
"node": "pve",
|
||||
}
|
||||
):
|
||||
self.get_vm_mock.return_value = None
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
self.module.main()
|
||||
|
||||
assert self.get_vm_mock.call_count == 1
|
||||
assert self.get_node_mock.call_count == 1
|
||||
result = exc_info.value.args[0]
|
||||
assert result["changed"] is True
|
||||
assert result["msg"] == "VM existing.vm.local with vmid 100 deployed"
|
||||
|
||||
def test_vm_not_created_when_name_already_exist_and_vmid_not_set(self):
|
||||
with set_module_args(
|
||||
{
|
||||
"api_host": "host",
|
||||
"api_user": "user",
|
||||
"api_password": "password",
|
||||
"name": "existing.vm.local",
|
||||
"node": "pve",
|
||||
}
|
||||
):
|
||||
with patch.object(proxmox_utils.ProxmoxAnsible, "get_vmid") as get_vmid_mock:
|
||||
get_vmid_mock.return_value = {
|
||||
"vmid": 100,
|
||||
"name": "existing.vm.local",
|
||||
}
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
self.module.main()
|
||||
|
||||
assert get_vmid_mock.call_count == 1
|
||||
result = exc_info.value.args[0]
|
||||
assert result["changed"] is False
|
||||
|
||||
def test_vm_created_when_name_doesnt_exist_and_vmid_not_set(self):
|
||||
with set_module_args(
|
||||
{
|
||||
"api_host": "host",
|
||||
"api_user": "user",
|
||||
"api_password": "password",
|
||||
"name": "existing.vm.local",
|
||||
"node": "pve",
|
||||
}
|
||||
):
|
||||
self.get_vm_mock.return_value = None
|
||||
with patch.multiple(
|
||||
proxmox_utils.ProxmoxAnsible, get_vmid=DEFAULT, get_nextvmid=DEFAULT
|
||||
) as utils_mock:
|
||||
utils_mock["get_vmid"].return_value = None
|
||||
utils_mock["get_nextvmid"].return_value = 101
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
self.module.main()
|
||||
|
||||
assert utils_mock["get_vmid"].call_count == 1
|
||||
assert utils_mock["get_nextvmid"].call_count == 1
|
||||
result = exc_info.value.args[0]
|
||||
assert result["changed"] is True
|
||||
assert result["msg"] == "VM existing.vm.local with vmid 101 deployed"
|
||||
|
||||
def test_parse_mac(self):
|
||||
assert (
|
||||
proxmox_kvm.parse_mac("virtio=00:11:22:AA:BB:CC,bridge=vmbr0,firewall=1")
|
||||
== "00:11:22:AA:BB:CC"
|
||||
)
|
||||
|
||||
def test_parse_dev(self):
|
||||
assert (
|
||||
proxmox_kvm.parse_dev("local-lvm:vm-1000-disk-0,format=qcow2")
|
||||
== "local-lvm:vm-1000-disk-0"
|
||||
)
|
||||
assert (
|
||||
proxmox_kvm.parse_dev("local-lvm:vm-101-disk-1,size=8G")
|
||||
== "local-lvm:vm-101-disk-1"
|
||||
)
|
||||
assert (
|
||||
proxmox_kvm.parse_dev("local-zfs:vm-1001-disk-0")
|
||||
== "local-zfs:vm-1001-disk-0"
|
||||
)
|
|
@ -1,131 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2019, Ansible Project
|
||||
# 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
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
proxmoxer = pytest.importorskip('proxmoxer')
|
||||
mandatory_py_version = pytest.mark.skipif(
|
||||
sys.version_info < (2, 7),
|
||||
reason='The proxmoxer dependency requires python2.7 or higher'
|
||||
)
|
||||
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock, patch
|
||||
from ansible_collections.community.general.plugins.modules import proxmox_snap
|
||||
import ansible_collections.community.general.plugins.module_utils.proxmox as proxmox_utils
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import set_module_args
|
||||
|
||||
|
||||
def get_resources(type):
|
||||
return [{"diskwrite": 0,
|
||||
"vmid": 100,
|
||||
"node": "localhost",
|
||||
"id": "lxc/100",
|
||||
"maxdisk": 10000,
|
||||
"template": 0,
|
||||
"disk": 10000,
|
||||
"uptime": 10000,
|
||||
"maxmem": 10000,
|
||||
"maxcpu": 1,
|
||||
"netin": 10000,
|
||||
"type": "lxc",
|
||||
"netout": 10000,
|
||||
"mem": 10000,
|
||||
"diskread": 10000,
|
||||
"cpu": 0.01,
|
||||
"name": "test-lxc",
|
||||
"status": "running"}]
|
||||
|
||||
|
||||
def fake_api(mocker):
|
||||
r = mocker.MagicMock()
|
||||
r.cluster.resources.get = MagicMock(side_effect=get_resources)
|
||||
return r
|
||||
|
||||
|
||||
def test_proxmox_snap_without_argument(capfd):
|
||||
with set_module_args({}):
|
||||
with pytest.raises(SystemExit) as results:
|
||||
proxmox_snap.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
assert json.loads(out)['failed']
|
||||
|
||||
|
||||
@patch('ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect')
|
||||
def test_create_snapshot_check_mode(connect_mock, capfd, mocker):
|
||||
with set_module_args({
|
||||
"hostname": "test-lxc",
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"state": "present",
|
||||
"snapname": "test",
|
||||
"timeout": "1",
|
||||
"force": True,
|
||||
"_ansible_check_mode": True
|
||||
}):
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
connect_mock.side_effect = lambda: fake_api(mocker)
|
||||
with pytest.raises(SystemExit) as results:
|
||||
proxmox_snap.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
assert not json.loads(out)['changed']
|
||||
|
||||
|
||||
@patch('ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect')
|
||||
def test_remove_snapshot_check_mode(connect_mock, capfd, mocker):
|
||||
with set_module_args({
|
||||
"hostname": "test-lxc",
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"state": "absent",
|
||||
"snapname": "test",
|
||||
"timeout": "1",
|
||||
"force": True,
|
||||
"_ansible_check_mode": True
|
||||
}):
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
connect_mock.side_effect = lambda: fake_api(mocker)
|
||||
with pytest.raises(SystemExit) as results:
|
||||
proxmox_snap.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
assert not json.loads(out)['changed']
|
||||
|
||||
|
||||
@patch('ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect')
|
||||
def test_rollback_snapshot_check_mode(connect_mock, capfd, mocker):
|
||||
with set_module_args({
|
||||
"hostname": "test-lxc",
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"state": "rollback",
|
||||
"snapname": "test",
|
||||
"timeout": "1",
|
||||
"force": True,
|
||||
"_ansible_check_mode": True
|
||||
}):
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
connect_mock.side_effect = lambda: fake_api(mocker)
|
||||
with pytest.raises(SystemExit) as results:
|
||||
proxmox_snap.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
output = json.loads(out)
|
||||
assert not output['changed']
|
||||
assert output['msg'] == "Snapshot test does not exist"
|
|
@ -1,90 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2023, Julian Vanden Broeck <julian.vandenbroeck at dalibo.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
proxmoxer = pytest.importorskip("proxmoxer")
|
||||
|
||||
from ansible_collections.community.general.plugins.modules import proxmox_storage_contents_info
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
|
||||
AnsibleExitJson,
|
||||
AnsibleFailJson,
|
||||
ModuleTestCase,
|
||||
set_module_args,
|
||||
)
|
||||
import ansible_collections.community.general.plugins.module_utils.proxmox as proxmox_utils
|
||||
|
||||
NODE1 = "pve"
|
||||
RAW_LIST_OUTPUT = [
|
||||
{
|
||||
"content": "backup",
|
||||
"ctime": 1702528474,
|
||||
"format": "pbs-vm",
|
||||
"size": 273804166061,
|
||||
"subtype": "qemu",
|
||||
"vmid": 931,
|
||||
"volid": "datastore:backup/vm/931/2023-12-14T04:34:34Z",
|
||||
},
|
||||
{
|
||||
"content": "backup",
|
||||
"ctime": 1702582560,
|
||||
"format": "pbs-vm",
|
||||
"size": 273804166059,
|
||||
"subtype": "qemu",
|
||||
"vmid": 931,
|
||||
"volid": "datastore:backup/vm/931/2023-12-14T19:36:00Z",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_module_args(node, storage, content="all", vmid=None):
|
||||
return {
|
||||
"api_host": "host",
|
||||
"api_user": "user",
|
||||
"api_password": "password",
|
||||
"node": node,
|
||||
"storage": storage,
|
||||
"content": content,
|
||||
"vmid": vmid,
|
||||
}
|
||||
|
||||
|
||||
class TestProxmoxStorageContentsInfo(ModuleTestCase):
|
||||
def setUp(self):
|
||||
super(TestProxmoxStorageContentsInfo, self).setUp()
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
self.module = proxmox_storage_contents_info
|
||||
self.connect_mock = patch(
|
||||
"ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect",
|
||||
).start()
|
||||
self.connect_mock.return_value.nodes.return_value.storage.return_value.content.return_value.get.return_value = (
|
||||
RAW_LIST_OUTPUT
|
||||
)
|
||||
self.connect_mock.return_value.nodes.get.return_value = [{"node": NODE1}]
|
||||
|
||||
def tearDown(self):
|
||||
self.connect_mock.stop()
|
||||
super(TestProxmoxStorageContentsInfo, self).tearDown()
|
||||
|
||||
def test_module_fail_when_required_args_missing(self):
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
with set_module_args({}):
|
||||
self.module.main()
|
||||
|
||||
def test_storage_contents_info(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
with set_module_args(get_module_args(node=NODE1, storage="datastore")):
|
||||
expected_output = {}
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert not result["changed"]
|
||||
assert result["proxmox_storage_content"] == RAW_LIST_OUTPUT
|
|
@ -1,206 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2021, Andreas Botzner (@paginabianca) <andreas at botzner dot com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Proxmox Tasks module unit tests.
|
||||
# The API responses used in these tests were recorded from PVE version 6.4-8
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
proxmoxer = pytest.importorskip('proxmoxer')
|
||||
mandatory_py_version = pytest.mark.skipif(
|
||||
sys.version_info < (2, 7),
|
||||
reason='The proxmoxer dependency requires python2.7 or higher'
|
||||
)
|
||||
|
||||
from ansible_collections.community.general.plugins.modules import proxmox_tasks_info
|
||||
import ansible_collections.community.general.plugins.module_utils.proxmox as proxmox_utils
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import set_module_args
|
||||
|
||||
NODE = 'node01'
|
||||
TASK_UPID = 'UPID:iaclab-01-01:000029DD:1599528B:6108F068:srvreload:networking:root@pam:'
|
||||
TASKS = [
|
||||
{
|
||||
"endtime": 1629092710,
|
||||
"id": "networking",
|
||||
"node": "iaclab-01-01",
|
||||
"pid": 3539,
|
||||
"pstart": 474062216,
|
||||
"starttime": 1629092709,
|
||||
"status": "OK",
|
||||
"type": "srvreload",
|
||||
"upid": "UPID:iaclab-01-01:00000DD3:1C419D88:6119FB65:srvreload:networking:root@pam:",
|
||||
"user": "root@pam"
|
||||
},
|
||||
{
|
||||
"endtime": 1627975785,
|
||||
"id": "networking",
|
||||
"node": "iaclab-01-01",
|
||||
"pid": 10717,
|
||||
"pstart": 362369675,
|
||||
"starttime": 1627975784,
|
||||
"status": "command 'ifreload -a' failed: exit code 1",
|
||||
"type": "srvreload",
|
||||
"upid": "UPID:iaclab-01-01:000029DD:1599528B:6108F068:srvreload:networking:root@pam:",
|
||||
"user": "root@pam"
|
||||
},
|
||||
{
|
||||
"endtime": 1627975503,
|
||||
"id": "networking",
|
||||
"node": "iaclab-01-01",
|
||||
"pid": 6778,
|
||||
"pstart": 362341540,
|
||||
"starttime": 1627975503,
|
||||
"status": "OK",
|
||||
"type": "srvreload",
|
||||
"upid": "UPID:iaclab-01-01:00001A7A:1598E4A4:6108EF4F:srvreload:networking:root@pam:",
|
||||
"user": "root@pam"
|
||||
}
|
||||
]
|
||||
EXPECTED_TASKS = [
|
||||
{
|
||||
"endtime": 1629092710,
|
||||
"id": "networking",
|
||||
"node": "iaclab-01-01",
|
||||
"pid": 3539,
|
||||
"pstart": 474062216,
|
||||
"starttime": 1629092709,
|
||||
"status": "OK",
|
||||
"type": "srvreload",
|
||||
"upid": "UPID:iaclab-01-01:00000DD3:1C419D88:6119FB65:srvreload:networking:root@pam:",
|
||||
"user": "root@pam",
|
||||
"failed": False
|
||||
},
|
||||
{
|
||||
"endtime": 1627975785,
|
||||
"id": "networking",
|
||||
"node": "iaclab-01-01",
|
||||
"pid": 10717,
|
||||
"pstart": 362369675,
|
||||
"starttime": 1627975784,
|
||||
"status": "command 'ifreload -a' failed: exit code 1",
|
||||
"type": "srvreload",
|
||||
"upid": "UPID:iaclab-01-01:000029DD:1599528B:6108F068:srvreload:networking:root@pam:",
|
||||
"user": "root@pam",
|
||||
"failed": True
|
||||
},
|
||||
{
|
||||
"endtime": 1627975503,
|
||||
"id": "networking",
|
||||
"node": "iaclab-01-01",
|
||||
"pid": 6778,
|
||||
"pstart": 362341540,
|
||||
"starttime": 1627975503,
|
||||
"status": "OK",
|
||||
"type": "srvreload",
|
||||
"upid": "UPID:iaclab-01-01:00001A7A:1598E4A4:6108EF4F:srvreload:networking:root@pam:",
|
||||
"user": "root@pam",
|
||||
"failed": False
|
||||
}
|
||||
]
|
||||
|
||||
EXPECTED_SINGLE_TASK = [
|
||||
{
|
||||
"endtime": 1627975785,
|
||||
"id": "networking",
|
||||
"node": "iaclab-01-01",
|
||||
"pid": 10717,
|
||||
"pstart": 362369675,
|
||||
"starttime": 1627975784,
|
||||
"status": "command 'ifreload -a' failed: exit code 1",
|
||||
"type": "srvreload",
|
||||
"upid": "UPID:iaclab-01-01:000029DD:1599528B:6108F068:srvreload:networking:root@pam:",
|
||||
"user": "root@pam",
|
||||
"failed": True
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@patch('ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect')
|
||||
def test_without_required_parameters(connect_mock, capfd, mocker):
|
||||
with set_module_args({}):
|
||||
with pytest.raises(SystemExit):
|
||||
proxmox_tasks_info.main()
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
assert json.loads(out)['failed']
|
||||
|
||||
|
||||
def mock_api_tasks_response(mocker):
|
||||
m = mocker.MagicMock()
|
||||
g = mocker.MagicMock()
|
||||
m.nodes = mocker.MagicMock(return_value=g)
|
||||
g.tasks.get = mocker.MagicMock(return_value=TASKS)
|
||||
return m
|
||||
|
||||
|
||||
@patch('ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect')
|
||||
def test_get_tasks(connect_mock, capfd, mocker):
|
||||
with set_module_args({
|
||||
'api_host': 'proxmoxhost',
|
||||
'api_user': 'root@pam',
|
||||
'api_password': 'supersecret',
|
||||
'node': NODE
|
||||
}):
|
||||
connect_mock.side_effect = lambda: mock_api_tasks_response(mocker)
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
proxmox_tasks_info.main()
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
assert len(json.loads(out)['proxmox_tasks']) != 0
|
||||
assert not json.loads(out)['changed']
|
||||
|
||||
|
||||
@patch('ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect')
|
||||
def test_get_single_task(connect_mock, capfd, mocker):
|
||||
with set_module_args({
|
||||
'api_host': 'proxmoxhost',
|
||||
'api_user': 'root@pam',
|
||||
'api_password': 'supersecret',
|
||||
'node': NODE,
|
||||
'task': TASK_UPID
|
||||
}):
|
||||
connect_mock.side_effect = lambda: mock_api_tasks_response(mocker)
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
proxmox_tasks_info.main()
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
assert len(json.loads(out)['proxmox_tasks']) == 1
|
||||
assert json.loads(out)
|
||||
assert not json.loads(out)['changed']
|
||||
|
||||
|
||||
@patch('ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect')
|
||||
def test_get_non_existent_task(connect_mock, capfd, mocker):
|
||||
with set_module_args({
|
||||
'api_host': 'proxmoxhost',
|
||||
'api_user': 'root@pam',
|
||||
'api_password': 'supersecret',
|
||||
'node': NODE,
|
||||
'task': 'UPID:nonexistent'
|
||||
}):
|
||||
connect_mock.side_effect = lambda: mock_api_tasks_response(mocker)
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
proxmox_tasks_info.main()
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
assert json.loads(out)['failed']
|
||||
assert 'proxmox_tasks' not in json.loads(out)
|
||||
assert not json.loads(out)['changed']
|
||||
assert json.loads(
|
||||
out)['msg'] == 'Task: UPID:nonexistent does not exist on node: node01.'
|
|
@ -1,66 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2023, Sergei Antipov <greendayonfire at gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
proxmoxer = pytest.importorskip('proxmoxer')
|
||||
mandatory_py_version = pytest.mark.skipif(
|
||||
sys.version_info < (2, 7),
|
||||
reason='The proxmoxer dependency requires python2.7 or higher'
|
||||
)
|
||||
|
||||
from ansible_collections.community.general.plugins.modules import proxmox_template
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch, Mock
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
|
||||
AnsibleFailJson,
|
||||
ModuleTestCase,
|
||||
set_module_args,
|
||||
)
|
||||
import ansible_collections.community.general.plugins.module_utils.proxmox as proxmox_utils
|
||||
|
||||
|
||||
class TestProxmoxTemplateModule(ModuleTestCase):
|
||||
def setUp(self):
|
||||
super(TestProxmoxTemplateModule, self).setUp()
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
self.module = proxmox_template
|
||||
self.connect_mock = patch(
|
||||
"ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect"
|
||||
)
|
||||
self.connect_mock.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.connect_mock.stop()
|
||||
super(TestProxmoxTemplateModule, self).tearDown()
|
||||
|
||||
@patch("os.stat")
|
||||
@patch.multiple(os.path, exists=Mock(return_value=True), isfile=Mock(return_value=True))
|
||||
def test_module_fail_when_toolbelt_not_installed_and_file_size_is_big(self, mock_stat):
|
||||
self.module.HAS_REQUESTS_TOOLBELT = False
|
||||
mock_stat.return_value.st_size = 268435460
|
||||
with set_module_args(
|
||||
{
|
||||
"api_host": "host",
|
||||
"api_user": "user",
|
||||
"api_password": "password",
|
||||
"node": "pve",
|
||||
"src": "/tmp/mock.iso",
|
||||
"content_type": "iso"
|
||||
}
|
||||
):
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["failed"] is True
|
||||
assert result["msg"] == "'requests_toolbelt' module is required to upload files larger than 256MB"
|
|
@ -1,714 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2023, Sergei Antipov <greendayonfire at gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
proxmoxer = pytest.importorskip("proxmoxer")
|
||||
mandatory_py_version = pytest.mark.skipif(
|
||||
sys.version_info < (2, 7),
|
||||
reason="The proxmoxer dependency requires python2.7 or higher",
|
||||
)
|
||||
|
||||
from ansible_collections.community.general.plugins.modules import proxmox_vm_info
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch
|
||||
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
|
||||
AnsibleExitJson,
|
||||
AnsibleFailJson,
|
||||
ModuleTestCase,
|
||||
set_module_args,
|
||||
)
|
||||
import ansible_collections.community.general.plugins.module_utils.proxmox as proxmox_utils
|
||||
|
||||
NODE1 = "pve"
|
||||
NODE2 = "pve2"
|
||||
RAW_CLUSTER_OUTPUT = [
|
||||
{
|
||||
"cpu": 0.174069059487628,
|
||||
"disk": 0,
|
||||
"diskread": 6656,
|
||||
"diskwrite": 0,
|
||||
"id": "qemu/100",
|
||||
"maxcpu": 1,
|
||||
"maxdisk": 34359738368,
|
||||
"maxmem": 4294967296,
|
||||
"mem": 35304543,
|
||||
"name": "pxe.home.arpa",
|
||||
"netin": 416956,
|
||||
"netout": 17330,
|
||||
"node": NODE1,
|
||||
"status": "running",
|
||||
"template": 0,
|
||||
"type": "qemu",
|
||||
"uptime": 669,
|
||||
"vmid": 100,
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "qemu/101",
|
||||
"maxcpu": 1,
|
||||
"maxdisk": 0,
|
||||
"maxmem": 536870912,
|
||||
"mem": 0,
|
||||
"name": "test1",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"node": NODE2,
|
||||
"pool": "pool1",
|
||||
"status": "stopped",
|
||||
"template": 0,
|
||||
"type": "qemu",
|
||||
"uptime": 0,
|
||||
"vmid": 101,
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"disk": 352190464,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "lxc/102",
|
||||
"maxcpu": 2,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"mem": 28192768,
|
||||
"name": "test-lxc.home.arpa",
|
||||
"netin": 102757,
|
||||
"netout": 446,
|
||||
"node": NODE1,
|
||||
"status": "running",
|
||||
"template": 0,
|
||||
"type": "lxc",
|
||||
"uptime": 161,
|
||||
"vmid": 102,
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "lxc/103",
|
||||
"maxcpu": 2,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"mem": 0,
|
||||
"name": "test1-lxc.home.arpa",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"node": NODE2,
|
||||
"pool": "pool1",
|
||||
"status": "stopped",
|
||||
"template": 0,
|
||||
"type": "lxc",
|
||||
"uptime": 0,
|
||||
"vmid": 103,
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "lxc/104",
|
||||
"maxcpu": 2,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"mem": 0,
|
||||
"name": "test-lxc.home.arpa",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"node": NODE2,
|
||||
"pool": "pool1",
|
||||
"status": "stopped",
|
||||
"template": 0,
|
||||
"type": "lxc",
|
||||
"uptime": 0,
|
||||
"vmid": 104,
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "lxc/105",
|
||||
"maxcpu": 2,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"mem": 0,
|
||||
"name": "",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"node": NODE2,
|
||||
"pool": "pool1",
|
||||
"status": "stopped",
|
||||
"template": 0,
|
||||
"type": "lxc",
|
||||
"uptime": 0,
|
||||
"vmid": 105,
|
||||
},
|
||||
]
|
||||
RAW_LXC_OUTPUT = [
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 2,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"maxswap": 536870912,
|
||||
"mem": 0,
|
||||
"name": "test1-lxc.home.arpa",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"status": "stopped",
|
||||
"swap": 0,
|
||||
"type": "lxc",
|
||||
"uptime": 0,
|
||||
"vmid": "103",
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 2,
|
||||
"disk": 352190464,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"maxswap": 536870912,
|
||||
"mem": 28192768,
|
||||
"name": "test-lxc.home.arpa",
|
||||
"netin": 102757,
|
||||
"netout": 446,
|
||||
"pid": 4076752,
|
||||
"status": "running",
|
||||
"swap": 0,
|
||||
"type": "lxc",
|
||||
"uptime": 161,
|
||||
"vmid": "102",
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 2,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"maxswap": 536870912,
|
||||
"mem": 0,
|
||||
"name": "test-lxc.home.arpa",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"status": "stopped",
|
||||
"swap": 0,
|
||||
"type": "lxc",
|
||||
"uptime": 0,
|
||||
"vmid": "104",
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 2,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"maxswap": 536870912,
|
||||
"mem": 0,
|
||||
"name": "",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"status": "stopped",
|
||||
"swap": 0,
|
||||
"type": "lxc",
|
||||
"uptime": 0,
|
||||
"vmid": "105",
|
||||
},
|
||||
]
|
||||
RAW_QEMU_OUTPUT = [
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 1,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"maxdisk": 0,
|
||||
"maxmem": 536870912,
|
||||
"mem": 0,
|
||||
"name": "test1",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"status": "stopped",
|
||||
"uptime": 0,
|
||||
"vmid": 101,
|
||||
},
|
||||
{
|
||||
"cpu": 0.174069059487628,
|
||||
"cpus": 1,
|
||||
"disk": 0,
|
||||
"diskread": 6656,
|
||||
"diskwrite": 0,
|
||||
"maxdisk": 34359738368,
|
||||
"maxmem": 4294967296,
|
||||
"mem": 35304543,
|
||||
"name": "pxe.home.arpa",
|
||||
"netin": 416956,
|
||||
"netout": 17330,
|
||||
"pid": 4076688,
|
||||
"status": "running",
|
||||
"uptime": 669,
|
||||
"vmid": 100,
|
||||
},
|
||||
]
|
||||
EXPECTED_VMS_OUTPUT = [
|
||||
{
|
||||
"cpu": 0.174069059487628,
|
||||
"cpus": 1,
|
||||
"disk": 0,
|
||||
"diskread": 6656,
|
||||
"diskwrite": 0,
|
||||
"id": "qemu/100",
|
||||
"maxcpu": 1,
|
||||
"maxdisk": 34359738368,
|
||||
"maxmem": 4294967296,
|
||||
"mem": 35304543,
|
||||
"name": "pxe.home.arpa",
|
||||
"netin": 416956,
|
||||
"netout": 17330,
|
||||
"node": NODE1,
|
||||
"pid": 4076688,
|
||||
"status": "running",
|
||||
"template": False,
|
||||
"type": "qemu",
|
||||
"uptime": 669,
|
||||
"vmid": 100,
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 1,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "qemu/101",
|
||||
"maxcpu": 1,
|
||||
"maxdisk": 0,
|
||||
"maxmem": 536870912,
|
||||
"mem": 0,
|
||||
"name": "test1",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"node": NODE2,
|
||||
"pool": "pool1",
|
||||
"status": "stopped",
|
||||
"template": False,
|
||||
"type": "qemu",
|
||||
"uptime": 0,
|
||||
"vmid": 101,
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 2,
|
||||
"disk": 352190464,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "lxc/102",
|
||||
"maxcpu": 2,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"maxswap": 536870912,
|
||||
"mem": 28192768,
|
||||
"name": "test-lxc.home.arpa",
|
||||
"netin": 102757,
|
||||
"netout": 446,
|
||||
"node": NODE1,
|
||||
"pid": 4076752,
|
||||
"status": "running",
|
||||
"swap": 0,
|
||||
"template": False,
|
||||
"type": "lxc",
|
||||
"uptime": 161,
|
||||
"vmid": 102,
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 2,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "lxc/103",
|
||||
"maxcpu": 2,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"maxswap": 536870912,
|
||||
"mem": 0,
|
||||
"name": "test1-lxc.home.arpa",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"node": NODE2,
|
||||
"pool": "pool1",
|
||||
"status": "stopped",
|
||||
"swap": 0,
|
||||
"template": False,
|
||||
"type": "lxc",
|
||||
"uptime": 0,
|
||||
"vmid": 103,
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 2,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "lxc/104",
|
||||
"maxcpu": 2,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"maxswap": 536870912,
|
||||
"mem": 0,
|
||||
"name": "test-lxc.home.arpa",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"node": NODE2,
|
||||
"pool": "pool1",
|
||||
"status": "stopped",
|
||||
"swap": 0,
|
||||
"template": False,
|
||||
"type": "lxc",
|
||||
"uptime": 0,
|
||||
"vmid": 104,
|
||||
},
|
||||
{
|
||||
"cpu": 0,
|
||||
"cpus": 2,
|
||||
"disk": 0,
|
||||
"diskread": 0,
|
||||
"diskwrite": 0,
|
||||
"id": "lxc/105",
|
||||
"maxcpu": 2,
|
||||
"maxdisk": 10737418240,
|
||||
"maxmem": 536870912,
|
||||
"maxswap": 536870912,
|
||||
"mem": 0,
|
||||
"name": "",
|
||||
"netin": 0,
|
||||
"netout": 0,
|
||||
"node": NODE2,
|
||||
"pool": "pool1",
|
||||
"status": "stopped",
|
||||
"swap": 0,
|
||||
"template": False,
|
||||
"type": "lxc",
|
||||
"uptime": 0,
|
||||
"vmid": 105,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_module_args(type="all", node=None, vmid=None, name=None, config="none"):
|
||||
return {
|
||||
"api_host": "host",
|
||||
"api_user": "user",
|
||||
"api_password": "password",
|
||||
"node": node,
|
||||
"type": type,
|
||||
"vmid": vmid,
|
||||
"name": name,
|
||||
"config": config,
|
||||
}
|
||||
|
||||
|
||||
class TestProxmoxVmInfoModule(ModuleTestCase):
|
||||
def setUp(self):
|
||||
super(TestProxmoxVmInfoModule, self).setUp()
|
||||
proxmox_utils.HAS_PROXMOXER = True
|
||||
self.module = proxmox_vm_info
|
||||
self.connect_mock = patch(
|
||||
"ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect",
|
||||
).start()
|
||||
self.connect_mock.return_value.nodes.return_value.lxc.return_value.get.return_value = (
|
||||
RAW_LXC_OUTPUT
|
||||
)
|
||||
self.connect_mock.return_value.nodes.return_value.qemu.return_value.get.return_value = (
|
||||
RAW_QEMU_OUTPUT
|
||||
)
|
||||
self.connect_mock.return_value.cluster.return_value.resources.return_value.get.return_value = (
|
||||
RAW_CLUSTER_OUTPUT
|
||||
)
|
||||
self.connect_mock.return_value.nodes.get.return_value = [{"node": NODE1}]
|
||||
|
||||
def tearDown(self):
|
||||
self.connect_mock.stop()
|
||||
super(TestProxmoxVmInfoModule, self).tearDown()
|
||||
|
||||
def test_module_fail_when_required_args_missing(self):
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
with set_module_args({}):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["msg"] == "missing required arguments: api_host, api_user"
|
||||
|
||||
def test_get_lxc_vms_information(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
with set_module_args(get_module_args(type="lxc")):
|
||||
expected_output = [vm for vm in EXPECTED_VMS_OUTPUT if vm["type"] == "lxc"]
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["changed"] is False
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
|
||||
def test_get_qemu_vms_information(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
with set_module_args(get_module_args(type="qemu")):
|
||||
expected_output = [vm for vm in EXPECTED_VMS_OUTPUT if vm["type"] == "qemu"]
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
|
||||
def test_get_all_vms_information(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
with set_module_args(get_module_args()):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == EXPECTED_VMS_OUTPUT
|
||||
|
||||
def test_vmid_is_converted_to_int(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
with set_module_args(get_module_args(type="lxc")):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert isinstance(result["proxmox_vms"][0]["vmid"], int)
|
||||
|
||||
def test_get_specific_lxc_vm_information(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
vmid = 102
|
||||
expected_output = [
|
||||
vm
|
||||
for vm in EXPECTED_VMS_OUTPUT
|
||||
if vm["vmid"] == vmid and vm["type"] == "lxc"
|
||||
]
|
||||
with set_module_args(get_module_args(type="lxc", vmid=vmid)):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
assert len(result["proxmox_vms"]) == 1
|
||||
|
||||
def test_get_specific_qemu_vm_information(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
vmid = 100
|
||||
expected_output = [
|
||||
vm
|
||||
for vm in EXPECTED_VMS_OUTPUT
|
||||
if vm["vmid"] == vmid and vm["type"] == "qemu"
|
||||
]
|
||||
with set_module_args(get_module_args(type="qemu", vmid=vmid)):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
assert len(result["proxmox_vms"]) == 1
|
||||
|
||||
def test_get_specific_vm_information(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
vmid = 100
|
||||
expected_output = [vm for vm in EXPECTED_VMS_OUTPUT if vm["vmid"] == vmid]
|
||||
with set_module_args(get_module_args(type="all", vmid=vmid)):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
assert len(result["proxmox_vms"]) == 1
|
||||
|
||||
def test_get_specific_vm_information_by_using_name(self):
|
||||
name = "test1-lxc.home.arpa"
|
||||
self.connect_mock.return_value.cluster.resources.get.return_value = [
|
||||
{"name": name, "vmid": "103"}
|
||||
]
|
||||
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
expected_output = [vm for vm in EXPECTED_VMS_OUTPUT if vm["name"] == name]
|
||||
with set_module_args(get_module_args(type="all", name=name)):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
assert len(result["proxmox_vms"]) == 1
|
||||
|
||||
def test_get_multiple_vms_with_the_same_name(self):
|
||||
name = "test-lxc.home.arpa"
|
||||
self.connect_mock.return_value.cluster.resources.get.return_value = [
|
||||
{"name": name, "vmid": "102"},
|
||||
{"name": name, "vmid": "104"},
|
||||
]
|
||||
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
expected_output = [vm for vm in EXPECTED_VMS_OUTPUT if vm["name"] == name]
|
||||
with set_module_args(get_module_args(type="all", name=name)):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
assert len(result["proxmox_vms"]) == 2
|
||||
|
||||
def test_get_vm_with_an_empty_name(self):
|
||||
name = ""
|
||||
self.connect_mock.return_value.cluster.resources.get.return_value = [
|
||||
{"name": name, "vmid": "105"},
|
||||
]
|
||||
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
expected_output = [vm for vm in EXPECTED_VMS_OUTPUT if vm["name"] == name]
|
||||
with set_module_args(get_module_args(type="all", name=name)):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
assert len(result["proxmox_vms"]) == 1
|
||||
|
||||
def test_get_all_lxc_vms_from_specific_node(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
expected_output = [
|
||||
vm
|
||||
for vm in EXPECTED_VMS_OUTPUT
|
||||
if vm["node"] == NODE1 and vm["type"] == "lxc"
|
||||
]
|
||||
with set_module_args(get_module_args(type="lxc", node=NODE1)):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
assert len(result["proxmox_vms"]) == 1
|
||||
|
||||
def test_get_all_qemu_vms_from_specific_node(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
expected_output = [
|
||||
vm
|
||||
for vm in EXPECTED_VMS_OUTPUT
|
||||
if vm["node"] == NODE1 and vm["type"] == "qemu"
|
||||
]
|
||||
with set_module_args(get_module_args(type="qemu", node=NODE1)):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
assert len(result["proxmox_vms"]) == 1
|
||||
|
||||
def test_get_all_vms_from_specific_node(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
expected_output = [vm for vm in EXPECTED_VMS_OUTPUT if vm["node"] == NODE1]
|
||||
with set_module_args(get_module_args(node=NODE1)):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
||||
assert len(result["proxmox_vms"]) == 2
|
||||
|
||||
def test_module_returns_empty_list_when_vm_does_not_exist(self):
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
vmid = 200
|
||||
with set_module_args(get_module_args(type="all", vmid=vmid)):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == []
|
||||
|
||||
def test_module_fail_when_qemu_request_fails(self):
|
||||
self.connect_mock.return_value.nodes.return_value.qemu.return_value.get.side_effect = IOError(
|
||||
"Some mocked connection error."
|
||||
)
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
with set_module_args(get_module_args(type="qemu")):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert "Failed to retrieve QEMU VMs information:" in result["msg"]
|
||||
|
||||
def test_module_fail_when_lxc_request_fails(self):
|
||||
self.connect_mock.return_value.nodes.return_value.lxc.return_value.get.side_effect = IOError(
|
||||
"Some mocked connection error."
|
||||
)
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
with set_module_args(get_module_args(type="lxc")):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert "Failed to retrieve LXC VMs information:" in result["msg"]
|
||||
|
||||
def test_module_fail_when_cluster_resources_request_fails(self):
|
||||
self.connect_mock.return_value.cluster.return_value.resources.return_value.get.side_effect = IOError(
|
||||
"Some mocked connection error."
|
||||
)
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
with set_module_args(get_module_args()):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert (
|
||||
"Failed to retrieve VMs information from cluster resources:"
|
||||
in result["msg"]
|
||||
)
|
||||
|
||||
def test_module_fail_when_node_does_not_exist(self):
|
||||
with pytest.raises(AnsibleFailJson) as exc_info:
|
||||
with set_module_args(get_module_args(type="all", node="NODE3")):
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["msg"] == "Node NODE3 doesn't exist in PVE cluster"
|
||||
|
||||
def test_call_to_get_vmid_is_not_used_when_vmid_provided(self):
|
||||
with patch(
|
||||
"ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible.get_vmid"
|
||||
) as get_vmid_mock:
|
||||
with pytest.raises(AnsibleExitJson):
|
||||
vmid = 100
|
||||
with set_module_args(
|
||||
get_module_args(type="all", vmid=vmid, name="something")
|
||||
):
|
||||
self.module.main()
|
||||
|
||||
assert get_vmid_mock.call_count == 0
|
||||
|
||||
def test_config_returned_when_specified_qemu_vm_with_config(self):
|
||||
config_vm_value = {
|
||||
'scsi0': 'local-lvm:vm-101-disk-0,iothread=1,size=32G',
|
||||
'net0': 'virtio=4E:79:9F:A8:EE:E4,bridge=vmbr0,firewall=1',
|
||||
'scsihw': 'virtio-scsi-single',
|
||||
'cores': 1,
|
||||
'name': 'test1',
|
||||
'ostype': 'l26',
|
||||
'boot': 'order=scsi0;ide2;net0',
|
||||
'memory': 2048,
|
||||
'sockets': 1,
|
||||
}
|
||||
(self.connect_mock.return_value.nodes.return_value.qemu.return_value.
|
||||
config.return_value.get.return_value) = config_vm_value
|
||||
|
||||
with pytest.raises(AnsibleExitJson) as exc_info:
|
||||
vmid = 101
|
||||
with set_module_args(get_module_args(
|
||||
type="qemu",
|
||||
vmid=vmid,
|
||||
config="current",
|
||||
)):
|
||||
expected_output = [vm for vm in EXPECTED_VMS_OUTPUT if vm["vmid"] == vmid]
|
||||
expected_output[0]["config"] = config_vm_value
|
||||
self.module.main()
|
||||
|
||||
result = exc_info.value.args[0]
|
||||
assert result["proxmox_vms"] == expected_output
|
|
@ -47,13 +47,6 @@ elastic-apm ; python_version >= '3.6'
|
|||
# requirements for scaleway modules
|
||||
passlib[argon2]
|
||||
|
||||
# requirements for the proxmox modules
|
||||
proxmoxer < 2.0.0 ; python_version >= '2.7' and python_version <= '3.6'
|
||||
proxmoxer ; python_version > '3.6'
|
||||
|
||||
# requirements for the proxmox_pct_remote connection plugin
|
||||
paramiko >= 3.0.0 ; python_version >= '3.6'
|
||||
|
||||
#requirements for nomad_token modules
|
||||
python-nomad < 2.0.0 ; python_version <= '3.6'
|
||||
python-nomad >= 2.0.0 ; python_version >= '3.7'
|
||||
|
@ -62,4 +55,7 @@ python-nomad >= 2.0.0 ; python_version >= '3.7'
|
|||
python-jenkins >= 0.4.12
|
||||
|
||||
# requirement for json_patch, json_patch_recipe and json_patch plugins
|
||||
jsonpatch
|
||||
jsonpatch
|
||||
|
||||
# requirements for the wsl connection plugin
|
||||
paramiko >= 3.0.0 ; python_version >= '3.6'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue