mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-08 06:24:44 +08:00
+188
-45
@@ -18,6 +18,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"clicd/internal/config"
|
||||
@@ -227,44 +228,53 @@ 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"`
|
||||
LANIPv4Mode string `json:"lan_ipv4_mode,omitempty"`
|
||||
LANInterface string `json:"lan_interface,omitempty"`
|
||||
LANIPv4Address string `json:"lan_ipv4_address,omitempty"`
|
||||
LANIPv4PrefixLen int `json:"lan_ipv4_prefix_len,omitempty"`
|
||||
LANIPv4Gateway string `json:"lan_ipv4_gateway,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"`
|
||||
Name string `json:"name"`
|
||||
Virtualization string `json:"virtualization,omitempty"`
|
||||
TemplateID string `json:"template_id"`
|
||||
StoragePoolID string `json:"storage_pool_id,omitempty"`
|
||||
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"`
|
||||
LANIPv4Mode string `json:"lan_ipv4_mode,omitempty"`
|
||||
LANInterface string `json:"lan_interface,omitempty"`
|
||||
LANIPv4Address string `json:"lan_ipv4_address,omitempty"`
|
||||
LANIPv4PrefixLen int `json:"lan_ipv4_prefix_len,omitempty"`
|
||||
LANIPv4Gateway string `json:"lan_ipv4_gateway,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"`
|
||||
Progress func(stage, detail string) `json:"-"`
|
||||
}
|
||||
|
||||
// ReportProgress reports a best-effort creation phase to the task queue.
|
||||
func (cfg ContainerConfig) ReportProgress(stage, detail string) {
|
||||
if cfg.Progress != nil {
|
||||
cfg.Progress(stage, detail)
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *ContainerConfig) NormalizeResourceAliases() {
|
||||
@@ -324,6 +334,7 @@ func (cfg ContainerConfig) WantsLANIPv4() bool {
|
||||
// CreateContainer creates a new LXC container. Uses ct-{id} as LXC name internally.
|
||||
func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
cfg.NormalizeResourceAliases()
|
||||
cfg.ReportProgress("preparing", "检查模板与创建参数")
|
||||
tmpl := FindTemplate(cfg.TemplateID)
|
||||
if tmpl == nil {
|
||||
return fmt.Errorf("template not found: %s", cfg.TemplateID)
|
||||
@@ -369,6 +380,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
fmt.Printf("Creating LXC container: %s (ID=%d, template: %s/%s/%s)\n",
|
||||
lxcName, id, tmpl.Distro, tmpl.Release, tmpl.Arch)
|
||||
|
||||
cfg.ReportProgress("rootfs", "下载模板并创建基础文件系统")
|
||||
args := []string{"-n", lxcName, "-t", "download", "--",
|
||||
"-d", tmpl.Distro, "-r", tmpl.Release, "-a", tmpl.Arch}
|
||||
if tmpl.Variant != "" {
|
||||
@@ -380,10 +392,19 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
return fmt.Errorf("lxc-create failed: %v, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
cfg.ReportProgress("storage", "复制容器数据到存储磁盘")
|
||||
storagePoolID, storagePath, err := m.moveContainerToStoragePool(lxcName, cfg.StoragePoolID)
|
||||
if err != nil {
|
||||
_ = m.cleanupContainerStorage(lxcName)
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.ReportProgress("disk", "创建容量限制磁盘并复制 rootfs")
|
||||
if err := m.applyDiskLimit(lxcName, cfg.DiskGB); err != nil {
|
||||
_ = m.cleanupContainerStorage(lxcName)
|
||||
return err
|
||||
}
|
||||
cfg.ReportProgress("resources", "配置 CPU、内存与网络限制")
|
||||
if cfg.WantsLANIPv4() {
|
||||
iface, err := m.applyLANIPv4Config(lxcName, cfg)
|
||||
if err != nil {
|
||||
@@ -399,6 +420,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.ReportProgress("addresses", "分配 IPv4、IPv6 与 NAT 端口")
|
||||
publicIPv4s, err := AllocatePublicIPv4Assignments(id, cfg.PublicIPv4s, cfg.IPv4Count, cfg.AssignIPv4)
|
||||
if err != nil {
|
||||
_ = m.cleanupContainerStorage(lxcName)
|
||||
@@ -472,6 +494,8 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
Name: cfg.Name,
|
||||
Virtualization: config.VirtualizationLXC,
|
||||
LXCName: lxcName,
|
||||
StoragePoolID: storagePoolID,
|
||||
StoragePath: storagePath,
|
||||
Template: cfg.TemplateID,
|
||||
VCPU: cfg.VCPU,
|
||||
RAMMB: cfg.RAMMB,
|
||||
@@ -509,16 +533,19 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
ExpiresAt: cfg.ExpiresAt,
|
||||
}
|
||||
container.NormalizeNetworkAssignments()
|
||||
cfg.ReportProgress("metadata", "保存容器配置")
|
||||
config.AddContainer(container)
|
||||
|
||||
// Pre-configure network and SSH in the rootfs before first boot.
|
||||
rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
|
||||
cfg.ReportProgress("network", "写入容器网络配置")
|
||||
m.preconfigureNetwork(rootfsPath, cfg)
|
||||
if len(ipv6Assignments) > 0 {
|
||||
if err := installContainerIPv6Init(rootfsPath, ipv6AssignmentAddresses(ipv6Assignments)...); err != nil {
|
||||
fmt.Printf("Warning: failed to install IPv6 init in %s: %v\n", lxcName, err)
|
||||
}
|
||||
}
|
||||
cfg.ReportProgress("ssh", "安装并配置 SSH 服务")
|
||||
if err := m.preconfigureSSH(rootfsPath, cfg.TemplateID, sshAccess.Mode); err != nil {
|
||||
fmt.Printf("Warning: failed to pre-configure SSH in %s: %v\n", lxcName, err)
|
||||
}
|
||||
@@ -530,6 +557,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
cfg.ReportProgress("permissions", "转换非特权容器文件权限")
|
||||
if err := m.shiftRootfsForUnprivileged(lxcName); err != nil {
|
||||
_ = m.cleanupContainerStorage(lxcName)
|
||||
config.RemoveContainer(id)
|
||||
@@ -538,6 +566,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
|
||||
// Set root password AFTER shiftRootfsForUnprivileged,
|
||||
// otherwise /etc/shadow ownership breaks and SSHD cannot authenticate.
|
||||
cfg.ReportProgress("credentials", "设置容器登录凭据")
|
||||
if err := m.setRootfsPassword(rootfsPath, sshPassword); err != nil {
|
||||
fmt.Printf("Warning: failed to set root password in %s: %v\n", lxcName, err)
|
||||
}
|
||||
@@ -1128,6 +1157,78 @@ func (m *Manager) applyLoopbackDiskLimit(lxcName string, diskGB int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) moveContainerToStoragePool(lxcName string, requestedPoolID string) (string, string, error) {
|
||||
sourceDir := filepath.Join(m.LxcPath, lxcName)
|
||||
requiredBytes := dirSizeBytes(sourceDir)
|
||||
pool, err := config.SelectStoragePoolForContent(config.StorageContentLXC, requestedPoolID, requiredBytes)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
targetRoot := filepath.Join(pool.Path, "lxc")
|
||||
targetDir := filepath.Join(targetRoot, lxcName)
|
||||
sourceAbs, err := filepath.Abs(sourceDir)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
targetAbs, err := filepath.Abs(targetDir)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if sourceAbs == targetAbs {
|
||||
return pool.ID, targetAbs, nil
|
||||
}
|
||||
if err := os.MkdirAll(targetRoot, 0755); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if _, err := os.Lstat(targetDir); err == nil {
|
||||
return "", "", fmt.Errorf("target storage directory already exists: %s", targetDir)
|
||||
}
|
||||
if err := moveLXCStorageDirectory(sourceDir, targetDir); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return pool.ID, targetAbs, nil
|
||||
}
|
||||
|
||||
func moveLXCStorageDirectory(sourceDir, targetDir string) error {
|
||||
if err := os.Rename(sourceDir, targetDir); err == nil {
|
||||
if err := os.Symlink(targetDir, sourceDir); err != nil {
|
||||
_ = os.Rename(targetDir, sourceDir)
|
||||
return fmt.Errorf("failed to create LXC storage symlink: %v", err)
|
||||
}
|
||||
return nil
|
||||
} else if !errors.Is(err, syscall.EXDEV) {
|
||||
return fmt.Errorf("failed to move LXC container to storage pool: %v", err)
|
||||
}
|
||||
|
||||
if err := copyTree(sourceDir, targetDir); err != nil {
|
||||
_ = os.RemoveAll(targetDir)
|
||||
return fmt.Errorf("failed to copy LXC container to storage pool: %v", err)
|
||||
}
|
||||
backupDir := sourceDir + fmt.Sprintf(".storage-move-%d", time.Now().UnixNano())
|
||||
if err := os.Rename(sourceDir, backupDir); err != nil {
|
||||
_ = os.RemoveAll(targetDir)
|
||||
return fmt.Errorf("failed to finalize LXC storage move: %v", err)
|
||||
}
|
||||
if err := os.Symlink(targetDir, sourceDir); err != nil {
|
||||
_ = os.Rename(backupDir, sourceDir)
|
||||
_ = os.RemoveAll(targetDir)
|
||||
return fmt.Errorf("failed to create LXC storage symlink: %v", err)
|
||||
}
|
||||
if err := os.RemoveAll(backupDir); err != nil {
|
||||
fmt.Printf("Warning: LXC storage moved but source cleanup failed: %v\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func storagePoolAllowsContent(pool config.StoragePool, content string) bool {
|
||||
for _, item := range pool.ContentTypes {
|
||||
if item == content {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) ensureDiskImageMounted(lxcName string) error {
|
||||
containerDir := filepath.Join(m.LxcPath, lxcName)
|
||||
rootfsPath := filepath.Join(containerDir, "rootfs")
|
||||
@@ -1183,15 +1284,30 @@ func diskImageMounted(lxcName, rootfsPath string) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
targetAbs, err := filepath.Abs(strings.TrimSpace(target))
|
||||
if err != nil {
|
||||
return false
|
||||
return sameFilesystemPath(strings.TrimSpace(target), rootfsPath)
|
||||
}
|
||||
|
||||
func sameFilesystemPath(left, right string) bool {
|
||||
leftInfo, leftErr := os.Stat(left)
|
||||
rightInfo, rightErr := os.Stat(right)
|
||||
if leftErr == nil && rightErr == nil && os.SameFile(leftInfo, rightInfo) {
|
||||
return true
|
||||
}
|
||||
rootfsAbs, err := filepath.Abs(rootfsPath)
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
canonical := func(path string) (string, error) {
|
||||
absolute, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resolved, err := filepath.EvalSymlinks(absolute)
|
||||
if err == nil {
|
||||
absolute = resolved
|
||||
}
|
||||
return filepath.Clean(absolute), nil
|
||||
}
|
||||
return targetAbs == rootfsAbs
|
||||
leftPath, leftErr := canonical(left)
|
||||
rightPath, rightErr := canonical(right)
|
||||
return leftErr == nil && rightErr == nil && leftPath == rightPath
|
||||
}
|
||||
|
||||
func applyXFSProjectQuota(rootfsPath, lxcName string, diskGB int) error {
|
||||
@@ -2745,6 +2861,17 @@ func (m *Manager) cleanupContainerStorage(lxcName string) error {
|
||||
if _, err := os.Stat(cleanPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
var linkedTarget string
|
||||
if info, err := os.Lstat(cleanPath); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
||||
if target, err := os.Readlink(cleanPath); err == nil {
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(filepath.Dir(cleanPath), target)
|
||||
}
|
||||
if abs, err := filepath.Abs(target); err == nil && lxcStorageTargetAllowed(abs) {
|
||||
linkedTarget = abs
|
||||
}
|
||||
}
|
||||
}
|
||||
exec.Command("lxc-stop", "-n", lxcName, "-k").Run()
|
||||
exec.Command("lxc-destroy", "-n", lxcName, "-f").Run()
|
||||
m.detachContainerMounts(cleanPath)
|
||||
@@ -2756,9 +2883,25 @@ func (m *Manager) cleanupContainerStorage(lxcName string) error {
|
||||
if err := os.RemoveAll(cleanPath); err != nil {
|
||||
return fmt.Errorf("failed to remove container directory %s: %v", cleanPath, err)
|
||||
}
|
||||
if linkedTarget != "" {
|
||||
m.detachContainerMounts(linkedTarget)
|
||||
m.detachContainerLoopDevices(linkedTarget)
|
||||
_ = os.RemoveAll(linkedTarget)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lxcStorageTargetAllowed(path string) bool {
|
||||
for _, pool := range config.StoragePoolsForContent(config.StorageContentLXC) {
|
||||
root := filepath.Join(pool.Path, "lxc")
|
||||
rel, err := filepath.Rel(root, path)
|
||||
if err == nil && rel != "." && !strings.HasPrefix(rel, "..") && !filepath.IsAbs(rel) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) detachContainerMounts(containerDir string) {
|
||||
out, err := exec.Command("findmnt", "-R", "-n", "-o", "TARGET", containerDir).Output()
|
||||
if err != nil {
|
||||
|
||||
@@ -121,6 +121,29 @@ func TestManagedPrlimitLinesDoNotSetNproc(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameFilesystemPathResolvesContainerStorageSymlink(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
storageContainer := filepath.Join(base, "storage", "ct-1")
|
||||
rootfs := filepath.Join(storageContainer, "rootfs")
|
||||
if err := os.MkdirAll(rootfs, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lxcPath := filepath.Join(base, "lxc")
|
||||
if err := os.MkdirAll(lxcPath, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
containerLink := filepath.Join(lxcPath, "ct-1")
|
||||
if err := os.Symlink(storageContainer, containerLink); err != nil {
|
||||
t.Skipf("directory symlinks are unavailable: %v", err)
|
||||
}
|
||||
|
||||
linkedRootfs := filepath.Join(containerLink, "rootfs")
|
||||
if !sameFilesystemPath(rootfs, linkedRootfs) {
|
||||
t.Fatalf("sameFilesystemPath(%q, %q) = false, want true", rootfs, linkedRootfs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendMissingSeccompRulesAddsFutexMitigationOnce(t *testing.T) {
|
||||
base := "2\ndenylist\n[all]\nopen_by_handle_at errno 1\n"
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
var snapshotMu sync.Mutex
|
||||
|
||||
func (m *Manager) CreateSnapshot(id int, createdBy string, scheduled bool, rotateLimit int) (config.Snapshot, error) {
|
||||
func (m *Manager) CreateSnapshot(id int, createdBy string, scheduled bool, rotateLimit int, storagePoolID ...string) (config.Snapshot, error) {
|
||||
snapshotMu.Lock()
|
||||
defer snapshotMu.Unlock()
|
||||
|
||||
@@ -42,12 +42,21 @@ func (m *Manager) CreateSnapshot(id int, createdBy string, scheduled bool, rotat
|
||||
if _, err := os.Stat(containerDir); err != nil {
|
||||
return config.Snapshot{}, fmt.Errorf("container storage not found: %v", err)
|
||||
}
|
||||
pool, err := config.SelectStoragePoolForContent(
|
||||
config.StorageContentSnapshots,
|
||||
firstString(storagePoolID),
|
||||
dirSizeBytes(containerDir),
|
||||
)
|
||||
if err != nil {
|
||||
return config.Snapshot{}, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
snapshotID := fmt.Sprintf("snap-%d-%s", id, now.Format("20060102150405-000000000"))
|
||||
// Use container ID instead of lxcName to avoid collision when containers are recreated
|
||||
snapshotDir := filepath.Join(snapshotBaseDir(), strconv.Itoa(id), snapshotID)
|
||||
if err := safePathUnder(snapshotDir, snapshotBaseDir()); err != nil {
|
||||
baseDir := filepath.Join(pool.Path, "snapshots")
|
||||
snapshotDir := filepath.Join(baseDir, strconv.Itoa(id), snapshotID)
|
||||
if err := safePathUnder(snapshotDir, baseDir); err != nil {
|
||||
return config.Snapshot{}, err
|
||||
}
|
||||
if err := os.MkdirAll(snapshotDir, 0700); err != nil {
|
||||
@@ -100,7 +109,7 @@ func (m *Manager) DeleteSnapshot(id string) error {
|
||||
|
||||
func (m *Manager) deleteSnapshotLocked(snapshot config.Snapshot) error {
|
||||
if snapshot.Path != "" {
|
||||
if err := safePathUnder(snapshot.Path, snapshotBaseDir()); err != nil {
|
||||
if err := safeSnapshotPath(snapshot.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.RemoveAll(snapshot.Path); err != nil {
|
||||
@@ -122,7 +131,7 @@ func (m *Manager) RestoreSnapshot(id string) error {
|
||||
if snapshot.Path == "" {
|
||||
return fmt.Errorf("snapshot path is empty")
|
||||
}
|
||||
if err := safePathUnder(snapshot.Path, snapshotBaseDir()); err != nil {
|
||||
if err := safeSnapshotPath(snapshot.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(snapshot.Path); err != nil {
|
||||
@@ -295,7 +304,33 @@ func (m *Manager) prepareContainerForColdCopy(id int, lxcName string, containerD
|
||||
}
|
||||
|
||||
func snapshotBaseDir() string {
|
||||
return filepath.Join(config.AppConfig.DataDir, "snapshots")
|
||||
return snapshotBaseDirForPool("")
|
||||
}
|
||||
|
||||
func snapshotBaseDirForPool(poolID string) string {
|
||||
if pool, err := config.SelectStoragePoolForContent(config.StorageContentSnapshots, poolID, 0); err == nil {
|
||||
return filepath.Join(pool.Path, "snapshots")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func safeSnapshotPath(path string) error {
|
||||
if err := safePathUnder(path, filepath.Join(config.AppConfig.DataDir, "snapshots")); err == nil {
|
||||
return nil
|
||||
}
|
||||
for _, pool := range config.StoragePoolsForContent(config.StorageContentSnapshots) {
|
||||
if err := safePathUnder(path, filepath.Join(pool.Path, "snapshots")); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("unsafe snapshot path: %s", path)
|
||||
}
|
||||
|
||||
func firstString(values []string) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(values[0])
|
||||
}
|
||||
|
||||
func copyTree(src string, dst string) error {
|
||||
@@ -313,6 +348,9 @@ func copyTree(src string, dst string) error {
|
||||
}
|
||||
|
||||
func dirSizeBytes(path string) int64 {
|
||||
if resolved, err := filepath.EvalSymlinks(path); err == nil {
|
||||
path = resolved
|
||||
}
|
||||
out, err := exec.Command("du", "-s", "-B1", path).Output()
|
||||
if err != nil {
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user