qontract-reconcile 0.10.1rc813__py3-none-any.whl → 0.10.1rc814__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc813
3
+ Version: 0.10.1rc814
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -319,7 +319,7 @@ reconcile/gql_definitions/ldap_groups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
319
319
  reconcile/gql_definitions/ldap_groups/roles.py,sha256=goGDnkBBFy0mdLsXqL9qlSLPCFd9rwiD1rrsIH-6nZQ,2888
320
320
  reconcile/gql_definitions/ldap_groups/settings.py,sha256=KR6eKqXQWVYZAUEdatL1RCARaTOWl9X-QmxEMVjVNDE,2227
321
321
  reconcile/gql_definitions/maintenance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
322
- reconcile/gql_definitions/maintenance/maintenances.py,sha256=0fTy0SiRjtnSrUwm2qcPtU3RSFJ1ox1eQuC9v8l94dY,2666
322
+ reconcile/gql_definitions/maintenance/maintenances.py,sha256=ObAoOHUrTxL1l8CUhdOACc1s49Pt5j1fsErXE49fs1g,3042
323
323
  reconcile/gql_definitions/membershipsources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
324
324
  reconcile/gql_definitions/membershipsources/roles.py,sha256=d3nv3GLsj_eKgwB1glsiK6smpC4i16WO3dU5rIdRg94,3678
325
325
  reconcile/gql_definitions/ocm_labels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -354,7 +354,7 @@ reconcile/gql_definitions/slo_documents/slo_documents.py,sha256=pOrm9NXAonlo6Lxq
354
354
  reconcile/gql_definitions/status_board/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
355
355
  reconcile/gql_definitions/status_board/status_board.py,sha256=vHEzncabujkqbjJ-ibMYNJTODgTc4DMf4y6TW3I_7II,4700
356
356
  reconcile/gql_definitions/statuspage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
357
- reconcile/gql_definitions/statuspage/statuspages.py,sha256=RybRGzA8LiQ4yKct_IarI_i_ExQi8Q-jjW5y7J6AZKE,4564
357
+ reconcile/gql_definitions/statuspage/statuspages.py,sha256=fCTJCcXQkIuoSDLcW_kj85qioaEuk2mygiDcggakYUw,5171
358
358
  reconcile/gql_definitions/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
359
359
  reconcile/gql_definitions/templating/template_collection.py,sha256=lS0vzEKV2ZrzOqOEriqpy0yBgKjb2Ftrzgx6PIH46_4,3310
360
360
  reconcile/gql_definitions/templating/templates.py,sha256=ejAvQ13zfNMQTz3FWtRUic6dSvio3aAgBKEqt600hbk,2821
@@ -436,14 +436,14 @@ reconcile/skupper_network/models.py,sha256=DNTI7HZv-rqY42GIIxyRuvroHLvdH6rJerjIq
436
436
  reconcile/skupper_network/reconciler.py,sha256=XS-1oKBr_1l3dYUAVqUH6gCHg1G5ZuOfY_7fgGVAiFA,9996
437
437
  reconcile/skupper_network/site_controller.py,sha256=A3K-62BjJ5HiFVydV0ouGoD1NwrO7XhAH15BHAcS9fk,1550
438
438
  reconcile/statuspage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
439
- reconcile/statuspage/atlassian.py,sha256=m8tRECVD_9KnIvqWs6pUocAnmfj2w_eDTFE4il7hkH8,15335
439
+ reconcile/statuspage/atlassian.py,sha256=odYbYX8l3oST7AYm66Q4MiaxSbvDzsqEO6VBYJggxkc,17420
440
440
  reconcile/statuspage/integration.py,sha256=hsazrQMceJbr61nEkJLxJbHhudTGtFuH0mlCo66-2ug,711
441
- reconcile/statuspage/page.py,sha256=DB69C6yAfwZIdo4HBQi8BTUZXL8l9yOPnjnt3k_HZgk,3177
441
+ reconcile/statuspage/page.py,sha256=WHDwV2PXEo4WwI2EgPOkS6j_T7geZEDXTgSaqpDo75U,5101
442
442
  reconcile/statuspage/state.py,sha256=HD9EOoKm_nEqCMLIwW809En3cq5VhyzKJPUbsh-bae8,1617
443
443
  reconcile/statuspage/status.py,sha256=mfRJ_tW7jM4_Vy_1cc8C0fKJEoA2GwrA3gJeV1KImAw,2834
444
444
  reconcile/statuspage/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
445
445
  reconcile/statuspage/integrations/components.py,sha256=49KHd_E9AdRvcEA6n75q1McZv2LfN-hRsW-WA7dgw9g,2651
446
- reconcile/statuspage/integrations/maintenances.py,sha256=pwt3WHymfkqP7ox1GB-D6Wn0crmcrDE_p4SW9wRaqWE,2651
446
+ reconcile/statuspage/integrations/maintenances.py,sha256=7gmMS9HwORMWMMBtib6pNGLXR6sAH2aW-HXKHJn6BN4,3083
447
447
  reconcile/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
448
448
  reconcile/templates/aws_access_key_email.j2,sha256=2MUr1ERmyISzKgHqsWYLd-1Wbl-peUa-FsGUS-JLUFc,238
449
449
  reconcile/templates/email.yml.j2,sha256=OZgczNRgXPj2gVYTgwQyHAQrMGu7xp-e4W1rX19GcrU,690
@@ -779,7 +779,7 @@ tools/app_interface_metrics_exporter.py,sha256=zkwkxdAUAxjdc-pzx2_oJXG25fo0Fnyd5
779
779
  tools/app_interface_reporter.py,sha256=upA-J-n-HXHKVDINRuMR7vTt-iJvQORKUVi9D3leQto,17738
780
780
  tools/glitchtip_access_reporter.py,sha256=oPBnk_YoDuljU3v0FaChzOwwnk4vap1xEE67QEjzdqs,2948
781
781
  tools/glitchtip_access_revalidation.py,sha256=8kbBJk04mkq28kWoRDDkfCGIF3GRg3pJrFAh1sW0dbk,2821
782
- tools/qontract_cli.py,sha256=GduWl9WzfSmOmWjZfyoyR9U6oP8uf8ckF0iHf78tHvE,114858
782
+ tools/qontract_cli.py,sha256=PiT1dKAegpt_S-rALEQNEG7CKcgY5sPOY6haZEnxBzs,114860
783
783
  tools/sd_app_sre_alert_report.py,sha256=e9vAdyenUz2f5c8-z-5WY0wv-SJ9aePKDH2r4IwB6pc,5063
784
784
  tools/template_validation.py,sha256=-U-lTGeLaci8yWPEblCJeev2DOlY1jM9QOOh-O1zts8,3376
785
785
  tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -806,8 +806,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
806
806
  tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jrss,4941
807
807
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
808
808
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
809
- qontract_reconcile-0.10.1rc813.dist-info/METADATA,sha256=U8YS8DBrl_RNuDcEDjbn3RPBdccojeIOYIg3NII1GvM,2314
810
- qontract_reconcile-0.10.1rc813.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
811
- qontract_reconcile-0.10.1rc813.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
812
- qontract_reconcile-0.10.1rc813.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
813
- qontract_reconcile-0.10.1rc813.dist-info/RECORD,,
809
+ qontract_reconcile-0.10.1rc814.dist-info/METADATA,sha256=izBuRBYFxzthBAfeBBKkMgqw9MspsZkQJ5HzbfT5tRA,2314
810
+ qontract_reconcile-0.10.1rc814.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
811
+ qontract_reconcile-0.10.1rc814.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
812
+ qontract_reconcile-0.10.1rc814.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
813
+ qontract_reconcile-0.10.1rc814.dist-info/RECORD,,
@@ -34,6 +34,9 @@ query Maintenances {
34
34
  page {
35
35
  name
36
36
  }
37
+ remindSubscribers
38
+ notifySubscribersOnStart
39
+ notifySubscribersOnCompletion
37
40
  }
38
41
  }
39
42
  }
