|
18 | 18 | MonitorService, |
19 | 19 | MonitorServiceToken, |
20 | 20 | ) |
21 | | -from linode_api4.objects.filtering import and_ |
22 | 21 | from linode_api4.objects.monitor import ( |
23 | 22 | AkamaiObjectStorageLogsDestinationDetails, |
| 23 | + BasicAuthenticationDetails, |
24 | 24 | ChannelDetails, |
| 25 | + CustomHeader, |
25 | 26 | CustomHTTPSLogsDestinationDetails, |
| 27 | + DestinationAuthentication, |
| 28 | + EmailDetails, |
26 | 29 | LogsStreamDetails, |
| 30 | + WebhookDetails, |
27 | 31 | ) |
28 | 32 |
|
29 | 33 | __all__ = [ |
@@ -215,6 +219,193 @@ def alert_channels(self, *filters) -> PaginatedList: |
215 | 219 | """ |
216 | 220 | return self.client._get_and_filter(AlertChannel, *filters) |
217 | 221 |
|
| 222 | + def channel_create( |
| 223 | + self, |
| 224 | + label: str, |
| 225 | + channel_type: str, |
| 226 | + details: "ChannelDetails", |
| 227 | + ) -> AlertChannel: |
| 228 | + """ |
| 229 | + Create a new alert channel. |
| 230 | +
|
| 231 | + Alert channels define destinations for alert notifications. Supported |
| 232 | + channel types include email, webhook, PagerDuty, and Slack. |
| 233 | +
|
| 234 | + **Webhook Channel Constraints:** |
| 235 | + - If ``channel_type`` is "webhook", the following are required: |
| 236 | + - ``details.webhook.endpoint_url`` must be provided |
| 237 | + - ``details.webhook.authentication.type`` must be specified ("basic" or "none") |
| 238 | + - If ``authentication.type`` is "basic", both ``basic_authentication_user`` and |
| 239 | + ``basic_authentication_password`` must be provided in ``details.webhook.authentication.details`` |
| 240 | + - Client Certificate Configuration (Optional but must be complete): |
| 241 | + - If ``details.webhook.client_certificate_details`` is provided, all three certificates |
| 242 | + must be included: ``client_ca_certificate``, ``client_certificate``, and ``client_private_key`` |
| 243 | + - ``tls_hostname`` is optional |
| 244 | + - Custom Headers: |
| 245 | + - ``Content-Type`` header must NOT be set by the user; it will be managed by the API |
| 246 | +
|
| 247 | + API Documentation: https://techdocs.akamai.com/linode-api/reference/post-notification-channel |
| 248 | +
|
| 249 | + :param label: A human-readable name for the alert channel. |
| 250 | + :type label: str |
| 251 | + :param channel_type: The channel type (e.g., ``"email"``, ``"webhook"``). |
| 252 | + :type channel_type: str |
| 253 | + :param details: Configuration details specific to the channel type. |
| 254 | + :type details: ChannelDetails |
| 255 | +
|
| 256 | + :returns: The newly created alert channel. |
| 257 | + :rtype: AlertChannel |
| 258 | +
|
| 259 | + :raises ValueError: If webhook channel configuration is invalid or missing required fields. |
| 260 | + """ |
| 261 | + pass |
| 262 | + |
| 263 | + # Validate webhook channel requirements |
| 264 | + if channel_type == "webhook": |
| 265 | + self._validate_webhook_details(details) |
| 266 | + |
| 267 | + params = { |
| 268 | + "label": label, |
| 269 | + "channel_type": channel_type, |
| 270 | + "details": ( |
| 271 | + details._serialize() |
| 272 | + if hasattr(details, "_serialize") |
| 273 | + else details |
| 274 | + ), |
| 275 | + } |
| 276 | + |
| 277 | + result = self.client.post("/monitor/alert-channels", data=params) |
| 278 | + |
| 279 | + if "id" not in result: |
| 280 | + raise UnexpectedResponseError( |
| 281 | + "Unexpected response when creating alert channel!", |
| 282 | + json=result, |
| 283 | + ) |
| 284 | + |
| 285 | + return AlertChannel(self.client, result["id"], result) |
| 286 | + |
| 287 | + def verify_webhook( |
| 288 | + self, |
| 289 | + webhook: "WebhookDetails", |
| 290 | + ) -> bool: |
| 291 | + """ |
| 292 | + Verify a webhook configuration by testing connectivity to the endpoint. |
| 293 | +
|
| 294 | + This endpoint validates that the webhook endpoint is reachable and |
| 295 | + accepts the request format. It's recommended to verify webhook |
| 296 | + configurations before creating a webhook channel. |
| 297 | +
|
| 298 | + **Webhook Configuration Requirements:** |
| 299 | + - ``endpoint_url`` must be provided |
| 300 | + - ``authentication.type`` must be specified ("basic" or "none") |
| 301 | + - If ``authentication.type`` is "basic", both ``basic_authentication_user`` and |
| 302 | + ``basic_authentication_password`` must be provided |
| 303 | + - Client certificates (if used) must include all three: ``client_ca_certificate``, |
| 304 | + ``client_certificate``, and ``client_private_key`` |
| 305 | +
|
| 306 | + API Documentation: https://techdocs.akamai.com/linode-api/reference/post-verify-webhook |
| 307 | +
|
| 308 | + :param webhook: The webhook configuration to verify. |
| 309 | + :type webhook: WebhookDetails |
| 310 | +
|
| 311 | + :returns: True if verification succeeds. |
| 312 | + :rtype: bool |
| 313 | +
|
| 314 | + :raises ValueError: If webhook configuration is invalid. |
| 315 | + :raises ApiError: If the webhook verification fails. |
| 316 | + """ |
| 317 | + from linode_api4.objects.monitor import ChannelDetails |
| 318 | + |
| 319 | + # Validate webhook configuration |
| 320 | + self._validate_webhook_details(ChannelDetails(webhook=webhook)) |
| 321 | + |
| 322 | + data = { |
| 323 | + "webhook": ( |
| 324 | + webhook._serialize() |
| 325 | + if hasattr(webhook, "_serialize") |
| 326 | + else webhook |
| 327 | + ), |
| 328 | + } |
| 329 | + |
| 330 | + result = self.client.post("/monitor/alert-channels/verify", data=data) |
| 331 | + |
| 332 | + return result.get("success", True) |
| 333 | + |
| 334 | + def _validate_webhook_details(self, details: "ChannelDetails") -> None: |
| 335 | + """ |
| 336 | + Validate webhook channel details against API requirements. |
| 337 | +
|
| 338 | + :param details: The channel details to validate. |
| 339 | + :type details: ChannelDetails |
| 340 | +
|
| 341 | + :raises ValueError: If validation fails. |
| 342 | + """ |
| 343 | + if not details or not details.webhook: |
| 344 | + raise ValueError( |
| 345 | + "Webhook details are required for webhook channel type" |
| 346 | + ) |
| 347 | + |
| 348 | + webhook = details.webhook |
| 349 | + |
| 350 | + # Validate required endpoint_url |
| 351 | + if not webhook.endpoint_url: |
| 352 | + raise ValueError( |
| 353 | + "Webhook channel requires 'endpoint_url' to be specified" |
| 354 | + ) |
| 355 | + |
| 356 | + # Validate required authentication.type |
| 357 | + if not webhook.authentication or not webhook.authentication.type: |
| 358 | + raise ValueError( |
| 359 | + "Webhook channel requires 'authentication.type' to be specified " |
| 360 | + "(e.g., 'basic' or 'none')" |
| 361 | + ) |
| 362 | + |
| 363 | + auth_type = webhook.authentication.type |
| 364 | + if auth_type == "basic": |
| 365 | + # For basic auth, both username and password are required |
| 366 | + if not webhook.authentication.details: |
| 367 | + raise ValueError( |
| 368 | + "Basic authentication requires 'authentication.details' to be specified" |
| 369 | + ) |
| 370 | + |
| 371 | + auth_details = webhook.authentication.details |
| 372 | + if not auth_details.basic_authentication_user: |
| 373 | + raise ValueError( |
| 374 | + "Basic authentication requires 'basic_authentication_user' to be specified" |
| 375 | + ) |
| 376 | + |
| 377 | + if not auth_details.basic_authentication_password: |
| 378 | + raise ValueError( |
| 379 | + "Basic authentication requires 'basic_authentication_password' to be specified" |
| 380 | + ) |
| 381 | + |
| 382 | + # Validate client certificate configuration (all three must be present together) |
| 383 | + if webhook.client_certificate_details: |
| 384 | + cert_details = webhook.client_certificate_details |
| 385 | + |
| 386 | + # Check if any certificate field is present |
| 387 | + has_ca_cert = bool(cert_details.client_ca_certificate) |
| 388 | + has_client_cert = bool(cert_details.client_certificate) |
| 389 | + has_private_key = bool(cert_details.client_private_key) |
| 390 | + |
| 391 | + # If any certificate field is present, all must be present |
| 392 | + if has_ca_cert or has_client_cert or has_private_key: |
| 393 | + if not (has_ca_cert and has_client_cert and has_private_key): |
| 394 | + raise ValueError( |
| 395 | + "Client certificate configuration requires all three to be specified: " |
| 396 | + "'client_ca_certificate', 'client_certificate', and 'client_private_key'. " |
| 397 | + "'tls_hostname' is optional." |
| 398 | + ) |
| 399 | + |
| 400 | + # Validate custom headers don't include Content-Type |
| 401 | + if webhook.custom_headers: |
| 402 | + for header in webhook.custom_headers: |
| 403 | + if header.name and header.name.lower() == "content-type": |
| 404 | + raise ValueError( |
| 405 | + "Custom headers must NOT include 'Content-Type'; " |
| 406 | + "it will be managed by the API" |
| 407 | + ) |
| 408 | + |
218 | 409 | def create_alert_definition( |
219 | 410 | self, |
220 | 411 | service_type: str, |
@@ -425,110 +616,6 @@ def alert_definition_entities( |
425 | 616 | endpoint=endpoint, |
426 | 617 | ) |
427 | 618 |
|
428 | | - def channel_create( |
429 | | - self, |
430 | | - label: str, |
431 | | - channel_type: str, |
432 | | - details: ChannelDetails, |
433 | | - ) -> AlertChannel: |
434 | | - """ |
435 | | - Creates a new alert channel for the authenticated account. |
436 | | -
|
437 | | - An alert channel defines a notification destination (for example: an |
438 | | - email list) that can be associated with one or more alert definitions. |
439 | | - Currently only ``email`` is supported as a ``channel_type``. |
440 | | -
|
441 | | - API Documentation: https://techdocs.akamai.com/linode-api/reference/post-notification-channel |
442 | | -
|
443 | | - :param label: Human-readable name for the new alert channel. |
444 | | - :type label: str |
445 | | - :param channel_type: The type of notification channel (e.g. ``"email"``). |
446 | | - :type channel_type: str |
447 | | - :param details: Notification-type-specific configuration. |
448 | | - :type details: ChannelDetails |
449 | | -
|
450 | | - :returns: The newly created :class:`AlertChannel`. |
451 | | - :rtype: AlertChannel |
452 | | -
|
453 | | - .. note:: |
454 | | - If you need to obtain a single :class:`AlertChannel`, use :meth:`LinodeClient.load`. |
455 | | - Example: ``client.load(AlertChannel, channel_id)``. |
456 | | - For updating an alert channel, use the ``save()`` method on the :class:`AlertChannel` object. |
457 | | - For deleting an alert channel, use the ``delete()`` method directly on the :class:`AlertChannel` object. |
458 | | - """ |
459 | | - params = { |
460 | | - "label": label, |
461 | | - "channel_type": channel_type, |
462 | | - "details": details.dict, |
463 | | - } |
464 | | - |
465 | | - result = self.client.post("/monitor/alert-channels", data=params) |
466 | | - |
467 | | - if "id" not in result: |
468 | | - raise UnexpectedResponseError( |
469 | | - "Unexpected response when creating alert channel!", |
470 | | - json=result, |
471 | | - ) |
472 | | - |
473 | | - return AlertChannel(self.client, result["id"], result) |
474 | | - |
475 | | - def alert_channel_alerts(self, channel_id: int, *filters) -> PaginatedList: |
476 | | - """ |
477 | | - Retrieve all alerts associated with a specific alert channel. |
478 | | -
|
479 | | - Returns a paginated collection of alert definitions associated with the |
480 | | - specified alert channel. This allows you to see which alert definitions |
481 | | - are configured to notify this specific channel. |
482 | | -
|
483 | | - API Documentation: https://techdocs.akamai.com/linode-api/reference/get-notification-channel-alerts |
484 | | -
|
485 | | - :param channel_id: The ID of the alert channel to retrieve alerts for. |
486 | | - :type channel_id: int |
487 | | - :param filters: Optional filter expressions to apply to the collection. |
488 | | - See :doc:`Filtering Collections</linode_api4/objects/filtering>` for details. |
489 | | -
|
490 | | - :returns: A paginated list of alert definitions associated with this channel. |
491 | | - :rtype: PaginatedList[AlertDefinition] |
492 | | - """ |
493 | | - endpoint = f"/monitor/alert-channels/{channel_id}/alerts" |
494 | | - |
495 | | - # Build filter dict if filters provided |
496 | | - parsed_filters = None |
497 | | - if filters: |
498 | | - parsed_filters = ( |
499 | | - and_(*filters).dct if len(filters) > 1 else filters[0].dct |
500 | | - ) |
501 | | - |
502 | | - response_json = self.client.get(endpoint, filters=parsed_filters) |
503 | | - |
504 | | - if "data" not in response_json: |
505 | | - raise UnexpectedResponseError( |
506 | | - "Unexpected response when retrieving alert channel alerts!", |
507 | | - json=response_json, |
508 | | - ) |
509 | | - |
510 | | - # Create AlertDefinition objects with proper parent_id (service_type) |
511 | | - result = [ |
512 | | - AlertDefinition.make_instance( |
513 | | - obj["id"], |
514 | | - self.client, |
515 | | - parent_id=obj["service_type"], |
516 | | - json=obj, |
517 | | - ) |
518 | | - for obj in response_json.get("data", []) |
519 | | - if "id" in obj and "service_type" in obj |
520 | | - ] |
521 | | - |
522 | | - return PaginatedList( |
523 | | - self.client, |
524 | | - endpoint[1:], |
525 | | - page=result, |
526 | | - max_pages=response_json.get("pages", 1), |
527 | | - total_items=response_json.get("results", len(result)), |
528 | | - parent_id=None, |
529 | | - filters=parsed_filters, |
530 | | - ) |
531 | | - |
532 | 619 | def destinations(self, *filters) -> PaginatedList: |
533 | 620 | """ |
534 | 621 | List available logs destinations. |
|
0 commit comments