Merge branch 'develop' into feature/server-transfers
This commit is contained in:
commit
5693d0431e
|
@ -176,3 +176,29 @@ func (r *PanelRequest) SendTransferSuccess(uuid string) (*RequestError, error) {
|
|||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type BackupRequest struct {
|
||||
Successful bool `json:"successful"`
|
||||
Sha256Hash string `json:"sha256_hash"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
}
|
||||
|
||||
func (r *PanelRequest) SendBackupStatus(uuid string, backup string, data BackupRequest) (*RequestError, error) {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
resp, err := r.Post(fmt.Sprintf("/servers/%s/backup/%s", uuid, backup), b)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
if r.HasError() {
|
||||
return r.Error(), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -69,6 +69,9 @@ type SystemConfiguration struct {
|
|||
// Directory where server archives for transferring will be stored.
|
||||
ArchiveDirectory string `default:"/srv/daemon-data/.archives" yaml:"archive_directory"`
|
||||
|
||||
// Directory where local backups will be stored on the machine.
|
||||
BackupDirectory string `default:"/srv/daemon-data/.backups" yaml:"backup_directory"`
|
||||
|
||||
// The user that should own all of the server files, and be used for containers.
|
||||
Username string `default:"pterodactyl" yaml:"username"`
|
||||
|
||||
|
|
5
go.sum
5
go.sum
|
@ -50,8 +50,9 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
|
|||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
|
@ -99,6 +100,8 @@ github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2Em
|
|||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
|
||||
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee h1:IquUs3fIykn10zWDIyddanhpTqBvAHMaPnFhQuyYw5U=
|
||||
github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee/go.mod h1:eT2/Pcsim3XBjbvldGiJBvvgiqZkAFyiOJJsDKXs/ts=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
|
|
53
http.go
53
http.go
|
@ -518,7 +518,57 @@ func (rt *Router) routeServerReinstall(w http.ResponseWriter, r *http.Request, p
|
|||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (rt *Router) routeSystemInformation(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
func (rt *Router) routeServerBackup(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
s := rt.GetServer(ps.ByName("server"))
|
||||
defer r.Body.Close()
|
||||
|
||||
data := rt.ReaderToBytes(r.Body)
|
||||
b, err := s.NewBackup(data)
|
||||
if err != nil {
|
||||
zap.S().Errorw("failed to create backup struct for server", zap.String("server", s.Uuid), zap.Error(err))
|
||||
|
||||
http.Error(w, "failed to update data structure", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
zap.S().Infow("starting backup process for server", zap.String("server", s.Uuid), zap.String("backup", b.Uuid))
|
||||
go func(bk *server.Backup) {
|
||||
if err := bk.BackupAndNotify(); err != nil {
|
||||
zap.S().Errorw("failed to generate backup for server", zap.Error(err))
|
||||
} else {
|
||||
zap.S().Infow("completed backup process for server", zap.String("backup", b.Uuid))
|
||||
}
|
||||
}(b)
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (rt *Router) routeServerBackup(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
s := rt.GetServer(ps.ByName("server"))
|
||||
defer r.Body.Close()
|
||||
|
||||
data := rt.ReaderToBytes(r.Body)
|
||||
b, err := s.NewBackup(data)
|
||||
if err != nil {
|
||||
zap.S().Errorw("failed to create backup struct for server", zap.String("server", s.Uuid), zap.Error(err))
|
||||
|
||||
http.Error(w, "failed to update data structure", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
zap.S().Infow("starting backup process for server", zap.String("server", s.Uuid), zap.String("backup", b.Uuid))
|
||||
go func(bk *server.Backup) {
|
||||
if err := bk.BackupAndNotify(); err != nil {
|
||||
zap.S().Errorw("failed to generate backup for server", zap.Error(err))
|
||||
} else {
|
||||
zap.S().Infow("completed backup process for server", zap.String("backup", b.Uuid))
|
||||
}
|
||||
}(b)
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (rt *Router) routeSystemInformation(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
defer r.Body.Close()
|
||||
|
||||
s, err := GetSystemInformation()
|
||||
|
@ -841,6 +891,7 @@ func (rt *Router) ConfigureRouter() *httprouter.Router {
|
|||
router.POST("/api/servers/:server/power", rt.AuthenticateRequest(rt.routeServerPower))
|
||||
router.POST("/api/servers/:server/commands", rt.AuthenticateRequest(rt.routeServerSendCommand))
|
||||
router.POST("/api/servers/:server/reinstall", rt.AuthenticateRequest(rt.routeServerReinstall))
|
||||
router.POST("/api/servers/:server/backup", rt.AuthenticateRequest(rt.routeServerBackup))
|
||||
router.PATCH("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerUpdate))
|
||||
router.DELETE("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerDelete))
|
||||
|
||||
|
|
190
server/backup.go
Normal file
190
server/backup.go
Normal file
|
@ -0,0 +1,190 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Backup struct {
|
||||
Uuid string `json:"uuid"`
|
||||
IgnoredFiles []string `json:"ignored_files"`
|
||||
server *Server
|
||||
localDirectory string
|
||||
}
|
||||
|
||||
// Create a new Backup struct from data passed through in a request.
|
||||
func (s *Server) NewBackup(data []byte) (*Backup, error) {
|
||||
backup := &Backup{}
|
||||
|
||||
if err := json.Unmarshal(data, backup); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
backup.server = s
|
||||
backup.localDirectory = path.Join(config.Get().System.BackupDirectory, s.Uuid)
|
||||
|
||||
return backup, nil
|
||||
}
|
||||
|
||||
// Ensures that the local backup destination for files exists.
|
||||
func (b *Backup) ensureLocalBackupLocation() error {
|
||||
if _, err := os.Stat(b.localDirectory); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return os.MkdirAll(b.localDirectory, 0700)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the path for this specific backup.
|
||||
func (b *Backup) GetPath() string {
|
||||
return path.Join(b.localDirectory, b.Uuid+".tar.gz")
|
||||
}
|
||||
|
||||
func (b *Backup) GetChecksum() ([]byte, error) {
|
||||
h := sha256.New()
|
||||
|
||||
f, err := os.Open(b.GetPath())
|
||||
if err != nil {
|
||||
return []byte{}, errors.WithStack(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return []byte{}, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
// Generates a backup of the selected files and pushes it to the defined location
|
||||
// for this instance.
|
||||
func (b *Backup) Backup() (*api.BackupRequest, error) {
|
||||
rootPath := b.server.Filesystem.Path()
|
||||
|
||||
if err := b.ensureLocalBackupLocation(); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
zap.S().Debugw("starting archive of server files for backup", zap.String("server", b.server.Uuid), zap.String("backup", b.Uuid))
|
||||
if err := archiver.Archive([]string{rootPath}, b.GetPath()); err != nil {
|
||||
if strings.HasPrefix(err.Error(), "file already exists") {
|
||||
zap.S().Debugw("backup already exists on system, removing and re-attempting", zap.String("backup", b.Uuid))
|
||||
|
||||
if rerr := os.Remove(b.GetPath()); rerr != nil {
|
||||
return nil, errors.WithStack(rerr)
|
||||
}
|
||||
|
||||
// Re-attempt this backup.
|
||||
return b.Backup()
|
||||
}
|
||||
|
||||
// If there was some error with the archive, just go ahead and ensure the backup
|
||||
// is completely destroyed at this point. Ignore any errors from this function.
|
||||
os.Remove(b.GetPath())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
var checksum string
|
||||
// Calculate the checksum for the file.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
resp, err := b.GetChecksum()
|
||||
if err != nil {
|
||||
zap.S().Errorw("failed to calculate checksum for backup", zap.String("backup", b.Uuid), zap.Error(err))
|
||||
}
|
||||
|
||||
checksum = hex.EncodeToString(resp)
|
||||
}()
|
||||
|
||||
var s int64
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
st, err := os.Stat(b.GetPath())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = st.Size()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return &api.BackupRequest{
|
||||
Successful: true,
|
||||
Sha256Hash: checksum,
|
||||
FileSize: s,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Performs a server backup and then notifies the Panel of the completed status
|
||||
// so that the backup shows up for the user correctly.
|
||||
func (b *Backup) BackupAndNotify() error {
|
||||
resp, err := b.Backup()
|
||||
if err != nil {
|
||||
b.notifyPanel(resp)
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := b.notifyPanel(resp); err != nil {
|
||||
// These errors indicate that the Panel will not know about the status of this
|
||||
// backup, so let's just go ahead and delete it, and let the Panel handle the
|
||||
// cleanup process for the backups.
|
||||
//
|
||||
// @todo perhaps in the future we can sync the backups from the servers on boot?
|
||||
os.Remove(b.GetPath())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Backup) notifyPanel(request *api.BackupRequest) error {
|
||||
r := api.NewRequester()
|
||||
|
||||
rerr, err := r.SendBackupStatus(b.server.Uuid, b.Uuid, *request)
|
||||
if rerr != nil || err != nil {
|
||||
if err != nil {
|
||||
zap.S().Errorw(
|
||||
"failed to notify panel of backup status due to internal code error",
|
||||
zap.String("server", b.server.Uuid),
|
||||
zap.String("backup", b.Uuid),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
zap.S().Warnw(
|
||||
rerr.String(),
|
||||
zap.String("server", b.server.Uuid),
|
||||
zap.String("backup", b.Uuid),
|
||||
)
|
||||
|
||||
return errors.New(rerr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user