@@ -61,6 +64,9 @@ class StatusPageV1(ConfiguredBaseModel):
61
64
 
62
65
  class MaintenanceStatuspageAnnouncementV1(MaintenanceAnnouncementV1):
63
66
  page: StatusPageV1 = Field(..., alias="page")
67
+ remind_subscribers: Optional[bool] = Field(..., alias="remindSubscribers")
68
+ notify_subscribers_on_start: Optional[bool] = Field(..., alias="notifySubscribersOnStart")
69
+ notify_subscribers_on_completion: Optional[bool] = Field(..., alias="notifySubscribersOnCompletion")
64
70
 
65
71
 
66
72
  class MaintenanceV1(ConfiguredBaseModel):
@@ -61,12 +61,18 @@ query StatusPages {
61
61
  message
62
62
  scheduledStart
63
63
  scheduledEnd
64
+ affectedServices {
65
+ name
66
+ }
64
67
  announcements {
65
68
  provider
66
69
  ... on MaintenanceStatuspageAnnouncement_v1 {
67
70
  page {
68
71
  name
69
72
  }
73
+ remindSubscribers
74
+ notifySubscribersOnStart
75
+ notifySubscribersOnCompletion
70
76
  }
71
77
  }
72
78
  }
@@ -109,6 +115,10 @@ class StatusPageComponentV1(ConfiguredBaseModel):
109
115
  status_config: Optional[list[Union[ManualStatusProviderV1, StatusProviderV1]]] = Field(..., alias="status_config")
110
116
 
111
117
 
118
+ class MaintenanceV1_AppV1(ConfiguredBaseModel):
119
+ name: str = Field(..., alias="name")
120
+
121
+
112
122
  class MaintenanceAnnouncementV1(ConfiguredBaseModel):
113
123
  provider: str = Field(..., alias="provider")
114
124
 
@@ -119,6 +129,9 @@ class MaintenanceStatuspageAnnouncementV1_StatusPageV1(ConfiguredBaseModel):
119
129
 
120
130
  class MaintenanceStatuspageAnnouncementV1(MaintenanceAnnouncementV1):
121
131
  page: MaintenanceStatuspageAnnouncementV1_StatusPageV1 = Field(..., alias="page")
132
+ remind_subscribers: Optional[bool] = Field(..., alias="remindSubscribers")
133
+ notify_subscribers_on_start: Optional[bool] = Field(..., alias="notifySubscribersOnStart")
134
+ notify_subscribers_on_completion: Optional[bool] = Field(..., alias="notifySubscribersOnCompletion")
122
135
 
123
136
 
124
137
  class MaintenanceV1(ConfiguredBaseModel):
@@ -126,6 +139,7 @@ class MaintenanceV1(ConfiguredBaseModel):
126
139
  message: str = Field(..., alias="message")
127
140
  scheduled_start: str = Field(..., alias="scheduledStart")
128
141
  scheduled_end: str = Field(..., alias="scheduledEnd")
142
+ affected_services: list[MaintenanceV1_AppV1] = Field(..., alias="affectedServices")
129
143
  announcements: Optional[list[Union[MaintenanceStatuspageAnnouncementV1, MaintenanceAnnouncementV1]]] = Field(..., alias="announcements")
130
144
 
131
145
 
@@ -15,6 +15,7 @@ from reconcile.gql_definitions.statuspage.statuspages import StatusPageV1
15
15
  from reconcile.statuspage.page import (
16
16
  StatusComponent,
17
17
  StatusMaintenance,
18
+ StatusMaintenanceAnnouncement,
18
19
  StatusPage,
19
20
  )
20
21
  from reconcile.statuspage.state import ComponentBindingState
@@ -54,6 +55,10 @@ class AtlassianRawMaintenance(BaseModel):
54
55
  scheduled_for: str
55
56
  scheduled_until: str
56
57
  incident_updates: list[AtlassianRawMaintenanceUpdate]
58
+ components: list[AtlassianRawComponent]
59
+ auto_transition_deliver_notifications_at_end: bool
60
+ auto_transition_deliver_notifications_at_start: bool
61
+ scheduled_remind_prior: bool
57
62
 
