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/dimensiondata_vlan.py
patchback[bot] b769b0bc01
[PR #11400/236b9c0e backport][stable-12] Sort imports with ruff check --fix (#11409)
Sort imports with ruff check --fix (#11400)

Sort imports with ruff check --fix.

(cherry picked from commit 236b9c0e04)

Co-authored-by: Felix Fontein <felix@fontein.de>
2026-01-09 19:36:52 +01:00

530 lines
17 KiB
Python

#!/usr/bin/python
# Copyright (c) 2016 Dimension Data
# 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
#
# Authors:
# - Adam Friedman <tintoy@tintoy.io>
from __future__ import annotations
DOCUMENTATION = r"""
module: dimensiondata_vlan
short_description: Manage a VLAN in a Cloud Control network domain
extends_documentation_fragment:
- community.general.dimensiondata
- community.general.dimensiondata_wait
- community.general.attributes
description:
- Manage VLANs in Cloud Control network domains.
deprecated:
removed_in: 13.0.0
why: Service and its endpoints are no longer available.
alternative: There is none.
author: 'Adam Friedman (@tintoy)'
attributes:
check_mode:
support: none
diff_mode:
support: none
options:
name:
description:
- The name of the target VLAN.
type: str
required: true
description:
description:
- A description of the VLAN.
type: str
default: ''
network_domain:
description:
- The ID or name of the target network domain.
required: true
type: str
private_ipv4_base_address:
description:
- The base address for the VLAN's IPv4 network (for example V(192.168.1.0)).
type: str
default: ''
private_ipv4_prefix_size:
description:
- The size of the IPv4 address space, for example V(24).
- Required, if O(private_ipv4_base_address) is specified.
type: int
default: 0
state:
description:
- The desired state for the target VLAN.
- V(readonly) ensures that the state is only ever read, not modified (the module fails if the resource does not exist).
choices: [present, absent, readonly]
default: present
type: str
allow_expand:
description:
- Permit expansion of the target VLAN's network if the module parameters specify a larger network than the VLAN currently
possesses.
- If V(false), the module fails under these conditions.
- This is intended to prevent accidental expansion of a VLAN's network (since this operation is not reversible).
type: bool
default: false
"""
EXAMPLES = r"""
- name: Add or update VLAN
community.general.dimensiondata_vlan:
region: na
location: NA5
network_domain: test_network
name: my_vlan1
description: A test VLAN
private_ipv4_base_address: 192.168.23.0
private_ipv4_prefix_size: 24
state: present
wait: true
- name: Read / get VLAN details
community.general.dimensiondata_vlan:
region: na
location: NA5
network_domain: test_network
name: my_vlan1
state: readonly
wait: true
- name: Delete a VLAN
community.general.dimensiondata_vlan:
region: na
location: NA5
network_domain: test_network
name: my_vlan_1
state: absent
wait: true
"""
RETURN = r"""
vlan:
description: Dictionary describing the VLAN.
returned: On success when O(state=present)
type: complex
contains:
id:
description: VLAN ID.
type: str
sample: "aaaaa000-a000-4050-a215-2808934ccccc"
name:
description: VLAN name.
type: str
sample: "My VLAN"
description:
description: VLAN description.
type: str
sample: "My VLAN description"
location:
description: Datacenter location.
type: str
sample: NA3
private_ipv4_base_address:
description: The base address for the VLAN's private IPV4 network.
type: str
sample: 192.168.23.0
private_ipv4_prefix_size:
description: The prefix size for the VLAN's private IPV4 network.
type: int
sample: 24
private_ipv4_gateway_address:
description: The gateway address for the VLAN's private IPV4 network.
type: str
sample: 192.168.23.1
private_ipv6_base_address:
description: The base address for the VLAN's IPV6 network.
type: str
sample: 2402:9900:111:1195:0:0:0:0
private_ipv6_prefix_size:
description: The prefix size for the VLAN's IPV6 network.
type: int
sample: 64
private_ipv6_gateway_address:
description: The gateway address for the VLAN's IPV6 network.
type: str
sample: 2402:9900:111:1195:0:0:0:1
status:
description: VLAN status.
type: str
sample: NORMAL
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.dimensiondata import (
DimensionDataModule,
UnknownNetworkError,
)
try:
from libcloud.common.dimensiondata import DimensionDataAPIException, DimensionDataVlan
HAS_LIBCLOUD = True
except ImportError:
DimensionDataVlan = None
HAS_LIBCLOUD = False
class DimensionDataVlanModule(DimensionDataModule):
"""
The dimensiondata_vlan module for Ansible.
"""
def __init__(self):
"""
Create a new Dimension Data VLAN module.
"""
super().__init__(
module=AnsibleModule(
argument_spec=DimensionDataModule.argument_spec_with_wait(
name=dict(required=True, type="str"),
description=dict(default="", type="str"),
network_domain=dict(required=True, type="str"),
private_ipv4_base_address=dict(default="", type="str"),
private_ipv4_prefix_size=dict(default=0, type="int"),
allow_expand=dict(default=False, type="bool"),
state=dict(default="present", choices=["present", "absent", "readonly"]),
),
required_together=DimensionDataModule.required_together(),
)
)
self.name = self.module.params["name"]
self.description = self.module.params["description"]
self.network_domain_selector = self.module.params["network_domain"]
self.private_ipv4_base_address = self.module.params["private_ipv4_base_address"]
self.private_ipv4_prefix_size = self.module.params["private_ipv4_prefix_size"]
self.state = self.module.params["state"]
self.allow_expand = self.module.params["allow_expand"]
if self.wait and self.state != "present":
self.module.fail_json(msg='The wait parameter is only supported when state is "present".')
def state_present(self):
"""
Ensure that the target VLAN is present.
"""
network_domain = self._get_network_domain()
vlan = self._get_vlan(network_domain)
if not vlan:
if self.module.check_mode:
self.module.exit_json(
msg=f'VLAN "{self.name}" is absent from network domain "{self.network_domain_selector}" (should be present).',
changed=True,
)
vlan = self._create_vlan(network_domain)
self.module.exit_json(
msg=f'Created VLAN "{self.name}" in network domain "{self.network_domain_selector}".',
vlan=vlan_to_dict(vlan),
changed=True,
)
else:
diff = VlanDiff(vlan, self.module.params)
if not diff.has_changes():
self.module.exit_json(
msg=f'VLAN "{self.name}" is present in network domain "{self.network_domain_selector}" (no changes detected).',
vlan=vlan_to_dict(vlan),
changed=False,
)
return
try:
diff.ensure_legal_change()
except InvalidVlanChangeError as invalid_vlan_change:
self.module.fail_json(
msg=f'Unable to update VLAN "{self.name}" in network domain "{self.network_domain_selector}": {invalid_vlan_change}'
)
if diff.needs_expand() and not self.allow_expand:
self.module.fail_json(
msg=f"The configured private IPv4 network size ({self.private_ipv4_prefix_size}-bit prefix) for "
f"the VLAN differs from its current network size ({vlan.private_ipv4_range_size}-bit prefix) "
"and needs to be expanded. Use allow_expand=true if this is what you want."
)
if self.module.check_mode:
self.module.exit_json(
msg=f'VLAN "{self.name}" is present in network domain "{self.network_domain_selector}" (changes detected).',
vlan=vlan_to_dict(vlan),
changed=True,
)
if diff.needs_edit():
vlan.name = self.name
vlan.description = self.description
self.driver.ex_update_vlan(vlan)
if diff.needs_expand():
vlan.private_ipv4_range_size = self.private_ipv4_prefix_size
self.driver.ex_expand_vlan(vlan)
self.module.exit_json(
msg=f'Updated VLAN "{self.name}" in network domain "{self.network_domain_selector}".',
vlan=vlan_to_dict(vlan),
changed=True,
)
def state_readonly(self):
"""
Read the target VLAN's state.
"""
network_domain = self._get_network_domain()
vlan = self._get_vlan(network_domain)
if vlan:
self.module.exit_json(vlan=vlan_to_dict(vlan), changed=False)
else:
self.module.fail_json(
msg=f'VLAN "{self.name}" does not exist in network domain "{self.network_domain_selector}".'
)
def state_absent(self):
"""
Ensure that the target VLAN is not present.
"""
network_domain = self._get_network_domain()
vlan = self._get_vlan(network_domain)
if not vlan:
self.module.exit_json(
msg=f'VLAN "{self.name}" is absent from network domain "{self.network_domain_selector}".', changed=False
)
return
if self.module.check_mode:
self.module.exit_json(
msg=f'VLAN "{self.name}" is present in network domain "{self.network_domain_selector}" (should be absent).',
vlan=vlan_to_dict(vlan),
changed=True,
)
self._delete_vlan(vlan)
self.module.exit_json(
msg=f'Deleted VLAN "{self.name}" from network domain "{self.network_domain_selector}".', changed=True
)
def _get_vlan(self, network_domain):
"""
Retrieve the target VLAN details from CloudControl.
:param network_domain: The target network domain.
:return: The VLAN, or None if the target VLAN was not found.
:rtype: DimensionDataVlan
"""
vlans = self.driver.ex_list_vlans(location=self.location, network_domain=network_domain)
matching_vlans = [vlan for vlan in vlans if vlan.name == self.name]
if matching_vlans:
return matching_vlans[0]
return None
def _create_vlan(self, network_domain):
vlan = self.driver.ex_create_vlan(
network_domain, self.name, self.private_ipv4_base_address, self.description, self.private_ipv4_prefix_size
)
if self.wait:
vlan = self._wait_for_vlan_state(vlan.id, "NORMAL")
return vlan
def _delete_vlan(self, vlan):
try:
self.driver.ex_delete_vlan(vlan)
# Not currently supported for deletes due to a bug in libcloud (module will error out if "wait" is specified when "state" is not "present").
if self.wait:
self._wait_for_vlan_state(vlan, "NOT_FOUND")
except DimensionDataAPIException as api_exception:
self.module.fail_json(
msg=f'Failed to delete VLAN "{vlan.id}" due to unexpected error from the CloudControl API: {api_exception.msg}'
)
def _wait_for_vlan_state(self, vlan, state_to_wait_for):
network_domain = self._get_network_domain()
wait_poll_interval = self.module.params["wait_poll_interval"]
wait_time = self.module.params["wait_time"]
# Bizarre bug in libcloud when checking status after delete; socket.error is too generic to catch in this context so for now we don't even try.
try:
return self.driver.connection.wait_for_state(
state_to_wait_for, self.driver.ex_get_vlan, wait_poll_interval, wait_time, vlan
)
except DimensionDataAPIException as api_exception:
if api_exception.code != "RESOURCE_NOT_FOUND":
raise
return DimensionDataVlan(
id=vlan.id,
status="NOT_FOUND",
name="",
description="",
private_ipv4_range_address="",
private_ipv4_range_size=0,
ipv4_gateway="",
ipv6_range_address="",
ipv6_range_size=0,
ipv6_gateway="",
location=self.location,
network_domain=network_domain,
)
def _get_network_domain(self):
"""
Retrieve the target network domain from the Cloud Control API.
:return: The network domain.
"""
try:
return self.get_network_domain(self.network_domain_selector, self.location)
except UnknownNetworkError:
self.module.fail_json(
msg=f'Cannot find network domain "{self.network_domain_selector}" in datacenter "{self.location}".'
)
return None
class InvalidVlanChangeError(Exception):
"""
Error raised when an illegal change to VLAN state is attempted.
"""
pass
class VlanDiff:
"""
Represents differences between VLAN information (from CloudControl) and module parameters.
"""
def __init__(self, vlan, module_params):
"""
:param vlan: The VLAN information from CloudControl.
:type vlan: DimensionDataVlan
:param module_params: The module parameters.
:type module_params: dict
"""
self.vlan = vlan
self.module_params = module_params
self.name_changed = module_params["name"] != vlan.name
self.description_changed = module_params["description"] != vlan.description
self.private_ipv4_base_address_changed = (
module_params["private_ipv4_base_address"] != vlan.private_ipv4_range_address
)
self.private_ipv4_prefix_size_changed = (
module_params["private_ipv4_prefix_size"] != vlan.private_ipv4_range_size
)
# Is configured prefix size greater than or less than the actual prefix size?
private_ipv4_prefix_size_difference = module_params["private_ipv4_prefix_size"] - vlan.private_ipv4_range_size
self.private_ipv4_prefix_size_increased = private_ipv4_prefix_size_difference > 0
self.private_ipv4_prefix_size_decreased = private_ipv4_prefix_size_difference < 0
def has_changes(self):
"""
Does the VlanDiff represent any changes between the VLAN and module configuration?
:return: True, if there are change changes; otherwise, False.
"""
return self.needs_edit() or self.needs_expand()
def ensure_legal_change(self):
"""
Ensure the change (if any) represented by the VlanDiff represents a legal change to VLAN state.
- private_ipv4_base_address cannot be changed
- private_ipv4_prefix_size must be greater than or equal to the VLAN's existing private_ipv4_range_size
:raise InvalidVlanChangeError: The VlanDiff does not represent a legal change to VLAN state.
"""
# Cannot change base address for private IPv4 network.
if self.private_ipv4_base_address_changed:
raise InvalidVlanChangeError("Cannot change the private IPV4 base address for an existing VLAN.")
# Cannot shrink private IPv4 network (by increasing prefix size).
if self.private_ipv4_prefix_size_increased:
raise InvalidVlanChangeError(
"Cannot shrink the private IPV4 network for an existing VLAN (only expand is supported)."
)
def needs_edit(self):
"""
Is an Edit operation required to resolve the differences between the VLAN information and the module parameters?
:return: True, if an Edit operation is required; otherwise, False.
"""
return self.name_changed or self.description_changed
def needs_expand(self):
"""
Is an Expand operation required to resolve the differences between the VLAN information and the module parameters?
The VLAN's network is expanded by reducing the size of its network prefix.
:return: True, if an Expand operation is required; otherwise, False.
"""
return self.private_ipv4_prefix_size_decreased
def vlan_to_dict(vlan):
return {
"id": vlan.id,
"name": vlan.name,
"description": vlan.description,
"location": vlan.location.id,
"private_ipv4_base_address": vlan.private_ipv4_range_address,
"private_ipv4_prefix_size": vlan.private_ipv4_range_size,
"private_ipv4_gateway_address": vlan.ipv4_gateway,
"ipv6_base_address": vlan.ipv6_range_address,
"ipv6_prefix_size": vlan.ipv6_range_size,
"ipv6_gateway_address": vlan.ipv6_gateway,
"status": vlan.status,
}
def main():
module = DimensionDataVlanModule()
if module.state == "present":
module.state_present()
elif module.state == "readonly":
module.state_readonly()
elif module.state == "absent":
module.state_absent()
if __name__ == "__main__":
main()