Skip to content

Added support for Host/VM Maintenance #532

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

Open
wants to merge 3 commits into
base: proj/maintenance-policy
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions linode_api4/groups/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .lke import *
from .lke_tier import *
from .longview import *
from .maintenance import *
from .networking import *
from .nodebalancer import *
from .object_storage import *
Expand Down
2 changes: 1 addition & 1 deletion linode_api4/groups/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def maintenance(self):
"""
Returns a collection of Maintenance objects for any entity a user has permissions to view. Cancelled Maintenance objects are not returned.

API Documentation: https://techdocs.akamai.com/linode-api/reference/get-account-logins
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-maintenance

:returns: A list of Maintenance objects on this account.
:rtype: List of Maintenance objects as MappedObjects
Expand Down
5 changes: 5 additions & 0 deletions linode_api4/groups/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def instance_create(
int,
]
] = None,
maintenance_policy_id: Optional[int] = None,
**kwargs,
):
"""
Expand Down Expand Up @@ -296,6 +297,9 @@ def instance_create(
:type interfaces: list[ConfigInterface] or list[dict[str, Any]]
:param placement_group: A Placement Group to create this Linode under.
:type placement_group: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]
:param maintenance_policy_id: The ID of the maintenance policy to apply during maintenance.
If not provided, the default policy (migrate) will be applied.
:type maintenance_policy_id: int
:returns: A new Instance object, or a tuple containing the new Instance and
the generated password.
Expand Down Expand Up @@ -327,6 +331,7 @@ def instance_create(
"firewall_id": firewall,
"backup_id": backup,
"stackscript_id": stackscript,
"maintenance_policy_id": maintenance_policy_id,
# Special cases
"disk_encryption": (
str(disk_encryption) if disk_encryption else None
Expand Down
23 changes: 23 additions & 0 deletions linode_api4/groups/maintenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from linode_api4.groups import Group
from linode_api4.objects import MappedObject


class MaintenanceGroup(Group):
"""
Collections related to Maintenance.
"""

def maintenance_policies(self):
"""
Returns a collection of MaintenancePolicy objects representing
available maintenance policies that can be applied to Linodes

API Documentation: TODO

:returns: A list of Maintenance Policies that can be applied to Linodes
:rtype: List of MaintenancePolicy objects as MappedObjects
"""

result = self.client.get("/maintenance/policies", model=self)

return [MappedObject(**r) for r in result]
5 changes: 5 additions & 0 deletions linode_api4/linode_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
LinodeGroup,
LKEGroup,
LongviewGroup,
MaintenanceGroup,
NetworkingGroup,
NodeBalancerGroup,
ObjectStorageGroup,
Expand Down Expand Up @@ -149,6 +150,10 @@ def __init__(
#: more information
self.account = AccountGroup(self)

#: Access methods related to Maintenance Policies - see :any:`MaintenanceGroup` for
#: more information
self.maintenance = MaintenanceGroup(self)

#: Access methods related to networking on your account - see
#: :any:`NetworkingGroup` for more information
self.networking = NetworkingGroup(self)
Expand Down
9 changes: 8 additions & 1 deletion linode_api4/objects/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ class AccountSettings(Base):
),
"object_storage": Property(),
"backups_enabled": Property(mutable=True),
"maintenance_policy_id": Property(mutable=True),
}


Expand All @@ -219,12 +220,18 @@ class Event(Base):
"user_id": Property(),
"username": Property(),
"entity": Property(),
"time_remaining": Property(),
"time_remaining": Property(), # Deprecated
"rate": Property(),
"status": Property(),
"duration": Property(),
"secondary_entity": Property(),
"message": Property(),
"maintenance_policy_set": Property(),
"description": Property(),
"source": Property(),
"not_before": Property(is_datetime=True),
"start_time": Property(is_datetime=True),
"complete_time": Property(is_datetime=True),
}

@property
Expand Down
1 change: 1 addition & 0 deletions linode_api4/objects/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ class Instance(Base):
"disk_encryption": Property(),
"lke_cluster_id": Property(),
"capabilities": Property(unordered=True),
"maintenance_policy_id": Property(mutable=True),
}

