osism 0.20250627.0__tar.gz → 0.20250628.0__tar.gz

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.
Files changed (113) hide show
  1. osism-0.20250628.0/ChangeLog +7 -0
  2. {osism-0.20250627.0 → osism-0.20250628.0}/Containerfile +1 -1
  3. {osism-0.20250627.0 → osism-0.20250628.0}/Dockerfile +1 -1
  4. {osism-0.20250627.0/osism.egg-info → osism-0.20250628.0}/PKG-INFO +2 -2
  5. osism-0.20250628.0/osism/api.py +326 -0
  6. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/apply.py +2 -2
  7. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/validate.py +1 -1
  8. {osism-0.20250627.0 → osism-0.20250628.0/osism.egg-info}/PKG-INFO +2 -2
  9. {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/SOURCES.txt +2 -5
  10. osism-0.20250628.0/osism.egg-info/pbr.json +1 -0
  11. {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/requires.txt +1 -1
  12. {osism-0.20250627.0 → osism-0.20250628.0}/requirements.txt +1 -1
  13. osism-0.20250627.0/ChangeLog +0 -7
  14. osism-0.20250627.0/osism/api.py +0 -200
  15. osism-0.20250627.0/osism/core/__init__.py +0 -1
  16. osism-0.20250627.0/osism/plugins/__init__.py +0 -1
  17. osism-0.20250627.0/osism/services/__init__.py +0 -1
  18. osism-0.20250627.0/osism.egg-info/pbr.json +0 -1
  19. {osism-0.20250627.0 → osism-0.20250628.0}/.flake8 +0 -0
  20. {osism-0.20250627.0 → osism-0.20250628.0}/.github/renovate.json +0 -0
  21. {osism-0.20250627.0 → osism-0.20250628.0}/.github/workflows/publish.yml +0 -0
  22. {osism-0.20250627.0 → osism-0.20250628.0}/.hadolint.yaml +0 -0
  23. {osism-0.20250627.0 → osism-0.20250628.0}/.zuul.yaml +0 -0
  24. {osism-0.20250627.0 → osism-0.20250628.0}/AUTHORS +0 -0
  25. {osism-0.20250627.0 → osism-0.20250628.0}/LICENSE +0 -0
  26. {osism-0.20250627.0 → osism-0.20250628.0}/Pipfile +0 -0
  27. {osism-0.20250627.0 → osism-0.20250628.0}/Pipfile.lock +0 -0
  28. {osism-0.20250627.0 → osism-0.20250628.0}/README.md +0 -0
  29. {osism-0.20250627.0 → osism-0.20250628.0}/files/change.sh +0 -0
  30. {osism-0.20250627.0 → osism-0.20250628.0}/files/cleanup-ansible-collections.sh +0 -0
  31. {osism-0.20250627.0 → osism-0.20250628.0}/files/clustershell/clush.conf +0 -0
  32. {osism-0.20250627.0 → osism-0.20250628.0}/files/clustershell/groups.conf +0 -0
  33. {osism-0.20250627.0 → osism-0.20250628.0}/files/data/SCS-Spec.MandatoryFlavors.verbose.yaml +0 -0
  34. {osism-0.20250627.0 → osism-0.20250628.0}/files/netbox-manager/settings.toml +0 -0
  35. {osism-0.20250627.0 → osism-0.20250628.0}/files/run-ansible-console.sh +0 -0
  36. {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/config_db.json +0 -0
  37. {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS4625-54T.ini +0 -0
  38. {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS5835-54T.ini +0 -0
  39. {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS5835-54X.ini +0 -0
  40. {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS7326-56X.ini +0 -0
  41. {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS7726-32X.ini +0 -0
  42. {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS9716-32D.ini +0 -0
  43. {osism-0.20250627.0 → osism-0.20250628.0}/osism/__init__.py +0 -0
  44. {osism-0.20250627.0 → osism-0.20250628.0}/osism/__main__.py +0 -0
  45. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/__init__.py +0 -0
  46. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/baremetal.py +0 -0
  47. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/compose.py +0 -0
  48. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/compute.py +0 -0
  49. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/configuration.py +0 -0
  50. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/console.py +0 -0
  51. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/container.py +0 -0
  52. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/get.py +0 -0
  53. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/log.py +0 -0
  54. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/manage.py +0 -0
  55. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/netbox.py +0 -0
  56. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/noset.py +0 -0
  57. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/reconciler.py +0 -0
  58. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/server.py +0 -0
  59. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/service.py +0 -0
  60. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/set.py +0 -0
  61. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/status.py +0 -0
  62. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/sync.py +0 -0
  63. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/task.py +0 -0
  64. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/vault.py +0 -0
  65. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/volume.py +0 -0
  66. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/wait.py +0 -0
  67. {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/worker.py +0 -0
  68. {osism-0.20250627.0 → osism-0.20250628.0}/osism/data/__init__.py +0 -0
  69. {osism-0.20250627.0/osism/core → osism-0.20250628.0/osism/data}/enums.py +0 -0
  70. {osism-0.20250627.0/osism/core → osism-0.20250628.0/osism/data}/playbooks.py +0 -0
  71. {osism-0.20250627.0 → osism-0.20250628.0}/osism/main.py +0 -0
  72. {osism-0.20250627.0/osism/actions → osism-0.20250628.0/osism/services}/__init__.py +0 -0
  73. {osism-0.20250627.0 → osism-0.20250628.0}/osism/services/listener.py +0 -0
  74. {osism-0.20250627.0 → osism-0.20250628.0}/osism/settings.py +0 -0
  75. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/__init__.py +0 -0
  76. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/ansible.py +0 -0
  77. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/ceph.py +0 -0
  78. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/__init__.py +0 -0
  79. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/config.py +0 -0
  80. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/ironic.py +0 -0
  81. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/netbox.py +0 -0
  82. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/__init__.py +0 -0
  83. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/bgp.py +0 -0
  84. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/cache.py +0 -0
  85. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/config_generator.py +0 -0
  86. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/connections.py +0 -0
  87. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/constants.py +0 -0
  88. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/device.py +0 -0
  89. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/exporter.py +0 -0
  90. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/interface.py +0 -0
  91. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/sync.py +0 -0
  92. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/utils.py +0 -0
  93. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor.py +0 -0
  94. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/kolla.py +0 -0
  95. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/kubernetes.py +0 -0
  96. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/netbox.py +0 -0
  97. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/openstack.py +0 -0
  98. {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/reconciler.py +0 -0
  99. {osism-0.20250627.0 → osism-0.20250628.0}/osism/utils/__init__.py +0 -0
  100. {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/dependency_links.txt +0 -0
  101. {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/entry_points.txt +0 -0
  102. {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/not-zip-safe +0 -0
  103. {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/top_level.txt +0 -0
  104. {osism-0.20250627.0 → osism-0.20250628.0}/playbooks/build.yml +0 -0
  105. {osism-0.20250627.0 → osism-0.20250628.0}/playbooks/pre.yml +0 -0
  106. {osism-0.20250627.0 → osism-0.20250628.0}/playbooks/test-setup.yml +0 -0
  107. {osism-0.20250627.0 → osism-0.20250628.0}/requirements.ansible.txt +0 -0
  108. {osism-0.20250627.0 → osism-0.20250628.0}/requirements.netbox-manager.txt +0 -0
  109. {osism-0.20250627.0 → osism-0.20250628.0}/requirements.openstack-flavor-manager.txt +0 -0
  110. {osism-0.20250627.0 → osism-0.20250628.0}/requirements.openstack-image-manager.txt +0 -0
  111. {osism-0.20250627.0 → osism-0.20250628.0}/requirements.yml +0 -0
  112. {osism-0.20250627.0 → osism-0.20250628.0}/setup.cfg +0 -0
  113. {osism-0.20250627.0 → osism-0.20250628.0}/setup.py +0 -0
@@ -0,0 +1,7 @@
1
+ CHANGES
2
+ =======
3
+
4
+ v0.20250628.0
5
+ -------------
6
+
7
+ * Move playbooks.py and enums.py from core to data package (#1524)
@@ -7,7 +7,7 @@ FROM ${IMAGE}:${PYTHON_VERSION}-alpine${ALPINE_VERSION}
7
7
  ENV PYTHONWARNINGS="ignore::UserWarning"
8
8
 
9
9
  COPY . /src
10
- COPY --from=ghcr.io/astral-sh/uv:0.7.15 /uv /usr/local/bin/uv
10
+ COPY --from=ghcr.io/astral-sh/uv:0.7.16 /uv /usr/local/bin/uv
11
11
 
12
12
  COPY files/data /data
13
13
  COPY files/change.sh /change.sh
@@ -7,7 +7,7 @@ FROM ${IMAGE}:${PYTHON_VERSION}-alpine${ALPINE_VERSION}
7
7
  ENV PYTHONWARNINGS="ignore::UserWarning"
8
8
 
9
9
  COPY . /src
10
- COPY --from=ghcr.io/astral-sh/uv:0.7.15 /uv /usr/local/bin/uv
10
+ COPY --from=ghcr.io/astral-sh/uv:0.7.16 /uv /usr/local/bin/uv
11
11
 
12
12
  COPY files/data /data
13
13
  COPY files/change.sh /change.sh
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osism
3
- Version: 0.20250627.0
3
+ Version: 0.20250628.0
4
4
  Summary: OSISM manager interface
5
5
  Home-page: https://github.com/osism/python-osism
6
6
  Author: OSISM GmbH
@@ -52,7 +52,7 @@ Requires-Dist: sqlmodel==0.0.24
52
52
  Requires-Dist: sushy==5.6.0
53
53
  Requires-Dist: tabulate==0.9.0
54
54
  Requires-Dist: transitions==0.9.2
55
- Requires-Dist: uvicorn[standard]==0.34.3
55
+ Requires-Dist: uvicorn[standard]==0.35.0
56
56
  Requires-Dist: watchdog==6.0.0
57
57
  Provides-Extra: ansible
58
58
  Requires-Dist: ansible-runner==2.4.1; extra == "ansible"
@@ -0,0 +1,326 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ import datetime
4
+ from logging.config import dictConfig
5
+ import logging
6
+ from typing import Optional, Dict, Any
7
+ from uuid import UUID
8
+
9
+ from fastapi import FastAPI, Header, Request, Response, HTTPException, status
10
+ from pydantic import BaseModel, Field
11
+ from starlette.middleware.cors import CORSMiddleware
12
+
13
+ from osism.tasks import reconciler
14
+ from osism import utils
15
+ from osism.services.listener import BaremetalEvents
16
+
17
+
18
+ class NotificationBaremetal(BaseModel):
19
+ priority: str = Field(..., description="Notification priority level")
20
+ event_type: str = Field(..., description="Type of the event")
21
+ timestamp: str = Field(..., description="Event timestamp")
22
+ publisher_id: str = Field(..., description="ID of the event publisher")
23
+ message_id: UUID = Field(..., description="Unique message identifier")
24
+ payload: Dict[str, Any] = Field(..., description="Event payload data")
25
+
26
+
27
+ class WebhookNetboxResponse(BaseModel):
28
+ result: str = Field(..., description="Operation result status")
29
+
30
+
31
+ class WebhookNetboxData(BaseModel):
32
+ username: str = Field(..., description="Username triggering the webhook")
33
+ data: Dict[str, Any] = Field(..., description="Webhook data payload")
34
+ snapshots: Dict[str, Any] = Field(..., description="Data snapshots")
35
+ event: str = Field(..., description="Event type")
36
+ timestamp: datetime.datetime = Field(..., description="Event timestamp")
37
+ model: str = Field(..., description="Model type")
38
+ request_id: UUID = Field(..., description="Unique request identifier")
39
+
40
+
41
+ class LogConfig(BaseModel):
42
+ """Logging configuration for the OSISM API server."""
43
+
44
+ LOGGER_NAME: str = "osism"
45
+ LOG_FORMAT: str = "%(levelname)s | %(asctime)s | %(name)s | %(message)s"
46
+ LOG_LEVEL: str = "INFO"
47
+
48
+ # Logging config
49
+ version: int = 1
50
+ disable_existing_loggers: bool = False
51
+ formatters: Dict[str, Any] = {
52
+ "default": {
53
+ "format": LOG_FORMAT,
54
+ "datefmt": "%Y-%m-%d %H:%M:%S",
55
+ },
56
+ }
57
+ handlers: Dict[str, Any] = {
58
+ "default": {
59
+ "formatter": "default",
60
+ "class": "logging.StreamHandler",
61
+ "stream": "ext://sys.stderr",
62
+ },
63
+ }
64
+ loggers: Dict[str, Any] = {
65
+ "osism": {"handlers": ["default"], "level": LOG_LEVEL, "propagate": False},
66
+ "api": {"handlers": ["default"], "level": LOG_LEVEL, "propagate": False},
67
+ "uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
68
+ "uvicorn.error": {"handlers": ["default"], "level": "INFO", "propagate": False},
69
+ "uvicorn.access": {
70
+ "handlers": ["default"],
71
+ "level": "INFO",
72
+ "propagate": False,
73
+ },
74
+ }
75
+
76
+
77
+ app = FastAPI(
78
+ title="OSISM API",
79
+ description="API for OpenStack Infrastructure & Service Manager",
80
+ version="1.0.0",
81
+ docs_url="/docs",
82
+ redoc_url="/redoc",
83
+ )
84
+
85
+ # Configure CORS - in production, replace with specific origins
86
+ app.add_middleware(
87
+ CORSMiddleware,
88
+ allow_origins=["*"], # TODO: Replace with actual allowed origins in production
89
+ allow_credentials=True,
90
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
91
+ allow_headers=[
92
+ "Accept",
93
+ "Accept-Language",
94
+ "Content-Language",
95
+ "Content-Type",
96
+ "Authorization",
97
+ "X-Hook-Signature",
98
+ ],
99
+ )
100
+
101
+ dictConfig(LogConfig().model_dump())
102
+ logger = logging.getLogger("osism.api")
103
+
104
+ baremetal_events = BaremetalEvents()
105
+
106
+
107
+ class DeviceSearchResult(BaseModel):
108
+ result: str = Field(..., description="Operation result status")
109
+ device: Optional[str] = Field(None, description="Device name if found")
110
+
111
+
112
+ def find_device_by_identifier(identifier: str):
113
+ """Find a device in NetBox by various identifiers."""
114
+ if not utils.nb:
115
+ return None
116
+
117
+ device = None
118
+
119
+ # Search by device name
120
+ devices = utils.nb.dcim.devices.filter(name=identifier)
121
+ if devices:
122
+ device = list(devices)[0]
123
+
124
+ # Search by inventory_hostname custom field
125
+ if not device:
126
+ devices = utils.nb.dcim.devices.filter(cf_inventory_hostname=identifier)
127
+ if devices:
128
+ device = list(devices)[0]
129
+
130
+ # Search by serial number
131
+ if not device:
132
+ devices = utils.nb.dcim.devices.filter(serial=identifier)
133
+ if devices:
134
+ device = list(devices)[0]
135
+
136
+ return device
137
+
138
+
139
+ @app.get("/", tags=["health"])
140
+ async def root() -> Dict[str, str]:
141
+ """Health check endpoint."""
142
+ return {"result": "ok"}
143
+
144
+
145
+ @app.get("/v1", tags=["health"])
146
+ async def v1() -> Dict[str, str]:
147
+ """API version 1 health check endpoint."""
148
+ return {"result": "ok"}
149
+
150
+
151
+ class SinkResponse(BaseModel):
152
+ result: str = Field(..., description="Operation result status")
153
+
154
+
155
+ @app.post("/v1/meters/sink", response_model=SinkResponse, tags=["telemetry"])
156
+ async def write_sink_meters(request: Request) -> SinkResponse:
157
+ """Write telemetry meters to sink."""
158
+ try:
159
+ data = await request.json()
160
+ # TODO: Implement meter processing logic
161
+ logger.info(
162
+ f"Received meters data: {len(data) if isinstance(data, list) else 1} entries"
163
+ )
164
+ return SinkResponse(result="ok")
165
+ except Exception as e:
166
+ logger.error(f"Error processing meters: {str(e)}")
167
+ raise HTTPException(
168
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
169
+ detail="Failed to process meters data",
170
+ )
171
+
172
+
173
+ @app.post("/v1/events/sink", response_model=SinkResponse, tags=["telemetry"])
174
+ async def write_sink_events(request: Request) -> SinkResponse:
175
+ """Write telemetry events to sink."""
176
+ try:
177
+ data = await request.json()
178
+ # TODO: Implement event processing logic
179
+ logger.info(
180
+ f"Received events data: {len(data) if isinstance(data, list) else 1} entries"
181
+ )
182
+ return SinkResponse(result="ok")
183
+ except Exception as e:
184
+ logger.error(f"Error processing events: {str(e)}")
185
+ raise HTTPException(
186
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
187
+ detail="Failed to process events data",
188
+ )
189
+
190
+
191
+ @app.post("/v1/notifications/baremetal", status_code=204, tags=["notifications"])
192
+ async def notifications_baremetal(notification: NotificationBaremetal) -> None:
193
+ """Handle baremetal notifications."""
194
+ try:
195
+ handler = baremetal_events.get_handler(notification.event_type)
196
+ handler(notification.payload)
197
+ logger.info(
198
+ f"Successfully processed baremetal notification: {notification.event_type}"
199
+ )
200
+ except Exception as e:
201
+ logger.error(f"Error processing baremetal notification: {str(e)}")
202
+ if isinstance(e, HTTPException):
203
+ raise
204
+ raise HTTPException(
205
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
206
+ detail="Failed to process baremetal notification",
207
+ )
208
+
209
+
210
+ @app.post(
211
+ "/v1/switches/{identifier}/ztp/complete",
212
+ response_model=DeviceSearchResult,
213
+ tags=["switches"],
214
+ )
215
+ async def switches_ztp_complete(identifier: str) -> DeviceSearchResult:
216
+ """Mark a switch as ZTP complete by setting provision_state to active."""
217
+ if not utils.nb:
218
+ raise HTTPException(
219
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
220
+ detail="NetBox is not enabled",
221
+ )
222
+
223
+ try:
224
+ device = find_device_by_identifier(identifier)
225
+
226
+ if device:
227
+ logger.info(
228
+ f"Found device {device.name} for ZTP complete with identifier {identifier}"
229
+ )
230
+
231
+ # Set provision_state custom field to active
232
+ device.custom_fields["provision_state"] = "active"
233
+ device.save()
234
+
235
+ return DeviceSearchResult(result="ok", device=device.name)
236
+ else:
237
+ logger.warning(
238
+ f"No device found for ZTP complete with identifier {identifier}"
239
+ )
240
+ raise HTTPException(
241
+ status_code=status.HTTP_404_NOT_FOUND,
242
+ detail=f"Device not found with identifier: {identifier}",
243
+ )
244
+ except Exception as e:
245
+ logger.error(f"Error completing ZTP for device {identifier}: {str(e)}")
246
+ if isinstance(e, HTTPException):
247
+ raise
248
+ raise HTTPException(
249
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
250
+ detail="Failed to complete ZTP process",
251
+ )
252
+
253
+
254
+ def process_netbox_webhook(webhook_input: WebhookNetboxData) -> None:
255
+ """Process NetBox webhook data."""
256
+ data = webhook_input.data
257
+ url = data["url"]
258
+ name = data["name"]
259
+
260
+ if "devices" in url:
261
+ tags = [x["name"] for x in data["tags"]]
262
+ custom_fields = data["custom_fields"]
263
+ device_type = custom_fields.get("device_type") or "node"
264
+
265
+ elif "interfaces" in url:
266
+ device_type = "interface"
267
+ device_id = data["device"]["id"]
268
+ device = utils.nb.dcim.devices.get(id=device_id)
269
+ tags = [str(x) for x in device.tags]
270
+ custom_fields = device.custom_fields
271
+ else:
272
+ logger.warning(f"Unknown webhook URL type: {url}")
273
+ return
274
+
275
+ if "Managed by OSISM" in tags:
276
+ if device_type == "server":
277
+ logger.info(
278
+ f"Handling change for managed device {name} of type {device_type}"
279
+ )
280
+ reconciler.run.delay()
281
+ elif device_type == "switch":
282
+ logger.info(
283
+ f"Handling change for managed device {name} of type {device_type}"
284
+ )
285
+ # TODO: Implement switch configuration generation
286
+ # netbox.generate.delay(name, custom_fields['configuration_template'])
287
+ elif device_type == "interface":
288
+ logger.info(
289
+ f"Handling change for interface {name} on managed device {device.name} of type {custom_fields['device_type']}"
290
+ )
291
+ # TODO: Implement interface configuration generation
292
+ # netbox.generate.delay(device.name, custom_fields['configuration_template'])
293
+ else:
294
+ logger.info(f"Ignoring change for unmanaged device {name}")
295
+
296
+
297
+ @app.post(
298
+ "/v1/webhook/netbox",
299
+ response_model=WebhookNetboxResponse,
300
+ status_code=200,
301
+ tags=["webhooks"],
302
+ )
303
+ async def webhook(
304
+ webhook_input: WebhookNetboxData,
305
+ request: Request,
306
+ response: Response,
307
+ content_length: int = Header(...),
308
+ x_hook_signature: Optional[str] = Header(None),
309
+ ) -> WebhookNetboxResponse:
310
+ """Handle NetBox webhook notifications."""
311
+ if not utils.nb:
312
+ raise HTTPException(
313
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
314
+ detail="NetBox webhook processing is not enabled",
315
+ )
316
+
317
+ try:
318
+ # TODO: Validate webhook signature if x_hook_signature is provided
319
+ process_netbox_webhook(webhook_input)
320
+ return WebhookNetboxResponse(result="ok")
321
+ except Exception as e:
322
+ logger.error(f"Error processing NetBox webhook: {str(e)}")
323
+ raise HTTPException(
324
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
325
+ detail="Failed to process NetBox webhook",
326
+ )
@@ -9,8 +9,8 @@ from cliff.command import Command
9
9
  from loguru import logger
10
10
  from tabulate import tabulate
11
11
 
12
- from osism.core import enums
13
- from osism.core.playbooks import MAP_ROLE2ENVIRONMENT, MAP_ROLE2RUNTIME
12
+ from osism.data import enums
13
+ from osism.data.playbooks import MAP_ROLE2ENVIRONMENT, MAP_ROLE2RUNTIME
14
14
  from osism.tasks import ansible, ceph, kolla, kubernetes, handle_task
15
15
 
16
16
 
@@ -5,7 +5,7 @@ import argparse
5
5
  from cliff.command import Command
6
6
  from loguru import logger
7
7
 
8
- from osism.core.enums import VALIDATE_PLAYBOOKS
8
+ from osism.data.enums import VALIDATE_PLAYBOOKS
9
9
  from osism.tasks import ansible, ceph, kolla
10
10
  from osism import utils
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osism
3
- Version: 0.20250627.0
3
+ Version: 0.20250628.0
4
4
  Summary: OSISM manager interface
5
5
  Home-page: https://github.com/osism/python-osism
6
6
  Author: OSISM GmbH
@@ -52,7 +52,7 @@ Requires-Dist: sqlmodel==0.0.24
52
52
  Requires-Dist: sushy==5.6.0
53
53
  Requires-Dist: tabulate==0.9.0
54
54
  Requires-Dist: transitions==0.9.2
55
- Requires-Dist: uvicorn[standard]==0.34.3
55
+ Requires-Dist: uvicorn[standard]==0.35.0
56
56
  Requires-Dist: watchdog==6.0.0
57
57
  Provides-Extra: ansible
58
58
  Requires-Dist: ansible-runner==2.4.1; extra == "ansible"
@@ -46,7 +46,6 @@ osism.egg-info/not-zip-safe
46
46
  osism.egg-info/pbr.json
47
47
  osism.egg-info/requires.txt
48
48
  osism.egg-info/top_level.txt
49
- osism/actions/__init__.py
50
49
  osism/commands/__init__.py
51
50
  osism/commands/apply.py
52
51
  osism/commands/baremetal.py
@@ -72,11 +71,9 @@ osism/commands/vault.py
72
71
  osism/commands/volume.py
73
72
  osism/commands/wait.py
74
73
  osism/commands/worker.py
75
- osism/core/__init__.py
76
- osism/core/enums.py
77
- osism/core/playbooks.py
78
74
  osism/data/__init__.py
79
- osism/plugins/__init__.py
75
+ osism/data/enums.py
76
+ osism/data/playbooks.py
80
77
  osism/services/__init__.py
81
78
  osism/services/listener.py
82
79
  osism/tasks/__init__.py
@@ -0,0 +1 @@
1
+ {"git_version": "f3fe7d7", "is_release": false}
@@ -29,7 +29,7 @@ sqlmodel==0.0.24
29
29
  sushy==5.6.0
30
30
  tabulate==0.9.0
31
31
  transitions==0.9.2
32
- uvicorn[standard]==0.34.3
32
+ uvicorn[standard]==0.35.0
33
33
  watchdog==6.0.0
34
34
 
35
35
  [ansible]
@@ -29,5 +29,5 @@ sqlmodel==0.0.24
29
29
  sushy==5.6.0
30
30
  tabulate==0.9.0
31
31
  transitions==0.9.2
32
- uvicorn[standard]==0.34.3
32
+ uvicorn[standard]==0.35.0
33
33
  watchdog==6.0.0
@@ -1,7 +0,0 @@
1
- CHANGES
2
- =======
3
-
4
- v0.20250627.0
5
- -------------
6
-
7
- * Remove loopback BGP neighbors from SONiC config generation (#1517)
@@ -1,200 +0,0 @@
1
- # SPDX-License-Identifier: Apache-2.0
2
-
3
- import datetime
4
- from logging.config import dictConfig
5
- import logging
6
- from uuid import UUID
7
-
8
- from fastapi import FastAPI, Header, Request, Response
9
- from pydantic import BaseModel
10
- from starlette.middleware.cors import CORSMiddleware
11
-
12
- from osism.tasks import reconciler
13
- from osism import utils
14
- from osism.services.listener import BaremetalEvents
15
-
16
-
17
- class NotificationBaremetal(BaseModel):
18
- priority: str
19
- event_type: str
20
- timestamp: str
21
- publisher_id: str
22
- message_id: UUID
23
- payload: dict
24
-
25
-
26
- class WebhookNetboxResponse(BaseModel):
27
- result: str
28
-
29
-
30
- class WebhookNetboxData(BaseModel):
31
- username: str
32
- data: dict
33
- snapshots: dict
34
- event: str
35
- timestamp: datetime.datetime
36
- model: str
37
- request_id: UUID
38
-
39
-
40
- # https://stackoverflow.com/questions/63510041/adding-python-logging-to-fastapi-endpoints-hosted-on-docker-doesnt-display-api
41
- class LogConfig(BaseModel):
42
- """Logging configuration to be set for the server"""
43
-
44
- LOGGER_NAME: str = "osism"
45
- LOG_FORMAT: str = "%(levelprefix)s | %(asctime)s | %(message)s"
46
- LOG_LEVEL: str = "DEBUG"
47
-
48
- # Logging config
49
- version: int = 1
50
- disable_existing_loggers: bool = False
51
- formatters: dict = {
52
- "default": {
53
- "()": "uvicorn.logging.DefaultFormatter",
54
- "fmt": LOG_FORMAT,
55
- "datefmt": "%Y-%m-%d %H:%M:%S",
56
- },
57
- }
58
- handlers: dict = {
59
- "default": {
60
- "formatter": "default",
61
- "class": "logging.StreamHandler",
62
- "stream": "ext://sys.stderr",
63
- },
64
- }
65
- loggers: dict = {
66
- "api": {"handlers": ["default"], "level": LOG_LEVEL},
67
- }
68
-
69
-
70
- app = FastAPI()
71
-
72
- app.add_middleware(CORSMiddleware)
73
-
74
- dictConfig(LogConfig().dict())
75
- logger = logging.getLogger("api")
76
-
77
- baremetal_events = BaremetalEvents()
78
-
79
-
80
- @app.get("/")
81
- async def root():
82
- return {"result": "ok"}
83
-
84
-
85
- @app.get("/v1")
86
- async def v1():
87
- return {"result": "ok"}
88
-
89
-
90
- @app.post("/v1/meters/sink")
91
- async def write_sink_meters(request: Request):
92
- data = await request.json()
93
-
94
-
95
- @app.post("/v1/events/sink")
96
- async def write_sink_events(request: Request):
97
- data = await request.json()
98
-
99
-
100
- @app.post("/v1/notifications/baremetal", status_code=204)
101
- async def notifications_baremetal(notification: NotificationBaremetal) -> None:
102
-
103
- handler = baremetal_events.get_handler(notification.event_type)
104
- handler(notification.payload)
105
-
106
-
107
- @app.post("/v1/switches/{identifier}/ztp/complete")
108
- async def switches_ztp_complete(identifier: str):
109
- if not utils.nb:
110
- return {"result": "netbox not enabled"}
111
-
112
- device = None
113
-
114
- # Search by device name
115
- devices = utils.nb.dcim.devices.filter(name=identifier)
116
- if devices:
117
- device = devices[0]
118
-
119
- # Search by inventory_hostname custom field
120
- if not device:
121
- devices = utils.nb.dcim.devices.filter(cf_inventory_hostname=identifier)
122
- if devices:
123
- device = devices[0]
124
-
125
- # Search by serial number
126
- if not device:
127
- devices = utils.nb.dcim.devices.filter(serial=identifier)
128
- if devices:
129
- device = devices[0]
130
-
131
- if device:
132
- logger.info(
133
- f"Found device {device.name} for ZTP complete with identifier {identifier}"
134
- )
135
-
136
- # Set provision_state custom field to active
137
- device.custom_fields["provision_state"] = "active"
138
- device.save()
139
-
140
- return {"result": "ok", "device": device.name}
141
- else:
142
- logger.warning(f"No device found for ZTP complete with identifier {identifier}")
143
- return {"result": "device not found"}
144
-
145
-
146
- @app.post("/v1/webhook/netbox", response_model=WebhookNetboxResponse, status_code=200)
147
- async def webhook(
148
- webhook_input: WebhookNetboxData,
149
- request: Request,
150
- response: Response,
151
- content_length: int = Header(...),
152
- x_hook_signature: str = Header(None),
153
- ):
154
- if utils.nb:
155
- data = webhook_input.data
156
- url = data["url"]
157
- name = data["name"]
158
-
159
- if "devices" in url:
160
- tags = [x["name"] for x in data["tags"]]
161
-
162
- custom_fields = data["custom_fields"]
163
- device_type = custom_fields["device_type"]
164
-
165
- # NOTE: device without a defined device_type are nodes
166
- if not device_type:
167
- device_type = "node"
168
-
169
- elif "interfaces" in url:
170
- device_type = "interface"
171
-
172
- device_id = data["device"]["id"]
173
- device = utils.nb.dcim.devices.get(id=device_id)
174
- tags = [str(x) for x in device.tags]
175
- custom_fields = device.custom_fields
176
-
177
- if "Managed by OSISM" in tags:
178
- if device_type == "server":
179
- logger.info(
180
- f"Handling change for managed device {name} of type {device_type}"
181
- )
182
- reconciler.run.delay()
183
- elif device_type == "switch":
184
- logger.info(
185
- f"Handling change for managed device {name} of type {device_type}"
186
- )
187
- # netbox.generate.delay(name, custom_fields['configuration_template'])
188
- elif device_type == "interface":
189
- logger.info(
190
- f"Handling change for interface {name} on managed device {device.name} of type {custom_fields['device_type']}"
191
- )
192
- # netbox.generate.delay(device.name, custom_fields['configuration_template'])
193
-
194
- else:
195
- logger.info(f"Ignoring change for unmanaged device {name}")
196
-
197
- return {"result": "ok"}
198
-
199
- else:
200
- return {"result": "webhook netbox not enabled"}
@@ -1 +0,0 @@
1
- # SPDX-License-Identifier: Apache-2.0
@@ -1 +0,0 @@
1
- # SPDX-License-Identifier: Apache-2.0
@@ -1 +0,0 @@
1
- # SPDX-License-Identifier: Apache-2.0
@@ -1 +0,0 @@
1
- {"git_version": "e1bd41d", "is_release": false}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes