1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-02-04 07:51:50 +00:00

lxd_storage_pool_info, lxd_storage_volume_info: new modules (#11198)

* Fix mistaken rebase

* plugins/modules/lxd_storage_: include error codes, clean up notes

* plugins/modules/lxd_storage_: snap_url, ruff fix

* plugins/modules/lxd_storage_volume_info.py: remove checks on expected api returned bits

* plugins/modules/lxd_storage_volume_info.py: required: true

* tests/integration/targets/lxd_storage_volume_info/tasks/main.yaml: add Test fetching specific volume by name

* tests/unit/plugins/modules/test_lxd_storage_: add unit tests

* tests/integration/targets/lxd_storage_pool_info/tasks/main.yaml: add integratio tests

* tests/integration/targets/lxd_storage_: not required

* tests/integration/targets/lxd_storage_: not required perhaps, lxd_project has them

* tests/unit/plugins/modules/test_lxd_storage_volume_info.py: fix python3.8 tests

* tests/unit/plugins/modules/test_lxd_storage_pool_info.py: fix python3.8

* tests/integration/targets/lxd_storage_: correct paths for aliases

* tests/unit/plugins/modules/test_lxd_storage_volume_info.py: remove backticks

* tests/unit/plugins/modules/test_lxd_storage_volume_info.py: remove blank line

* tests/unit/plugins/modules/test_lxd_storage_: python3.8 changes

* tests/unit/plugins/modules/test_lxd_storage_: python3.8 changes

* tests/unit/plugins/lookup/test_github_app_access_token.py: restore

* tests/unit/plugins/connection/test_wsl.py: restore

* plugins/modules/lxd_storage_: use ANSIBLE_LXD_DEFAULT_SNAP_URL and put API version into const

* lxd_storage_volume_info: use recursion to gather all volume details

* tests/integration/targets/lxd_storage_volume_info/tasks/main.yaml: fix silet skipped failures

* tests/integration/targets/lxd_storage_pool_info/tasks/main.yaml: fix silet failures

* lxd_storage_pool_info: update to use recursion to gather all details in one shot

* Remove unnecessary change.

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Sean McAvoy 2025-12-01 02:58:45 -03:00 committed by GitHub
parent 16d51a8233
commit 6365b5a981
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1243 additions and 0 deletions

View file

@ -0,0 +1,5 @@
# 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

View file

@ -0,0 +1,69 @@
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
# 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
- name: Get information about all storage pools
community.general.lxd_storage_pool_info:
register: pool_info_all
- name: Verify pools information was retrieved
assert:
that:
- pool_info_all is not changed
- pool_info_all is success
- pool_info_all.storage_pools is defined
- pool_info_all.storage_pools is iterable
- name: Get information about default pool
community.general.lxd_storage_pool_info:
name: default
register: pool_info_default
- name: Verify default pool information
assert:
that:
- pool_info_default is not changed
- pool_info_default is success
- pool_info_default.storage_pools | length == 1
- pool_info_default.storage_pools[0].name == 'default'
- name: Test filtering by pool type (dir)
community.general.lxd_storage_pool_info:
type:
- dir
register: pool_info_dir
- name: Verify type filtering works
assert:
that:
- pool_info_dir is not changed
- pool_info_dir is success
- pool_info_dir.storage_pools is defined
- name: Test with non-existent pool (should fail)
community.general.lxd_storage_pool_info:
name: nonexistent-pool-12345
register: pool_info_nonexistent
ignore_errors: true
- name: Verify error handling for non-existent pool
assert:
that:
- pool_info_nonexistent is failed
- name: Test check mode
community.general.lxd_storage_pool_info:
check_mode: true
register: pool_info_check
- name: Verify check mode works
assert:
that:
- pool_info_check is not changed
- pool_info_check is success
- pool_info_check.storage_pools is defined

View file

@ -0,0 +1,5 @@
# 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

View file

@ -0,0 +1,101 @@
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
# 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
- name: Get information about all volumes in default pool
community.general.lxd_storage_volume_info:
pool: default
register: volume_info_all
- name: Verify volumes information was retrieved
assert:
that:
- volume_info_all is not changed
- volume_info_all is success
- volume_info_all.storage_volumes is defined
- volume_info_all.storage_volumes is iterable
- name: Test filtering by volume type (custom)
community.general.lxd_storage_volume_info:
pool: default
type: custom
register: volume_info_custom
- name: Verify type filtering works
assert:
that:
- volume_info_custom is not changed
- volume_info_custom is success
- volume_info_custom.storage_volumes is defined
- volume_info_custom.storage_volumes is iterable
- name: Test filtering by volume type (container)
community.general.lxd_storage_volume_info:
pool: default
type: container
register: volume_info_container
- name: Verify container type filtering works
assert:
that:
- volume_info_container is not changed
- volume_info_container is success
- volume_info_container.storage_volumes is defined
- name: Test fetching specific volume by name
community.general.lxd_storage_volume_info:
pool: default
name: "{{ volume_info_all.storage_volumes[0].name }}"
register: volume_info_specific
when: volume_info_all.storage_volumes | length > 0
- name: Verify specific volume retrieval
assert:
that:
- volume_info_specific is not changed
- volume_info_specific is success
- volume_info_specific.storage_volumes | length == 1
- volume_info_specific.storage_volumes[0].name == volume_info_all.storage_volumes[0].name
when: volume_info_all.storage_volumes | length > 0
- name: Test with non-existent pool (should fail)
community.general.lxd_storage_volume_info:
pool: nonexistent-pool-12345
register: volume_info_nonexistent_pool
ignore_errors: true
- name: Verify error handling for non-existent pool
assert:
that:
- volume_info_nonexistent_pool is failed
- name: Test with non-existent volume name (should fail)
community.general.lxd_storage_volume_info:
pool: default
name: nonexistent-volume-12345
register: volume_info_nonexistent_volume
ignore_errors: true
- name: Verify error handling for non-existent volume
assert:
that:
- volume_info_nonexistent_volume is failed
when: volume_info_nonexistent_volume is defined
- name: Test check mode
community.general.lxd_storage_volume_info:
pool: default
check_mode: true
register: volume_info_check
- name: Verify check mode works
assert:
that:
- volume_info_check is not changed
- volume_info_check is success
- volume_info_check.storage_volumes is defined

View file

@ -0,0 +1,125 @@
# Copyright (c) 2025, Sean McAvoy (@smcavoy) <seanmcavoy@gmail.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
from unittest.mock import patch
from ansible_collections.community.general.plugins.modules import lxd_storage_pool_info as module
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
AnsibleExitJson,
AnsibleFailJson,
ModuleTestCase,
set_module_args,
)
class FakeLXDClient:
responses: dict[tuple[str, str], dict] = {}
def __init__(self, url, key_file=None, cert_file=None, debug=False, **kwargs):
self.url = url
self.key_file = key_file
self.cert_file = cert_file
self.debug = debug
self.logs = [{"type": "fake-request"}] if debug else []
def authenticate(self, trust_password):
self.trust_password = trust_password
def do(self, method, url, ok_error_codes=None, **kwargs):
try:
return self.responses[(method, url)]
except KeyError as exc:
raise AssertionError(f"Unexpected call: {method} {url}") from exc
class TestLXDStoragePoolInfo(ModuleTestCase):
def setUp(self):
super().setUp()
self.module = module
def test_returns_storage_pools(self):
"""Pool metadata from the API is returned unchanged using recursion."""
FakeLXDClient.responses = {
("GET", "/1.0/storage-pools?recursion=1"): {
"type": "sync",
"metadata": [
{"name": "default", "driver": "dir"},
{"name": "fast", "driver": "zfs"},
],
},
}
with patch.object(self.module, "LXDClient", FakeLXDClient):
with patch.object(self.module.os.path, "exists", return_value=False):
with self.assertRaises(AnsibleExitJson) as exc:
with set_module_args({}):
self.module.main()
result = exc.exception.args[0]
assert result["storage_pools"] == [
{"name": "default", "driver": "dir"},
{"name": "fast", "driver": "zfs"},
]
def test_returns_specific_storage_pool(self):
"""When name is specified, only that pool is returned."""
FakeLXDClient.responses = {
("GET", "/1.0/storage-pools/default"): {"type": "sync", "metadata": {"name": "default", "driver": "dir"}},
}
with patch.object(self.module, "LXDClient", FakeLXDClient):
with patch.object(self.module.os.path, "exists", return_value=False):
with self.assertRaises(AnsibleExitJson) as exc:
with set_module_args({"name": "default"}):
self.module.main()
result = exc.exception.args[0]
assert result["storage_pools"] == [
{"name": "default", "driver": "dir"},
]
def test_filters_storage_pools_by_type(self):
"""Pools can be filtered by driver type using recursion."""
FakeLXDClient.responses = {
("GET", "/1.0/storage-pools?recursion=1"): {
"type": "sync",
"metadata": [
{"name": "default", "driver": "dir"},
{"name": "fast", "driver": "zfs"},
],
},
}
with patch.object(self.module, "LXDClient", FakeLXDClient):
with patch.object(self.module.os.path, "exists", return_value=False):
with self.assertRaises(AnsibleExitJson) as exc:
with set_module_args({"type": ["zfs"]}):
self.module.main()
result = exc.exception.args[0]
assert result["storage_pools"] == [
{"name": "fast", "driver": "zfs"},
]
def test_error_code_returned_on_failure(self):
"""Failures surface the LXD error code for easier debugging."""
FakeLXDClient.responses = {
("GET", "/1.0/storage-pools?recursion=1"): {
"type": "error",
"error": "unavailable",
"error_code": 503,
}
}
with patch.object(self.module, "LXDClient", FakeLXDClient):
with patch.object(self.module.os.path, "exists", return_value=False):
with self.assertRaises(AnsibleFailJson) as exc:
with set_module_args({}):
self.module.main()
result = exc.exception.args[0]
assert result["error_code"] == 503
assert "Failed to retrieve storage pools" in result["msg"]