58
63
 
59
64
  class AtlassianAPI:
@@ -121,6 +126,11 @@ class AtlassianAPI:
121
126
  all_scheduled_incidents = self._list_items(url)
122
127
  return [AtlassianRawMaintenance(**i) for i in all_scheduled_incidents]
123
128
 
129
+ def list_active_maintenances(self) -> list[AtlassianRawMaintenance]:
130
+ url = f"{self.api_url}/v1/pages/{self.page_id}/incidents/active_maintenance"
131
+ all_active_incidents = self._list_items(url)
132
+ return [AtlassianRawMaintenance(**i) for i in all_active_incidents]
133
+
124
134
  def create_incident(self, data: dict[str, Any]) -> str:
125
135
  url = f"{self.api_url}/v1/pages/{self.page_id}/incidents"
126
136
  response = requests.post(
@@ -190,6 +200,26 @@ class AtlassianStatusPageProvider:
190
200
  components=[c for c in components if c is not None],
191
201
  )
192
202
 
203
+ def _raw_component_to_status_component(
204
+ self, raw_component: AtlassianRawComponent, name_override: Optional[str] = None
205
+ ) -> StatusComponent:
206
+ group_name = (
207
+ self._group_id_to_name.get(raw_component.group_id)
208
+ if raw_component.group_id
209
+ else None
210
+ )
211
+ return StatusComponent(
212
+ name=name_override or raw_component.name,
213
+ display_name=raw_component.name,
214
+ description=raw_component.description,
215
+ group_name=group_name,
216
+ status_provider_configs=[
217
+ ManualStatusProvider(
218
+ component_status=raw_component.status,
219
+ )
220
+ ],
221
+ )
222
+
193
223
  def _bound_raw_component_to_status_component(
194
224
  self, raw_component: AtlassianRawComponent
195
225
  ) -> Optional[StatusComponent]:
@@ -197,21 +227,8 @@ class AtlassianStatusPageProvider:
197
227
  raw_component.id
198
228
  )
199
229
  if bound_component_name:
