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:
parent
16d51a8233
commit
6365b5a981
9 changed files with 1243 additions and 0 deletions
5
tests/integration/targets/lxd_storage_pool_info/aliases
Normal file
5
tests/integration/targets/lxd_storage_pool_info/aliases
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
125
tests/unit/plugins/modules/test_lxd_storage_pool_info.py
Normal file
125
tests/unit/plugins/modules/test_lxd_storage_pool_info.py
Normal 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"]
|
||||
156
tests/unit/plugins/modules/test_lxd_storage_volume_info.py
Normal file
156
tests/unit/plugins/modules/test_lxd_storage_volume_info.py
Normal 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"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue