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.
- osism-0.20250628.0/ChangeLog +7 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/Containerfile +1 -1
- {osism-0.20250627.0 → osism-0.20250628.0}/Dockerfile +1 -1
- {osism-0.20250627.0/osism.egg-info → osism-0.20250628.0}/PKG-INFO +2 -2
- osism-0.20250628.0/osism/api.py +326 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/apply.py +2 -2
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/validate.py +1 -1
- {osism-0.20250627.0 → osism-0.20250628.0/osism.egg-info}/PKG-INFO +2 -2
- {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/SOURCES.txt +2 -5
- osism-0.20250628.0/osism.egg-info/pbr.json +1 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/requires.txt +1 -1
- {osism-0.20250627.0 → osism-0.20250628.0}/requirements.txt +1 -1
- osism-0.20250627.0/ChangeLog +0 -7
- osism-0.20250627.0/osism/api.py +0 -200
- osism-0.20250627.0/osism/core/__init__.py +0 -1
- osism-0.20250627.0/osism/plugins/__init__.py +0 -1
- osism-0.20250627.0/osism/services/__init__.py +0 -1
- osism-0.20250627.0/osism.egg-info/pbr.json +0 -1
- {osism-0.20250627.0 → osism-0.20250628.0}/.flake8 +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/.github/renovate.json +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/.github/workflows/publish.yml +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/.hadolint.yaml +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/.zuul.yaml +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/AUTHORS +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/LICENSE +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/Pipfile +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/Pipfile.lock +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/README.md +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/change.sh +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/cleanup-ansible-collections.sh +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/clustershell/clush.conf +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/clustershell/groups.conf +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/data/SCS-Spec.MandatoryFlavors.verbose.yaml +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/netbox-manager/settings.toml +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/run-ansible-console.sh +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/config_db.json +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS4625-54T.ini +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS5835-54T.ini +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS5835-54X.ini +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS7326-56X.ini +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS7726-32X.ini +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/files/sonic/port_config/Accton-AS9716-32D.ini +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/__init__.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/__main__.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/__init__.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/baremetal.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/compose.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/compute.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/configuration.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/console.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/container.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/get.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/log.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/manage.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/netbox.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/noset.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/reconciler.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/server.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/service.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/set.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/status.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/sync.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/task.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/vault.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/volume.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/wait.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/commands/worker.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/data/__init__.py +0 -0
- {osism-0.20250627.0/osism/core → osism-0.20250628.0/osism/data}/enums.py +0 -0
- {osism-0.20250627.0/osism/core → osism-0.20250628.0/osism/data}/playbooks.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/main.py +0 -0
- {osism-0.20250627.0/osism/actions → osism-0.20250628.0/osism/services}/__init__.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/services/listener.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/settings.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/__init__.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/ansible.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/ceph.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/__init__.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/config.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/ironic.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/netbox.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/__init__.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/bgp.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/cache.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/config_generator.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/connections.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/constants.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/device.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/exporter.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/interface.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/sonic/sync.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor/utils.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/conductor.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/kolla.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/kubernetes.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/netbox.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/openstack.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/tasks/reconciler.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism/utils/__init__.py +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/dependency_links.txt +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/entry_points.txt +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/not-zip-safe +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/osism.egg-info/top_level.txt +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/playbooks/build.yml +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/playbooks/pre.yml +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/playbooks/test-setup.yml +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/requirements.ansible.txt +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/requirements.netbox-manager.txt +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/requirements.openstack-flavor-manager.txt +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/requirements.openstack-image-manager.txt +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/requirements.yml +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/setup.cfg +0 -0
- {osism-0.20250627.0 → osism-0.20250628.0}/setup.py +0 -0
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
13
|
-
from osism.
|
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
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: osism
|
3
|
-
Version: 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.
|
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/
|
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}
|
osism-0.20250627.0/ChangeLog
DELETED
osism-0.20250627.0/osism/api.py
DELETED
@@ -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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|