fix installer script
Some checks failed
CI / test-build (push) Failing after 2m14s

This commit is contained in:
2025-12-15 01:55:39 +07:00
parent c405ca27dd
commit f45c878051
3 changed files with 798 additions and 18 deletions

View File

@@ -438,7 +438,16 @@ MAINGO
chmod 755 "$INSTALL_DIR/bin/atlas-api"
chmod 755 "$INSTALL_DIR/bin/atlas-tui"
echo -e "${GREEN}Binaries built successfully${NC}"
# Create symlinks in /usr/local/bin for global access
echo -e "${GREEN}Creating symlinks for global access...${NC}"
ln -sf "$INSTALL_DIR/bin/atlas-api" /usr/local/bin/atlas-api
ln -sf "$INSTALL_DIR/bin/atlas-tui" /usr/local/bin/atlas-tui
chmod 755 /usr/local/bin/atlas-api
chmod 755 /usr/local/bin/atlas-tui
echo -e "${GREEN}Binaries built and installed successfully${NC}"
echo " Binaries available at: $INSTALL_DIR/bin/"
echo " Global commands: atlas-api, atlas-tui"
}
# Create systemd service

View File

@@ -5,13 +5,15 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
)
// App represents the TUI application
type App struct {
client *APIClient
reader *bufio.Reader
client *APIClient
reader *bufio.Reader
currentUser map[string]interface{}
}
// NewApp creates a new TUI application
@@ -47,6 +49,10 @@ func (a *App) Run() error {
a.handleSystemMenu()
case "5":
a.handleBackupMenu()
case "6":
a.handleUserMenu()
case "7":
a.handleServiceMenu()
case "0", "q", "exit":
fmt.Println("Goodbye!")
return nil
@@ -67,11 +73,30 @@ func (a *App) login() error {
username := a.readInput("Username: ")
password := a.readPassword("Password: ")
token, err := a.client.Login(username, password)
token, user, err := a.client.Login(username, password)
if err != nil {
return err
}
// Store current user info
if user != nil {
a.currentUser = user
} else {
// Fallback: get user info from API
data, err := a.client.Get("/api/v1/users")
if err == nil {
var users []map[string]interface{}
if json.Unmarshal(data, &users) == nil {
for _, u := range users {
if uname, ok := u["username"].(string); ok && uname == username {
a.currentUser = u
break
}
}
}
}
}
fmt.Println("Login successful!")
_ = token
return nil
@@ -100,6 +125,8 @@ func (a *App) showMainMenu() {
fmt.Println("3. Snapshots")
fmt.Println("4. System Information")
fmt.Println("5. Backup & Restore")
fmt.Println("6. User Management")
fmt.Println("7. Service Management")
fmt.Println("0. Exit")
fmt.Println()
}
@@ -109,9 +136,20 @@ func (a *App) handleZFSMenu() {
for {
fmt.Println("\n=== ZFS Management ===")
fmt.Println("1. List Pools")
fmt.Println("2. List Datasets")
fmt.Println("3. List ZVOLs")
fmt.Println("4. List Disks")
fmt.Println("2. Create Pool")
fmt.Println("3. Delete Pool")
fmt.Println("4. Import Pool")
fmt.Println("5. Export Pool")
fmt.Println("6. List Available Pools")
fmt.Println("7. Start Scrub")
fmt.Println("8. Get Scrub Status")
fmt.Println("9. List Datasets")
fmt.Println("10. Create Dataset")
fmt.Println("11. Delete Dataset")
fmt.Println("12. List ZVOLs")
fmt.Println("13. Create ZVOL")
fmt.Println("14. Delete ZVOL")
fmt.Println("15. List Disks")
fmt.Println("0. Back")
fmt.Println()
@@ -121,10 +159,32 @@ func (a *App) handleZFSMenu() {
case "1":
a.listPools()
case "2":
a.listDatasets()
a.createPool()
case "3":
a.listZVOLs()
a.deletePool()
case "4":
a.importPool()
case "5":
a.exportPool()
case "6":
a.listAvailablePools()
case "7":
a.startScrub()
case "8":
a.getScrubStatus()
case "9":
a.listDatasets()
case "10":
a.createDataset()
case "11":
a.deleteDataset()
case "12":
a.listZVOLs()
case "13":
a.createZVOL()
case "14":
a.deleteZVOL()
case "15":
a.listDisks()
case "0":
return
@@ -887,3 +947,672 @@ func (a *App) restoreBackup() {
}
}
}
// ===== ZFS Pool CRUD Operations =====
// createPool creates a new ZFS pool
func (a *App) createPool() {
name := a.readInput("Pool name: ")
vdevsStr := a.readInput("VDEVs (comma-separated, e.g., /dev/sdb,/dev/sdc): ")
vdevs := []string{}
if vdevsStr != "" {
vdevs = strings.Split(vdevsStr, ",")
for i := range vdevs {
vdevs[i] = strings.TrimSpace(vdevs[i])
}
}
reqBody := map[string]interface{}{
"name": name,
"vdevs": vdevs,
}
data, err := a.client.Post("/api/v1/pools", reqBody)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Pool created successfully!")
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err == nil {
if name, ok := result["name"].(string); ok {
fmt.Printf("Pool: %s\n", name)
}
}
}
// deletePool deletes a ZFS pool
func (a *App) deletePool() {
a.listPools()
name := a.readInput("Pool name to delete: ")
confirm := a.readInput("WARNING: This will destroy the pool and all data! Type 'yes' to confirm: ")
if confirm != "yes" {
fmt.Println("Deletion cancelled.")
return
}
err := a.client.Delete("/api/v1/pools/" + name)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Pool %s deleted successfully!\n", name)
}
// importPool imports a ZFS pool
func (a *App) importPool() {
a.listAvailablePools()
name := a.readInput("Pool name to import: ")
readonly := a.readInput("Import as read-only? (yes/no, default: no): ")
reqBody := map[string]interface{}{
"name": name,
}
if readonly == "yes" {
reqBody["options"] = map[string]string{
"readonly": "on",
}
}
data, err := a.client.Post("/api/v1/pools/import", reqBody)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Pool imported successfully!")
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err == nil {
if msg, ok := result["message"].(string); ok {
fmt.Printf("%s\n", msg)
}
}
}
// exportPool exports a ZFS pool
func (a *App) exportPool() {
a.listPools()
name := a.readInput("Pool name to export: ")
forceStr := a.readInput("Force export? (yes/no, default: no): ")
force := forceStr == "yes"
reqBody := map[string]interface{}{
"force": force,
}
data, err := a.client.Post("/api/v1/pools/"+name+"/export", reqBody)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Pool exported successfully!")
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err == nil {
if msg, ok := result["message"].(string); ok {
fmt.Printf("%s\n", msg)
}
}
}
// listAvailablePools lists pools available for import
func (a *App) listAvailablePools() {
data, err := a.client.Get("/api/v1/pools/available")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
fmt.Printf("Error parsing response: %v\n", err)
return
}
fmt.Println("\n=== Available Pools (for import) ===")
if pools, ok := result["pools"].([]interface{}); ok {
if len(pools) == 0 {
fmt.Println("No pools available for import.")
return
}
for i, pool := range pools {
fmt.Printf("%d. %v\n", i+1, pool)
}
} else {
fmt.Println("No pools available for import.")
}
}
// startScrub starts a scrub operation on a pool
func (a *App) startScrub() {
a.listPools()
name := a.readInput("Pool name to scrub: ")
data, err := a.client.Post("/api/v1/pools/"+name+"/scrub", map[string]interface{}{})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Scrub started successfully!")
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err == nil {
if msg, ok := result["message"].(string); ok {
fmt.Printf("%s\n", msg)
}
}
}
// getScrubStatus gets scrub status for a pool
func (a *App) getScrubStatus() {
a.listPools()
name := a.readInput("Pool name: ")
data, err := a.client.Get("/api/v1/pools/" + name + "/scrub")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
var status map[string]interface{}
if err := json.Unmarshal(data, &status); err != nil {
fmt.Printf("Error parsing response: %v\n", err)
return
}
fmt.Println("\n=== Scrub Status ===")
if state, ok := status["state"].(string); ok {
fmt.Printf("State: %s\n", state)
}
if progress, ok := status["progress"].(float64); ok {
fmt.Printf("Progress: %.2f%%\n", progress)
}
if elapsed, ok := status["elapsed"].(string); ok {
fmt.Printf("Elapsed: %s\n", elapsed)
}
if remaining, ok := status["remaining"].(string); ok {
fmt.Printf("Remaining: %s\n", remaining)
}
if speed, ok := status["speed"].(string); ok {
fmt.Printf("Speed: %s\n", speed)
}
if errors, ok := status["errors"].(float64); ok {
fmt.Printf("Errors: %.0f\n", errors)
}
}
// createDataset creates a new dataset
func (a *App) createDataset() {
name := a.readInput("Dataset name (e.g., pool/dataset): ")
quota := a.readInput("Quota (optional, e.g., 10G): ")
compression := a.readInput("Compression (optional, e.g., lz4, zstd): ")
reqBody := map[string]interface{}{
"name": name,
}
if quota != "" {
reqBody["quota"] = quota
}
if compression != "" {
reqBody["compression"] = compression
}
data, err := a.client.Post("/api/v1/datasets", reqBody)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Dataset created successfully!")
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err == nil {
if name, ok := result["name"].(string); ok {
fmt.Printf("Dataset: %s\n", name)
}
}
}
// deleteDataset deletes a dataset
func (a *App) deleteDataset() {
a.listDatasets()
name := a.readInput("Dataset name to delete: ")
confirm := a.readInput("Delete dataset? (yes/no): ")
if confirm != "yes" {
fmt.Println("Deletion cancelled.")
return
}
err := a.client.Delete("/api/v1/datasets/" + name)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Dataset %s deleted successfully!\n", name)
}
// createZVOL creates a new ZVOL
func (a *App) createZVOL() {
name := a.readInput("ZVOL name (e.g., pool/zvol): ")
sizeStr := a.readInput("Size (e.g., 10G): ")
reqBody := map[string]interface{}{
"name": name,
"size": sizeStr,
}
data, err := a.client.Post("/api/v1/zvols", reqBody)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("ZVOL created successfully!")
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err == nil {
if name, ok := result["name"].(string); ok {
fmt.Printf("ZVOL: %s\n", name)
}
}
}
// deleteZVOL deletes a ZVOL
func (a *App) deleteZVOL() {
a.listZVOLs()
name := a.readInput("ZVOL name to delete: ")
confirm := a.readInput("Delete ZVOL? (yes/no): ")
if confirm != "yes" {
fmt.Println("Deletion cancelled.")
return
}
err := a.client.Delete("/api/v1/zvols/" + name)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("ZVOL %s deleted successfully!\n", name)
}
// ===== User Management =====
// handleUserMenu handles user management menu
func (a *App) handleUserMenu() {
for {
fmt.Println("\n=== User Management ===")
fmt.Println("1. List Users")
fmt.Println("2. Create User")
fmt.Println("3. Update User")
fmt.Println("4. Delete User")
fmt.Println("5. Change Password")
fmt.Println("0. Back")
fmt.Println()
choice := a.readInput("Select option: ")
switch choice {
case "1":
a.listUsers()
case "2":
a.createUser()
case "3":
a.updateUser()
case "4":
a.deleteUser()
case "5":
a.changePassword()
case "0":
return
default:
fmt.Println("Invalid option.")
}
}
}
// listUsers lists all users
func (a *App) listUsers() {
data, err := a.client.Get("/api/v1/users")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
var users []map[string]interface{}
if err := json.Unmarshal(data, &users); err != nil {
fmt.Printf("Error parsing response: %v\n", err)
return
}
fmt.Println("\n=== Users ===")
if len(users) == 0 {
fmt.Println("No users found.")
return
}
for i, user := range users {
fmt.Printf("%d. %s\n", i+1, user["username"])
if email, ok := user["email"].(string); ok && email != "" {
fmt.Printf(" Email: %s\n", email)
}
if role, ok := user["role"].(string); ok {
fmt.Printf(" Role: %s\n", role)
}
if active, ok := user["active"].(bool); ok {
fmt.Printf(" Active: %v\n", active)
}
fmt.Println()
}
}
// createUser creates a new user
func (a *App) createUser() {
username := a.readInput("Username: ")
email := a.readInput("Email (optional): ")
password := a.readPassword("Password: ")
role := a.readInput("Role (Administrator/Operator/Viewer, default: Viewer): ")
if role == "" {
role = "Viewer"
}
reqBody := map[string]interface{}{
"username": username,
"password": password,
"role": role,
}
if email != "" {
reqBody["email"] = email
}
data, err := a.client.Post("/api/v1/users", reqBody)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("User created successfully!")
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err == nil {
if username, ok := result["username"].(string); ok {
fmt.Printf("User: %s\n", username)
}
}
}
// updateUser updates a user
func (a *App) updateUser() {
a.listUsers()
userID := a.readInput("User ID: ")
email := a.readInput("New email (press Enter to skip): ")
role := a.readInput("New role (Administrator/Operator/Viewer, press Enter to skip): ")
activeStr := a.readInput("Active (true/false, press Enter to skip): ")
reqBody := map[string]interface{}{}
if email != "" {
reqBody["email"] = email
}
if role != "" {
reqBody["role"] = role
}
if activeStr != "" {
reqBody["active"] = activeStr == "true"
}
if len(reqBody) == 0 {
fmt.Println("No changes specified.")
return
}
data, err := a.client.Put("/api/v1/users/"+userID, reqBody)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("User updated successfully!")
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err == nil {
if username, ok := result["username"].(string); ok {
fmt.Printf("User: %s\n", username)
}
}
}
// deleteUser deletes a user
func (a *App) deleteUser() {
a.listUsers()
userID := a.readInput("User ID to delete: ")
confirm := a.readInput("Delete user? (yes/no): ")
if confirm != "yes" {
fmt.Println("Deletion cancelled.")
return
}
err := a.client.Delete("/api/v1/users/" + userID)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("User %s deleted successfully!\n", userID)
}
// changePassword changes the current user's password
func (a *App) changePassword() {
// Get user ID from current user or ask for username
var userID string
if a.currentUser != nil {
if id, ok := a.currentUser["id"].(string); ok {
userID = id
}
}
if userID == "" {
// Fallback: ask for username and look it up
username := a.readInput("Your username: ")
data, err := a.client.Get("/api/v1/users")
if err != nil {
fmt.Printf("Error getting users: %v\n", err)
return
}
var users []map[string]interface{}
if err := json.Unmarshal(data, &users); err != nil {
fmt.Printf("Error parsing users: %v\n", err)
return
}
for _, user := range users {
if u, ok := user["username"].(string); ok && u == username {
if id, ok := user["id"].(string); ok {
userID = id
break
}
}
}
if userID == "" {
fmt.Printf("User '%s' not found.\n", username)
return
}
}
oldPassword := a.readPassword("Current password: ")
newPassword := a.readPassword("New password: ")
confirmPassword := a.readPassword("Confirm new password: ")
if newPassword != confirmPassword {
fmt.Println("Passwords do not match.")
return
}
reqBody := map[string]interface{}{
"old_password": oldPassword,
"new_password": newPassword,
}
data, err := a.client.Put("/api/v1/users/"+userID+"/password", reqBody)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Password changed successfully!")
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err == nil {
if msg, ok := result["message"].(string); ok {
fmt.Printf("%s\n", msg)
}
}
}
// ===== Service Management =====
// handleServiceMenu handles service management menu
func (a *App) handleServiceMenu() {
for {
fmt.Println("\n=== Service Management ===")
fmt.Println("1. Service Status")
fmt.Println("2. Start Service")
fmt.Println("3. Stop Service")
fmt.Println("4. Restart Service")
fmt.Println("5. Reload Service")
fmt.Println("6. View Service Logs")
fmt.Println("0. Back")
fmt.Println()
choice := a.readInput("Select option: ")
switch choice {
case "1":
a.serviceStatus()
case "2":
a.serviceStart()
case "3":
a.serviceStop()
case "4":
a.serviceRestart()
case "5":
a.serviceReload()
case "6":
a.serviceLogs()
case "0":
return
default:
fmt.Println("Invalid option.")
}
}
}
// serviceStatus shows service status
func (a *App) serviceStatus() {
fmt.Println("\n=== Service Status ===")
fmt.Println("Checking atlas-api service status...")
// Use systemctl to check status
cmd := exec.Command("systemctl", "status", "atlas-api", "--no-pager")
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Error checking status: %v\n", err)
fmt.Println(string(output))
return
}
fmt.Println(string(output))
}
// serviceStart starts the service
func (a *App) serviceStart() {
fmt.Println("\n=== Start Service ===")
cmd := exec.Command("systemctl", "start", "atlas-api")
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Error starting service: %v\n", err)
fmt.Println(string(output))
return
}
fmt.Println("Service started successfully!")
}
// serviceStop stops the service
func (a *App) serviceStop() {
fmt.Println("\n=== Stop Service ===")
cmd := exec.Command("systemctl", "stop", "atlas-api")
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Error stopping service: %v\n", err)
fmt.Println(string(output))
return
}
fmt.Println("Service stopped successfully!")
}
// serviceRestart restarts the service
func (a *App) serviceRestart() {
fmt.Println("\n=== Restart Service ===")
cmd := exec.Command("systemctl", "restart", "atlas-api")
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Error restarting service: %v\n", err)
fmt.Println(string(output))
return
}
fmt.Println("Service restarted successfully!")
}
// serviceReload reloads the service
func (a *App) serviceReload() {
fmt.Println("\n=== Reload Service ===")
cmd := exec.Command("systemctl", "reload", "atlas-api")
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Error reloading service: %v\n", err)
fmt.Println(string(output))
return
}
fmt.Println("Service reloaded successfully!")
}
// serviceLogs shows service logs
func (a *App) serviceLogs() {
linesStr := a.readInput("Number of log lines (default: 50): ")
lines := "50"
if linesStr != "" {
lines = linesStr
}
fmt.Printf("\n=== Service Logs (last %s lines) ===\n", lines)
cmd := exec.Command("journalctl", "-u", "atlas-api", "-n", lines, "--no-pager")
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Error viewing logs: %v\n", err)
fmt.Println(string(output))
return
}
fmt.Println(string(output))
}

