From 3e1a7db132eef8408485d9603733e48b93638e43 Mon Sep 17 00:00:00 2001 From: Zhiwei Liang Date: Thu, 22 May 2025 03:38:30 -0400 Subject: [PATCH 1/7] Fix `parse_linode_types` for `JSONObject` --- plugins/module_utils/linode_helper.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/linode_helper.py b/plugins/module_utils/linode_helper.py index 4a30c4741..bbadfb3d0 100644 --- a/plugins/module_utils/linode_helper.py +++ b/plugins/module_utils/linode_helper.py @@ -113,7 +113,7 @@ def handle_updates( obj: linode_api4.Base, params: dict, mutable_fields: set, - register_func: Any, + register_func: Callable, ignore_keys: Set[str] = None, ) -> Set[str]: """Handles updates for a linode_api4 object""" @@ -180,7 +180,7 @@ def handle_updates( return result -def parse_linode_types(value: any) -> any: +def parse_linode_types(value: Any) -> Any: """Helper function for handle_updates. Parses Linode Object types into collections of strings.""" @@ -201,6 +201,9 @@ def parse_linode_types(value: any) -> any: if isinstance(value, MappedObject): return mapping_to_dict(value) + + if isinstance(value, linode_api4.JSONObject): + return value._serialize() return value From 18b1ebf66eb1015c8d4cb264b3a10af9ab5a9940 Mon Sep 17 00:00:00 2001 From: Zhiwei Liang Date: Fri, 23 May 2025 02:44:51 -0400 Subject: [PATCH 2/7] Add `firewall_settings` module and docs --- README.md | 1 + docs/modules/firewall_settings.md | 61 +++++++ .../doc_fragments/firewall_settings.py | 18 +++ plugins/modules/firewall_settings.py | 153 ++++++++++++++++++ 4 files changed, 233 insertions(+) create mode 100644 docs/modules/firewall_settings.md create mode 100644 plugins/module_utils/doc_fragments/firewall_settings.py create mode 100644 plugins/modules/firewall_settings.py diff --git a/README.md b/README.md index b50821ebe..7dc4d3a92 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Name | Description | [linode.cloud.domain_record](./docs/modules/domain_record.md)|Manage Linode Domain Records.| [linode.cloud.firewall](./docs/modules/firewall.md)|Manage Linode Firewalls.| [linode.cloud.firewall_device](./docs/modules/firewall_device.md)|Manage Linode Firewall Devices.| +[linode.cloud.firewall_settings](./docs/modules/firewall_settings.md)|Configure the firewall settings for the account.| [linode.cloud.image](./docs/modules/image.md)|Manage a Linode Image.| [linode.cloud.instance](./docs/modules/instance.md)|Manage Linode Instances, Configs, and Disks.| [linode.cloud.ip](./docs/modules/ip.md)|Allocates a new IPv4 Address on your Account. The Linode must be configured to support additional addresses - please Open a support ticket requesting additional addresses before attempting allocation.| diff --git a/docs/modules/firewall_settings.md b/docs/modules/firewall_settings.md new file mode 100644 index 000000000..f1955fb7f --- /dev/null +++ b/docs/modules/firewall_settings.md @@ -0,0 +1,61 @@ +# firewall_settings + +Configure the firewall settings for the account. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: Update the default firewall settings + linode.cloud.firewall_settings: + default_firewall_ids: + linode: 123456 + nodebalancer: 123456 + public_interface: 123456 + vpc_interface: 123456 +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| [`default_firewall_ids` (sub-options)](#default_firewall_ids) |
`dict`
|
Optional
| The default firewall ID for a `linode`, `nodebalancer`, `public_interface`, or `vpc_interface`. Default firewalls can't be deleted or disabled. **(Updatable)** | +| `state` |
`str`
|
Optional
| The desired state of the target. **(Choices: `present`)** | + +### default_firewall_ids + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `linode` |
`int`
|
Optional
| The Linode's default firewall. | +| `nodebalancer` |
`int`
|
Optional
| The NodeBalancer's default firewall. | +| `public_interface` |
`int`
|
Optional
| The public interface's default firewall. | +| `vpc_interface` |
`int`
|
Optional
| The VPC interface's default firewall. | + +## Return Values + +- `default_firewall_ids` - The default firewall ID for a `linode`, `nodebalancer`, `public_interface`, or `vpc_interface`. Default firewalls can't be deleted or disabled. + + - Sample Response: + ```json + { + "default_firewall_ids": { + "linode": 123456, + "nodebalancer": 123456, + "public_interface": 123456, + "vpc_interface": 123456 + } + } + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/put-firewall-settings) for a list of returned fields + + diff --git a/plugins/module_utils/doc_fragments/firewall_settings.py b/plugins/module_utils/doc_fragments/firewall_settings.py new file mode 100644 index 000000000..b55eb7620 --- /dev/null +++ b/plugins/module_utils/doc_fragments/firewall_settings.py @@ -0,0 +1,18 @@ +specdoc_examples = [''' +- name: Update the default firewall settings + linode.cloud.firewall_settings: + default_firewall_ids: + linode: 123456 + nodebalancer: 123456 + public_interface: 123456 + vpc_interface: 123456''' +] + +result_firewall_settings_samples = ['''{ + "default_firewall_ids": { + "linode": 123456, + "nodebalancer": 123456, + "public_interface": 123456, + "vpc_interface": 123456 + } +}'''] diff --git a/plugins/modules/firewall_settings.py b/plugins/modules/firewall_settings.py new file mode 100644 index 000000000..f5efa063d --- /dev/null +++ b/plugins/modules/firewall_settings.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains all of the functionality for Linode Firewalls Settings.""" + +from __future__ import absolute_import, division, print_function + +from typing import Any, Optional + +import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.firewall_settings as docs +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_collections.linode.cloud.plugins.module_utils.linode_helper import ( + filter_null_values, + handle_updates, +) +from ansible_specdoc.objects import ( + FieldType, + SpecDocMeta, + SpecField, + SpecReturnValue, +) + +try: + from linode_api4 import FirewallSettings +except ImportError: + # handled in module_utils.linode_common + pass + +default_firewall_ids_spec: dict = { + "linode": SpecField( + type=FieldType.integer, + description=["The Linode's default firewall."], + ), + "nodebalancer": SpecField( + type=FieldType.integer, + description=["The NodeBalancer's default firewall."], + ), + "public_interface": SpecField( + type=FieldType.integer, + description=["The public interface's default firewall."], + ), + "vpc_interface": SpecField( + type=FieldType.integer, + description=["The VPC interface's default firewall."], + ), +} + +DEFAULT_FIREWALL_SETTING_DESCRIPTION = "The default firewall ID for a `linode`, `nodebalancer`, `public_interface`, or `vpc_interface`. Default firewalls can't be deleted or disabled." + +firewall_settings_spec: dict = { + "default_firewall_ids": SpecField( + type=FieldType.dict, + suboptions=default_firewall_ids_spec, + editable=True, + description=[DEFAULT_FIREWALL_SETTING_DESCRIPTION], + ), + "state": SpecField( + type=FieldType.string, + description=["The desired state of the target."], + choices=["present"], + required=False, # Firewall settings cannot be removed. So marking `state` field as optional here. + ), +} + +SPECDOC_META = SpecDocMeta( + description=["Configure the firewall settings for the account."], + requirements=global_requirements, + author=global_authors, + options=firewall_settings_spec, + examples=docs.specdoc_examples, + return_values={ + "default_firewall_ids": SpecReturnValue( + description=DEFAULT_FIREWALL_SETTING_DESCRIPTION, + docs_url="https://techdocs.akamai.com/linode-api/reference/put-firewall-settings", + type=FieldType.dict, + sample=docs.result_firewall_settings_samples, + ), + }, +) + + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + + +class LinodeFirewall(LinodeModuleBase): + """Module for creating and destroying Linode Firewalls""" + + def __init__(self) -> None: + self.module_arg_spec = SPECDOC_META.ansible_spec + + self.results: dict = { + "changed": False, + "actions": [], + "firewall_settings": None, + } + + self._firewall_settings: Optional[FirewallSettings] = None + + self._state = "present" + + super().__init__(module_arg_spec=self.module_arg_spec) + + def _get_firewall_settings(self) -> Optional[FirewallSettings]: + try: + return self.client.networking.firewall_settings() + except Exception as exception: + return self.fail( + msg=f"failed to get firewall settings: {exception}" + ) + + def _update_firewall_settings(self) -> None: + """Update the firewall settings""" + + self._firewall_settings = self._get_firewall_settings() + + handle_updates( + self._firewall_settings, + filter_null_values(self.module.params), + set(["default_firewall_ids"]), + self.register_action, + ) + + self._firewall_settings._api_get() + self.results["firewall_settings"] = self._firewall_settings._raw_json + + def exec_module(self, **kwargs: Any) -> Optional[dict]: + """Entrypoint for Firewall Settings module""" + + self._state = self.module.params.get("state", "present") + self._update_firewall_settings() + + return self.results + + +def main() -> None: + """Constructs and calls the Linode Firewall Settings module""" + + LinodeFirewall() + + +if __name__ == "__main__": + main() From 684627e66eaee8c2482041f847cf9837cc1a14cc Mon Sep 17 00:00:00 2001 From: Zhiwei Liang Date: Fri, 23 May 2025 02:45:29 -0400 Subject: [PATCH 3/7] Add `firewall_settings_info` module, docs, and tests --- README.md | 1 + docs/modules/firewall_settings_info.md | 40 ++++++++++++ .../doc_fragments/firewall_settings_info.py | 4 ++ plugins/modules/firewall_settings_info.py | 43 +++++++++++++ .../targets/firewall_settings/tasks/main.yaml | 61 +++++++++++++++++++ 5 files changed, 149 insertions(+) create mode 100644 docs/modules/firewall_settings_info.md create mode 100644 plugins/module_utils/doc_fragments/firewall_settings_info.py create mode 100644 plugins/modules/firewall_settings_info.py create mode 100644 tests/integration/targets/firewall_settings/tasks/main.yaml diff --git a/README.md b/README.md index 7dc4d3a92..1b0aaeabf 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Name | Description | [linode.cloud.domain_info](./docs/modules/domain_info.md)|Get info about a Linode Domain.| [linode.cloud.domain_record_info](./docs/modules/domain_record_info.md)|Get info about a Linode Domain Records.| [linode.cloud.firewall_info](./docs/modules/firewall_info.md)|Get info about a Linode Firewall.| +[linode.cloud.firewall_settings_info](./docs/modules/firewall_settings_info.md)|Get info about a Linode Firewall Settings.| [linode.cloud.image_info](./docs/modules/image_info.md)|Get info about a Linode Image.| [linode.cloud.instance_info](./docs/modules/instance_info.md)|Get info about a Linode Instance.| [linode.cloud.ip_info](./docs/modules/ip_info.md)|Get info about a Linode IP.| diff --git a/docs/modules/firewall_settings_info.md b/docs/modules/firewall_settings_info.md new file mode 100644 index 000000000..2ecd6505a --- /dev/null +++ b/docs/modules/firewall_settings_info.md @@ -0,0 +1,40 @@ +# firewall_settings_info + +Get info about a Linode Firewall Settings. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: Update the default firewall settings + linode.cloud.firewall_settings_info: {} +``` + + +## Return Values + +- `firewall_settings` - The returned Firewall Settings. + + - Sample Response: + ```json + { + "default_firewall_ids": { + "linode": 123456, + "nodebalancer": 123456, + "public_interface": 123456, + "vpc_interface": 123456 + } + } + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-firewall-settings) for a list of returned fields + + diff --git a/plugins/module_utils/doc_fragments/firewall_settings_info.py b/plugins/module_utils/doc_fragments/firewall_settings_info.py new file mode 100644 index 000000000..b2175da0d --- /dev/null +++ b/plugins/module_utils/doc_fragments/firewall_settings_info.py @@ -0,0 +1,4 @@ +specdoc_examples = [''' +- name: Update the default firewall settings + linode.cloud.firewall_settings_info: {}''' +] diff --git a/plugins/modules/firewall_settings_info.py b/plugins/modules/firewall_settings_info.py new file mode 100644 index 000000000..386a9db3b --- /dev/null +++ b/plugins/modules/firewall_settings_info.py @@ -0,0 +1,43 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains all of the functionality for Linode Firewalls Settings.""" + +from __future__ import absolute_import, division, print_function + +import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.firewall_settings as docs_parent +import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.firewall_settings_info as docs +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_info import ( + InfoModule, + InfoModuleResult, +) +from ansible_specdoc.objects import ( + FieldType, +) +from linode_api4 import FirewallSettings + +module = InfoModule( + examples=docs.specdoc_examples, + primary_result=InfoModuleResult( + display_name="Firewall Settings", + field_name="firewall_settings", + field_type=FieldType.dict, + docs_url="https://techdocs.akamai.com/linode-api/reference/get-firewall-settings", + samples=docs_parent.result_firewall_settings_samples, + get=lambda client, _: client.load(FirewallSettings, None)._raw_json, + ), + attributes=[], +) + +SPECDOC_META = module.spec + + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/tests/integration/targets/firewall_settings/tasks/main.yaml b/tests/integration/targets/firewall_settings/tasks/main.yaml new file mode 100644 index 000000000..5ebc04640 --- /dev/null +++ b/tests/integration/targets/firewall_settings/tasks/main.yaml @@ -0,0 +1,61 @@ +# this test case only works if you have configured default firewall to your account + +- name: firewall_device + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Get the default firewall settings + linode.cloud.firewall_settings_info: {} + register: default_firewall_settings + + - name: Create a Linode Firewall + linode.cloud.firewall: + api_version: v4beta + label: 'ansible-test-{{ r }}' + rules: + inbound: [] + inbound_policy: DROP + outbound: [] + outbound_policy: DROP + state: present + register: fw + + - name: Assert firewall created + assert: + that: + - fw.changed + + - name: Update firewall settings + linode.cloud.firewall_settings: + api_version: v4beta + default_firewall_ids: + linode: '{{ fw.firewall.id }}' + register: updated_firewall_settings + + - name: Assert firewall settings updated + assert: + that: + - updated_firewall_settings.changed + - updated_firewall_settings.firewall_settings.default_firewall_ids.linode == fw.firewall.id + + always: + - ignore_errors: true + block: + - name: Restore default firewall settings + linode.cloud.firewall_settings: + api_version: v4beta + default_firewall_ids: + linode: '{{ default_firewall_settings.firewall_settings.default_firewall_ids.linode }}' + + - name: Delete Firewall + linode.cloud.firewall: + label: '{{ fw.firewall.label }}' + state: absent + + environment: + LINODE_UA_PREFIX: '{{ ua_prefix }}' + LINODE_API_TOKEN: '{{ api_token }}' + LINODE_API_URL: '{{ api_url }}' + LINODE_API_VERSION: '{{ api_version }}' + LINODE_CA: '{{ ca_file or "" }}' From cbda88be159c77c1b2ceaa5a28e849a9efe3672e Mon Sep 17 00:00:00 2001 From: Zhiwei Liang Date: Fri, 23 May 2025 02:45:39 -0400 Subject: [PATCH 4/7] Fix helper --- plugins/module_utils/linode_helper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/linode_helper.py b/plugins/module_utils/linode_helper.py index bbadfb3d0..436f2d9d3 100644 --- a/plugins/module_utils/linode_helper.py +++ b/plugins/module_utils/linode_helper.py @@ -5,6 +5,7 @@ import linode_api4 import polling from linode_api4 import ( + JSONObject, LinodeClient, LKENodePool, LKENodePoolNode, @@ -190,6 +191,9 @@ def parse_linode_types(value: Any) -> Any: if isinstance(value, dict): return {k: parse_linode_types(elem) for k, elem in value.items()} + if issubclass(type(value), JSONObject): + return parse_linode_types(value.dict) + if type(value) in { linode_api4.objects.linode.Type, linode_api4.objects.linode.Region, @@ -201,9 +205,6 @@ def parse_linode_types(value: Any) -> Any: if isinstance(value, MappedObject): return mapping_to_dict(value) - - if isinstance(value, linode_api4.JSONObject): - return value._serialize() return value From 5b3e7eb3df7975b57b6ac302f6bea46e3467d1ae Mon Sep 17 00:00:00 2001 From: Zhiwei Liang Date: Fri, 23 May 2025 02:54:56 -0400 Subject: [PATCH 5/7] lint fix --- plugins/modules/firewall_settings.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/modules/firewall_settings.py b/plugins/modules/firewall_settings.py index f5efa063d..88df9955f 100644 --- a/plugins/modules/firewall_settings.py +++ b/plugins/modules/firewall_settings.py @@ -51,7 +51,11 @@ ), } -DEFAULT_FIREWALL_SETTING_DESCRIPTION = "The default firewall ID for a `linode`, `nodebalancer`, `public_interface`, or `vpc_interface`. Default firewalls can't be deleted or disabled." +DEFAULT_FIREWALL_SETTING_DESCRIPTION = ( + "The default firewall ID for a `linode`, `nodebalancer`, " + "`public_interface`, or `vpc_interface`. Default firewalls " + "can't be deleted or disabled." +) firewall_settings_spec: dict = { "default_firewall_ids": SpecField( @@ -64,7 +68,9 @@ type=FieldType.string, description=["The desired state of the target."], choices=["present"], - required=False, # Firewall settings cannot be removed. So marking `state` field as optional here. + # Firewall settings cannot be removed. + # Thus marking `state` field as optional here. + required=False, ), } From decc5fe4aa34f8f582392ae61479aac72f8a872d Mon Sep 17 00:00:00 2001 From: Zhiwei Liang Date: Fri, 23 May 2025 04:11:15 -0400 Subject: [PATCH 6/7] more lint fix --- plugins/modules/firewall_settings_info.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/modules/firewall_settings_info.py b/plugins/modules/firewall_settings_info.py index 386a9db3b..510eb6e40 100644 --- a/plugins/modules/firewall_settings_info.py +++ b/plugins/modules/firewall_settings_info.py @@ -5,8 +5,12 @@ from __future__ import absolute_import, division, print_function -import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.firewall_settings as docs_parent -import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.firewall_settings_info as docs +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + firewall_settings as docs_parent, +) +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + firewall_settings_info as docs, +) from ansible_collections.linode.cloud.plugins.module_utils.linode_common_info import ( InfoModule, InfoModuleResult, From 599747ba84ca27e84851cb51c96e95dacf059bbe Mon Sep 17 00:00:00 2001 From: Zhiwei Liang Date: Fri, 23 May 2025 11:59:54 -0400 Subject: [PATCH 7/7] more more lint fixes --- plugins/module_utils/doc_fragments/firewall_settings.py | 2 ++ plugins/module_utils/doc_fragments/firewall_settings_info.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/plugins/module_utils/doc_fragments/firewall_settings.py b/plugins/module_utils/doc_fragments/firewall_settings.py index b55eb7620..2f414276f 100644 --- a/plugins/module_utils/doc_fragments/firewall_settings.py +++ b/plugins/module_utils/doc_fragments/firewall_settings.py @@ -1,3 +1,5 @@ +"""Documentation fragments for the firewall_settings module""" + specdoc_examples = [''' - name: Update the default firewall settings linode.cloud.firewall_settings: diff --git a/plugins/module_utils/doc_fragments/firewall_settings_info.py b/plugins/module_utils/doc_fragments/firewall_settings_info.py index b2175da0d..0a842ec44 100644 --- a/plugins/module_utils/doc_fragments/firewall_settings_info.py +++ b/plugins/module_utils/doc_fragments/firewall_settings_info.py @@ -1,3 +1,5 @@ +"""Documentation fragments for the firewall_settings_info module""" + specdoc_examples = [''' - name: Update the default firewall settings linode.cloud.firewall_settings_info: {}'''