Skip to content

Commit a2a1a56

Browse files
Added support for Host/VM Maintenance (#532)
* Added support for changes in Account Events, Maintenance, and Settings * Added support for changes in Instance and added Maintenance group * Add docstring and fix imports
1 parent 0ecf79a commit a2a1a56

16 files changed

+260
-54
lines changed

linode_api4/groups/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .lke import *
1111
from .lke_tier import *
1212
from .longview import *
13+
from .maintenance import *
1314
from .networking import *
1415
from .nodebalancer import *
1516
from .object_storage import *

linode_api4/groups/account.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def maintenance(self):
201201
"""
202202
Returns a collection of Maintenance objects for any entity a user has permissions to view. Cancelled Maintenance objects are not returned.
203203
204-
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-account-logins
204+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-maintenance
205205
206206
:returns: A list of Maintenance objects on this account.
207207
:rtype: List of Maintenance objects as MappedObjects

linode_api4/groups/linode.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ def instance_create(
153153
int,
154154
]
155155
] = None,
156+
maintenance_policy_id: Optional[int] = None,
156157
**kwargs,
157158
):
158159
"""
@@ -296,6 +297,9 @@ def instance_create(
296297
:type interfaces: list[ConfigInterface] or list[dict[str, Any]]
297298
:param placement_group: A Placement Group to create this Linode under.
298299
:type placement_group: Union[InstancePlacementGroupAssignment, PlacementGroup, Dict[str, Any], int]
300+
:param maintenance_policy_id: The ID of the maintenance policy to apply during maintenance.
301+
If not provided, the default policy (migrate) will be applied.
302+
:type maintenance_policy_id: int
299303
300304
:returns: A new Instance object, or a tuple containing the new Instance and
301305
the generated password.
@@ -327,6 +331,7 @@ def instance_create(
327331
"firewall_id": firewall,
328332
"backup_id": backup,
329333
"stackscript_id": stackscript,
334+
"maintenance_policy_id": maintenance_policy_id,
330335
# Special cases
331336
"disk_encryption": (
332337
str(disk_encryption) if disk_encryption else None

linode_api4/groups/maintenance.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from linode_api4.groups import Group
2+
from linode_api4.objects import MappedObject
3+
4+
5+
class MaintenanceGroup(Group):
6+
"""
7+
Collections related to Maintenance.
8+
"""
9+
10+
def maintenance_policies(self):
11+
"""
12+
Returns a collection of MaintenancePolicy objects representing
13+
available maintenance policies that can be applied to Linodes
14+
15+
API Documentation: TODO
16+
17+
:returns: A list of Maintenance Policies that can be applied to Linodes
18+
:rtype: List of MaintenancePolicy objects as MappedObjects
19+
"""
20+
21+
result = self.client.get("/maintenance/policies", model=self)
22+
23+
return [MappedObject(**r) for r in result]

linode_api4/linode_client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
LinodeGroup,
2020
LKEGroup,
2121
LongviewGroup,
22+
MaintenanceGroup,
2223
NetworkingGroup,
2324
NodeBalancerGroup,
2425
ObjectStorageGroup,
@@ -149,6 +150,10 @@ def __init__(
149150
#: more information
150151
self.account = AccountGroup(self)
151152

153+
#: Access methods related to Maintenance Policies - see :any:`MaintenanceGroup` for
154+
#: more information
155+
self.maintenance = MaintenanceGroup(self)
156+
152157
#: Access methods related to networking on your account - see
153158
#: :any:`NetworkingGroup` for more information
154159
self.networking = NetworkingGroup(self)

linode_api4/objects/account.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ class AccountSettings(Base):
197197
),
198198
"object_storage": Property(),
199199
"backups_enabled": Property(mutable=True),
200+
"maintenance_policy_id": Property(mutable=True),
200201
}
201202

202203

@@ -219,12 +220,18 @@ class Event(Base):
219220
"user_id": Property(),
220221
"username": Property(),
221222
"entity": Property(),
222-
"time_remaining": Property(),
223+
"time_remaining": Property(), # Deprecated
223224
"rate": Property(),
224225
"status": Property(),
225226
"duration": Property(),
226227
"secondary_entity": Property(),
227228
"message": Property(),
229+
"maintenance_policy_set": Property(),
230+
"description": Property(),
231+
"source": Property(),
232+
"not_before": Property(is_datetime=True),
233+
"start_time": Property(is_datetime=True),
234+
"complete_time": Property(is_datetime=True),
228235
}
229236

230237
@property

linode_api4/objects/linode.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ class Instance(Base):
686686
"disk_encryption": Property(),
687687
"lke_cluster_id": Property(),
688688
"capabilities": Property(unordered=True),
689+
"maintenance_policy_id": Property(mutable=True),
689690
}
690691

691692
@property

test/fixtures/account_events_123.json

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
{
2-
"action": "ticket_create",
3-
"created": "2018-01-01T00:01:01",
4-
"duration": 300.56,
5-
"entity": {
6-
"id": 11111,
7-
"label": "Problem booting my Linode",
8-
"type": "ticket",
9-
"url": "/v4/support/tickets/11111"
10-
},
11-
"id": 123,
12-
"message": "None",
13-
"percent_complete": null,
14-
"rate": null,
15-
"read": true,
16-
"secondary_entity": {
17-
"id": "linode/debian9",
18-
"label": "linode1234",
19-
"type": "linode",
20-
"url": "/v4/linode/instances/1234"
21-
},
22-
"seen": true,
23-
"status": null,
24-
"time_remaining": null,
25-
"username": "exampleUser"
26-
}
27-
2+
"action": "ticket_create",
3+
"created": "2025-03-25T12:00:00",
4+
"duration": 300.56,
5+
"entity": {
6+
"id": 11111,
7+
"label": "Problem booting my Linode",
8+
"type": "ticket",
9+
"url": "/v4/support/tickets/11111"
10+
},
11+
"id": 123,
12+
"message": "Ticket created for user issue.",
13+
"percent_complete": null,
14+
"rate": null,
15+
"read": true,
16+
"secondary_entity": {
17+
"id": "linode/debian9",
18+
"label": "linode1234",
19+
"type": "linode",
20+
"url": "/v4/linode/instances/1234"
21+
},
22+
"seen": true,
23+
"status": "completed",
24+
"username": "exampleUser",
25+
"maintenance_policy_set": "Tentative",
26+
"description": "Scheduled maintenance",
27+
"source": "user",
28+
"not_before": "2025-03-25T12:00:00",
29+
"start_time": "2025-03-25T12:30:00",
30+
"complete_time": "2025-03-25T13:00:00"
31+
}
Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
{
2-
"data": [
3-
{
4-
"entity": {
5-
"id": 123,
6-
"label": "demo-linode",
7-
"type": "Linode",
8-
"url": "https://api.linode.com/v4/linode/instances/{linodeId}"
9-
},
10-
"reason": "This maintenance will allow us to update the BIOS on the host's motherboard.",
11-
"status": "started",
12-
"type": "reboot",
13-
"when": "2020-07-09T00:01:01"
14-
}
15-
],
16-
"page": 1,
17-
"pages": 1,
18-
"results": 1
2+
"pages": 1,
3+
"page": 1,
4+
"results": 1,
5+
"data": [
6+
{
7+
"body": "Scheduled upgrade to faster NVMe hardware. This will affect Linode #1234.",
8+
"entity": {
9+
"id": 1234,
10+
"label": "Linode #1234",
11+
"type": "linode",
12+
"url": "/linodes/1234"
13+
},
14+
"label": "Scheduled Maintenance for Linode #1234",
15+
"message": "Scheduled upgrade to faster NVMe hardware.",
16+
"severity": "major",
17+
"type": "maintenance_scheduled",
18+
"event_type": "linode_migrate",
19+
"maintenance_policy_set": "Power on/off",
20+
"description": "Scheduled Maintenance",
21+
"source": "platform",
22+
"not_before": "2025-03-25T10:00:00",
23+
"start_time": "2025-03-25T12:00:00",
24+
"complete_time": "2025-03-25T14:00:00",
25+
"status": "scheduled",
26+
"when": "2025-03-25T12:00:00",
27+
"until": "2025-03-25T14:00:00"
28+
}
29+
]
1930
}

