diff --git a/Makefile b/Makefile index b3d5fe5..42f2c84 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,9 @@ build: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_amd64 -v wings.go GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_arm64 -v wings.go +race: + go build -ldflags="-X github.com/pterodactyl/wings/system.Version=$(GIT_HEAD)" -race + debug: go build -ldflags="-X github.com/pterodactyl/wings/system.Version=$(GIT_HEAD)" sudo ./wings --debug --ignore-certificate-errors --config config.yml --pprof --pprof-block-rate 1 diff --git a/cmd/migrate_vhd.go b/cmd/migrate_vhd.go index 2adba30..36e1eb4 100644 --- a/cmd/migrate_vhd.go +++ b/cmd/migrate_vhd.go @@ -14,6 +14,7 @@ import ( "github.com/pterodactyl/wings/loggers/cli" "github.com/pterodactyl/wings/remote" "github.com/pterodactyl/wings/server" + "github.com/pterodactyl/wings/server/filesystem" "github.com/spf13/cobra" ) @@ -55,7 +56,7 @@ func (m *MigrateVHDCommand) Run(ctx context.Context) error { for _, s := range m.manager.All() { s.Log().Debug("starting migration of server contents to virtual disk...") - v := s.Filesystem().NewVHD() + v := vhd.New(s.DiskSpace(), filesystem.VirtualDiskPath(s.Id()), s.Filesystem().Path()) if err := v.Allocate(ctx); err != nil { return errors.WithStackIf(err) } diff --git a/installer/installer.go b/installer/installer.go index f414918..6a2cf65 100644 --- a/installer/installer.go +++ b/installer/installer.go @@ -38,7 +38,7 @@ func New(ctx context.Context, manager *server.Manager, details ServerDetails) (* // Create a new server instance using the configuration we wrote to the disk // so that everything gets instantiated correctly on the struct. - s, err := manager.InitServer(c) + s, err := manager.InitServer(ctx, c) if err != nil { return nil, errors.WrapIf(err, "installer: could not init server instance") } diff --git a/internal/vhd/vhd.go b/internal/vhd/vhd.go index bd4f19c..05b887b 100644 --- a/internal/vhd/vhd.go +++ b/internal/vhd/vhd.go @@ -43,6 +43,7 @@ type CfgOption func(d *Disk) *Disk // Disk represents the underlying virtual disk for the instance. type Disk struct { + // The total size of the disk allowed in bytes. size int64 diskPath string mountAt string @@ -51,8 +52,8 @@ type Disk struct { } // New returns a new Disk instance. The "size" parameter should be provided in -// megabytes of space allowed for the disk. An additional slice of option -// callbacks can be provided to programatically swap out the underlying filesystem +// bytes of space allowed for the disk. An additional slice of option callbacks +// can be provided to programatically swap out the underlying filesystem // implementation or the underlying command exection engine. func New(size int64, diskPath string, mountAt string, opts ...func(*Disk)) *Disk { if diskPath == "" || mountAt == "" { @@ -181,7 +182,10 @@ func (d *Disk) Allocate(ctx context.Context) error { } else if err != nil { return errors.Wrap(err, "vhd: failed to check for existence of root disk") } - cmd := d.commander(ctx, "fallocate", "-l", fmt.Sprintf("%dM", d.size), d.diskPath) + // We use 1024 as the multiplier for all of the disk space logic within the + // application. Passing "K" (/1024) is the same as "KiB" for fallocate, but + // is different than "KB" (/1000). + cmd := d.commander(ctx, "fallocate", "-l", fmt.Sprintf("%dK", d.size / 1024), d.diskPath) if _, err := cmd.Output(); err != nil { msg := "vhd: failed to execute fallocate command" if v, ok := err.(*exec.ExitError); ok { diff --git a/internal/vhd/vhd_test.go b/internal/vhd/vhd_test.go index ab427ae..a92e79a 100644 --- a/internal/vhd/vhd_test.go +++ b/internal/vhd/vhd_test.go @@ -62,14 +62,14 @@ func newMockDisk(c CommanderProvider) *Disk { if c != nil { w = c } - return New(100, "/foo", "/bar", WithFs(afero.NewMemMapFs()), WithCommander(w)) + return New(100 * 1024 * 1024, "/foo", "/bar", WithFs(afero.NewMemMapFs()), WithCommander(w)) } func Test_New(t *testing.T) { t.Run("creates expected struct", func(t *testing.T) { - d := New(100, "/foo", "/bar") + d := New(100 * 1024 * 1024, "/foo", "/bar") assert.NotNil(t, d) - assert.Equal(t, int64(100), d.size) + assert.Equal(t, int64(100 * 1024 * 1024), d.size) assert.Equal(t, "/foo", d.diskPath) assert.Equal(t, "/bar", d.mountAt) @@ -360,7 +360,7 @@ func TestDisk_Allocate(t *testing.T) { output: func() ([]byte, error) { called = true assert.Equal(t, "fallocate", name) - assert.Equal(t, []string{"-l", "100M", "/foo"}, args) + assert.Equal(t, []string{"-l", "102400K", "/foo"}, args) return nil, nil }, } diff --git a/server/filesystem/filesystem.go b/server/filesystem/filesystem.go index cb70b44..73f5cbe 100644 --- a/server/filesystem/filesystem.go +++ b/server/filesystem/filesystem.go @@ -2,6 +2,7 @@ package filesystem import ( "bufio" + "context" "io" "io/ioutil" "os" @@ -20,6 +21,7 @@ import ( ignore "github.com/sabhiram/go-gitignore" "github.com/pterodactyl/wings/config" + "github.com/pterodactyl/wings/internal/vhd" "github.com/pterodactyl/wings/system" ) @@ -30,6 +32,7 @@ type Filesystem struct { diskUsed int64 diskCheckInterval time.Duration denylist *ignore.GitIgnore + vhd *vhd.Disk // The maximum amount of disk space (in bytes) that this Filesystem instance can use. diskLimit int64 @@ -41,8 +44,9 @@ type Filesystem struct { } // New creates a new Filesystem instance for a given server. -func New(root string, size int64, denylist []string) *Filesystem { - return &Filesystem{ +func New(uuid string, size int64, denylist []string) *Filesystem { + root := filepath.Join(config.Get().System.Data, uuid) + fs := Filesystem{ root: root, diskLimit: size, diskCheckInterval: time.Duration(config.Get().System.DiskCheckInterval), @@ -50,6 +54,16 @@ func New(root string, size int64, denylist []string) *Filesystem { lookupInProgress: system.NewAtomicBool(false), denylist: ignore.CompileIgnoreLines(denylist...), } + + if config.Get().System.UseVirtualDisks { + fs.vhd = vhd.New(size, VirtualDiskPath(uuid), fs.root) + } + + return &fs +} + +func VirtualDiskPath(uuid string) string { + return filepath.Join(config.Get().System.Data, ".vhd/", uuid+".img") } // Path returns the root path for the Filesystem instance. @@ -57,6 +71,25 @@ func (fs *Filesystem) Path() string { return fs.root } +// IsVirtual returns true if the filesystem is currently using a virtual disk. +func (fs *Filesystem) IsVirtual() bool { + return fs.vhd != nil +} + +// MountDisk will attempt to mount the underlying virtual disk for the server. +// If the disk is already mounted this is a no-op function. If the filesystem is +// not configured for virtual disks this function will panic. +func (fs *Filesystem) MountDisk(ctx context.Context) error { + if !fs.IsVirtual() { + panic(errors.New("filesystem: cannot call MountDisk on Filesystem instance without VHD present")) + } + err := fs.vhd.Mount(ctx) + if errors.Is(err, vhd.ErrFilesystemMounted) { + return nil + } + return errors.WrapIf(err, "filesystem: failed to mount VHD") +} + // File returns a reader for a file instance as well as the stat information. func (fs *Filesystem) File(p string) (*os.File, Stat, error) { cleaned, err := fs.SafePath(p) diff --git a/server/filesystem/vhd.go b/server/filesystem/vhd.go deleted file mode 100644 index 3a34bb6..0000000 --- a/server/filesystem/vhd.go +++ /dev/null @@ -1,17 +0,0 @@ -package filesystem - -import ( - "path/filepath" - "strings" - - "github.com/pterodactyl/wings/config" - "github.com/pterodactyl/wings/internal/vhd" -) - -func (fs *Filesystem) NewVHD() *vhd.Disk { - parts := strings.Split(fs.root, "/") - disk := filepath.Join(config.Get().System.Data, ".disks/", parts[len(parts)-1]+".img") - - return vhd.New(250, disk, fs.root) - // return vhd.New(fs.diskLimit/1024/1024, disk, fs.root) -} diff --git a/server/manager.go b/server/manager.go index 6733b35..51ab1d8 100644 --- a/server/manager.go +++ b/server/manager.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "os" - "path/filepath" "runtime" "sync" "time" @@ -184,7 +183,7 @@ func (m *Manager) ReadStates() (map[string]string, error) { // InitServer initializes a server using a data byte array. This will be // marshaled into the given struct using a YAML marshaler. This will also // configure the given environment for a server. -func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, error) { +func (m *Manager) InitServer(ctx context.Context, data remote.ServerConfigurationResponse) (*Server, error) { s, err := New(m.client) if err != nil { return nil, err @@ -196,7 +195,15 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, return nil, errors.WithStackIf(err) } - s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.ID()), s.DiskSpace(), s.Config().Egg.FileDenylist) + s.fs = filesystem.New(s.Id(), s.DiskSpace(), s.Config().Egg.FileDenylist) + // If this is a virtuakl filesystem we need to go ahead and mount the disk + // so that everything is accessible. + if s.fs.IsVirtual() { + log.WithField("server", s.Id()).Info("mounting virtual disk for server") + if err := s.fs.MountDisk(ctx); err != nil { + return nil, err + } + } // Right now we only support a Docker based environment, so I'm going to hard code // this logic in. When we're ready to support other environment we'll need to make @@ -258,7 +265,7 @@ func (m *Manager) init(ctx context.Context) error { log.WithField("server", data.Uuid).WithField("error", err).Error("failed to parse server configuration from API response, skipping...") return } - s, err := m.InitServer(d) + s, err := m.InitServer(ctx, d) if err != nil { log.WithField("server", data.Uuid).WithField("error", err).Error("failed to load server, skipping...") return