1
0
Fork 0
mirror of https://github.com/ansible-collections/hetzner.hcloud.git synced 2026-02-03 23:51:48 +00:00
hetzner.hcloud/plugins/modules/primary_ip.py
Julian Tölle 241f61338b
fix: unassign primary ip and floating ip before deletion (#796)
##### SUMMARY

Explicitly unassign Primary IP and Floating IPs before deleting them.
This allows us to better handle any errors that happen during the
unassign and is consistent with the flow in our Terraform provider.

##### ISSUE TYPE

- Bugfix Pull Request

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below
-->

- `floating_ip`
- `primary_ip`

---------

Co-authored-by: jo <ljonas@riseup.net>
2026-01-29 11:10:01 +01:00

366 lines
12 KiB
Python

#!/usr/bin/python
# Copyright: (c) 2022, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = """
---
module: primary_ip
short_description: Create and manage cloud Primary IPs on the Hetzner Cloud.
description:
- Create, update and manage cloud Primary IPs on the Hetzner Cloud.
- To manage the DNS pointer of a Primary IP, use the M(hetzner.hcloud.rdns) module.
author:
- Lukas Kaemmerling (@lkaemmerling)
version_added: 1.8.0
options:
id:
description:
- The ID of the Hetzner Cloud Primary IPs to manage.
- Only required if no Primary IP O(name) is given.
type: int
name:
description:
- The Name of the Hetzner Cloud Primary IPs to manage.
- Only required if no Primary IP O(id) is given or a Primary IP does not exist.
type: str
location:
description:
- ID or name of the Location the Hetzner Cloud Primary IP will be bound to.
- Required if no O(server) or O(datacenter) is given and Primary IP does not exist.
type: str
datacenter:
description:
- B(Deprecated:) The O(datacenter) argument is deprecated and will be removed
after 1 July 2026. Please use the O(location) argument instead.
See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters.
- Home Location of the Hetzner Cloud Primary IP.
- Required if no O(server) or O(location) is given and Primary IP does not exist.
type: str
server:
description:
- Name or ID of the Hetzner Cloud Server the Primary IP should be assigned to.
- The Primary IP cannot be assigned to a running server.
- Required if no O(datacenter) is given and the Primary IP does not exist.
type: str
type:
description:
- Type of the Primary IP.
- Required if Primary IP does not exist
choices: [ ipv4, ipv6 ]
type: str
auto_delete:
description:
- Delete the Primary IP when the resource it is assigned to is deleted.
type: bool
default: false
delete_protection:
description:
- Protect the Primary IP for deletion.
type: bool
labels:
description:
- User-defined labels (key-value pairs).
type: dict
state:
description:
- State of the Primary IP.
default: present
choices: [ absent, present ]
type: str
extends_documentation_fragment:
- hetzner.hcloud.hcloud
"""
EXAMPLES = """
- name: Create a IPv4 Primary IP
hetzner.hcloud.primary_ip:
name: my-primary-ip
location: fsn1
type: ipv4
state: present
- name: Create a IPv6 Primary IP
hetzner.hcloud.primary_ip:
name: my-primary-ip
location: fsn1
type: ipv6
state: present
- name: Delete a Primary IP
hetzner.hcloud.primary_ip:
name: my-primary-ip
state: absent
- name: Ensure the server is stopped
hetzner.hcloud.server:
name: my-server
state: stopped
- name: Create a Primary IP attached to a Server
hetzner.hcloud.primary_ip:
name: my-primary-ip
server: my-server
type: ipv4
state: present
- name: Ensure the server is started
hetzner.hcloud.server:
name: my-server
state: started
"""
RETURN = """
hcloud_primary_ip:
description: The Primary IP instance
returned: Always
type: dict
contains:
id:
description: ID of the Primary IP
type: int
returned: Always
sample: 12345
name:
description: Name of the Primary IP
type: str
returned: Always
sample: my-primary-ip
ip:
description: IP Address of the Primary IP
type: str
returned: Always
sample: 116.203.104.109
type:
description: Type of the Primary IP
type: str
returned: Always
sample: ipv4
location:
description: Name of the Location of the Primary IP
type: str
returned: Always
sample: fsn1
datacenter:
description: |
Name of the datacenter of the Primary IP
B(Deprecated:) The RV(hcloud_primary_ip.datacenter) value is deprecated and will be removed
after 1 July 2026. Please use the RV(hcloud_primary_ip.location) value instead.
See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters.
type: str
returned: Always
sample: fsn1-dc14
delete_protection:
description: True if Primary IP is protected for deletion
type: bool
returned: always
sample: false
labels:
description: User-defined labels (key-value pairs)
type: dict
returned: Always
sample:
key: value
mylabel: 123
assignee_id:
description: ID of the resource the Primary IP is assigned to, null if it is not assigned.
type: int
returned: always
sample: 1937415
assignee_type:
description: Resource type the Primary IP can be assigned to.
type: str
returned: always
sample: server
auto_delete:
description: Delete the Primary IP when the resource it is assigned to is deleted.
type: bool
returned: always
sample: false
"""
from typing import TYPE_CHECKING
from ..module_utils import _primary_ip
from ..module_utils._base import AnsibleHCloud, AnsibleModule
from ..module_utils._vendor.hcloud import HCloudException
from ..module_utils._vendor.hcloud.primary_ips import BoundPrimaryIP
if TYPE_CHECKING:
from ..module_utils._vendor.hcloud.servers import BoundServer
class AnsiblePrimaryIP(AnsibleHCloud):
represent = "primary_ip"
primary_ip: BoundPrimaryIP | None = None
def _prepare_result(self):
if self.primary_ip is None:
return {}
return _primary_ip.prepare_result(self.primary_ip)
def _get(self):
if (value := self.module.params.get("id")) is not None:
self.primary_ip = self.client.primary_ips.get_by_id(value)
elif (value := self.module.params.get("name")) is not None:
self.primary_ip = self.client.primary_ips.get_by_name(value)
def _create(self):
self.fail_on_invalid_params(
required=["name", "type"],
required_one_of=[["server", "location", "datacenter"]],
)
params = {
"name": self.module.params.get("name"),
"type": self.module.params.get("type"),
}
if (value := self.module.params.get("location")) is not None:
params["location"] = self._client_get_by_name_or_id("locations", value)
elif (value := self.module.params.get("datacenter")) is not None:
self.module.warn(
"The `datacenter` argument is deprecated and will be removed "
"after 1 July 2026. Please use the `location` argument instead. "
"See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters."
)
# Backward compatible datacenter argument.
# datacenter hel1-dc2 => location hel1
# pylint: disable=disallowed-name
part1, _, _ = str(value).partition("-")
params["location"] = self.client.locations.get_by_name(part1)
elif (value := self.module.params.get("server")) is not None:
server: BoundServer = self._client_get_by_name_or_id("servers", value)
params["assignee_id"] = server.id
if (value := self.module.params.get("auto_delete")) is not None:
params["auto_delete"] = value
if (value := self.module.params.get("labels")) is not None:
params["labels"] = value
if not self.module.check_mode:
resp = self.client.primary_ips.create(**params)
if resp.action is not None:
resp.action.wait_until_finished()
self.primary_ip = resp.primary_ip
self._mark_as_changed()
if (value := self.module.params.get("delete_protection")) is not None:
if not self.module.check_mode:
action = self.primary_ip.change_protection(delete=value)
action.wait_until_finished()
self._mark_as_changed()
if not self.module.check_mode:
self.primary_ip.reload()
def _update(self):
need_reload = False
if (value := self.module.params.get("delete_protection")) is not None:
if value != self.primary_ip.protection["delete"]:
if not self.module.check_mode:
action = self.primary_ip.change_protection(delete=value)
action.wait_until_finished()
need_reload = True
self._mark_as_changed()
params = {}
if (value := self.module.params.get("auto_delete")) is not None:
if value != self.primary_ip.auto_delete:
params["auto_delete"] = value
self._mark_as_changed()
if (value := self.module.params.get("labels")) is not None:
if value != self.primary_ip.labels:
params["labels"] = value
self._mark_as_changed()
if params or need_reload:
if not self.module.check_mode:
self.primary_ip = self.primary_ip.update(**params)
def _delete(self):
if self.primary_ip.assignee_id is not None:
if not self.module.check_mode:
action = self.primary_ip.unassign()
action.wait_until_finished()
self._mark_as_changed()
if not self.module.check_mode:
self.primary_ip.delete()
self.primary_ip = None
self._mark_as_changed()
def present(self):
try:
self._get()
if self.primary_ip is None:
self._create()
else:
self._update()
except HCloudException as exception:
self.fail_json_hcloud(exception)
def delete(self):
try:
self._get()
if self.primary_ip is not None:
self._delete()
except HCloudException as exception:
self.fail_json_hcloud(exception)
@classmethod
def define_module(cls):
return AnsibleModule(
argument_spec=dict(
id={"type": "int"},
name={"type": "str"},
location={"type": "str"},
datacenter={
"type": "str",
"removed_at_date": "2026-07-01",
"removed_from_collection": "hetzner.hcloud",
},
server={"type": "str"},
auto_delete={"type": "bool", "default": False},
type={"choices": ["ipv4", "ipv6"]},
labels={"type": "dict"},
delete_protection={"type": "bool"},
state={
"choices": ["absent", "present"],
"default": "present",
},
**super().base_module_arguments(),
),
required_one_of=[["id", "name"]],
supports_check_mode=True,
)
def main():
o = AnsiblePrimaryIP(AnsiblePrimaryIP.define_module())
match o.module.params["state"]:
case "absent":
o.delete()
case "present":
o.present()
result = o.get_result()
result["hcloud_primary_ip"] = result.pop(o.represent)
o.module.exit_json(**result)
if __name__ == "__main__":
main()