Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: Add InfoModule class to simplify info module implementations #409

Merged
merged 12 commits into from
Sep 19, 2023
5 changes: 5 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ name: Run Unit test
jobs:
run-tests:
runs-on: ubuntu-latest
defaults:
run:
working-directory: .ansible/collections/ansible_collections/linode/cloud
steps:
- name: checkout repo
uses: actions/checkout@v3
with:
path: .ansible/collections/ansible_collections/linode/cloud

- name: update packages
run: sudo apt-get update -y
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ tmp
__pycache__/
galaxy.yml
venv
collections
.pytest_cache
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ testall: create-integration-config
./scripts/test_all.sh

unittest:
python -m pytest tests/unit/
ansible-test units --target-python default

create-integration-config:
ifneq ("${LINODE_TOKEN}", "")
Expand Down
12 changes: 6 additions & 6 deletions docs/modules/instance_info.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ Get info about a Linode Instance.

| Field | Type | Required | Description |
|-----------|------|----------|------------------------------------------------------------------------------|
| `id` | <center>`int`</center> | <center>Optional</center> | The unique ID of the instance. Optional if `label` is defined. **(Conflicts With: `label`)** |
| `label` | <center>`str`</center> | <center>Optional</center> | The instance’s label. Optional if `id` is defined. **(Conflicts With: `id`)** |
| `label` | <center>`str`</center> | <center>Optional</center> | The label of the Instance to resolve. |
| `id` | <center>`int`</center> | <center>Optional</center> | The ID of the Instance to resolve. |

## Return Values

- `instance` - The instance description in JSON serialized form.
- `instance` - The returned Instance.