View file

@ -0,0 +1,156 @@
# Copyright (c) 2025, Sean McAvoy (@smcavoy) <seanmcavoy@gmail.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
from unittest.mock import patch
from ansible_collections.community.general.plugins.modules import lxd_storage_volume_info as module
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
AnsibleExitJson,
AnsibleFailJson,
ModuleTestCase,
set_module_args,
)
class FakeLXDClient:
responses: dict[tuple[str, str], dict] = {}
def __init__(self, url, key_file=None, cert_file=None, debug=False, **kwargs):
self.url = url
self.key_file = key_file
self.cert_file = cert_file
self.debug = debug
self.logs = [{"type": "fake-request"}] if debug else []
def authenticate(self, trust_password):
self.trust_password = trust_password
def do(self, method, url, ok_error_codes=None, **kwargs):
try:
return self.responses[(method, url)]
except KeyError as exc:
raise AssertionError(f"Unexpected call: {method} {url}") from exc
class TestLXDStorageVolumeInfo(ModuleTestCase):
def setUp(self):
super().setUp()
self.module = module
def test_returns_all_volumes_for_pool(self):
"""Volume metadata is returned for every volume in the pool using recursion."""
FakeLXDClient.responses = {
("GET", "/1.0/storage-pools/default"): {"type": "sync", "metadata": {"name": "default"}},
("GET", "/1.0/storage-pools/default/volumes?recursion=1"): {
"type": "sync",
"metadata": [
{"name": "data", "type": "custom"},
{"name": "web", "type": "container"},
],
},
}
with patch.object(self.module, "LXDClient", FakeLXDClient):
with patch.object(self.module.os.path, "exists", return_value=False):
with self.assertRaises(AnsibleExitJson) as exc:
with set_module_args({"pool": "default"}):
self.module.main()
result = exc.exception.args[0]
assert result["storage_volumes"] == [
{"name": "data", "type": "custom"},
{"name": "web", "type": "container"},
]
def test_returns_specific_storage_volume(self):
"""When name is specified without type, recursion is used to find it."""
FakeLXDClient.responses = {
("GET", "/1.0/storage-pools/default"): {"type": "sync", "metadata": {"name": "default"}},
("GET", "/1.0/storage-pools/default/volumes?recursion=1"): {
"type": "sync",
"metadata": [
{"name": "data", "type": "custom"},
{"name": "web", "type": "container"},
],
},
}
with patch.object(self.module, "LXDClient", FakeLXDClient):
with patch.object(self.module.os.path, "exists", return_value=False):
with self.assertRaises(AnsibleExitJson) as exc:
with set_module_args({"pool": "default", "name": "data"}):
self.module.main()
result = exc.exception.args[0]
assert result["storage_volumes"] == [
{"name": "data", "type": "custom"},
]
def test_filters_storage_volumes_by_type(self):
"""Volumes can be filtered by type using recursion."""
FakeLXDClient.responses = {
("GET", "/1.0/storage-pools/default"): {"type": "sync", "metadata": {"name": "default"}},
("GET", "/1.0/storage-pools/default/volumes?recursion=1"): {
"type": "sync",
"metadata": [
{"name": "data", "type": "custom"},
{"name": "web", "type": "container"},
],
},
}
with patch.object(self.module, "LXDClient", FakeLXDClient):
with patch.object(self.module.os.path, "exists", return_value=False):
with self.assertRaises(AnsibleExitJson) as exc:
with set_module_args({"pool": "default", "type": "container"}):
self.module.main()
result = exc.exception.args[0]
assert result["storage_volumes"] == [
{"name": "web", "type": "container"},
]
def test_returns_specific_volume_with_type_using_direct_request(self):
"""When both name and type are specified, a direct API call is made (no recursion)."""
FakeLXDClient.responses = {
("GET", "/1.0/storage-pools/default"): {"type": "sync", "metadata": {"name": "default"}},
("GET", "/1.0/storage-pools/default/volumes/custom/data"): {
"type": "sync",
"metadata": {"name": "data", "type": "custom", "config": {"size": "10GiB"}},
},
}
with patch.object(self.module, "LXDClient", FakeLXDClient):
with patch.object(self.module.os.path, "exists", return_value=False):
with self.assertRaises(AnsibleExitJson) as exc:
with set_module_args({"pool": "default", "name": "data", "type": "custom"}):
self.module.main()
result = exc.exception.args[0]
assert result["storage_volumes"] == [
{"name": "data", "type": "custom", "config": {"size": "10GiB"}},
]
def test_error_code_returned_when_listing_volumes_fails(self):
"""Errors from LXD are surfaced with the numeric code."""
FakeLXDClient.responses = {
("GET", "/1.0/storage-pools/default"): {"type": "sync", "metadata": {"name": "default"}},
("GET", "/1.0/storage-pools/default/volumes?recursion=1"): {
"type": "error",
"error": "service unavailable",
"error_code": 503,
},
}
with patch.object(self.module, "LXDClient", FakeLXDClient):
with patch.object(self.module.os.path, "exists", return_value=False):
with self.assertRaises(AnsibleFailJson) as exc:
with set_module_args({"pool": "default"}):
self.module.main()
result = exc.exception.args[0]
assert result["error_code"] == 503
assert "Failed to retrieve volumes from pool" in result["msg"]