Skip to content

Commit 3354d35

Browse files
committed
nix_csi: improve code quality and observability
- Remove duplicate constants from identityservicer.py, import from constants.py - Fix incorrect variable names in error messages in NodeUnpublishVolume - Add error context logging on cache copy retry failures - Cache store names in prepare_volume() to avoid duplicate extraction calls - Parallelize gcroot installations using asyncio.gather - Add extract_store_paths_set() convenience function - Make CSI socket path configurable via CSI_SOCKET_PATH env var - Add structured logging fields to Publish/Unpublish operations - Enhance Probe method to verify /nix/store accessibility
1 parent f14a786 commit 3354d35

7 files changed

Lines changed: 83 additions & 34 deletions

File tree

python/nix_csi/cache.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,11 @@ async def copy_to_cache(package_path: Path) -> None:
9090
if nix_copy.returncode == 0:
9191
logger.debug(f"Successfully copied to cache: {package_path}")
9292
break
93+
else:
94+
logger.debug(
95+
f"Copy attempt {attempt + 1}/6 failed for {package_path}: {nix_copy.combined}"
96+
)
9397
else:
94-
logger.error(f"Failed to copy to cache after 6 attempts: {package_path}")
98+
logger.error(
99+
f"Failed to copy to cache after 6 attempts: {package_path}"
100+
)

python/nix_csi/cli.py

100755100644
File mode changed.

python/nix_csi/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@
3333

3434
# Simple string check is fine - value controlled by easykubenix (always "true" or "false")
3535
CACHE_ENABLED = os.environ.get("CACHE_ENABLED", "false") == "true"
36+
37+
# CSI socket path for gRPC server
38+
CSI_SOCKET_PATH = os.environ.get("CSI_SOCKET_PATH", "/csi/csi.sock")

python/nix_csi/identityservicer.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
from pathlib import Path
2+
13
from csi import csi_grpc, csi_pb2
24
from google.protobuf.wrappers_pb2 import BoolValue
35
from grpclib import GRPCError
46
from grpclib.const import Status
5-
from importlib import metadata
67

7-
CSI_PLUGIN_NAME = "nix.csi.store"
8-
CSI_VENDOR_VERSION = metadata.version("nix-csi")
8+
from .constants import CSI_PLUGIN_NAME, CSI_VENDOR_VERSION
99

1010

1111
class IdentityServicer(csi_grpc.IdentityBase):
@@ -44,5 +44,10 @@ async def Probe(self, stream):
4444
request: csi_pb2.ProbeRequest | None = await stream.recv_message()
4545
if request is None:
4646
raise GRPCError(Status.INVALID_ARGUMENT, "Received None request in Probe")
47-
reply = csi_pb2.ProbeResponse(ready=BoolValue(value=True))
47+
48+
# Verify /nix/store is accessible - critical for CSI driver operation
49+
nix_store = Path("/nix/store")
50+
ready = nix_store.is_dir()
51+
52+
reply = csi_pb2.ProbeResponse(ready=BoolValue(value=ready))
4853
await stream.send_message(reply)

python/nix_csi/service.py

100755100644
Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@
1616

1717
from .builders import build_builder_args, get_builder_uris
1818
from .cache import check_cache_connectivity, copy_to_cache, get_substituter_args
19-
from .constants import CSI_GCROOTS, CSI_VOLUMES, NIX_BUILD_TIMEOUT
19+
from .constants import CSI_GCROOTS, CSI_SOCKET_PATH, CSI_VOLUMES, NIX_BUILD_TIMEOUT
2020
from .identityservicer import IdentityServicer
2121
from .nix import build_flake_ref, build_nix_expr, build_store_path, get_current_system
22-
from .store import extract_store_paths
23-
from .volume import cleanup_failed_volume, is_mount, mount_volume, prepare_volume, unmount
22+
from .store import extract_store_paths_set
23+
from .volume import (
24+
cleanup_failed_volume,
25+
is_mount,
26+
mount_volume,
27+
prepare_volume,
28+
unmount,
29+
)
2430

2531
logger = logging.getLogger("nix-csi")
2632

@@ -81,7 +87,7 @@ async def _build_pod_packages(
8187
)
8288

8389
package_paths = []
84-
pod_store_paths = set(extract_store_paths(pod.raw))
90+
pod_store_paths = extract_store_paths_set(pod.raw)
8591

8692
for package_path in pod_store_paths:
8793
logger.debug(f"{package_path=}")
@@ -155,7 +161,13 @@ async def NodePublishVolume(self, stream):
155161
if request is None:
156162
raise GRPCError(Status.INVALID_ARGUMENT, "NodePublishVolumeRequest is None")
157163

158-
logger.info(f"Publish {request.target_path}")
164+
logger.info(
165+
"Publishing volume",
166+
extra={
167+
"volume_id": request.volume_id,
168+
"target_path": request.target_path,
169+
},
170+
)
159171

