From f4ab4af61327376d062be90f8ad799e74d7e7bb5 Mon Sep 17 00:00:00 2001 From: mawasthy Date: Thu, 18 Jun 2026 15:48:21 +0530 Subject: [PATCH 1/2] adding create channel api changes --- linode_api4/groups/monitor.py | 62 +++++++++++++++++++ linode_api4/objects/monitor.py | 11 ++-- .../models/monitor/test_monitor.py | 61 +++++++++++++++++- test/unit/groups/monitor_api_test.py | 54 ++++++++++++++++ test/unit/objects/monitor_test.py | 57 +++++++++++++++++ 5 files changed, 238 insertions(+), 7 deletions(-) diff --git a/linode_api4/groups/monitor.py b/linode_api4/groups/monitor.py index 0d7f19ce8..45b12cbe9 100644 --- a/linode_api4/groups/monitor.py +++ b/linode_api4/groups/monitor.py @@ -13,6 +13,7 @@ MonitorService, MonitorServiceToken, ) +from linode_api4.objects.monitor import ChannelDetails __all__ = [ "MonitorGroup", @@ -332,3 +333,64 @@ def alert_definition_entities( *filters, endpoint=endpoint, ) + + def channel_create( + self, + label: str, + channel_type: str, + details: ChannelDetails, + ) -> AlertChannel: + """ + Creates a new alert channel for the authenticated account. + + An alert channel defines a notification destination (for example: an + email list) that can be associated with one or more alert definitions. + Currently only ``email`` is supported as a ``channel_type``. + + Example usage:: + + from linode_api4.objects.monitor import ChannelDetails, EmailDetails + + client = LinodeClient(TOKEN) + + new_channel = client.monitor.channel_create( + label="Email channel for api change", + channel_type="email", + details=ChannelDetails( + email=EmailDetails( + recipient_type="user", + usernames=["username-test"], + ) + ), + ) + + API Documentation: https://techdocs.akamai.com/linode-api/reference/post-alert-channel + + :param label: Human-readable name for the new alert channel. + :type label: str + :param channel_type: The type of notification channel (e.g. ``"email"``). + :type channel_type: str + :param details: Notification-type-specific configuration. Use + :class:`~linode_api4.objects.monitor.ChannelDetails` with + a nested :class:`~linode_api4.objects.monitor.EmailDetails` + for email channels. + :type details: ChannelDetails + + :returns: The newly created :class:`AlertChannel`. + :rtype: AlertChannel + """ + params = { + "label": label, + "channel_type": channel_type, + "details": details.dict, + } + + result = self.client.post("/monitor/alert-channels", data=params) + + if "id" not in result: + raise UnexpectedResponseError( + "Unexpected response when creating alert channel!", + json=result, + ) + + return AlertChannel(self.client, result["id"], result) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index 7e0f4ae4d..6efd23354 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -492,13 +492,12 @@ class AlertChannel(Base): fire. Alert channels define a destination and configuration for notifications (for example: email lists, webhooks, PagerDuty, Slack, etc.). - API Documentation: https://techdocs.akamai.com/linode-api/reference/get-notification-channels + API Documentation: + List/Get: https://techdocs.akamai.com/linode-api/reference/get-alert-channels + Create: https://techdocs.akamai.com/linode-api/reference/post-alert-channel - This class maps to the Monitor API's `/monitor/alert-channels` resource - and is used by the SDK to list, load, and inspect channels. - - NOTE: Only read operations are supported for AlertChannel at this time. - Create, update, and delete (CRUD) operations are not allowed. + This class maps to the Monitor API's ``/monitor/alert-channels`` resource + and is used by the SDK to list, load, create, and inspect channels. """ api_endpoint = "/monitor/alert-channels/{id}" diff --git a/test/integration/models/monitor/test_monitor.py b/test/integration/models/monitor/test_monitor.py index ceb9fdc3a..765bc8b21 100644 --- a/test/integration/models/monitor/test_monitor.py +++ b/test/integration/models/monitor/test_monitor.py @@ -9,6 +9,7 @@ from linode_api4 import LinodeClient, PaginatedList from linode_api4.objects import ( + AlertChannel, AlertDefinition, AlertDefinitionEntity, ApiError, @@ -17,7 +18,7 @@ MonitorService, MonitorServiceToken, ) -from linode_api4.objects.monitor import AlertStatus +from linode_api4.objects.monitor import AlertStatus, ChannelDetails, EmailDetails # List all dashboards @@ -311,3 +312,61 @@ def test_alert_definition_entities(test_linode_client): assert entity.label assert entity.url assert entity._type == service_type + + +def test_integration_create_get_delete_alert_channel(test_linode_client): + """E2E: create an alert channel, fetch it, then delete it. + + This test creates an alert channel with email details, retrieves it, + and then deletes it. It ensures the feature is working end-to-end + against the actual API. + """ + client = test_linode_client + label = get_test_label() + "-e2e-channel" + label = f"{label}-{int(time.time())}" + + created_channel = None + + try: + # Create an alert channel with email details + created_channel = client.monitor.channel_create( + label=label, + channel_type="email", + details=ChannelDetails( + email=EmailDetails( + recipient_type="user", + usernames=["mawasthy_tenant02_admin"], + ) + ), + ) + + # Assert the created channel has expected properties + assert isinstance(created_channel, AlertChannel) + assert created_channel.id is not None + assert created_channel.label == label + assert created_channel.channel_type == "email" + assert created_channel.details is not None + + # Fetch the channel to verify it exists + channels = list(client.monitor.alert_channels()) + assert len(channels) > 0, "No channels found after creation" + + # Find the created channel in the list + found_channel = None + for ch in channels: + if ch.id == created_channel.id: + found_channel = ch + break + + assert found_channel is not None, "Created channel not found in list" + assert found_channel.label == label + assert found_channel.channel_type == "email" + + finally: + if created_channel: + # Clean up: delete the created channel + try: + created_channel.delete() + except Exception as e: + # Log but don't fail if cleanup fails + print(f"Warning: Failed to delete channel {created_channel.id}: {e}") diff --git a/test/unit/groups/monitor_api_test.py b/test/unit/groups/monitor_api_test.py index fdc93060c..fd7e1c784 100644 --- a/test/unit/groups/monitor_api_test.py +++ b/test/unit/groups/monitor_api_test.py @@ -3,11 +3,13 @@ from linode_api4 import PaginatedList from linode_api4.objects import ( AggregateFunction, + AlertChannel, AlertDefinition, AlertDefinitionChannel, AlertDefinitionEntity, EntityMetricOptions, ) +from linode_api4.objects.monitor import ChannelDetails, EmailDetails class MonitorAPITest(MonitorClientBaseCase): @@ -180,3 +182,55 @@ def test_alert_definition_entities(self): assert entities[2].label == "mydatabase-3" assert entities[2].url == "/v4/databases/mysql/instances/3" assert entities[2]._type == "dbaas" + + def test_create_channel(self): + url = "/monitor/alert-channels" + result = { + "id": 123, + "label": "email channel for api change", + "type": "user", + "channel_type": "email", + "details": { + "email": { + "usernames": ["mawasthy_tenant02_admin"], + "recipient_type": "user", + } + }, + "alerts": { + "url": "/monitor/alert-channels/123/alerts", + "type": "alerts-definitions", + "alert_count": 0, + }, + "created": "2024-01-01T00:00:00", + "updated": "2024-01-01T00:00:00", + "created_by": "mawasthy_tenant02_admin", + "updated_by": "mawasthy_tenant02_admin", + } + + with self.mock_post(result) as mock_post: + channel = self.client.monitor.channel_create( + label="email channel for api change", + channel_type="email", + details=ChannelDetails( + email=EmailDetails( + recipient_type="user", + usernames=["mawasthy_tenant02_admin"], + ) + ), + ) + + assert mock_post.call_url == url + # payload should include the provided fields + assert mock_post.call_data["label"] == "email channel for api change" + assert mock_post.call_data["channel_type"] == "email" + assert "details" in mock_post.call_data + + assert isinstance(channel, AlertChannel) + assert channel.id == 123 + assert channel.label == "email channel for api change" + assert channel.channel_type == "email" + + # fetch the same response from the client and assert + resp = self.client.post(url, data={}) + assert resp["label"] == "email channel for api change" + assert resp["channel_type"] == "email" diff --git a/test/unit/objects/monitor_test.py b/test/unit/objects/monitor_test.py index 5913b3b28..2daa1486c 100644 --- a/test/unit/objects/monitor_test.py +++ b/test/unit/objects/monitor_test.py @@ -2,6 +2,7 @@ from test.unit.base import ClientBaseCase from linode_api4.objects import AlertChannel, MonitorDashboard, MonitorService +from linode_api4.objects.monitor import ChannelDetails, EmailDetails class MonitorTest(ClientBaseCase): @@ -169,3 +170,59 @@ def test_alert_channels(self): "/monitor/alert-channels/123/alerts", ) self.assertEqual(channels[0].alerts.alert_count, 0) + + def test_create_channel(self): + + create_response = { + "id": 456, + "label": "Email channel for api change", + "type": "user", + "channel_type": "email", + "details": { + "email": { + "recipient_type": "user", + "usernames": ["mawasthy_tenant02_admin"], + } + }, + "alerts": { + "url": "/monitor/alert-channels/456/alerts", + "type": "alerts-definitions", + "alert_count": 0, + }, + "created": "2024-01-01T00:00:00", + "updated": "2024-01-01T00:00:00", + "created_by": "mawasthy_tenant02_admin", + "updated_by": "mawasthy_tenant02_admin", + } + + with self.mock_post(create_response) as m: + result = self.client.monitor.channel_create( + label="Email channel for api change", + channel_type="email", + details=ChannelDetails( + email=EmailDetails( + recipient_type="user", + usernames=["mawasthy_tenant02_admin"], + ) + ), + ) + + self.assertEqual(m.call_url, "/monitor/alert-channels") + self.assertEqual(m.call_data["label"], "Email channel for api change") + self.assertEqual(m.call_data["channel_type"], "email") + self.assertEqual( + m.call_data["details"]["email"]["recipient_type"], "user" + ) + self.assertEqual( + m.call_data["details"]["email"]["usernames"], ["mawasthy_tenant02_admin"] + ) + + self.assertIsInstance(result, AlertChannel) + self.assertEqual(result.id, 456) + self.assertEqual(result.label, "Email channel for api change") + self.assertEqual(result.type, "user") + self.assertEqual(result.channel_type, "email") + self.assertIsNotNone(result.details) + self.assertIsNotNone(result.details.email) + self.assertEqual(result.details.email.recipient_type, "user") + self.assertEqual(result.details.email.usernames, ["mawasthy_tenant02_admin"]) From d33a7140adf3457579fbb075bee028dfd7517e35 Mon Sep 17 00:00:00 2001 From: mawasthy Date: Wed, 1 Jul 2026 17:08:40 +0530 Subject: [PATCH 2/2] adding delete and update channel changes --- linode_api4/groups/monitor.py | 4 + linode_api4/objects/monitor.py | 4 +- .../models/monitor/test_monitor.py | 30 ++++++-- test/unit/groups/monitor_api_test.py | 67 ++++++++++------ test/unit/objects/monitor_test.py | 76 +++++++++---------- 5 files changed, 110 insertions(+), 71 deletions(-) diff --git a/linode_api4/groups/monitor.py b/linode_api4/groups/monitor.py index 45b12cbe9..6dafc1a36 100644 --- a/linode_api4/groups/monitor.py +++ b/linode_api4/groups/monitor.py @@ -378,6 +378,10 @@ def channel_create( :returns: The newly created :class:`AlertChannel`. :rtype: AlertChannel + + .. note:: + For updating an alert channel, use the ``save()`` method on the :class:`AlertChannel` object. + For deleting an alert channel, use the ``delete()`` method directly on the :class:`AlertChannel` object. """ params = { "label": label, diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index 6efd23354..00fb12cc2 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -504,10 +504,10 @@ class AlertChannel(Base): properties = { "id": Property(identifier=True), - "label": Property(), + "label": Property(mutable=True), "type": Property(), "channel_type": Property(), - "details": Property(mutable=False, json_object=ChannelDetails), + "details": Property(mutable=True, json_object=ChannelDetails), "alerts": Property(mutable=False, json_object=AlertInfo), "created": Property(is_datetime=True), "updated": Property(is_datetime=True), diff --git a/test/integration/models/monitor/test_monitor.py b/test/integration/models/monitor/test_monitor.py index 765bc8b21..b247bacd1 100644 --- a/test/integration/models/monitor/test_monitor.py +++ b/test/integration/models/monitor/test_monitor.py @@ -18,7 +18,11 @@ MonitorService, MonitorServiceToken, ) -from linode_api4.objects.monitor import AlertStatus, ChannelDetails, EmailDetails +from linode_api4.objects.monitor import ( + AlertStatus, + ChannelDetails, + EmailDetails, +) # List all dashboards @@ -314,12 +318,12 @@ def test_alert_definition_entities(test_linode_client): assert entity._type == service_type -def test_integration_create_get_delete_alert_channel(test_linode_client): - """E2E: create an alert channel, fetch it, then delete it. +def test_integration_create_get_update_delete_alert_channel(test_linode_client): + """E2E: create an alert channel, fetch it, update it, then delete it. This test creates an alert channel with email details, retrieves it, - and then deletes it. It ensures the feature is working end-to-end - against the actual API. + updates it, and then deletes it. It ensures the full CRUD feature is + working end-to-end against the actual API. """ client = test_linode_client label = get_test_label() + "-e2e-channel" @@ -362,6 +366,18 @@ def test_integration_create_get_delete_alert_channel(test_linode_client): assert found_channel.label == label assert found_channel.channel_type == "email" + # Update the channel label + updated_label = f"{label}-updated" + created_channel.label = updated_label + result = created_channel.save() + assert result is True, "Failed to update channel" + + # Fetch the updated channel to verify the change + reloaded_channel = client.load(AlertChannel, created_channel.id) + assert ( + reloaded_channel.label == updated_label + ), "Channel label was not updated" + finally: if created_channel: # Clean up: delete the created channel @@ -369,4 +385,6 @@ def test_integration_create_get_delete_alert_channel(test_linode_client): created_channel.delete() except Exception as e: # Log but don't fail if cleanup fails - print(f"Warning: Failed to delete channel {created_channel.id}: {e}") + print( + f"Warning: Failed to delete channel {created_channel.id}: {e}" + ) diff --git a/test/unit/groups/monitor_api_test.py b/test/unit/groups/monitor_api_test.py index fd7e1c784..48b8bd57d 100644 --- a/test/unit/groups/monitor_api_test.py +++ b/test/unit/groups/monitor_api_test.py @@ -183,54 +183,71 @@ def test_alert_definition_entities(self): assert entities[2].url == "/v4/databases/mysql/instances/3" assert entities[2]._type == "dbaas" - def test_create_channel(self): - url = "/monitor/alert-channels" - result = { - "id": 123, - "label": "email channel for api change", + def test_create_update_delete_alert_channel(self): + """ + E2E test for alert channel CRUD: create, update, and delete. + Verifies the full lifecycle of an alert channel. + """ + create_url = "/monitor/alert-channels" + channel_id = 789 + channel_url = f"{create_url}/{channel_id}" + + # Create channel + create_response = { + "id": channel_id, + "label": "Test Channel", "type": "user", "channel_type": "email", "details": { "email": { - "usernames": ["mawasthy_tenant02_admin"], + "usernames": ["test_user"], "recipient_type": "user", } }, "alerts": { - "url": "/monitor/alert-channels/123/alerts", + "url": f"{channel_url}/alerts", "type": "alerts-definitions", "alert_count": 0, }, "created": "2024-01-01T00:00:00", "updated": "2024-01-01T00:00:00", - "created_by": "mawasthy_tenant02_admin", - "updated_by": "mawasthy_tenant02_admin", + "created_by": "test_user", + "updated_by": "test_user", } - with self.mock_post(result) as mock_post: + with self.mock_post(create_response) as mock_post: channel = self.client.monitor.channel_create( - label="email channel for api change", + label="Test Channel", channel_type="email", details=ChannelDetails( email=EmailDetails( recipient_type="user", - usernames=["mawasthy_tenant02_admin"], + usernames=["test_user"], ) ), ) - assert mock_post.call_url == url - # payload should include the provided fields - assert mock_post.call_data["label"] == "email channel for api change" - assert mock_post.call_data["channel_type"] == "email" - assert "details" in mock_post.call_data - + assert mock_post.call_url == create_url assert isinstance(channel, AlertChannel) - assert channel.id == 123 - assert channel.label == "email channel for api change" - assert channel.channel_type == "email" + assert channel.id == channel_id + assert channel.label == "Test Channel" - # fetch the same response from the client and assert - resp = self.client.post(url, data={}) - assert resp["label"] == "email channel for api change" - assert resp["channel_type"] == "email" + # Update channel + updated_response = create_response.copy() + updated_response["label"] = "Test Channel Updated" + updated_response["updated"] = "2024-01-02T00:00:00" + + with self.mock_put(updated_response) as mock_put: + channel.label = "Test Channel Updated" + result = channel.save() + + assert mock_put.call_url == channel_url + assert result is True + assert channel.label == "Test Channel Updated" + + # Delete channel + with self.mock_delete() as mock_delete: + result = channel.delete() + + assert mock_delete.call_url == channel_url + assert result is True diff --git a/test/unit/objects/monitor_test.py b/test/unit/objects/monitor_test.py index 2daa1486c..52f2b629d 100644 --- a/test/unit/objects/monitor_test.py +++ b/test/unit/objects/monitor_test.py @@ -2,7 +2,6 @@ from test.unit.base import ClientBaseCase from linode_api4.objects import AlertChannel, MonitorDashboard, MonitorService -from linode_api4.objects.monitor import ChannelDetails, EmailDetails class MonitorTest(ClientBaseCase): @@ -171,58 +170,59 @@ def test_alert_channels(self): ) self.assertEqual(channels[0].alerts.alert_count, 0) - def test_create_channel(self): - + def test_create_update_delete_channel(self): + """ + Test CRUD operations for AlertChannel: create, update, and delete. + Verifies the full lifecycle of an alert channel object. + """ + channel_id = 999 + url = f"/monitor/alert-channels/{channel_id}" + + # Create the channel create_response = { - "id": 456, - "label": "Email channel for api change", + "id": channel_id, + "label": "CRUD Test Channel", "type": "user", "channel_type": "email", "details": { "email": { + "usernames": ["crud_user"], "recipient_type": "user", - "usernames": ["mawasthy_tenant02_admin"], } }, "alerts": { - "url": "/monitor/alert-channels/456/alerts", + "url": f"{url}/alerts", "type": "alerts-definitions", "alert_count": 0, }, "created": "2024-01-01T00:00:00", "updated": "2024-01-01T00:00:00", - "created_by": "mawasthy_tenant02_admin", - "updated_by": "mawasthy_tenant02_admin", + "created_by": "crud_user", + "updated_by": "crud_user", } - with self.mock_post(create_response) as m: - result = self.client.monitor.channel_create( - label="Email channel for api change", - channel_type="email", - details=ChannelDetails( - email=EmailDetails( - recipient_type="user", - usernames=["mawasthy_tenant02_admin"], - ) - ), - ) + with self.mock_get(create_response) as m_get: + channel = self.client.load(AlertChannel, channel_id) + self.assertIsInstance(channel, AlertChannel) + self.assertEqual(channel.id, channel_id) + self.assertEqual(channel.label, "CRUD Test Channel") - self.assertEqual(m.call_url, "/monitor/alert-channels") - self.assertEqual(m.call_data["label"], "Email channel for api change") - self.assertEqual(m.call_data["channel_type"], "email") - self.assertEqual( - m.call_data["details"]["email"]["recipient_type"], "user" - ) - self.assertEqual( - m.call_data["details"]["email"]["usernames"], ["mawasthy_tenant02_admin"] - ) + # Update the channel + updated_response = create_response.copy() + updated_response["label"] = "CRUD Test Channel Updated" + updated_response["updated"] = "2024-01-02T00:00:00" + + with self.mock_put(updated_response) as m_put: + channel.label = "CRUD Test Channel Updated" + result = channel.save() + + self.assertEqual(m_put.call_url, url) + self.assertTrue(result) + self.assertEqual(channel.label, "CRUD Test Channel Updated") + + # Delete the channel + with self.mock_delete() as m_delete: + result = channel.delete() - self.assertIsInstance(result, AlertChannel) - self.assertEqual(result.id, 456) - self.assertEqual(result.label, "Email channel for api change") - self.assertEqual(result.type, "user") - self.assertEqual(result.channel_type, "email") - self.assertIsNotNone(result.details) - self.assertIsNotNone(result.details.email) - self.assertEqual(result.details.email.recipient_type, "user") - self.assertEqual(result.details.email.usernames, ["mawasthy_tenant02_admin"]) + self.assertEqual(m_delete.call_url, url) + self.assertTrue(result)