mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
支持限制用户可选择的系统
This commit is contained in:
@@ -240,6 +240,12 @@ func createContainer(w http.ResponseWriter, r *http.Request) {
|
||||
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Template is not enabled or downloaded"})
|
||||
return
|
||||
}
|
||||
if ids, err := normalizeAllowedImageIDs(cfg.AllowedImageIDs); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
} else {
|
||||
cfg.AllowedImageIDs = ids
|
||||
}
|
||||
if cfg.VCPU <= 0 {
|
||||
cfg.VCPU = 1
|
||||
}
|
||||
|
||||
@@ -22,12 +22,13 @@ import (
|
||||
)
|
||||
|
||||
type HostInfo struct {
|
||||
CPU CpuInfo `json:"cpu"`
|
||||
RAM MemoryInfo `json:"ram"`
|
||||
Disk DiskInfo `json:"disk"`
|
||||
Network NetworkInfo `json:"network"`
|
||||
DiskIO DiskIOInfo `json:"disk_io"`
|
||||
Load LoadInfo `json:"load"`
|
||||
CPU CpuInfo `json:"cpu"`
|
||||
RAM MemoryInfo `json:"ram"`
|
||||
Disk DiskInfo `json:"disk"`
|
||||
Network NetworkInfo `json:"network"`
|
||||
DiskIO DiskIOInfo `json:"disk_io"`
|
||||
Load LoadInfo `json:"load"`
|
||||
Runtime HostRuntimeProbe `json:"runtime"`
|
||||
}
|
||||
|
||||
type HostProbeReport struct {
|
||||
@@ -247,9 +248,32 @@ func getHostInfo() HostInfo {
|
||||
info.CPU.Usage = getCPUUsage()
|
||||
info.Network, info.DiskIO = getHostRates()
|
||||
info.Load = getLoadInfo()
|
||||
info.Runtime = detectRuntimeProbeQuick()
|
||||
return info
|
||||
}
|
||||
|
||||
func detectRuntimeProbeQuick() HostRuntimeProbe {
|
||||
devKVM := fileExists("/dev/kvm")
|
||||
nested, detail := detectNestedVirtualization()
|
||||
lxcOK := commandExists("lxc-create")
|
||||
kvmSupportedArch := runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64"
|
||||
kvmOK := kvmSupportedArch && devKVM && commandExists("virsh") && commandExists(kvmQEMUCheckKey())
|
||||
probe := HostRuntimeProbe{
|
||||
LXCAvailable: lxcOK,
|
||||
KVMAvailable: kvmOK,
|
||||
DevKVM: devKVM,
|
||||
NestedVirtualization: nested,
|
||||
NestedDetail: detail,
|
||||
SupportMode: "unsupported",
|
||||
}
|
||||
if probe.KVMAvailable {
|
||||
probe.SupportMode = "kvm_lxc"
|
||||
} else if probe.LXCAvailable {
|
||||
probe.SupportMode = "lxc_only"
|
||||
}
|
||||
return probe
|
||||
}
|
||||
|
||||
func getMemoryInfo() MemoryInfo {
|
||||
f, err := os.Open("/proc/meminfo")
|
||||
if err != nil {
|
||||
|
||||
@@ -541,6 +541,24 @@ func HandleEnabledImages(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
runtime := runtimeFromRequest(r.URL.Query().Get("type"))
|
||||
enabledSet := getEnabledImageSet()
|
||||
var subUser *config.SubUser
|
||||
var targetContainer *config.Container
|
||||
currentImageIDs := map[string]bool{}
|
||||
if isSubUserRequest(r) {
|
||||
subUser = subUserFromRequest(r)
|
||||
if identifier := r.URL.Query().Get("container"); identifier != "" {
|
||||
targetContainer = containerByIdentifier(identifier)
|
||||
if targetContainer == nil || !isContainerAllowedForRequest(r, identifier) {
|
||||
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Access denied to this container"})
|
||||
return
|
||||
}
|
||||
currentImageIDs[targetContainer.Template] = true
|
||||
} else {
|
||||
for _, id := range subUserCurrentImageIDs(subUser) {
|
||||
currentImageIDs[id] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]map[string]string, 0)
|
||||
if runtime == config.VirtualizationKVM {
|
||||
@@ -549,7 +567,10 @@ func HandleEnabledImages(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
for _, t := range kvm.GetImages() {
|
||||
if downloaded, _ := kvm.ImageDownloadedInfo(t.ID); enabledSet[t.ID] && downloaded {
|
||||
if subUser != nil && !isImageAllowedForSubUser(subUser, targetContainer, t.ID) {
|
||||
continue
|
||||
}
|
||||
if downloaded, _ := kvm.ImageDownloadedInfo(t.ID); downloaded && (enabledSet[t.ID] || currentImageIDs[t.ID]) {
|
||||
result = append(result, map[string]string{
|
||||
"id": t.ID, "name": t.Name, "distro": t.Distro, "release": t.Release, "arch": t.Arch,
|
||||
"description": t.Description, "type": config.VirtualizationKVM, "desktop": t.Desktop,
|
||||
@@ -558,7 +579,10 @@ func HandleEnabledImages(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
} else {
|
||||
for _, t := range lxc.GetTemplates() {
|
||||
if enabledSet[t.ID] && isImageDownloaded(t.Distro, t.Release, t.Arch) {
|
||||
if subUser != nil && !isImageAllowedForSubUser(subUser, targetContainer, t.ID) {
|
||||
continue
|
||||
}
|
||||
if downloaded := isImageDownloaded(t.Distro, t.Release, t.Arch); downloaded && (enabledSet[t.ID] || currentImageIDs[t.ID]) {
|
||||
result = append(result, map[string]string{
|
||||
"id": t.ID, "name": t.Name, "distro": t.Distro, "release": t.Release, "arch": t.Arch,
|
||||
"variant": t.Variant, "description": t.Description, "type": config.VirtualizationLXC,
|
||||
@@ -574,6 +598,42 @@ func isTemplateEnabledAndDownloaded(templateID string) bool {
|
||||
return isImageEnabledAndDownloaded(templateID, runtimeFromTemplateID(templateID))
|
||||
}
|
||||
|
||||
func imageTemplateExists(templateID string) bool {
|
||||
return lxc.FindTemplate(templateID) != nil || kvm.FindImage(templateID) != nil
|
||||
}
|
||||
|
||||
func isImageDownloadedForRuntime(templateID string, runtime string) bool {
|
||||
runtime = runtimeFromRequest(runtime)
|
||||
if runtime == config.VirtualizationKVM {
|
||||
if !hostKVMAvailable() {
|
||||
return false
|
||||
}
|
||||
image := kvm.FindImage(templateID)
|
||||
if image == nil {
|
||||
return false
|
||||
}
|
||||
downloaded, _ := kvm.ImageDownloadedInfo(image.ID)
|
||||
return downloaded
|
||||
}
|
||||
tmpl := lxc.FindTemplate(templateID)
|
||||
if tmpl == nil {
|
||||
return false
|
||||
}
|
||||
return isImageDownloaded(tmpl.Distro, tmpl.Release, tmpl.Arch)
|
||||
}
|
||||
|
||||
func isTemplateAvailableForRequest(r *http.Request, c *config.Container, templateID string, runtime string) bool {
|
||||
if isSubUserRequest(r) {
|
||||
if !isTemplateAllowedForRequest(r, c, templateID) {
|
||||
return false
|
||||
}
|
||||
if c != nil && c.Template == templateID {
|
||||
return isImageDownloadedForRuntime(templateID, runtime)
|
||||
}
|
||||
}
|
||||
return isImageEnabledAndDownloaded(templateID, runtime)
|
||||
}
|
||||
|
||||
func isImageEnabledAndDownloaded(templateID string, runtime string) bool {
|
||||
runtime = runtimeFromRequest(runtime)
|
||||
if runtime == config.VirtualizationKVM {
|
||||
|
||||
+230
-41
@@ -22,24 +22,30 @@ func generateRandomStr(length int) string {
|
||||
}
|
||||
|
||||
type subUserResponse struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
ContainerNames []string `json:"container_names"`
|
||||
ContainerUUIDs []string `json:"container_uuids,omitempty"`
|
||||
AccessCode string `json:"access_code"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
ContainerNames []string `json:"container_names"`
|
||||
ContainerUUIDs []string `json:"container_uuids,omitempty"`
|
||||
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
||||
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
||||
CurrentImageIDs []string `json:"current_image_ids,omitempty"`
|
||||
AccessCode string `json:"access_code"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func newSubUserResponse(su config.SubUser, password string) subUserResponse {
|
||||
return subUserResponse{
|
||||
ID: su.ID,
|
||||
Username: su.Username,
|
||||
Password: password,
|
||||
ContainerNames: su.ContainerNames,
|
||||
ContainerUUIDs: su.ContainerUUIDs,
|
||||
AccessCode: su.AccessCode,
|
||||
CreatedAt: su.CreatedAt,
|
||||
ID: su.ID,
|
||||
Username: su.Username,
|
||||
Password: password,
|
||||
ContainerNames: su.ContainerNames,
|
||||
ContainerUUIDs: su.ContainerUUIDs,
|
||||
AllowedImageIDs: effectiveSubUserAllowedImageIDs(&su),
|
||||
ImageLimitConfigured: su.ImageLimitConfigured,
|
||||
CurrentImageIDs: subUserCurrentImageIDs(&su),
|
||||
AccessCode: su.AccessCode,
|
||||
CreatedAt: su.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +100,10 @@ func HandleSubUserCreate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
su.ContainerNames = appendUniqueString(su.ContainerNames, containerName)
|
||||
su.ContainerUUIDs = appendUniqueString(su.ContainerUUIDs, c.UUID)
|
||||
if !su.ImageLimitConfigured && len(su.AllowedImageIDs) == 0 {
|
||||
su.AllowedImageIDs = effectiveContainerAllowedImageIDs(c)
|
||||
su.ImageLimitConfigured = true
|
||||
}
|
||||
config.SaveConfig()
|
||||
jsonResponse(w, http.StatusOK, APIResponse{
|
||||
Success: true,
|
||||
@@ -114,14 +124,16 @@ func HandleSubUserCreate(w http.ResponseWriter, r *http.Request) {
|
||||
accessCode := generateRandomStr(8)
|
||||
|
||||
subUser := config.SubUser{
|
||||
ID: "sub-" + generateRandomStr(8),
|
||||
Username: username,
|
||||
Password: password,
|
||||
PassHash: string(hash),
|
||||
ContainerNames: []string{containerName},
|
||||
ContainerUUIDs: []string{c.UUID},
|
||||
AccessCode: accessCode,
|
||||
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
|
||||
ID: "sub-" + generateRandomStr(8),
|
||||
Username: username,
|
||||
Password: password,
|
||||
PassHash: string(hash),
|
||||
ContainerNames: []string{containerName},
|
||||
ContainerUUIDs: []string{c.UUID},
|
||||
AllowedImageIDs: effectiveContainerAllowedImageIDs(c),
|
||||
ImageLimitConfigured: true,
|
||||
AccessCode: accessCode,
|
||||
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
config.AppConfig.SubUsers = append(config.AppConfig.SubUsers, subUser)
|
||||
@@ -306,6 +318,155 @@ func requestAllowedContainers(r *http.Request) (subUserAccess, bool) {
|
||||
return subUserAllowedContainers(r)
|
||||
}
|
||||
|
||||
func subUserFromRequest(r *http.Request) *config.SubUser {
|
||||
username := ""
|
||||
if ctx, ok := authContextFromRequest(r); ok && ctx.Type == authTypeSubUser {
|
||||
username = ctx.Username
|
||||
}
|
||||
if username == "" {
|
||||
if claims, ok := claimsFromRequest(r); ok {
|
||||
username, _ = claims["sub_user"].(string)
|
||||
}
|
||||
}
|
||||
if username == "" {
|
||||
return nil
|
||||
}
|
||||
for i := range config.AppConfig.SubUsers {
|
||||
if config.AppConfig.SubUsers[i].Username == username {
|
||||
return &config.AppConfig.SubUsers[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeAllowedImageIDs(ids []string) ([]string, error) {
|
||||
seen := map[string]bool{}
|
||||
result := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" || seen[id] {
|
||||
continue
|
||||
}
|
||||
if !imageTemplateExists(id) {
|
||||
return nil, fmt.Errorf("unknown image template: %s", id)
|
||||
}
|
||||
seen[id] = true
|
||||
result = append(result, id)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isTemplateAllowedForRequest(r *http.Request, c *config.Container, templateID string) bool {
|
||||
if !isSubUserRequest(r) {
|
||||
return true
|
||||
}
|
||||
return isImageAllowedForSubUser(subUserFromRequest(r), c, templateID)
|
||||
}
|
||||
|
||||
func isImageAllowedForSubUser(su *config.SubUser, c *config.Container, templateID string) bool {
|
||||
if su == nil || strings.TrimSpace(templateID) == "" {
|
||||
return false
|
||||
}
|
||||
for _, id := range effectiveSubUserAllowedImageIDs(su) {
|
||||
if id == templateID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func effectiveContainerAllowedImageIDs(c *config.Container) []string {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
if c.ImageLimitConfigured || len(c.AllowedImageIDs) > 0 {
|
||||
return cleanImageIDList(c.AllowedImageIDs)
|
||||
}
|
||||
if c.Template != "" {
|
||||
return []string{c.Template}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func effectiveSubUserAllowedImageIDs(su *config.SubUser) []string {
|
||||
if su == nil {
|
||||
return nil
|
||||
}
|
||||
if su.ImageLimitConfigured || len(su.AllowedImageIDs) > 0 {
|
||||
return cleanImageIDList(su.AllowedImageIDs)
|
||||
}
|
||||
result := []string{}
|
||||
seen := map[string]bool{}
|
||||
for _, c := range subUserAssignedContainers(su) {
|
||||
for _, id := range effectiveContainerAllowedImageIDs(c) {
|
||||
if id != "" && !seen[id] {
|
||||
seen[id] = true
|
||||
result = append(result, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func cleanImageIDList(ids []string) []string {
|
||||
result := make([]string, 0, len(ids))
|
||||
seen := map[string]bool{}
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" || seen[id] {
|
||||
continue
|
||||
}
|
||||
seen[id] = true
|
||||
result = append(result, id)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func subUserCurrentImageIDs(su *config.SubUser) []string {
|
||||
seen := map[string]bool{}
|
||||
result := []string{}
|
||||
for _, c := range subUserAssignedContainers(su) {
|
||||
if c.Template != "" && !seen[c.Template] {
|
||||
seen[c.Template] = true
|
||||
result = append(result, c.Template)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func subUserAssignedContainers(su *config.SubUser) []*config.Container {
|
||||
if su == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]*config.Container, 0, len(su.ContainerUUIDs)+len(su.ContainerNames))
|
||||
seen := map[string]bool{}
|
||||
for _, uuid := range su.ContainerUUIDs {
|
||||
if c := config.FindContainerByUUID(uuid); c != nil {
|
||||
key := c.UUID
|
||||
if key == "" {
|
||||
key = c.Name
|
||||
}
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, name := range su.ContainerNames {
|
||||
if c := config.FindContainerByName(name); c != nil {
|
||||
key := c.UUID
|
||||
if key == "" {
|
||||
key = c.Name
|
||||
}
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func isAccessRestrictedRequest(r *http.Request) bool {
|
||||
_, restricted := requestAllowedContainers(r)
|
||||
return restricted
|
||||
@@ -580,18 +741,21 @@ func splitBy(s, sep string) []string {
|
||||
|
||||
// SubUserListItem is the enriched sub-user info returned by the list API
|
||||
type SubUserListItem struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
ContainerNames []string `json:"container_names"`
|
||||
ContainerUUIDs []string `json:"container_uuids"`
|
||||
ContainerName string `json:"container_name"`
|
||||
ContainerUUID string `json:"container_uuid"`
|
||||
AccessCode string `json:"access_code"`
|
||||
Password string `json:"password,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastLogin string `json:"last_login"`
|
||||
LastLoginIP string `json:"last_login_ip"`
|
||||
LastLoginUA string `json:"last_login_ua"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
ContainerNames []string `json:"container_names"`
|
||||
ContainerUUIDs []string `json:"container_uuids"`
|
||||
AllowedImageIDs []string `json:"allowed_image_ids"`
|
||||
ImageLimitConfigured bool `json:"image_limit_configured"`
|
||||
CurrentImageIDs []string `json:"current_image_ids"`
|
||||
ContainerName string `json:"container_name"`
|
||||
ContainerUUID string `json:"container_uuid"`
|
||||
AccessCode string `json:"access_code"`
|
||||
Password string `json:"password,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastLogin string `json:"last_login"`
|
||||
LastLoginIP string `json:"last_login_ip"`
|
||||
LastLoginUA string `json:"last_login_ua"`
|
||||
}
|
||||
|
||||
// HandleSubUserList returns the list of all sub-users with container info
|
||||
@@ -607,13 +771,16 @@ func HandleSubUserList(w http.ResponseWriter, r *http.Request) {
|
||||
result := make([]SubUserListItem, 0, len(config.AppConfig.SubUsers))
|
||||
for _, su := range config.AppConfig.SubUsers {
|
||||
item := SubUserListItem{
|
||||
ID: su.ID,
|
||||
Username: su.Username,
|
||||
ContainerNames: su.ContainerNames,
|
||||
ContainerUUIDs: su.ContainerUUIDs,
|
||||
AccessCode: su.AccessCode,
|
||||
Password: su.Password,
|
||||
CreatedAt: su.CreatedAt,
|
||||
ID: su.ID,
|
||||
Username: su.Username,
|
||||
ContainerNames: su.ContainerNames,
|
||||
ContainerUUIDs: su.ContainerUUIDs,
|
||||
AllowedImageIDs: effectiveSubUserAllowedImageIDs(&su),
|
||||
ImageLimitConfigured: su.ImageLimitConfigured,
|
||||
CurrentImageIDs: subUserCurrentImageIDs(&su),
|
||||
AccessCode: su.AccessCode,
|
||||
Password: su.Password,
|
||||
CreatedAt: su.CreatedAt,
|
||||
}
|
||||
|
||||
// Resolve container name from first active UUID
|
||||
@@ -711,6 +878,28 @@ func HandleSubUserAction(w http.ResponseWriter, r *http.Request) {
|
||||
logs := filterSubUserLoginLogs(target.Username)
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: logs})
|
||||
|
||||
case action == "images" && r.Method == http.MethodPut:
|
||||
if !requireScope(w, r, "subuser:update") {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
AllowedImageIDs []string `json:"allowed_image_ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
|
||||
return
|
||||
}
|
||||
ids, err := normalizeAllowedImageIDs(req.AllowedImageIDs)
|
||||
if err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
target.AllowedImageIDs = ids
|
||||
target.ImageLimitConfigured = true
|
||||
target.TokenVersion++
|
||||
config.SaveConfig()
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: newSubUserResponse(*target, target.Password)})
|
||||
|
||||
default:
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Action not found"})
|
||||
}
|
||||
|
||||
@@ -560,7 +560,11 @@ func HandleSingleTaskAction(w http.ResponseWriter, r *http.Request, id int, acti
|
||||
if c := config.FindContainer(id); c != nil {
|
||||
runtime = c.Runtime()
|
||||
}
|
||||
if !isImageEnabledAndDownloaded(templateID, runtime) {
|
||||
if !isTemplateAllowedForRequest(r, c, templateID) {
|
||||
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Template is not allowed for this user"})
|
||||
return
|
||||
}
|
||||
if !isTemplateAvailableForRequest(r, c, templateID, runtime) {
|
||||
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Template is not enabled or downloaded"})
|
||||
return
|
||||
}
|
||||
@@ -656,6 +660,12 @@ func HandleBatchCreate(w http.ResponseWriter, r *http.Request) {
|
||||
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: name + ": template is not enabled or downloaded"})
|
||||
return
|
||||
}
|
||||
if ids, err := normalizeAllowedImageIDs(req.Containers[i].AllowedImageIDs); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: name + ": " + err.Error()})
|
||||
return
|
||||
} else {
|
||||
req.Containers[i].AllowedImageIDs = ids
|
||||
}
|
||||
if req.Containers[i].PortMappingCount < 0 {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: name + ": port mapping count cannot be negative"})
|
||||
return
|
||||
@@ -777,6 +787,10 @@ func HandleBatchAction(w http.ResponseWriter, r *http.Request) {
|
||||
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Access denied to one or more containers"})
|
||||
return
|
||||
}
|
||||
if taskType == TaskReinstall && !isTemplateAllowedForRequest(r, c, req.TemplateID) {
|
||||
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: c.Name + ": template is not allowed for this user"})
|
||||
return
|
||||
}
|
||||
if taskConfig != nil {
|
||||
if err := validateReinstallSSHAuth(c, req.TemplateID, *taskConfig); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: c.Name + ": " + err.Error()})
|
||||
|
||||
@@ -143,6 +143,8 @@ type Container struct {
|
||||
FirewallEnabled bool `json:"firewall_enabled"`
|
||||
FirewallDefaultAction string `json:"firewall_default_action"`
|
||||
FirewallRules []FirewallRule `json:"firewall_rules"`
|
||||
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
||||
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
||||
SnapshotLimit int `json:"snapshot_limit"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
@@ -319,16 +321,18 @@ func DeleteApiKey(id string) {
|
||||
}
|
||||
|
||||
type SubUser struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
PassHash string `json:"pass_hash"`
|
||||
ContainerNames []string `json:"container_names"`
|
||||
ContainerUUIDs []string `json:"container_uuids,omitempty"`
|
||||
Token string `json:"-"`
|
||||
AccessCode string `json:"access_code"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
TokenVersion int `json:"token_version"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
PassHash string `json:"pass_hash"`
|
||||
ContainerNames []string `json:"container_names"`
|
||||
ContainerUUIDs []string `json:"container_uuids,omitempty"`
|
||||
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
||||
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
||||
Token string `json:"-"`
|
||||
AccessCode string `json:"access_code"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
TokenVersion int `json:"token_version"`
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
|
||||
@@ -20,37 +20,39 @@ var (
|
||||
)
|
||||
|
||||
type savedTaskConfig struct {
|
||||
Name string `json:"name"`
|
||||
Virtualization string `json:"virtualization,omitempty"`
|
||||
TemplateID string `json:"template_id"`
|
||||
VCPU float64 `json:"vcpu"`
|
||||
CPUPercent int `json:"cpu_percent"`
|
||||
RAMMB int `json:"ram_mb"`
|
||||
DiskGB int `json:"disk_gb"`
|
||||
NetworkBWMbps int `json:"network_bw_mbps"`
|
||||
NetworkDownMbps int `json:"network_down_mbps"`
|
||||
NetworkUpMbps int `json:"network_up_mbps"`
|
||||
MonthlyTrafficGB int `json:"monthly_traffic_gb"`
|
||||
TrafficMode string `json:"traffic_mode"`
|
||||
TrafficInGB int `json:"traffic_in_gb"`
|
||||
TrafficOutGB int `json:"traffic_out_gb"`
|
||||
IOSpeedMBps int `json:"io_speed_mbps"`
|
||||
IOReadMBps int `json:"io_read_mbps"`
|
||||
IOWriteMBps int `json:"io_write_mbps"`
|
||||
ExtraPorts []int `json:"extra_ports"`
|
||||
PortMappingCount int `json:"port_mapping_count"`
|
||||
AssignNAT *bool `json:"assign_nat,omitempty"`
|
||||
SnapshotLimit int `json:"snapshot_limit"`
|
||||
AssignIPv4 bool `json:"assign_ipv4"`
|
||||
IPv4Count int `json:"ipv4_count,omitempty"`
|
||||
PublicIPv4s []string `json:"public_ipv4s,omitempty"`
|
||||
AssignIPv6 bool `json:"assign_ipv6"`
|
||||
IPv6Count int `json:"ipv6_count,omitempty"`
|
||||
IPv6Addresses []string `json:"ipv6_addresses,omitempty"`
|
||||
SSHAuthMode string `json:"ssh_auth_mode,omitempty"`
|
||||
SSHPassword string `json:"ssh_password,omitempty"`
|
||||
SSHPublicKey string `json:"ssh_public_key,omitempty"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
Name string `json:"name"`
|
||||
Virtualization string `json:"virtualization,omitempty"`
|
||||
TemplateID string `json:"template_id"`
|
||||
VCPU float64 `json:"vcpu"`
|
||||
CPUPercent int `json:"cpu_percent"`
|
||||
RAMMB int `json:"ram_mb"`
|
||||
DiskGB int `json:"disk_gb"`
|
||||
NetworkBWMbps int `json:"network_bw_mbps"`
|
||||
NetworkDownMbps int `json:"network_down_mbps"`
|
||||
NetworkUpMbps int `json:"network_up_mbps"`
|
||||
MonthlyTrafficGB int `json:"monthly_traffic_gb"`
|
||||
TrafficMode string `json:"traffic_mode"`
|
||||
TrafficInGB int `json:"traffic_in_gb"`
|
||||
TrafficOutGB int `json:"traffic_out_gb"`
|
||||
IOSpeedMBps int `json:"io_speed_mbps"`
|
||||
IOReadMBps int `json:"io_read_mbps"`
|
||||
IOWriteMBps int `json:"io_write_mbps"`
|
||||
ExtraPorts []int `json:"extra_ports"`
|
||||
PortMappingCount int `json:"port_mapping_count"`
|
||||
AssignNAT *bool `json:"assign_nat,omitempty"`
|
||||
SnapshotLimit int `json:"snapshot_limit"`
|
||||
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
||||
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
||||
AssignIPv4 bool `json:"assign_ipv4"`
|
||||
IPv4Count int `json:"ipv4_count,omitempty"`
|
||||
PublicIPv4s []string `json:"public_ipv4s,omitempty"`
|
||||
AssignIPv6 bool `json:"assign_ipv6"`
|
||||
IPv6Count int `json:"ipv6_count,omitempty"`
|
||||
IPv6Addresses []string `json:"ipv6_addresses,omitempty"`
|
||||
SSHAuthMode string `json:"ssh_auth_mode,omitempty"`
|
||||
SSHPassword string `json:"ssh_password,omitempty"`
|
||||
SSHPublicKey string `json:"ssh_public_key,omitempty"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
|
||||
func parseSavedTaskConfig(raw string) savedTaskConfig {
|
||||
@@ -222,7 +224,9 @@ func ensureSchema() error {
|
||||
snapshot_schedule_created_by TEXT,
|
||||
policy_blocked INTEGER,
|
||||
policy_blocked_reason TEXT,
|
||||
policy_blocked_at TEXT
|
||||
policy_blocked_at TEXT,
|
||||
allowed_image_ids TEXT,
|
||||
image_limit_configured INTEGER NOT NULL DEFAULT 0
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS port_mappings (
|
||||
container_id INTEGER NOT NULL,
|
||||
@@ -258,7 +262,9 @@ func ensureSchema() error {
|
||||
pass_hash TEXT,
|
||||
access_code TEXT,
|
||||
created_at TEXT,
|
||||
token_version INTEGER
|
||||
token_version INTEGER,
|
||||
allowed_image_ids TEXT,
|
||||
image_limit_configured INTEGER NOT NULL DEFAULT 0
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS sub_user_container_names (
|
||||
sub_user_id TEXT NOT NULL,
|
||||
@@ -348,6 +354,8 @@ func ensureSchema() error {
|
||||
cfg_ssh_auth_mode TEXT,
|
||||
cfg_ssh_password TEXT,
|
||||
cfg_ssh_public_key TEXT,
|
||||
cfg_allowed_image_ids TEXT,
|
||||
cfg_image_limit_configured INTEGER NOT NULL DEFAULT 0,
|
||||
cfg_expires_at TEXT
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS task_extra_ports (
|
||||
@@ -415,9 +423,13 @@ func ensureSchemaMigrations() error {
|
||||
{"tasks", "cfg_ssh_auth_mode", "TEXT"},
|
||||
{"tasks", "cfg_ssh_password", "TEXT"},
|
||||
{"tasks", "cfg_ssh_public_key", "TEXT"},
|
||||
{"tasks", "cfg_allowed_image_ids", "TEXT"},
|
||||
{"tasks", "cfg_image_limit_configured", "INTEGER NOT NULL DEFAULT 0"},
|
||||
{"port_mappings", "host_ip", "TEXT"},
|
||||
{"container_public_ipv4s", "prefix_len", "INTEGER"},
|
||||
{"container_public_ipv4s", "gateway", "TEXT"},
|
||||
{"sub_users", "allowed_image_ids", "TEXT"},
|
||||
{"sub_users", "image_limit_configured", "INTEGER NOT NULL DEFAULT 0"},
|
||||
{"containers", "network_down_mbps", "INTEGER NOT NULL DEFAULT 0"},
|
||||
{"containers", "network_up_mbps", "INTEGER NOT NULL DEFAULT 0"},
|
||||
{"containers", "io_read_mbps", "INTEGER NOT NULL DEFAULT 0"},
|
||||
@@ -425,6 +437,8 @@ func ensureSchemaMigrations() error {
|
||||
{"containers", "firewall_enabled", "INTEGER NOT NULL DEFAULT 0"},
|
||||
{"containers", "firewall_default_action", "TEXT NOT NULL DEFAULT 'DROP'"},
|
||||
{"containers", "firewall_rules", "TEXT"},
|
||||
{"containers", "allowed_image_ids", "TEXT"},
|
||||
{"containers", "image_limit_configured", "INTEGER NOT NULL DEFAULT 0"},
|
||||
} {
|
||||
wasAdded, err := ensureColumn(column.table, column.name, column.def)
|
||||
if err != nil {
|
||||
@@ -677,6 +691,7 @@ func saveMeta(tx *sql.Tx) error {
|
||||
func saveContainers(tx *sql.Tx) error {
|
||||
for _, c := range AppConfig.Containers {
|
||||
NormalizeContainerResourceAliases(&c)
|
||||
allowedImageIDs := encodeStringSlice(c.AllowedImageIDs)
|
||||
if _, err := tx.Exec(`INSERT INTO containers (
|
||||
id, uuid, name, virtualization, lxc_name, kvm_name, disk_image, mac_address, template,
|
||||
vcpu, ram_mb, disk_gb, network_bw_mbps, network_down_mbps, network_up_mbps,
|
||||
@@ -688,8 +703,8 @@ func saveContainers(tx *sql.Tx) error {
|
||||
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
|
||||
snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by,
|
||||
policy_blocked, policy_blocked_reason, policy_blocked_at,
|
||||
firewall_enabled, firewall_default_action, firewall_rules
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
firewall_enabled, firewall_default_action, firewall_rules, allowed_image_ids, image_limit_configured
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
c.ID, c.UUID, c.Name, c.Virtualization, c.LXCName, c.KVMName, c.DiskImage, c.MACAddress, c.Template,
|
||||
c.VCPU, c.RAMMB, c.DiskGB, c.NetworkBWMbps, c.NetworkDownMbps, c.NetworkUpMbps,
|
||||
c.MonthlyTrafficGB, c.TrafficMode, c.TrafficInGB,
|
||||
@@ -700,7 +715,7 @@ func saveContainers(tx *sql.Tx) error {
|
||||
boolInt(c.SnapshotScheduleEnabled), c.SnapshotScheduleIntervalHours, c.SnapshotScheduleTime,
|
||||
c.SnapshotScheduleLastRun, c.SnapshotScheduleNextRun, c.SnapshotScheduleCreatedBy,
|
||||
boolInt(c.PolicyBlocked), c.PolicyBlockedReason, c.PolicyBlockedAt,
|
||||
boolInt(c.FirewallEnabled), normalizeFirewallDefaultAction(c.FirewallDefaultAction), marshalFirewallRules(c.FirewallRules),
|
||||
boolInt(c.FirewallEnabled), normalizeFirewallDefaultAction(c.FirewallDefaultAction), marshalFirewallRules(c.FirewallRules), allowedImageIDs, boolInt(c.ImageLimitConfigured),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -728,8 +743,9 @@ func saveContainers(tx *sql.Tx) error {
|
||||
|
||||
func saveSubUsers(tx *sql.Tx) error {
|
||||
for _, su := range AppConfig.SubUsers {
|
||||
if _, err := tx.Exec(`INSERT INTO sub_users(id, username, password, pass_hash, access_code, created_at, token_version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`, su.ID, su.Username, su.Password, su.PassHash, su.AccessCode, su.CreatedAt, su.TokenVersion); err != nil {
|
||||
allowedImageIDs := encodeStringSlice(su.AllowedImageIDs)
|
||||
if _, err := tx.Exec(`INSERT INTO sub_users(id, username, password, pass_hash, access_code, created_at, token_version, allowed_image_ids, image_limit_configured)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, su.ID, su.Username, su.Password, su.PassHash, su.AccessCode, su.CreatedAt, su.TokenVersion, allowedImageIDs, boolInt(su.ImageLimitConfigured)); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, name := range su.ContainerNames {
|
||||
@@ -840,8 +856,8 @@ func saveTasksDB(tx *sql.Tx) error {
|
||||
cfg_traffic_out_gb, cfg_io_speed_mbps, cfg_io_read_mbps, cfg_io_write_mbps,
|
||||
cfg_port_mapping_count, cfg_assign_nat, cfg_snapshot_limit,
|
||||
cfg_assign_ipv4, cfg_ipv4_count, cfg_public_ipv4s, cfg_assign_ipv6, cfg_ipv6_count, cfg_ipv6_addresses,
|
||||
cfg_ssh_auth_mode, cfg_ssh_password, cfg_ssh_public_key, cfg_expires_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
cfg_ssh_auth_mode, cfg_ssh_password, cfg_ssh_public_key, cfg_allowed_image_ids, cfg_image_limit_configured, cfg_expires_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
task.ID, task.Type, task.ContainerID, task.ContainerName, task.Status, task.Error, task.CreatedAt, task.TemplateID, task.User, task.IP, task.UserAgent,
|
||||
cfg.Name, cfg.Virtualization, cfg.TemplateID, cfg.VCPU, cfg.CPUPercent, cfg.RAMMB, cfg.DiskGB,
|
||||
cfg.NetworkBWMbps, cfg.NetworkDownMbps, cfg.NetworkUpMbps,
|
||||
@@ -850,7 +866,7 @@ func saveTasksDB(tx *sql.Tx) error {
|
||||
cfg.PortMappingCount, boolPtrInt(cfg.AssignNAT), cfg.SnapshotLimit,
|
||||
boolInt(cfg.AssignIPv4), cfg.IPv4Count, encodeStringSlice(cfg.PublicIPv4s),
|
||||
boolInt(cfg.AssignIPv6), cfg.IPv6Count, encodeStringSlice(cfg.IPv6Addresses),
|
||||
cfg.SSHAuthMode, cfg.SSHPassword, cfg.SSHPublicKey, cfg.ExpiresAt,
|
||||
cfg.SSHAuthMode, cfg.SSHPassword, cfg.SSHPublicKey, encodeStringSlice(cfg.AllowedImageIDs), boolInt(cfg.ImageLimitConfigured), cfg.ExpiresAt,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -904,7 +920,7 @@ func loadContainers() ([]Container, error) {
|
||||
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
|
||||
snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by,
|
||||
policy_blocked, policy_blocked_reason, policy_blocked_at,
|
||||
firewall_enabled, firewall_default_action, firewall_rules
|
||||
firewall_enabled, firewall_default_action, firewall_rules, allowed_image_ids, image_limit_configured
|
||||
FROM containers ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -914,9 +930,9 @@ func loadContainers() ([]Container, error) {
|
||||
result := []Container{}
|
||||
for rows.Next() {
|
||||
var c Container
|
||||
var scheduleEnabled, policyBlocked, firewallEnabled int
|
||||
var scheduleEnabled, policyBlocked, firewallEnabled, imageLimitConfigured int
|
||||
var firewallDefaultAction string
|
||||
var firewallRulesJSON sql.NullString
|
||||
var firewallRulesJSON, allowedImageIDs sql.NullString
|
||||
if err := rows.Scan(
|
||||
&c.ID, &c.UUID, &c.Name, &c.Virtualization, &c.LXCName, &c.KVMName, &c.DiskImage, &c.MACAddress, &c.Template,
|
||||
&c.VCPU, &c.RAMMB, &c.DiskGB, &c.NetworkBWMbps, &c.NetworkDownMbps, &c.NetworkUpMbps,
|
||||
@@ -928,7 +944,7 @@ func loadContainers() ([]Container, error) {
|
||||
&scheduleEnabled, &c.SnapshotScheduleIntervalHours, &c.SnapshotScheduleTime,
|
||||
&c.SnapshotScheduleLastRun, &c.SnapshotScheduleNextRun, &c.SnapshotScheduleCreatedBy,
|
||||
&policyBlocked, &c.PolicyBlockedReason, &c.PolicyBlockedAt,
|
||||
&firewallEnabled, &firewallDefaultAction, &firewallRulesJSON,
|
||||
&firewallEnabled, &firewallDefaultAction, &firewallRulesJSON, &allowedImageIDs, &imageLimitConfigured,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -936,9 +952,11 @@ func loadContainers() ([]Container, error) {
|
||||
c.PolicyBlocked = policyBlocked != 0
|
||||
c.FirewallEnabled = firewallEnabled != 0
|
||||
c.FirewallDefaultAction = normalizeFirewallDefaultAction(firewallDefaultAction)
|
||||
c.ImageLimitConfigured = imageLimitConfigured != 0
|
||||
if firewallRulesJSON.Valid && strings.TrimSpace(firewallRulesJSON.String) != "" {
|
||||
_ = json.Unmarshal([]byte(firewallRulesJSON.String), &c.FirewallRules)
|
||||
}
|
||||
c.AllowedImageIDs = decodeStringSlice(allowedImageIDs.String)
|
||||
NormalizeContainerResourceAliases(&c)
|
||||
result = append(result, c)
|
||||
}
|
||||
@@ -1034,7 +1052,7 @@ func loadContainerIPv6Addresses(containerID int) ([]IPv6Assignment, error) {
|
||||
}
|
||||
|
||||
func loadSubUsers() ([]SubUser, error) {
|
||||
rows, err := db.Query(`SELECT id, username, password, pass_hash, access_code, created_at, token_version FROM sub_users ORDER BY created_at, id`)
|
||||
rows, err := db.Query(`SELECT id, username, password, pass_hash, access_code, created_at, token_version, allowed_image_ids, image_limit_configured FROM sub_users ORDER BY created_at, id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1042,9 +1060,13 @@ func loadSubUsers() ([]SubUser, error) {
|
||||
result := []SubUser{}
|
||||
for rows.Next() {
|
||||
var su SubUser
|
||||
if err := rows.Scan(&su.ID, &su.Username, &su.Password, &su.PassHash, &su.AccessCode, &su.CreatedAt, &su.TokenVersion); err != nil {
|
||||
var allowedImageIDs sql.NullString
|
||||
var imageLimitConfigured int
|
||||
if err := rows.Scan(&su.ID, &su.Username, &su.Password, &su.PassHash, &su.AccessCode, &su.CreatedAt, &su.TokenVersion, &allowedImageIDs, &imageLimitConfigured); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
su.AllowedImageIDs = decodeStringSlice(allowedImageIDs.String)
|
||||
su.ImageLimitConfigured = imageLimitConfigured != 0
|
||||
result = append(result, su)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -1138,7 +1160,7 @@ func loadTasks() ([]SavedTask, error) {
|
||||
cfg_traffic_out_gb, cfg_io_speed_mbps, cfg_io_read_mbps, cfg_io_write_mbps,
|
||||
cfg_port_mapping_count, cfg_assign_nat, cfg_snapshot_limit,
|
||||
cfg_assign_ipv4, cfg_ipv4_count, cfg_public_ipv4s, cfg_assign_ipv6, cfg_ipv6_count, cfg_ipv6_addresses,
|
||||
cfg_ssh_auth_mode, cfg_ssh_password, cfg_ssh_public_key, cfg_expires_at
|
||||
cfg_ssh_auth_mode, cfg_ssh_password, cfg_ssh_public_key, cfg_allowed_image_ids, cfg_image_limit_configured, cfg_expires_at
|
||||
FROM tasks ORDER BY created_at, id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1149,9 +1171,9 @@ func loadTasks() ([]SavedTask, error) {
|
||||
for rows.Next() {
|
||||
var t SavedTask
|
||||
var cfg savedTaskConfig
|
||||
var assignIPv4, assignIPv6 int
|
||||
var assignIPv4, assignIPv6, imageLimitConfigured int
|
||||
var ip, userAgent, publicIPv4s, ipv6Addresses sql.NullString
|
||||
var sshAuthMode, sshPassword, sshPublicKey sql.NullString
|
||||
var sshAuthMode, sshPassword, sshPublicKey, allowedImageIDs sql.NullString
|
||||
var assignNAT, ipv4Count, ipv6Count sql.NullInt64
|
||||
if err := rows.Scan(
|
||||
&t.ID, &t.Type, &t.ContainerID, &t.ContainerName, &t.Status, &t.Error, &t.CreatedAt, &t.TemplateID, &t.User, &ip, &userAgent,
|
||||
@@ -1161,7 +1183,7 @@ func loadTasks() ([]SavedTask, error) {
|
||||
&cfg.TrafficOutGB, &cfg.IOSpeedMBps, &cfg.IOReadMBps, &cfg.IOWriteMBps,
|
||||
&cfg.PortMappingCount, &assignNAT, &cfg.SnapshotLimit,
|
||||
&assignIPv4, &ipv4Count, &publicIPv4s, &assignIPv6, &ipv6Count, &ipv6Addresses,
|
||||
&sshAuthMode, &sshPassword, &sshPublicKey, &cfg.ExpiresAt,
|
||||
&sshAuthMode, &sshPassword, &sshPublicKey, &allowedImageIDs, &imageLimitConfigured, &cfg.ExpiresAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1184,6 +1206,8 @@ func loadTasks() ([]SavedTask, error) {
|
||||
cfg.SSHAuthMode = sshAuthMode.String
|
||||
cfg.SSHPassword = sshPassword.String
|
||||
cfg.SSHPublicKey = sshPublicKey.String
|
||||
cfg.AllowedImageIDs = decodeStringSlice(allowedImageIDs.String)
|
||||
cfg.ImageLimitConfigured = imageLimitConfigured != 0
|
||||
normalizeSavedTaskConfigLimits(&cfg)
|
||||
result = append(result, t)
|
||||
configs = append(configs, cfg)
|
||||
|
||||
@@ -376,6 +376,10 @@ func (m *Manager) CreateContainer(cfg lxc.ContainerConfig) error {
|
||||
if cfg.SnapshotLimit <= 0 {
|
||||
cfg.SnapshotLimit = config.DefaultSnapshotLimit
|
||||
}
|
||||
if !cfg.ImageLimitConfigured && len(cfg.AllowedImageIDs) == 0 && cfg.TemplateID != "" {
|
||||
cfg.AllowedImageIDs = []string{cfg.TemplateID}
|
||||
cfg.ImageLimitConfigured = true
|
||||
}
|
||||
|
||||
id := config.AllocateContainerID()
|
||||
vmName := fmt.Sprintf("vm-%d", id)
|
||||
@@ -567,11 +571,13 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
|
||||
}
|
||||
return sshPassword
|
||||
}(),
|
||||
PortMappings: portMappings,
|
||||
PortMappingLimit: cfg.PortMappingCount,
|
||||
SnapshotLimit: config.NormalizeSnapshotLimit(cfg.SnapshotLimit),
|
||||
CreatedAt: now,
|
||||
ExpiresAt: cfg.ExpiresAt,
|
||||
PortMappings: portMappings,
|
||||
PortMappingLimit: cfg.PortMappingCount,
|
||||
AllowedImageIDs: append([]string(nil), cfg.AllowedImageIDs...),
|
||||
ImageLimitConfigured: cfg.ImageLimitConfigured,
|
||||
SnapshotLimit: config.NormalizeSnapshotLimit(cfg.SnapshotLimit),
|
||||
CreatedAt: now,
|
||||
ExpiresAt: cfg.ExpiresAt,
|
||||
}
|
||||
container.NormalizeNetworkAssignments()
|
||||
return container, nil
|
||||
|
||||
+70
-62
@@ -218,37 +218,39 @@ func NewManager() *Manager {
|
||||
|
||||
// ContainerConfig defines container creation parameters
|
||||
type ContainerConfig struct {
|
||||
Name string `json:"name"`
|
||||
Virtualization string `json:"virtualization,omitempty"`
|
||||
TemplateID string `json:"template_id"`
|
||||
VCPU float64 `json:"vcpu"`
|
||||
CPUPercent int `json:"cpu_percent"`
|
||||
RAMMB int `json:"ram_mb"`
|
||||
DiskGB int `json:"disk_gb"`
|
||||
NetworkBWMbps int `json:"network_bw_mbps"`
|
||||
NetworkDownMbps int `json:"network_down_mbps"`
|
||||
NetworkUpMbps int `json:"network_up_mbps"`
|
||||
MonthlyTrafficGB int `json:"monthly_traffic_gb"`
|
||||
TrafficMode string `json:"traffic_mode"` // "total" or "in_out"
|
||||
TrafficInGB int `json:"traffic_in_gb"` // 0=unlimited
|
||||
TrafficOutGB int `json:"traffic_out_gb"` // 0=unlimited
|
||||
IOSpeedMBps int `json:"io_speed_mbps"`
|
||||
IOReadMBps int `json:"io_read_mbps"`
|
||||
IOWriteMBps int `json:"io_write_mbps"`
|
||||
ExtraPorts []int `json:"extra_ports"`
|
||||
PortMappingCount int `json:"port_mapping_count"`
|
||||
AssignNAT *bool `json:"assign_nat,omitempty"`
|
||||
SnapshotLimit int `json:"snapshot_limit"`
|
||||
AssignIPv4 bool `json:"assign_ipv4"`
|
||||
IPv4Count int `json:"ipv4_count,omitempty"`
|
||||
PublicIPv4s []string `json:"public_ipv4s,omitempty"`
|
||||
AssignIPv6 bool `json:"assign_ipv6"`
|
||||
IPv6Count int `json:"ipv6_count,omitempty"`
|
||||
IPv6Addresses []string `json:"ipv6_addresses,omitempty"`
|
||||
SSHAuthMode string `json:"ssh_auth_mode,omitempty"`
|
||||
SSHPassword string `json:"ssh_password,omitempty"`
|
||||
SSHPublicKey string `json:"ssh_public_key,omitempty"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
Name string `json:"name"`
|
||||
Virtualization string `json:"virtualization,omitempty"`
|
||||
TemplateID string `json:"template_id"`
|
||||
VCPU float64 `json:"vcpu"`
|
||||
CPUPercent int `json:"cpu_percent"`
|
||||
RAMMB int `json:"ram_mb"`
|
||||
DiskGB int `json:"disk_gb"`
|
||||
NetworkBWMbps int `json:"network_bw_mbps"`
|
||||
NetworkDownMbps int `json:"network_down_mbps"`
|
||||
NetworkUpMbps int `json:"network_up_mbps"`
|
||||
MonthlyTrafficGB int `json:"monthly_traffic_gb"`
|
||||
TrafficMode string `json:"traffic_mode"` // "total" or "in_out"
|
||||
TrafficInGB int `json:"traffic_in_gb"` // 0=unlimited
|
||||
TrafficOutGB int `json:"traffic_out_gb"` // 0=unlimited
|
||||
IOSpeedMBps int `json:"io_speed_mbps"`
|
||||
IOReadMBps int `json:"io_read_mbps"`
|
||||
IOWriteMBps int `json:"io_write_mbps"`
|
||||
ExtraPorts []int `json:"extra_ports"`
|
||||
PortMappingCount int `json:"port_mapping_count"`
|
||||
AssignNAT *bool `json:"assign_nat,omitempty"`
|
||||
SnapshotLimit int `json:"snapshot_limit"`
|
||||
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
||||
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
||||
AssignIPv4 bool `json:"assign_ipv4"`
|
||||
IPv4Count int `json:"ipv4_count,omitempty"`
|
||||
PublicIPv4s []string `json:"public_ipv4s,omitempty"`
|
||||
AssignIPv6 bool `json:"assign_ipv6"`
|
||||
IPv6Count int `json:"ipv6_count,omitempty"`
|
||||
IPv6Addresses []string `json:"ipv6_addresses,omitempty"`
|
||||
SSHAuthMode string `json:"ssh_auth_mode,omitempty"`
|
||||
SSHPassword string `json:"ssh_password,omitempty"`
|
||||
SSHPublicKey string `json:"ssh_public_key,omitempty"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
|
||||
func (cfg *ContainerConfig) NormalizeResourceAliases() {
|
||||
@@ -306,6 +308,10 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
if cfg.SnapshotLimit <= 0 {
|
||||
cfg.SnapshotLimit = config.DefaultSnapshotLimit
|
||||
}
|
||||
if !cfg.ImageLimitConfigured && len(cfg.AllowedImageIDs) == 0 && cfg.TemplateID != "" {
|
||||
cfg.AllowedImageIDs = []string{cfg.TemplateID}
|
||||
cfg.ImageLimitConfigured = true
|
||||
}
|
||||
|
||||
if !config.IsValidContainerName(cfg.Name) {
|
||||
return fmt.Errorf("invalid container name: %s", cfg.Name)
|
||||
@@ -424,37 +430,39 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
trafficResetDate := now[:7] // YYYY-MM for monthly tracking
|
||||
|
||||
container := config.Container{
|
||||
ID: id,
|
||||
UUID: config.NewContainerUUID(),
|
||||
Name: cfg.Name,
|
||||
Virtualization: config.VirtualizationLXC,
|
||||
Template: cfg.TemplateID,
|
||||
VCPU: cfg.VCPU,
|
||||
RAMMB: cfg.RAMMB,
|
||||
DiskGB: cfg.DiskGB,
|
||||
NetworkBWMbps: cfg.NetworkBWMbps,
|
||||
NetworkDownMbps: cfg.NetworkDownMbps,
|
||||
NetworkUpMbps: cfg.NetworkUpMbps,
|
||||
MonthlyTrafficGB: cfg.MonthlyTrafficGB,
|
||||
TrafficMode: trafficMode,
|
||||
TrafficInGB: cfg.TrafficInGB,
|
||||
TrafficOutGB: cfg.TrafficOutGB,
|
||||
TrafficResetDate: trafficResetDate,
|
||||
IOSpeedMBps: cfg.IOSpeedMBps,
|
||||
IOReadMBps: cfg.IOReadMBps,
|
||||
IOWriteMBps: cfg.IOWriteMBps,
|
||||
Status: "stopped",
|
||||
IP: "",
|
||||
PublicIPv4s: publicIPv4s,
|
||||
IPv6Addresses: ipv6Assignments,
|
||||
VNCPort: 0,
|
||||
SSHPort: sshPort,
|
||||
SSHPassword: sshPassword,
|
||||
PortMappings: portMappings,
|
||||
PortMappingLimit: cfg.PortMappingCount,
|
||||
SnapshotLimit: config.NormalizeSnapshotLimit(cfg.SnapshotLimit),
|
||||
CreatedAt: now,
|
||||
ExpiresAt: cfg.ExpiresAt,
|
||||
ID: id,
|
||||
UUID: config.NewContainerUUID(),
|
||||
Name: cfg.Name,
|
||||
Virtualization: config.VirtualizationLXC,
|
||||
Template: cfg.TemplateID,
|
||||
VCPU: cfg.VCPU,
|
||||
RAMMB: cfg.RAMMB,
|
||||
DiskGB: cfg.DiskGB,
|
||||
NetworkBWMbps: cfg.NetworkBWMbps,
|
||||
NetworkDownMbps: cfg.NetworkDownMbps,
|
||||
NetworkUpMbps: cfg.NetworkUpMbps,
|
||||
MonthlyTrafficGB: cfg.MonthlyTrafficGB,
|
||||
TrafficMode: trafficMode,
|
||||
TrafficInGB: cfg.TrafficInGB,
|
||||
TrafficOutGB: cfg.TrafficOutGB,
|
||||
TrafficResetDate: trafficResetDate,
|
||||
IOSpeedMBps: cfg.IOSpeedMBps,
|
||||
IOReadMBps: cfg.IOReadMBps,
|
||||
IOWriteMBps: cfg.IOWriteMBps,
|
||||
Status: "stopped",
|
||||
IP: "",
|
||||
PublicIPv4s: publicIPv4s,
|
||||
IPv6Addresses: ipv6Assignments,
|
||||
VNCPort: 0,
|
||||
SSHPort: sshPort,
|
||||
SSHPassword: sshPassword,
|
||||
PortMappings: portMappings,
|
||||
PortMappingLimit: cfg.PortMappingCount,
|
||||
AllowedImageIDs: append([]string(nil), cfg.AllowedImageIDs...),
|
||||
ImageLimitConfigured: cfg.ImageLimitConfigured,
|
||||
SnapshotLimit: config.NormalizeSnapshotLimit(cfg.SnapshotLimit),
|
||||
CreatedAt: now,
|
||||
ExpiresAt: cfg.ExpiresAt,
|
||||
}
|
||||
container.NormalizeNetworkAssignments()
|
||||
config.AddContainer(container)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
Reference in New Issue
Block a user