Coverage for volexport/api_export.py: 96%
72 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-28 12:48 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-28 12:48 +0000
1from fastapi import APIRouter, HTTPException, Request
2from pydantic import BaseModel, Field, SecretStr, field_serializer
3from .config2 import config2
4from .tgtd import Tgtd
5from .lvm2 import LV
7router = APIRouter()
10class ExportRequest(BaseModel):
11 name: str = Field(description="Volume name to export", examples=["volume1"])
12 acl: list[str] | None = Field(description="Source IP Addresses to allow access")
13 readonly: bool = Field(default=False, description="read-only if true", examples=[True, False])
14 user: str | None = Field(default=None, description="user name for access. auto-generate if null")
15 passwd: SecretStr | None = Field(default=None, description="password for access. auto-generate if null")
18class ExportResponse(BaseModel):
19 protocol: str = Field(description="access protocol", examples=["iscsi"])
20 addresses: list[str] = Field(description="IP addresses of the target")
21 targetname: str = Field(description="target name", examples=["iqn.2025-08.volexport:abcde"])
22 tid: int = Field(description="target ID")
23 user: str = Field(description="user name for access", examples=["admin"])
24 passwd: SecretStr = Field(description="password for access", examples=["password123"])
25 lun: int = Field(description="LUN number", examples=[1, 2, 3])
26 acl: list[str] = Field(description="Access Control List (ACL) for the export")
28 @field_serializer("passwd", when_used="json")
29 def dump_secret(self, v):
30 return v.get_secret_value()
33class ClientInfo(BaseModel):
34 address: list[str] = Field(description="IP addresses of the client")
35 initiator: str = Field(description="Initiator name", examples=["iqn.2025-08.volimport:client1"])
38class ExportReadResponse(BaseModel):
39 protocol: str = Field(description="access protocol", examples=["iscsi"])
40 connected: list[ClientInfo] = Field(description="List of connected clients")
41 targetname: str = Field(description="target name", examples=["iqn.2025-08.volexport:abcde"])
42 tid: int = Field(description="target ID")
43 volumes: list[str] = Field(description="List of volumes exported", examples=["volume1"])
44 users: list[str] = Field(description="List of users with access", examples=["admin", "user1"])
45 acl: list[str] = Field(description="Access Control List (ACL) for the export")
48class ExportStats(BaseModel):
49 targets: int = Field(description="Number of export targets", examples=[5])
50 clients: int = Field(description="Number of connected clients", examples=[10])
51 volumes: int = Field(description="Number of volumes exported", examples=[15])
54def _fixpath(data: dict) -> dict:
55 if "volumes" in data: 55 ↛ 57line 55 didn't jump to line 57 because the condition on line 55 was always true
56 data["volumes"] = [LV(config2.VG).volume_path2vol(x) for x in data["volumes"]]
57 return data
60@router.get("/export", description="List all exports")
61def list_export(volume: str | None = None) -> list[ExportReadResponse]:
62 res = [ExportReadResponse.model_validate(_fixpath(x)) for x in Tgtd().export_list()]
63 if volume:
64 res = [x for x in res if volume in x.volumes]
65 return res
68@router.post("/export", description="Create a new export")
69def create_export(req: Request, arg: ExportRequest) -> ExportResponse:
70 filename = LV(config2.VG, arg.name).volume_vol2path()
71 if not arg.acl:
72 assert req.client is not None
73 arg.acl = [req.client.host]
74 return ExportResponse.model_validate(
75 Tgtd().export_volume(
76 filename=filename,
77 acl=arg.acl,
78 readonly=arg.readonly,
79 user=arg.user,
80 passwd=arg.passwd.get_secret_value() if arg.passwd else None,
81 )
82 )
85@router.get("/export/{name}", description="Read export details by name or TID")
86def read_export(name) -> ExportReadResponse:
87 res = [_fixpath(x) for x in Tgtd().export_list() if x["targetname"] == name or x["tid"] == name]
88 if len(res) == 0: 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 raise HTTPException(status_code=404, detail="export not found")
90 return ExportReadResponse.model_validate(res[0])
93@router.delete("/export/{name}", description="Delete an export by name or TID")
94def delete_export(name, force: bool = False):
95 return Tgtd().unexport_volume(targetname=name, force=force)
98@router.get("/address", description="Get addresses of the target")
99def get_address() -> list[str]:
100 return Tgtd().myaddress()
103@router.get("/stats/export", description="Get statistics of exports")
104def stats_export() -> ExportStats:
105 info = Tgtd().export_list()
106 return ExportStats(
107 targets=len(info),
108 clients=sum([len(x["connected"]) for x in info]),
109 volumes=sum([len(x["volumes"]) for x in info]),
110 )