Skip to content
Draft
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
62 changes: 62 additions & 0 deletions linode_api4/groups/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
MonitorService,
MonitorServiceToken,
)
from linode_api4.objects.monitor import ChannelDetails

__all__ = [
"MonitorGroup",
Expand Down Expand Up @@ -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)
11 changes: 5 additions & 6 deletions linode_api4/objects/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
61 changes: 60 additions & 1 deletion test/integration/models/monitor/test_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from linode_api4 import LinodeClient, PaginatedList
from linode_api4.objects import (
AlertChannel,
AlertDefinition,
AlertDefinitionEntity,
ApiError,
Expand All @@ -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
Expand Down Expand Up @@ -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}")
54 changes: 54 additions & 0 deletions test/unit/groups/monitor_api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"
57 changes: 57 additions & 0 deletions test/unit/objects/monitor_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"])