160172
if not request.volume_context.get("csi.storage.k8s.io/ephemeral"):
161173
raise GRPCError(
@@ -230,7 +242,13 @@ async def NodeUnpublishVolume(self, stream):
230242
if request is None:
231243
raise ValueError("NodeUnpublishVolumeRequest is None")
232244

233-
logger.info(f"Unpublish {request.target_path}")
245+
logger.info(
246+
"Unpublishing volume",
247+
extra={
248+
"volume_id": request.volume_id,
249+
"target_path": request.target_path,
250+
},
251+
)
234252

235253
async with self.volumeLocks[request.volume_id]:
236254
target_path = Path(request.target_path)
@@ -262,9 +280,7 @@ async def NodeUnpublishVolume(self, stream):
262280
shutil.rmtree(gc_root, ignore_errors=True)
263281
logger.debug(f"unlinked {gc_root=}")
264282
except Exception as ex:
265-
raise GRPCError(
266-
Status.INTERNAL, f"unlinking {target_path=} failed", ex
267-
)
283+
raise GRPCError(Status.INTERNAL, f"unlinking {gc_root=} failed", ex)
268284

269285
# Remove hardlink farm
270286
volume_path = CSI_VOLUMES / request.volume_id
@@ -274,7 +290,7 @@ async def NodeUnpublishVolume(self, stream):
274290
logger.debug(f"removed {volume_path=}")
275291
except Exception as ex:
276292
raise GRPCError(
277-
Status.INTERNAL, f"recursive removing {target_path=} failed", ex
293+
Status.INTERNAL, f"removing {volume_path=} failed", ex
278294
)
279295

280296
await stream.send_message(csi_pb2.NodeUnpublishVolumeResponse())
@@ -313,7 +329,7 @@ async def NodeUnstageVolume(self, stream):
313329

314330

315331
async def serve():
316-
sock_path = "/csi/csi.sock"
332+
sock_path = CSI_SOCKET_PATH
317333
Path(sock_path).unlink(missing_ok=True)
318334

319335
identityServicer = IdentityServicer()

python/nix_csi/store.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,10 @@ def extract_store_paths(value: Any) -> Iterator[Path]:
1919
yield from extract_store_paths(item)
2020

2121

22+
def extract_store_paths_set(value: Any) -> set[Path]:
23+
"""Convenience wrapper that returns a deduplicated set of store paths."""
24+
return set(extract_store_paths(value))
25+
26+
2227
def extract_store_name(path: Path | str) -> str:
2328
return str(path).removeprefix("/nix/store/")

python/nix_csi/volume.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
from grpclib import GRPCError
66
from grpclib.const import Status
77

8-
from .constants import CSI_GCROOTS, CSI_VOLUMES, MOUNT_ALREADY_MOUNTED, NIX_BUILD_TIMEOUT
8+
from .constants import (
9+
CSI_GCROOTS,
10+
CSI_VOLUMES,
11+
MOUNT_ALREADY_MOUNTED,
12+
NIX_BUILD_TIMEOUT,
13+
)
914
from .hardlinks import deref_hardlink_tree, hardlink_closure
1015
from .nix import get_closure_paths, init_database, install_gcroot, install_result_link
1116
from .store import extract_store_name
@@ -32,20 +37,26 @@ async def prepare_volume(
3237
# Get storepaths from all packages
3338
store_paths = await get_closure_paths(package_paths)
3439

40+
# Pre-compute store names to avoid duplicate calls
41+
package_names = {p: extract_store_name(p) for p in package_paths}
42+
3543
# This block is essentially nix copy into a chroot store with
3644
# extra steps. (Hardlinking instead of dumbcopying)
3745

38-
# Install CSI gcroots
39-
for package_path in package_paths:
40-
name = extract_store_name(package_path)
41-
await try_captured(
42-
"nix",
43-
"build",
44-
"--out-link",
45-
gc_root / name,
46-
package_path,
47-
timeout=NIX_BUILD_TIMEOUT,
48-
)
46+
# Install CSI gcroots (parallel since packages are independent)
47+
await asyncio.gather(
48+
*[
49+
try_captured(
50+
"nix",
51+
"build",
52+
"--out-link",
53+
gc_root / package_names[package_path],
54+
package_path,
55+
timeout=NIX_BUILD_TIMEOUT,
56+
)
57+
for package_path in package_paths
58+
]
59+
)
4960

5061
# Copy closure to substore
5162
hardlink_closure([Path(p) for p in store_paths], volume_root / "nix/store")
@@ -56,9 +67,14 @@ async def prepare_volume(
5667
# Install gcroots in container using chroot store. This is
5768
# required because the auto roots created for /nix/var/result
5869
# will point to Narnia while this one points into store.
59-
for package_path in package_paths:
60-
name = extract_store_name(package_path)
61-
await install_gcroot(volume_root, package_path, name, NIX_STATE_DIR)
70+
await asyncio.gather(
71+
*[
72+
install_gcroot(
73+
volume_root, package_path, package_names[package_path], NIX_STATE_DIR
74+
)
75+
for package_path in package_paths
76+
]
77+
)
6278

6379
# Install /nix/var/result in container using chroot store
6480
if primary_package is not None:
@@ -134,6 +150,4 @@ async def unmount(path: Path) -> None:
134150
"""Unmount a path. Raises GRPCError on failure if still mounted."""
135151
result = await run_captured("umount", "--verbose", path)
136152
if result.returncode != 0 and await is_mount(path):
137-
raise GRPCError(
138-
Status.INTERNAL, "unmount failed", f"{result.combined=}"
139-
)
153+
raise GRPCError(Status.INTERNAL, "unmount failed", f"{result.combined=}")

0 commit comments

Comments
 (0)