1
0
Fork 0
mirror of https://github.com/ansible-collections/hetzner.hcloud.git synced 2026-02-04 08:01:49 +00:00

feat: per location server types (#692)

[Server Types](https://docs.hetzner.cloud/reference/cloud#server-types)
now depend on
[Locations](https://docs.hetzner.cloud/reference/cloud#locations).

- We added a new `locations` property to the [Server
Types](https://docs.hetzner.cloud/reference/cloud#server-types)
resource. The new property defines a list of supported
[Locations](https://docs.hetzner.cloud/reference/cloud#locations) and
additional per
[Locations](https://docs.hetzner.cloud/reference/cloud#locations)
details such as deprecations information.

- We deprecated the `deprecation` property from the [Server
Types](https://docs.hetzner.cloud/reference/cloud#server-types)
resource. The property will gradually be phased out as per
[Locations](https://docs.hetzner.cloud/reference/cloud#locations)
deprecations are being announced. Please use the new per
[Locations](https://docs.hetzner.cloud/reference/cloud#locations)
deprecation information instead.

See our
[changelog](https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types)
for more details.

**Upgrading**

```yaml
---
- name: Validate server type
  hosts: localhost
  connection: local
  tasks:
    - name: Fetch server type info
      hetzner.hcloud.server_type_info:
        name: cx22
      register: server_type

    - name: Ensure server type exists
      ansible.builtin.assert:
        fail_msg: server type does not exists
        that:
          - server_type.hcloud_server_type_info | count == 1

    - name: Ensure server type is not deprecated
      ansible.builtin.assert:
        fail_msg: server type is deprecated
        that:
          - server_type.hcloud_server_type_info[0].deprecation is none
```

```yaml
---
- name: Validate server type
  hosts: localhost
  connection: local
  tasks:
    - name: Fetch location info
      hetzner.hcloud.location_info:
        name: fsn1
      register: location

    - name: Fetch server type info
      hetzner.hcloud.server_type_info:
        name: cx22
      register: server_type

    - name: Ensure server type exists
      ansible.builtin.assert:
        fail_msg: server type does not exists
        that:
          - server_type.hcloud_server_type_info | count == 1

    - name: Extract server type location info
      ansible.builtin.set_fact:
        server_type_location: >
          {{
            server_type.hcloud_server_type_info[0].locations
            | selectattr("name", "eq", location.hcloud_location_info[0].name)
            | first
          }}

    - name: Ensure server type is not deprecated
      ansible.builtin.assert:
        fail_msg: server type is deprecated in location
        that:
          - server_type_location.deprecation is none
```
This commit is contained in:
Jonas L. 2025-09-26 11:50:05 +02:00 committed by GitHub
parent 4caf3e67f4
commit 826e6a5309
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 500 additions and 36 deletions

View file

@ -0,0 +1,3 @@
minor_changes:
- server_type_info - Return new Server Type ``locations`` property. The new property defines a list of supported Locations and additional per Locations details such as deprecations information.
- server_type_info - Deprecate Server Type ``deprecation`` property. The property will gradually be phased out as per Locations deprecations are being announced. Please use the new per Locations deprecation information instead.

View file

@ -0,0 +1,35 @@
---
- name: Validate server type
hosts: localhost
connection: local
tasks:
- name: Fetch location info
hetzner.hcloud.location_info:
name: fsn1
register: location
- name: Fetch server type info
hetzner.hcloud.server_type_info:
name: cx22
register: server_type
- name: Ensure server type exists
ansible.builtin.assert:
fail_msg: server type does not exists
that:
- server_type.hcloud_server_type_info | count == 1
- name: Extract server type location info
ansible.builtin.set_fact:
server_type_location: >
{{
server_type.hcloud_server_type_info[0].locations
| selectattr("name", "eq", location.hcloud_location_info[0].name)
| first
}}
- name: Ensure server type is not deprecated
ansible.builtin.assert:
fail_msg: server type is deprecated in location
that:
- server_type_location.deprecation is none

View file

@ -0,0 +1,124 @@
from __future__ import annotations
import warnings
from datetime import datetime, timezone
from ansible.module_utils.basic import AnsibleModule
from .vendor.hcloud.locations import BoundLocation
from .vendor.hcloud.server_types import BoundServerType, ServerTypeLocation
DEPRECATED_EXISTING_SERVERS = """
Existing servers of that type will continue to work as before and no action is \
required on your part.
""".strip()
def deprecated_server_type_warning(
module: AnsibleModule,
server_type: BoundServerType,
location: BoundLocation | None = None,
) -> None:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
if server_type.deprecation is not None:
if server_type.deprecation.unavailable_after < datetime.now(timezone.utc):
module.warn(
str.format(
"Server type {server_type} is unavailable in all locations and can no longer be ordered. ",
server_type=server_type.name,
)
+ DEPRECATED_EXISTING_SERVERS,
)
else:
module.warn(
str.format(
"Server type {server_type} is deprecated in all locations and will no longer be available "
"for order as of {unavailable_after}. ",
server_type=server_type.name,
unavailable_after=server_type.deprecation.unavailable_after.strftime("%Y-%m-%d"),
)
+ DEPRECATED_EXISTING_SERVERS,
)
return
deprecated_locations: list[ServerTypeLocation] = []
unavailable_locations: list[ServerTypeLocation] = []
for o in server_type.locations or []:
if o.deprecation is not None:
deprecated_locations.append(o)
if o.deprecation.unavailable_after < datetime.now(timezone.utc):
unavailable_locations.append(o)
if not deprecated_locations:
return
# Warn when the server type is deprecated in the given location
if location:
found = [o for o in deprecated_locations if location.name == o.location.name]
if not found:
return
deprecated_location = found[0]
if deprecated_location in unavailable_locations:
module.warn(
str.format(
"Server type {server_type} is unavailable in {location} and can no longer be ordered. ",
server_type=server_type.name,
location=deprecated_location.location.name,
)
+ DEPRECATED_EXISTING_SERVERS,
)
else:
module.warn(
str.format(
"Server type {server_type} is deprecated in {location} and will no longer be available "
"for order as of {unavailable_after}. ",
server_type=server_type.name,
location=deprecated_location.location.name,
unavailable_after=deprecated_location.deprecation.unavailable_after.strftime("%Y-%m-%d"),
)
+ DEPRECATED_EXISTING_SERVERS,
)
return
# No location given, only warn when all locations are deprecated
if len(server_type.locations) != len(deprecated_locations):
return
if unavailable_locations:
if len(deprecated_locations) != len(unavailable_locations):
module.warn(
str.format(
"Server type {server_type} is deprecated in all locations ({deprecated_locations}) and can no "
"longer be ordered in some locations ({unavailable_locations}). ",
server_type=server_type.name,
deprecated_locations=",".join(o.location.name for o in deprecated_locations),
unavailable_locations=",".join(o.location.name for o in unavailable_locations),
)
+ DEPRECATED_EXISTING_SERVERS,
)
else:
module.warn(
str.format(
"Server type {server_type} is unavailable in all locations ({unavailable_locations}) and can no "
"longer be ordered. ",
server_type=server_type.name,
unavailable_locations=",".join(o.location.name for o in unavailable_locations),
)
+ DEPRECATED_EXISTING_SERVERS,
)
else:
module.warn(
str.format(
"Server type {server_type} is deprecated in all locations ({deprecated_locations}) and will no "
"longer be available for order. ",
server_type=server_type.name,
deprecated_locations=",".join(o.location.name for o in deprecated_locations),
)
+ DEPRECATED_EXISTING_SERVERS,
)

View file

@ -340,11 +340,12 @@ root_password:
sample: YItygq1v3GYjjMomLaKc
"""
from datetime import datetime, timedelta, timezone
from datetime import timedelta
from typing import TYPE_CHECKING, Literal
from ansible.module_utils.basic import AnsibleModule
from ..module_utils.deprecation import deprecated_server_type_warning
from ..module_utils.hcloud import AnsibleHCloud
from ..module_utils.vendor.hcloud import HCloudException
from ..module_utils.vendor.hcloud.firewalls import FirewallResource
@ -408,7 +409,7 @@ class AnsibleHCloudServer(AnsibleHCloud):
def _create_server(self):
self.module.fail_on_missing_params(required_params=["name", "server_type", "image"])
server_type = self._get_server_type()
server_type = self._client_get_by_name_or_id("server_types", self.module.params.get("server_type"))
image = self._get_image(server_type)
params = {
@ -458,18 +459,28 @@ class AnsibleHCloudServer(AnsibleHCloud):
for name_or_id in self.module.params.get("firewalls")
]
server_type_location = None
if self.module.params.get("location") is None and self.module.params.get("datacenter") is None:
# When not given, the API will choose the location.
params["location"] = None
params["datacenter"] = None
elif self.module.params.get("location") is not None and self.module.params.get("datacenter") is None:
params["location"] = self._client_get_by_name_or_id("locations", self.module.params.get("location"))
server_type_location = params["location"]
elif self.module.params.get("location") is None and self.module.params.get("datacenter") is not None:
params["datacenter"] = self._client_get_by_name_or_id("datacenters", self.module.params.get("datacenter"))
server_type_location = params["datacenter"].location
if self.module.params.get("state") == "stopped" or self.module.params.get("state") == "created":
params["start_after_create"] = False
deprecated_server_type_warning(
self.module,
server_type,
server_type_location,
)
if not self.module.check_mode:
try:
resp = self.client.servers.create(**params)
@ -536,35 +547,6 @@ class AnsibleHCloudServer(AnsibleHCloud):
)
return image
def _get_server_type(self) -> ServerType:
server_type = self._client_get_by_name_or_id("server_types", self.module.params.get("server_type"))
self._check_and_warn_deprecated_server(server_type)
return server_type
def _check_and_warn_deprecated_server(self, server_type: ServerType) -> None:
if server_type.deprecation is None:
return
if server_type.deprecation.unavailable_after < datetime.now(timezone.utc):
self.module.warn(
f"Attention: The server plan {server_type.name} is deprecated and can "
"no longer be ordered. Existing servers of that plan will continue to "
"work as before and no action is required on your part. "
"It is possible to migrate this server to another server plan by setting "
"the server_type parameter on the hetzner.hcloud.server module."
)
else:
server_type_unavailable_date = server_type.deprecation.unavailable_after.strftime("%Y-%m-%d")
self.module.warn(
f"Attention: The server plan {server_type.name} is deprecated and will "
f"no longer be available for order as of {server_type_unavailable_date}. "
"Existing servers of that plan will continue to work as before and no "
"action is required on your part. "
"It is possible to migrate this server to another server plan by setting "
"the server_type parameter on the hetzner.hcloud.server module."
)
def _update_server(self) -> None:
try:
previous_server_status = self.hcloud_server.status
@ -686,16 +668,29 @@ class AnsibleHCloudServer(AnsibleHCloud):
# Return if nothing changed
if current.has_id_or_name(wanted):
# Check if we should warn for using an deprecated server type
self._check_and_warn_deprecated_server(self.hcloud_server.server_type)
deprecated_server_type_warning(
self.module,
self.hcloud_server.server_type,
self.hcloud_server.datacenter.location,
)
return
server_type = self._client_get_by_name_or_id("server_types", wanted)
# Check if we should warn for updating to a deprecated server type
deprecated_server_type_warning(
self.module,
server_type,
self.hcloud_server.datacenter.location,
)
self.stop_server_if_forced()
if not self.module.check_mode:
upgrade_disk = self.module.params.get("upgrade_disk")
action = self.hcloud_server.change_type(
server_type=self._get_server_type(),
server_type=server_type,
upgrade_disk=upgrade_disk,
)
# Upgrading a server takes 160 seconds on average, upgrading the disk should

View file

@ -100,6 +100,36 @@ hcloud_server_type_info:
returned: always
type: str
sample: x86
locations:
description: List of supported Locations
returned: always
type: list
contains:
id:
description: Numeric identifier of the Location
returned: always
type: int
sample: 1
name:
description: Name of the Location
returned: always
type: str
sample: fsn1
deprecation:
description: Wether the Server Type is deprecated in the Location.
returned: when deprecated
type: dict
contains:
announced:
description: Date of the deprecation announcement.
returned: when deprecated
type: str
sample: "2025-09-09T09:00:00Z"
unavailable_after:
description: Date after which the Server Type will be unavailable for new order.
returned: when deprecated
type: str
sample: "2025-12-09T09:00:00Z"
included_traffic:
description: |
Free traffic per month in bytes
@ -113,6 +143,9 @@ hcloud_server_type_info:
description: |
Describes if, when & how the resources was deprecated.
If this field is set to None the resource is not deprecated. If it has a value, it is considered deprecated.
B(Deprecated): This field is deprecated and will gradually be phased starting 24 September 2025. Use the locations field instead.
See U(https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types).
returned: success
type: dict
contains:
@ -163,6 +196,21 @@ class AnsibleHCloudServerTypeInfo(AnsibleHCloud):
"storage_type": server_type.storage_type,
"cpu_type": server_type.cpu_type,
"architecture": server_type.architecture,
"locations": [
{
"id": o.location.id,
"name": o.location.name,
"deprecation": (
{
"announced": o.deprecation.announced.isoformat(),
"unavailable_after": o.deprecation.unavailable_after.isoformat(),
}
if o.deprecation is not None
else None
),
}
for o in server_type.locations or []
],
"included_traffic": server_type.included_traffic,
"deprecation": (
{

View file

@ -1,4 +1,4 @@
# Copyright: (c) 2019, 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)
---
hcloud_server_type_id_deprecated: 2 # cx11-ceph
hcloud_server_type_id_deprecated: 1 # cx11

View file

@ -65,5 +65,10 @@
that:
- result.hcloud_server_type_info | list | count == 1
- result.hcloud_server_type_info[0].deprecation is not none
- result.hcloud_server_type_info[0].deprecation.announced == '2021-11-09T09:00:00+00:00'
- result.hcloud_server_type_info[0].deprecation.unavailable_after == '2021-12-01T00:00:00+00:00'
- result.hcloud_server_type_info[0].deprecation.announced == '2024-06-06T08:00:00+00:00'
- result.hcloud_server_type_info[0].deprecation.unavailable_after == '2024-09-06T00:00:00+00:00'
- result.hcloud_server_type_info[0].locations[0].id == 2
- result.hcloud_server_type_info[0].locations[0].name == "nbg1"
- result.hcloud_server_type_info[0].locations[0].deprecation is not none
- result.hcloud_server_type_info[0].locations[0].deprecation.announced == '2024-06-06T08:00:00+00:00'
- result.hcloud_server_type_info[0].locations[0].deprecation.unavailable_after == '2024-09-06T00:00:00+00:00'

View file

@ -0,0 +1,254 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from unittest import mock
import pytest
from ansible_collections.hetzner.hcloud.plugins.module_utils.deprecation import (
deprecated_server_type_warning,
)
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.locations import (
BoundLocation,
)
from ansible_collections.hetzner.hcloud.plugins.module_utils.vendor.hcloud.server_types import (
BoundServerType,
)
PAST = datetime.now(timezone.utc) - timedelta(days=14)
FUTURE = datetime.now(timezone.utc) + timedelta(days=14)
LOCATION_FSN = {
"id": 1,
"name": "fsn1",
}
LOCATION_NBG = {
"id": 2,
"name": "nbg1",
}
DEPRECATION_NONE = {
"deprecation": None,
}
DEPRECATION_DEPRECATED = {
"deprecation": {
"announced": PAST.isoformat(),
"unavailable_after": FUTURE.isoformat(),
},
}
DEPRECATION_UNAVAILABLE = {
"deprecation": {
"announced": PAST.isoformat(),
"unavailable_after": PAST.isoformat(),
},
}
@pytest.mark.parametrize(
("server_type", "location", "calls"),
[
(
BoundServerType(
mock.Mock(),
{"name": "cx22", "locations": []},
),
BoundLocation(mock.Mock(), LOCATION_FSN),
[],
),
# - Deprecated (backward compatible)
(
BoundServerType(
mock.Mock(),
{"name": "cx22", **DEPRECATION_DEPRECATED},
),
None,
[
mock.call(
"Server type cx22 is deprecated in all locations and will no longer "
f"be available for order as of {FUTURE.strftime('%Y-%m-%d')}. "
"Existing servers of that type will continue to work as before and "
"no action is required on your part."
)
],
),
# - Unavailable (backward compatible)
(
BoundServerType(
mock.Mock(),
{"name": "cx22", **DEPRECATION_UNAVAILABLE},
),
None,
[
mock.call(
"Server type cx22 is unavailable in all locations and can no longer "
"be ordered. Existing servers of that type will continue to work as "
"before and no action is required on your part."
)
],
),
# - SOME locations are deprecated
# - Given location is NOT deprecated
(
BoundServerType(
mock.Mock(),
{
"name": "cx22",
"locations": [
{**LOCATION_FSN, **DEPRECATION_NONE},
{**LOCATION_NBG, **DEPRECATION_DEPRECATED},
],
},
),
BoundLocation(mock.Mock(), LOCATION_FSN),
[],
),
# - SOME locations are deprecated
# - Given location is deprecated
(
BoundServerType(
mock.Mock(),
{
"name": "cx22",
"locations": [
{**LOCATION_FSN, **DEPRECATION_NONE},
{**LOCATION_NBG, **DEPRECATION_DEPRECATED},
],
},
),
BoundLocation(mock.Mock(), LOCATION_NBG),
[
mock.call(
"Server type cx22 is deprecated in nbg1 and will no longer be available "
f"for order as of {FUTURE.strftime('%Y-%m-%d')}. Existing servers of "
"that type will continue to work as before and no action is required "
"on your part."
)
],
),
# - SOME locations are unavailable
# - Given location is unavailable
(
BoundServerType(
mock.Mock(),
{
"name": "cx22",
"locations": [
{**LOCATION_FSN, **DEPRECATION_NONE},
{**LOCATION_NBG, **DEPRECATION_UNAVAILABLE},
],
},
),
BoundLocation(mock.Mock(), LOCATION_NBG),
[
mock.call(
"Server type cx22 is unavailable in nbg1 and can no longer be ordered. "
"Existing servers of that type will continue to work as before and no "
"action is required on your part."
)
],
),
# - SOME locations are deprecated
# - Location is not given
(
BoundServerType(
mock.Mock(),
{
"name": "cx22",
"locations": [
{**LOCATION_FSN, **DEPRECATION_NONE},
{**LOCATION_NBG, **DEPRECATION_DEPRECATED},
],
},
),
None,
[],
),
# - SOME locations are unavailable
# - Location is not given
(
BoundServerType(
mock.Mock(),
{
"name": "cx22",
"locations": [
{**LOCATION_FSN, **DEPRECATION_NONE},
{**LOCATION_NBG, **DEPRECATION_UNAVAILABLE},
],
},
),
None,
[],
),
# - SOME locations are deprecated
# - SOME locations are unavailable
# - Location is not given
(
BoundServerType(
mock.Mock(),
{
"name": "cx22",
"locations": [
{**LOCATION_FSN, **DEPRECATION_DEPRECATED},
{**LOCATION_NBG, **DEPRECATION_UNAVAILABLE},
],
},
),
None,
[
mock.call(
"Server type cx22 is deprecated in all locations (fsn1,nbg1) and "
"can no longer be ordered in some locations (nbg1). Existing servers"
" of that type will continue to work as before and no action is "
"required on your part."
)
],
),
# - ALL locations are deprecated
# - Location is not given
(
BoundServerType(
mock.Mock(),
{
"name": "cx22",
"locations": [
{**LOCATION_FSN, **DEPRECATION_DEPRECATED},
{**LOCATION_NBG, **DEPRECATION_DEPRECATED},
],
},
),
None,
[
mock.call(
"Server type cx22 is deprecated in all locations (fsn1,nbg1) and "
"will no longer be available for order. Existing servers of that "
"type will continue to work as before and no action is required on "
"your part."
)
],
),
# - ALL locations are unavailable
# - Location is not given
(
BoundServerType(
mock.Mock(),
{
"name": "cx22",
"locations": [
{**LOCATION_FSN, **DEPRECATION_UNAVAILABLE},
{**LOCATION_NBG, **DEPRECATION_UNAVAILABLE},
],
},
),
None,
[
mock.call(
"Server type cx22 is unavailable in all locations (fsn1,nbg1) and "
"can no longer be ordered. Existing servers of that type will "
"continue to work as before and no action is required on your part."
)
],
),
],
)
def test_deprecated_server_type_warning(server_type, location, calls):
m = mock.Mock()
deprecated_server_type_warning(m, server_type, location)
m.warn.assert_has_calls(calls)