支持限制用户可选择的系统

This commit is contained in:
MengMengCode
2026-07-16 20:47:26 +08:00
parent 58d86b5d08
commit fdd83977fc
14 changed files with 707 additions and 190 deletions
+6
View File
@@ -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
}
+30 -6
View File
@@ -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 {
+62 -2
View File
@@ -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
View File
@@ -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"})
}
+15 -1
View File
@@ -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()})
+14 -10
View File
@@ -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 {
+75 -51
View File
@@ -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)
+11 -5
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@