1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-02-04 07:51:50 +00:00
community.general/plugins/modules/lxd_storage_volume_info.py
patchback[bot] 721d2bd35d
[PR #11198/6365b5a9 backport][stable-12] lxd_storage_pool_info, lxd_storage_volume_info: new modules (#11238)
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.

---------


(cherry picked from commit 6365b5a981)

Co-authored-by: Sean McAvoy <seanmcavoy@gmail.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
2025-12-01 07:20:55 +01:00

407 lines
14 KiB
Python

# 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
DOCUMENTATION = """
---
module: lxd_storage_volume_info
short_description: Retrieve information about LXD storage volumes
version_added: 12.1.0
description:
- Retrieve information about LXD storage volumes in a specific storage pool.
- This module returns details about all volumes or a specific volume in a pool.
author: "Sean McAvoy (@smcavoy)"
extends_documentation_fragment:
- community.general.attributes
- community.general.attributes.info_module
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
pool:
description:
- Name of the storage pool to query for volumes.
- This parameter is required.
required: true
type: str
name:
description:
- Name of a specific storage volume to retrieve information about.
- If not specified, information about all volumes in the pool are returned.
type: str
type:
description:
- Filter volumes by type.
- Common types include V(container), V(virtual-machine), V(image), and V(custom).
- If not specified, all volume types are returned.
type: str
project:
description:
- 'Project of the storage volume.
See U(https://documentation.ubuntu.com/lxd/en/latest/projects/).'
type: str
url:
description:
- The Unix domain socket path or the https URL for the LXD server.
default: unix:/var/lib/lxd/unix.socket
type: str
snap_url:
description:
- The Unix domain socket path when LXD is installed by snap package manager.
default: unix:/var/snap/lxd/common/lxd/unix.socket
type: str
client_key:
description:
- The client certificate key file path.
- If not specified, it defaults to C($HOME/.config/lxc/client.key).
aliases: [key_file]
type: path
client_cert:
description:
- The client certificate file path.
- If not specified, it defaults to C($HOME/.config/lxc/client.crt).
aliases: [cert_file]
type: path
trust_password:
description:
- The client trusted password.
- 'You need to set this password on the LXD server before
running this module using the following command:
C(lxc config set core.trust_password <some random password>)
See U(https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/).'
- If O(trust_password) is set, this module sends a request for
authentication before sending any requests.
type: str
"""
EXAMPLES = """
- name: Get information about all volumes in the default storage pool
community.general.lxd_storage_volume_info:
pool: default
register: result
- name: Get information about a specific volume
community.general.lxd_storage_volume_info:
pool: default
name: my-volume
register: result
- name: Get information about all custom volumes in a pool
community.general.lxd_storage_volume_info:
pool: default
type: custom
register: result
- name: Get volume information via HTTPS connection
community.general.lxd_storage_volume_info:
url: https://127.0.0.1:8443
trust_password: mypassword
pool: default
name: my-volume
register: result
- name: Get volume information for a specific project
community.general.lxd_storage_volume_info:
project: myproject
pool: default
register: result
- name: Get container volumes only
community.general.lxd_storage_volume_info:
pool: default
type: container
register: result
"""
RETURN = """
storage_volumes:
description: List of LXD storage volumes.
returned: success
type: list
elements: dict
sample: [
{
"name": "my-volume",
"type": "custom",
"used_by": [],
"config": {
"size": "10GiB"
},
"description": "My custom volume",
"content_type": "filesystem",
"location": "none"
}
]
contains:
name:
description: The name of the storage volume.
type: str
returned: success
type:
description: The type of the storage volume.
type: str
returned: success
used_by:
description: List of resources using this storage volume.
type: list
elements: str
returned: success
config:
description: Configuration of the storage volume.
type: dict
returned: success
description:
description: Description of the storage volume.
type: str
returned: success
content_type:
description: Content type of the volume (filesystem or block).
type: str
returned: success
location:
description: Cluster member location for the volume.
type: str
returned: success
logs:
description: The logs of requests and responses.
returned: when ansible-playbook is invoked with -vvvv.
type: list
elements: dict
sample: [
{
"type": "sent request",
"request": {
"method": "GET",
"url": "/1.0/storage-pools/default/volumes",
"json": null,
"timeout": null
},
"response": {
"json": {"type": "sync", "status": "Success"}
}
}
]
"""
import os
from urllib.parse import quote, urlencode
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.lxd import (
LXDClient,
LXDClientException,
default_cert_file,
default_key_file,
)
# ANSIBLE_LXD_DEFAULT_URL is a default value of the lxd endpoint
ANSIBLE_LXD_DEFAULT_URL = "unix:/var/lib/lxd/unix.socket"
ANSIBLE_LXD_DEFAULT_SNAP_URL = "unix:/var/snap/lxd/common/lxd/unix.socket"
# API endpoints
LXD_API_VERSION = "1.0"
LXD_API_STORAGE_POOLS_ENDPOINT = f"/{LXD_API_VERSION}/storage-pools"
class LXDStorageVolumeInfo:
def __init__(self, module: AnsibleModule) -> None:
"""Gather information about LXD storage volumes.
:param module: Processed Ansible Module.
:type module: AnsibleModule
"""
self.module = module
self.pool = self.module.params["pool"]
self.name = self.module.params["name"]
self.volume_type = self.module.params["type"]
self.project = self.module.params["project"]
self.key_file = self.module.params["client_key"]
if self.key_file is None:
self.key_file = default_key_file()
self.cert_file = self.module.params["client_cert"]
if self.cert_file is None:
self.cert_file = default_cert_file()
self.debug = self.module._verbosity >= 4
# check if domain socket to be used
snap_socket_path = self.module.params["snap_url"]
if snap_socket_path.startswith("unix:"):
snap_socket_path = snap_socket_path[5:]
if self.module.params["url"] != ANSIBLE_LXD_DEFAULT_URL:
self.url = self.module.params["url"]
elif os.path.exists(snap_socket_path):
self.url = self.module.params["snap_url"]
else:
self.url = self.module.params["url"]
try:
self.client = LXDClient(
self.url,
key_file=self.key_file,
cert_file=self.cert_file,
debug=self.debug,
)
except LXDClientException as e:
self._fail_from_lxd_exception(e)
self.trust_password = self.module.params["trust_password"]
def _fail_from_lxd_exception(self, exception: LXDClientException) -> None:
"""Build failure parameters from LXDClientException and fail.
:param exception: The LXDClientException instance
:type exception: LXDClientException
"""
fail_params = {
"msg": exception.msg,
"changed": False,
}
if self.client.debug and "logs" in exception.kwargs:
fail_params["logs"] = exception.kwargs["logs"]
self.module.fail_json(**fail_params)
def _build_url(self, endpoint: str) -> str:
"""Build URL with project parameter if specified."""
if self.project:
return f"{endpoint}?{urlencode({'project': self.project})}"
return endpoint
def _check_pool_exists(self) -> None:
"""Verify that the storage pool exists."""
url = self._build_url(f"{LXD_API_STORAGE_POOLS_ENDPOINT}/{quote(self.pool, safe='')}")
resp_json = self.client.do("GET", url, ok_error_codes=[404])
if resp_json["type"] == "error":
if resp_json.get("error_code") == 404:
self.module.fail_json(msg=f'Storage pool "{self.pool}" not found')
else:
self.module.fail_json(
msg=f'Failed to retrieve storage pool "{self.pool}": {resp_json.get("error", "Unknown error")}'
)
def _get_volume_list(self, recursion: int = 0) -> list:
"""Get list of all volumes in the storage pool.
:param recursion: API recursion level (0 for URLs only, 1 for full objects)
:type recursion: int
:return: List of volume URLs (recursion=0) or volume objects (recursion=1)
:rtype: list
"""
endpoint = f"{LXD_API_STORAGE_POOLS_ENDPOINT}/{quote(self.pool, safe='')}/volumes"
if recursion > 0:
endpoint = f"{endpoint}?recursion={recursion}"
url = self._build_url(endpoint)
resp_json = self.client.do("GET", url, ok_error_codes=[])
if resp_json["type"] == "error":
self.module.fail_json(
msg=f'Failed to retrieve volumes from pool "{self.pool}": {resp_json.get("error", "Unknown error")}',
error_code=resp_json.get("error_code"),
)
# With recursion=0: list of volume URLs like ['/1.0/storage-pools/default/volumes/custom/my-volume']
# With recursion=1: list of volume objects with full metadata
return resp_json.get("metadata", [])
def _get_volume_info(self, volume_type: str, volume_name: str) -> dict:
"""Get detailed information about a specific storage volume."""
url = self._build_url(
f"{LXD_API_STORAGE_POOLS_ENDPOINT}/{quote(self.pool, safe='')}/volumes/{quote(volume_type, safe='')}/{quote(volume_name, safe='')}"
)
resp_json = self.client.do("GET", url, ok_error_codes=[404])
if resp_json["type"] == "error":
if resp_json.get("error_code") == 404:
self.module.fail_json(
msg=f'Storage volume "{volume_name}" of type "{volume_type}" not found in pool "{self.pool}"'
)
else:
self.module.fail_json(
msg=f'Failed to retrieve volume "{volume_name}" of type "{volume_type}": {resp_json.get("error", "Unknown error")}'
)
return resp_json.get("metadata", {})
def get_storage_volumes(self) -> list[dict]:
"""Retrieve storage volume information based on module parameters."""
# First check if the pool exists
self._check_pool_exists()
storage_volumes = []
if self.name:
# Get information about a specific volume
# We need to determine the type if not specified
if self.volume_type:
volume_info = self._get_volume_info(self.volume_type, self.name)
storage_volumes.append(volume_info)
else:
# Try to find the volume by name using recursion for efficiency
# This gets all volume objects in a single API call
volumes = self._get_volume_list(recursion=1)
found = False
for volume in volumes:
if volume.get("name") == self.name:
storage_volumes.append(volume)
found = True
break
if not found:
self.module.fail_json(msg=f'Storage volume "{self.name}" not found in pool "{self.pool}"')
else:
# Get information about all volumes in the pool using recursion
# This retrieves all volume details in a single API call instead of one call per volume
volumes = self._get_volume_list(recursion=1)
for volume in volumes:
# Apply type filter if specified
if self.volume_type and volume.get("type") != self.volume_type:
continue
storage_volumes.append(volume)
return storage_volumes
def run(self) -> None:
"""Run the main method."""
try:
if self.trust_password is not None:
self.client.authenticate(self.trust_password)
storage_volumes = self.get_storage_volumes()
result_json = {"storage_volumes": storage_volumes}
if self.client.debug:
result_json["logs"] = self.client.logs
self.module.exit_json(**result_json)
except LXDClientException as e:
self._fail_from_lxd_exception(e)
def main() -> None:
"""Ansible Main module."""
module = AnsibleModule(
argument_spec=dict(
pool=dict(type="str", required=True),
name=dict(type="str"),
type=dict(type="str"),
project=dict(type="str"),
url=dict(type="str", default=ANSIBLE_LXD_DEFAULT_URL),
snap_url=dict(type="str", default=ANSIBLE_LXD_DEFAULT_SNAP_URL),
client_key=dict(type="path", aliases=["key_file"]),
client_cert=dict(type="path", aliases=["cert_file"]),
trust_password=dict(type="str", no_log=True),
),
supports_check_mode=True,
)
lxd_info = LXDStorageVolumeInfo(module=module)
lxd_info.run()
if __name__ == "__main__":
main()