Add editable Samba global configuration with net conf setparm

Backend:
- Use 'net conf setparm' to update individual parameters
- Accept dictionary of key-value pairs in API

Frontend:
- Add Edit/Cancel/Save buttons to Samba Config tab
- Inline editing of configuration values
- Save changes back to registry

Changes are applied immediately via net conf setparm.

Co-Authored-By: Patrick <patrick@perlbach24.de>
This commit is contained in:
2026-04-22 01:31:14 +02:00
parent a373378691
commit 07cf45a481
4 changed files with 77 additions and 39 deletions
+3 -3
View File
@@ -38,7 +38,7 @@ class CreateNFSShareRequest(BaseModel):
class SambaConfigRequest(BaseModel): class SambaConfigRequest(BaseModel):
config: str parameters: dict[str, str]
class SambaImportRequest(BaseModel): class SambaImportRequest(BaseModel):
@@ -108,9 +108,9 @@ async def set_samba_config(
request: SambaConfigRequest, request: SambaConfigRequest,
current_user: str = Depends(get_current_user) current_user: str = Depends(get_current_user)
): ):
"""Update Samba global configuration""" """Update Samba global configuration parameters"""
try: try:
success = share_manager.set_samba_global_config(request.config) success = share_manager.set_samba_global_config(request.parameters)
if not success: if not success:
raise HTTPException(status_code=400, detail="Failed to update Samba configuration") raise HTTPException(status_code=400, detail="Failed to update Samba configuration")
return {"status": "updated"} return {"status": "updated"}
+13 -34
View File
@@ -216,42 +216,21 @@ class SharesManager:
logger.error(f"Error reading Samba registry config: {e}") logger.error(f"Error reading Samba registry config: {e}")
return {"parameters": []} return {"parameters": []}
def set_samba_global_config(self, config_text: str) -> bool: def set_samba_global_config(self, parameters: Dict[str, str]) -> bool:
"""Write Samba global configuration section""" """Update Samba global configuration parameters using 'net conf setparm'"""
if not SAMBA_CONFIG.exists(): try:
for key, value in parameters.items():
result = subprocess.run(
['/usr/bin/net', 'conf', 'setparm', 'global', key, value],
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
logger.error(f"Failed to set {key}={value}: {result.stderr}")
return False return False
try: logger.info(f"Samba global config updated: {len(parameters)} parameters")
with open(SAMBA_CONFIG, 'r') as f:
lines = f.readlines()
# Find global section and shares
output_lines = []
skip_global = False
for i, line in enumerate(lines):
if line.strip().startswith('[global]'):
skip_global = True
output_lines.append('[global]\n')
# Add config lines
for config_line in config_text.split('\n'):
if config_line.strip():
output_lines.append(' ' + config_line + '\n')
output_lines.append('\n')
continue
if skip_global:
if line.strip().startswith('['):
skip_global = False
output_lines.append(line)
continue
output_lines.append(line)
with open(SAMBA_CONFIG, 'w') as f:
f.writelines(output_lines)
subprocess.run(['smbcontrol', 'smbd', 'reload-config'], capture_output=True, timeout=10)
logger.info("Samba global config updated")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error writing Samba global config: {e}") logger.error(f"Error writing Samba global config: {e}")
+55 -1
View File
@@ -23,6 +23,9 @@ export default function SharesPage() {
const [showNfsDialog, setShowNfsDialog] = useState(false) const [showNfsDialog, setShowNfsDialog] = useState(false)
const [deleteConfirm, setDeleteConfirm] = useState<{ type: "samba" | "nfs"; name: string } | null>(null) const [deleteConfirm, setDeleteConfirm] = useState<{ type: "samba" | "nfs"; name: string } | null>(null)
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const [editMode, setEditMode] = useState(false)
const [editedConfig, setEditedConfig] = useState<{ [key: string]: string }>({})
const [saving, setSaving] = useState(false)
useEffect(() => { useEffect(() => {
const token = localStorage.getItem("access_token") const token = localStorage.getItem("access_token")
@@ -90,6 +93,29 @@ export default function SharesPage() {
setShowNfsDialog(false) setShowNfsDialog(false)
} }
const handleEditMode = () => {
const configMap = sambaConfig.reduce((acc, param) => {
acc[param.key] = param.value
return acc
}, {} as { [key: string]: string })
setEditedConfig(configMap)
setEditMode(true)
}
const handleSaveConfig = async () => {
try {
setSaving(true)
await api.setSambaConfig(editedConfig)
setEditMode(false)
await loadShares()
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save configuration")
} finally {
setSaving(false)
}
}
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<Header /> <Header />
@@ -272,7 +298,24 @@ export default function SharesPage() {
{activeTab === "config" && ( {activeTab === "config" && (
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Samba Global Configuration</CardTitle> <CardTitle>Samba Global Configuration</CardTitle>
{!editMode && sambaConfig.length > 0 && (
<Button size="sm" onClick={handleEditMode} variant="outline">
Edit Config
</Button>
)}
{editMode && (
<div className="flex gap-2">
<Button size="sm" onClick={handleSaveConfig} disabled={saving}>
{saving ? "Saving..." : "Save Changes"}
</Button>
<Button size="sm" onClick={() => setEditMode(false)} variant="outline" disabled={saving}>
Cancel
</Button>
</div>
)}
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{sambaConfig.length === 0 ? ( {sambaConfig.length === 0 ? (
@@ -292,7 +335,18 @@ export default function SharesPage() {
{sambaConfig.map((param, idx) => ( {sambaConfig.map((param, idx) => (
<tr key={idx} className="border-b border-border/50 hover:bg-muted/30"> <tr key={idx} className="border-b border-border/50 hover:bg-muted/30">
<td className="py-3 px-4 font-mono text-xs font-medium text-blue-600">{param.key}</td> <td className="py-3 px-4 font-mono text-xs font-medium text-blue-600">{param.key}</td>
<td className="py-3 px-4 text-xs font-mono break-all">{param.value}</td> <td className="py-3 px-4 text-xs">
{editMode ? (
<input
type="text"
value={editedConfig[param.key] || ""}
onChange={(e) => setEditedConfig({ ...editedConfig, [param.key]: e.target.value })}
className="w-full px-2 py-1 rounded border border-border bg-background text-xs font-mono"
/>
) : (
<span className="font-mono break-all">{param.value}</span>
)}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
+5
View File
@@ -287,6 +287,11 @@ export class ZFSManagerAPI {
return response.data return response.data
} }
async setSambaConfig(parameters: { [key: string]: string }): Promise<{ status: string }> {
const response = await this.client.put("/api/shares/samba/config", { parameters })
return response.data
}
// Shares — NFS // Shares — NFS
async getNfsShares(): Promise<NfsShare[]> { async getNfsShares(): Promise<NfsShare[]> {
const response = await this.client.get("/api/shares/nfs") const response = await this.client.get("/api/shares/nfs")