Compare commits

..

3 Commits

Author SHA1 Message Date
CanbiZ (MickLesk)
1b9feadfbc Update frontend/public/json/sqlserver2025.json
Co-authored-by: Tobias <96661824+CrazyWolf13@users.noreply.github.com>
2026-02-12 20:13:06 +01:00
CanbiZ (MickLesk)
10938cb3c2 Update warning note for SQL Server SQLPAL compatibility 2026-02-12 13:07:43 +01:00
CanbiZ (MickLesk)
524ad21c26 docs(sqlserver2025): add PVE9/Kernel 6.x incompatibility warning 2026-02-12 13:06:23 +01:00
51 changed files with 1192 additions and 2631 deletions

View File

@@ -401,98 +401,8 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details> </details>
## 2026-02-14
### 🚀 Updated Scripts
- #### ✨ New Features
- core: overwriteable app version [@CrazyWolf13](https://github.com/CrazyWolf13) ([#11753](https://github.com/community-scripts/ProxmoxVE/pull/11753))
### 💾 Core
- #### ✨ New Features
- core: validate container IDs cluster-wide across all nodes [@MickLesk](https://github.com/MickLesk) ([#11906](https://github.com/community-scripts/ProxmoxVE/pull/11906))
- core: improve error reporting with structured error strings and better categorization + output formatting [@MickLesk](https://github.com/MickLesk) ([#11907](https://github.com/community-scripts/ProxmoxVE/pull/11907))
- core: unified logging system with combined logs [@MickLesk](https://github.com/MickLesk) ([#11761](https://github.com/community-scripts/ProxmoxVE/pull/11761))
### 🧰 Tools
- lxc-updater: add patchmon aware [@failure101](https://github.com/failure101) ([#11905](https://github.com/community-scripts/ProxmoxVE/pull/11905))
### ❔ Uncategorized
- Disable UniFi script - APT packages no longer available [@Copilot](https://github.com/Copilot) ([#11898](https://github.com/community-scripts/ProxmoxVE/pull/11898))
## 2026-02-13
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- OpenWebUI: pin numba constraint [@MickLesk](https://github.com/MickLesk) ([#11874](https://github.com/community-scripts/ProxmoxVE/pull/11874))
- Planka: add migrate step to update function [@ZimmermannLeon](https://github.com/ZimmermannLeon) ([#11877](https://github.com/community-scripts/ProxmoxVE/pull/11877))
- Pangolin: switch sqlite-specific back to generic [@MickLesk](https://github.com/MickLesk) ([#11868](https://github.com/community-scripts/ProxmoxVE/pull/11868))
- [Hotfix] Jotty: Copy contents of config backup into /opt/jotty/config [@vhsdream](https://github.com/vhsdream) ([#11864](https://github.com/community-scripts/ProxmoxVE/pull/11864))
- #### 🔧 Refactor
- Refactor: Radicale [@vhsdream](https://github.com/vhsdream) ([#11850](https://github.com/community-scripts/ProxmoxVE/pull/11850))
- chore(donetick): add config entry for v0.1.73 [@tomfrenzel](https://github.com/tomfrenzel) ([#11872](https://github.com/community-scripts/ProxmoxVE/pull/11872))
### 💾 Core
- #### 🔧 Refactor
- core: retry reporting with fallback payloads [@MickLesk](https://github.com/MickLesk) ([#11885](https://github.com/community-scripts/ProxmoxVE/pull/11885))
### 📡 API
- #### ✨ New Features
- error-handler: Implement json_escape and enhance error handling [@MickLesk](https://github.com/MickLesk) ([#11875](https://github.com/community-scripts/ProxmoxVE/pull/11875))
### 🌐 Website
- #### 📝 Script Information
- SQLServer-2025: add PVE9/Kernel 6.x incompatibility warning [@MickLesk](https://github.com/MickLesk) ([#11829](https://github.com/community-scripts/ProxmoxVE/pull/11829))
## 2026-02-12 ## 2026-02-12
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- EMQX: increase disk to 6GB and add optional MQ disable prompt [@MickLesk](https://github.com/MickLesk) ([#11844](https://github.com/community-scripts/ProxmoxVE/pull/11844))
- Increased the Grafana container default disk size. [@shtefko](https://github.com/shtefko) ([#11840](https://github.com/community-scripts/ProxmoxVE/pull/11840))
- Pangolin: Update database generation command in install script [@tremor021](https://github.com/tremor021) ([#11825](https://github.com/community-scripts/ProxmoxVE/pull/11825))
- Deluge: add python3-setuptools as dep [@MickLesk](https://github.com/MickLesk) ([#11833](https://github.com/community-scripts/ProxmoxVE/pull/11833))
- Dispatcharr: migrate to uv sync [@MickLesk](https://github.com/MickLesk) ([#11831](https://github.com/community-scripts/ProxmoxVE/pull/11831))
- #### ✨ New Features
- Archlinux-VM: fix LVM/LVM-thin storage and improve error reporting | VM's add correct exit_code for analytics [@MickLesk](https://github.com/MickLesk) ([#11842](https://github.com/community-scripts/ProxmoxVE/pull/11842))
- Debian13-VM: Optimize First Boot & add noCloud/Cloud Selection [@MickLesk](https://github.com/MickLesk) ([#11810](https://github.com/community-scripts/ProxmoxVE/pull/11810))
### 💾 Core
- #### ✨ New Features
- tools.func: auto-detect binary vs armored GPG keys in setup_deb822_repo [@MickLesk](https://github.com/MickLesk) ([#11841](https://github.com/community-scripts/ProxmoxVE/pull/11841))
- core: remove old Go API and extend misc/api.func with new backend [@MickLesk](https://github.com/MickLesk) ([#11822](https://github.com/community-scripts/ProxmoxVE/pull/11822))
- #### 🔧 Refactor
- error_handler: prevent stuck 'installing' status [@MickLesk](https://github.com/MickLesk) ([#11845](https://github.com/community-scripts/ProxmoxVE/pull/11845))
### 🧰 Tools
- #### 🐞 Bug Fixes
- Tailscale: fix DNS check and keyrings directory issues [@MickLesk](https://github.com/MickLesk) ([#11837](https://github.com/community-scripts/ProxmoxVE/pull/11837))
## 2026-02-11 ## 2026-02-11
### 🆕 New Scripts ### 🆕 New Scripts

5
api/.env.example Normal file
View File

@@ -0,0 +1,5 @@
MONGO_USER=
MONGO_PASSWORD=
MONGO_IP=
MONGO_PORT=
MONGO_DATABASE=

23
api/go.mod Normal file
View File

@@ -0,0 +1,23 @@
module proxmox-api
go 1.24.0
require (
github.com/gorilla/mux v1.8.1
github.com/joho/godotenv v1.5.1
github.com/rs/cors v1.11.1
go.mongodb.org/mongo-driver v1.17.2
)
require (
github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.31.0 // indirect
)

56
api/go.sum Normal file
View File

@@ -0,0 +1,56 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

450
api/main.go Normal file
View File

@@ -0,0 +1,450 @@
// Copyright (c) 2021-2026 community-scripts ORG
// Author: Michel Roegl-Brunner (michelroegl-brunner)
// License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
"github.com/rs/cors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var client *mongo.Client
var collection *mongo.Collection
func loadEnv() {
if err := godotenv.Load(); err != nil {
log.Fatal("Error loading .env file")
}
}
// DataModel represents a single document in MongoDB
type DataModel struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
CT_TYPE uint `json:"ct_type" bson:"ct_type"`
DISK_SIZE float32 `json:"disk_size" bson:"disk_size"`
CORE_COUNT uint `json:"core_count" bson:"core_count"`
RAM_SIZE uint `json:"ram_size" bson:"ram_size"`
OS_TYPE string `json:"os_type" bson:"os_type"`
OS_VERSION string `json:"os_version" bson:"os_version"`
DISABLEIP6 string `json:"disableip6" bson:"disableip6"`
NSAPP string `json:"nsapp" bson:"nsapp"`
METHOD string `json:"method" bson:"method"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
PVEVERSION string `json:"pve_version" bson:"pve_version"`
STATUS string `json:"status" bson:"status"`
RANDOM_ID string `json:"random_id" bson:"random_id"`
TYPE string `json:"type" bson:"type"`
ERROR string `json:"error" bson:"error"`
}
type StatusModel struct {
RANDOM_ID string `json:"random_id" bson:"random_id"`
ERROR string `json:"error" bson:"error"`
STATUS string `json:"status" bson:"status"`
}
type CountResponse struct {
TotalEntries int64 `json:"total_entries"`
StatusCount map[string]int64 `json:"status_count"`
NSAPPCount map[string]int64 `json:"nsapp_count"`
}
// ConnectDatabase initializes the MongoDB connection
func ConnectDatabase() {
loadEnv()
mongoURI := fmt.Sprintf("mongodb://%s:%s@%s:%s",
os.Getenv("MONGO_USER"),
os.Getenv("MONGO_PASSWORD"),
os.Getenv("MONGO_IP"),
os.Getenv("MONGO_PORT"))
database := os.Getenv("MONGO_DATABASE")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var err error
client, err = mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
if err != nil {
log.Fatal("Failed to connect to MongoDB!", err)
}
collection = client.Database(database).Collection("data_models")
fmt.Println("Connected to MongoDB on 10.10.10.18")
}
// UploadJSON handles API requests and stores data as a document in MongoDB
func UploadJSON(w http.ResponseWriter, r *http.Request) {
var input DataModel
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
input.CreatedAt = time.Now()
_, err := collection.InsertOne(context.Background(), input)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Println("Received data:", input)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"message": "Data saved successfully"})
}
// UpdateStatus updates the status of a record based on RANDOM_ID
func UpdateStatus(w http.ResponseWriter, r *http.Request) {
var input StatusModel
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
filter := bson.M{"random_id": input.RANDOM_ID}
update := bson.M{"$set": bson.M{"status": input.STATUS, "error": input.ERROR}}
_, err := collection.UpdateOne(context.Background(), filter, update)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Println("Updated data:", input)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "Record updated successfully"})
}
// GetDataJSON fetches all data from MongoDB
func GetDataJSON(w http.ResponseWriter, r *http.Request) {
var records []DataModel
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cursor, err := collection.Find(ctx, bson.M{})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var record DataModel
if err := cursor.Decode(&record); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
records = append(records, record)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(records)
}
func GetPaginatedData(w http.ResponseWriter, r *http.Request) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if page < 1 {
page = 1
}
if limit < 1 {
limit = 10
}
skip := (page - 1) * limit
var records []DataModel
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
options := options.Find().SetSkip(int64(skip)).SetLimit(int64(limit))
cursor, err := collection.Find(ctx, bson.M{}, options)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var record DataModel
if err := cursor.Decode(&record); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
records = append(records, record)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(records)
}
func GetSummary(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
totalCount, err := collection.CountDocuments(ctx, bson.M{})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
statusCount := make(map[string]int64)
nsappCount := make(map[string]int64)
pipeline := []bson.M{
{"$group": bson.M{"_id": "$status", "count": bson.M{"$sum": 1}}},
}
cursor, err := collection.Aggregate(ctx, pipeline)
if err == nil {
for cursor.Next(ctx) {
var result struct {
ID string `bson:"_id"`
Count int64 `bson:"count"`
}
if err := cursor.Decode(&result); err == nil {
statusCount[result.ID] = result.Count
}
}
}
pipeline = []bson.M{
{"$group": bson.M{"_id": "$nsapp", "count": bson.M{"$sum": 1}}},
}
cursor, err = collection.Aggregate(ctx, pipeline)
if err == nil {
for cursor.Next(ctx) {
var result struct {
ID string `bson:"_id"`
Count int64 `bson:"count"`
}
if err := cursor.Decode(&result); err == nil {
nsappCount[result.ID] = result.Count
}
}
}
response := CountResponse{
TotalEntries: totalCount,
StatusCount: statusCount,
NSAPPCount: nsappCount,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func GetByNsapp(w http.ResponseWriter, r *http.Request) {
nsapp := r.URL.Query().Get("nsapp")
var records []DataModel
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cursor, err := collection.Find(ctx, bson.M{"nsapp": nsapp})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var record DataModel
if err := cursor.Decode(&record); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
records = append(records, record)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(records)
}
func GetByDateRange(w http.ResponseWriter, r *http.Request) {
startDate := r.URL.Query().Get("start_date")
endDate := r.URL.Query().Get("end_date")
if startDate == "" || endDate == "" {
http.Error(w, "Both start_date and end_date are required", http.StatusBadRequest)
return
}
start, err := time.Parse("2006-01-02T15:04:05.999999+00:00", startDate+"T00:00:00+00:00")
if err != nil {
http.Error(w, "Invalid start_date format", http.StatusBadRequest)
return
}
end, err := time.Parse("2006-01-02T15:04:05.999999+00:00", endDate+"T23:59:59+00:00")
if err != nil {
http.Error(w, "Invalid end_date format", http.StatusBadRequest)
return
}
var records []DataModel
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cursor, err := collection.Find(ctx, bson.M{
"created_at": bson.M{
"$gte": start,
"$lte": end,
},
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var record DataModel
if err := cursor.Decode(&record); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
records = append(records, record)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(records)
}
func GetByStatus(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
var records []DataModel
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cursor, err := collection.Find(ctx, bson.M{"status": status})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var record DataModel
if err := cursor.Decode(&record); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
records = append(records, record)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(records)
}
func GetByOS(w http.ResponseWriter, r *http.Request) {
osType := r.URL.Query().Get("os_type")
osVersion := r.URL.Query().Get("os_version")
var records []DataModel
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cursor, err := collection.Find(ctx, bson.M{"os_type": osType, "os_version": osVersion})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var record DataModel
if err := cursor.Decode(&record); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
records = append(records, record)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(records)
}
func GetErrors(w http.ResponseWriter, r *http.Request) {
errorCount := make(map[string]int)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cursor, err := collection.Find(ctx, bson.M{"error": bson.M{"$ne": ""}})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var record DataModel
if err := cursor.Decode(&record); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if record.ERROR != "" {
errorCount[record.ERROR]++
}
}
type ErrorCountResponse struct {
Error string `json:"error"`
Count int `json:"count"`
}
var errorCounts []ErrorCountResponse
for err, count := range errorCount {
errorCounts = append(errorCounts, ErrorCountResponse{
Error: err,
Count: count,
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(struct {
ErrorCounts []ErrorCountResponse `json:"error_counts"`
}{
ErrorCounts: errorCounts,
})
}
func main() {
ConnectDatabase()
router := mux.NewRouter()
router.HandleFunc("/upload", UploadJSON).Methods("POST")
router.HandleFunc("/upload/updatestatus", UpdateStatus).Methods("POST")
router.HandleFunc("/data/json", GetDataJSON).Methods("GET")
router.HandleFunc("/data/paginated", GetPaginatedData).Methods("GET")
router.HandleFunc("/data/summary", GetSummary).Methods("GET")
router.HandleFunc("/data/nsapp", GetByNsapp).Methods("GET")
router.HandleFunc("/data/date", GetByDateRange).Methods("GET")
router.HandleFunc("/data/status", GetByStatus).Methods("GET")
router.HandleFunc("/data/os", GetByOS).Methods("GET")
router.HandleFunc("/data/errors", GetErrors).Methods("GET")
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
})
handler := c.Handler(router)
fmt.Println("Server running on port 8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}

View File

@@ -9,7 +9,7 @@ APP="Alpine-Grafana"
var_tags="${var_tags:-alpine;monitoring}" var_tags="${var_tags:-alpine;monitoring}"
var_cpu="${var_cpu:-1}" var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-256}" var_ram="${var_ram:-256}"
var_disk="${var_disk:-2}" var_disk="${var_disk:-1}"
var_os="${var_os:-alpine}" var_os="${var_os:-alpine}"
var_version="${var_version:-3.23}" var_version="${var_version:-3.23}"
var_unprivileged="${var_unprivileged:-1}" var_unprivileged="${var_unprivileged:-1}"

View File

@@ -28,7 +28,6 @@ function update_script() {
exit exit
fi fi
msg_info "Updating Deluge" msg_info "Updating Deluge"
ensure_dependencies python3-setuptools
$STD apt update $STD apt update
$STD pip3 install deluge[all] --upgrade $STD pip3 install deluge[all] --upgrade
msg_ok "Updated Deluge" msg_ok "Updated Deluge"

View File

@@ -104,7 +104,7 @@ function update_script() {
cd /opt/dispatcharr cd /opt/dispatcharr
rm -rf .venv rm -rf .venv
$STD uv venv --clear $STD uv venv --clear
$STD uv sync $STD uv pip install -r requirements.txt --index-strategy unsafe-best-match
$STD uv pip install gunicorn gevent celery redis daphne $STD uv pip install gunicorn gevent celery redis daphne
msg_ok "Updated Dispatcharr Backend" msg_ok "Updated Dispatcharr Backend"

View File

@@ -42,8 +42,7 @@ function update_script() {
msg_info "Restoring Configurations" msg_info "Restoring Configurations"
mv /opt/selfhosted.yaml /opt/donetick/config mv /opt/selfhosted.yaml /opt/donetick/config
grep -q 'http://localhost"$' /opt/donetick/config/selfhosted.yaml || sed -i '/https:\/\/localhost"$/a\ - "http://localhost"' /opt/donetick/config/selfhosted.yaml sed -i '/capacitor:\/\/localhost/d' /opt/donetick/config/selfhosted.yaml
grep -q 'capacitor://localhost' /opt/donetick/config/selfhosted.yaml || sed -i '/http:\/\/localhost"$/a\ - "capacitor://localhost"' /opt/donetick/config/selfhosted.yaml
mv /opt/donetick.db /opt/donetick mv /opt/donetick.db /opt/donetick
msg_ok "Restored Configurations" msg_ok "Restored Configurations"

View File

@@ -9,7 +9,7 @@ APP="EMQX"
var_tags="${var_tags:-mqtt}" var_tags="${var_tags:-mqtt}"
var_cpu="${var_cpu:-2}" var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-1024}" var_ram="${var_ram:-1024}"
var_disk="${var_disk:-6}" var_disk="${var_disk:-4}"
var_os="${var_os:-debian}" var_os="${var_os:-debian}"
var_version="${var_version:-13}" var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}" var_unprivileged="${var_unprivileged:-1}"

View File

@@ -9,7 +9,7 @@ APP="Grafana"
var_tags="${var_tags:-monitoring;visualization}" var_tags="${var_tags:-monitoring;visualization}"
var_cpu="${var_cpu:-1}" var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-512}" var_ram="${var_ram:-512}"
var_disk="${var_disk:-4}" var_disk="${var_disk:-2}"
var_os="${var_os:-debian}" var_os="${var_os:-debian}"
var_version="${var_version:-13}" var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}" var_unprivileged="${var_unprivileged:-1}"

View File

@@ -46,7 +46,7 @@ function update_script() {
msg_info "Restoring configuration & data" msg_info "Restoring configuration & data"
mv /opt/app.env /opt/jotty/.env mv /opt/app.env /opt/jotty/.env
[[ -d /opt/data ]] && mv /opt/data /opt/jotty/data [[ -d /opt/data ]] && mv /opt/data /opt/jotty/data
[[ -d /opt/jotty/config ]] && cp -a /opt/config/* /opt/jotty/config && rm -rf /opt/config [[ -d /opt/jotty/config ]] && mv /opt/config/* /opt/jotty/config
msg_ok "Restored configuration & data" msg_ok "Restored configuration & data"
msg_info "Starting Service" msg_info "Starting Service"

View File

@@ -44,7 +44,7 @@ function update_script() {
msg_info "Installing uv-based Open-WebUI" msg_info "Installing uv-based Open-WebUI"
PYTHON_VERSION="3.12" setup_uv PYTHON_VERSION="3.12" setup_uv
$STD uv tool install --python 3.12 --constraint <(echo "numba>=0.60") open-webui[all] $STD uv tool install --python 3.12 open-webui[all]
msg_ok "Installed uv-based Open-WebUI" msg_ok "Installed uv-based Open-WebUI"
msg_info "Restoring data" msg_info "Restoring data"
@@ -126,7 +126,7 @@ EOF
msg_info "Updating Open WebUI via uv" msg_info "Updating Open WebUI via uv"
PYTHON_VERSION="3.12" setup_uv PYTHON_VERSION="3.12" setup_uv
$STD uv tool install --force --python 3.12 --constraint <(echo "numba>=0.60") open-webui[all] $STD uv tool upgrade --python 3.12 open-webui[all]
systemctl restart open-webui systemctl restart open-webui
msg_ok "Updated Open WebUI" msg_ok "Updated Open WebUI"
msg_ok "Updated successfully!" msg_ok "Updated successfully!"

View File

@@ -51,7 +51,7 @@ function update_script() {
$STD npm run db:generate $STD npm run db:generate
$STD npm run build $STD npm run build
$STD npm run build:cli $STD npm run build:cli
$STD npm run db:push $STD npm run db:sqlite:push
cp -R .next/standalone ./ cp -R .next/standalone ./
chmod +x ./dist/cli.mjs chmod +x ./dist/cli.mjs
cp server/db/names.json ./dist/names.json cp server/db/names.json ./dist/names.json

View File

@@ -61,12 +61,6 @@ function update_script() {
rm -rf "$BK" rm -rf "$BK"
msg_ok "Restored data" msg_ok "Restored data"
msg_ok "Migrate Database"
cd /opt/planka
$STD npm run db:upgrade
$STD npm run db:migrate
msg_ok "Migrated Database"
msg_info "Starting Service" msg_info "Starting Service"
systemctl start planka systemctl start planka
msg_ok "Started Service" msg_ok "Started Service"

View File

@@ -28,55 +28,16 @@ function update_script() {
exit exit
fi fi
if check_for_gh_release "Radicale" "Kozea/Radicale"; then msg_info "Updating ${APP}"
msg_info "Stopping service" $STD python3 -m venv /opt/radicale
systemctl stop radicale source /opt/radicale/bin/activate
msg_ok "Stopped service" $STD python3 -m pip install --upgrade https://github.com/Kozea/Radicale/archive/master.tar.gz
msg_ok "Updated ${APP}"
msg_info "Backing up users file" msg_info "Starting Service"
cp /opt/radicale/users /opt/radicale_users_backup systemctl enable -q --now radicale
msg_ok "Backed up users file" msg_ok "Started Service"
msg_ok "Updated successfully!"
PYTHON_VERSION="3.13" setup_uv
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Radicale" "Kozea/Radicale" "tarball" "latest" "/opt/radicale"
msg_info "Restoring users file"
rm -f /opt/radicale/users
mv /opt/radicale_users_backup /opt/radicale/users
msg_ok "Restored users file"
if grep -q 'start.sh' /etc/systemd/system/radicale.service; then
sed -i -e '/^Description/i[Unit]' \
-e '\|^ExecStart|iWorkingDirectory=/opt/radicale' \
-e 's|^ExecStart=.*|ExecStart=/usr/local/bin/uv run -m radicale --config /etc/radicale/config|' /etc/systemd/system/radicale.service
systemctl daemon-reload
fi
if [[ ! -f /etc/radicale/config ]]; then
msg_info "Migrating to config file (/etc/radicale/config)"
mkdir -p /etc/radicale
cat <<EOF >/etc/radicale/config
[server]
hosts = 0.0.0.0:5232
[auth]
type = htpasswd
htpasswd_filename = /opt/radicale/users
htpasswd_encryption = sha512
[storage]
type = multifilesystem
filesystem_folder = /var/lib/radicale/collections
[web]
type = internal
EOF
msg_ok "Migrated to config (/etc/radicale/config)"
fi
msg_info "Starting service"
systemctl start radicale
msg_ok "Started service"
msg_ok "Updated Successfully!"
fi
exit exit
} }

View File

@@ -21,7 +21,7 @@
"resources": { "resources": {
"cpu": 2, "cpu": 2,
"ram": 1024, "ram": 1024,
"hdd": 6, "hdd": 4,
"os": "debian", "os": "debian",
"version": "13" "version": "13"
} }

View File

@@ -1,5 +1,5 @@
{ {
"generated": "2026-02-14T12:08:41Z", "generated": "2026-02-12T06:25:40Z",
"versions": [ "versions": [
{ {
"slug": "2fauth", "slug": "2fauth",
@@ -67,9 +67,9 @@
{ {
"slug": "autobrr", "slug": "autobrr",
"repo": "autobrr/autobrr", "repo": "autobrr/autobrr",
"version": "v1.73.0", "version": "v1.72.1",
"pinned": false, "pinned": false,
"date": "2026-02-13T16:37:28Z" "date": "2026-01-30T12:57:58Z"
}, },
{ {
"slug": "autocaliweb", "slug": "autocaliweb",
@@ -193,16 +193,16 @@
{ {
"slug": "cleanuparr", "slug": "cleanuparr",
"repo": "Cleanuparr/Cleanuparr", "repo": "Cleanuparr/Cleanuparr",
"version": "v2.6.1", "version": "v2.5.1",
"pinned": false, "pinned": false,
"date": "2026-02-13T10:00:19Z" "date": "2026-01-11T00:46:17Z"
}, },
{ {
"slug": "cloudreve", "slug": "cloudreve",
"repo": "cloudreve/cloudreve", "repo": "cloudreve/cloudreve",
"version": "4.14.0", "version": "4.13.0",
"pinned": false, "pinned": false,
"date": "2026-02-14T06:05:06Z" "date": "2026-02-05T12:53:24Z"
}, },
{ {
"slug": "comfyui", "slug": "comfyui",
@@ -284,9 +284,9 @@
{ {
"slug": "domain-locker", "slug": "domain-locker",
"repo": "Lissy93/domain-locker", "repo": "Lissy93/domain-locker",
"version": "v0.1.4", "version": "v0.1.3",
"pinned": false, "pinned": false,
"date": "2026-02-14T07:41:29Z" "date": "2026-02-11T10:03:32Z"
}, },
{ {
"slug": "domain-monitor", "slug": "domain-monitor",
@@ -298,9 +298,9 @@
{ {
"slug": "donetick", "slug": "donetick",
"repo": "donetick/donetick", "repo": "donetick/donetick",
"version": "v0.1.73", "version": "v0.1.71",
"pinned": false, "pinned": false,
"date": "2026-02-12T23:42:30Z" "date": "2026-02-11T06:01:13Z"
}, },
{ {
"slug": "drawio", "slug": "drawio",
@@ -354,9 +354,9 @@
{ {
"slug": "firefly", "slug": "firefly",
"repo": "firefly-iii/firefly-iii", "repo": "firefly-iii/firefly-iii",
"version": "v6.4.19", "version": "v6.4.18",
"pinned": false, "pinned": false,
"date": "2026-02-14T11:55:40Z" "date": "2026-02-08T07:28:00Z"
}, },
{ {
"slug": "fladder", "slug": "fladder",
@@ -403,9 +403,9 @@
{ {
"slug": "ghostfolio", "slug": "ghostfolio",
"repo": "ghostfolio/ghostfolio", "repo": "ghostfolio/ghostfolio",
"version": "2.238.0", "version": "2.237.0",
"pinned": false, "pinned": false,
"date": "2026-02-12T18:28:55Z" "date": "2026-02-08T13:59:53Z"
}, },
{ {
"slug": "gitea", "slug": "gitea",
@@ -445,9 +445,9 @@
{ {
"slug": "gotify", "slug": "gotify",
"repo": "gotify/server", "repo": "gotify/server",
"version": "v2.9.0", "version": "v2.8.0",
"pinned": false, "pinned": false,
"date": "2026-02-13T15:22:31Z" "date": "2026-01-02T11:56:16Z"
}, },
{ {
"slug": "grist", "slug": "grist",
@@ -508,9 +508,9 @@
{ {
"slug": "homarr", "slug": "homarr",
"repo": "homarr-labs/homarr", "repo": "homarr-labs/homarr",
"version": "v1.53.1", "version": "v1.53.0",
"pinned": false, "pinned": false,
"date": "2026-02-13T19:47:11Z" "date": "2026-02-06T19:42:58Z"
}, },
{ {
"slug": "homebox", "slug": "homebox",
@@ -543,9 +543,9 @@
{ {
"slug": "huntarr", "slug": "huntarr",
"repo": "plexguide/Huntarr.io", "repo": "plexguide/Huntarr.io",
"version": "9.2.4.1", "version": "9.2.3",
"pinned": false, "pinned": false,
"date": "2026-02-12T22:17:47Z" "date": "2026-02-07T04:44:20Z"
}, },
{ {
"slug": "immich-public-proxy", "slug": "immich-public-proxy",
@@ -571,16 +571,16 @@
{ {
"slug": "invoiceninja", "slug": "invoiceninja",
"repo": "invoiceninja/invoiceninja", "repo": "invoiceninja/invoiceninja",
"version": "v5.12.59", "version": "v5.12.57",
"pinned": false, "pinned": false,
"date": "2026-02-13T02:26:13Z" "date": "2026-02-11T23:08:56Z"
}, },
{ {
"slug": "jackett", "slug": "jackett",
"repo": "Jackett/Jackett", "repo": "Jackett/Jackett",
"version": "v0.24.1109", "version": "v0.24.1098",
"pinned": false, "pinned": false,
"date": "2026-02-14T05:54:26Z" "date": "2026-02-12T05:56:25Z"
}, },
{ {
"slug": "jellystat", "slug": "jellystat",
@@ -599,9 +599,9 @@
{ {
"slug": "jotty", "slug": "jotty",
"repo": "fccview/jotty", "repo": "fccview/jotty",
"version": "1.20.0", "version": "1.19.1",
"pinned": false, "pinned": false,
"date": "2026-02-12T09:23:30Z" "date": "2026-01-26T21:30:39Z"
}, },
{ {
"slug": "kapowarr", "slug": "kapowarr",
@@ -823,16 +823,16 @@
{ {
"slug": "metube", "slug": "metube",
"repo": "alexta69/metube", "repo": "alexta69/metube",
"version": "2026.02.14", "version": "2026.02.08",
"pinned": false, "pinned": false,
"date": "2026-02-14T07:49:11Z" "date": "2026-02-08T17:01:37Z"
}, },
{ {
"slug": "miniflux", "slug": "miniflux",
"repo": "miniflux/v2", "repo": "miniflux/v2",
"version": "2.2.17", "version": "2.2.16",
"pinned": false, "pinned": false,
"date": "2026-02-13T20:30:17Z" "date": "2026-01-07T03:26:27Z"
}, },
{ {
"slug": "monica", "slug": "monica",
@@ -998,9 +998,9 @@
{ {
"slug": "pangolin", "slug": "pangolin",
"repo": "fosrl/pangolin", "repo": "fosrl/pangolin",
"version": "1.15.4", "version": "1.15.3",
"pinned": false, "pinned": false,
"date": "2026-02-13T23:01:29Z" "date": "2026-02-12T06:10:19Z"
}, },
{ {
"slug": "paperless-ai", "slug": "paperless-ai",
@@ -1026,9 +1026,9 @@
{ {
"slug": "patchmon", "slug": "patchmon",
"repo": "PatchMon/PatchMon", "repo": "PatchMon/PatchMon",
"version": "v1.4.0", "version": "v1.3.7",
"pinned": false, "pinned": false,
"date": "2026-02-13T10:39:03Z" "date": "2025-12-25T11:08:14Z"
}, },
{ {
"slug": "paymenter", "slug": "paymenter",
@@ -1096,9 +1096,9 @@
{ {
"slug": "pocketbase", "slug": "pocketbase",
"repo": "pocketbase/pocketbase", "repo": "pocketbase/pocketbase",
"version": "v0.36.3", "version": "v0.36.2",
"pinned": false, "pinned": false,
"date": "2026-02-13T18:38:58Z" "date": "2026-02-01T08:12:42Z"
}, },
{ {
"slug": "pocketid", "slug": "pocketid",
@@ -1219,13 +1219,6 @@
"pinned": false, "pinned": false,
"date": "2025-11-16T22:39:01Z" "date": "2025-11-16T22:39:01Z"
}, },
{
"slug": "radicale",
"repo": "Kozea/Radicale",
"version": "v3.6.0",
"pinned": false,
"date": "2026-01-10T06:56:46Z"
},
{ {
"slug": "rclone", "slug": "rclone",
"repo": "rclone/rclone", "repo": "rclone/rclone",
@@ -1299,9 +1292,9 @@
{ {
"slug": "scraparr", "slug": "scraparr",
"repo": "thecfu/scraparr", "repo": "thecfu/scraparr",
"version": "v3.0.3", "version": "v3.0.1",
"pinned": false, "pinned": false,
"date": "2026-02-12T14:20:56Z" "date": "2026-02-11T17:42:23Z"
}, },
{ {
"slug": "seelf", "slug": "seelf",
@@ -1313,9 +1306,9 @@
{ {
"slug": "semaphore", "slug": "semaphore",
"repo": "semaphoreui/semaphore", "repo": "semaphoreui/semaphore",
"version": "v2.17.0", "version": "v2.16.51",
"pinned": false, "pinned": false,
"date": "2026-02-13T21:08:30Z" "date": "2026-01-12T16:26:38Z"
}, },
{ {
"slug": "shelfmark", "slug": "shelfmark",
@@ -1411,9 +1404,9 @@
{ {
"slug": "tandoor", "slug": "tandoor",
"repo": "TandoorRecipes/recipes", "repo": "TandoorRecipes/recipes",
"version": "2.5.1", "version": "2.5.0",
"pinned": false, "pinned": false,
"date": "2026-02-13T15:57:27Z" "date": "2026-02-08T13:23:02Z"
}, },
{ {
"slug": "tasmoadmin", "slug": "tasmoadmin",
@@ -1439,9 +1432,9 @@
{ {
"slug": "termix", "slug": "termix",
"repo": "Termix-SSH/Termix", "repo": "Termix-SSH/Termix",
"version": "release-1.11.1-tag", "version": "release-1.11.0-tag",
"pinned": false, "pinned": false,
"date": "2026-02-13T04:49:16Z" "date": "2026-01-25T02:09:52Z"
}, },
{ {
"slug": "the-lounge", "slug": "the-lounge",
@@ -1467,9 +1460,9 @@
{ {
"slug": "tianji", "slug": "tianji",
"repo": "msgbyte/tianji", "repo": "msgbyte/tianji",
"version": "v1.31.13", "version": "v1.31.10",
"pinned": false, "pinned": false,
"date": "2026-02-13T16:30:09Z" "date": "2026-02-04T17:21:04Z"
}, },
{ {
"slug": "traccar", "slug": "traccar",
@@ -1516,9 +1509,9 @@
{ {
"slug": "tududi", "slug": "tududi",
"repo": "chrisvel/tududi", "repo": "chrisvel/tududi",
"version": "v0.88.5", "version": "v0.88.4",
"pinned": false, "pinned": false,
"date": "2026-02-13T13:54:14Z" "date": "2026-01-20T15:11:58Z"
}, },
{ {
"slug": "tunarr", "slug": "tunarr",
@@ -1558,16 +1551,16 @@
{ {
"slug": "upsnap", "slug": "upsnap",
"repo": "seriousm4x/UpSnap", "repo": "seriousm4x/UpSnap",
"version": "5.2.8", "version": "5.2.7",
"pinned": false, "pinned": false,
"date": "2026-02-13T00:02:37Z" "date": "2026-01-07T23:48:00Z"
}, },
{ {
"slug": "uptimekuma", "slug": "uptimekuma",
"repo": "louislam/uptime-kuma", "repo": "louislam/uptime-kuma",
"version": "2.1.1", "version": "2.1.0",
"pinned": false, "pinned": false,
"date": "2026-02-13T16:07:33Z" "date": "2026-02-07T02:31:49Z"
}, },
{ {
"slug": "vaultwarden", "slug": "vaultwarden",

View File

@@ -21,7 +21,7 @@
"resources": { "resources": {
"cpu": 1, "cpu": 1,
"ram": 512, "ram": 512,
"hdd": 4, "hdd": 2,
"os": "debian", "os": "debian",
"version": "13" "version": "13"
} }
@@ -32,7 +32,7 @@
"resources": { "resources": {
"cpu": 1, "cpu": 1,
"ram": 256, "ram": 256,
"hdd": 2, "hdd": 1,
"os": "alpine", "os": "alpine",
"version": "3.23" "version": "3.23"
} }

View File

@@ -12,7 +12,7 @@
"documentation": "https://radicale.org/master.html#documentation-1", "documentation": "https://radicale.org/master.html#documentation-1",
"website": "https://radicale.org/", "website": "https://radicale.org/",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/radicale.webp", "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/radicale.webp",
"config_path": "/etc/radicale/config", "config_path": "/etc/radicale/config or ~/.config/radicale/config",
"description": "Radicale is a small but powerful CalDAV (calendars, to-do lists) and CardDAV (contacts)", "description": "Radicale is a small but powerful CalDAV (calendars, to-do lists) and CardDAV (contacts)",
"install_methods": [ "install_methods": [
{ {

View File

@@ -14,8 +14,6 @@
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ubiquiti-unifi.webp", "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ubiquiti-unifi.webp",
"config_path": "", "config_path": "",
"description": "UniFi Network Server is a software that helps manage and monitor UniFi networks (Wi-Fi, Ethernet, etc.) by providing an intuitive user interface and advanced features. It allows network administrators to configure, monitor, and upgrade network devices, as well as view network statistics, client devices, and historical events. The aim of the application is to make the management of UniFi networks easier and more efficient.", "description": "UniFi Network Server is a software that helps manage and monitor UniFi networks (Wi-Fi, Ethernet, etc.) by providing an intuitive user interface and advanced features. It allows network administrators to configure, monitor, and upgrade network devices, as well as view network statistics, client devices, and historical events. The aim of the application is to make the management of UniFi networks easier and more efficient.",
"disable": true,
"disable_description": "This script is disabled because UniFi no longer delivers APT packages for Debian systems. The installation relies on APT repositories that are no longer maintained or available. For more details, see: https://github.com/community-scripts/ProxmoxVE/issues/11876",
"install_methods": [ "install_methods": [
{ {
"type": "default", "type": "default",

View File

@@ -16,8 +16,7 @@ update_os
msg_info "Installing Dependencies" msg_info "Installing Dependencies"
$STD apt install -y \ $STD apt install -y \
python3-pip \ python3-pip \
python3-libtorrent \ python3-libtorrent
python3-setuptools
msg_ok "Installed Dependencies" msg_ok "Installed Dependencies"
msg_info "Installing Deluge" msg_info "Installing Deluge"

View File

@@ -37,7 +37,7 @@ fetch_and_deploy_gh_release "dispatcharr" "Dispatcharr/Dispatcharr" "tarball"
msg_info "Installing Python Dependencies with uv" msg_info "Installing Python Dependencies with uv"
cd /opt/dispatcharr cd /opt/dispatcharr
$STD uv venv --clear $STD uv venv --clear
$STD uv sync $STD uv pip install -r requirements.txt --index-strategy unsafe-best-match
$STD uv pip install gunicorn gevent celery redis daphne $STD uv pip install gunicorn gevent celery redis daphne
msg_ok "Installed Python Dependencies" msg_ok "Installed Python Dependencies"

View File

@@ -38,18 +38,6 @@ rm -f "$DEB_FILE"
echo "$LATEST_VERSION" >~/.emqx echo "$LATEST_VERSION" >~/.emqx
msg_ok "Installed EMQX" msg_ok "Installed EMQX"
read -r -p "${TAB3}Would you like to disable the EMQX MQ feature? (reduces disk/CPU usage) <y/N> " prompt
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
msg_info "Disabling EMQX MQ feature"
mkdir -p /etc/emqx
if ! grep -q "^mq.enable" /etc/emqx/emqx.conf 2>/dev/null; then
echo "mq.enable = false" >>/etc/emqx/emqx.conf
else
sed -i 's/^mq.enable.*/mq.enable = false/' /etc/emqx/emqx.conf
fi
msg_ok "Disabled EMQX MQ feature"
fi
msg_info "Starting EMQX service" msg_info "Starting EMQX service"
$STD systemctl enable -q --now emqx $STD systemctl enable -q --now emqx
msg_ok "Enabled EMQX service" msg_ok "Enabled EMQX service"

View File

@@ -24,7 +24,7 @@ setup_hwaccel
PYTHON_VERSION="3.12" setup_uv PYTHON_VERSION="3.12" setup_uv
msg_info "Installing Open WebUI" msg_info "Installing Open WebUI"
$STD uv tool install --python 3.12 --constraint <(echo "numba>=0.60") open-webui[all] $STD uv tool install --python 3.12 open-webui[all]
msg_ok "Installed Open WebUI" msg_ok "Installed Open WebUI"
read -r -p "${TAB3}Would you like to add Ollama? <y/N> " prompt read -r -p "${TAB3}Would you like to add Ollama? <y/N> " prompt

View File

@@ -178,7 +178,7 @@ http:
servers: servers:
- url: "http://$LOCAL_IP:3000" - url: "http://$LOCAL_IP:3000"
EOF EOF
$STD npm run db:push $STD npm run db:sqlite:push
. /etc/os-release . /etc/os-release
if [ "$VERSION_CODENAME" = "trixie" ]; then if [ "$VERSION_CODENAME" = "trixie" ]; then

View File

@@ -14,51 +14,42 @@ network_check
update_os update_os
msg_info "Installing Dependencies" msg_info "Installing Dependencies"
$STD apt install -y apache2-utils $STD apt install -y \
apache2-utils \
python3-pip \
python3-venv
msg_ok "Installed Dependencies" msg_ok "Installed Dependencies"
PYTHON_VERSION="3.13" setup_uv
fetch_and_deploy_gh_release "Radicale" "Kozea/Radicale" "tarball" "latest" "/opt/radicale"
msg_info "Setting up Radicale" msg_info "Setting up Radicale"
cd /opt/radicale python3 -m venv /opt/radicale
source /opt/radicale/bin/activate
$STD python3 -m pip install --upgrade https://github.com/Kozea/Radicale/archive/master.tar.gz
RNDPASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13) RNDPASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
$STD htpasswd -c -b -5 /opt/radicale/users admin "$RNDPASS" $STD htpasswd -c -b -5 /opt/radicale/users admin $RNDPASS
{ {
echo "Radicale Credentials" echo "Radicale Credentials"
echo "Admin User: admin" echo "Admin User: admin"
echo "Admin Password: $RNDPASS" echo "Admin Password: $RNDPASS"
} >>~/radicale.creds } >>~/radicale.creds
msg_ok "Done setting up Radicale"
mkdir -p /etc/radicale msg_info "Setup Service"
cat <<EOF >/etc/radicale/config
[server]
hosts = 0.0.0.0:5232
[auth] cat <<EOF >/opt/radicale/start.sh
type = htpasswd #!/usr/bin/env bash
htpasswd_filename = /opt/radicale/users source /opt/radicale/bin/activate
htpasswd_encryption = sha512 python3 -m radicale --storage-filesystem-folder=/var/lib/radicale/collections --hosts 0.0.0.0:5232 --auth-type htpasswd --auth-htpasswd-filename /opt/radicale/users --auth-htpasswd-encryption sha512
[storage]
type = multifilesystem
filesystem_folder = /var/lib/radicale/collections
[web]
type = internal
EOF EOF
msg_ok "Set up Radicale"
msg_info "Creating Service" chmod +x /opt/radicale/start.sh
cat <<EOF >/etc/systemd/system/radicale.service cat <<EOF >/etc/systemd/system/radicale.service
[Unit]
Description=A simple CalDAV (calendar) and CardDAV (contact) server Description=A simple CalDAV (calendar) and CardDAV (contact) server
After=network.target After=network.target
Requires=network.target Requires=network.target
[Service] [Service]
WorkingDirectory=/opt/radicale ExecStart=/opt/radicale/start.sh
ExecStart=/usr/local/bin/uv run -m radicale --config /etc/radicale/config
Restart=on-failure Restart=on-failure
# User=radicale # User=radicale
# Deny other users access to the calendar data # Deny other users access to the calendar data

View File

@@ -15,18 +15,16 @@ update_os
msg_info "Installing Dependencies" msg_info "Installing Dependencies"
$STD apt install -y apt-transport-https $STD apt install -y apt-transport-https
curl -fsSL "https://dl.ui.com/unifi/unifi-repo.gpg" -o "/usr/share/keyrings/unifi-repo.gpg"
cat <<EOF | sudo tee /etc/apt/sources.list.d/100-ubnt-unifi.sources >/dev/null
Types: deb
URIs: https://www.ui.com/downloads/unifi/debian
Suites: stable
Components: ubiquiti
Architectures: amd64
Signed-By: /usr/share/keyrings/unifi-repo.gpg
EOF
$STD apt update
msg_ok "Installed Dependencies" msg_ok "Installed Dependencies"
setup_deb822_repo \
"unifi" \
"https://dl.ui.com/unifi/unifi-repo.gpg" \
"https://www.ui.com/downloads/unifi/debian" \
"stable" \
"ubiquiti" \
"amd64"
JAVA_VERSION="21" setup_java JAVA_VERSION="21" setup_java
if lscpu | grep -q 'avx'; then if lscpu | grep -q 'avx'; then

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,6 @@ variables() {
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable. RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files
BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log
combined_log="/tmp/install-${SESSION_ID}-combined.log" # Combined log (build + install) for failed installations
CTTYPE="${CTTYPE:-${CT_TYPE:-1}}" CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
# Parse dev_mode early # Parse dev_mode early
@@ -277,9 +276,8 @@ install_ssh_keys_into_ct() {
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# validate_container_id() # validate_container_id()
# #
# - Validates if a container ID is available for use (CLUSTER-WIDE) # - Validates if a container ID is available for use
# - Checks cluster resources via pvesh for VMs/CTs on ALL nodes # - Checks if ID is already used by VM or LXC container
# - Falls back to local config file check if pvesh unavailable
# - Checks if ID is used in LVM logical volumes # - Checks if ID is used in LVM logical volumes
# - Returns 0 if ID is available, 1 if already in use # - Returns 0 if ID is available, 1 if already in use
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@@ -291,35 +289,11 @@ validate_container_id() {
return 1 return 1
fi fi
# CLUSTER-WIDE CHECK: Query all VMs/CTs across all nodes # Check if config file exists for VM or LXC
# This catches IDs used on other nodes in the cluster
# NOTE: Works on single-node too - Proxmox always has internal cluster structure
# Falls back gracefully if pvesh unavailable or returns empty
if command -v pvesh &>/dev/null; then
local cluster_ids
cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true)
if [[ -n "$cluster_ids" ]] && echo "$cluster_ids" | grep -qw "$ctid"; then
return 1
fi
fi
# LOCAL FALLBACK: Check if config file exists for VM or LXC
# This handles edge cases where pvesh might not return all info
if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then
return 1 return 1
fi fi
# Check ALL nodes in cluster for config files (handles pmxcfs sync delays)
# NOTE: On single-node, /etc/pve/nodes/ contains just the one node - still works
if [[ -d "/etc/pve/nodes" ]]; then
for node_dir in /etc/pve/nodes/*/; do
if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then
return 1
fi
done
fi
# Check if ID is used in LVM logical volumes # Check if ID is used in LVM logical volumes
if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then
return 1 return 1
@@ -331,30 +305,63 @@ validate_container_id() {
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# get_valid_container_id() # get_valid_container_id()
# #
# - Returns a valid, unused container ID (CLUSTER-AWARE) # - Returns a valid, unused container ID
# - Uses pvesh /cluster/nextid as starting point (already cluster-aware)
# - If provided ID is valid, returns it # - If provided ID is valid, returns it
# - Otherwise increments until a free one is found across entire cluster # - Otherwise increments from suggested ID until a free one is found
# - Calls validate_container_id() to check availability # - Calls validate_container_id() to check availability
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
get_valid_container_id() { get_valid_container_id() {
local suggested_id="${1:-$(pvesh get /cluster/nextid 2>/dev/null || echo 100)}" local suggested_id="${1:-$(pvesh get /cluster/nextid)}"
# Ensure we have a valid starting ID
if ! [[ "$suggested_id" =~ ^[0-9]+$ ]]; then
suggested_id=$(pvesh get /cluster/nextid 2>/dev/null || echo 100)
fi
local max_attempts=1000
local attempts=0
while ! validate_container_id "$suggested_id"; do while ! validate_container_id "$suggested_id"; do
suggested_id=$((suggested_id + 1)) suggested_id=$((suggested_id + 1))
attempts=$((attempts + 1)) done
if [[ $attempts -ge $max_attempts ]]; then
msg_error "Could not find available container ID after $max_attempts attempts" echo "$suggested_id"
exit 1 }
# ------------------------------------------------------------------------------
# validate_container_id()
#
# - Validates if a container ID is available for use
# - Checks if ID is already used by VM or LXC container
# - Checks if ID is used in LVM logical volumes
# - Returns 0 if ID is available, 1 if already in use
# ------------------------------------------------------------------------------
validate_container_id() {
local ctid="$1"
# Check if ID is numeric
if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then
return 1
fi fi
# Check if config file exists for VM or LXC
if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then
return 1
fi
# Check if ID is used in LVM logical volumes
if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# get_valid_container_id()
#
# - Returns a valid, unused container ID
# - If provided ID is valid, returns it
# - Otherwise increments from suggested ID until a free one is found
# - Calls validate_container_id() to check availability
# ------------------------------------------------------------------------------
get_valid_container_id() {
local suggested_id="${1:-$(pvesh get /cluster/nextid)}"
while ! validate_container_id "$suggested_id"; do
suggested_id=$((suggested_id + 1))
done done
echo "$suggested_id" echo "$suggested_id"
@@ -378,7 +385,7 @@ validate_hostname() {
# Split by dots and validate each label # Split by dots and validate each label
local IFS='.' local IFS='.'
read -ra labels <<<"$hostname" read -ra labels <<< "$hostname"
for label in "${labels[@]}"; do for label in "${labels[@]}"; do
# Each label: 1-63 chars, alphanumeric, hyphens allowed (not at start/end) # Each label: 1-63 chars, alphanumeric, hyphens allowed (not at start/end)
if [[ -z "$label" ]] || [[ ${#label} -gt 63 ]]; then if [[ -z "$label" ]] || [[ ${#label} -gt 63 ]]; then
@@ -482,7 +489,7 @@ validate_ipv6_address() {
# Check that no segment exceeds 4 hex chars # Check that no segment exceeds 4 hex chars
local IFS=':' local IFS=':'
local -a segments local -a segments
read -ra segments <<<"$addr" read -ra segments <<< "$addr"
for seg in "${segments[@]}"; do for seg in "${segments[@]}"; do
if [[ ${#seg} -gt 4 ]]; then if [[ ${#seg} -gt 4 ]]; then
return 1 return 1
@@ -532,14 +539,14 @@ validate_gateway_in_subnet() {
# Convert IPs to integers # Convert IPs to integers
local IFS='.' local IFS='.'
read -r i1 i2 i3 i4 <<<"$ip" read -r i1 i2 i3 i4 <<< "$ip"
read -r g1 g2 g3 g4 <<<"$gateway" read -r g1 g2 g3 g4 <<< "$gateway"
local ip_int=$(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4)) local ip_int=$(( (i1 << 24) + (i2 << 16) + (i3 << 8) + i4 ))
local gw_int=$(((g1 << 24) + (g2 << 16) + (g3 << 8) + g4)) local gw_int=$(( (g1 << 24) + (g2 << 16) + (g3 << 8) + g4 ))
# Check if both are in same network # Check if both are in same network
if (((ip_int & mask) != (gw_int & mask))); then if (( (ip_int & mask) != (gw_int & mask) )); then
return 1 return 1
fi fi
@@ -1171,7 +1178,7 @@ load_vars_file() {
fi fi
fi fi
;; ;;
var_fuse | var_tun | var_gpu | var_ssh | var_verbose | var_protection) var_fuse|var_tun|var_gpu|var_ssh|var_verbose|var_protection)
if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then
msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring" msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring"
continue continue
@@ -2757,26 +2764,6 @@ Advanced:
[[ "$APT_CACHER" == "yes" ]] && echo -e "${INFO}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}" [[ "$APT_CACHER" == "yes" ]] && echo -e "${INFO}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}"
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
echo -e "${CREATING}${BOLD}${RD}Creating an LXC of ${APP} using the above advanced settings${CL}" echo -e "${CREATING}${BOLD}${RD}Creating an LXC of ${APP} using the above advanced settings${CL}"
# Log settings to file
log_section "CONTAINER SETTINGS (ADVANCED) - ${APP}"
log_msg "Application: ${APP}"
log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
log_msg "Operating System: $var_os ($var_version)"
log_msg "Container Type: $([ "$CT_TYPE" == "1" ] && echo "Unprivileged" || echo "Privileged")"
log_msg "Container ID: $CT_ID"
log_msg "Hostname: $HN"
log_msg "Disk Size: ${DISK_SIZE} GB"
log_msg "CPU Cores: $CORE_COUNT"
log_msg "RAM Size: ${RAM_SIZE} MiB"
log_msg "Bridge: $BRG"
log_msg "IPv4: $NET"
log_msg "IPv6: $IPV6_METHOD"
log_msg "FUSE Support: ${ENABLE_FUSE:-no}"
log_msg "Nesting: $([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")"
log_msg "GPU Passthrough: ${ENABLE_GPU:-no}"
log_msg "Verbose Mode: $VERBOSE"
log_msg "Session ID: ${SESSION_ID}"
} }
# ============================================================================== # ==============================================================================
@@ -2884,7 +2871,6 @@ diagnostics_menu() {
# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.) # - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
# - Uses icons and formatting for readability # - Uses icons and formatting for readability
# - Convert CT_TYPE to description # - Convert CT_TYPE to description
# - Also logs settings to log file for debugging
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
echo_default() { echo_default() {
CT_TYPE_DESC="Unprivileged" CT_TYPE_DESC="Unprivileged"
@@ -2906,20 +2892,6 @@ echo_default() {
fi fi
echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
echo -e " " echo -e " "
# Log settings to file
log_section "CONTAINER SETTINGS - ${APP}"
log_msg "Application: ${APP}"
log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
log_msg "Container ID: ${CT_ID}"
log_msg "Operating System: $var_os ($var_version)"
log_msg "Container Type: $CT_TYPE_DESC"
log_msg "Disk Size: ${DISK_SIZE} GB"
log_msg "CPU Cores: ${CORE_COUNT}"
log_msg "RAM Size: ${RAM_SIZE} MiB"
[[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]] && log_msg "GPU Passthrough: Enabled"
[[ "$VERBOSE" == "yes" ]] && log_msg "Verbose Mode: Enabled"
log_msg "Session ID: ${SESSION_ID}"
} }
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@@ -3664,9 +3636,6 @@ $PCT_OPTIONS_STRING"
exit 214 exit 214
fi fi
msg_ok "Storage space validated" msg_ok "Storage space validated"
# Report installation start to API (early - captures failed installs too)
post_to_api
fi fi
create_lxc_container || exit $? create_lxc_container || exit $?
@@ -4041,9 +4010,6 @@ EOF'
# Install SSH keys # Install SSH keys
install_ssh_keys_into_ct install_ssh_keys_into_ct
# Start timer for duration tracking
start_install_timer
# Run application installer # Run application installer
# Disable error trap - container errors are handled internally via flag file # Disable error trap - container errors are handled internally via flag file
set +Eeuo pipefail # Disable ALL error handling temporarily set +Eeuo pipefail # Disable ALL error handling temporarily
@@ -4074,58 +4040,25 @@ EOF'
if [[ $install_exit_code -ne 0 ]]; then if [[ $install_exit_code -ne 0 ]]; then
msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})" msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})"
# Copy install log from container BEFORE API call so get_error_text() can read it # Copy both logs from container before potential deletion
local build_log_copied=false local build_log_copied=false
local install_log_copied=false local install_log_copied=false
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
# Create combined log with header # Copy BUILD_LOG (creation log) if it exists
{
echo "================================================================================"
echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
echo "Container ID: ${CTID}"
echo "Session ID: ${SESSION_ID}"
echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
echo "================================================================================"
echo ""
} >"$combined_log"
# Append BUILD_LOG (host-side creation log) if it exists
if [[ -f "${BUILD_LOG}" ]]; then if [[ -f "${BUILD_LOG}" ]]; then
{ cp "${BUILD_LOG}" "/tmp/create-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null && build_log_copied=true
echo "================================================================================"
echo "PHASE 1: CONTAINER CREATION (Host)"
echo "================================================================================"
cat "${BUILD_LOG}"
echo ""
} >>"$combined_log"
build_log_copied=true
fi fi
# Copy and append INSTALL_LOG from container # Copy INSTALL_LOG from container
local temp_install_log="/tmp/.install-temp-${SESSION_ID}.log" if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "/tmp/install-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null; then
if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_install_log" 2>/dev/null; then
{
echo "================================================================================"
echo "PHASE 2: APPLICATION INSTALLATION (Container)"
echo "================================================================================"
cat "$temp_install_log"
echo ""
} >>"$combined_log"
rm -f "$temp_install_log"
install_log_copied=true install_log_copied=true
# Point INSTALL_LOG to combined log so get_error_text() finds it
INSTALL_LOG="$combined_log"
fi
fi fi
# Report failure to telemetry API (now with log available on host) # Show available logs
post_update_to_api "failed" "$install_exit_code" echo ""
[[ "$build_log_copied" == true ]] && echo -e "${GN}${CL} Container creation log: ${BL}/tmp/create-lxc-${CTID}-${SESSION_ID}.log${CL}"
# Show combined log location [[ "$install_log_copied" == true ]] && echo -e "${GN}${CL} Installation log: ${BL}/tmp/install-lxc-${CTID}-${SESSION_ID}.log${CL}"
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
msg_custom "📋" "${YW}" "Installation log: ${combined_log}"
fi fi
# Dev mode: Keep container or open breakpoint shell # Dev mode: Keep container or open breakpoint shell
@@ -4148,21 +4081,19 @@ EOF'
exit $install_exit_code exit $install_exit_code
fi fi
# Prompt user for cleanup with 60s timeout # Prompt user for cleanup with 60s timeout (plain echo - no msg_info to avoid spinner)
echo "" echo ""
echo -en "${TAB}${TAB}${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}" echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
if read -t 60 -r response; then if read -t 60 -r response; then
if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
# Remove container # Remove container
echo "" echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}"
msg_info "Removing container ${CTID}"
pct stop "$CTID" &>/dev/null || true pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true pct destroy "$CTID" &>/dev/null || true
msg_ok "Container ${CTID} removed" echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
elif [[ "$response" =~ ^[Nn]$ ]]; then elif [[ "$response" =~ ^[Nn]$ ]]; then
echo "" echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}"
msg_warn "Container ${CTID} kept for debugging"
# Dev mode: Setup MOTD/SSH for debugging access to broken container # Dev mode: Setup MOTD/SSH for debugging access to broken container
if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then
@@ -4178,17 +4109,13 @@ EOF'
fi fi
else else
# Timeout - auto-remove # Timeout - auto-remove
echo "" echo -e "\n${YW}No response - auto-removing container${CL}"
msg_info "No response - removing container ${CTID}" echo -e "${TAB}${HOLD}${YW}Removing container ${CTID}${CL}"
pct stop "$CTID" &>/dev/null || true pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true pct destroy "$CTID" &>/dev/null || true
msg_ok "Container ${CTID} removed" echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
fi fi
# Force one final status update attempt after cleanup
# This ensures status is updated even if the first attempt failed (e.g., HTTP 400)
post_update_to_api "failed" "$install_exit_code" "force"
exit $install_exit_code exit $install_exit_code
fi fi
} }
@@ -5192,74 +5119,18 @@ EOF
# SECTION 10: ERROR HANDLING & EXIT TRAPS # SECTION 10: ERROR HANDLING & EXIT TRAPS
# ============================================================================== # ==============================================================================
# ------------------------------------------------------------------------------
# ensure_log_on_host()
#
# - Ensures INSTALL_LOG points to a readable file on the host
# - If INSTALL_LOG points to a container path (e.g. /root/.install-*),
# tries to pull it from the container and create a combined log
# - This allows get_error_text() to find actual error output for telemetry
# ------------------------------------------------------------------------------
ensure_log_on_host() {
# Already readable on host? Nothing to do.
[[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]] && return 0
# Try pulling from container and creating combined log
if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]] && command -v pct &>/dev/null; then
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
if [[ ! -s "$combined_log" ]]; then
# Create combined log
{
echo "================================================================================"
echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
echo "Container ID: ${CTID}"
echo "Session ID: ${SESSION_ID}"
echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
echo "================================================================================"
echo ""
} >"$combined_log" 2>/dev/null || return 0
# Append BUILD_LOG if it exists
if [[ -f "${BUILD_LOG:-}" ]]; then
{
echo "================================================================================"
echo "PHASE 1: CONTAINER CREATION (Host)"
echo "================================================================================"
cat "${BUILD_LOG}"
echo ""
} >>"$combined_log"
fi
# Pull INSTALL_LOG from container
local temp_log="/tmp/.install-temp-${SESSION_ID}.log"
if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_log" 2>/dev/null; then
{
echo "================================================================================"
echo "PHASE 2: APPLICATION INSTALLATION (Container)"
echo "================================================================================"
cat "$temp_log"
echo ""
} >>"$combined_log"
rm -f "$temp_log"
fi
fi
if [[ -s "$combined_log" ]]; then
INSTALL_LOG="$combined_log"
fi
fi
}
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# api_exit_script() # api_exit_script()
# #
# - Exit trap handler for reporting to API telemetry # - Exit trap handler for reporting to API telemetry
# - Captures exit code and reports to PocketBase using centralized error descriptions # - Captures exit code and reports to API using centralized error descriptions
# - Uses explain_exit_code() from api.func for consistent error messages # - Uses explain_exit_code() from error_handler.func for consistent error messages
# - Posts failure status with exit code to API (error description resolved automatically) # - Posts failure status with exit code to API (error description added automatically)
# - Only executes on non-zero exit codes # - Only executes on non-zero exit codes
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
api_exit_script() { api_exit_script() {
exit_code=$? exit_code=$?
if [ $exit_code -ne 0 ]; then if [ $exit_code -ne 0 ]; then
ensure_log_on_host
post_update_to_api "failed" "$exit_code" post_update_to_api "failed" "$exit_code"
fi fi
} }
@@ -5267,6 +5138,6 @@ api_exit_script() {
if command -v pveversion >/dev/null 2>&1; then if command -v pveversion >/dev/null 2>&1; then
trap 'api_exit_script' EXIT trap 'api_exit_script' EXIT
fi fi
trap 'ensure_log_on_host; post_update_to_api "failed" "$?"' ERR trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR
trap 'ensure_log_on_host; post_update_to_api "failed" "130"' SIGINT trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'ensure_log_on_host; post_update_to_api "failed" "143"' SIGTERM trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM

View File

@@ -115,7 +115,7 @@ icons() {
BRIDGE="${TAB}🌉${TAB}${CL}" BRIDGE="${TAB}🌉${TAB}${CL}"
NETWORK="${TAB}📡${TAB}${CL}" NETWORK="${TAB}📡${TAB}${CL}"
GATEWAY="${TAB}🌐${TAB}${CL}" GATEWAY="${TAB}🌐${TAB}${CL}"
ICON_DISABLEIPV6="${TAB}🚫${TAB}${CL}" DISABLEIPV6="${TAB}🚫${TAB}${CL}"
DEFAULT="${TAB}⚙️${TAB}${CL}" DEFAULT="${TAB}⚙️${TAB}${CL}"
MACADDRESS="${TAB}🔗${TAB}${CL}" MACADDRESS="${TAB}🔗${TAB}${CL}"
VLANTAG="${TAB}🏷️${TAB}${CL}" VLANTAG="${TAB}🏷️${TAB}${CL}"
@@ -413,69 +413,6 @@ get_active_logfile() {
# Legacy compatibility: SILENT_LOGFILE points to active log # Legacy compatibility: SILENT_LOGFILE points to active log
SILENT_LOGFILE="$(get_active_logfile)" SILENT_LOGFILE="$(get_active_logfile)"
# ------------------------------------------------------------------------------
# strip_ansi()
#
# - Removes ANSI escape sequences from input text
# - Used to clean colored output for log files
# - Handles both piped input and arguments
# ------------------------------------------------------------------------------
strip_ansi() {
if [[ $# -gt 0 ]]; then
echo -e "$*" | sed 's/\x1b\[[0-9;]*m//g; s/\x1b\[[0-9;]*[a-zA-Z]//g'
else
sed 's/\x1b\[[0-9;]*m//g; s/\x1b\[[0-9;]*[a-zA-Z]//g'
fi
}
# ------------------------------------------------------------------------------
# log_msg()
#
# - Writes message to active log file without ANSI codes
# - Adds timestamp prefix for log correlation
# - Creates log file if it doesn't exist
# - Arguments: message text (can include ANSI codes, will be stripped)
# ------------------------------------------------------------------------------
log_msg() {
local msg="$*"
local logfile
logfile="$(get_active_logfile)"
[[ -z "$msg" ]] && return
[[ -z "$logfile" ]] && return
# Ensure log directory exists
mkdir -p "$(dirname "$logfile")" 2>/dev/null || true
# Strip ANSI codes and write with timestamp
local clean_msg
clean_msg=$(strip_ansi "$msg")
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $clean_msg" >>"$logfile"
}
# ------------------------------------------------------------------------------
# log_section()
#
# - Writes a section header to the log file
# - Used for separating different phases of installation
# - Arguments: section name
# ------------------------------------------------------------------------------
log_section() {
local section="$1"
local logfile
logfile="$(get_active_logfile)"
[[ -z "$logfile" ]] && return
mkdir -p "$(dirname "$logfile")" 2>/dev/null || true
{
echo ""
echo "================================================================================"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $section"
echo "================================================================================"
} >>"$logfile"
}
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# silent() # silent()
# #
@@ -522,9 +459,15 @@ silent() {
msg_custom "→" "${YWB}" "${cmd}" msg_custom "→" "${YWB}" "${cmd}"
if [[ -s "$logfile" ]]; then if [[ -s "$logfile" ]]; then
echo -e "\n${TAB}--- Last 10 lines of log ---" local log_lines=$(wc -l <"$logfile")
echo "--- Last 10 lines of silent log ---"
tail -n 10 "$logfile" tail -n 10 "$logfile"
echo -e "${TAB}-----------------------------------\n" echo "-----------------------------------"
# Show how to view full log if there are more lines
if [[ $log_lines -gt 10 ]]; then
msg_custom "📋" "${YW}" "View full log (${log_lines} lines): ${logfile}"
fi
fi fi
exit "$rc" exit "$rc"
@@ -545,7 +488,7 @@ spinner() {
local i=0 local i=0
while true; do while true; do
local index=$((i++ % ${#chars[@]})) local index=$((i++ % ${#chars[@]}))
printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${msg}${CS_CL}" printf "\r\033[2K%s %b" "${CS_YWB}${TAB}${chars[$index]}${TAB}${CS_CL}" "${CS_YWB}${msg}${CS_CL}"
sleep 0.1 sleep 0.1
done done
} }
@@ -612,9 +555,6 @@ msg_info() {
[[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return [[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return
MSG_INFO_SHOWN["$msg"]=1 MSG_INFO_SHOWN["$msg"]=1
# Log to file
log_msg "[INFO] $msg"
stop_spinner stop_spinner
SPINNER_MSG="$msg" SPINNER_MSG="$msg"
@@ -658,10 +598,7 @@ msg_ok() {
stop_spinner stop_spinner
clear_line clear_line
echo -e "$CM ${GN}${msg}${CL}" echo -e "$CM ${GN}${msg}${CL}"
log_msg "[OK] $msg" unset MSG_INFO_SHOWN["$msg"]
local sanitized_msg
sanitized_msg=$(printf '%s' "$msg" | sed 's/\x1b\[[0-9;]*m//g; s/[^a-zA-Z0-9_]/_/g')
unset 'MSG_INFO_SHOWN['"$sanitized_msg"']' 2>/dev/null || true
} }
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@@ -676,7 +613,6 @@ msg_error() {
stop_spinner stop_spinner
local msg="$1" local msg="$1"
echo -e "${BFR:-}${CROSS:-✖️} ${RD}${msg}${CL}" >&2 echo -e "${BFR:-}${CROSS:-✖️} ${RD}${msg}${CL}" >&2
log_msg "[ERROR] $msg"
} }
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@@ -691,7 +627,6 @@ msg_warn() {
stop_spinner stop_spinner
local msg="$1" local msg="$1"
echo -e "${BFR:-}${INFO:-} ${YWB}${msg}${CL}" >&2 echo -e "${BFR:-}${INFO:-} ${YWB}${msg}${CL}" >&2
log_msg "[WARN] $msg"
} }
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@@ -709,7 +644,6 @@ msg_custom() {
[[ -z "$msg" ]] && return [[ -z "$msg" ]] && return
stop_spinner stop_spinner
echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}" echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}"
log_msg "$msg"
} }
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@@ -874,562 +808,6 @@ is_verbose_mode() {
[[ "$verbose" != "no" || ! -t 2 ]] [[ "$verbose" != "no" || ! -t 2 ]]
} }
# ------------------------------------------------------------------------------
# is_unattended()
#
# - Detects if script is running in unattended/non-interactive mode
# - Checks MODE variable first (primary method)
# - Falls back to legacy flags (PHS_SILENT, var_unattended)
# - Returns 0 (true) if unattended, 1 (false) otherwise
# - Used by prompt functions to auto-apply defaults
#
# Modes that are unattended:
# - default (1) : Use script defaults, no prompts
# - mydefaults (3) : Use user's default.vars, no prompts
# - appdefaults (4) : Use app-specific defaults, no prompts
#
# Modes that are interactive:
# - advanced (2) : Full wizard with all options
#
# Note: Even in advanced mode, install scripts run unattended because
# all values are already collected during the wizard phase.
# ------------------------------------------------------------------------------
is_unattended() {
# Primary: Check MODE variable (case-insensitive)
local mode="${MODE:-${mode:-}}"
mode="${mode,,}" # lowercase
case "$mode" in
default | 1)
return 0
;;
mydefaults | userdefaults | 3)
return 0
;;
appdefaults | 4)
return 0
;;
advanced | 2)
# Advanced mode is interactive ONLY during wizard
# Inside container (install scripts), it should be unattended
# Check if we're inside a container (no pveversion command)
if ! command -v pveversion &>/dev/null; then
# We're inside the container - all values already collected
return 0
fi
# On host during wizard - interactive
return 1
;;
esac
# Legacy fallbacks for compatibility
[[ "${PHS_SILENT:-0}" == "1" ]] && return 0
[[ "${var_unattended:-}" =~ ^(yes|true|1)$ ]] && return 0
[[ "${UNATTENDED:-}" =~ ^(yes|true|1)$ ]] && return 0
# No TTY available = unattended
[[ ! -t 0 ]] && return 0
# Default: interactive
return 1
}
# ------------------------------------------------------------------------------
# show_missing_values_warning()
#
# - Displays a summary of required values that used fallback defaults
# - Should be called at the end of install scripts
# - Only shows warning if MISSING_REQUIRED_VALUES array has entries
# - Provides clear guidance on what needs manual configuration
#
# Global:
# MISSING_REQUIRED_VALUES - Array of variable names that need configuration
#
# Example:
# # At end of install script:
# show_missing_values_warning
# ------------------------------------------------------------------------------
show_missing_values_warning() {
if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then
echo ""
echo -e "${YW}╔════════════════════════════════════════════════════════════╗${CL}"
echo -e "${YW}║ ⚠️ MANUAL CONFIGURATION REQUIRED ║${CL}"
echo -e "${YW}╠════════════════════════════════════════════════════════════╣${CL}"
echo -e "${YW}║ The following values were not provided and need to be ║${CL}"
echo -e "${YW}║ configured manually for the service to work properly: ║${CL}"
echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}"
for val in "${MISSING_REQUIRED_VALUES[@]}"; do
printf "${YW}${CL} • %-56s ${YW}${CL}\n" "$val"
done
echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}"
echo -e "${YW}║ Check the service configuration files or environment ║${CL}"
echo -e "${YW}║ variables and update the placeholder values. ║${CL}"
echo -e "${YW}╚════════════════════════════════════════════════════════════╝${CL}"
echo ""
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# prompt_confirm()
#
# - Prompts user for yes/no confirmation with timeout and unattended support
# - In unattended mode: immediately returns default value
# - In interactive mode: waits for user input with configurable timeout
# - After timeout: auto-applies default value
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value: "y" or "n" (optional, default: "n")
# $3 - Timeout in seconds (optional, default: 60)
#
# Returns:
# 0 - User confirmed (yes)
# 1 - User declined (no) or timeout with default "n"
#
# Example:
# if prompt_confirm "Proceed with installation?" "y" 30; then
# echo "Installing..."
# fi
#
# # Unattended: prompt_confirm will use default without waiting
# var_unattended=yes
# prompt_confirm "Delete files?" "n" && echo "Deleting" || echo "Skipped"
# ------------------------------------------------------------------------------
prompt_confirm() {
local message="${1:-Confirm?}"
local default="${2:-n}"
local timeout="${3:-60}"
local response
# Normalize default to lowercase
default="${default,,}"
[[ "$default" != "y" ]] && default="n"
# Build prompt hint
local hint
if [[ "$default" == "y" ]]; then
hint="[Y/n]"
else
hint="[y/N]"
fi
# Unattended mode: apply default immediately
if is_unattended; then
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
# Not a TTY, use default
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
# Interactive prompt with timeout
echo -en "${YW}${message} ${hint} (auto-${default} in ${timeout}s): ${CL}"
if read -t "$timeout" -r response; then
# User provided input
response="${response,,}" # lowercase
case "$response" in
y | yes)
return 0
;;
n | no)
return 1
;;
"")
# Empty response, use default
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
;;
*)
# Invalid input, use default
echo -e "${YW}Invalid response, using default: ${default}${CL}"
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
;;
esac
else
# Timeout occurred
echo "" # Newline after timeout
echo -e "${YW}Timeout - auto-selecting: ${default}${CL}"
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
}
# ------------------------------------------------------------------------------
# prompt_input()
#
# - Prompts user for text input with timeout and unattended support
# - In unattended mode: immediately returns default value
# - In interactive mode: waits for user input with configurable timeout
# - After timeout: auto-applies default value
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value (optional, default: "")
# $3 - Timeout in seconds (optional, default: 60)
#
# Output:
# Prints the user input or default value to stdout
#
# Example:
# username=$(prompt_input "Enter username:" "admin" 30)
# echo "Using username: $username"
#
# # With validation
# while true; do
# port=$(prompt_input "Enter port:" "8080" 30)
# [[ "$port" =~ ^[0-9]+$ ]] && break
# echo "Invalid port number"
# done
# ------------------------------------------------------------------------------
prompt_input() {
local message="${1:-Enter value:}"
local default="${2:-}"
local timeout="${3:-60}"
local response
# Build display default hint
local hint=""
[[ -n "$default" ]] && hint=" (default: ${default})"
# Unattended mode: return default immediately
if is_unattended; then
echo "$default"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
# Not a TTY, use default
echo "$default"
return 0
fi
# Interactive prompt with timeout
echo -en "${YW}${message}${hint} (auto-default in ${timeout}s): ${CL}" >&2
if read -t "$timeout" -r response; then
# User provided input (or pressed Enter for empty)
if [[ -n "$response" ]]; then
echo "$response"
else
echo "$default"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - using default: ${default}${CL}" >&2
echo "$default"
fi
}
# ------------------------------------------------------------------------------
# prompt_input_required()
#
# - Prompts user for REQUIRED text input with fallback support
# - In unattended mode: Uses fallback value if no env var set (with warning)
# - In interactive mode: loops until user provides non-empty input
# - Tracks missing required values for end-of-script summary
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Fallback/example value for unattended mode (optional)
# $3 - Timeout in seconds (optional, default: 120)
# $4 - Environment variable name hint for error messages (optional)
#
# Output:
# Prints the user input or fallback value to stdout
#
# Returns:
# 0 - Success (value provided or fallback used)
# 1 - Failed (interactive timeout without input)
#
# Global:
# MISSING_REQUIRED_VALUES - Array tracking fields that used fallbacks
#
# Example:
# # With fallback - script continues even in unattended mode
# token=$(prompt_input_required "Enter API Token:" "YOUR_TOKEN_HERE" 60 "var_api_token")
#
# # Check at end of script if any values need manual configuration
# if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then
# msg_warn "Please configure: ${MISSING_REQUIRED_VALUES[*]}"
# fi
# ------------------------------------------------------------------------------
# Global array to track missing required values
declare -g -a MISSING_REQUIRED_VALUES=()
prompt_input_required() {
local message="${1:-Enter required value:}"
local fallback="${2:-CHANGE_ME}"
local timeout="${3:-120}"
local env_var_hint="${4:-}"
local response=""
# Check if value is already set via environment variable (if hint provided)
if [[ -n "$env_var_hint" ]]; then
local env_value="${!env_var_hint:-}"
if [[ -n "$env_value" ]]; then
echo "$env_value"
return 0
fi
fi
# Unattended mode: use fallback with warning
if is_unattended; then
if [[ -n "$env_var_hint" ]]; then
echo -e "${YW}⚠ Required value '${env_var_hint}' not set - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("$env_var_hint")
else
echo -e "${YW}⚠ Required value not provided - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("(unnamed)")
fi
echo "$fallback"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo -e "${YW}⚠ Not interactive - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-unnamed}")
echo "$fallback"
return 0
fi
# Interactive prompt - loop until non-empty input or use fallback on timeout
local attempts=0
while [[ -z "$response" ]]; do
attempts=$((attempts + 1))
if [[ $attempts -gt 3 ]]; then
echo -e "${YW}Too many empty inputs - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-manual_input}")
echo "$fallback"
return 0
fi
echo -en "${YW}${message} (required, timeout ${timeout}s): ${CL}" >&2
if read -t "$timeout" -r response; then
if [[ -z "$response" ]]; then
echo -e "${YW}This field is required. Please enter a value. (attempt ${attempts}/3)${CL}" >&2
fi
else
# Timeout occurred - use fallback
echo "" >&2
echo -e "${YW}Timeout - using fallback value: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-timeout}")
echo "$fallback"
return 0
fi
done
echo "$response"
}
# ------------------------------------------------------------------------------
# prompt_select()
#
# - Prompts user to select from a list of options with timeout support
# - In unattended mode: immediately returns default selection
# - In interactive mode: displays numbered menu and waits for choice
# - After timeout: auto-applies default selection
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default option number, 1-based (optional, default: 1)
# $3 - Timeout in seconds (optional, default: 60)
# $4+ - Options to display (required, at least 2)
#
# Output:
# Prints the selected option value to stdout
#
# Returns:
# 0 - Success
# 1 - No options provided or invalid state
#
# Example:
# choice=$(prompt_select "Select database:" 1 30 "PostgreSQL" "MySQL" "SQLite")
# echo "Selected: $choice"
#
# # With array
# options=("Option A" "Option B" "Option C")
# selected=$(prompt_select "Choose:" 2 60 "${options[@]}")
# ------------------------------------------------------------------------------
prompt_select() {
local message="${1:-Select option:}"
local default="${2:-1}"
local timeout="${3:-60}"
shift 3
local options=("$@")
local num_options=${#options[@]}
# Validate options
if [[ $num_options -eq 0 ]]; then
echo "" >&2
return 1
fi
# Validate default
if [[ ! "$default" =~ ^[0-9]+$ ]] || [[ "$default" -lt 1 ]] || [[ "$default" -gt "$num_options" ]]; then
default=1
fi
# Unattended mode: return default immediately
if is_unattended; then
echo "${options[$((default - 1))]}"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo "${options[$((default - 1))]}"
return 0
fi
# Display menu
echo -e "${YW}${message}${CL}" >&2
local i
for i in "${!options[@]}"; do
local num=$((i + 1))
if [[ $num -eq $default ]]; then
echo -e " ${GN}${num})${CL} ${options[$i]} ${YW}(default)${CL}" >&2
else
echo -e " ${GN}${num})${CL} ${options[$i]}" >&2
fi
done
# Interactive prompt with timeout
echo -en "${YW}Select [1-${num_options}] (auto-select ${default} in ${timeout}s): ${CL}" >&2
local response
if read -t "$timeout" -r response; then
if [[ -z "$response" ]]; then
# Empty response, use default
echo "${options[$((default - 1))]}"
elif [[ "$response" =~ ^[0-9]+$ ]] && [[ "$response" -ge 1 ]] && [[ "$response" -le "$num_options" ]]; then
# Valid selection
echo "${options[$((response - 1))]}"
else
# Invalid input, use default
echo -e "${YW}Invalid selection, using default: ${options[$((default - 1))]}${CL}" >&2
echo "${options[$((default - 1))]}"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - auto-selecting: ${options[$((default - 1))]}${CL}" >&2
echo "${options[$((default - 1))]}"
fi
}
# ------------------------------------------------------------------------------
# prompt_password()
#
# - Prompts user for password input with hidden characters
# - In unattended mode: returns default or generates random password
# - Supports auto-generation of secure passwords
# - After timeout: generates random password if allowed
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value or "generate" for auto-generation (optional)
# $3 - Timeout in seconds (optional, default: 60)
# $4 - Minimum length for validation (optional, default: 0 = no minimum)
#
# Output:
# Prints the password to stdout
#
# Example:
# password=$(prompt_password "Enter password:" "generate" 30 8)
# echo "Password set"
#
# # Require user input (no default)
# db_pass=$(prompt_password "Database password:" "" 60 12)
# ------------------------------------------------------------------------------
prompt_password() {
local message="${1:-Enter password:}"
local default="${2:-}"
local timeout="${3:-60}"
local min_length="${4:-0}"
local response
# Generate random password if requested
local generated=""
if [[ "$default" == "generate" ]]; then
generated=$(openssl rand -base64 16 2>/dev/null | tr -dc 'a-zA-Z0-9' | head -c 16)
[[ -z "$generated" ]] && generated=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16)
default="$generated"
fi
# Unattended mode: return default immediately
if is_unattended; then
echo "$default"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo "$default"
return 0
fi
# Build hint
local hint=""
if [[ -n "$generated" ]]; then
hint=" (Enter for auto-generated)"
elif [[ -n "$default" ]]; then
hint=" (Enter for default)"
fi
[[ "$min_length" -gt 0 ]] && hint="${hint} [min ${min_length} chars]"
# Interactive prompt with timeout (silent input)
echo -en "${YW}${message}${hint} (timeout ${timeout}s): ${CL}" >&2
if read -t "$timeout" -rs response; then
echo "" >&2 # Newline after hidden input
if [[ -n "$response" ]]; then
# Validate minimum length
if [[ "$min_length" -gt 0 ]] && [[ ${#response} -lt "$min_length" ]]; then
echo -e "${YW}Password too short (min ${min_length}), using default${CL}" >&2
echo "$default"
else
echo "$response"
fi
else
echo "$default"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - using generated password${CL}" >&2
echo "$default"
fi
}
# ============================================================================== # ==============================================================================
# SECTION 6: CLEANUP & MAINTENANCE # SECTION 6: CLEANUP & MAINTENANCE
# ============================================================================== # ==============================================================================
@@ -1518,13 +896,15 @@ check_or_create_swap() {
msg_error "No active swap detected" msg_error "No active swap detected"
if ! prompt_confirm "Do you want to create a swap file?" "n" 60; then read -p "Do you want to create a swap file? [y/N]: " create_swap
create_swap="${create_swap,,}" # to lowercase
if [[ "$create_swap" != "y" && "$create_swap" != "yes" ]]; then
msg_info "Skipping swap file creation" msg_info "Skipping swap file creation"
return 1 return 1
fi fi
local swap_size_mb read -p "Enter swap size in MB (e.g., 2048 for 2GB): " swap_size_mb
swap_size_mb=$(prompt_input "Enter swap size in MB (e.g., 2048 for 2GB):" "2048" 60)
if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then
msg_error "Invalid size input. Aborting." msg_error "Invalid size input. Aborting."
return 1 return 1

View File

@@ -27,54 +27,70 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# explain_exit_code() # explain_exit_code()
# #
# - Canonical version is defined in api.func (sourced before this file) # - Maps numeric exit codes to human-readable error descriptions
# - This section only provides a fallback if api.func was not loaded # - Supports:
# - See api.func SECTION 1 for the authoritative exit code mappings # * Generic/Shell errors (1, 2, 126, 127, 128, 130, 137, 139, 143)
# * Package manager errors (APT, DPKG: 100, 101, 255)
# * Node.js/npm errors (243-249, 254)
# * Python/pip/uv errors (210-212)
# * PostgreSQL errors (231-234)
# * MySQL/MariaDB errors (241-244)
# * MongoDB errors (251-254)
# * Proxmox custom codes (200-231)
# - Returns description string for given exit code
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
if ! declare -f explain_exit_code &>/dev/null; then explain_exit_code() {
explain_exit_code() {
local code="$1" local code="$1"
case "$code" in case "$code" in
# --- Generic / Shell ---
1) echo "General error / Operation not permitted" ;; 1) echo "General error / Operation not permitted" ;;
2) echo "Misuse of shell builtins (e.g. syntax error)" ;; 2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
6) echo "curl: DNS resolution failed (could not resolve host)" ;;
7) echo "curl: Failed to connect (network unreachable / host down)" ;;
22) echo "curl: HTTP error returned (404, 429, 500+)" ;;
28) echo "curl: Operation timeout (network slow or server not responding)" ;;
35) echo "curl: SSL/TLS handshake failed (certificate error)" ;;
100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
102) echo "APT: Lock held by another process (dpkg/apt still running)" ;;
124) echo "Command timed out (timeout command)" ;;
126) echo "Command invoked cannot execute (permission problem?)" ;; 126) echo "Command invoked cannot execute (permission problem?)" ;;
127) echo "Command not found" ;; 127) echo "Command not found" ;;
128) echo "Invalid argument to exit" ;; 128) echo "Invalid argument to exit" ;;
130) echo "Terminated by Ctrl+C (SIGINT)" ;; 130) echo "Terminated by Ctrl+C (SIGINT)" ;;
134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;;
137) echo "Killed (SIGKILL / Out of memory?)" ;; 137) echo "Killed (SIGKILL / Out of memory?)" ;;
139) echo "Segmentation fault (core dumped)" ;; 139) echo "Segmentation fault (core dumped)" ;;
141) echo "Broken pipe (SIGPIPE - output closed prematurely)" ;;
143) echo "Terminated (SIGTERM)" ;; 143) echo "Terminated (SIGTERM)" ;;
150) echo "Systemd: Service failed to start" ;;
151) echo "Systemd: Service unit not found" ;; # --- Package manager / APT / DPKG ---
152) echo "Permission denied (EACCES)" ;; 100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
153) echo "Build/compile failed (make/gcc/cmake)" ;; 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
154) echo "Node.js: Native addon build failed (node-gyp)" ;; 255) echo "DPKG: Fatal internal error" ;;
160) echo "Python: Virtualenv / uv environment missing or broken" ;;
161) echo "Python: Dependency resolution failed" ;; # --- Node.js / npm / pnpm / yarn ---
162) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;; 243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
170) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;; 245) echo "Node.js: Invalid command-line option" ;;
171) echo "PostgreSQL: Authentication failed (bad user/password)" ;; 246) echo "Node.js: Internal JavaScript Parse Error" ;;
172) echo "PostgreSQL: Database does not exist" ;; 247) echo "Node.js: Fatal internal error" ;;
173) echo "PostgreSQL: Fatal error in query / syntax" ;; 248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
180) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;; 249) echo "Node.js: Inspector error" ;;
181) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;; 254) echo "npm/pnpm/yarn: Unknown fatal error" ;;
182) echo "MySQL/MariaDB: Database does not exist" ;;
183) echo "MySQL/MariaDB: Fatal error in query / syntax" ;; # --- Python / pip / uv ---
190) echo "MongoDB: Connection failed (server not running)" ;; 210) echo "Python: Virtualenv / uv environment missing or broken" ;;
191) echo "MongoDB: Authentication failed (bad user/password)" ;; 211) echo "Python: Dependency resolution failed" ;;
192) echo "MongoDB: Database not found" ;; 212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
193) echo "MongoDB: Fatal query error" ;;
# --- PostgreSQL ---
231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
232) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
233) echo "PostgreSQL: Database does not exist" ;;
234) echo "PostgreSQL: Fatal error in query / syntax" ;;
# --- MySQL / MariaDB ---
241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
243) echo "MySQL/MariaDB: Database does not exist" ;;
244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
# --- MongoDB ---
251) echo "MongoDB: Connection failed (server not running)" ;;
252) echo "MongoDB: Authentication failed (bad user/password)" ;;
253) echo "MongoDB: Database not found" ;;
254) echo "MongoDB: Fatal query error" ;;
# --- Proxmox Custom Codes ---
200) echo "Proxmox: Failed to create lock file" ;; 200) echo "Proxmox: Failed to create lock file" ;;
203) echo "Proxmox: Missing CTID variable" ;; 203) echo "Proxmox: Missing CTID variable" ;;
204) echo "Proxmox: Missing PCT_OSTYPE variable" ;; 204) echo "Proxmox: Missing PCT_OSTYPE variable" ;;
@@ -91,26 +107,20 @@ if ! declare -f explain_exit_code &>/dev/null; then
215) echo "Proxmox: Container created but not listed (ghost state)" ;; 215) echo "Proxmox: Container created but not listed (ghost state)" ;;
216) echo "Proxmox: RootFS entry missing in config" ;; 216) echo "Proxmox: RootFS entry missing in config" ;;
217) echo "Proxmox: Storage not accessible" ;; 217) echo "Proxmox: Storage not accessible" ;;
218) echo "Proxmox: Template file corrupted or incomplete" ;;
219) echo "Proxmox: CephFS does not support containers - use RBD" ;; 219) echo "Proxmox: CephFS does not support containers - use RBD" ;;
224) echo "Proxmox: PBS storage is for backups only" ;;
218) echo "Proxmox: Template file corrupted or incomplete" ;;
220) echo "Proxmox: Unable to resolve template path" ;; 220) echo "Proxmox: Unable to resolve template path" ;;
221) echo "Proxmox: Template file not readable" ;; 221) echo "Proxmox: Template file not readable" ;;
222) echo "Proxmox: Template download failed" ;; 222) echo "Proxmox: Template download failed" ;;
223) echo "Proxmox: Template not available after download" ;; 223) echo "Proxmox: Template not available after download" ;;
224) echo "Proxmox: PBS storage is for backups only" ;;
225) echo "Proxmox: No template available for OS/Version" ;; 225) echo "Proxmox: No template available for OS/Version" ;;
231) echo "Proxmox: LXC stack upgrade failed" ;; 231) echo "Proxmox: LXC stack upgrade failed" ;;
243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
245) echo "Node.js: Invalid command-line option" ;; # --- Default ---
246) echo "Node.js: Internal JavaScript Parse Error" ;;
247) echo "Node.js: Fatal internal error" ;;
248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
249) echo "npm/pnpm/yarn: Unknown fatal error" ;;
255) echo "DPKG: Fatal internal error" ;;
*) echo "Unknown error" ;; *) echo "Unknown error" ;;
esac esac
} }
fi
# ============================================================================== # ==============================================================================
# SECTION 2: ERROR HANDLERS # SECTION 2: ERROR HANDLERS
@@ -175,9 +185,9 @@ error_handler() {
fi fi
if [[ -n "$active_log" && -s "$active_log" ]]; then if [[ -n "$active_log" && -s "$active_log" ]]; then
echo -e "\n${TAB}--- Last 20 lines of log ---" echo "--- Last 20 lines of silent log ---"
tail -n 20 "$active_log" tail -n 20 "$active_log"
echo -e "${TAB}-----------------------------------\n" echo "-----------------------------------"
# Detect context: Container (INSTALL_LOG set + /root exists) vs Host (BUILD_LOG) # Detect context: Container (INSTALL_LOG set + /root exists) vs Host (BUILD_LOG)
if [[ -n "${INSTALL_LOG:-}" && -d /root ]]; then if [[ -n "${INSTALL_LOG:-}" && -d /root ]]; then
@@ -187,7 +197,12 @@ error_handler() {
# Create error flag file with exit code for host detection # Create error flag file with exit code for host detection
echo "$exit_code" >"/root/.install-${SESSION_ID:-error}.failed" 2>/dev/null || true echo "$exit_code" >"/root/.install-${SESSION_ID:-error}.failed" 2>/dev/null || true
# Log path is shown by host as combined log - no need to show container path
if declare -f msg_custom >/dev/null 2>&1; then
msg_custom "📋" "${YW}" "Log saved to: ${container_log}"
else
echo -e "${YW}Log saved to:${CL} ${BL}${container_log}${CL}"
fi
else else
# HOST CONTEXT: Show local log path and offer container cleanup # HOST CONTEXT: Show local log path and offer container cleanup
if declare -f msg_custom >/dev/null 2>&1; then if declare -f msg_custom >/dev/null 2>&1; then
@@ -198,64 +213,26 @@ error_handler() {
# Offer to remove container if it exists (build errors after container creation) # Offer to remove container if it exists (build errors after container creation)
if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null && pct status "$CTID" &>/dev/null; then if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null && pct status "$CTID" &>/dev/null; then
# Report failure to API before container cleanup
if declare -f post_update_to_api &>/dev/null; then
post_update_to_api "failed" "$exit_code"
fi
echo "" echo ""
if declare -f msg_custom >/dev/null 2>&1; then
echo -en "${TAB}${TAB}${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
else
echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}" echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
fi
if read -t 60 -r response; then if read -t 60 -r response; then
if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
echo "" echo -e "\n${YW}Removing container ${CTID}${CL}"
if declare -f msg_info >/dev/null 2>&1; then
msg_info "Removing container ${CTID}"
else
echo -e "${YW}Removing container ${CTID}${CL}"
fi
pct stop "$CTID" &>/dev/null || true pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true pct destroy "$CTID" &>/dev/null || true
if declare -f msg_ok >/dev/null 2>&1; then
msg_ok "Container ${CTID} removed"
else
echo -e "${GN}${CL} Container ${CTID} removed" echo -e "${GN}${CL} Container ${CTID} removed"
fi
elif [[ "$response" =~ ^[Nn]$ ]]; then elif [[ "$response" =~ ^[Nn]$ ]]; then
echo "" echo -e "\n${YW}Container ${CTID} kept for debugging${CL}"
if declare -f msg_warn >/dev/null 2>&1; then
msg_warn "Container ${CTID} kept for debugging"
else
echo -e "${YW}Container ${CTID} kept for debugging${CL}"
fi
fi fi
else else
# Timeout - auto-remove # Timeout - auto-remove
echo "" echo -e "\n${YW}No response - auto-removing container${CL}"
if declare -f msg_info >/dev/null 2>&1; then
msg_info "No response - removing container ${CTID}"
else
echo -e "${YW}No response - removing container ${CTID}${CL}"
fi
pct stop "$CTID" &>/dev/null || true pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true pct destroy "$CTID" &>/dev/null || true
if declare -f msg_ok >/dev/null 2>&1; then
msg_ok "Container ${CTID} removed"
else
echo -e "${GN}${CL} Container ${CTID} removed" echo -e "${GN}${CL} Container ${CTID} removed"
fi fi
fi fi
# Force one final status update attempt after cleanup
# This ensures status is updated even if the first attempt failed (e.g., HTTP 400)
if declare -f post_update_to_api &>/dev/null; then
post_update_to_api "failed" "$exit_code" "force"
fi
fi
fi fi
fi fi
@@ -276,22 +253,6 @@ error_handler() {
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
on_exit() { on_exit() {
local exit_code=$? local exit_code=$?
# Report orphaned "installing" records to telemetry API
# Catches ALL exit paths: errors (non-zero), signals, AND clean exits where
# post_to_api was called ("installing" sent) but post_update_to_api was never called
if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then
if declare -f post_update_to_api >/dev/null 2>&1; then
# Ensure log is accessible on host before reporting
if declare -f ensure_log_on_host >/dev/null 2>&1; then
ensure_log_on_host
fi
if [[ $exit_code -ne 0 ]]; then
post_update_to_api "failed" "$exit_code"
else
post_update_to_api "failed" "1"
fi
fi
fi
[[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile" [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
exit "$exit_code" exit "$exit_code"
} }
@@ -304,14 +265,6 @@ on_exit() {
# - Exits with code 130 (128 + SIGINT=2) # - Exits with code 130 (128 + SIGINT=2)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
on_interrupt() { on_interrupt() {
# Ensure log is accessible on host before reporting
if declare -f ensure_log_on_host >/dev/null 2>&1; then
ensure_log_on_host
fi
# Report interruption to telemetry API (prevents stuck "installing" records)
if declare -f post_update_to_api >/dev/null 2>&1; then
post_update_to_api "failed" "130"
fi
if declare -f msg_error >/dev/null 2>&1; then if declare -f msg_error >/dev/null 2>&1; then
msg_error "Interrupted by user (SIGINT)" msg_error "Interrupted by user (SIGINT)"
else else
@@ -329,14 +282,6 @@ on_interrupt() {
# - Triggered by external process termination # - Triggered by external process termination
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
on_terminate() { on_terminate() {
# Ensure log is accessible on host before reporting
if declare -f ensure_log_on_host >/dev/null 2>&1; then
ensure_log_on_host
fi
# Report termination to telemetry API (prevents stuck "installing" records)
if declare -f post_update_to_api >/dev/null 2>&1; then
post_update_to_api "failed" "143"
fi
if declare -f msg_error >/dev/null 2>&1; then if declare -f msg_error >/dev/null 2>&1; then
msg_error "Terminated by signal (SIGTERM)" msg_error "Terminated by signal (SIGTERM)"
else else

View File

@@ -465,7 +465,6 @@ manage_tool_repository() {
msg_error "Failed to download MongoDB GPG key" msg_error "Failed to download MongoDB GPG key"
return 1 return 1
fi fi
chmod 644 "/etc/apt/keyrings/mongodb-server-${version}.gpg"
# Setup repository # Setup repository
local distro_codename local distro_codename
@@ -1295,33 +1294,12 @@ setup_deb822_repo() {
return 1 return 1
} }
# Import GPG key (auto-detect binary vs ASCII-armored format) # Import GPG
local tmp_gpg curl -fsSL "$gpg_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/${name}.gpg" || {
tmp_gpg=$(mktemp) || return 1 msg_error "Failed to import GPG key for ${name}"
curl -fsSL "$gpg_url" -o "$tmp_gpg" || {
msg_error "Failed to download GPG key for ${name}"
rm -f "$tmp_gpg"
return 1 return 1
} }
if grep -q "BEGIN PGP" "$tmp_gpg" 2>/dev/null; then
# ASCII-armored — dearmor to binary
gpg --dearmor --yes -o "/etc/apt/keyrings/${name}.gpg" < "$tmp_gpg" || {
msg_error "Failed to dearmor GPG key for ${name}"
rm -f "$tmp_gpg"
return 1
}
else
# Already in binary GPG format — copy directly
cp "$tmp_gpg" "/etc/apt/keyrings/${name}.gpg" || {
msg_error "Failed to install GPG key for ${name}"
rm -f "$tmp_gpg"
return 1
}
fi
rm -f "$tmp_gpg"
chmod 644 "/etc/apt/keyrings/${name}.gpg"
# Write deb822 # Write deb822
{ {
echo "Types: deb" echo "Types: deb"
@@ -1913,7 +1891,7 @@ function fetch_and_deploy_codeberg_release() {
local app="$1" local app="$1"
local repo="$2" local repo="$2"
local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile | tag local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile | tag
local version="${var_appversion:-${4:-latest}}" local version="${4:-latest}"
local target="${5:-/opt/$app}" local target="${5:-/opt/$app}"
local asset_pattern="${6:-}" local asset_pattern="${6:-}"
@@ -2443,7 +2421,7 @@ function fetch_and_deploy_gh_release() {
local app="$1" local app="$1"
local repo="$2" local repo="$2"
local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile
local version="${var_appversion:-${4:-latest}}" local version="${4:-latest}"
local target="${5:-/opt/$app}" local target="${5:-/opt/$app}"
local asset_pattern="${6:-}" local asset_pattern="${6:-}"

View File

@@ -207,9 +207,15 @@ silent() {
msg_custom "→" "${YWB}" "${cmd}" msg_custom "→" "${YWB}" "${cmd}"
if [[ -s "$logfile" ]]; then if [[ -s "$logfile" ]]; then
echo -e "\n${TAB}--- Last 10 lines of log ---" local log_lines=$(wc -l <"$logfile")
echo "--- Last 10 lines of log ---"
tail -n 10 "$logfile" tail -n 10 "$logfile"
echo -e "${TAB}----------------------------\n" echo "----------------------------"
# Show how to view full log if there are more lines
if [[ $log_lines -gt 10 ]]; then
msg_custom "📋" "${YW}" "View full log (${log_lines} lines): ${logfile}"
fi
fi fi
exit "$rc" exit "$rc"

View File

@@ -75,37 +75,14 @@ pct exec "$CTID" -- bash -c '
set -e set -e
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
# Source os-release properly (handles quoted values) ID=$(grep "^ID=" /etc/os-release | cut -d"=" -f2)
source /etc/os-release VER=$(grep "^VERSION_CODENAME=" /etc/os-release | cut -d"=" -f2)
# Fallback if DNS is poisoned or blocked # fallback if DNS is poisoned or blocked
ORIG_RESOLV="/etc/resolv.conf" ORIG_RESOLV="/etc/resolv.conf"
BACKUP_RESOLV="/tmp/resolv.conf.backup" BACKUP_RESOLV="/tmp/resolv.conf.backup"
# Check DNS resolution using multiple methods (dig may not be installed) if ! dig +short pkgs.tailscale.com | grep -qvE "^127\.|^0\.0\.0\.0$"; then
dns_check_failed=true
if command -v dig &>/dev/null; then
if dig +short pkgs.tailscale.com 2>/dev/null | grep -qvE "^127\.|^0\.0\.0\.0$|^$"; then
dns_check_failed=false
fi
elif command -v host &>/dev/null; then
if host pkgs.tailscale.com 2>/dev/null | grep -q "has address"; then
dns_check_failed=false
fi
elif command -v nslookup &>/dev/null; then
if nslookup pkgs.tailscale.com 2>/dev/null | grep -q "Address:"; then
dns_check_failed=false
fi
elif command -v getent &>/dev/null; then
if getent hosts pkgs.tailscale.com &>/dev/null; then
dns_check_failed=false
fi
else
# No DNS tools available, try curl directly and assume DNS works
dns_check_failed=false
fi
if $dns_check_failed; then
echo "[INFO] DNS resolution for pkgs.tailscale.com failed (blocked or redirected)." echo "[INFO] DNS resolution for pkgs.tailscale.com failed (blocked or redirected)."
echo "[INFO] Temporarily overriding /etc/resolv.conf with Cloudflare DNS (1.1.1.1)" echo "[INFO] Temporarily overriding /etc/resolv.conf with Cloudflare DNS (1.1.1.1)"
cp "$ORIG_RESOLV" "$BACKUP_RESOLV" cp "$ORIG_RESOLV" "$BACKUP_RESOLV"
@@ -115,22 +92,17 @@ fi
if ! command -v curl &>/dev/null; then if ! command -v curl &>/dev/null; then
echo "[INFO] curl not found, installing..." echo "[INFO] curl not found, installing..."
apt-get update -qq apt-get update -qq
apt update -qq apt-get install -y curl >/dev/null
apt install -y curl >/dev/null
fi fi
# Ensure keyrings directory exists curl -fsSL https://pkgs.tailscale.com/stable/${ID}/${VER}.noarmor.gpg \
mkdir -p /usr/share/keyrings
curl -fsSL "https://pkgs.tailscale.com/stable/${ID}/${VERSION_CODENAME}.noarmor.gpg" \
| tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/${ID} ${VERSION_CODENAME} main" \ echo "deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/${ID} ${VER} main" \
>/etc/apt/sources.list.d/tailscale.list >/etc/apt/sources.list.d/tailscale.list
apt-get update -qq apt-get update -qq
apt update -qq apt-get install -y tailscale >/dev/null
apt install -y tailscale >/dev/null
if [[ -f /tmp/resolv.conf.backup ]]; then if [[ -f /tmp/resolv.conf.backup ]]; then
echo "[INFO] Restoring original /etc/resolv.conf" echo "[INFO] Restoring original /etc/resolv.conf"

View File

@@ -110,11 +110,6 @@ for container in $(pct list | awk '{if(NR>1) print $1}'); do
container_hostname=$(pct exec "$container" hostname) container_hostname=$(pct exec "$container" hostname)
containers_needing_reboot+=("$container ($container_hostname)") containers_needing_reboot+=("$container ($container_hostname)")
fi fi
# check if patchmon agent is present in container and run a report if found
if pct exec "$container" -- [ -e "/usr/local/bin/patchmon-agent" ]; then
echo -e "${BL}[Info]${GN} patchmon-agent found in ${BL} $container ${CL}, triggering report. \n"
pct exec "$container" -- "/usr/local/bin/patchmon-agent" "report"
fi
fi fi
done done
wait wait

View File

@@ -70,7 +70,7 @@ function error_handler() {
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
post_update_to_api "failed" "${exit_code}" post_update_to_api "failed" "${command}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
} }
@@ -203,6 +203,7 @@ function exit-script() {
function default_settings() { function default_settings() {
VMID=$(get_valid_nextid) VMID=$(get_valid_nextid)
FORMAT=",efitype=4m"
MACHINE="" MACHINE=""
DISK_SIZE="4G" DISK_SIZE="4G"
DISK_CACHE="" DISK_CACHE=""
@@ -258,9 +259,11 @@ function advanced_settings() {
3>&1 1>&2 2>&3); then 3>&1 1>&2 2>&3); then
if [ "$MACH" = q35 ]; then if [ "$MACH" = q35 ]; then
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}" echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
FORMAT=""
MACHINE=" -machine q35" MACHINE=" -machine q35"
else else
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}" echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
FORMAT=",efitype=4m"
MACHINE="" MACHINE=""
fi fi
else else
@@ -473,45 +476,31 @@ case $STORAGE_TYPE in
nfs | dir | cifs) nfs | dir | cifs)
DISK_EXT=".qcow2" DISK_EXT=".qcow2"
DISK_REF="$VMID/" DISK_REF="$VMID/"
DISK_IMPORT="--format qcow2" DISK_IMPORT="-format qcow2"
THIN="" THIN=""
;; ;;
btrfs) btrfs)
DISK_EXT=".raw" DISK_EXT=".raw"
DISK_REF="$VMID/" DISK_REF="$VMID/"
DISK_IMPORT="--format raw" DISK_IMPORT="-format raw"
FORMAT=",efitype=4m"
THIN="" THIN=""
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="--format raw"
;;
esac esac
for i in {0,1}; do
disk="DISK$i"
eval DISK"${i}"=vm-"${VMID}"-disk-"${i}"${DISK_EXT:-}
eval DISK"${i}"_REF="${STORAGE}":"${DISK_REF:-}"${!disk}
done
msg_info "Creating a Arch Linux VM" msg_info "Creating a Arch Linux VM"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \ qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
-name $HN -tags community-script -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci -name $HN -tags community-script -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
pvesm alloc $STORAGE $VMID $DISK0 4M 1>&/dev/null
if qm disk import --help >/dev/null 2>&1; then qm importdisk $VMID ${FILE} $STORAGE ${DISK_IMPORT:-} 1>&/dev/null
IMPORT_CMD=(qm disk import)
else
IMPORT_CMD=(qm importdisk)
fi
IMPORT_OUT="$("${IMPORT_CMD[@]}" "$VMID" "${FILE}" "$STORAGE" ${DISK_IMPORT:-} 2>&1 || true)"
DISK_REF_IMPORTED="$(printf '%s\n' "$IMPORT_OUT" | sed -n "s/.*successfully imported disk '\([^']\+\)'.*/\1/p" | tr -d "\r\"'")"
[[ -z "$DISK_REF_IMPORTED" ]] && DISK_REF_IMPORTED="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$5 ~ ("vm-"id"-disk-") {print $1":"$5}' | sort | tail -n1)"
[[ -z "$DISK_REF_IMPORTED" ]] && {
msg_error "Unable to determine imported disk reference."
echo "$IMPORT_OUT"
exit 1
}
msg_ok "Imported disk (${CL}${BL}${DISK_REF_IMPORTED}${CL})"
qm set $VMID \ qm set $VMID \
-efidisk0 ${STORAGE}:0,efitype=4m \ -efidisk0 ${DISK0_REF}${FORMAT} \
-scsi0 ${DISK_REF_IMPORTED},${DISK_CACHE}${THIN%,} \ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
-ide2 ${STORAGE}:cloudinit \ -ide2 ${STORAGE}:cloudinit \
-boot order=scsi0 \ -boot order=scsi0 \
-serial0 socket >/dev/null -serial0 socket >/dev/null

View File

@@ -70,7 +70,7 @@ function error_handler() {
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
post_update_to_api "failed" "${exit_code}" post_update_to_api "failed" "${command}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
} }
@@ -201,17 +201,6 @@ function exit-script() {
exit exit
} }
function select_cloud_init() {
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" \
--yesno "Enable Cloud-Init for VM configuration?\n\nCloud-Init allows automatic configuration of:\n- User accounts and passwords\n- SSH keys\n- Network settings (DHCP/Static)\n- DNS configuration\n\nYou can also configure these settings later in Proxmox UI.\n\nNote: Without Cloud-Init, the nocloud image will be used with console auto-login." --defaultno 18 68); then
CLOUD_INIT="yes"
echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}yes${CL}"
else
CLOUD_INIT="no"
echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}no${CL}"
fi
}
function default_settings() { function default_settings() {
VMID=$(get_valid_nextid) VMID=$(get_valid_nextid)
FORMAT=",efitype=4m" FORMAT=",efitype=4m"
@@ -227,6 +216,7 @@ function default_settings() {
VLAN="" VLAN=""
MTU="" MTU=""
START_VM="yes" START_VM="yes"
CLOUD_INIT="no"
METHOD="default" METHOD="default"
echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}" echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}" echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
@@ -240,7 +230,7 @@ function default_settings() {
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}" echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}" echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}" echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
select_cloud_init echo -e "${CLOUD}${BOLD}${DGN}Configure Cloud-init: ${BGN}no${CL}"
echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}" echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
echo -e "${CREATING}${BOLD}${DGN}Creating a Debian 13 VM using the above default settings${CL}" echo -e "${CREATING}${BOLD}${DGN}Creating a Debian 13 VM using the above default settings${CL}"
} }
@@ -410,7 +400,13 @@ function advanced_settings() {
exit-script exit-script
fi fi
select_cloud_init if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" --yesno "Configure the VM with Cloud-init?" --defaultno 10 58); then
echo -e "${CLOUD}${BOLD}${DGN}Configure Cloud-init: ${BGN}yes${CL}"
CLOUD_INIT="yes"
else
echo -e "${CLOUD}${BOLD}${DGN}Configure Cloud-init: ${BGN}no${CL}"
CLOUD_INIT="no"
fi
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}" echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
@@ -477,17 +473,6 @@ else
fi fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location." msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}." msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
# ==============================================================================
# PREREQUISITES
# ==============================================================================
if ! command -v virt-customize &>/dev/null; then
msg_info "Installing libguestfs-tools"
apt-get update >/dev/null 2>&1
apt-get install -y libguestfs-tools >/dev/null 2>&1
msg_ok "Installed libguestfs-tools"
fi
msg_info "Retrieving the URL for the Debian 13 Qcow2 Disk Image" msg_info "Retrieving the URL for the Debian 13 Qcow2 Disk Image"
if [ "$CLOUD_INIT" == "yes" ]; then if [ "$CLOUD_INIT" == "yes" ]; then
URL=https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 URL=https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2
@@ -501,50 +486,6 @@ echo -en "\e[1A\e[0K"
FILE=$(basename $URL) FILE=$(basename $URL)
msg_ok "Downloaded ${CL}${BL}${FILE}${CL}" msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
# ==============================================================================
# IMAGE CUSTOMIZATION
# ==============================================================================
msg_info "Customizing ${FILE} image"
WORK_FILE=$(mktemp --suffix=.qcow2)
cp "$FILE" "$WORK_FILE"
# Set hostname
virt-customize -q -a "$WORK_FILE" --hostname "${HN}" >/dev/null 2>&1
# Prepare for unique machine-id on first boot
virt-customize -q -a "$WORK_FILE" --run-command "truncate -s 0 /etc/machine-id" >/dev/null 2>&1
virt-customize -q -a "$WORK_FILE" --run-command "rm -f /var/lib/dbus/machine-id" >/dev/null 2>&1
# Disable systemd-firstboot to prevent interactive prompts blocking the console
virt-customize -q -a "$WORK_FILE" --run-command "systemctl disable systemd-firstboot.service 2>/dev/null; rm -f /etc/systemd/system/sysinit.target.wants/systemd-firstboot.service; ln -sf /dev/null /etc/systemd/system/systemd-firstboot.service" >/dev/null 2>&1 || true
# Pre-seed firstboot settings so it won't prompt even if triggered
virt-customize -q -a "$WORK_FILE" --run-command "echo 'Etc/UTC' > /etc/timezone && ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime" >/dev/null 2>&1 || true
virt-customize -q -a "$WORK_FILE" --run-command "touch /etc/locale.conf" >/dev/null 2>&1 || true
if [ "$CLOUD_INIT" == "yes" ]; then
# Cloud-Init handles SSH and login
virt-customize -q -a "$WORK_FILE" --run-command "sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config" >/dev/null 2>&1 || true
virt-customize -q -a "$WORK_FILE" --run-command "sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config" >/dev/null 2>&1 || true
else
# Configure auto-login on serial console (ttyS0) and virtual console (tty1)
virt-customize -q -a "$WORK_FILE" --run-command "mkdir -p /etc/systemd/system/serial-getty@ttyS0.service.d" >/dev/null 2>&1 || true
virt-customize -q -a "$WORK_FILE" --run-command 'cat > /etc/systemd/system/serial-getty@ttyS0.service.d/autologin.conf << EOF
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear %I \$TERM
EOF' >/dev/null 2>&1 || true
virt-customize -q -a "$WORK_FILE" --run-command "mkdir -p /etc/systemd/system/getty@tty1.service.d" >/dev/null 2>&1 || true
virt-customize -q -a "$WORK_FILE" --run-command 'cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << EOF
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear %I \$TERM
EOF' >/dev/null 2>&1 || true
fi
msg_ok "Customized image"
STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}') STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in case $STORAGE_TYPE in
nfs | dir) nfs | dir)
@@ -560,11 +501,6 @@ btrfs)
FORMAT=",efitype=4m" FORMAT=",efitype=4m"
THIN="" THIN=""
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="-format raw"
;;
esac esac
for i in {0,1}; do for i in {0,1}; do
disk="DISK$i" disk="DISK$i"
@@ -576,7 +512,7 @@ msg_info "Creating a Debian 13 VM"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \ qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
-name $HN -tags community-script -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci -name $HN -tags community-script -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
pvesm alloc $STORAGE $VMID $DISK0 4M 1>&/dev/null pvesm alloc $STORAGE $VMID $DISK0 4M 1>&/dev/null
qm importdisk $VMID ${WORK_FILE} $STORAGE ${DISK_IMPORT:-} 1>&/dev/null qm importdisk $VMID ${FILE} $STORAGE ${DISK_IMPORT:-} 1>&/dev/null
if [ "$CLOUD_INIT" == "yes" ]; then if [ "$CLOUD_INIT" == "yes" ]; then
qm set $VMID \ qm set $VMID \
-efidisk0 ${DISK0_REF}${FORMAT} \ -efidisk0 ${DISK0_REF}${FORMAT} \
@@ -591,10 +527,6 @@ else
-boot order=scsi0 \ -boot order=scsi0 \
-serial0 socket >/dev/null -serial0 socket >/dev/null
fi fi
# Clean up work file
rm -f "$WORK_FILE"
DESCRIPTION=$( DESCRIPTION=$(
cat <<EOF cat <<EOF
<div align='center'> <div align='center'>

View File

@@ -70,7 +70,7 @@ function error_handler() {
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
post_update_to_api "failed" "${exit_code}" post_update_to_api "failed" "${command}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
} }
@@ -501,11 +501,6 @@ btrfs)
FORMAT=",efitype=4m" FORMAT=",efitype=4m"
THIN="" THIN=""
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="-format raw"
;;
esac esac
for i in {0,1}; do for i in {0,1}; do
disk="DISK$i" disk="DISK$i"

View File

@@ -45,7 +45,7 @@ function error_handler() {
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
post_update_to_api "failed" "${exit_code}" post_update_to_api "failed" "${command}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
} }

View File

@@ -74,7 +74,7 @@ function error_handler() {
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
post_update_to_api "failed" "${exit_code}" post_update_to_api "failed" "${command}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
} }

View File

@@ -71,7 +71,7 @@ function error_handler() {
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
post_update_to_api "failed" "${exit_code}" post_update_to_api "failed" "${command}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
} }
@@ -566,11 +566,6 @@ zfspool)
DISK_REF="" DISK_REF=""
DISK_IMPORT="-format raw" DISK_IMPORT="-format raw"
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="-format raw"
;;
esac esac
DISK_VAR="vm-${VMID}-disk-0${DISK_EXT:-}" DISK_VAR="vm-${VMID}-disk-0${DISK_EXT:-}"

View File

@@ -70,7 +70,7 @@ function error_handler() {
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
post_update_to_api "failed" "${exit_code}" post_update_to_api "failed" "${command}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
} }
@@ -487,11 +487,6 @@ btrfs)
FORMAT=",efitype=4m" FORMAT=",efitype=4m"
THIN="" THIN=""
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="-format raw"
;;
esac esac
for i in {0,1,2}; do for i in {0,1,2}; do
disk="DISK$i" disk="DISK$i"

View File

@@ -74,7 +74,7 @@ function error_handler() {
local exit_code="$?" local exit_code="$?"
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
post_update_to_api "failed" "$exit_code" post_update_to_api "failed" "$command"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid

View File

@@ -48,7 +48,7 @@ function error_handler() {
local exit_code="$?" local exit_code="$?"
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
post_update_to_api "failed" "$exit_code" post_update_to_api "failed" "$command"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
@@ -619,11 +619,6 @@ btrfs)
FORMAT=",efitype=4m" FORMAT=",efitype=4m"
THIN="" THIN=""
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="-format raw"
;;
esac esac
for i in {0,1}; do for i in {0,1}; do
disk="DISK$i" disk="DISK$i"

View File

@@ -71,7 +71,7 @@ function error_handler() {
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
post_update_to_api "failed" "${exit_code}" post_update_to_api "failed" "${command}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
} }
@@ -500,11 +500,6 @@ btrfs)
FORMAT=",efitype=4m" FORMAT=",efitype=4m"
THIN="" THIN=""
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="-format raw"
;;
esac esac
for i in {0,1,2}; do for i in {0,1,2}; do
disk="DISK$i" disk="DISK$i"

View File

@@ -79,7 +79,7 @@ function error_handler() {
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
post_update_to_api "failed" "${exit_code}" post_update_to_api "failed" "${command}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
} }
@@ -402,11 +402,6 @@ nfs | dir)
DISK_REF="$VMID/" DISK_REF="$VMID/"
DISK_IMPORT="-format qcow2" DISK_IMPORT="-format qcow2"
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="-format raw"
;;
esac esac
for i in {0,1}; do for i in {0,1}; do
disk="DISK$i" disk="DISK$i"

View File

@@ -66,7 +66,7 @@ function error_handler() {
local exit_code="$?" local exit_code="$?"
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
post_update_to_api "failed" "$exit_code" post_update_to_api "failed" "$command"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
@@ -482,11 +482,6 @@ btrfs)
FORMAT=",efitype=4m" FORMAT=",efitype=4m"
THIN="" THIN=""
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="-format raw"
;;
esac esac
for i in {0,1}; do for i in {0,1}; do
disk="DISK$i" disk="DISK$i"

View File

@@ -69,7 +69,7 @@ function error_handler() {
local exit_code="$?" local exit_code="$?"
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
post_update_to_api "failed" "$exit_code" post_update_to_api "failed" "$command"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
@@ -484,11 +484,6 @@ btrfs)
FORMAT=",efitype=4m" FORMAT=",efitype=4m"
THIN="" THIN=""
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="-format raw"
;;
esac esac
for i in {0,1}; do for i in {0,1}; do
disk="DISK$i" disk="DISK$i"

View File

@@ -68,7 +68,7 @@ function error_handler() {
local exit_code="$?" local exit_code="$?"
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
post_update_to_api "failed" "$exit_code" post_update_to_api "failed" "$command"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
@@ -483,11 +483,6 @@ btrfs)
FORMAT=",efitype=4m" FORMAT=",efitype=4m"
THIN="" THIN=""
;; ;;
*)
DISK_EXT=""
DISK_REF=""
DISK_IMPORT="-format raw"
;;
esac esac
for i in {0,1}; do for i in {0,1}; do
disk="DISK$i" disk="DISK$i"

View File

@@ -69,7 +69,7 @@ function error_handler() {
local line_number="$1" local line_number="$1"
local command="$2" local command="$2"
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
post_update_to_api "failed" "${exit_code}" post_update_to_api "failed" "${command}"
echo -e "\n$error_message\n" echo -e "\n$error_message\n"
cleanup_vmid cleanup_vmid
} }