@property
Expand Down
56 changes: 30 additions & 26 deletions test/fixtures/account_events_123.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
{
"action": "ticket_create",
"created": "2018-01-01T00:01:01",
"duration": 300.56,
"entity": {
"id": 11111,
"label": "Problem booting my Linode",
"type": "ticket",
"url": "/v4/support/tickets/11111"
},
"id": 123,
"message": "None",
"percent_complete": null,
"rate": null,
"read": true,
"secondary_entity": {
"id": "linode/debian9",
"label": "linode1234",
"type": "linode",
"url": "/v4/linode/instances/1234"
},
"seen": true,
"status": null,
"time_remaining": null,
"username": "exampleUser"
}

"action": "ticket_create",
"created": "2025-03-25T12:00:00",
"duration": 300.56,
"entity": {
"id": 11111,
"label": "Problem booting my Linode",
"type": "ticket",
"url": "/v4/support/tickets/11111"
},
"id": 123,
"message": "Ticket created for user issue.",
"percent_complete": null,
"rate": null,
"read": true,
"secondary_entity": {
"id": "linode/debian9",
"label": "linode1234",
"type": "linode",
"url": "/v4/linode/instances/1234"
},
"seen": true,
"status": "completed",
"username": "exampleUser",
"maintenance_policy_set": "Tentative",
"description": "Scheduled maintenance",
"source": "user",
"not_before": "2025-03-25T12:00:00",
"start_time": "2025-03-25T12:30:00",
"complete_time": "2025-03-25T13:00:00"
}
45 changes: 28 additions & 17 deletions test/fixtures/account_maintenance.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
{
"data": [
{
"entity": {
"id": 123,
"label": "demo-linode",
"type": "Linode",
"url": "https://api.linode.com/v4/linode/instances/{linodeId}"
},
"reason": "This maintenance will allow us to update the BIOS on the host's motherboard.",
"status": "started",
"type": "reboot",
"when": "2020-07-09T00:01:01"
}
],
"page": 1,
"pages": 1,
"results": 1
"pages": 1,
"page": 1,
"results": 1,
"data": [
{
"body": "Scheduled upgrade to faster NVMe hardware. This will affect Linode #1234.",
"entity": {
"id": 1234,
"label": "Linode #1234",
"type": "linode",
"url": "/linodes/1234"
},
"label": "Scheduled Maintenance for Linode #1234",
"message": "Scheduled upgrade to faster NVMe hardware.",
"severity": "major",
"type": "maintenance_scheduled",
"event_type": "linode_migrate",
"maintenance_policy_set": "Power on/off",
"description": "Scheduled Maintenance",
"source": "platform",
"not_before": "2025-03-25T10:00:00",
"start_time": "2025-03-25T12:00:00",
"complete_time": "2025-03-25T14:00:00",
"status": "scheduled",
"when": "2025-03-25T12:00:00",
"until": "2025-03-25T14:00:00"
}
]
}
3 changes: 2 additions & 1 deletion test/fixtures/account_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"managed": false,
"network_helper": false,
"object_storage": "active",
"backups_enabled": true
"backups_enabled": true,
"maintenance_policy_id": 1
}
6 changes: 4 additions & 2 deletions test/fixtures/linode_instances.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"label": "test",
"placement_group_type": "anti_affinity:local",
"placement_group_policy": "strict"
}
},
"maintenance_policy_id" : 1
},
{
"group": "test",
Expand Down Expand Up @@ -90,7 +91,8 @@
"watchdog_enabled": false,
"disk_encryption": "enabled",
"lke_cluster_id": 18881,
"placement_group": null
"placement_group": null,
"maintenance_policy_id" : 2
}
]
}
18 changes: 18 additions & 0 deletions test/fixtures/maintenance_policies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"id": "1",
"name": "Default Migrate",
"description": "predefined maintenance policy default for all linodes",
"type": "migrate",
"notification_period_sec": 3600,
"is_default": true
},
{
"id": "2",
"name": "Default Power On/Off",
"description": "predefined maintenance policy for general use cases",
"type": "power on/off",
"notification_period_sec": 1800,
"is_default": false
}
]
14 changes: 14 additions & 0 deletions test/unit/groups/linode_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ def test_create_with_placement_group(self):
m.call_data["placement_group"], {"id": 123, "compliant_only": True}
)

