Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ jobs:
name: proxbox-api-coverage
path: coverage.xml

# Build and test Docker images. On PR, push with PR ref tag for E2E testing.
# Publish Docker images only from main branch pushes.
docker-images:
name: Publish Docker images (Hub)
needs: [test]
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/testing') || github.event_name == 'pull_request'
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: ./.github/workflows/docker-hub-publish.yml
secrets: inherit

Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/docker-hub-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ on:
type: string
required: false
default: ""
pull_request:
branches: [main, testing]
workflow_dispatch:
inputs:
git_ref:
Expand Down
32 changes: 20 additions & 12 deletions proxbox_api/services/sync/storages.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ async def _fetch_cluster_storages(proxmox):
return_exceptions=True,
)

# Some deployments expose the same cluster via multiple endpoints and/or repeated
# storage rows in the same API response. Reconcile each (cluster, storage) once per run
# to avoid duplicate create attempts against the unique DB constraint.
unique_payloads: dict[tuple[str, str], dict] = {}

for cluster_data in cluster_storage_sets:
if isinstance(cluster_data, Exception):
continue
Expand All @@ -63,7 +68,10 @@ async def _fetch_cluster_storages(proxmox):
"enabled": not bool(storage.get("disable")),
"tags": tag_refs,
}
record = await rest_reconcile_async(
unique_payloads.setdefault((cluster_name, storage_name), payload)

for (cluster_name, storage_name), payload in unique_payloads.items():
record = await rest_reconcile_async(
nb,
"/api/plugins/proxbox/storage/",
lookup={"cluster": cluster_name, "name": storage_name},
Expand All @@ -80,17 +88,17 @@ async def _fetch_cluster_storages(proxmox):
"enabled": item.get("enabled"),
"tags": item.get("tags"),
},
)
data = record.serialize()
synced.append(data)
if use_websocket and websocket:
await websocket.send_json(
{
"step": "storage",
"status": "synced",
"message": f"Synced storage {cluster_name}/{storage_name}",
"result": {"id": data.get("id"), "name": storage_name},
}
)
data = record.serialize()
synced.append(data)
if use_websocket and websocket:
await websocket.send_json(
{
"step": "storage",
"status": "synced",
"message": f"Synced storage {cluster_name}/{storage_name}",
"result": {"id": data.get("id"), "name": storage_name},
}
)

return synced
34 changes: 34 additions & 0 deletions tests/test_storage_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,40 @@ async def _fake_sync_storages(**kwargs):
assert '"count": 2' in payload


def test_create_storages_deduplicates_cluster_storage_pairs(monkeypatch):
class _Record:
def __init__(self, payload):
self.payload = payload

def serialize(self):
return {"id": 1, **self.payload}

storages = [
{"storage": "local-zfs", "type": "zfspool", "shared": 0, "disable": 0},
{"storage": "local-zfs", "type": "zfspool", "shared": 0, "disable": 0},
]
calls: list[tuple[dict, dict]] = []

def _fake_get_storage_list(_px):
return storages

async def _fake_reconcile(_nb, _path, lookup, payload, **kwargs):
calls.append((lookup, payload))
return _Record(payload)

monkeypatch.setattr("proxbox_api.services.sync.storages.get_storage_list", _fake_get_storage_list)
monkeypatch.setattr("proxbox_api.services.sync.storages.dump_models", lambda items: items)
monkeypatch.setattr("proxbox_api.services.sync.storages.rest_reconcile_async", _fake_reconcile)

tag = SimpleNamespace(id=1, name="Proxbox", slug="proxbox", color="ff5722")
pxs = [SimpleNamespace(name="TEST-CLUSTER"), SimpleNamespace(name="TEST-CLUSTER")]

asyncio.run(create_storages(netbox_session=object(), pxs=pxs, tag=tag))

assert len(calls) == 1
assert calls[0][0] == {"cluster": "TEST-CLUSTER", "name": "local-zfs"}


async def _collect_async_frames(stream) -> list[str]:
output: list[str] = []
async for frame in stream:
Expand Down