test/fixtures/account_settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"managed": false,
44
"network_helper": false,
55
"object_storage": "active",
6-
"backups_enabled": true
6+
"backups_enabled": true,
7+
"maintenance_policy_id": 1
78
}

test/fixtures/linode_instances.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
"label": "test",
4949
"placement_group_type": "anti_affinity:local",
5050
"placement_group_policy": "strict"
51-
}
51+
},
52+
"maintenance_policy_id" : 1
5253
},
5354
{
5455
"group": "test",
@@ -90,7 +91,8 @@
9091
"watchdog_enabled": false,
9192
"disk_encryption": "enabled",
9293
"lke_cluster_id": 18881,
93-
"placement_group": null
94+
"placement_group": null,
95+
"maintenance_policy_id" : 2
9496
}
9597
]
9698
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[
2+
{
3+
"id": "1",
4+
"name": "Default Migrate",
5+
"description": "predefined maintenance policy default for all linodes",
6+
"type": "migrate",
7+
"notification_period_sec": 3600,
8+
"is_default": true
9+
},
10+
{
11+
"id": "2",
12+
"name": "Default Power On/Off",
13+
"description": "predefined maintenance policy for general use cases",
14+
"type": "power on/off",
15+
"notification_period_sec": 1800,
16+
"is_default": false
17+
}
18+
]

test/unit/groups/linode_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ def test_create_with_placement_group(self):
9696
m.call_data["placement_group"], {"id": 123, "compliant_only": True}
9797
)
9898

99+
def test_create_with_mainteance_policy_id(self):
100+
"""
101+
Tests that you can create a Linode with a maintenance policy
102+
"""
103+
104+
with self.mock_post("linode/instances/123") as m:
105+
self.client.linode.instance_create(
106+
"g6-nanode-1",
107+
"eu-west",
108+
maintenance_policy_id=1,
109+
)
110+
111+
self.assertEqual(m.call_data["maintenance_policy_id"], 1)
112+
99113

100114
class TypeTest(ClientBaseCase):
101115
def test_get_types(self):

test/unit/linode_client_test.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,51 @@ def get_mock(*params, verify=True, **kwargs):
307307
assert called
308308

309309

310+
class MaintenanceGroupTest(ClientBaseCase):
311+
"""
312+
Tests methods of the MaintenanceGroup
313+
"""
314+
315+
def test_maintenance(self):
316+
"""
317+
Tests that maintenance can be retrieved
318+
"""
319+
with self.mock_get("/maintenance/policies") as m:
320+
result = self.client.maintenance.maintenance_policies()
321+
322+
self.assertEqual(m.call_url, "/maintenance/policies")
323+
self.assertEqual(len(result), 2)
324+
325+
policy_default_migrate = result[0]
326+
policy_default_power_on_off = result[1]
327+
328+
self.assertEqual(policy_default_migrate.id, "1")
329+
self.assertEqual(policy_default_migrate.name, "Default Migrate")
330+
self.assertEqual(
331+
policy_default_migrate.description,
332+
"predefined maintenance policy default for all linodes",
333+
)
334+
self.assertEqual(policy_default_migrate.type, "migrate")
335+
self.assertEqual(
336+
policy_default_migrate.notification_period_sec, 3600
337+
)
338+
self.assertEqual(policy_default_migrate.is_default, True)
339+
340+
self.assertEqual(policy_default_power_on_off.id, "2")
341+
self.assertEqual(
342+
policy_default_power_on_off.name, "Default Power On/Off"
343+
)
344+
self.assertEqual(
345+
policy_default_power_on_off.description,
346+
"predefined maintenance policy for general use cases",
347+
)
348+
self.assertEqual(policy_default_power_on_off.type, "power on/off")
349+
self.assertEqual(
350+
policy_default_power_on_off.notification_period_sec, 1800
351+
)
352+
self.assertEqual(policy_default_power_on_off.is_default, False)
353+
354+
310355
class AccountGroupTest(ClientBaseCase):
311356
"""
312357
Tests methods of the AccountGroup
@@ -353,12 +398,39 @@ def test_maintenance(self):
353398
"""
354399
with self.mock_get("/account/maintenance") as m:
355400
result = self.client.account.maintenance()
401+
356402
self.assertEqual(m.call_url, "/account/maintenance")
357403
self.assertEqual(len(result), 1)
404+
405+
maintenance = result[0]
406+
407+
self.assertEqual(
408+
maintenance.body,
409+
"Scheduled upgrade to faster NVMe hardware. This will affect Linode #1234.",
410+
)
411+
self.assertEqual(maintenance.entity.id, 1234)
412+
self.assertEqual(maintenance.entity.label, "Linode #1234")
413+
self.assertEqual(maintenance.entity.type, "linode")
414+
self.assertEqual(maintenance.entity.url, "/linodes/1234")
415+
self.assertEqual(
416+
maintenance.label, "Scheduled Maintenance for Linode #1234"
417+
)
358418
self.assertEqual(
359-
result[0].reason,
360-
"This maintenance will allow us to update the BIOS on the host's motherboard.",
419+
maintenance.message,
420+
"Scheduled upgrade to faster NVMe hardware.",
361421
)
422+
self.assertEqual(maintenance.severity, "major")
423+
self.assertEqual(maintenance.type, "maintenance_scheduled")
424+
self.assertEqual(maintenance.event_type, "linode_migrate")
425+
self.assertEqual(maintenance.maintenance_policy_set, "Power on/off")
426+
self.assertEqual(maintenance.description, "Scheduled Maintenance")
427+
self.assertEqual(maintenance.source, "platform")
428+
self.assertEqual(maintenance.not_before, "2025-03-25T10:00:00")
429+
self.assertEqual(maintenance.start_time, "2025-03-25T12:00:00")
430+
self.assertEqual(maintenance.complete_time, "2025-03-25T14:00:00")
431+
self.assertEqual(maintenance.status, "scheduled")
432+
self.assertEqual(maintenance.when, "2025-03-25T12:00:00")
433+
self.assertEqual(maintenance.until, "2025-03-25T14:00:00")
362434

363435
def test_notifications(self):
364436
"""

0 commit comments

Comments
 (0)