def test_create_with_mainteance_policy_id(self):
"""
Tests that you can create a Linode with a maintenance policy
"""

with self.mock_post("linode/instances/123") as m:
self.client.linode.instance_create(
"g6-nanode-1",
"eu-west",
maintenance_policy_id=1,
)

self.assertEqual(m.call_data["maintenance_policy_id"], 1)


class TypeTest(ClientBaseCase):
def test_get_types(self):
Expand Down
76 changes: 74 additions & 2 deletions test/unit/linode_client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,51 @@ def get_mock(*params, verify=True, **kwargs):
assert called


class MaintenanceGroupTest(ClientBaseCase):
"""
Tests methods of the MaintenanceGroup
"""

def test_maintenance(self):
"""
Tests that maintenance can be retrieved
"""
with self.mock_get("/maintenance/policies") as m:
result = self.client.maintenance.maintenance_policies()

self.assertEqual(m.call_url, "/maintenance/policies")
self.assertEqual(len(result), 2)

policy_default_migrate = result[0]
policy_default_power_on_off = result[1]

self.assertEqual(policy_default_migrate.id, "1")
self.assertEqual(policy_default_migrate.name, "Default Migrate")
self.assertEqual(
policy_default_migrate.description,
"predefined maintenance policy default for all linodes",
)
self.assertEqual(policy_default_migrate.type, "migrate")
self.assertEqual(
policy_default_migrate.notification_period_sec, 3600
)
self.assertEqual(policy_default_migrate.is_default, True)

self.assertEqual(policy_default_power_on_off.id, "2")
self.assertEqual(
policy_default_power_on_off.name, "Default Power On/Off"
)
self.assertEqual(
policy_default_power_on_off.description,
"predefined maintenance policy for general use cases",
)
self.assertEqual(policy_default_power_on_off.type, "power on/off")
self.assertEqual(
policy_default_power_on_off.notification_period_sec, 1800
)
self.assertEqual(policy_default_power_on_off.is_default, False)


class AccountGroupTest(ClientBaseCase):
"""
Tests methods of the AccountGroup
Expand Down Expand Up @@ -353,12 +398,39 @@ def test_maintenance(self):
"""
with self.mock_get("/account/maintenance") as m:
result = self.client.account.maintenance()

self.assertEqual(m.call_url, "/account/maintenance")
self.assertEqual(len(result), 1)

maintenance = result[0]

self.assertEqual(
maintenance.body,
"Scheduled upgrade to faster NVMe hardware. This will affect Linode #1234.",
)
self.assertEqual(maintenance.entity.id, 1234)
self.assertEqual(maintenance.entity.label, "Linode #1234")
self.assertEqual(maintenance.entity.type, "linode")
self.assertEqual(maintenance.entity.url, "/linodes/1234")
self.assertEqual(
maintenance.label, "Scheduled Maintenance for Linode #1234"
)
self.assertEqual(
result[0].reason,
"This maintenance will allow us to update the BIOS on the host's motherboard.",
maintenance.message,
"Scheduled upgrade to faster NVMe hardware.",
)
self.assertEqual(maintenance.severity, "major")
self.assertEqual(maintenance.type, "maintenance_scheduled")
self.assertEqual(maintenance.event_type, "linode_migrate")
self.assertEqual(maintenance.maintenance_policy_set, "Power on/off")
self.assertEqual(maintenance.description, "Scheduled Maintenance")
self.assertEqual(maintenance.source, "platform")
self.assertEqual(maintenance.not_before, "2025-03-25T10:00:00")
self.assertEqual(maintenance.start_time, "2025-03-25T12:00:00")
self.assertEqual(maintenance.complete_time, "2025-03-25T14:00:00")
self.assertEqual(maintenance.status, "scheduled")
self.assertEqual(maintenance.when, "2025-03-25T12:00:00")
self.assertEqual(maintenance.until, "2025-03-25T14:00:00")

def test_notifications(self):
"""
Expand Down
Loading