This commit is contained in:
172
PlutoOS_SRS_v1.md
Normal file
172
PlutoOS_SRS_v1.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
SOFTWARE REQUIREMENTS SPECIFICATION (SRS)
|
||||||
|
PlutoOS – Storage Controller Operating System (v1)
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
1. INTRODUCTION
|
||||||
|
--------------------------------------------------
|
||||||
|
1.1 Purpose
|
||||||
|
This document defines the functional and non-functional requirements for PlutoOS v1,
|
||||||
|
a storage controller operating system built on Linux with ZFS as the core storage engine.
|
||||||
|
It serves as the authoritative reference for development scope, validation, and acceptance.
|
||||||
|
|
||||||
|
1.2 Scope
|
||||||
|
PlutoOS v1 provides:
|
||||||
|
- ZFS pool, dataset, and ZVOL management
|
||||||
|
- Storage services: SMB, NFS, iSCSI (ZVOL-backed)
|
||||||
|
- Automated snapshot management
|
||||||
|
- Role-Based Access Control (RBAC) and audit logging
|
||||||
|
- Web-based GUI and local TUI
|
||||||
|
- Monitoring and Prometheus-compatible metrics
|
||||||
|
|
||||||
|
The following are explicitly out of scope for v1:
|
||||||
|
- High Availability (HA) or clustering
|
||||||
|
- Multi-node replication
|
||||||
|
- Object storage (S3)
|
||||||
|
- Active Directory / LDAP integration
|
||||||
|
|
||||||
|
1.3 Definitions
|
||||||
|
Dataset : ZFS filesystem
|
||||||
|
ZVOL : ZFS block device
|
||||||
|
LUN : Logical Unit Number exposed via iSCSI
|
||||||
|
Job : Asynchronous long-running operation
|
||||||
|
Desired State : Configuration stored in DB and applied atomically to system
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
2. SYSTEM OVERVIEW
|
||||||
|
--------------------------------------------------
|
||||||
|
PlutoOS consists of:
|
||||||
|
- Base OS : Minimal Linux (Ubuntu/Debian)
|
||||||
|
- Data Plane : ZFS and storage services
|
||||||
|
- Control Plane: Go backend with HTMX-based UI
|
||||||
|
- Interfaces : Web GUI, TUI, Metrics endpoint
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
3. USER CLASSES
|
||||||
|
--------------------------------------------------
|
||||||
|
Administrator : Full system and storage control
|
||||||
|
Operator : Storage and service operations
|
||||||
|
Viewer : Read-only access
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
4. FUNCTIONAL REQUIREMENTS
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
4.1 Authentication & Authorization
|
||||||
|
- System SHALL require authentication for all management access
|
||||||
|
- System SHALL enforce RBAC with predefined roles
|
||||||
|
- Access SHALL be denied by default
|
||||||
|
|
||||||
|
4.2 ZFS Management
|
||||||
|
- System SHALL list available disks (read-only)
|
||||||
|
- System SHALL create, import, and export ZFS pools
|
||||||
|
- System SHALL report pool health status
|
||||||
|
- System SHALL create and manage datasets
|
||||||
|
- System SHALL create ZVOLs for block storage
|
||||||
|
- System SHALL support scrub operations with progress monitoring
|
||||||
|
|
||||||
|
4.3 Snapshot Management
|
||||||
|
- System SHALL support manual snapshot creation
|
||||||
|
- System SHALL support automated snapshot policies
|
||||||
|
- System SHALL allow per-dataset snapshot enable/disable
|
||||||
|
- System SHALL prune snapshots based on retention policy
|
||||||
|
|
||||||
|
4.4 SMB Service
|
||||||
|
- System SHALL create SMB shares mapped to datasets
|
||||||
|
- System SHALL manage share permissions
|
||||||
|
- System SHALL apply configuration atomically
|
||||||
|
- System SHALL reload service safely
|
||||||
|
|
||||||
|
4.5 NFS Service
|
||||||
|
- System SHALL create NFS exports per dataset
|
||||||
|
- System SHALL support RW/RO and client restrictions
|
||||||
|
- System SHALL regenerate exports from desired state
|
||||||
|
- System SHALL reload NFS exports safely
|
||||||
|
|
||||||
|
4.6 iSCSI Block Storage
|
||||||
|
- System SHALL provision ZVOL-backed LUNs
|
||||||
|
- System SHALL create iSCSI targets with IQN
|
||||||
|
- System SHALL map LUNs to targets
|
||||||
|
- System SHALL configure initiator ACLs
|
||||||
|
- System SHALL expose connection instructions
|
||||||
|
|
||||||
|
4.7 Job Management
|
||||||
|
- System SHALL execute long-running operations as jobs
|
||||||
|
- System SHALL track job status and progress
|
||||||
|
- System SHALL persist job history
|
||||||
|
- Failed jobs SHALL not leave system inconsistent
|
||||||
|
|
||||||
|
4.8 Audit Logging
|
||||||
|
- System SHALL log all mutating operations
|
||||||
|
- Audit log SHALL record actor, action, resource, and timestamp
|
||||||
|
- Audit log SHALL be immutable from the UI
|
||||||
|
|
||||||
|
4.9 Web GUI
|
||||||
|
- System SHALL provide a web-based management interface
|
||||||
|
- GUI SHALL support partial updates
|
||||||
|
- GUI SHALL display system health and alerts
|
||||||
|
- Destructive actions SHALL require confirmation
|
||||||
|
|
||||||
|
4.10 TUI
|
||||||
|
- System SHALL provide a local console interface
|
||||||
|
- TUI SHALL support initial system setup
|
||||||
|
- TUI SHALL allow monitoring and maintenance operations
|
||||||
|
- TUI SHALL function without web UI availability
|
||||||
|
|
||||||
|
4.11 Monitoring & Metrics
|
||||||
|
- System SHALL expose /metrics in Prometheus format
|
||||||
|
- System SHALL expose pool health and capacity metrics
|
||||||
|
- System SHALL expose job failure metrics
|
||||||
|
- GUI SHALL present a metrics summary
|
||||||
|
|
||||||
|
4.12 Update & Maintenance
|
||||||
|
- System SHALL support safe update mechanisms
|
||||||
|
- Configuration SHALL be backed up prior to updates
|
||||||
|
- Maintenance mode SHALL disable user operations
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
5. NON-FUNCTIONAL REQUIREMENTS
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
5.1 Reliability
|
||||||
|
- Storage operations SHALL be transactional where possible
|
||||||
|
- System SHALL recover gracefully from partial failures
|
||||||
|
|
||||||
|
5.2 Performance
|
||||||
|
- Management UI read operations SHOULD respond within 500ms
|
||||||
|
- Background jobs SHALL not block UI responsiveness
|
||||||
|
|
||||||
|
5.3 Security
|
||||||
|
- HTTPS SHALL be enforced for the web UI
|
||||||
|
- Secrets SHALL NOT be logged in plaintext
|
||||||
|
- Least-privilege access SHALL be enforced
|
||||||
|
|
||||||
|
5.4 Maintainability
|
||||||
|
- Configuration SHALL be declarative
|
||||||
|
- System SHALL provide diagnostic information for support
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
6. CONSTRAINTS & ASSUMPTIONS
|
||||||
|
--------------------------------------------------
|
||||||
|
- Single-node controller
|
||||||
|
- Linux kernel with ZFS support
|
||||||
|
- Local storage only
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
7. ACCEPTANCE CRITERIA (v1)
|
||||||
|
--------------------------------------------------
|
||||||
|
PlutoOS v1 is accepted when:
|
||||||
|
- ZFS pool, dataset, share, and LUN lifecycle works end-to-end
|
||||||
|
- Snapshot policies are active and observable
|
||||||
|
- RBAC and audit logging are enforced
|
||||||
|
- GUI, TUI, and metrics endpoints are functional
|
||||||
|
- No manual configuration file edits are required
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
END OF DOCUMENT
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/httpapp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
addr := env("PLUTO_HTTP_ADDR", ":8080")
|
|
||||||
|
|
||||||
app, err := httpapp.New(httpapp.Config{
|
|
||||||
Addr: addr,
|
|
||||||
TemplatesDir: "web/templates",
|
|
||||||
StaticDir: "web/static",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("init app: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := &http.Server{
|
|
||||||
Addr: addr,
|
|
||||||
Handler: app.Router(),
|
|
||||||
ReadHeaderTimeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
go func() {
|
|
||||||
log.Printf("[pluto-api] listening on %s", addr)
|
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Fatalf("listen: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
stop := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(stop, os.Interrupt)
|
|
||||||
<-stop
|
|
||||||
log.Printf("[pluto-api] shutdown requested")
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
|
||||||
log.Printf("[pluto-api] shutdown error: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("[pluto-api] shutdown complete")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func env(key, def string) string {
|
|
||||||
if v := os.Getenv(key); v != "" {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
280
internal/httpapp/api_handlers.go
Normal file
280
internal/httpapp/api_handlers.go
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
package httpapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pathParam is now in router_helpers.go
|
||||||
|
|
||||||
|
// ZFS Pool Handlers
|
||||||
|
func (a *App) handleListPools(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO: Implement ZFS pool listing
|
||||||
|
pools := []models.Pool{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, pools)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleCreatePool(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO: Implement pool creation
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetPool(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := pathParam(r, "/api/v1/pools/")
|
||||||
|
// TODO: Implement pool retrieval
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleDeletePool(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := pathParam(r, "/api/v1/pools/")
|
||||||
|
// TODO: Implement pool deletion
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleScrubPool(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := pathParam(r, "/api/v1/pools/")
|
||||||
|
// TODO: Implement pool scrub
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dataset Handlers
|
||||||
|
func (a *App) handleListDatasets(w http.ResponseWriter, r *http.Request) {
|
||||||
|
datasets := []models.Dataset{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, datasets)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleCreateDataset(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Pool string `json:"pool"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: Implement dataset creation
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetDataset(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := pathParam(r, "/api/v1/datasets/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleUpdateDataset(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := pathParam(r, "/api/v1/datasets/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleDeleteDataset(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := pathParam(r, "/api/v1/datasets/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZVOL Handlers
|
||||||
|
func (a *App) handleListZVOLs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
zvols := []models.ZVOL{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, zvols)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleCreateZVOL(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetZVOL(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := pathParam(r, "/api/v1/zvols/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleDeleteZVOL(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := pathParam(r, "/api/v1/zvols/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot Handlers
|
||||||
|
func (a *App) handleListSnapshots(w http.ResponseWriter, r *http.Request) {
|
||||||
|
snapshots := []models.Snapshot{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, snapshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleCreateSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := pathParam(r, "/api/v1/snapshots/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleDeleteSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := pathParam(r, "/api/v1/snapshots/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "name": name})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot Policy Handlers
|
||||||
|
func (a *App) handleListSnapshotPolicies(w http.ResponseWriter, r *http.Request) {
|
||||||
|
policies := []models.SnapshotPolicy{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, policies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleCreateSnapshotPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetSnapshotPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dataset := pathParam(r, "/api/v1/snapshot-policies/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "dataset": dataset})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleUpdateSnapshotPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dataset := pathParam(r, "/api/v1/snapshot-policies/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "dataset": dataset})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleDeleteSnapshotPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dataset := pathParam(r, "/api/v1/snapshot-policies/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "dataset": dataset})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMB Share Handlers
|
||||||
|
func (a *App) handleListSMBShares(w http.ResponseWriter, r *http.Request) {
|
||||||
|
shares := []models.SMBShare{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, shares)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleCreateSMBShare(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetSMBShare(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/shares/smb/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleUpdateSMBShare(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/shares/smb/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleDeleteSMBShare(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/shares/smb/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NFS Export Handlers
|
||||||
|
func (a *App) handleListNFSExports(w http.ResponseWriter, r *http.Request) {
|
||||||
|
exports := []models.NFSExport{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, exports)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleCreateNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/exports/nfs/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleUpdateNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/exports/nfs/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleDeleteNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/exports/nfs/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
// iSCSI Handlers
|
||||||
|
func (a *App) handleListISCSITargets(w http.ResponseWriter, r *http.Request) {
|
||||||
|
targets := []models.ISCSITarget{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, targets)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleCreateISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/iscsi/targets/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleUpdateISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/iscsi/targets/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleDeleteISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/iscsi/targets/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleAddLUN(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/iscsi/targets/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleRemoveLUN(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/iscsi/targets/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Job Handlers
|
||||||
|
func (a *App) handleListJobs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
jobs := []models.Job{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, jobs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetJob(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/jobs/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleCancelJob(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/jobs/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth Handlers (stubs)
|
||||||
|
func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"message": "logged out"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// User Handlers
|
||||||
|
func (a *App) handleListUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
users := []models.User{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, users)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/users/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/users/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := pathParam(r, "/api/v1/users/")
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "not implemented", "id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audit Log Handlers
|
||||||
|
func (a *App) handleListAuditLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logs := []models.AuditLog{} // Stub
|
||||||
|
writeJSON(w, http.StatusOK, logs)
|
||||||
|
}
|
||||||
@@ -48,18 +48,7 @@ func (a *App) Router() http.Handler {
|
|||||||
return requestID(logging(a.mux))
|
return requestID(logging(a.mux))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) routes() {
|
// routes() is now in routes.go
|
||||||
// Static
|
|
||||||
fs := http.FileServer(http.Dir(a.cfg.StaticDir))
|
|
||||||
a.mux.Handle("/static/", http.StripPrefix("/static/", fs))
|
|
||||||
|
|
||||||
// Core pages
|
|
||||||
a.mux.HandleFunc("/", a.handleDashboard)
|
|
||||||
|
|
||||||
// Health & metrics
|
|
||||||
a.mux.HandleFunc("/healthz", a.handleHealthz)
|
|
||||||
a.mux.HandleFunc("/metrics", a.handleMetrics)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTemplates(dir string) (*template.Template, error) {
|
func parseTemplates(dir string) (*template.Template, error) {
|
||||||
pattern := filepath.Join(dir, "*.html")
|
pattern := filepath.Join(dir, "*.html")
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ func (a *App) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
|
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, _ = w.Write([]byte(
|
_, _ = w.Write([]byte(
|
||||||
`# HELP pluto_build_info Build info
|
`# HELP atlas_build_info Build info
|
||||||
# TYPE pluto_build_info gauge
|
# TYPE atlas_build_info gauge
|
||||||
pluto_build_info{version="v0.1.0-dev"} 1
|
atlas_build_info{version="v0.1.0-dev"} 1
|
||||||
# HELP pluto_up Whether the pluto-api process is up
|
# HELP atlas_up Whether the atlas-api process is up
|
||||||
# TYPE pluto_up gauge
|
# TYPE atlas_up gauge
|
||||||
pluto_up 1
|
atlas_up 1
|
||||||
`,
|
`,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
185
internal/httpapp/router_helpers.go
Normal file
185
internal/httpapp/router_helpers.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package httpapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// methodHandler routes requests based on HTTP method
|
||||||
|
func methodHandler(get, post, put, delete, patch http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
if get != nil {
|
||||||
|
get(w, r)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
case http.MethodPost:
|
||||||
|
if post != nil {
|
||||||
|
post(w, r)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
case http.MethodPut:
|
||||||
|
if put != nil {
|
||||||
|
put(w, r)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
case http.MethodDelete:
|
||||||
|
if delete != nil {
|
||||||
|
delete(w, r)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
case http.MethodPatch:
|
||||||
|
if patch != nil {
|
||||||
|
patch(w, r)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathParam extracts the last segment from a path
|
||||||
|
func pathParam(r *http.Request, prefix string) string {
|
||||||
|
path := strings.TrimPrefix(r.URL.Path, prefix)
|
||||||
|
path = strings.Trim(path, "/")
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePoolOps routes pool operations by method
|
||||||
|
func (a *App) handlePoolOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/scrub") {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
a.handleScrubPool(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleGetPool(w, r) },
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleDeletePool(w, r) },
|
||||||
|
nil,
|
||||||
|
)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDatasetOps routes dataset operations by method
|
||||||
|
func (a *App) handleDatasetOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleGetDataset(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateDataset(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateDataset(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteDataset(w, r) },
|
||||||
|
nil,
|
||||||
|
)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleZVOLOps routes ZVOL operations by method
|
||||||
|
func (a *App) handleZVOLOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleGetZVOL(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateZVOL(w, r) },
|
||||||
|
nil,
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteZVOL(w, r) },
|
||||||
|
nil,
|
||||||
|
)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSnapshotOps routes snapshot operations by method
|
||||||
|
func (a *App) handleSnapshotOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleGetSnapshot(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSnapshot(w, r) },
|
||||||
|
nil,
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteSnapshot(w, r) },
|
||||||
|
nil,
|
||||||
|
)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSnapshotPolicyOps routes snapshot policy operations by method
|
||||||
|
func (a *App) handleSnapshotPolicyOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleGetSnapshotPolicy(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSnapshotPolicy(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateSnapshotPolicy(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteSnapshotPolicy(w, r) },
|
||||||
|
nil,
|
||||||
|
)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSMBShareOps routes SMB share operations by method
|
||||||
|
func (a *App) handleSMBShareOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleGetSMBShare(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSMBShare(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateSMBShare(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteSMBShare(w, r) },
|
||||||
|
nil,
|
||||||
|
)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNFSExportOps routes NFS export operations by method
|
||||||
|
func (a *App) handleNFSExportOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleGetNFSExport(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateNFSExport(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateNFSExport(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteNFSExport(w, r) },
|
||||||
|
nil,
|
||||||
|
)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleISCSITargetOps routes iSCSI target operations by method
|
||||||
|
func (a *App) handleISCSITargetOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleGetISCSITarget(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateISCSITarget(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateISCSITarget(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteISCSITarget(w, r) },
|
||||||
|
nil,
|
||||||
|
)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleJobOps routes job operations by method
|
||||||
|
func (a *App) handleJobOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/cancel") {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
a.handleCancelJob(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleGetJob(w, r) },
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
)(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUserOps routes user operations by method
|
||||||
|
func (a *App) handleUserOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleGetUser(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateUser(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleUpdateUser(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleDeleteUser(w, r) },
|
||||||
|
nil,
|
||||||
|
)(w, r)
|
||||||
|
}
|
||||||
104
internal/httpapp/routes.go
Normal file
104
internal/httpapp/routes.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package httpapp
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func (a *App) routes() {
|
||||||
|
// Static files
|
||||||
|
fs := http.FileServer(http.Dir(a.cfg.StaticDir))
|
||||||
|
a.mux.Handle("/static/", http.StripPrefix("/static/", fs))
|
||||||
|
|
||||||
|
// Web UI
|
||||||
|
a.mux.HandleFunc("/", a.handleDashboard)
|
||||||
|
|
||||||
|
// Health & metrics
|
||||||
|
a.mux.HandleFunc("/healthz", a.handleHealthz)
|
||||||
|
a.mux.HandleFunc("/metrics", a.handleMetrics)
|
||||||
|
|
||||||
|
// API v1 routes - ZFS Management
|
||||||
|
a.mux.HandleFunc("/api/v1/pools", methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleListPools(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreatePool(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/pools/", a.handlePoolOps)
|
||||||
|
|
||||||
|
a.mux.HandleFunc("/api/v1/datasets", methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleListDatasets(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateDataset(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/datasets/", a.handleDatasetOps)
|
||||||
|
|
||||||
|
a.mux.HandleFunc("/api/v1/zvols", methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleListZVOLs(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateZVOL(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/zvols/", a.handleZVOLOps)
|
||||||
|
|
||||||
|
// Snapshot Management
|
||||||
|
a.mux.HandleFunc("/api/v1/snapshots", methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleListSnapshots(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSnapshot(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/snapshots/", a.handleSnapshotOps)
|
||||||
|
a.mux.HandleFunc("/api/v1/snapshot-policies", methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleListSnapshotPolicies(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSnapshotPolicy(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/snapshot-policies/", a.handleSnapshotPolicyOps)
|
||||||
|
|
||||||
|
// Storage Services - SMB
|
||||||
|
a.mux.HandleFunc("/api/v1/shares/smb", methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleListSMBShares(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateSMBShare(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/shares/smb/", a.handleSMBShareOps)
|
||||||
|
|
||||||
|
// Storage Services - NFS
|
||||||
|
a.mux.HandleFunc("/api/v1/exports/nfs", methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleListNFSExports(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateNFSExport(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/exports/nfs/", a.handleNFSExportOps)
|
||||||
|
|
||||||
|
// Storage Services - iSCSI
|
||||||
|
a.mux.HandleFunc("/api/v1/iscsi/targets", methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleListISCSITargets(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateISCSITarget(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/iscsi/targets/", a.handleISCSITargetOps)
|
||||||
|
|
||||||
|
// Job Management
|
||||||
|
a.mux.HandleFunc("/api/v1/jobs", methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleListJobs(w, r) },
|
||||||
|
nil, nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/jobs/", a.handleJobOps)
|
||||||
|
|
||||||
|
// Authentication & Authorization
|
||||||
|
a.mux.HandleFunc("/api/v1/auth/login", methodHandler(
|
||||||
|
nil,
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleLogin(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/auth/logout", methodHandler(
|
||||||
|
nil,
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleLogout(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/users", methodHandler(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleListUsers(w, r) },
|
||||||
|
func(w http.ResponseWriter, r *http.Request) { a.handleCreateUser(w, r) },
|
||||||
|
nil, nil, nil,
|
||||||
|
))
|
||||||
|
a.mux.HandleFunc("/api/v1/users/", a.handleUserOps)
|
||||||
|
|
||||||
|
// Audit Logs
|
||||||
|
a.mux.HandleFunc("/api/v1/audit", a.handleListAuditLogs)
|
||||||
|
}
|
||||||
36
internal/models/auth.go
Normal file
36
internal/models/auth.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Role represents a user role
|
||||||
|
type Role string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleAdministrator Role = "administrator"
|
||||||
|
RoleOperator Role = "operator"
|
||||||
|
RoleViewer Role = "viewer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a system user
|
||||||
|
type User struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Role Role `json:"role"`
|
||||||
|
Active bool `json:"active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuditLog represents an audit log entry
|
||||||
|
type AuditLog struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Actor string `json:"actor"` // user ID or system
|
||||||
|
Action string `json:"action"` // "pool.create", "share.delete", etc.
|
||||||
|
Resource string `json:"resource"` // resource type and ID
|
||||||
|
Result string `json:"result"` // "success", "failure"
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
UserAgent string `json:"user_agent,omitempty"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
28
internal/models/job.go
Normal file
28
internal/models/job.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// JobStatus represents the state of a job
|
||||||
|
type JobStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
JobStatusPending JobStatus = "pending"
|
||||||
|
JobStatusRunning JobStatus = "running"
|
||||||
|
JobStatusCompleted JobStatus = "completed"
|
||||||
|
JobStatusFailed JobStatus = "failed"
|
||||||
|
JobStatusCancelled JobStatus = "cancelled"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Job represents a long-running asynchronous operation
|
||||||
|
type Job struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"` // "pool_create", "snapshot_create", etc.
|
||||||
|
Status JobStatus `json:"status"`
|
||||||
|
Progress int `json:"progress"` // 0-100
|
||||||
|
Message string `json:"message"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||||
|
}
|
||||||
42
internal/models/storage.go
Normal file
42
internal/models/storage.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// SMBShare represents an SMB/CIFS share
|
||||||
|
type SMBShare struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"` // dataset mountpoint
|
||||||
|
Dataset string `json:"dataset"` // ZFS dataset name
|
||||||
|
Description string `json:"description"`
|
||||||
|
ReadOnly bool `json:"read_only"`
|
||||||
|
GuestOK bool `json:"guest_ok"`
|
||||||
|
ValidUsers []string `json:"valid_users"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NFSExport represents an NFS export
|
||||||
|
type NFSExport struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Path string `json:"path"` // dataset mountpoint
|
||||||
|
Dataset string `json:"dataset"` // ZFS dataset name
|
||||||
|
Clients []string `json:"clients"` // allowed clients (CIDR or hostnames)
|
||||||
|
ReadOnly bool `json:"read_only"`
|
||||||
|
RootSquash bool `json:"root_squash"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISCSITarget represents an iSCSI target
|
||||||
|
type ISCSITarget struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
IQN string `json:"iqn"` // iSCSI Qualified Name
|
||||||
|
LUNs []LUN `json:"luns"`
|
||||||
|
Initiators []string `json:"initiators"` // ACL list
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LUN represents a Logical Unit Number backed by a ZVOL
|
||||||
|
type LUN struct {
|
||||||
|
ID int `json:"id"` // LUN number
|
||||||
|
ZVOL string `json:"zvol"` // ZVOL name
|
||||||
|
Size uint64 `json:"size"` // bytes
|
||||||
|
Backend string `json:"backend"` // "zvol"
|
||||||
|
}
|
||||||
56
internal/models/zfs.go
Normal file
56
internal/models/zfs.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Pool represents a ZFS pool
|
||||||
|
type Pool struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"` // ONLINE, DEGRADED, FAULTED, etc.
|
||||||
|
Size uint64 `json:"size"` // bytes
|
||||||
|
Allocated uint64 `json:"allocated"`
|
||||||
|
Free uint64 `json:"free"`
|
||||||
|
Health string `json:"health"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dataset represents a ZFS filesystem
|
||||||
|
type Dataset struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Pool string `json:"pool"`
|
||||||
|
Type string `json:"type"` // filesystem, volume
|
||||||
|
Size uint64 `json:"size"`
|
||||||
|
Used uint64 `json:"used"`
|
||||||
|
Available uint64 `json:"available"`
|
||||||
|
Mountpoint string `json:"mountpoint"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZVOL represents a ZFS block device
|
||||||
|
type ZVOL struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Pool string `json:"pool"`
|
||||||
|
Size uint64 `json:"size"` // bytes
|
||||||
|
Used uint64 `json:"used"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot represents a ZFS snapshot
|
||||||
|
type Snapshot struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Dataset string `json:"dataset"`
|
||||||
|
Size uint64 `json:"size"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SnapshotPolicy defines automated snapshot rules
|
||||||
|
type SnapshotPolicy struct {
|
||||||
|
Dataset string `json:"dataset"`
|
||||||
|
Frequent int `json:"frequent"` // keep N frequent snapshots
|
||||||
|
Hourly int `json:"hourly"` // keep N hourly snapshots
|
||||||
|
Daily int `json:"daily"` // keep N daily snapshots
|
||||||
|
Weekly int `json:"weekly"` // keep N weekly snapshots
|
||||||
|
Monthly int `json:"monthly"` // keep N monthly snapshots
|
||||||
|
Yearly int `json:"yearly"` // keep N yearly snapshots
|
||||||
|
Autosnap bool `json:"autosnap"` // enable/disable
|
||||||
|
Autoprune bool `json:"autoprune"` // enable/disable
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>{{.Title}} • PlutoOS</title>
|
<title>{{.Title}} • atlasOS</title>
|
||||||
|
|
||||||
<!-- v1: Tailwind CDN (later: bundle local) -->
|
<!-- v1: Tailwind CDN (later: bundle local) -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
<header class="border-b border-slate-800 bg-slate-950/80 backdrop-blur">
|
<header class="border-b border-slate-800 bg-slate-950/80 backdrop-blur">
|
||||||
<div class="mx-auto max-w-6xl px-4 py-3 flex items-center justify-between">
|
<div class="mx-auto max-w-6xl px-4 py-3 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="h-9 w-9 rounded-lg bg-slate-800 flex items-center justify-center font-bold">P</div>
|
<div class="h-9 w-9 rounded-lg bg-slate-800 flex items-center justify-center font-bold">A</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold leading-tight">PlutoOS</div>
|
<div class="font-semibold leading-tight">atlasOS</div>
|
||||||
<div class="text-xs text-slate-400 leading-tight">Storage Controller v1</div>
|
<div class="text-xs text-slate-400 leading-tight">Storage Controller v1</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<footer class="mx-auto max-w-6xl px-4 pb-10 text-xs text-slate-500">
|
<footer class="mx-auto max-w-6xl px-4 pb-10 text-xs text-slate-500">
|
||||||
<div class="border-t border-slate-800 pt-4 flex items-center justify-between">
|
<div class="border-t border-slate-800 pt-4 flex items-center justify-between">
|
||||||
<span>PlutoOS • {{nowRFC3339}}</span>
|
<span>atlasOS • {{nowRFC3339}}</span>
|
||||||
<span>Build: {{index .Build "version"}}</span>
|
<span>Build: {{index .Build "version"}}</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-white mb-2">Dashboard</h1>
|
<h1 class="text-3xl font-bold text-white mb-2">Dashboard</h1>
|
||||||
<p class="text-slate-400">Welcome to PlutoOS Storage Controller</p>
|
<p class="text-slate-400">Welcome to atlasOS Storage Controller</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user