200
- group_name = (
201
- self._group_id_to_name.get(raw_component.group_id)
202
- if raw_component.group_id
203
- else None
204
- )
205
- return StatusComponent(
206
- name=bound_component_name,
207
- display_name=raw_component.name,
208
- description=raw_component.description,
209
- group_name=group_name,
210
- status_provider_configs=[
211
- ManualStatusProvider(
212
- component_status=raw_component.status,
213
- )
214
- ],
230
+ return self._raw_component_to_status_component(
231
+ raw_component, name_override=bound_component_name
215
232
  )
216
233
  return None
217
234
 
@@ -332,7 +349,13 @@ class AtlassianStatusPageProvider:
332
349
  # resolve status
333
350
  desired_component_status = desired.desired_component_status()
334
351
  if desired_component_status:
335
- component_update["status"] = desired_component_status
352
+ active_maintenance_affecting_component = [
353
+ m
354
+ for m in self.active_maintenances
355
+ if desired.display_name in [c.name for c in m.components]
356
+ ]
357
+ if not active_maintenance_affecting_component:
358
+ component_update["status"] = desired_component_status
336
359
 
337
360
  if current_component:
338
361
  logging.info(f"update component {desired.name}: {component_update}")
@@ -377,6 +400,27 @@ class AtlassianStatusPageProvider:
377
400
  if not dry_run:
378
401
  self._binding_state.bind_component(component_name, component_id)
379
402
 
403
+ def _raw_maintenance_to_status_maintenance(
404
+ self,
405
+ raw_maintenance: AtlassianRawMaintenance,
406
+ name_override: Optional[str] = None,
407
+ ) -> StatusMaintenance:
408
+ return StatusMaintenance(
409
+ name=name_override or raw_maintenance.name,
410
+ message=raw_maintenance.incident_updates[0].body,
411
+ schedule_start=raw_maintenance.scheduled_for,
412
+ schedule_end=raw_maintenance.scheduled_until,
413
+ components=[
414
+ self._raw_component_to_status_component(c)
415
+ for c in raw_maintenance.components
416
+ ],
417
+ announcements=StatusMaintenanceAnnouncement(
418
+ remind_subscribers=raw_maintenance.scheduled_remind_prior,
419
+ notify_subscribers_on_start=raw_maintenance.auto_transition_deliver_notifications_at_start,
420
+ notify_subscribers_on_completion=raw_maintenance.auto_transition_deliver_notifications_at_end,
421
+ ),
422
+ )
423
+
380
424
  @classmethod
381
425
  def init_from_page(
382
426
  cls,
@@ -398,17 +442,19 @@ class AtlassianStatusPageProvider:
398
442
  )
399
443
 
400
444
  @property
401
- def maintenances(self) -> list[StatusMaintenance]:
445
+ def scheduled_maintenances(self) -> list[StatusMaintenance]:
402
446
  return [
403
- StatusMaintenance(
404
- name=m.name,
405
- message=m.incident_updates[0].body,
406
- schedule_start=m.scheduled_for,
407
- schedule_end=m.scheduled_until,
408
- )
447
+ self._raw_maintenance_to_status_maintenance(m)
409
448
  for m in self._api.list_scheduled_maintenances()
410
449
  ]
411
450
 
451
+ @property
452
+ def active_maintenances(self) -> list[StatusMaintenance]:
453
+ return [
454
+ self._raw_maintenance_to_status_maintenance(m)
455
+ for m in self._api.list_active_maintenances()
456
+ ]
457
+
412
458
  def create_maintenance(self, maintenance: StatusMaintenance) -> None:
413
459
  data = {
414
460
  "name": maintenance.name,
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import sys
3
+ from datetime import datetime, timezone
3
4
 
4
5
  from reconcile.statuspage.atlassian import AtlassianStatusPageProvider
5
6
  from reconcile.statuspage.integration import get_binding_state, get_status_pages
@@ -46,23 +47,32 @@ class StatusPageMaintenancesIntegration(QontractReconcileIntegration[NoParams]):
46
47
  def run(self, dry_run: bool = False) -> None:
47
48
  binding_state = get_binding_state(self.name, self.secret_reader)
48
49
  pages = get_status_pages()
50
+ now = datetime.now(timezone.utc)
49
51
 
50
52
  error = False
51
53
  for p in pages:
52
54
  try:
53
55
  desired_state = [
54
- StatusMaintenance.init_from_maintenance(m)
56
+ StatusMaintenance.init_from_maintenance(
57
+ m, page_components=p.components or []
58
+ )
55
59
  for m in p.maintenances or []
60
+ if datetime.fromisoformat(m.scheduled_start) > now
56
61
  ]
57
62
  page_provider = AtlassianStatusPageProvider.init_from_page(
58
63
  page=p,
59
64
  token=self.secret_reader.read_secret(p.credentials),
60
65
  component_binding_state=binding_state,
61
66
  )
67
+ current_state = [
68
+ m
69
+ for m in page_provider.scheduled_maintenances
70
+ if page_provider.has_component_binding_for(m.name)
71
+ ]
62
72
  self.reconcile(
63
73
  dry_run=dry_run,
64
74
  desired_state=desired_state,
65
- current_state=page_provider.maintenances,
75
+ current_state=current_state,
66
76
  provider=page_provider,
67
77
  )
68
78
  except Exception:
@@ -1,7 +1,10 @@
1
- from typing import Optional, Self
1
+ from typing import Optional, Self, cast
2
2
 
3
3
  from pydantic import BaseModel
4
4
 
5
+ from reconcile.gql_definitions.maintenance.maintenances import (
6
+ MaintenanceStatuspageAnnouncementV1,
7
+ )
5
8
  from reconcile.gql_definitions.statuspage.statuspages import (
6
9
  MaintenanceV1,
7
10
  StatusPageComponentV1,
@@ -12,6 +15,8 @@ from reconcile.statuspage.status import (
12
15
  build_status_provider_config,
13
16
  )
14
17
 
18
+ PROVIDER_NAME = "statuspage"
19
+
15
20
 
16
21
  class StatusComponent(BaseModel):
17
22
  """
@@ -47,12 +52,14 @@ class StatusComponent(BaseModel):
47
52
  arbitrary_types_allowed = True
48
53
 
49
54
  @classmethod
50
- def init_from_page_component(cls, component: StatusPageComponentV1) -> Self:
55
+ def init_from_page_component(
56
+ cls, component: StatusPageComponentV1, name_override: Optional[str] = None
57
+ ) -> Self:
51
58
  status_configs = [
52
59
  build_status_provider_config(cfg) for cfg in component.status_config or []
53
60
  ]
54
61
  return cls(
55
- name=component.name,
62
+ name=name_override or component.name,
56
63
  display_name=component.display_name,
57
64
  description=component.description,
58
65
  group_name=component.group_name,
@@ -94,6 +101,26 @@ class StatusPage(BaseModel):
94
101
  )
95
102
 
96
103
 
104
+ class StatusMaintenanceAnnouncement(BaseModel):
105
+ """
106
+ Represents the desired state of a status maintenance.
107
+ """
108
+
109
+ remind_subscribers: Optional[bool] = None
110
+ notify_subscribers_on_start: Optional[bool] = None
111
+ notify_subscribers_on_completion: Optional[bool] = None
112
+
113
+ @classmethod
114
+ def init_from_announcement(
115
+ cls, announcement: MaintenanceStatuspageAnnouncementV1
116
+ ) -> Self:
117
+ return cls(
118
+ remind_subscribers=announcement.remind_subscribers,
119
+ notify_subscribers_on_start=announcement.notify_subscribers_on_start,
120
+ notify_subscribers_on_completion=announcement.notify_subscribers_on_completion,
121
+ )
122
+
123
+
97
124
  class StatusMaintenance(BaseModel):
98
125
  """
99
126
  Represents the desired state of a status maintenance.
@@ -103,12 +130,37 @@ class StatusMaintenance(BaseModel):
103
130
  message: str
104
131
  schedule_start: str
105
132
  schedule_end: str
133
+ components: list[StatusComponent]
134
+ announcements: StatusMaintenanceAnnouncement
106
135
 
107
136
  @classmethod
108
- def init_from_maintenance(cls, maintenance: MaintenanceV1) -> Self:
137
+ def init_from_maintenance(
138
+ cls,
139
+ maintenance: MaintenanceV1,
140
+ page_components: list[StatusPageComponentV1],
141
+ ) -> Self:
142
+ affected_services = [a.name for a in maintenance.affected_services]
143
+ affected_components = [
144
+ StatusComponent.init_from_page_component(c, name_override=c.display_name)
145
+ for c in page_components
146
+ if c.app.name in affected_services
147
+ ]
148
+ statuspage_announcements = [
149
+ StatusMaintenanceAnnouncement.init_from_announcement(
150
+ cast(MaintenanceStatuspageAnnouncementV1, m)
151
+ )
152
+ for m in maintenance.announcements or []
153
+ if m.provider == PROVIDER_NAME
154
+ ]
155
+ if len(statuspage_announcements) != 1:
156
+ raise ValueError(
157
+ f"Maintenanace announcements must include exactly one item of provider {PROVIDER_NAME}"
158
+ )
109
159
  return cls(
110
160
  name=maintenance.name,
111
161
  message=maintenance.message.rstrip("\n"),
112
162
  schedule_start=maintenance.scheduled_start,
113
163
  schedule_end=maintenance.scheduled_end,
164
+ components=affected_components,
165
+ announcements=statuspage_announcements[0],
114
166
  )
tools/qontract_cli.py CHANGED
@@ -2597,7 +2597,7 @@ def maintenances(ctx):
2597
2597
  "services": ", ".join(a.name for a in m.affected_services),
2598
2598
  }
2599
2599
  for m in maintenances
2600
- if datetime.fromisoformat(m.scheduled_end) > now
2600
+ if datetime.fromisoformat(m.scheduled_start) > now
2601
2601
  ]
2602
2602
  columns = [
2603
2603
  "name",