View File

@@ -31,8 +31,8 @@ func (c *APIClient) SetToken(token string) {
c.token = token
}
// Login authenticates with the API
func (c *APIClient) Login(username, password string) (string, error) {
// Login authenticates with the API and returns user info
func (c *APIClient) Login(username, password string) (string, map[string]interface{}, error) {
reqBody := map[string]string{
"username": username,
"password": password,
@@ -40,39 +40,46 @@ func (c *APIClient) Login(username, password string) (string, error) {
body, err := json.Marshal(reqBody)
if err != nil {
return "", err
return "", nil, err
}
req, err := http.NewRequest("POST", c.baseURL+"/api/v1/auth/login", bytes.NewReader(body))
if err != nil {
return "", err
return "", nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("request failed: %w", err)
return "", nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("login failed: %s", string(body))
return "", nil, fmt.Errorf("login failed: %s", string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
return "", nil, err
}
token, ok := result["token"].(string)
if !ok {
return "", fmt.Errorf("no token in response")
return "", nil, fmt.Errorf("no token in response")
}
c.SetToken(token)
return token, nil
// Extract user info if available
var user map[string]interface{}
if u, ok := result["user"].(map[string]interface{}); ok {
user = u
}
return token, user, nil
}
// Get performs a GET request
@@ -163,3 +170,38 @@ func (c *APIClient) Delete(path string) error {
return nil
}
// Put performs a PUT request
func (c *APIClient) Put(path string, data interface{}) ([]byte, error) {
body, err := json.Marshal(data)
if err != nil {
return nil, err
}
req, err := http.NewRequest("PUT", c.baseURL+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(respBody))
}
return respBody, nil
}