- Sample Response:
```json
Expand Down Expand Up @@ -82,7 +82,7 @@ Get info about a Linode Instance.
- See the [Linode API response documentation](https://www.linode.com/docs/api/linode-instances/#linode-view__responses) for a list of returned fields


- `configs` - A list of configs tied to this Linode Instance.
- `configs` - The returned Configs.

- Sample Response:
```json
Expand Down Expand Up @@ -150,7 +150,7 @@ Get info about a Linode Instance.
- See the [Linode API response documentation](https://www.linode.com/docs/api/linode-instances/#configuration-profile-view__responses) for a list of returned fields


- `disks` - A list of disks tied to this Linode Instance.
- `disks` - The returned Disks.

- Sample Response:
```json
Expand All @@ -169,7 +169,7 @@ Get info about a Linode Instance.
- See the [Linode API response documentation](https://www.linode.com/docs/api/linode-instances/#disk-view__responses) for a list of returned fields


- `networking` - Networking information about this Linode Instance.
- `networking` - The returned Networking Configuration.

- Sample Response:
```json
Expand Down
7 changes: 3 additions & 4 deletions docs/modules/stackscript_info.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ Get info about a Linode StackScript.

| Field | Type | Required | Description |
|-----------|------|----------|------------------------------------------------------------------------------|
| `id` | <center>`int`</center> | <center>Optional</center> | The ID of the StackScript. **(Conflicts With: `label`)** |
| `label` | <center>`str`</center> | <center>Optional</center> | The label of the StackScript. **(Conflicts With: `id`)** |
| `label` | <center>`str`</center> | <center>Optional</center> | The label of the StackScript to resolve. |
| `id` | <center>`int`</center> | <center>Optional</center> | The ID of the StackScript to resolve. |

## Return Values

- `stackscript` - The StackScript in JSON serialized form.
- `stackscript` - The returned StackScript.

- Sample Response:
```json
Expand Down Expand Up @@ -64,6 +64,5 @@ Get info about a Linode StackScript.
"username": "myuser"
}
```
- See the [Linode API response documentation](https://www.linode.com/docs/api/stackscripts/#stackscript-view__response-samples) for a list of returned fields


215 changes: 215 additions & 0 deletions plugins/module_utils/linode_common_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

"""This module allows users to list SSH keys in their Linode profile."""

from __future__ import absolute_import, division, print_function

from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional

from ansible_collections.linode.cloud.plugins.module_utils.linode_common import (
LinodeModuleBase,
)
from ansible_collections.linode.cloud.plugins.module_utils.linode_docs import (
global_authors,
global_requirements,
)
from ansible_specdoc.objects import (
FieldType,
SpecDocMeta,
SpecField,
SpecReturnValue,
)
from linode_api4 import LinodeClient


@dataclass
class InfoModuleParam:
"""
Contains information about a required parameter that is necessary to resolve a resource.
e.g. A parent resource ID.

Attributes:
display_name (str): The formatted name of this param for documentation purposes.
type (FieldType): The type of this field.
"""

name: str
display_name: str
type: FieldType


@dataclass
class InfoModuleAttr:
"""
Contains information about an attribute that can be used to select a specific resource
by property.

Attributes:
display_name (str): The formatted name of this attribute for documentation purposes.
type (FieldType): The type of this field.
get (Callable): A function to retrieve a resource from this attribute.
"""

name: str
display_name: str
type: FieldType
get: Callable[[LinodeClient, Dict[str, Any]], Any]


@dataclass
class InfoModuleResult:
"""
Contains information about a result field returned from an info module.

Attributes:
field_name (str): The name of the field to be returned. (e.g. `returned_field`)
field_type (FieldType): The type of the field to be returned.
display_name (str): The formatted name of this field for use in documentation.
docs_url (Optional[str]): The URL of the related API documentation for this field.
samples (Optional[List[str]]): A list of sample results for this field.
get (Optional[Callable]): A function to call out to the API and return the data
for this field.
NOTE: This is only relevant for secondary results.
"""

field_name: str
field_type: FieldType
display_name: str

docs_url: Optional[str] = None
samples: Optional[List[str]] = None
get: Optional[
Callable[[LinodeClient, Dict[str, Any], Dict[str, Any]], Any]
] = None


class InfoModule(LinodeModuleBase):
"""A common module for listing API resources given a set of filters."""

def __init__(
self,
primary_result: InfoModuleResult,
secondary_results: List[InfoModuleResult] = None,
params: List[InfoModuleParam] = None,
attributes: List[InfoModuleAttr] = None,
examples: List[str] = None,
) -> None:
self.primary_result = primary_result
self.secondary_results = secondary_results or []
self.params = params or []
self.attributes = attributes or []
self.examples = examples or []

self.module_arg_spec = self.spec.ansible_spec
self.results: Dict[str, Any] = {
k: None
for k in [
v.field_name
for v in self.secondary_results + [self.primary_result]
]
}

def exec_module(self, **kwargs: Any) -> Optional[dict]:
"""Entrypoint for info modules."""

primary_result = None

# Get the primary result using the attr get functions
for attr in self.attributes:
attr_value = kwargs.get(attr.name)
if attr_value is None:
continue

try:
primary_result = attr.get(self.client, kwargs)
except Exception as exception:
self.fail(
msg=f"Failed to get {self.primary_result.display_name} "
f"with {attr.display_name} {attr_value}: {exception}"
)
break

if primary_result is None:
raise ValueError("Expected a result; got None")

self.results[self.primary_result.field_name] = primary_result

# Pass primary result into secondary result get functions
for attr in self.secondary_results:
try:
secondary_result = attr.get(self.client, primary_result, kwargs)
except Exception as exception:
self.fail(
msg=f"Failed to get {attr.display_name} for "
f"{self.primary_result.display_name}: {exception}"
)
self.results[attr.field_name] = secondary_result

return self.results

@property
def spec(self):
"""
Returns the ansible-specdoc spec for this module.
"""

options = {
"state": SpecField(
type=FieldType.string, required=False, doc_hide=True
),
"label": SpecField(
type=FieldType.string, required=False, doc_hide=True
),
}

# Add params to spec
for param in self.params:
options[param.name] = SpecField(
type=param.type,
required=True,
description=f"The ID of the {param.display_name} for this resource.",
)

# Add attrs to spec
for attr in self.attributes:
options[attr.name] = SpecField(
type=attr.type,
description=f"The {attr.display_name} of the "
f"{self.primary_result.display_name} to resolve.",
)

# Add responses to spec
responses = {
v.field_name: SpecReturnValue(
description=f"The returned {v.display_name}.",
docs_url=v.docs_url,
type=v.field_type,
sample=v.samples,
)
for v in [self.primary_result] + self.secondary_results
}

return SpecDocMeta(
description=[
f"Get info about a Linode {self.primary_result.display_name}."
],
requirements=global_requirements,
author=global_authors,
options=options,
examples=self.examples,
return_values=responses,
)

def run(self) -> None:
"""
Initializes and runs the info module.
"""
attribute_names = [v.name for v in self.attributes]

super().__init__(
module_arg_spec=self.module_arg_spec,
required_one_of=[attribute_names],
mutually_exclusive=[attribute_names],
)
15 changes: 15 additions & 0 deletions plugins/module_utils/linode_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,18 @@ def poll_condition(
step=step,
timeout=timeout,
)


def safe_find(
func: Callable[[Tuple[Filter]], List[Any]], *filters: Filter
) -> Any:
"""
Wraps a resource list function with error handling.
If no entries are returned, this function returns None rather than
raising an error.
"""
try:
list_results = func(*filters)
return None if len(list_results) < 1 else list_results[0]
except Exception as exception:
raise Exception(f"failed to get resource: {exception}") from exception
Loading