diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 24a9fc45e8..fc5f7abbdc 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -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: diff --git a/antsibull-nox.toml b/antsibull-nox.toml index a35be2b895..76e8947a02 100644 --- a/antsibull-nox.toml +++ b/antsibull-nox.toml @@ -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 diff --git a/changelogs/fragments/10155-proxmox-bugfix.yml b/changelogs/fragments/10155-proxmox-bugfix.yml deleted file mode 100644 index 79dacc4602..0000000000 --- a/changelogs/fragments/10155-proxmox-bugfix.yml +++ /dev/null @@ -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)." diff --git a/changelogs/fragments/10176-fix-proxmox_snap_timeout.yml b/changelogs/fragments/10176-fix-proxmox_snap_timeout.yml deleted file mode 100644 index 17460d5d5b..0000000000 --- a/changelogs/fragments/10176-fix-proxmox_snap_timeout.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/7402-proxmox-template-support-server-side-artifact-fetching.yaml b/changelogs/fragments/7402-proxmox-template-support-server-side-artifact-fetching.yaml deleted file mode 100644 index 4a5fefdc96..0000000000 --- a/changelogs/fragments/7402-proxmox-template-support-server-side-artifact-fetching.yaml +++ /dev/null @@ -1,2 +0,0 @@ -minor_changes: - - proxmox_template - add server side artifact fetching support (https://github.com/ansible-collections/community.general/pull/9113). \ No newline at end of file diff --git a/changelogs/fragments/9223-proxmox-backup-bugfixes.yml b/changelogs/fragments/9223-proxmox-backup-bugfixes.yml deleted file mode 100644 index 559e1f45bc..0000000000 --- a/changelogs/fragments/9223-proxmox-backup-bugfixes.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: - - proxmox_backup - fix incorrect key lookup in vmid permission check (https://github.com/ansible-collections/community.general/pull/9223). diff --git a/changelogs/fragments/9225-proxmox-module-refactoring.yml b/changelogs/fragments/9225-proxmox-module-refactoring.yml deleted file mode 100644 index 4e49037ab2..0000000000 --- a/changelogs/fragments/9225-proxmox-module-refactoring.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9228-fix-issue-header.yml b/changelogs/fragments/9228-fix-issue-header.yml deleted file mode 100644 index 450a23f8e5..0000000000 --- a/changelogs/fragments/9228-fix-issue-header.yml +++ /dev/null @@ -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/). diff --git a/changelogs/fragments/9239-proxmox-backup-refactor.yml b/changelogs/fragments/9239-proxmox-backup-refactor.yml deleted file mode 100644 index 4f73fe6dde..0000000000 --- a/changelogs/fragments/9239-proxmox-backup-refactor.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9256-proxmox_disk-fix-async-method-of-resize_disk.yml b/changelogs/fragments/9256-proxmox_disk-fix-async-method-of-resize_disk.yml deleted file mode 100644 index 0b0a826a0d..0000000000 --- a/changelogs/fragments/9256-proxmox_disk-fix-async-method-of-resize_disk.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9277-proxmox_template-fix-the-wrong-path-called-on-proxmox_template.task_status.yaml b/changelogs/fragments/9277-proxmox_template-fix-the-wrong-path-called-on-proxmox_template.task_status.yaml deleted file mode 100644 index 166c040e3b..0000000000 --- a/changelogs/fragments/9277-proxmox_template-fix-the-wrong-path-called-on-proxmox_template.task_status.yaml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9323-fstr-inventory-plugins.yml b/changelogs/fragments/9323-fstr-inventory-plugins.yml index 03ded1f0ec..26fa87209b 100644 --- a/changelogs/fragments/9323-fstr-inventory-plugins.yml +++ b/changelogs/fragments/9323-fstr-inventory-plugins.yml @@ -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). diff --git a/changelogs/fragments/9379-refactor.yml b/changelogs/fragments/9379-refactor.yml index 0a87b2d0c1..b2c68aa8b1 100644 --- a/changelogs/fragments/9379-refactor.yml +++ b/changelogs/fragments/9379-refactor.yml @@ -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)." diff --git a/changelogs/fragments/9584-py3-imports-connectioninventory.yml b/changelogs/fragments/9584-py3-imports-connectioninventory.yml index 0b50cdf052..4fbd215258 100644 --- a/changelogs/fragments/9584-py3-imports-connectioninventory.yml +++ b/changelogs/fragments/9584-py3-imports-connectioninventory.yml @@ -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). diff --git a/changelogs/fragments/9601-proxmox-template-support-for-checksums.yml b/changelogs/fragments/9601-proxmox-template-support-for-checksums.yml deleted file mode 100644 index 40ffbfbec0..0000000000 --- a/changelogs/fragments/9601-proxmox-template-support-for-checksums.yml +++ /dev/null @@ -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). \ No newline at end of file diff --git a/changelogs/fragments/9645-proxmox-fix-pubkey.yml b/changelogs/fragments/9645-proxmox-fix-pubkey.yml deleted file mode 100644 index 786daf88df..0000000000 --- a/changelogs/fragments/9645-proxmox-fix-pubkey.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9653-proxmox-kvm-allow-vm-hibernation.yml b/changelogs/fragments/9653-proxmox-kvm-allow-vm-hibernation.yml deleted file mode 100644 index b9c347e767..0000000000 --- a/changelogs/fragments/9653-proxmox-kvm-allow-vm-hibernation.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9760-proxmox-inventory.yml b/changelogs/fragments/9760-proxmox-inventory.yml deleted file mode 100644 index c0405eee32..0000000000 --- a/changelogs/fragments/9760-proxmox-inventory.yml +++ /dev/null @@ -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)." diff --git a/changelogs/fragments/9809-proxmox-fix-status-getter.yml b/changelogs/fragments/9809-proxmox-fix-status-getter.yml deleted file mode 100644 index eef4ab072e..0000000000 --- a/changelogs/fragments/9809-proxmox-fix-status-getter.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9847-Adding_audio_device-support_to_proxmox_kvm.yml b/changelogs/fragments/9847-Adding_audio_device-support_to_proxmox_kvm.yml deleted file mode 100644 index 27633edfb0..0000000000 --- a/changelogs/fragments/9847-Adding_audio_device-support_to_proxmox_kvm.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9875-proxmox-dont-expect-key-template-to-exist.yml b/changelogs/fragments/9875-proxmox-dont-expect-key-template-to-exist.yml deleted file mode 100644 index ff926e9cb3..0000000000 --- a/changelogs/fragments/9875-proxmox-dont-expect-key-template-to-exist.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9895-proxmox_tags_with_uppercase_chars.yml b/changelogs/fragments/9895-proxmox_tags_with_uppercase_chars.yml deleted file mode 100644 index d14b5e21b4..0000000000 --- a/changelogs/fragments/9895-proxmox_tags_with_uppercase_chars.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9952-proxmox-inventory-plugin-improve-ansible_host.yml b/changelogs/fragments/9952-proxmox-inventory-plugin-improve-ansible_host.yml deleted file mode 100644 index dc973835c7..0000000000 --- a/changelogs/fragments/9952-proxmox-inventory-plugin-improve-ansible_host.yml +++ /dev/null @@ -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). diff --git a/changelogs/fragments/9963-proxmox_disk-storage.yml b/changelogs/fragments/9963-proxmox_disk-storage.yml deleted file mode 100644 index a93fb7d022..0000000000 --- a/changelogs/fragments/9963-proxmox_disk-storage.yml +++ /dev/null @@ -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)." diff --git a/changelogs/fragments/deprecations.yml b/changelogs/fragments/deprecations.yml index a53e1eb394..a848bf1e75 100644 --- a/changelogs/fragments/deprecations.yml +++ b/changelogs/fragments/deprecations.yml @@ -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)." diff --git a/changelogs/fragments/proxmox-removal.yml b/changelogs/fragments/proxmox-removal.yml new file mode 100644 index 0000000000..11affae252 --- /dev/null +++ b/changelogs/fragments/proxmox-removal.yml @@ -0,0 +1,7 @@ +removed_features: + - "The Proxmox content (modules and plugins) has been moved to the `new collection 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)." diff --git a/meta/runtime.yml b/meta/runtime.yml index cfaf81ff1f..174493bc72 100644 --- a/meta/runtime.yml +++ b/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: diff --git a/plugins/connection/proxmox_pct_remote.py b/plugins/connection/proxmox_pct_remote.py deleted file mode 100644 index c46090083e..0000000000 --- a/plugins/connection/proxmox_pct_remote.py +++ /dev/null @@ -1,857 +0,0 @@ -# -*- coding: utf-8 -*- -# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan -# Copyright (c) 2024 Nils Stein (@mietzen) -# 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) -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 .' - 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 diff --git a/plugins/doc_fragments/proxmox.py b/plugins/doc_fragments/proxmox.py deleted file mode 100644 index 4641c36d3e..0000000000 --- a/plugins/doc_fragments/proxmox.py +++ /dev/null @@ -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 -""" diff --git a/plugins/inventory/proxmox.py b/plugins/inventory/proxmox.py deleted file mode 100644 index b334b0f6ba..0000000000 --- a/plugins/inventory/proxmox.py +++ /dev/null @@ -1,715 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2016 Guido Günther , Daniel Lobato Garcia -# 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) - 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 diff --git a/plugins/module_utils/proxmox.py b/plugins/module_utils/proxmox.py deleted file mode 100644 index bdd575ac26..0000000000 --- a/plugins/module_utils/proxmox.py +++ /dev/null @@ -1,242 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2020, Tristan Le Guern -# 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) - ) diff --git a/plugins/modules/proxmox.py b/plugins/modules/proxmox.py deleted file mode 100644 index a46ac04d9c..0000000000 --- a/plugins/modules/proxmox.py +++ /dev/null @@ -1,1731 +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 -short_description: Management of instances in Proxmox VE cluster -description: - - Allows you to create/delete/stop instances in Proxmox VE cluster. - - The module automatically detects containerization type (lxc for PVE 4, openvz for older). - - Since community.general 4.0.0 on, there are no more default values. -attributes: - check_mode: - support: none - diff_mode: - support: none - action_group: - version_added: 9.0.0 -options: - password: - description: - - The instance root password. - type: str - hostname: - description: - - The instance hostname. - - Required only for O(state=present). - - Must be unique if vmid is not passed. - type: str - ostemplate: - description: - - The template for VM creating. - - Required only for O(state=present). - type: str - disk: - description: - - This option was previously described as "hard disk size in GB for instance" however several formats describing a lxc - mount are permitted. - - Older versions of Proxmox will accept a numeric value for size using the O(storage) parameter to automatically choose - which storage to allocate from, however new versions enforce the C(:) syntax. - - Additional options are available by using some combination of the following key-value pairs as a comma-delimited list - C([volume=] - [,acl=<1|0>] [,mountoptions=] [,replicate=<1|0>] [,ro=<1|0>] [,shared=<1|0>] - [,size=]). - - See U(https://pve.proxmox.com/wiki/Linux_Container) for a full description. - - This option is mutually exclusive with O(disk_volume). - type: str - disk_volume: - description: - - Specify a hash/dictionary of the C(rootfs) disk. - - See U(https://pve.proxmox.com/wiki/Linux_Container#pct_mount_points) for a full description. - - This option is mutually exclusive with O(storage) and O(disk). - type: dict - version_added: 9.2.0 - suboptions: - storage: - description: - - O(disk_volume.storage) is the storage identifier of the storage to use for the C(rootfs). - - Mutually exclusive with O(disk_volume.host_path). - type: str - volume: - description: - - O(disk_volume.volume) is the name of an existing volume. - - If not defined, the module will check if one exists. If not, a new volume will be created. - - If defined, the volume must exist under that name. - - Required only if O(disk_volume.storage) is defined, and mutually exclusive with O(disk_volume.host_path). - type: str - size: - description: - - O(disk_volume.size) is the size of the storage to use. - - The size is given in GiB. - - Required only if O(disk_volume.storage) is defined, and mutually exclusive with O(disk_volume.host_path). - type: int - host_path: - description: - - O(disk_volume.host_path) defines a bind or device path on the PVE host to use for the C(rootfs). - - Mutually exclusive with O(disk_volume.storage), O(disk_volume.volume), and O(disk_volume.size). - type: path - options: - description: - - O(disk_volume.options) is a dict of extra options. - - The value of any given option must be a string, for example V("1"). - type: dict - cores: - description: - - Specify number of cores per socket. - type: int - cpus: - description: - - Number of allocated cpus for instance. - type: int - memory: - description: - - Memory size in MB for instance. - type: int - swap: - description: - - Swap memory size in MB for instance. - type: int - netif: - description: - - Specifies network interfaces for the container. As a hash/dictionary defining interfaces. - type: dict - features: - description: - - Specifies a list of features to be enabled. For valid options, see U(https://pve.proxmox.com/wiki/Linux_Container#pct_options). - - Some features require the use of a privileged container. - type: list - elements: str - version_added: 2.0.0 - startup: - description: - - Specifies the startup order of the container. - - Use C(order=#) where C(#) is a non-negative number to define the general startup order. Shutdown in done with reverse - ordering. - - Use C(up=#) where C(#) is in seconds, to specify a delay to wait before the next VM is started. - - Use C(down=#) where C(#) is in seconds, to specify a delay to wait before the next VM is stopped. - type: list - elements: str - version_added: 8.5.0 - mounts: - description: - - Specifies additional mounts (separate disks) for the container. As a hash/dictionary defining mount points as strings. - - This Option is mutually exclusive with O(mount_volumes). - type: dict - mount_volumes: - description: - - Specify additional mounts (separate disks) for the container. As a hash/dictionary defining mount points. - - See U(https://pve.proxmox.com/wiki/Linux_Container#pct_mount_points) for a full description. - - This Option is mutually exclusive with O(mounts). - type: list - elements: dict - version_added: 9.2.0 - suboptions: - id: - description: - - O(mount_volumes[].id) is the identifier of the mount point written as C(mp[n]). - type: str - required: true - storage: - description: - - O(mount_volumes[].storage) is the storage identifier of the storage to use. - - Mutually exclusive with O(mount_volumes[].host_path). - type: str - volume: - description: - - O(mount_volumes[].volume) is the name of an existing volume. - - If not defined, the module will check if one exists. If not, a new volume will be created. - - If defined, the volume must exist under that name. - - Required only if O(mount_volumes[].storage) is defined and mutually exclusive with O(mount_volumes[].host_path). - type: str - size: - description: - - O(mount_volumes[].size) is the size of the storage to use. - - The size is given in GiB. - - Required only if O(mount_volumes[].storage) is defined and mutually exclusive with O(mount_volumes[].host_path). - type: int - host_path: - description: - - O(mount_volumes[].host_path) defines a bind or device path on the PVE host to use for the C(rootfs). - - Mutually exclusive with O(mount_volumes[].storage), O(mount_volumes[].volume), and O(mount_volumes[].size). - type: path - mountpoint: - description: - - O(mount_volumes[].mountpoint) is the mount point of the volume. - type: path - required: true - options: - description: - - O(mount_volumes[].options) is a dict of extra options. - - The value of any given option must be a string, for example V("1"). - type: dict - ip_address: - description: - - Specifies the address the container will be assigned. - type: str - onboot: - description: - - Specifies whether a VM will be started during system bootup. - type: bool - storage: - description: - - Target storage. - - This option is mutually exclusive with O(disk_volume) and O(mount_volumes). - type: str - default: 'local' - ostype: - description: - - Specifies the C(ostype) of the LXC container. - - If set to V(auto), no C(ostype) will be provided on instance creation. - choices: ['auto', 'debian', 'devuan', 'ubuntu', 'centos', 'fedora', 'opensuse', 'archlinux', 'alpine', 'gentoo', 'nixos', - 'unmanaged'] - type: str - default: 'auto' - version_added: 8.1.0 - cpuunits: - description: - - CPU weight for a VM. - type: int - nameserver: - description: - - Sets DNS server IP address for a container. - type: str - searchdomain: - description: - - Sets DNS search domain for a container. - type: str - tags: - description: - - List of tags to apply to the container. - - Tags must start with V([a-z0-9_]) followed by zero or more of the following characters V([a-z0-9_-+.]). - - Tags are only available in Proxmox 7+. - type: list - elements: str - version_added: 6.2.0 - timeout: - description: - - Timeout for operations. - type: int - default: 30 - update: - description: - - If V(true), the container will be updated with new values. - - If V(false), it will not be updated. - - The default changed from V(false) to V(true) in community.general 11.0.0. - type: bool - version_added: 8.1.0 - default: true - force: - description: - - Forcing operations. - - Can be used only with states V(present), V(stopped), V(restarted). - - With O(state=present) force option allow to overwrite existing container. - - With states V(stopped), V(restarted) allow to force stop instance. - type: bool - default: false - purge: - description: - - Remove container from all related configurations. - - For example backup jobs, replication jobs, or HA. - - Related ACLs and Firewall entries will always be removed. - - Used with O(state=absent). - type: bool - default: false - version_added: 2.3.0 - state: - description: - - Indicate desired state of the instance. - - V(template) was added in community.general 8.1.0. - type: str - choices: ['present', 'started', 'absent', 'stopped', 'restarted', 'template'] - default: present - pubkey: - description: - - Public key to add to /root/.ssh/authorized_keys. This was added on Proxmox 4.2, it is ignored for earlier versions. - type: str - unprivileged: - description: - - Indicate if the container should be unprivileged. - - The default change to V(true) in community.general 7.0.0. It used to be V(false) before. - type: bool - default: true - description: - description: - - Specify the description for the container. Only used on the configuration web interface. - - This is saved as a comment inside the configuration file. - type: str - version_added: '0.2.0' - hookscript: - description: - - Script that will be executed during various steps in the containers lifetime. - type: str - version_added: '0.2.0' - timezone: - description: - - Timezone used by the container, accepts values like V(Europe/Paris). - - The special value V(host) configures the same timezone used by Proxmox host. - type: str - version_added: '7.1.0' - clone: - description: - - ID of the container to be cloned. - - O(description), O(hostname), and O(pool) will be copied from the cloned container if not specified. - - The type of clone created is defined by the O(clone_type) parameter. - - This operator is only supported for Proxmox clusters that use LXC containerization (PVE version >= 4). - type: int - version_added: 4.3.0 - clone_type: - description: - - Type of the clone created. - - V(full) creates a full clone, and O(storage) must be specified. - - V(linked) creates a linked clone, and the cloned container must be a template container. - - V(opportunistic) creates a linked clone if the cloned container is a template container, and a full clone if not. - O(storage) may be specified, if not it will fall back to the default. - type: str - choices: ['full', 'linked', 'opportunistic'] - default: opportunistic - version_added: 4.3.0 -author: Sergei Antipov (@UnderGreen) -seealso: - - module: community.general.proxmox_vm_info -extends_documentation_fragment: - - community.general.proxmox.actiongroup_proxmox - - community.general.proxmox.documentation - - community.general.proxmox.selection - - community.general.attributes -""" - -EXAMPLES = r""" -- name: Create new container with minimal options - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - -- name: Create new container with minimal options specifying disk storage location and size - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - disk: 'local-lvm:20' - -- name: Create new container with minimal options specifying disk storage location and size via disk_volume - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - disk_volume: - storage: local - size: 20 - -- name: Create new container with hookscript and description - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - hookscript: 'local:snippets/vm_hook.sh' - description: created with ansible - -- name: Create new container automatically selecting the next available vmid. - community.general.proxmox: - node: 'uk-mc02' - api_user: 'root@pam' - api_password: '1q2w3e' - api_host: 'node1' - password: '123456' - hostname: 'example.org' - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - -- name: Create new container with minimal options with force(it will rewrite existing container) - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - force: true - -- name: Create new container with minimal options use environment PROXMOX_PASSWORD variable(you should export it before) - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - -- name: Create new container with minimal options defining network interface with dhcp - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - netif: - net0: "name=eth0,ip=dhcp,ip6=dhcp,bridge=vmbr0" - -- name: Create new container with minimal options defining network interface with static ip - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - netif: - net0: "name=eth0,gw=192.168.0.1,ip=192.168.0.2/24,bridge=vmbr0" - -- name: Create new container with more options defining network interface with static ip4 and ip6 with vlan-tag and mtu - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - netif: - net0: "name=eth0,gw=192.168.0.1,ip=192.168.0.2/24,ip6=fe80::1227/64,gw6=fe80::1,bridge=vmbr0,firewall=1,tag=934,mtu=1500" - -- name: Create new container with minimal options defining a mount with 8GB - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - mounts: - mp0: "local:8,mp=/mnt/test/" - -- name: Create new container with minimal options defining a mount with 8GB using mount_volumes - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - mount_volumes: - - id: mp0 - storage: local - size: 8 - mountpoint: /mnt/test - -- name: Create new container with minimal options defining a cpu core limit - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - cores: 2 - -- name: Create new container with minimal options and same timezone as proxmox host - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - timezone: host - -- name: Create a new container with nesting enabled and allows the use of CIFS/NFS inside the container. - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - password: 123456 - hostname: example.org - ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' - features: - - nesting=1 - - mount=cifs,nfs - -- name: > - Create a linked clone of the template container with id 100. The newly created container with be a - linked clone, because no storage parameter is defined - community.general.proxmox: - vmid: 201 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - clone: 100 - hostname: clone.example.org - -- name: Create a full clone of the container with id 100 - community.general.proxmox: - vmid: 201 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - clone: 100 - hostname: clone.example.org - storage: local - -- name: Update container configuration - community.general.proxmox: - vmid: 100 - node: uk-mc02 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - netif: - net0: "name=eth0,gw=192.168.0.1,ip=192.168.0.3/24,bridge=vmbr0" - update: true - -- name: Start container - community.general.proxmox: - vmid: 100 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - state: started - -- name: > - Start container with mount. You should enter a 90-second timeout because servers - with additional disks take longer to boot - community.general.proxmox: - vmid: 100 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - state: started - timeout: 90 - -- name: Stop container - community.general.proxmox: - vmid: 100 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - state: stopped - -- name: Stop container with force - community.general.proxmox: - vmid: 100 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - force: true - state: stopped - -- name: Restart container(stopped or mounted container you can't restart) - community.general.proxmox: - vmid: 100 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - state: restarted - -- name: Convert container to template - community.general.proxmox: - vmid: 100 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - state: template - -- name: Convert container to template (stop container if running) - community.general.proxmox: - vmid: 100 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - state: template - force: true - -- name: Remove container - community.general.proxmox: - vmid: 100 - api_user: root@pam - api_password: 1q2w3e - api_host: node1 - state: absent -""" - -import re -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, - ansible_to_proxmox_bool, - proxmox_auth_argument_spec, -) -from ansible_collections.community.general.plugins.module_utils.version import LooseVersion - - -def get_proxmox_args(): - return dict( - vmid=dict(type="int", required=False), - node=dict(), - pool=dict(), - password=dict(no_log=True), - hostname=dict(), - ostemplate=dict(), - disk=dict(type="str"), - disk_volume=dict( - type="dict", - options=dict( - storage=dict(type="str"), - volume=dict(type="str"), - size=dict(type="int"), - host_path=dict(type="path"), - options=dict(type="dict"), - ), - required_together=[("storage", "size")], - required_by={ - "volume": ("storage", "size"), - }, - mutually_exclusive=[ - ("host_path", "storage"), - ("host_path", "volume"), - ("host_path", "size"), - ], - ), - cores=dict(type="int"), - cpus=dict(type="int"), - memory=dict(type="int"), - swap=dict(type="int"), - netif=dict(type="dict"), - mounts=dict(type="dict"), - mount_volumes=dict( - type="list", - elements="dict", - options=dict( - id=(dict(type="str", required=True)), - storage=dict(type="str"), - volume=dict(type="str"), - size=dict(type="int"), - host_path=dict(type="path"), - mountpoint=dict(type="path", required=True), - options=dict(type="dict"), - ), - required_together=[("storage", "size")], - required_by={ - "volume": ("storage", "size"), - }, - mutually_exclusive=[ - ("host_path", "storage"), - ("host_path", "volume"), - ("host_path", "size"), - ], - ), - ip_address=dict(), - ostype=dict( - default="auto", - choices=[ - "auto", - "debian", - "devuan", - "ubuntu", - "centos", - "fedora", - "opensuse", - "archlinux", - "alpine", - "gentoo", - "nixos", - "unmanaged", - ], - ), - onboot=dict(type="bool"), - features=dict(type="list", elements="str"), - startup=dict(type="list", elements="str"), - storage=dict(default="local"), - cpuunits=dict(type="int"), - nameserver=dict(), - searchdomain=dict(), - timeout=dict(type="int", default=30), - update=dict(type="bool", default=True), - force=dict(type="bool", default=False), - purge=dict(type="bool", default=False), - state=dict( - default="present", - choices=[ - "present", - "absent", - "stopped", - "started", - "restarted", - "template", - ], - ), - pubkey=dict(type="str"), - unprivileged=dict(type="bool", default=True), - description=dict(type="str"), - hookscript=dict(type="str"), - timezone=dict(type="str"), - clone=dict(type="int"), - clone_type=dict( - default="opportunistic", choices=["full", "linked", "opportunistic"] - ), - tags=dict(type="list", elements="str"), - ) - - -def get_ansible_module(): - module_args = proxmox_auth_argument_spec() - module_args.update(get_proxmox_args()) - - return AnsibleModule( - argument_spec=module_args, - required_if=[ - ("state", "present", ["node", "hostname"]), - # Require one of clone, ostemplate, or update. - # Together with mutually_exclusive this ensures that we either - # clone a container or create a new one from a template file. - ("state", "present", ("clone", "ostemplate", "update"), True), - ], - required_together=[("api_token_id", "api_token_secret")], - required_one_of=[ - ("api_password", "api_token_id"), - ("vmid", "hostname"), - ], - mutually_exclusive=[ - # Creating a new container is done either by cloning an existing one, or based on a template. - ("clone", "ostemplate", "update"), - ("disk", "disk_volume"), - ("storage", "disk_volume"), - ("mounts", "mount_volumes"), - ], - ) - - -class ProxmoxLxcAnsible(ProxmoxAnsible): - MINIMUM_VERSIONS = { - "disk_volume": "5.0", - "mount_volumes": "5.0", - "tags": "6.1", - "timezone": "6.3", - } - - def __init__(self, module): - super(ProxmoxLxcAnsible, self).__init__(module) - - self.VZ_TYPE = "openvz" if self.version() < LooseVersion("4.0") else "lxc" - self.params = self.module.params - - def run(self): - self.check_supported_features() - - state = self.params.get("state") - - vmid = self.params.get("vmid") - hostname = self.params.get("hostname") - - if not vmid and not hostname: - self.module.fail_json(msg="Either VMID or hostname must be provided.") - - if state == "present": - self.lxc_present( - vmid, - hostname, - node=self.params.get("node"), - update=self.params.get("update"), - force=self.params.get("force"), - ) - elif state == "absent": - self.lxc_absent( - vmid, - hostname, - node=self.params.get("node"), - timeout=self.params.get("timeout"), - purge=self.params.get("purge"), - ) - elif state == "started": - self.lxc_started( - vmid, - hostname, - node=self.params.get("node"), - timeout=self.params.get("timeout"), - ) - elif state == "stopped": - self.lxc_stopped( - vmid, - hostname, - node=self.params.get("node"), - timeout=self.params.get("timeout"), - force=self.params.get("force"), - ) - elif state == "restarted": - self.lxc_restarted( - vmid, - hostname, - node=self.params.get("node"), - timeout=self.params.get("timeout"), - force=self.params.get("force"), - ) - elif state == "template": - self.lxc_to_template( - vmid, - hostname, - node=self.params.get("node"), - timeout=self.params.get("timeout"), - force=self.params.get("force"), - ) - - def lxc_present(self, vmid, hostname, node, update, force): - try: - lxc = self.get_lxc_resource(vmid, hostname) - vmid = vmid or lxc["id"].split("/")[-1] - node = node or lxc["node"] - except LookupError: - lxc = None - vmid = vmid or self.get_nextvmid() - - if node is None: - raise ValueError( - "Argument 'node' is None, but should be found from VMID/hostname or provided." - ) - - # check if the container exists already - if lxc is not None: - if update: - # Update it if we should - identifier = self.format_vm_identifier(vmid, hostname) - self.update_lxc_instance( - vmid, - node, - cores=self.params.get("cores"), - cpus=self.params.get("cpus"), - cpuunits=self.params.get("cpuunits"), - description=self.params.get("description"), - disk=self.params.get("disk"), - disk_volume=self.params.get("disk_volume"), - features=self.params.get("features"), - hookscript=self.params.get("hookscript"), - hostname=self.params.get("hostname"), - ip_address=self.params.get("ip_address"), - memory=self.params.get("memory"), - mounts=self.params.get("mounts"), - mount_volumes=self.params.get("mount_volumes"), - nameserver=self.params.get("nameserver"), - netif=self.params.get("netif"), - onboot=ansible_to_proxmox_bool(self.params.get("onboot")), - searchdomain=self.params.get("searchdomain"), - startup=self.params.get("startup"), - swap=self.params.get("swap"), - tags=self.params.get("tags"), - timezone=self.params.get("timezone"), - ) - self.module.exit_json( - changed=True, vmid=vmid, msg="VM %s has been updated." % identifier - ) - elif not force: - # We're done if it shouldn't be forcefully created - identifier = self.format_vm_identifier(vmid, lxc["name"]) - self.module.exit_json( - changed=False, vmid=vmid, msg="VM %s already exists." % identifier - ) - identifier = self.format_vm_identifier(vmid, lxc["name"]) - self.module.debug( - "VM %s already exists, but we don't update and instead forcefully recreate it." - % identifier - ) - - self.new_lxc_instance( - vmid, - hostname, - node=self.params.get("node"), - clone_from=self.params.get("clone"), - ostemplate=self.params.get("ostemplate"), - force=force, - ) - - def lxc_absent(self, vmid, hostname, node, timeout, purge): - try: - lxc = self.get_lxc_resource(vmid, hostname) - except LookupError: - identifier = self.format_vm_identifier(vmid, hostname) - self.module.exit_json( - changed=False, vmid=vmid, msg="VM %s is already absent." % (identifier) - ) - - vmid = vmid or lxc["id"].split("/")[-1] - node = node or lxc["node"] - - lxc_status = self.get_lxc_status(vmid, node) - identifier = self.format_vm_identifier(vmid, hostname) - - if lxc_status == "running": - self.module.exit_json( - changed=False, - vmid=vmid, - msg="VM %s is running. Stop it before deletion." % identifier, - ) - if lxc_status == "mounted": - self.module.exit_json( - changed=False, - vmid=vmid, - msg="VM %s is mounted. Stop it with force option before deletion." - % identifier, - ) - - self.remove_lxc_instance(vmid, node, timeout, purge) - self.module.exit_json( - changed=True, vmid=vmid, msg="VM %s removed." % identifier - ) - - def lxc_started(self, vmid, hostname, node, timeout): - lxc = self.get_lxc_resource(vmid, hostname) - vmid = vmid or lxc["id"].split("/")[-1] - hostname = hostname or lxc["name"] - identifier = self.format_vm_identifier(vmid, hostname) - node = node or lxc["node"] - lxc_status = self.get_lxc_status(vmid, lxc["node"]) - - if lxc_status == "running": - self.module.exit_json( - changed=False, vmid=vmid, msg="VM %s is already running." % identifier - ) - - self.start_lxc_instance(vmid, node, timeout) - self.module.exit_json( - changed=True, vmid=vmid, msg="VM %s started." % identifier - ) - - def lxc_stopped(self, vmid, hostname, node, timeout, force): - lxc = self.get_lxc_resource(vmid, hostname) - vmid = vmid or lxc["id"].split("/")[-1] - hostname = hostname or lxc["name"] - identifier = self.format_vm_identifier(vmid, hostname) - node = node or lxc["node"] - lxc_status = self.get_lxc_status(vmid, node) - - if lxc_status == "mounted": - if force: - self.umount_lxc_instance(vmid, hostname, timeout) - else: - self.module.exit_json( - changed=False, - vmid=vmid, - msg="VM %s is already stopped, but mounted. Use force option to umount it." - % identifier, - ) - - if lxc_status == "stopped": - self.module.exit_json( - changed=False, vmid=vmid, msg="VM %s is already stopped." % identifier - ) - - self.stop_lxc_instance(vmid, node, timeout, force) - self.module.exit_json( - changed=True, vmid=vmid, msg="VM %s stopped." % identifier - ) - - def lxc_restarted(self, vmid, hostname, node, timeout, force): - lxc = self.get_lxc_resource(vmid, hostname) - - vmid = vmid or lxc["id"].split("/")[-1] - hostname = hostname or lxc["name"] - node = node or lxc["node"] - - identifier = self.format_vm_identifier(vmid, hostname) - lxc_status = self.get_lxc_status(vmid, node) - - if lxc_status in ["stopped", "mounted"]: - self.module.exit_json( - changed=False, vmid=vmid, msg="VM %s is not running." % identifier - ) - - self.stop_lxc_instance(vmid, node, timeout, force) - self.start_lxc_instance(vmid, node, timeout) - self.module.exit_json( - changed=True, vmid=vmid, msg="VM %s is restarted." % identifier - ) - - def lxc_to_template(self, vmid, hostname, node, timeout, force): - lxc = self.get_lxc_resource(vmid, hostname) - vmid = vmid or lxc["id"].split("/")[-1] - hostname = hostname or lxc["name"] - node = node or lxc["node"] - identifier = self.format_vm_identifier(vmid, hostname) - - if self.is_template_container(node, vmid): - self.module.exit_json( - changed=False, - vmid=vmid, - msg="VM %s is already a template." % identifier, - ) - - lxc_status = self.get_lxc_status(vmid, node) - if lxc_status == "running" and force: - self.stop_instance(vmid, hostname, node, timeout, force) - - proxmox_node = self.proxmox_api.nodes(node) - getattr(proxmox_node, self.VZ_TYPE)(vmid).template.post() - self.module.exit_json( - changed=True, vmid=vmid, msg="VM %s converted to template." % identifier - ) - - def update_lxc_instance(self, vmid, node, **kwargs): - if self.VZ_TYPE != "lxc": - self.module.fail_json( - msg="Updating LXC containers is only supported for LXC-enabled clusters in PVE 4.0 and above." - ) - - kwargs = {k: v for k, v in kwargs.items() if v is not None} - - self.validate_tags(kwargs.get("tags", [])) - - if "features" in kwargs: - kwargs["features"] = ",".join(kwargs.pop("features")) - if "startup" in kwargs: - kwargs["startup"] = ",".join(kwargs.pop("startup")) - - disk_updates = self.process_disk_keys( - vmid, - node, - kwargs.pop("disk", None), - kwargs.pop("disk_volume", None), - ) - mounts_updates = self.process_mount_keys( - vmid, - node, - kwargs.pop("mounts", None), - kwargs.pop("mount_volumes", None), - ) - kwargs.update(disk_updates) - kwargs.update(mounts_updates) - - if "cpus" in kwargs: - kwargs["cpulimit"] = kwargs.pop("cpus") - if "netif" in kwargs: - kwargs.update(kwargs.pop("netif")) - - if "pubkey" in kwargs: - pubkey = kwargs.pop("pubkey") - if self.version() >= LooseVersion("4.2"): - kwargs["ssh-public-keys"] = pubkey - else: - self.module.warn( - "'pubkey' is not supported for PVE 4.1 and below. Ignoring keyword." - ) - - # fetch current config - proxmox_node = self.proxmox_api.nodes(node) - current_config = getattr(proxmox_node, self.VZ_TYPE)(vmid).config.get() - - # create diff between the current and requested config - diff = {} - for arg, value in kwargs.items(): - # if the arg isn't in the current config, it needs to be added - if arg not in current_config: - diff[arg] = value - elif isinstance(value, str): - # compare all string values as lists as some of them may be lists separated by commas. order doesn't matter - current_values = current_config[arg].split(",") - requested_values = value.split(",") - for new_value in requested_values: - if new_value not in current_values: - diff[arg] = value - break - # if it's not a list (or string) just compare the values - # some types don't match with the API, so force a string comparison - elif str(value) != str(current_config[arg]): - diff[arg] = value - - if not diff: - self.module.exit_json( - changed=False, vmid=vmid, msg="Container config is already up to date." - ) - - # update the config - getattr(proxmox_node, self.VZ_TYPE)(vmid).config.put( - vmid=vmid, node=node, **kwargs - ) - - def new_lxc_instance(self, vmid, hostname, node, clone_from, ostemplate, force): - identifier = self.format_vm_identifier(vmid, hostname) - - if clone_from is not None: - self.clone_lxc_instance( - vmid, - node, - clone_from, - clone_type=self.params.get("clone_type"), - timeout=self.params.get("timeout"), - description=self.params.get("description"), - hostname=hostname, - pool=self.params.get("pool"), - storage=self.params.get("storage"), - ) - self.module.exit_json( - changed=True, - vmid=vmid, - msg="Cloned VM %s from %d" % (identifier, clone_from), - ) - - if ostemplate is not None: - self.create_lxc_instance( - vmid, - node, - ostemplate, - timeout=self.params.get("timeout"), - cores=self.params.get("cores"), - cpus=self.params.get("cpus"), - cpuunits=self.params.get("cpuunits"), - description=self.params.get("description"), - disk=self.params.get("disk"), - disk_volume=self.params.get("disk_volume"), - features=self.params.get("features"), - force=ansible_to_proxmox_bool(force), - hookscript=self.params.get("hookscript"), - hostname=hostname, - ip_address=self.params.get("ip_address"), - memory=self.params.get("memory"), - mounts=self.params.get("mounts"), - mount_volumes=self.params.get("mount_volumes"), - nameserver=self.params.get("nameserver"), - netif=self.params.get("netif"), - onboot=ansible_to_proxmox_bool(self.params.get("onboot")), - ostype=self.params.get("ostype"), - password=self.params.get("password"), - pool=self.params.get("pool"), - pubkey=self.params.get("pubkey"), - searchdomain=self.params.get("searchdomain"), - startup=self.params.get("startup"), - storage=self.params.get("storage"), - swap=self.params.get("swap"), - tags=self.params.get("tags"), - timezone=self.params.get("timezone"), - unprivileged=ansible_to_proxmox_bool(self.params.get("unprivileged")), - ) - self.module.exit_json( - changed=True, - vmid=vmid, - msg="Created VM %s from template %s" % (identifier, ostemplate), - ) - - self.module.fail_json( - vmid=vmid, - msg="VM %s does not exist but neither clone nor ostemplate were specified!" - % identifier, - ) - - def create_lxc_instance(self, vmid, node, ostemplate, timeout, **kwargs): - template_store = ostemplate.split(":")[0] - if not self.content_check(node, ostemplate, template_store): - self.module.fail_json( - vmid=vmid, - msg="ostemplate %s does not exist on node %s and storage %s." - % (ostemplate, node, template_store), - ) - - disk_updates = self.process_disk_keys( - vmid, - node, - kwargs.pop("disk"), - kwargs.pop("disk_volume"), - ) - mounts_updates = self.process_mount_keys( - vmid, - node, - kwargs.pop("mounts"), - kwargs.pop("mount_volumes"), - ) - kwargs.update(disk_updates) - kwargs.update(mounts_updates) - - # Remove empty values from kwargs - kwargs = {k: v for k, v in kwargs.items() if v is not None} - - if "features" in kwargs: - kwargs["features"] = ",".join(kwargs.pop("features")) - - if "startup" in kwargs: - kwargs["startup"] = ",".join(kwargs.pop("startup")) - - self.validate_tags(kwargs.get("tags", [])) - - if self.VZ_TYPE == "lxc": - if "cpus" in kwargs: - kwargs["cpuunits"] = kwargs.pop("cpus") - kwargs.update(kwargs.pop("netif", {})) - else: - if "mount_volumes" in kwargs: - kwargs.pop("mount_volumes") - self.module.warn( - "'mount_volumes' is not supported for non-LXC clusters. Ignoring keyword." - ) - - if "pubkey" in kwargs: - pubkey = kwargs.pop("pubkey") - if self.version() >= LooseVersion("4.2"): - kwargs["ssh-public-keys"] = pubkey - else: - self.module.warn( - "'pubkey' is not supported for PVE 4.1 and below. Ignoring keyword." - ) - - if kwargs.get("ostype") == "auto": - kwargs.pop("ostype") - - proxmox_node = self.proxmox_api.nodes(node) - taskid = getattr(proxmox_node, self.VZ_TYPE).create( - vmid=vmid, ostemplate=ostemplate, **kwargs - ) - self.handle_api_timeout( - vmid, - node, - taskid, - timeout, - "Reached timeout while waiting for creation of VM %s from template %s" - % (vmid, ostemplate), - ) - - def clone_lxc_instance(self, vmid, node, clone_from, clone_type, timeout, **kwargs): - if self.VZ_TYPE != "lxc": - self.module.fail_json( - msg="Cloning is only supported for LXC-enabled clusters in PVE 4.0 and above." - ) - - # Remove empty values from kwargs - kwargs = {k: v for k, v in kwargs.items() if v is not None} - - target_is_template = self.is_template_container(node, clone_from) - # By default, create a full copy only when the cloned container is not a template. - create_full_copy = not target_is_template - - # Only accept parameters that are compatible with the clone endpoint. - valid_clone_parameters = ["hostname", "pool", "description"] - - if "storage" not in kwargs and target_is_template: - # Cloning a template, so create a full copy instead of a linked copy - create_full_copy = True - elif "storage" not in kwargs and not target_is_template: - self.module.fail_json( - changed=False, - msg="Clone target container is not a template, storage needs to be specified.", - ) - - if clone_type == "linked" and not target_is_template: - self.module.fail_json( - changed=False, - msg="Cloning type 'linked' is only supported for template containers.", - ) - elif clone_type == "opportunistic" and not target_is_template: - # Cloned container is not a template, so we need our 'storage' parameter - valid_clone_parameters.append("storage") - elif clone_type == "full": - create_full_copy = True - valid_clone_parameters.append("storage") - - clone_parameters = {} - clone_parameters["full"] = ansible_to_proxmox_bool(create_full_copy) - - for param in valid_clone_parameters: - if param in kwargs: - clone_parameters[param] = kwargs[param] - - proxmox_node = self.proxmox_api.nodes(node) - taskid = getattr(proxmox_node, self.VZ_TYPE)(clone_from).clone.post( - newid=vmid, **clone_parameters - ) - self.handle_api_timeout( - vmid, - node, - taskid, - timeout, - timeout_msg="Reached timeout while waiting for VM to clone.", - ) - - def start_lxc_instance(self, vmid, node, timeout): - proxmox_node = self.proxmox_api.nodes(node) - taskid = getattr(proxmox_node, self.VZ_TYPE)(vmid).status.start.post() - - self.handle_api_timeout( - vmid, - node, - taskid, - timeout, - timeout_msg="Reached timeout while waiting for VM to start.", - ) - - def stop_lxc_instance(self, vmid, node, timeout, force): - stop_params = {} - if force: - stop_params["forceStop"] = 1 - - proxmox_node = self.proxmox_api.nodes(node) - taskid = getattr(proxmox_node, self.VZ_TYPE)(vmid).status.shutdown.post( - **stop_params - ) - - self.handle_api_timeout( - vmid, - node, - taskid, - timeout, - timeout_msg="Reached timeout while waiting for VM to stop.", - ) - - def umount_lxc_instance(self, vmid, node, timeout): - proxmox_node = self.proxmox_api.nodes(node) - taskid = getattr(proxmox_node, self.VZ_TYPE)(vmid).status.unmount.post() - - self.handle_api_timeout( - vmid, - node, - taskid, - timeout, - timeout_msg="Reached timeout while waiting for VM to be unmounted.", - ) - - def remove_lxc_instance(self, vmid, node, timeout, purge): - delete_params = {} - if purge: - delete_params["purge"] = 1 - - proxmox_node = self.proxmox_api.nodes(node) - taskid = getattr(proxmox_node, self.VZ_TYPE).delete(vmid, **delete_params) - - self.handle_api_timeout( - vmid, - node, - taskid, - timeout, - timeout_msg="Reached timeout while waiting for VM to be removed.", - ) - - def process_disk_keys(self, vmid, node, disk, disk_volume): - """ - Process disk keys and return a formatted disk volume with the `rootfs` key. - - Args: - vmid (int): VM identifier. - node (str): Node identifier. - disk (str, optional): Disk key in the format 'storage:volume'. Defaults to None. - disk_volume (Dict[str, Any], optional): Disk volume data. Defaults to None. - - Returns: - Dict[str, str]: Formatted disk volume with the `rootfs` or `disk` key (depending on the `VZ_TYPE`), or an empty dict if no disk volume is specified. - """ - if disk is None and disk_volume is None: - return {} - - disk_dict = {} - - if disk is not None: - if disk.isdigit(): - disk_dict["rootfs"] = disk - else: - disk_volume = self.parse_disk_string(disk) - - if disk_volume is not None: - disk_dict = self.build_volume(vmid, node, key="rootfs", **disk_volume) - - if self.VZ_TYPE != "lxc": - disk_dict["disk"] = disk_dict.pop("rootfs") - - return disk_dict - - def process_mount_keys(self, vmid, node, mounts, mount_volumes): - """ - Process mount keys and return a formatted mount volumes with the `mp[n]` keys. - - Args: - vmid (str): VM identifier. - node (str): Node identifier. - mounts (str, optional): Mount key in the format 'pool:volume'. Defaults to None. - mount_volumes (Dict[str, Any], optional): Mount volume data. Defaults to None. - - Returns: - Dict[str, str]: Formatted mount volumes with the `mp[n]` keys, or an empty dict if no mount volumes are specified. - """ - if mounts is not None: - mount_volumes = [] - for mount_key, mount_string in mounts.items(): - mount_config = self.parse_disk_string(mount_string) - mount_volumes.append(dict(id=mount_key, **mount_config)) - elif mount_volumes is None or mount_volumes == []: - return {} - - mounts_dict = {} - for mount_config in mount_volumes: - mount_key = mount_config.pop("id") - mount_dict = self.build_volume(vmid, node, key=mount_key, **mount_config) - mounts_dict.update(mount_dict) - - return mounts_dict - - def parse_disk_string(self, disk_string): - """ - Parse a disk string and return a dictionary with the disk details. - - Args: - disk_string (str): Disk string. - - Returns: - Dict[str, Any]: Disk details. - - Note: Below are some example disk strings that this function MUST be able to parse: - "acl=0,thin1:base-100-disk-1,size=8G" - "thin1:10,backup=0" - "local:20" - "local-lvm:0.50" - "tmp-dir:300/subvol-300-disk-0.subvol,acl=1,size=0T" - "tmplog-dir:300/vm-300-disk-0.raw,mp=/var/log,mountoptions=noatime,size=32M" - "volume=local-lvm:base-100-disk-1,size=20G" - "/mnt/bindmounts/shared,mp=/shared" - "volume=/dev/USB01,mp=/mnt/usb01" - """ - args = disk_string.split(",") - # If the volume is not explicitly defined but implicit by only passing a key, - # add the "volume=" key prefix for ease of parsing. - args = ["volume=" + arg if "=" not in arg else arg for arg in args] - # Then create a dictionary from the arguments - disk_kwargs = dict(map(lambda item: item.split("="), args)) - - VOLUME_PATTERN = r"""(?x) - ^ - (?: - (?: - (?P[\w\-.]+): - (?: - (?P\d+\.?\d*)| - (?P[^,\s]+) - ) - )| - (?P[^,\s]+) - ) - $ - """ - # DISCLAIMER: - # There are two things called a "volume": - # 1. The "volume" key which describes the storage volume, device or directory to mount into the container. - # 2. The storage volume of a storage-backed mount point in the PVE storage sub system. - # In this section, we parse the "volume" key and check which type of mount point we are dealing with. - pattern = re.compile(VOLUME_PATTERN) - volume_string = disk_kwargs.pop("volume") - match = pattern.match(volume_string) - if match is None: - raise ValueError(("Invalid volume string: %s", volume_string)) - match_dict = match.groupdict() - match_dict = {k: v for k, v in match_dict.items() if v is not None} - - if "storage" in match_dict and "volume" in match_dict: - disk_kwargs["storage"] = match_dict["storage"] - disk_kwargs["volume"] = match_dict["volume"] - elif "storage" in match_dict and "size" in match_dict: - disk_kwargs["storage"] = match_dict["storage"] - disk_kwargs["size"] = match_dict["size"] - elif "host_path" in match_dict: - disk_kwargs["host_path"] = match_dict["host_path"] - - # Pattern matching only available in Python 3.10+ - # TODO: Uncomment the following code once only Python 3.10+ is supported - # match match_dict: - # case {"storage": storage, "volume": volume}: - # disk_kwargs["storage"] = storage - # disk_kwargs["volume"] = volume - - # case {"storage": storage, "size": size}: - # disk_kwargs["storage"] = storage - # disk_kwargs["size"] = size - - # case {"host_path": host_path}: - # disk_kwargs["host_path"] = host_path - - return disk_kwargs - - def build_volume(self, vmid, node, key, storage=None, volume=None, host_path=None, size=None, mountpoint=None, options=None, **kwargs): - """ - Build a volume string for the specified VM. - - Args: - vmid (str): The VM ID. - node (str): The node where the VM resides. - key (str): The key for the volume in the VM's config. - storage (str, optional): The storage pool where the volume resides. Defaults to None. - volume (str, optional): The name of the volume. Defaults to None. - host_path (str, optional): The host path to mount. Defaults to None. - size (str | int, optional): The size of the volume in GiB. Defaults to None. - mountpoint (str, optional): The mountpoint for the volume. Defaults to None. - options (Dict[str, Any], optional): Additional options for the volume. Defaults to None. - **kwargs: Additional keyword arguments. - - Returns: - Dict[str, str]: The built volume string in the format {'volume_key': 'volume_string'}. - - Note: Further documentation can be found in the proxmox-api documentation: https://pve.proxmox.com/wiki/Linux_Container#pct_mount_points - Note: To build a valid volume string, we need ONE of the following: - A volume name, storage name, and size - Only a storage name and size (to create a new volume or assign the volume automatically) - A host directory to mount into the container - """ - if isinstance(size, int): - size = str(size) - if size is not None and isfloat(size): - size += "G" # default to GiB - # Handle volume checks/creation - # TODO: Change the code below to pattern matching once only Python 3.10+ is supported - # 1. Check if defined volume exists - if volume is not None: - storage_content = self.get_storage_content(node, storage, vmid=vmid) - vol_ids = [vol["volid"] for vol in storage_content] - volid = "{storage}:{volume}".format(storage=storage, volume=volume) - if volid not in vol_ids: - self.module.fail_json( - changed=False, - msg="Storage {storage} does not contain volume {volume}".format( - storage=storage, - volume=volume, - ), - ) - vol_string = "{storage}:{volume},size={size}".format( - storage=storage, volume=volume, size=size - ) - # 2. If volume not defined (but storage is), check if it exists - elif storage is not None: - proxmox_node = self.proxmox_api.nodes( - node - ) # The node must exist, but not the LXC - try: - vol = proxmox_node.lxc(vmid).get("config").get(key) - volume = self.parse_disk_string(vol).get("volume") - vol_string = "{storage}:{volume},size={size}".format( - storage=storage, volume=volume, size=size - ) - - # If not, we have proxmox create one using the special syntax - except Exception: - if size is None: - raise ValueError( - "Size must be provided for storage-backed volume creation." - ) - elif size.endswith("G"): - size = size.rstrip("G") - vol_string = "{storage}:{size}".format(storage=storage, size=size) - else: - raise ValueError( - "Size must be provided in GiB for storage-backed volume creation. Convert it to GiB or allocate a new storage manually." - ) - # 3. If we have a host_path, we don't have storage, a volume, or a size - # Then we don't have to do anything, just build and return the vol_string - elif host_path is not None: - vol_string = "" - else: - raise ValueError( - "Could not build a valid volume string. One of volume, storage, or host_path must be provided." - ) - - if host_path is not None: - vol_string += "," + host_path - - if mountpoint is not None: - vol_string += ",mp={}".format(mountpoint) - - if options is not None: - vol_string += "," + ",".join( - ["{0}={1}".format(k, v) for k, v in options.items()] - ) - - if kwargs: - vol_string += "," + ",".join( - ["{0}={1}".format(k, v) for k, v in kwargs.items()] - ) - return {key: vol_string} - - def get_lxc_resource(self, vmid, hostname): - if not vmid and not hostname: - self.module.fail_json(msg="Either VMID or hostname must be provided.") - - if vmid: - vm = self.get_lxc_resource_by_id(vmid) - elif hostname: - vm = self.get_lxc_resource_by_hostname(hostname) - - vmid = vm["vmid"] - if vm["type"] != self.VZ_TYPE: - identifier = self.format_vm_identifier(vmid, hostname) - self.module.fail_json( - msg="The specified VM %s is not an %s." % (identifier, self.VZ_TYPE) - ) - - return vm - - def get_lxc_resource_by_id(self, vmid): - vms = self.get_vm_resources() - - vms = [vm for vm in vms if vm["vmid"] == vmid] - if len(vms) == 0: - raise LookupError("VM with VMID %d does not exist in cluster." % vmid) - - return vms[0] - - def get_lxc_resource_by_hostname(self, hostname): - vms = self.get_vm_resources() - - vms = [vm for vm in vms if vm["name"] == hostname] - if len(vms) == 0: - raise LookupError( - "VM with hostname %s does not exist in cluster." % hostname - ) - elif len(vms) > 1: - raise ValueError( - "Multiple VMs found with hostname %s. Please specify VMID." % hostname - ) - - return vms[0] - - def get_vm_resources(self): - try: - return self.proxmox_api.cluster.resources.get(type="vm") - except Exception as e: - self.module.fail_json( - msg="Unable to retrieve list of %s VMs from cluster resources: %s" - % (self.VZ_TYPE, e) - ) - - def get_lxc_status(self, vmid, node_name): - try: - proxmox_node = self.proxmox_api.nodes(node_name) - except Exception as e: - self.module.fail_json(msg="Unable to retrieve node information: %s" % e) - return getattr(proxmox_node, self.VZ_TYPE)(vmid).status.current.get()['status'] - - def format_vm_identifier(self, vmid, hostname): - if vmid and hostname: - return "%s (%s)" % (hostname, vmid) - elif hostname: - return hostname - else: - return to_native(vmid) - - def handle_api_timeout(self, vmid, node, taskid, timeout, timeout_msg=""): - if timeout_msg != "": - timeout_msg = "%s " % timeout_msg - - while timeout > 0: - if self.api_task_ok(node, taskid): - return - timeout -= 1 - time.sleep(1) - - self.module.fail_json( - vmid=vmid, - taskid=taskid, - msg="%sLast line in task before timeout: %s" - % (timeout_msg, self.proxmox_api.nodes(node).tasks(taskid).log.get()[:1]), - ) - - def is_template_container(self, node, target): - """Check if the specified container is a template.""" - proxmox_node = self.proxmox_api.nodes(node) - config = getattr(proxmox_node, self.VZ_TYPE)(target).config.get() - return config.get("template", False) - - def content_check(self, node, ostemplate, template_store): - """Check if the specified ostemplate is present in the specified storage.""" - proxmox_node = self.proxmox_api.nodes(node) - storage_contents = proxmox_node.storage(template_store).content.get() - return any(content["volid"] == ostemplate for content in storage_contents) - - def validate_tags(self, tags): - """Check if the specified tags are valid.""" - re_tag = re.compile(r"^[a-zA-Z0-9_][a-zA-Z0-9_\-\+\.]*$") - for tag in tags: - if not re_tag.match(tag): - self.module.fail_json(msg="%s is not a valid tag" % tag) - return False - return True - - def check_supported_features(self): - for option, version in self.MINIMUM_VERSIONS.items(): - if self.version() < LooseVersion(version) and option in self.module.params: - self.module.fail_json( - changed=False, - msg="Feature {option} is only supported in PVE {version}+, and you're using PVE {pve_version}".format( - option=option, version=version, pve_version=self.version() - ), - ) - - -def isfloat(value): - if value is None: - return False - try: - float(value) - return True - except ValueError: - return False - - -def main(): - module = get_ansible_module() - proxmox = ProxmoxLxcAnsible(module) - - try: - proxmox.run() - except Exception as e: - module.fail_json(msg="An error occurred: %s" % to_native(e)) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/proxmox_backup.py b/plugins/modules/proxmox_backup.py deleted file mode 100644 index 63e19c1d35..0000000000 --- a/plugins/modules/proxmox_backup.py +++ /dev/null @@ -1,570 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2024, IamLunchbox -# 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) " -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() diff --git a/plugins/modules/proxmox_backup_info.py b/plugins/modules/proxmox_backup_info.py deleted file mode 100644 index 0889239b37..0000000000 --- a/plugins/modules/proxmox_backup_info.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2024 Marzieh Raoufnezhad -# Copyright (c) 2024 Maryam Mayabi -# 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) " - - "Maryam Mayabi (@mmayabi) " - -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() diff --git a/plugins/modules/proxmox_disk.py b/plugins/modules/proxmox_disk.py deleted file mode 100644 index 75eb0001e6..0000000000 --- a/plugins/modules/proxmox_disk.py +++ /dev/null @@ -1,877 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2022, Castor Sky (@castorsky) -# 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) " -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(:/) or V(/). - - 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(:iso/) 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 "". - :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=, media=cdrom, iso_image= - 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 "". - :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 "". - :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() diff --git a/plugins/modules/proxmox_domain_info.py b/plugins/modules/proxmox_domain_info.py deleted file mode 100644 index d9836da277..0000000000 --- a/plugins/modules/proxmox_domain_info.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright Tristan Le Guern (@tleguern) -# 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() diff --git a/plugins/modules/proxmox_group_info.py b/plugins/modules/proxmox_group_info.py deleted file mode 100644 index f62d467af8..0000000000 --- a/plugins/modules/proxmox_group_info.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright Tristan Le Guern -# 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() diff --git a/plugins/modules/proxmox_kvm.py b/plugins/modules/proxmox_kvm.py deleted file mode 100644 index c159ced6e6..0000000000 --- a/plugins/modules/proxmox_kvm.py +++ /dev/null @@ -1,1655 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2016, Abdoul Bah (@helldorado) -# 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_kvm -short_description: Management of Qemu(KVM) Virtual Machines in Proxmox VE cluster -description: - - Allows you to create/delete/stop Qemu(KVM) Virtual Machines in Proxmox VE cluster. - - Since community.general 4.0.0 on, there are no more default values. -author: "Abdoul Bah (@helldorado) " -attributes: - check_mode: - support: none - diff_mode: - support: none - action_group: - version_added: 9.0.0 -options: - archive: - description: - - Specify a path to an archive to restore (instead of creating or cloning a VM). - type: str - version_added: 6.5.0 - acpi: - description: - - Specify if ACPI should be enabled/disabled. - type: bool - agent: - description: - - Specify if the QEMU Guest Agent should be enabled/disabled. - - Since community.general 5.5.0, this can also be a string instead of a boolean. This allows to specify values such - as V(enabled=1,fstrim_cloned_disks=1). - type: str - args: - description: - - Pass arbitrary arguments to kvm. - - This option is for experts only! - type: str - audio: - description: - - A hash/dictionary of audio devices for the VM. O(audio={"key":"value", "key":"value"}). - - Keys allowed are - C(audio[n]) where 0 ≤ n ≤ N. - - Values allowed are - C(device="ich9-intel-hda|intel-hda|AC97",driver="none|spice"). - - C(device) is either V(ich9-intel-hda) or V(intel-hda) or V(AC97). - - Option C(driver) is V(none) or V(spice). - type: dict - version_added: 10.5.0 - autostart: - description: - - Specify if the VM should be automatically restarted after crash (currently ignored in PVE API). - type: bool - balloon: - description: - - Specify the amount of RAM for the VM in MB. - - Using zero disables the balloon driver. - type: int - bios: - description: - - Specify the BIOS implementation. - type: str - choices: ['seabios', 'ovmf'] - boot: - description: - - Specify the boot order -> boot on floppy V(a), hard disk V(c), CD-ROM V(d), or network V(n). - - For newer versions of Proxmox VE, use a boot order like V(order=scsi0;net0;hostpci0). - - You can combine to set order. - type: str - bootdisk: - description: - - Enable booting from specified disk. Format V((ide|sata|scsi|virtio\)\\d+). - type: str - cicustom: - description: - - 'Cloud-init: Specify custom files to replace the automatically generated ones at start.' - type: str - version_added: 1.3.0 - cipassword: - description: - - 'Cloud-init: password of default user to create.' - type: str - version_added: 1.3.0 - citype: - description: - - 'Cloud-init: Specifies the cloud-init configuration format.' - - The default depends on the configured operating system type (V(ostype)). - - We use the V(nocloud) format for Linux, and V(configdrive2) for Windows. - type: str - choices: ['nocloud', 'configdrive2'] - version_added: 1.3.0 - ciupgrade: - description: - - 'Cloud-init: do an automatic package upgrade after the first boot.' - type: bool - version_added: 10.0.0 - ciuser: - description: - - 'Cloud-init: username of default user to create.' - type: str - version_added: 1.3.0 - clone: - description: - - Name of VM to be cloned. If O(vmid) is set, O(clone) can take an arbitrary value but is required for initiating the - clone. - type: str - cores: - description: - - Specify number of cores per socket. - type: int - cpu: - description: - - Specify emulated CPU type. - type: str - cpulimit: - description: - - Specify if CPU usage will be limited. Value V(0) indicates no CPU limit. - - If the computer has 2 CPUs, it has total of '2' CPU time. - type: int - cpuunits: - description: - - Specify CPU weight for a VM. - - You can disable fair-scheduler configuration by setting this to V(0). - type: int - delete: - description: - - Specify a list of settings you want to delete. - type: str - description: - description: - - Specify the description for the VM. Only used on the configuration web interface. - - This is saved as comment inside the configuration file. - type: str - digest: - description: - - Specify if to prevent changes if current configuration file has different SHA1 digest. - - This can be used to prevent concurrent modifications. - type: str - efidisk0: - description: - - Specify a hash/dictionary of EFI disk options. - - Requires O(bios=ovmf) to be set to be able to use it. - type: dict - suboptions: - storage: - description: - - V(storage) is the storage identifier where to create the disk. - type: str - format: - description: - - V(format) is the drive's backing file's data format. Please refer to the Proxmox VE Administrator Guide, section - Proxmox VE Storage (see U(https://pve.proxmox.com/pve-docs/chapter-pvesm.html) for the latest version, tables - 3 to 14) to find out format supported by the provided storage backend. - type: str - efitype: - description: - - V(efitype) indicates the size of the EFI disk. - - V(2m) will allow for a 2MB EFI disk, which will be enough to persist boot order and new boot entries. - - V(4m) will allow for a 4MB EFI disk, which will additionally allow to store EFI keys in order to enable Secure - Boot. - type: str - choices: - - 2m - - 4m - pre_enrolled_keys: - description: - - V(pre_enrolled_keys) indicates whether EFI keys for Secure Boot should be enrolled V(1) in the VM firmware upon - creation or not (0). - - If set to V(1), Secure Boot will also be enabled by default when the VM is created. - type: bool - version_added: 4.5.0 - force: - description: - - Allow to force stop VM. - - Can be used with states V(stopped), V(restarted), and V(absent). - - Requires parameter O(archive). - type: bool - format: - description: - - Target drive's backing file's data format. - - Used only with clone. - - Use O(format=unspecified) and O(full=false) for a linked clone. - - Please refer to the Proxmox VE Administrator Guide, section Proxmox VE Storage (see U(https://pve.proxmox.com/pve-docs/chapter-pvesm.html) - for the latest version, tables 3 to 14) to find out format supported by the provided storage backend. - - Not specifying this option is equivalent to setting it to V(unspecified). - type: str - choices: ["cloop", "cow", "qcow", "qcow2", "qed", "raw", "vmdk", "unspecified"] - freeze: - description: - - Specify if PVE should freeze CPU at startup (use 'c' monitor command to start execution). - type: bool - full: - description: - - Create a full copy of all disk. This is always done when you clone a normal VM. - - For VM templates, we try to create a linked clone by default. - - Used only with clone. - type: bool - default: true - hookscript: - description: - - Script that will be executed during various steps in the containers lifetime. - type: str - version_added: 8.1.0 - hostpci: - description: - - Specify a hash/dictionary of map host pci devices into guest. O(hostpci='{"key":"value", "key":"value"}'). - - Keys allowed are - C(hostpci[n]) where 0 ≤ n ≤ N. - - Values allowed are - V("host="HOSTPCIID[;HOSTPCIID2...]",pcie="1|0",rombar="1|0",x-vga="1|0""). - - The C(host) parameter is Host PCI device pass through. HOSTPCIID syntax is V(bus:dev.func) (hexadecimal numbers). - - V(pcie=boolean) V(default=0) Choose the PCI-express bus (needs the q35 machine model). - - V(rombar=boolean) V(default=1) Specify whether or not the device's ROM will be visible in the guest's memory map. - - V(x-vga=boolean) V(default=0) Enable vfio-vga device support. - - /!\ This option allows direct access to host hardware. So it is no longer possible to migrate such machines - use - with special care. - type: dict - hotplug: - description: - - Selectively enable hotplug features. - - This is a comma separated list of hotplug features V(network), V(disk), V(cpu), V(memory), and V(usb). - - Value 0 disables hotplug completely and value 1 is an alias for the default V(network,disk,usb). - type: str - hugepages: - description: - - Enable/disable hugepages memory. - type: str - choices: ['any', '2', '1024'] - ide: - description: - - A hash/dictionary of volume used as IDE hard disk or CD-ROM. O(ide='{"key":"value", "key":"value"}'). - - Keys allowed are - V(ide[n]) where 0 ≤ n ≤ 3. - - Values allowed are - V("storage:size,format=value"). - - V(storage) is the storage identifier where to create the disk. - - V(size) is the size of the disk in GB. - - V(format) is the drive's backing file's data format. V(qcow2|raw|subvol). Please refer to the Proxmox VE Administrator - Guide, section Proxmox VE Storage (see U(https://pve.proxmox.com/pve-docs/chapter-pvesm.html) for the latest version, - tables 3 to 14) to find out format supported by the provided storage backend. - type: dict - ipconfig: - description: - - 'Cloud-init: Set the IP configuration.' - - A hash/dictionary of network IP configurations. O(ipconfig='{"key":"value", "key":"value"}'). - - Keys allowed are - V(ipconfig[n]) where 0 ≤ n ≤ network interfaces. - - Values allowed are - V("[gw=] [,gw6=] [,ip=] [,ip6=]"). - - 'Cloud-init: Specify IP addresses and gateways for the corresponding interface.' - - IP addresses use CIDR notation, gateways are optional but they should be in the same subnet of specified IP address. - - The special string V(dhcp) can be used for IP addresses to use DHCP, in which case no explicit gateway should be provided. - - For IPv6 the special string V(auto) can be used to use stateless autoconfiguration. - - If cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using dhcp on IPv4. - type: dict - version_added: 1.3.0 - keyboard: - description: - - Sets the keyboard layout for VNC server. - type: str - kvm: - description: - - Enable/disable KVM hardware virtualization. - type: bool - localtime: - description: - - Sets the real time clock to local time. - - This is enabled by default if ostype indicates a Microsoft OS. - type: bool - lock: - description: - - Lock/unlock the VM. - type: str - choices: ['migrate', 'backup', 'snapshot', 'rollback'] - machine: - description: - - Specifies the Qemu machine type. - - Type => V((pc|pc(-i440fx\)?-\\d+\\.\\d+(\\.pxe\)?|q35|pc-q35-\\d+\\.\\d+(\\.pxe\)?\)). - type: str - memory: - description: - - Memory size in MB for instance. - type: int - migrate: - description: - - Migrate the VM to O(node) if it is on another node. - type: bool - default: false - version_added: 7.0.0 - migrate_downtime: - description: - - Sets maximum tolerated downtime (in seconds) for migrations. - type: int - migrate_speed: - description: - - Sets maximum speed (in MB/s) for migrations. - - A value of 0 is no limit. - type: int - name: - description: - - Specifies the VM name. Name could be non-unique across the cluster. - - Required only for O(state=present). - - With O(state=present) if O(vmid) not provided and VM with name exists in the cluster then no changes will be made. - type: str - nameservers: - description: - - 'Cloud-init: DNS server IP address(es).' - - If unset, PVE host settings are used. - type: list - elements: str - version_added: 1.3.0 - net: - description: - - A hash/dictionary of network interfaces for the VM. O(net='{"key":"value", "key":"value"}'). - - Keys allowed are - C(net[n]) where 0 ≤ n ≤ N. - - Values allowed are - C("model="XX:XX:XX:XX:XX:XX",bridge="value",rate="value",tag="value",firewall="1|0",trunks="vlanid""). - - Model is one of C(e1000 e1000-82540em e1000-82544gc e1000-82545em i82551 i82557b i82559er ne2k_isa ne2k_pci pcnet - rtl8139 virtio vmxnet3). - - C(XX:XX:XX:XX:XX:XX) should be an unique MAC address. This is automatically generated if not specified. - - The C(bridge) parameter can be used to automatically add the interface to a bridge device. The Proxmox VE standard - bridge is called 'vmbr0'. - - Option C(rate) is used to limit traffic bandwidth from and to this interface. It is specified as floating point number, - unit is 'Megabytes per second'. - - If you specify no bridge, we create a kvm 'user' (NATed) network device, which provides DHCP and DNS services. - type: dict - newid: - description: - - VMID for the clone. Used only with clone. - - If newid is not set, the next available VM ID will be fetched from ProxmoxAPI. - type: int - numa: - description: - - A hash/dictionaries of NUMA topology. O(numa='{"key":"value", "key":"value"}'). - - Keys allowed are - V(numa[n]) where 0 ≤ n ≤ N. - - Values allowed are - V("cpu="",hostnodes="",memory="number",policy="(bind|interleave|preferred)""). - - V(cpus) CPUs accessing this NUMA node. - - V(hostnodes) Host NUMA nodes to use. - - V(memory) Amount of memory this NUMA node provides. - - V(policy) NUMA allocation policy. - type: dict - numa_enabled: - description: - - Enables NUMA. - type: bool - onboot: - description: - - Specifies whether a VM will be started during system bootup. - type: bool - ostype: - description: - - Specifies guest operating system. This is used to enable special optimization/features for specific operating systems. - - The l26 is Linux 2.6/3.X Kernel. - type: str - choices: ['other', 'wxp', 'w2k', 'w2k3', 'w2k8', 'wvista', 'win7', 'win8', 'win10', 'win11', 'l24', 'l26', 'solaris'] - parallel: - description: - - A hash/dictionary of map host parallel devices. O(parallel='{"key":"value", "key":"value"}'). - - Keys allowed are - (parallel[n]) where 0 ≤ n ≤ 2. - - Values allowed are - C("/dev/parport\d+|/dev/usb/lp\d+"). - type: dict - protection: - description: - - Enable/disable the protection flag of the VM. This will enable/disable the remove VM and remove disk operations. - type: bool - reboot: - description: - - Allow reboot. If set to V(true), the VM exit on reboot. - type: bool - revert: - description: - - Revert a pending change. - type: str - sata: - description: - - A hash/dictionary of volume used as sata hard disk or CD-ROM. O(sata='{"key":"value", "key":"value"}'). - - Keys allowed are - C(sata[n]) where 0 ≤ n ≤ 5. - - Values allowed are - C("storage:size,format=value"). - - C(storage) is the storage identifier where to create the disk. - - C(size) is the size of the disk in GB. - - C(format) is the drive's backing file's data format. C(qcow2|raw|subvol). Please refer to the Proxmox VE Administrator - Guide, section Proxmox VE Storage (see U(https://pve.proxmox.com/pve-docs/chapter-pvesm.html) for the latest version, - tables 3 to 14) to find out format supported by the provided storage backend. - type: dict - scsi: - description: - - A hash/dictionary of volume used as SCSI hard disk or CD-ROM. O(scsi='{"key":"value", "key":"value"}'). - - Keys allowed are - C(scsi[n]) where 0 ≤ n ≤ 13. - - Values allowed are - C("storage:size,format=value"). - - C(storage) is the storage identifier where to create the disk. - - C(size) is the size of the disk in GB. - - C(format) is the drive's backing file's data format. C(qcow2|raw|subvol). Please refer to the Proxmox VE Administrator - Guide, section Proxmox VE Storage (see U(https://pve.proxmox.com/pve-docs/chapter-pvesm.html) for the latest version, - tables 3 to 14) to find out format supported by the provided storage backend. - type: dict - scsihw: - description: - - Specifies the SCSI controller model. - type: str - choices: ['lsi', 'lsi53c810', 'virtio-scsi-pci', 'virtio-scsi-single', 'megasas', 'pvscsi'] - searchdomains: - description: - - 'Cloud-init: Sets DNS search domain(s).' - - If unset, PVE host settings are used. - type: list - elements: str - version_added: 1.3.0 - serial: - description: - - A hash/dictionary of serial device to create inside the VM. V('{"key":"value", "key":"value"}'). - - Keys allowed are - serial[n](str; required) where 0 ≤ n ≤ 3. - - Values allowed are - V((/dev/.+|socket\)). - - /!\ If you pass through a host serial device, it is no longer possible to migrate such machines - use with special - care. - type: dict - shares: - description: - - Rets amount of memory shares for auto-ballooning. (0 - 50000). - - The larger the number is, the more memory this VM gets. - - The number is relative to weights of all other running VMs. - - Using 0 disables auto-ballooning, this means no limit. - type: int - skiplock: - description: - - Ignore locks. - - Only root is allowed to use this option. - type: bool - smbios: - description: - - Specifies SMBIOS type 1 fields. - - Comma separated, Base64 encoded (optional) SMBIOS properties:. - - V([base64=<1|0>] [,family=]). - - V([,manufacturer=]). - - V([,product=]). - - V([,serial=]). - - V([,sku=]). - - V([,uuid=]). - - V([,version=]). - type: str - snapname: - description: - - The name of the snapshot. Used only with clone. - type: str - sockets: - description: - - Sets the number of CPU sockets. (1 - N). - type: int - sshkeys: - description: - - 'Cloud-init: SSH key to assign to the default user. NOT TESTED with multiple keys but a multi-line value should work.' - type: str - version_added: 1.3.0 - startdate: - description: - - Sets the initial date of the real time clock. - - Valid format for date are V('now') or V('2016-09-25T16:01:21') or V('2016-09-25'). - type: str - startup: - description: - - Startup and shutdown behavior. V([[order=]\\d+] [,up=\\d+] [,down=\\d+]). - - Order is a non-negative number defining the general startup order. - - Shutdown in done with reverse ordering. - type: str - state: - description: - - Indicates desired state of the instance. - - If V(current), the current state of the VM will be fetched. You can access it with C(results.status). - - V(template) was added in community.general 8.1.0. - - V(paused) and V(hibernated) were added in community.general 10.4.0. - type: str - choices: ['present', 'started', 'absent', 'stopped', 'restarted', 'current', 'template', 'paused', 'hibernated'] - default: present - storage: - description: - - Target storage for full clone. - type: str - tablet: - description: - - Enables/disables the USB tablet device. - type: bool - tags: - description: - - List of tags to apply to the VM instance. - - Tags must start with V([a-z0-9_]) followed by zero or more of the following characters V([a-z0-9_-+.]). - - Tags are only available in Proxmox 6+. - type: list - elements: str - version_added: 2.3.0 - target: - description: - - Target node. Only allowed if the original VM is on shared storage. - - Used only with clone. - type: str - tdf: - description: - - Enables/disables time drift fix. - type: bool - template: - description: - - Enables/disables the template. - type: bool - timeout: - description: - - Timeout for operations. - - When used with O(state=stopped) the option sets a graceful timeout for VM stop after which a VM will be forcefully - stopped. - type: int - default: 30 - tpmstate0: - description: - - A hash/dictionary of options for the Trusted Platform Module disk. - - A TPM state disk is required for Windows 11 installations. - suboptions: - storage: - description: - - O(tpmstate0.storage) is the storage identifier where to create the disk. - type: str - required: true - version: - description: - - The TPM version to use. - type: str - choices: ['1.2', '2.0'] - default: '2.0' - type: dict - version_added: 7.1.0 - usb: - description: - - A hash/dictionary of USB devices for the VM. O(usb='{"key":"value", "key":"value"}'). - - Keys allowed are - C(usb[n]) where 0 ≤ n ≤ N. - - Values allowed are - C(host="value|spice",mapping="value",usb3="1|0"). - - Host is either C(spice) or the USB id/port. - - Option C(mapping) is the mapped USB device name. - - Option C(usb3) enables USB 3 support. - type: dict - version_added: 9.0.0 - update: - description: - - If V(true), the VM will be updated with new value. - - Because of the operations of the API and security reasons, I have disabled the update of the following parameters - O(net), O(virtio), O(ide), O(sata), O(scsi). Per example updating O(net) update the MAC address and O(virtio) create - always new disk... This security feature can be disabled by setting the O(update_unsafe) to V(true). - - Update of O(pool) is disabled. It needs an additional API endpoint not covered by this module. - type: bool - default: false - update_unsafe: - description: - - If V(true), do not enforce limitations on parameters O(net), O(virtio), O(ide), O(sata), O(scsi), O(efidisk0), and - O(tpmstate0). Use this option with caution because an improper configuration might result in a permanent loss of data - (for example disk recreated). - type: bool - default: false - version_added: 8.4.0 - vcpus: - description: - - Sets number of hotplugged vcpus. - type: int - vga: - description: - - Select VGA type. If you want to use high resolution modes (>= 1280x1024x16) then you should use option V(std) or V(vmware). - type: str - choices: ['std', 'cirrus', 'vmware', 'qxl', 'serial0', 'serial1', 'serial2', 'serial3', 'qxl2', 'qxl3', 'qxl4'] - virtio: - description: - - A hash/dictionary of volume used as VIRTIO hard disk. O(virtio='{"key":"value", "key":"value"}'). - - Keys allowed are - V(virtio[n]) where 0 ≤ n ≤ 15. - - Values allowed are - V(storage:size,format=value). - - V(storage) is the storage identifier where to create the disk. - - V(size) is the size of the disk in GB. - - V(format) is the drive's backing file's data format. V(qcow2|raw|subvol). Please refer to the Proxmox VE Administrator - Guide, section Proxmox VE Storage (see U(https://pve.proxmox.com/pve-docs/chapter-pvesm.html) for the latest version, - tables 3 to 14) to find out format supported by the provided storage backend. - type: dict - watchdog: - description: - - Creates a virtual hardware watchdog device. - type: str -seealso: - - module: community.general.proxmox_vm_info -extends_documentation_fragment: - - community.general.proxmox.actiongroup_proxmox - - community.general.proxmox.documentation - - community.general.proxmox.selection - - community.general.attributes -""" - -EXAMPLES = r""" -- name: Create new VM with minimal options - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - -- name: Create a VM from archive (backup) - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - archive: backup-storage:backup/vm/140/2023-03-08T06:41:23Z - name: spynal - -- name: Create new VM with minimal options and given vmid - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - vmid: 100 - -- name: Create new VM with two network interface options - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - net: - net0: 'virtio,bridge=vmbr1,rate=200' - net1: 'e1000,bridge=vmbr2' - -- name: Create new VM with one network interface, three virto hard disk, 4 cores, and 2 vcpus - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - net: - net0: 'virtio,bridge=vmbr1,rate=200' - virtio: - virtio0: 'VMs_LVM:10' - virtio1: 'VMs:2,format=qcow2' - virtio2: 'VMs:5,format=raw' - cores: 4 - vcpus: 2 - -- name: Create VM with 1 10GB SATA disk and an EFI disk, with Secure Boot disabled by default - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - sata: - sata0: 'VMs_LVM:10,format=raw' - bios: ovmf - efidisk0: - storage: VMs_LVM_thin - format: raw - efitype: 4m - pre_enrolled_keys: false - -- name: Create VM with 1 10GB SATA disk and an EFI disk, with Secure Boot enabled by default - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - sata: - sata0: 'VMs_LVM:10,format=raw' - bios: ovmf - efidisk0: - storage: VMs_LVM - format: raw - efitype: 4m - pre_enrolled_keys: 1 - -- name: > - Clone VM with only source VM name. - The VM source is spynal. - The target VM name is zavala - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - clone: spynal - name: zavala - node: sabrewulf - storage: VMs - format: qcow2 - timeout: 500 - -- name: > - Create linked clone VM with only source VM name. - The VM source is spynal. - The target VM name is zavala - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - clone: spynal - name: zavala - node: sabrewulf - storage: VMs - full: false - format: unspecified - timeout: 500 - -- name: Clone VM with source vmid and target newid and raw format - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - clone: arbitrary_name - vmid: 108 - newid: 152 - name: zavala - node: sabrewulf - storage: LVM_STO - format: raw - timeout: 300 - -- name: Create new VM and lock it for snapshot - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - lock: snapshot - -- name: Create new VM and set protection to disable the remove VM and remove disk operations - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - protection: true - -- name: Create new VM using cloud-init with a username and password - community.general.proxmox_kvm: - node: sabrewulf - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - ide: - ide2: 'local:cloudinit,format=qcow2' - ciuser: mylinuxuser - cipassword: supersecret - searchdomains: 'mydomain.internal' - nameservers: 1.1.1.1 - net: - net0: 'virtio,bridge=vmbr1,tag=77' - ipconfig: - ipconfig0: 'ip=192.168.1.1/24,gw=192.168.1.1' - -- name: Create new VM using Cloud-Init with an ssh key - community.general.proxmox_kvm: - node: sabrewulf - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - ide: - ide2: 'local:cloudinit,format=qcow2' - sshkeys: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILJkVm98B71lD5XHfihwcYHE9TVpsJmK1vR1JcaU82L+' - searchdomains: 'mydomain.internal' - nameservers: - - '1.1.1.1' - - '8.8.8.8' - net: - net0: 'virtio,bridge=vmbr1,tag=77' - ipconfig: - ipconfig0: 'ip=192.168.1.1/24' - -- name: Start VM - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - state: started - -- name: Stop VM - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - state: stopped - -- name: Stop VM with force - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - state: stopped - force: true - -- name: Restart VM - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - state: restarted - -- name: Convert VM to template - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - state: template - -- name: Convert VM to template (stop VM if running) - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - state: template - force: true - -- name: Remove VM - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - state: absent - -- name: Get VM current state - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - state: current - -- name: Update VM configuration - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - cores: 8 - memory: 16384 - update: true - -- name: Update VM configuration (incl. unsafe options) - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - cores: 8 - memory: 16384 - net: - net0: virtio,bridge=vmbr1 - update: true - update_unsafe: true - -- name: Delete QEMU parameters - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - delete: 'args,template,cpulimit' - -- name: Revert a pending change - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf - revert: 'template,cpulimit' - -- name: Migrate VM on second node - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - name: spynal - node: sabrewulf-2 - migrate: true - -- name: Add hookscript to existing VM - community.general.proxmox_kvm: - api_user: root@pam - api_password: secret - api_host: helldorado - vmid: 999 - node: sabrewulf - hookscript: local:snippets/hookscript.pl - update: true -""" - -RETURN = r""" -vmid: - description: The VM vmid. - returned: success - type: int - sample: 115 -status: - description: The current virtual machine status. - returned: success, not clone, not absent, not update - type: str - sample: running -msg: - description: A short message. - returned: always - type: str - sample: "VM kropta with vmid = 110 is running" -""" - -import re -import time -from ansible.module_utils.six.moves.urllib.parse import quote - -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 ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.parsing.convert_bool import boolean - - -def parse_mac(netstr): - return re.search('=(.*?),', netstr).group(1) - - -def parse_dev(devstr): - return re.search('(.*?)(,|$)', devstr).group(1) - - -class ProxmoxKvmAnsible(ProxmoxAnsible): - def get_vminfo(self, node, vmid, **kwargs): - global results - results = {} - mac = {} - devices = {} - try: - vm = self.proxmox_api.nodes(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)) - - # Sanitize kwargs. Remove not defined args and ensure True and False converted to int. - kwargs = {k: v for k, v in kwargs.items() if v is not None} - - # Convert all dict in kwargs to elements. - # For hostpci[n], ide[n], net[n], numa[n], parallel[n], sata[n], scsi[n], serial[n], virtio[n] - for k in list(kwargs.keys()): - if isinstance(kwargs[k], dict): - kwargs.update(kwargs[k]) - del kwargs[k] - - # Split information by type - re_net = re.compile(r'net[0-9]') - re_dev = re.compile(r'(virtio|ide|scsi|sata|efidisk)[0-9]') - for k in kwargs.keys(): - if re_net.match(k): - mac[k] = parse_mac(vm[k]) - elif re_dev.match(k): - devices[k] = parse_dev(vm[k]) - - results['mac'] = mac - results['devices'] = devices - results['vmid'] = int(vmid) - - def settings(self, vmid, node, **kwargs): - proxmox_node = self.proxmox_api.nodes(node) - - # Sanitize kwargs. Remove not defined args and ensure True and False converted to int. - kwargs = {k: v for k, v in kwargs.items() if v is not None} - - return proxmox_node.qemu(vmid).config.set(**kwargs) is None - - def wait_for_task(self, node, taskid): - timeout = self.module.params['timeout'] - if self.module.params['state'] == 'stopped': - # Increase task timeout in case of stopped state to be sure it waits longer than VM stop operation itself - timeout += 10 - - while timeout: - if self.api_task_ok(node, taskid): - # Wait an extra second as the API can be a ahead of the hypervisor - time.sleep(1) - return True - timeout = timeout - 1 - if timeout == 0: - break - time.sleep(1) - return False - - def create_vm(self, vmid, newid, node, name, memory, cpu, cores, sockets, update, update_unsafe, **kwargs): - # Available only in PVE 4 - only_v4 = ['force', 'protection', 'skiplock'] - only_v6 = ['ciuser', 'cipassword', 'sshkeys', 'ipconfig', 'tags'] - only_v8 = ['ciupgrade'] - - # valid clone parameters - valid_clone_params = ['format', 'full', 'pool', 'snapname', 'storage', 'target'] - clone_params = {} - # Default args for vm. Note: -args option is for experts only. It allows you to pass arbitrary arguments to kvm. - vm_args = "-serial unix:/var/run/qemu-server/{0}.serial,server,nowait".format(vmid) - - proxmox_node = self.proxmox_api.nodes(node) - - # Sanitize kwargs. Remove not defined args and ensure True and False converted to int. - kwargs = {k: v for k, v in kwargs.items() if v is not None} - kwargs.update({k: int(v) for k, v in kwargs.items() if isinstance(v, bool)}) - - version = self.version() - pve_major_version = 3 if version < LooseVersion('4.0') else version.version[0] - - # The features work only on PVE 4+ - if pve_major_version < 4: - for p in only_v4: - if p in kwargs: - del kwargs[p] - - # The features work only on PVE 6 - if pve_major_version < 6: - for p in only_v6: - if p in kwargs: - del kwargs[p] - - # The features work only on PVE 8 - if pve_major_version < 8: - for p in only_v8: - if p in kwargs: - del kwargs[p] - - # 'sshkeys' param expects an urlencoded string - if 'sshkeys' in kwargs: - urlencoded_ssh_keys = quote(kwargs['sshkeys'], safe='') - kwargs['sshkeys'] = str(urlencoded_ssh_keys) - - # If update, don't update disk (virtio, efidisk0, tpmstate0, ide, sata, scsi) and network interface, unless update_unsafe=True - # pool parameter not supported by qemu//config endpoint on "update" (PVE 6.2) - only with "create" - if update: - if update_unsafe is False: - if 'virtio' in kwargs: - del kwargs['virtio'] - if 'sata' in kwargs: - del kwargs['sata'] - if 'scsi' in kwargs: - del kwargs['scsi'] - if 'ide' in kwargs: - del kwargs['ide'] - if 'efidisk0' in kwargs: - del kwargs['efidisk0'] - if 'tpmstate0' in kwargs: - del kwargs['tpmstate0'] - if 'net' in kwargs: - del kwargs['net'] - if 'force' in kwargs: - del kwargs['force'] - if 'pool' in kwargs: - del kwargs['pool'] - - # Check that the bios option is set to ovmf if the efidisk0 option is present - if 'efidisk0' in kwargs: - if ('bios' not in kwargs) or ('ovmf' != kwargs['bios']): - self.module.fail_json(msg='efidisk0 cannot be used if bios is not set to ovmf. ') - - # Flatten efidisk0 option to a string so that it is a string which is what Proxmoxer and the API expect - if 'efidisk0' in kwargs: - efidisk0_str = '' - # Regexp to catch underscores in keys name, to replace them after by hyphens - hyphen_re = re.compile(r'_') - # If present, the storage definition should be the first argument - if 'storage' in kwargs['efidisk0']: - efidisk0_str += kwargs['efidisk0'].get('storage') + ':1,' - kwargs['efidisk0'].pop('storage') - # Join other elements from the dict as key=value using commas as separator, replacing any underscore in key - # by hyphens (needed for pre_enrolled_keys to pre-enrolled-keys) - efidisk0_str += ','.join([hyphen_re.sub('-', k) + "=" + str(v) for k, v in kwargs['efidisk0'].items() - if 'storage' != k]) - kwargs['efidisk0'] = efidisk0_str - - # Flatten tpmstate0 option to a string so that it is a string which is what Proxmoxer and the API expect - if 'tpmstate0' in kwargs: - kwargs['tpmstate0'] = '{storage}:1,version=v{version}'.format( - storage=kwargs['tpmstate0'].get('storage'), - version=kwargs['tpmstate0'].get('version') - ) - - # Convert all dict in kwargs to elements. - # For audio[n], hostpci[n], ide[n], net[n], numa[n], parallel[n], sata[n], scsi[n], serial[n], virtio[n], ipconfig[n], usb[n] - for k in list(kwargs.keys()): - if isinstance(kwargs[k], dict): - kwargs.update(kwargs[k]) - del kwargs[k] - - if 'agent' in kwargs: - try: - # The API also allows booleans instead of e.g. `enabled=1` for backward-compatibility. - kwargs['agent'] = int(boolean(kwargs['agent'], strict=True)) - except TypeError: - # Not something that Ansible would parse as a boolean. - pass - - # Rename numa_enabled to numa, according the API documentation - if 'numa_enabled' in kwargs: - kwargs['numa'] = kwargs['numa_enabled'] - del kwargs['numa_enabled'] - - # PVE api expects strings for the following params - if 'nameservers' in self.module.params: - nameservers = self.module.params.pop('nameservers') - if nameservers: - kwargs['nameserver'] = ' '.join(nameservers) - if 'searchdomains' in self.module.params: - searchdomains = self.module.params.pop('searchdomains') - if searchdomains: - kwargs['searchdomain'] = ' '.join(searchdomains) - - # VM tags are expected to be valid and presented as a comma/semi-colon delimited string - if 'tags' in kwargs: - re_tag = re.compile(r'^[a-zA-Z0-9_][a-zA-Z0-9_\-\+\.]*$') - for tag in kwargs['tags']: - if not re_tag.match(tag): - self.module.fail_json(msg='%s is not a valid tag' % tag) - kwargs['tags'] = ",".join(kwargs['tags']) - - # -args and skiplock require root@pam user - but can not use api tokens - if self.module.params['api_user'] == "root@pam" and self.module.params['args'] is not None: - kwargs['args'] = self.module.params['args'] - elif self.module.params['api_user'] != "root@pam" and self.module.params['args'] is not None: - self.module.fail_json(msg='args parameter require root@pam user. ') - - if self.module.params['api_user'] != "root@pam" and self.module.params['skiplock'] is not None: - self.module.fail_json(msg='skiplock parameter require root@pam user. ') - - if update: - if proxmox_node.qemu(vmid).config.set(name=name, memory=memory, cpu=cpu, cores=cores, sockets=sockets, **kwargs) is None: - return True - else: - return False - elif self.module.params['clone'] is not None: - for param in valid_clone_params: - if self.module.params[param] is not None: - clone_params[param] = self.module.params[param] - clone_params.update({k: int(v) for k, v in clone_params.items() if isinstance(v, bool)}) - taskid = proxmox_node.qemu(vmid).clone.post(newid=newid, name=name, **clone_params) - else: - taskid = proxmox_node.qemu.create(vmid=vmid, name=name, memory=memory, cpu=cpu, cores=cores, sockets=sockets, **kwargs) - - if not self.wait_for_task(node, taskid): - self.module.fail_json(msg='Reached timeout while waiting for creating VM. Last line in task before timeout: %s' % - proxmox_node.tasks(taskid).log.get()[:1]) - return False - return True - - def start_vm(self, vm): - vmid = vm['vmid'] - proxmox_node = self.proxmox_api.nodes(vm['node']) - taskid = proxmox_node.qemu(vmid).status.start.post() - if not self.wait_for_task(vm['node'], taskid): - self.module.fail_json(msg='Reached timeout while waiting for starting VM. Last line in task before timeout: %s' % - proxmox_node.tasks(taskid).log.get()[:1]) - return False - return True - - def stop_vm(self, vm, force, timeout): - vmid = vm['vmid'] - proxmox_node = self.proxmox_api.nodes(vm['node']) - taskid = proxmox_node.qemu(vmid).status.shutdown.post(forceStop=(1 if force else 0), timeout=timeout) - if not self.wait_for_task(vm['node'], taskid): - self.module.fail_json(msg='Reached timeout while waiting for stopping VM. Last line in task before timeout: %s' % - proxmox_node.tasks(taskid).log.get()[:1]) - return False - return True - - def restart_vm(self, vm, force, **status): - vmid = vm['vmid'] - try: - proxmox_node = self.proxmox_api.nodes(vm['node']) - taskid = proxmox_node.qemu(vmid).status.reset.post() if force else proxmox_node.qemu(vmid).status.reboot.post() - if not self.wait_for_task(vm['node'], taskid): - self.module.fail_json(msg='Reached timeout while waiting for rebooting VM. Last line in task before timeout: %s' % - proxmox_node.tasks(taskid).log.get()[:1]) - return False - return True - except Exception as e: - self.module.fail_json(vmid=vmid, msg="restarting of VM %s failed with exception: %s" % (vmid, e)) - return False - - def convert_to_template(self, vm, timeout, force): - vmid = vm['vmid'] - try: - proxmox_node = self.proxmox_api.nodes(vm['node']) - if proxmox_node.qemu(vmid).status.current.get()['status'] == 'running' and force: - self.stop_instance(vm, vmid, timeout, force) - # not sure why, but templating a container doesn't return a taskid - proxmox_node.qemu(vmid).template.post() - return True - except Exception as e: - self.module.fail_json(vmid=vmid, msg="conversion of VM %s to template failed with exception: %s" % (vmid, e)) - return False - - def migrate_vm(self, vm, target_node): - vmid = vm['vmid'] - proxmox_node = self.proxmox_api.nodes(vm['node']) - taskid = proxmox_node.qemu(vmid).migrate.post(vmid=vmid, node=vm['node'], target=target_node, online=1) - if not self.wait_for_task(vm['node'], taskid): - self.module.fail_json(msg='Reached timeout while waiting for migrating VM. Last line in task before timeout: %s' % - proxmox_node.tasks(taskid).log.get()[:1]) - return False - return True - - def suspend_vm(self, vm, timeout, todisk): - vmid = vm['vmid'] - proxmox_node = self.proxmox_api.nodes(vm['node']) - taskid = proxmox_node.qemu(vmid).status.suspend.post(todisk=(1 if todisk else 0), timeout=timeout) - if not self.wait_for_task(vm['node'], taskid): - self.module.fail_json(msg='Reached timeout while waiting for suspending VM. Last line in task before timeout: %s' % - proxmox_node.tasks(taskid).log.get()[:1]) - return False - return True - - -def main(): - module_args = proxmox_auth_argument_spec() - kvm_args = dict( - archive=dict(type='str'), - acpi=dict(type='bool'), - agent=dict(type='str'), - args=dict(type='str'), - audio=dict(type='dict'), - autostart=dict(type='bool'), - balloon=dict(type='int'), - bios=dict(choices=['seabios', 'ovmf']), - boot=dict(type='str'), - bootdisk=dict(type='str'), - cicustom=dict(type='str'), - cipassword=dict(type='str', no_log=True), - citype=dict(type='str', choices=['nocloud', 'configdrive2']), - ciupgrade=dict(type='bool'), - ciuser=dict(type='str'), - clone=dict(type='str'), - cores=dict(type='int'), - cpu=dict(type='str'), - cpulimit=dict(type='int'), - cpuunits=dict(type='int'), - delete=dict(type='str'), - description=dict(type='str'), - digest=dict(type='str'), - efidisk0=dict(type='dict', - options=dict( - storage=dict(type='str'), - format=dict(type='str'), - efitype=dict(type='str', choices=['2m', '4m']), - pre_enrolled_keys=dict(type='bool'), - )), - force=dict(type='bool'), - format=dict(type='str', choices=['cloop', 'cow', 'qcow', 'qcow2', 'qed', 'raw', 'vmdk', 'unspecified']), - freeze=dict(type='bool'), - full=dict(type='bool', default=True), - hookscript=dict(type='str'), - hostpci=dict(type='dict'), - hotplug=dict(type='str'), - hugepages=dict(choices=['any', '2', '1024']), - ide=dict(type='dict'), - ipconfig=dict(type='dict'), - keyboard=dict(type='str'), - kvm=dict(type='bool'), - localtime=dict(type='bool'), - lock=dict(choices=['migrate', 'backup', 'snapshot', 'rollback']), - machine=dict(type='str'), - memory=dict(type='int'), - migrate=dict(type='bool', default=False), - migrate_downtime=dict(type='int'), - migrate_speed=dict(type='int'), - name=dict(type='str'), - nameservers=dict(type='list', elements='str'), - net=dict(type='dict'), - newid=dict(type='int'), - node=dict(), - numa=dict(type='dict'), - numa_enabled=dict(type='bool'), - onboot=dict(type='bool'), - ostype=dict(choices=['other', 'wxp', 'w2k', 'w2k3', 'w2k8', 'wvista', 'win7', 'win8', 'win10', 'win11', 'l24', 'l26', 'solaris']), - parallel=dict(type='dict'), - pool=dict(type='str'), - protection=dict(type='bool'), - reboot=dict(type='bool'), - revert=dict(type='str'), - sata=dict(type='dict'), - scsi=dict(type='dict'), - scsihw=dict(choices=['lsi', 'lsi53c810', 'virtio-scsi-pci', 'virtio-scsi-single', 'megasas', 'pvscsi']), - serial=dict(type='dict'), - searchdomains=dict(type='list', elements='str'), - shares=dict(type='int'), - skiplock=dict(type='bool'), - smbios=dict(type='str'), - snapname=dict(type='str'), - sockets=dict(type='int'), - sshkeys=dict(type='str', no_log=False), - startdate=dict(type='str'), - startup=dict(), - state=dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted', 'current', 'template', 'paused', 'hibernated']), - storage=dict(type='str'), - tablet=dict(type='bool'), - tags=dict(type='list', elements='str'), - target=dict(type='str'), - tdf=dict(type='bool'), - template=dict(type='bool'), - timeout=dict(type='int', default=30), - tpmstate0=dict(type='dict', - options=dict( - storage=dict(type='str', required=True), - version=dict(type='str', choices=['2.0', '1.2'], default='2.0') - )), - usb=dict(type='dict'), - update=dict(type='bool', default=False), - update_unsafe=dict(type='bool', default=False), - vcpus=dict(type='int'), - vga=dict(choices=['std', 'cirrus', 'vmware', 'qxl', 'serial0', 'serial1', 'serial2', 'serial3', 'qxl2', 'qxl3', 'qxl4']), - virtio=dict(type='dict'), - vmid=dict(type='int'), - watchdog=dict(), - ) - module_args.update(kvm_args) - - module = AnsibleModule( - argument_spec=module_args, - mutually_exclusive=[('delete', 'revert'), ('delete', 'update'), ('revert', 'update'), ('clone', 'update'), ('clone', 'delete'), ('clone', 'revert')], - required_together=[('api_token_id', 'api_token_secret')], - required_one_of=[('name', 'vmid'), ('api_password', 'api_token_id')], - required_if=[('state', 'present', ['node'])], - ) - - clone = module.params['clone'] - cpu = module.params['cpu'] - cores = module.params['cores'] - delete = module.params['delete'] - migrate = module.params['migrate'] - memory = module.params['memory'] - name = module.params['name'] - newid = module.params['newid'] - node = module.params['node'] - revert = module.params['revert'] - sockets = module.params['sockets'] - state = module.params['state'] - update = bool(module.params['update']) - update_unsafe = bool(module.params['update_unsafe']) - vmid = module.params['vmid'] - validate_certs = module.params['validate_certs'] - - if module.params['format'] == 'unspecified': - module.params['format'] = None - - proxmox = ProxmoxKvmAnsible(module) - - # If vmid is not defined then retrieve its value from the vm name, - # the cloned vm name or retrieve the next free VM id from ProxmoxAPI. - if not vmid: - if state == 'present' and not update and not clone and not delete and not revert and not migrate: - existing_vmid = proxmox.get_vmid(name, ignore_missing=True) - if existing_vmid: - vmid = existing_vmid - else: - try: - vmid = proxmox.get_nextvmid() - except Exception: - module.fail_json(msg="Can't get the next vmid for VM {0} automatically. Ensure your cluster state is good".format(name)) - else: - clone_target = clone or name - vmid = proxmox.get_vmid(clone_target, ignore_missing=True) - - if clone is not None: - # If newid is not defined then retrieve the next free id from ProxmoxAPI - if not newid: - try: - newid = proxmox.get_nextvmid() - except Exception: - module.fail_json(msg="Can't get the next vmid for VM {0} automatically. Ensure your cluster state is good".format(name)) - - # Ensure source VM name exists when cloning - if not vmid: - module.fail_json(msg='VM with name = %s does not exist in cluster' % clone) - - # Ensure source VM id exists when cloning - proxmox.get_vm(vmid) - - # Ensure the chosen VM name doesn't already exist when cloning - existing_vmid = proxmox.get_vmid(name, ignore_missing=True) - if existing_vmid: - module.exit_json(changed=False, vmid=existing_vmid, msg="VM with name <%s> already exists" % name) - - # Ensure the chosen VM id doesn't already exist when cloning - if proxmox.get_vm(newid, ignore_missing=True): - module.exit_json(changed=False, vmid=vmid, msg="vmid %s with VM name %s already exists" % (newid, name)) - - if delete is not None: - try: - proxmox.settings(vmid, node, delete=delete) - module.exit_json(changed=True, vmid=vmid, msg="Settings has deleted on VM {0} with vmid {1}".format(name, vmid)) - except Exception as e: - module.fail_json(vmid=vmid, msg='Unable to delete settings on VM {0} with vmid {1}: '.format(name, vmid) + str(e)) - - if revert is not None: - try: - proxmox.settings(vmid, node, revert=revert) - module.exit_json(changed=True, vmid=vmid, msg="Settings has reverted on VM {0} with vmid {1}".format(name, vmid)) - except Exception as e: - module.fail_json(vmid=vmid, msg='Unable to revert settings on VM {0} with vmid {1}: Maybe is not a pending task... '.format(name, vmid) + str(e)) - - if migrate: - try: - vm = proxmox.get_vm(vmid) - vm_node = vm['node'] - if node != vm_node: - proxmox.migrate_vm(vm, node) - module.exit_json(changed=True, vmid=vmid, msg="VM {0} has been migrated from {1} to {2}".format(vmid, vm_node, node)) - else: - module.exit_json(changed=False, vmid=vmid, msg="VM {0} is already on {1}".format(vmid, node)) - except Exception as e: - module.fail_json(vmid=vmid, msg='Unable to migrate VM {0} from {1} to {2}: {3}'.format(vmid, vm_node, node, e)) - - if state == 'present': - if not (update or clone) and proxmox.get_vm(vmid, ignore_missing=True): - module.exit_json(changed=False, vmid=vmid, msg="VM with vmid <%s> already exists" % vmid) - elif not (update or clone or vmid) and proxmox.get_vmid(name, ignore_missing=True): - module.exit_json(changed=False, vmid=proxmox.get_vmid(name), msg="VM with name <%s> already exists" % name) - elif not node: - module.fail_json(msg='node is mandatory for creating/updating VM') - elif update and not any([vmid, name]): - module.fail_json(msg='vmid or name is mandatory for updating VM') - elif not proxmox.get_node(node): - module.fail_json(msg="node '%s' does not exist in cluster" % node) - - try: - proxmox.create_vm(vmid, newid, node, name, memory, cpu, cores, sockets, update, update_unsafe, - archive=module.params['archive'], - acpi=module.params['acpi'], - agent=module.params['agent'], - audio=module.params['audio'], - autostart=module.params['autostart'], - balloon=module.params['balloon'], - bios=module.params['bios'], - boot=module.params['boot'], - bootdisk=module.params['bootdisk'], - cicustom=module.params['cicustom'], - cipassword=module.params['cipassword'], - citype=module.params['citype'], - ciupgrade=module.params['ciupgrade'], - ciuser=module.params['ciuser'], - cpulimit=module.params['cpulimit'], - cpuunits=module.params['cpuunits'], - description=module.params['description'], - digest=module.params['digest'], - efidisk0=module.params['efidisk0'], - force=module.params['force'], - freeze=module.params['freeze'], - hookscript=module.params['hookscript'], - hostpci=module.params['hostpci'], - hotplug=module.params['hotplug'], - hugepages=module.params['hugepages'], - ide=module.params['ide'], - ipconfig=module.params['ipconfig'], - keyboard=module.params['keyboard'], - kvm=module.params['kvm'], - localtime=module.params['localtime'], - lock=module.params['lock'], - machine=module.params['machine'], - migrate_downtime=module.params['migrate_downtime'], - migrate_speed=module.params['migrate_speed'], - net=module.params['net'], - numa=module.params['numa'], - numa_enabled=module.params['numa_enabled'], - onboot=module.params['onboot'], - ostype=module.params['ostype'], - parallel=module.params['parallel'], - pool=module.params['pool'], - protection=module.params['protection'], - reboot=module.params['reboot'], - sata=module.params['sata'], - scsi=module.params['scsi'], - scsihw=module.params['scsihw'], - serial=module.params['serial'], - shares=module.params['shares'], - skiplock=module.params['skiplock'], - smbios1=module.params['smbios'], - snapname=module.params['snapname'], - sshkeys=module.params['sshkeys'], - startdate=module.params['startdate'], - startup=module.params['startup'], - tablet=module.params['tablet'], - tags=module.params['tags'], - target=module.params['target'], - tdf=module.params['tdf'], - template=module.params['template'], - tpmstate0=module.params['tpmstate0'], - usb=module.params['usb'], - vcpus=module.params['vcpus'], - vga=module.params['vga'], - virtio=module.params['virtio'], - watchdog=module.params['watchdog']) - - if not clone: - proxmox.get_vminfo(node, vmid, - ide=module.params['ide'], - net=module.params['net'], - sata=module.params['sata'], - scsi=module.params['scsi'], - virtio=module.params['virtio']) - except Exception as e: - if update: - module.fail_json(vmid=vmid, msg="Unable to update vm {0} with vmid {1}=".format(name, vmid) + str(e)) - elif clone is not None: - module.fail_json(vmid=vmid, msg="Unable to clone vm {0} from vmid {1}=".format(name, vmid) + str(e)) - else: - module.fail_json(vmid=vmid, msg="creation of qemu VM %s with vmid %s failed with exception=%s" % (name, vmid, e)) - - if update: - module.exit_json(changed=True, vmid=vmid, msg="VM %s with vmid %s updated" % (name, vmid)) - elif clone is not None: - module.exit_json(changed=True, vmid=newid, msg="VM %s with newid %s cloned from vm with vmid %s" % (name, newid, vmid)) - else: - module.exit_json(changed=True, msg="VM %s with vmid %s deployed" % (name, vmid), **results) - - elif state == 'started': - if not vmid: - module.fail_json(msg='VM with name = %s does not exist in cluster' % name) - - status = {} - try: - vm = proxmox.get_vm(vmid) - current = proxmox.proxmox_api.nodes(vm['node']).qemu(vmid).status.current.get()['status'] - status['status'] = current - if current == 'running': - module.exit_json(changed=False, vmid=vmid, msg="VM %s is already running" % vmid, **status) - - if proxmox.start_vm(vm): - module.exit_json(changed=True, vmid=vmid, msg="VM %s started" % vmid, **status) - except Exception as e: - module.fail_json(vmid=vmid, msg="starting of VM %s failed with exception: %s" % (vmid, e), **status) - - elif state == 'stopped': - if not vmid: - module.fail_json(msg='VM with name = %s does not exist in cluster' % name) - - status = {} - try: - vm = proxmox.get_vm(vmid) - current = proxmox.proxmox_api.nodes(vm['node']).qemu(vmid).status.current.get()['status'] - status['status'] = current - if current == 'stopped': - module.exit_json(changed=False, vmid=vmid, msg="VM %s is already stopped" % vmid, **status) - - proxmox.stop_vm(vm, force=module.params['force'], timeout=module.params['timeout']) - module.exit_json(changed=True, vmid=vmid, msg="VM %s is shutting down" % vmid, **status) - except Exception as e: - module.fail_json(vmid=vmid, msg="stopping of VM %s failed with exception: %s" % (vmid, e), **status) - - elif state == 'template': - if not vmid: - module.fail_json(msg='VM with name = %s does not exist in cluster' % name) - - status = {} - try: - vm = proxmox.get_vm(vmid) - - if vm['template'] == 1: - module.exit_json(changed=False, vmid=vmid, msg="VM %s is already a template" % vmid, **status) - - if proxmox.convert_to_template(vm, force=module.params['force'], timeout=module.params['timeout']): - module.exit_json(changed=True, vmid=vmid, msg="VM %s is converting to template" % vmid, **status) - except Exception as e: - module.fail_json(vmid=vmid, msg="conversion of VM %s to template failed with exception: %s" % (vmid, e), **status) - - elif state == 'restarted': - if not vmid: - module.fail_json(msg='VM with name = %s does not exist in cluster' % name) - - status = {} - vm = proxmox.get_vm(vmid) - current = proxmox.proxmox_api.nodes(vm['node']).qemu(vmid).status.current.get()['status'] - status['status'] = current - if current == 'stopped': - module.exit_json(changed=False, vmid=vmid, msg="VM %s is not running" % vmid, **status) - - if proxmox.restart_vm(vm, force=module.params['force']): - module.exit_json(changed=True, vmid=vmid, msg="VM %s is restarted" % vmid, **status) - - elif state == 'absent': - status = {} - if not vmid: - module.exit_json(changed=False, msg='VM with name = %s is already absent' % name) - - try: - vm = proxmox.get_vm(vmid, ignore_missing=True) - if not vm: - module.exit_json(changed=False, vmid=vmid) - - proxmox_node = proxmox.proxmox_api.nodes(vm['node']) - current = proxmox_node.qemu(vmid).status.current.get()['status'] - status['status'] = current - if current == 'running': - if module.params['force']: - proxmox.stop_vm(vm, True, timeout=module.params['timeout']) - else: - module.exit_json(changed=False, vmid=vmid, msg="VM %s is running. Stop it before deletion or use force=true." % vmid) - taskid = proxmox_node.qemu.delete(vmid) - if not proxmox.wait_for_task(vm['node'], taskid): - module.fail_json(msg='Reached timeout while waiting for removing VM. Last line in task before timeout: %s' % - proxmox_node.tasks(taskid).log.get()[:1]) - else: - module.exit_json(changed=True, vmid=vmid, msg="VM %s removed" % vmid) - except Exception as e: - module.fail_json(msg="deletion of VM %s failed with exception: %s" % (vmid, e)) - - elif state == 'current': - status = {} - if not vmid: - module.fail_json(msg='VM with name = %s does not exist in cluster' % name) - vm = proxmox.get_vm(vmid) - if not name: - name = vm.get('name', '(unnamed)') - current = proxmox.proxmox_api.nodes(vm['node']).qemu(vmid).status.current.get()['status'] - status['status'] = current - if status: - module.exit_json(changed=False, vmid=vmid, msg="VM %s with vmid = %s is %s" % (name, vmid, current), **status) - - elif state in ['paused', 'hibernated']: - if not vmid: - module.fail_json(msg='VM with name = %s does not exist in cluster' % name) - - status = {} - try: - vm = proxmox.get_vm(vmid) - current = proxmox.proxmox_api.nodes(vm['node']).qemu(vmid).status.current.get()['status'] - status['status'] = current - if current != 'running': - module.exit_json(changed=False, vmid=vmid, msg="VM %s is not running" % vmid, **status) - - proxmox.suspend_vm(vm, force=module.params['force'], timeout=module.params['timeout'], todisk=(state == 'hibernated')) - module.exit_json(changed=True, vmid=vmid, msg="VM %s is suspending" % vmid, **status) - except Exception as e: - module.fail_json(vmid=vmid, msg="suspending of VM %s failed with exception: %s" % (vmid, e), **status) - - -if __name__ == '__main__': - main() diff --git a/plugins/modules/proxmox_nic.py b/plugins/modules/proxmox_nic.py deleted file mode 100644 index bd119fe5cc..0000000000 --- a/plugins/modules/proxmox_nic.py +++ /dev/null @@ -1,313 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2021, Lammert Hellinga (@Kogelvis) -# 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) " -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() diff --git a/plugins/modules/proxmox_node_info.py b/plugins/modules/proxmox_node_info.py deleted file mode 100644 index e243862134..0000000000 --- a/plugins/modules/proxmox_node_info.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright John Berninger (@jberning) -# 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() diff --git a/plugins/modules/proxmox_pool.py b/plugins/modules/proxmox_pool.py deleted file mode 100644 index c53e394eeb..0000000000 --- a/plugins/modules/proxmox_pool.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2023, Sergei Antipov (UnderGreen) -# 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) " -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() diff --git a/plugins/modules/proxmox_pool_member.py b/plugins/modules/proxmox_pool_member.py deleted file mode 100644 index bd32e94e42..0000000000 --- a/plugins/modules/proxmox_pool_member.py +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2023, Sergei Antipov (UnderGreen) -# 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) " -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() diff --git a/plugins/modules/proxmox_snap.py b/plugins/modules/proxmox_snap.py deleted file mode 100644 index 9abc5cf1ad..0000000000 --- a/plugins/modules/proxmox_snap.py +++ /dev/null @@ -1,395 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2020, Jeffrey van Pelt (@Thulium-Drake) -# 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() diff --git a/plugins/modules/proxmox_storage_contents_info.py b/plugins/modules/proxmox_storage_contents_info.py deleted file mode 100644 index e0e95565d7..0000000000 --- a/plugins/modules/proxmox_storage_contents_info.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright Julian Vanden Broeck (@l00ptr) -# 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() diff --git a/plugins/modules/proxmox_storage_info.py b/plugins/modules/proxmox_storage_info.py deleted file mode 100644 index 5b9b1b6aaa..0000000000 --- a/plugins/modules/proxmox_storage_info.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright Tristan Le Guern (@tleguern) -# 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() diff --git a/plugins/modules/proxmox_tasks_info.py b/plugins/modules/proxmox_tasks_info.py deleted file mode 100644 index 574a971427..0000000000 --- a/plugins/modules/proxmox_tasks_info.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright (c) 2021, Andreas Botzner (@paginabianca) -# 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) ' -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() diff --git a/plugins/modules/proxmox_template.py b/plugins/modules/proxmox_template.py deleted file mode 100644 index 0081171878..0000000000 --- a/plugins/modules/proxmox_template.py +++ /dev/null @@ -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() diff --git a/plugins/modules/proxmox_user_info.py b/plugins/modules/proxmox_user_info.py deleted file mode 100644 index a8da1ee30a..0000000000 --- a/plugins/modules/proxmox_user_info.py +++ /dev/null @@ -1,260 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright Tristan Le Guern -# 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() diff --git a/plugins/modules/proxmox_vm_info.py b/plugins/modules/proxmox_vm_info.py deleted file mode 100644 index 34d701c25e..0000000000 --- a/plugins/modules/proxmox_vm_info.py +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright (c) 2023, Sergei Antipov -# 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) ' -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() diff --git a/tests/integration/targets/connection_proxmox_pct_remote/aliases b/tests/integration/targets/connection_proxmox_pct_remote/aliases deleted file mode 100644 index d2fefd10c7..0000000000 --- a/tests/integration/targets/connection_proxmox_pct_remote/aliases +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2025 Nils Stein (@mietzen) -# 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 diff --git a/tests/integration/targets/connection_proxmox_pct_remote/dependencies.yml b/tests/integration/targets/connection_proxmox_pct_remote/dependencies.yml deleted file mode 100644 index c0a6718e32..0000000000 --- a/tests/integration/targets/connection_proxmox_pct_remote/dependencies.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -# Copyright (c) 2025 Nils Stein (@mietzen) -# 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" diff --git a/tests/integration/targets/connection_proxmox_pct_remote/files/pct b/tests/integration/targets/connection_proxmox_pct_remote/files/pct deleted file mode 100755 index 8a40280041..0000000000 --- a/tests/integration/targets/connection_proxmox_pct_remote/files/pct +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2025 Nils Stein (@mietzen) -# 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" diff --git a/tests/integration/targets/connection_proxmox_pct_remote/plugin-specific-tests.yml b/tests/integration/targets/connection_proxmox_pct_remote/plugin-specific-tests.yml deleted file mode 100644 index 41fe06cdb9..0000000000 --- a/tests/integration/targets/connection_proxmox_pct_remote/plugin-specific-tests.yml +++ /dev/null @@ -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." diff --git a/tests/integration/targets/connection_proxmox_pct_remote/runme.sh b/tests/integration/targets/connection_proxmox_pct_remote/runme.sh deleted file mode 100755 index 5d27e243d4..0000000000 --- a/tests/integration/targets/connection_proxmox_pct_remote/runme.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2025 Nils Stein (@mietzen) -# 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 \ - "$@" diff --git a/tests/integration/targets/connection_proxmox_pct_remote/test.sh b/tests/integration/targets/connection_proxmox_pct_remote/test.sh deleted file mode 120000 index 70aa5dbdba..0000000000 --- a/tests/integration/targets/connection_proxmox_pct_remote/test.sh +++ /dev/null @@ -1 +0,0 @@ -../connection_posix/test.sh \ No newline at end of file diff --git a/tests/integration/targets/connection_proxmox_pct_remote/test_connection.inventory b/tests/integration/targets/connection_proxmox_pct_remote/test_connection.inventory deleted file mode 100644 index 15592a61a6..0000000000 --- a/tests/integration/targets/connection_proxmox_pct_remote/test_connection.inventory +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2025 Nils Stein (@mietzen) -# 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 diff --git a/tests/integration/targets/proxmox/aliases b/tests/integration/targets/proxmox/aliases deleted file mode 100644 index 5e5957a5c2..0000000000 --- a/tests/integration/targets/proxmox/aliases +++ /dev/null @@ -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 diff --git a/tests/integration/targets/proxmox/tasks/main.yml b/tests/integration/targets/proxmox/tasks/main.yml deleted file mode 100644 index 4e393cbf3b..0000000000 --- a/tests/integration/targets/proxmox/tasks/main.yml +++ /dev/null @@ -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 -# 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' diff --git a/tests/integration/targets/proxmox_pool/aliases b/tests/integration/targets/proxmox_pool/aliases deleted file mode 100644 index 525dcd332b..0000000000 --- a/tests/integration/targets/proxmox_pool/aliases +++ /dev/null @@ -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 diff --git a/tests/integration/targets/proxmox_pool/defaults/main.yml b/tests/integration/targets/proxmox_pool/defaults/main.yml deleted file mode 100644 index 5a518ac734..0000000000 --- a/tests/integration/targets/proxmox_pool/defaults/main.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) 2023, Sergei Antipov -# 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 diff --git a/tests/integration/targets/proxmox_pool/tasks/main.yml b/tests/integration/targets/proxmox_pool/tasks/main.yml deleted file mode 100644 index 2b22960f2c..0000000000 --- a/tests/integration/targets/proxmox_pool/tasks/main.yml +++ /dev/null @@ -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 -# 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 diff --git a/tests/integration/targets/proxmox_template/aliases b/tests/integration/targets/proxmox_template/aliases deleted file mode 100644 index 5d9af81016..0000000000 --- a/tests/integration/targets/proxmox_template/aliases +++ /dev/null @@ -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 diff --git a/tests/integration/targets/proxmox_template/tasks/main.yml b/tests/integration/targets/proxmox_template/tasks/main.yml deleted file mode 100644 index 2d1187e890..0000000000 --- a/tests/integration/targets/proxmox_template/tasks/main.yml +++ /dev/null @@ -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 -# 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 diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index 6115954d00..841806971d 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -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 diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index 8ac70d76d7..7e8a51074d 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -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 diff --git a/tests/unit/plugins/connection/test_proxmox_pct_remote.py b/tests/unit/plugins/connection/test_proxmox_pct_remote.py deleted file mode 100644 index c0e8678cdc..0000000000 --- a/tests/unit/plugins/connection/test_proxmox_pct_remote.py +++ /dev/null @@ -1,585 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2024 Nils Stein (@mietzen) -# 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 diff --git a/tests/unit/plugins/inventory/test_proxmox.py b/tests/unit/plugins/inventory/test_proxmox.py deleted file mode 100644 index b8358df226..0000000000 --- a/tests/unit/plugins/inventory/test_proxmox.py +++ /dev/null @@ -1,786 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Jeffrey van Pelt -# 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"] diff --git a/tests/unit/plugins/modules/test_proxmox_backup.py b/tests/unit/plugins/modules/test_proxmox_backup.py deleted file mode 100644 index 372347d279..0000000000 --- a/tests/unit/plugins/modules/test_proxmox_backup.py +++ /dev/null @@ -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 diff --git a/tests/unit/plugins/modules/test_proxmox_backup_info.py b/tests/unit/plugins/modules/test_proxmox_backup_info.py deleted file mode 100644 index da63ce3fe7..0000000000 --- a/tests/unit/plugins/modules/test_proxmox_backup_info.py +++ /dev/null @@ -1,275 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2024 Marzieh Raoufnezhad -# Copyright (c) 2024 Maryam Mayabi -# 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 diff --git a/tests/unit/plugins/modules/test_proxmox_kvm.py b/tests/unit/plugins/modules/test_proxmox_kvm.py deleted file mode 100644 index ffc806bdc8..0000000000 --- a/tests/unit/plugins/modules/test_proxmox_kvm.py +++ /dev/null @@ -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" - ) diff --git a/tests/unit/plugins/modules/test_proxmox_snap.py b/tests/unit/plugins/modules/test_proxmox_snap.py deleted file mode 100644 index 2aef4cb982..0000000000 --- a/tests/unit/plugins/modules/test_proxmox_snap.py +++ /dev/null @@ -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" diff --git a/tests/unit/plugins/modules/test_proxmox_storage_contents_info.py b/tests/unit/plugins/modules/test_proxmox_storage_contents_info.py deleted file mode 100644 index 674dc45ac9..0000000000 --- a/tests/unit/plugins/modules/test_proxmox_storage_contents_info.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2023, Julian Vanden Broeck -# 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 diff --git a/tests/unit/plugins/modules/test_proxmox_tasks_info.py b/tests/unit/plugins/modules/test_proxmox_tasks_info.py deleted file mode 100644 index 08398d1504..0000000000 --- a/tests/unit/plugins/modules/test_proxmox_tasks_info.py +++ /dev/null @@ -1,206 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2021, Andreas Botzner (@paginabianca) -# 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.' diff --git a/tests/unit/plugins/modules/test_proxmox_template.py b/tests/unit/plugins/modules/test_proxmox_template.py deleted file mode 100644 index 3e20213e8b..0000000000 --- a/tests/unit/plugins/modules/test_proxmox_template.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2023, Sergei Antipov -# 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" diff --git a/tests/unit/plugins/modules/test_proxmox_vm_info.py b/tests/unit/plugins/modules/test_proxmox_vm_info.py deleted file mode 100644 index 8d2d0ab2ad..0000000000 --- a/tests/unit/plugins/modules/test_proxmox_vm_info.py +++ /dev/null @@ -1,714 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2023, Sergei Antipov -# 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 diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index fb24975d7b..fdcf99cbb4 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -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 \ No newline at end of file +jsonpatch + +# requirements for the wsl connection plugin +paramiko >= 3.0.0 ; python_version >= '3.6'