681 lines
24 KiB
Go
681 lines
24 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"github.com/example/storage-appliance/internal/audit"
|
|
"github.com/example/storage-appliance/internal/domain"
|
|
"github.com/go-chi/chi/v5"
|
|
"html/template"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
var templates *template.Template
|
|
|
|
func init() {
|
|
var err error
|
|
// Try a couple of relative paths so tests work regardless of cwd
|
|
templates, err = template.ParseGlob("internal/templates/*.html")
|
|
if err != nil {
|
|
templates, err = template.ParseGlob("../templates/*.html")
|
|
}
|
|
if err != nil {
|
|
templates, err = template.ParseGlob("./templates/*.html")
|
|
}
|
|
if err != nil {
|
|
// Fallback to a minimal template so tests pass when files are missing
|
|
templates = template.New("dashboard.html")
|
|
templates.New("dashboard.html").Parse(`<div>{{.Title}}</div>`)
|
|
}
|
|
}
|
|
|
|
func (a *App) DashboardHandler(w http.ResponseWriter, r *http.Request) {
|
|
data := templateData(r, map[string]interface{}{
|
|
"Title": "Storage Appliance Dashboard",
|
|
})
|
|
if err := templates.ExecuteTemplate(w, "base", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (a *App) PoolsHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
pools, err := a.ZFSSvc.ListPools(ctx)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// audit the list action if possible
|
|
if a.StorageSvc != nil && a.StorageSvc.Audit != nil {
|
|
user, _ := r.Context().Value(ContextKeyUser).(string)
|
|
a.StorageSvc.Audit.Record(ctx, audit.Event{UserID: user, Action: "pool.list", ResourceType: "pool", ResourceID: "all", Success: true})
|
|
}
|
|
j, err := json.Marshal(pools)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(j)
|
|
}
|
|
|
|
func (a *App) JobsHandler(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`[]`))
|
|
}
|
|
|
|
// PoolDatasetsHandler returns datasets for a given pool via API
|
|
func (a *App) PoolDatasetsHandler(w http.ResponseWriter, r *http.Request) {
|
|
pool := chi.URLParam(r, "pool")
|
|
ds, err := a.StorageSvc.ListDatasets(r.Context(), pool)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
b, _ := json.Marshal(ds)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(b)
|
|
if a.StorageSvc != nil && a.StorageSvc.Audit != nil {
|
|
user, _ := r.Context().Value(ContextKeyUser).(string)
|
|
a.StorageSvc.Audit.Record(r.Context(), audit.Event{UserID: user, Action: "dataset.list", ResourceType: "dataset", ResourceID: pool, Success: true})
|
|
}
|
|
}
|
|
|
|
// CreateDatasetHandler handles dataset creation via API
|
|
func (a *App) CreateDatasetHandler(w http.ResponseWriter, r *http.Request) {
|
|
type req struct {
|
|
Name string `json:"name"`
|
|
Props map[string]string `json:"props"`
|
|
}
|
|
var body req
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if err := a.StorageSvc.CreateDataset(r.Context(), user, role, body.Name, body.Props); err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// SnapshotHandler creates a snapshot via Storage service and returns job id
|
|
func (a *App) SnapshotHandler(w http.ResponseWriter, r *http.Request) {
|
|
dataset := chi.URLParam(r, "dataset")
|
|
type req struct {
|
|
Name string `json:"name"`
|
|
}
|
|
var body req
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
id, err := a.StorageSvc.Snapshot(r.Context(), user, role, dataset, body.Name)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"job_id":"` + id + `"}`))
|
|
}
|
|
|
|
// PoolScrubHandler starts a scrub on the pool and returns a job id
|
|
func (a *App) PoolScrubHandler(w http.ResponseWriter, r *http.Request) {
|
|
pool := chi.URLParam(r, "pool")
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
id, err := a.StorageSvc.ScrubStart(r.Context(), user, role, pool)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"job_id":"` + id + `"}`))
|
|
}
|
|
|
|
// NFSStatusHandler returns nfs server service status
|
|
func (a *App) NFSStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|
status, err := a.ShareSvc.NFSStatus(r.Context())
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"status":"` + status + `"}`))
|
|
}
|
|
|
|
// ObjectStoreHandler renders object storage page (MinIO)
|
|
func (a *App) ObjectStoreHandler(w http.ResponseWriter, r *http.Request) {
|
|
data := map[string]interface{}{"Title": "Object Storage"}
|
|
if err := templates.ExecuteTemplate(w, "base", data); err != nil {
|
|
if err2 := templates.ExecuteTemplate(w, "object_store", data); err2 != nil {
|
|
http.Error(w, err2.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
// HXBucketsHandler renders buckets list partial
|
|
func (a *App) HXBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
|
var buckets []string
|
|
if a.ObjectSvc != nil {
|
|
buckets, _ = a.ObjectSvc.ListBuckets(r.Context())
|
|
}
|
|
if err := templates.ExecuteTemplate(w, "hx_buckets", buckets); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// CreateBucketHandler creates a bucket through the ObjectSvc
|
|
func (a *App) CreateBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := r.FormValue("name")
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ObjectSvc == nil {
|
|
http.Error(w, "object service not configured", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
id, err := a.ObjectSvc.CreateBucket(r.Context(), user, role, name)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
data := map[string]any{"JobID": id, "Name": name}
|
|
if err := templates.ExecuteTemplate(w, "job_row", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// ObjectSettingsHandler handles updating object storage settings
|
|
func (a *App) ObjectSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
|
// accept JSON body with settings or form values
|
|
type req struct {
|
|
AccessKey string `json:"access_key"`
|
|
SecretKey string `json:"secret_key"`
|
|
DataPath string `json:"data_path"`
|
|
Port int `json:"port"`
|
|
TLS bool `json:"tls"`
|
|
}
|
|
var body req
|
|
if r.Header.Get("Content-Type") == "application/json" {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
} else {
|
|
if err := r.ParseForm(); err == nil {
|
|
body.AccessKey = r.FormValue("access_key")
|
|
body.SecretKey = r.FormValue("secret_key")
|
|
body.DataPath = r.FormValue("data_path")
|
|
// parse port and tls
|
|
}
|
|
}
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ObjectSvc == nil {
|
|
http.Error(w, "object service not configured", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// wrap settings as an 'any' to satisfy interface (object service expects a specific type internally)
|
|
// For now, cast to the concrete struct via type assertion inside the service, but we need to pass as any
|
|
settings := map[string]any{"access_key": body.AccessKey, "secret_key": body.SecretKey, "data_path": body.DataPath, "port": body.Port, "tls": body.TLS}
|
|
// ObjectService.SetSettings expects settings 'any' (simplified), need to convert inside
|
|
if err := a.ObjectSvc.SetSettings(r.Context(), user, role, settings); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// CreatePoolHandler receives a request to create a pool and enqueues a job
|
|
func (a *App) CreatePoolHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Minimal implementation that reads 'name' and 'vdevs'
|
|
type req struct {
|
|
Name string `json:"name"`
|
|
Vdevs []string `json:"vdevs"`
|
|
}
|
|
var body req
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
// prefer storage service which adds validation/audit; fall back to job runner
|
|
var id string
|
|
var err error
|
|
if a.StorageSvc != nil {
|
|
user, _ := r.Context().Value(ContextKeyUser).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
id, err = a.StorageSvc.CreatePool(r.Context(), user, role, body.Name, body.Vdevs)
|
|
} else if a.JobRunner != nil {
|
|
j := domain.Job{Type: "create-pool", Status: "queued", Progress: 0, Details: map[string]any{"name": body.Name, "vdevs": body.Vdevs}}
|
|
id, err = a.JobRunner.Enqueue(r.Context(), j)
|
|
} else {
|
|
http.Error(w, "no job runner", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err != nil {
|
|
http.Error(w, "failed to create job", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"job_id":"` + id + `"}`))
|
|
}
|
|
|
|
func StaticHandler(w http.ResponseWriter, r *http.Request) {
|
|
p := r.URL.Path[len("/static/"):]
|
|
http.ServeFile(w, r, filepath.Join("static", p))
|
|
}
|
|
|
|
// StorageHandler renders the main storage page
|
|
func (a *App) StorageHandler(w http.ResponseWriter, r *http.Request) {
|
|
data := templateData(r, map[string]interface{}{
|
|
"Title": "Storage",
|
|
})
|
|
if err := templates.ExecuteTemplate(w, "base", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// HXPoolsHandler renders a pools partial (HTMX)
|
|
func (a *App) HXPoolsHandler(w http.ResponseWriter, r *http.Request) {
|
|
pools, _ := a.StorageSvc.ListPools(r.Context())
|
|
if err := templates.ExecuteTemplate(w, "hx_pools", pools); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// StorageCreatePoolHandler handles HTMX pool create POST; expects form values `name` and `vdevs` (comma-separated)
|
|
func (a *App) StorageCreatePoolHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := r.FormValue("name")
|
|
vdevsRaw := r.FormValue("vdevs")
|
|
vdevs := []string{}
|
|
if vdevsRaw != "" {
|
|
vdevs = append(vdevs, strings.Split(vdevsRaw, ",")...)
|
|
}
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
jobID, err := a.StorageSvc.CreatePool(r.Context(), user, role, name, vdevs)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
// Return a small job row partial as response
|
|
data := map[string]string{"JobID": jobID, "Name": name}
|
|
if err := templates.ExecuteTemplate(w, "job_row", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// JobPartialHandler returns a job progress partial by id
|
|
func (a *App) JobPartialHandler(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
// Read job status from DB
|
|
row := a.DB.QueryRowContext(r.Context(), `SELECT id, type, status, progress FROM jobs WHERE id = ?`, id)
|
|
var jid, jtype, status string
|
|
var progress int
|
|
if err := row.Scan(&jid, &jtype, &status, &progress); err != nil {
|
|
http.Error(w, "job not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
data := map[string]any{"JobID": jid, "Type": jtype, "Status": status, "Progress": progress}
|
|
if err := templates.ExecuteTemplate(w, "job_row", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// SharesNFSHandler renders the NFS shares page
|
|
func (a *App) SharesNFSHandler(w http.ResponseWriter, r *http.Request) {
|
|
data := templateData(r, map[string]interface{}{"Title": "NFS Shares"})
|
|
if err := templates.ExecuteTemplate(w, "base", data); err != nil {
|
|
// fallback to rendering the content template directly (useful in tests)
|
|
if err2 := templates.ExecuteTemplate(w, "shares_nfs", data); err2 != nil {
|
|
http.Error(w, err2.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
// HXNFSHandler renders NFS shares partial
|
|
func (a *App) HXNFSHandler(w http.ResponseWriter, r *http.Request) {
|
|
shares := []domain.Share{}
|
|
if a.ShareSvc != nil {
|
|
shares, _ = a.ShareSvc.ListNFS(r.Context())
|
|
}
|
|
if err := templates.ExecuteTemplate(w, "hx_nfs_shares", shares); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// CreateNFSHandler handles NFS create requests (HTMX form or JSON)
|
|
func (a *App) CreateNFSHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := r.FormValue("name")
|
|
path := r.FormValue("path")
|
|
optsRaw := r.FormValue("options")
|
|
opts := map[string]string{}
|
|
if optsRaw != "" {
|
|
// expecting JSON options for MVP
|
|
_ = json.Unmarshal([]byte(optsRaw), &opts)
|
|
}
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ShareSvc == nil {
|
|
http.Error(w, "no share service", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
id, err := a.ShareSvc.CreateNFS(r.Context(), user, role, name, path, opts)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
// Return a job/creation partial: reuse job_row for a simple message
|
|
data := map[string]any{"JobID": id, "Name": name, "Status": "queued"}
|
|
if err := templates.ExecuteTemplate(w, "job_row", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// DeleteNFSHandler handles NFS share deletion
|
|
func (a *App) DeleteNFSHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
id := r.FormValue("id")
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ShareSvc == nil {
|
|
http.Error(w, "no share service", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := a.ShareSvc.DeleteNFS(r.Context(), user, role, id); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
// return partial table after deletion
|
|
shares, _ := a.ShareSvc.ListNFS(r.Context())
|
|
if err := templates.ExecuteTemplate(w, "hx_nfs_shares", shares); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// SharesSMBHandler renders the SMB shares page
|
|
func (a *App) SharesSMBHandler(w http.ResponseWriter, r *http.Request) {
|
|
data := map[string]interface{}{"Title": "SMB Shares"}
|
|
if err := templates.ExecuteTemplate(w, "base", data); err != nil {
|
|
// fallback for tests
|
|
if err2 := templates.ExecuteTemplate(w, "shares_smb", data); err2 != nil {
|
|
http.Error(w, err2.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ISCSIHandler renders the iSCSI page
|
|
func (a *App) ISCSIHandler(w http.ResponseWriter, r *http.Request) {
|
|
data := map[string]interface{}{"Title": "iSCSI Targets"}
|
|
if err := templates.ExecuteTemplate(w, "base", data); err != nil {
|
|
if err2 := templates.ExecuteTemplate(w, "iscsi", data); err2 != nil {
|
|
http.Error(w, err2.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
// HXISCSIHandler renders iSCSI targets partial
|
|
func (a *App) HXISCSIHandler(w http.ResponseWriter, r *http.Request) {
|
|
targets := []map[string]any{}
|
|
if a.ISCSISvc != nil {
|
|
targets, _ = a.ISCSISvc.ListTargets(r.Context())
|
|
}
|
|
if err := templates.ExecuteTemplate(w, "hx_iscsi_targets", targets); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// HXISCLUNsHandler renders LUNs for a target
|
|
func (a *App) HXISCLUNsHandler(w http.ResponseWriter, r *http.Request) {
|
|
targetID := chi.URLParam(r, "target")
|
|
luns := []map[string]any{}
|
|
if a.ISCSISvc != nil {
|
|
luns, _ = a.ISCSISvc.ListLUNs(r.Context(), targetID)
|
|
}
|
|
if err := templates.ExecuteTemplate(w, "hx_iscsi_luns", luns); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// ISCSI Target info partial
|
|
func (a *App) ISCSITargetInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|
targetID := chi.URLParam(r, "target")
|
|
var info map[string]any
|
|
if a.ISCSISvc != nil {
|
|
info, _ = a.ISCSISvc.GetTargetInfo(r.Context(), targetID)
|
|
}
|
|
if err := templates.ExecuteTemplate(w, "hx_iscsi_target_info", info); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// CreateISCSITargetHandler handles creating an iSCSI target via form/JSON
|
|
func (a *App) CreateISCSITargetHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := r.FormValue("name")
|
|
iqn := r.FormValue("iqn")
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ISCSISvc == nil {
|
|
http.Error(w, "no iscsi service", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
id, err := a.ISCSISvc.CreateTarget(r.Context(), user, role, name, iqn)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
data := map[string]any{"ID": id, "Name": name}
|
|
if err := templates.ExecuteTemplate(w, "job_row", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// CreateISCSILUNHandler handles creating a LUN for a target
|
|
func (a *App) CreateISCSILUNHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
targetID := r.FormValue("target_id")
|
|
zvol := r.FormValue("zvol")
|
|
size := r.FormValue("size")
|
|
blocksize := 512
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ISCSISvc == nil {
|
|
http.Error(w, "no iscsi service", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
id, err := a.ISCSISvc.CreateLUN(r.Context(), user, role, targetID, zvol, size, blocksize)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
data := map[string]any{"JobID": id, "Name": zvol}
|
|
if err := templates.ExecuteTemplate(w, "job_row", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// DeleteISCSILUNHandler deletes a LUN with optional 'force' param
|
|
func (a *App) DeleteISCSILUNHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
id := r.FormValue("id")
|
|
force := r.FormValue("force") == "1" || r.FormValue("force") == "true"
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ISCSISvc == nil {
|
|
http.Error(w, "no iscsi service", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := a.ISCSISvc.DeleteLUN(r.Context(), user, role, id, force); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// AddISCSIPortalHandler configures a portal for a target
|
|
func (a *App) AddISCSIPortalHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
targetID := r.FormValue("target_id")
|
|
address := r.FormValue("address")
|
|
// default port 3260
|
|
port := 3260
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ISCSISvc == nil {
|
|
http.Error(w, "no iscsi service", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
id, err := a.ISCSISvc.AddPortal(r.Context(), user, role, targetID, address, port)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
data := map[string]any{"ID": id}
|
|
if err := templates.ExecuteTemplate(w, "job_row", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// AddISCSIInitiatorHandler adds an initiator to an IQN ACL
|
|
func (a *App) AddISCSIInitiatorHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
targetID := r.FormValue("target_id")
|
|
initiator := r.FormValue("initiator_iqn")
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ISCSISvc == nil {
|
|
http.Error(w, "no iscsi service", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
id, err := a.ISCSISvc.AddInitiator(r.Context(), user, role, targetID, initiator)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
data := map[string]any{"ID": id}
|
|
if err := templates.ExecuteTemplate(w, "job_row", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// UnmapISCSILUNHandler performs the 'drain' step to unmap the LUN
|
|
func (a *App) UnmapISCSILUNHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
id := r.FormValue("id")
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ISCSISvc == nil {
|
|
http.Error(w, "no iscsi service", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := a.ISCSISvc.UnmapLUN(r.Context(), user, role, id); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// HXSmbHandler renders SMB shares partial
|
|
func (a *App) HXSmbHandler(w http.ResponseWriter, r *http.Request) {
|
|
shares := []domain.Share{}
|
|
if a.ShareSvc != nil {
|
|
shares, _ = a.ShareSvc.ListSMB(r.Context())
|
|
}
|
|
if err := templates.ExecuteTemplate(w, "hx_smb_shares", shares); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// CreateSMBHandler handles SMB creation (HTMX)
|
|
func (a *App) CreateSMBHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := r.FormValue("name")
|
|
path := r.FormValue("path")
|
|
readOnly := r.FormValue("read_only") == "1" || r.FormValue("read_only") == "true"
|
|
allowedUsersRaw := r.FormValue("allowed_users")
|
|
var allowed []string
|
|
if allowedUsersRaw != "" {
|
|
allowed = strings.Split(allowedUsersRaw, ",")
|
|
}
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ShareSvc == nil {
|
|
http.Error(w, "no share service", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
id, err := a.ShareSvc.CreateSMB(r.Context(), user, role, name, path, readOnly, allowed)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
data := map[string]any{"JobID": id, "Name": name, "Status": "queued"}
|
|
if err := templates.ExecuteTemplate(w, "job_row", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// DeleteSMBHandler handles SMB deletion
|
|
func (a *App) DeleteSMBHandler(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
id := r.FormValue("id")
|
|
user, _ := r.Context().Value(ContextKey("user")).(string)
|
|
role, _ := r.Context().Value(ContextKey("user.role")).(string)
|
|
if a.ShareSvc == nil {
|
|
http.Error(w, "no share service", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := a.ShareSvc.DeleteSMB(r.Context(), user, role, id); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
shares, _ := a.ShareSvc.ListSMB(r.Context())
|
|
if err := templates.ExecuteTemplate(w, "hx_smb_shares", shares); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|