Rename snapshots.Manager => exporter.Exporter
This commit is contained in:
parent
063f3f9b7d
commit
8d2855fdcb
23 changed files with 105 additions and 100 deletions
169
internal/component/exporter/backup.go
Normal file
169
internal/component/exporter/backup.go
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/logging"
|
||||
"github.com/tkw1536/goprogram/status"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Backup describes a backup
|
||||
type Backup struct {
|
||||
Description BackupDescription
|
||||
|
||||
// Start and End Time of the backup
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
|
||||
// various error states, which are ignored when creating the snapshot
|
||||
ErrPanic interface{}
|
||||
|
||||
// errors for the various components
|
||||
ComponentErrors map[string]error
|
||||
|
||||
// TODO: Make this proper
|
||||
ConfigFileErr error
|
||||
|
||||
// Snapshots containing instances
|
||||
InstanceListErr error
|
||||
InstanceSnapshots []Snapshot
|
||||
|
||||
// List of files included
|
||||
WithManifest
|
||||
}
|
||||
|
||||
// BackupDescription provides a description for a backup
|
||||
type BackupDescription struct {
|
||||
Dest string // Destination path
|
||||
|
||||
ConcurrentSnapshots int // maximum number of concurrent snapshots
|
||||
}
|
||||
|
||||
// New create a new Backup
|
||||
func (exporter *Exporter) NewBackup(io stream.IOStream, description BackupDescription) (backup Backup) {
|
||||
backup.Description = description
|
||||
|
||||
// catch anything critical that happened during the snapshot
|
||||
defer func() {
|
||||
backup.ErrPanic = recover()
|
||||
}()
|
||||
|
||||
// do the create keeping track of time!
|
||||
logging.LogOperation(func() error {
|
||||
backup.StartTime = time.Now().UTC()
|
||||
backup.run(io, exporter)
|
||||
backup.EndTime = time.Now().UTC()
|
||||
|
||||
return nil
|
||||
}, io, "Writing backup files")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (backup *Backup) run(ios stream.IOStream, exporter *Exporter) {
|
||||
// create a manifest
|
||||
manifest, done := backup.handleManifest(backup.Description.Dest)
|
||||
defer done()
|
||||
|
||||
// create a new status display
|
||||
backups := exporter.Backupable
|
||||
backup.ComponentErrors = make(map[string]error, len(backups))
|
||||
|
||||
// Component backup tasks
|
||||
logging.LogOperation(func() error {
|
||||
st := status.NewWithCompat(ios.Stdout, 0)
|
||||
st.Start()
|
||||
defer st.Stop()
|
||||
|
||||
errors := status.Group[component.Backupable, error]{
|
||||
PrefixString: func(item component.Backupable, index int) string {
|
||||
return fmt.Sprintf("[backup %q]: ", item.Name())
|
||||
},
|
||||
PrefixAlign: true,
|
||||
|
||||
Handler: func(bc component.Backupable, index int, writer io.Writer) error {
|
||||
return bc.Backup(
|
||||
component.NewStagingContext(
|
||||
exporter.Environment,
|
||||
stream.NewIOStream(writer, writer, nil, 0),
|
||||
filepath.Join(backup.Description.Dest, bc.BackupName()),
|
||||
manifest,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
||||
ResultString: status.DefaultErrorString[component.Backupable],
|
||||
}.Use(st, backups)
|
||||
|
||||
for i, bc := range backups {
|
||||
backup.ComponentErrors[bc.Name()] = errors[i]
|
||||
}
|
||||
|
||||
return nil
|
||||
}, ios, "Backing up core components")
|
||||
|
||||
// backup instances
|
||||
logging.LogOperation(func() error {
|
||||
st := status.NewWithCompat(ios.Stdout, 0)
|
||||
st.Start()
|
||||
defer st.Stop()
|
||||
|
||||
instancesBackupDir := filepath.Join(backup.Description.Dest, "instances")
|
||||
if err := exporter.Environment.Mkdir(instancesBackupDir, environment.DefaultDirPerm); err != nil {
|
||||
backup.InstanceListErr = err
|
||||
return nil
|
||||
}
|
||||
|
||||
// list all instances
|
||||
wissKIs, err := exporter.Instances.All()
|
||||
if err != nil {
|
||||
backup.InstanceListErr = err
|
||||
return nil
|
||||
}
|
||||
|
||||
// make a backup of the snapshots
|
||||
backup.InstanceSnapshots = status.Group[*wisski.WissKI, Snapshot]{
|
||||
PrefixString: func(item *wisski.WissKI, index int) string {
|
||||
return fmt.Sprintf("[snapshot %q]: ", item.Slug)
|
||||
},
|
||||
PrefixAlign: true,
|
||||
|
||||
Handler: func(instance *wisski.WissKI, index int, writer io.Writer) Snapshot {
|
||||
dir := filepath.Join(instancesBackupDir, instance.Slug)
|
||||
if err := exporter.Environment.Mkdir(dir, environment.DefaultDirPerm); err != nil {
|
||||
return Snapshot{
|
||||
ErrPanic: err,
|
||||
}
|
||||
}
|
||||
|
||||
manifest <- dir
|
||||
|
||||
return exporter.NewSnapshot(instance, stream.NewIOStream(writer, writer, nil, 0), SnapshotDescription{
|
||||
Dest: dir,
|
||||
})
|
||||
},
|
||||
ResultString: func(res Snapshot, item *wisski.WissKI, index int) string {
|
||||
return "done"
|
||||
},
|
||||
WaitString: status.DefaultWaitString[*wisski.WissKI],
|
||||
HandlerLimit: backup.Description.ConcurrentSnapshots,
|
||||
}.Use(st, wissKIs)
|
||||
|
||||
// sort the instances
|
||||
slices.SortFunc(backup.InstanceSnapshots, func(a, b Snapshot) bool {
|
||||
return a.Instance.Slug < b.Instance.Slug
|
||||
})
|
||||
|
||||
return nil
|
||||
}, ios, "Creating instance snapshots")
|
||||
|
||||
}
|
||||
83
internal/component/exporter/exporter.go
Normal file
83
internal/component/exporter/exporter.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/exporter/logger"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/sql"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/fsx"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/password"
|
||||
)
|
||||
|
||||
// Exporter manages snapshots and backups
|
||||
type Exporter struct {
|
||||
component.ComponentBase
|
||||
|
||||
SQL *sql.SQL
|
||||
Instances *instances.Instances
|
||||
ExporterLogger *logger.Logger
|
||||
|
||||
Snapshotable []component.Snapshotable
|
||||
Backupable []component.Backupable
|
||||
}
|
||||
|
||||
func (Exporter) Name() string { return "snapshots" }
|
||||
|
||||
// Path returns the path that contains all snapshot related data.
|
||||
func (dis *Exporter) Path() string {
|
||||
return filepath.Join(dis.Config.DeployRoot, "snapshots")
|
||||
}
|
||||
|
||||
// StagingPath returns the path to the directory containing a temporary staging area for snapshots.
|
||||
// Use NewSnapshotStagingDir to generate a new staging area.
|
||||
func (dis *Exporter) StagingPath() string {
|
||||
return filepath.Join(dis.Path(), "staging")
|
||||
}
|
||||
|
||||
// ArchivePath returns the path to the directory containing all exported archives.
|
||||
// Use NewSnapshotArchivePath to generate a path to a new archive in this directory.
|
||||
func (dis *Exporter) ArchivePath() string {
|
||||
return filepath.Join(dis.Path(), "archives")
|
||||
}
|
||||
|
||||
// NewArchivePath returns the path to a new archive with the provided prefix.
|
||||
// The path is guaranteed to not exist.
|
||||
func (dis *Exporter) NewArchivePath(prefix string) (path string) {
|
||||
// TODO: Consider moving these into a subdirectory with the provided prefix.
|
||||
for path == "" || fsx.Exists(dis.Environment, path) {
|
||||
name := dis.newSnapshotName(prefix) + ".tar.gz"
|
||||
path = filepath.Join(dis.ArchivePath(), name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// newSnapshot name returns a new basename for a snapshot with the provided prefix.
|
||||
// The name is guaranteed to be unique within this process.
|
||||
func (*Exporter) newSnapshotName(prefix string) string {
|
||||
suffix, _ := password.Password(10) // silently ignore any errors!
|
||||
if prefix == "" {
|
||||
prefix = "backup"
|
||||
} else {
|
||||
prefix = "snapshot-" + prefix
|
||||
}
|
||||
return fmt.Sprintf("%s-%d-%s", prefix, time.Now().Unix(), suffix)
|
||||
}
|
||||
|
||||
// NewStagingDir returns the path to a new snapshot directory.
|
||||
// The directory is guaranteed to have been freshly created.
|
||||
func (dis *Exporter) NewStagingDir(prefix string) (path string, err error) {
|
||||
for path == "" || environment.IsExist(err) {
|
||||
path = filepath.Join(dis.StagingPath(), dis.newSnapshotName(prefix))
|
||||
err = dis.Core.Environment.Mkdir(path, environment.DefaultFilePerm)
|
||||
}
|
||||
if err != nil {
|
||||
path = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
29
internal/component/exporter/extras_bookkeeping.go
Normal file
29
internal/component/exporter/extras_bookkeeping.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
)
|
||||
|
||||
type Bookkeeping struct {
|
||||
component.ComponentBase
|
||||
}
|
||||
|
||||
func (Bookkeeping) Name() string { return "bookkeeping" }
|
||||
|
||||
// SnapshotNeedsRunning returns if this Snapshotable requires a running instance.
|
||||
func (Bookkeeping) SnapshotNeedsRunning() bool { return false }
|
||||
|
||||
// SnapshotName returns a new name to be used as an argument for path.
|
||||
func (Bookkeeping) SnapshotName() string { return "bookkeeping.txt" }
|
||||
|
||||
// Snapshot creates a snapshot of this instance
|
||||
func (*Bookkeeping) Snapshot(wisski models.Instance, context component.StagingContext) error {
|
||||
return context.AddFile(".", func(file io.Writer) error {
|
||||
_, err := fmt.Fprintf(file, "%#v\n", wisski)
|
||||
return err
|
||||
})
|
||||
}
|
||||
43
internal/component/exporter/extras_config.go
Normal file
43
internal/component/exporter/extras_config.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
)
|
||||
|
||||
// Config implements backing up configuration
|
||||
type Config struct {
|
||||
component.ComponentBase
|
||||
}
|
||||
|
||||
func (Config) Name() string { return "config" }
|
||||
|
||||
func (*Config) BackupName() string {
|
||||
return "config"
|
||||
}
|
||||
|
||||
func (control *Config) Backup(context component.StagingContext) error {
|
||||
files := control.backupFiles()
|
||||
|
||||
return context.AddDirectory("", func() error {
|
||||
for _, src := range files {
|
||||
name := filepath.Base(src)
|
||||
if err := context.CopyFile(name, src); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// backupfiles lists the files to be backed up.
|
||||
func (control *Config) backupFiles() []string {
|
||||
return []string{
|
||||
control.Config.ConfigPath,
|
||||
control.Config.ExecutablePath(),
|
||||
control.Config.SelfOverridesFile,
|
||||
control.Config.SelfResolverBlockFile,
|
||||
control.Config.GlobalAuthorizedKeysFile,
|
||||
}
|
||||
}
|
||||
24
internal/component/exporter/extras_filesystem.go
Normal file
24
internal/component/exporter/extras_filesystem.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
)
|
||||
|
||||
// Filesystem implements snapshotting an instnace filesystem
|
||||
type Filesystem struct {
|
||||
component.ComponentBase
|
||||
}
|
||||
|
||||
func (Filesystem) Name() string { return "filesystem" }
|
||||
|
||||
// SnapshotNeedsRunning returns if this Snapshotable requires a running instance.
|
||||
func (Filesystem) SnapshotNeedsRunning() bool { return false }
|
||||
|
||||
// SnapshotName returns a new name to be used as an argument for path.
|
||||
func (Filesystem) SnapshotName() string { return "data" }
|
||||
|
||||
// Snapshot creates a snapshot of this instance
|
||||
func (*Filesystem) Snapshot(wisski models.Instance, context component.StagingContext) error {
|
||||
return context.CopyDirectory(".", wisski.FilesystemBase)
|
||||
}
|
||||
39
internal/component/exporter/extras_pathbuilders.go
Normal file
39
internal/component/exporter/extras_pathbuilders.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
)
|
||||
|
||||
type Pathbuilders struct {
|
||||
component.ComponentBase
|
||||
Instances *instances.Instances
|
||||
}
|
||||
|
||||
func (Pathbuilders) Name() string { return "pathbuilders" }
|
||||
|
||||
func (Pathbuilders) SnapshotNeedsRunning() bool { return true }
|
||||
|
||||
func (Pathbuilders) SnapshotName() string { return "pathbuilders" }
|
||||
|
||||
func (pbs *Pathbuilders) Snapshot(wisski models.Instance, context component.StagingContext) error {
|
||||
return context.AddDirectory(".", func() error {
|
||||
builders, err := pbs.Instances.Instance(wisski).AllPathbuilders(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, bytes := range builders {
|
||||
if err := context.AddFile(name+".xml", func(file io.Writer) error {
|
||||
_, err := file.Write([]byte(bytes))
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
166
internal/component/exporter/iface.go
Normal file
166
internal/component/exporter/iface.go
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/logging"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/targz"
|
||||
"github.com/tkw1536/goprogram/status"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
// ExportTask describes a task that makes either a [Backup] or a [Snapshot].
|
||||
// See [Exporter.MakeExport]
|
||||
type ExportTask struct {
|
||||
// Dest is the destination path to write the backup to.
|
||||
// When empty, this is created automatically in the staging or archive directory.
|
||||
Dest string
|
||||
|
||||
// By default, a .tar.gz file is generated.
|
||||
// To generated an unpacked directory, set [StagingOnly] to true.
|
||||
StagingOnly bool
|
||||
|
||||
// Instance is the instance to generate a snapshot of.
|
||||
// To generate a backup, leave this to be nil.
|
||||
Instance *wisski.WissKI
|
||||
|
||||
// BackupDescriptions and SnapshotDescriptions further specitfy options for the export.
|
||||
// The Dest parameter is ignored, and updated automatically.
|
||||
BackupDescription BackupDescription
|
||||
SnapshotDescription SnapshotDescription
|
||||
}
|
||||
|
||||
// export is implemented by [Backup] and [Snapshot]
|
||||
type export interface {
|
||||
LogEntry() models.Export
|
||||
Report(w io.Writer) (int, error)
|
||||
}
|
||||
|
||||
// MakeExport performs an export task as described by flags.
|
||||
// Output is directed to the provided io.
|
||||
func (exporter *Exporter) MakeExport(io stream.IOStream, task ExportTask) (err error) {
|
||||
// extract parameters
|
||||
Title := "Backup"
|
||||
Slug := ""
|
||||
if task.Instance != nil {
|
||||
Title = "Snapshot"
|
||||
Slug = task.Instance.Slug
|
||||
}
|
||||
|
||||
// determine target paths
|
||||
logging.LogMessage(io, "Determining target paths")
|
||||
var stagingDir, archivePath string
|
||||
if task.StagingOnly {
|
||||
stagingDir = task.Dest
|
||||
} else {
|
||||
archivePath = task.Dest
|
||||
}
|
||||
if stagingDir == "" {
|
||||
stagingDir, err = exporter.NewStagingDir(Slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !task.StagingOnly && archivePath == "" {
|
||||
archivePath = exporter.NewArchivePath(Slug)
|
||||
}
|
||||
io.Printf("Staging Directory: %s\n", stagingDir)
|
||||
io.Printf("Archive Path: %s\n", archivePath)
|
||||
|
||||
// create the staging directory
|
||||
logging.LogMessage(io, "Creating staging directory")
|
||||
err = exporter.Environment.Mkdir(stagingDir, environment.DefaultDirPerm)
|
||||
if !environment.IsExist(err) && err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if it was requested to not do staging only
|
||||
// we need the staging directory to be deleted at the end
|
||||
if !task.StagingOnly {
|
||||
defer func() {
|
||||
logging.LogMessage(io, "Removing staging directory")
|
||||
exporter.Environment.RemoveAll(stagingDir)
|
||||
}()
|
||||
}
|
||||
|
||||
// create the actual snapshot or backup
|
||||
// write out the report
|
||||
// and retain a log entry
|
||||
var entry models.Export
|
||||
logging.LogOperation(func() error {
|
||||
var sl export
|
||||
if task.Instance == nil {
|
||||
task.BackupDescription.Dest = stagingDir
|
||||
backup := exporter.NewBackup(io, task.BackupDescription)
|
||||
sl = &backup
|
||||
} else {
|
||||
task.SnapshotDescription.Dest = stagingDir
|
||||
snapshot := exporter.NewSnapshot(task.Instance, io, task.SnapshotDescription)
|
||||
sl = &snapshot
|
||||
}
|
||||
|
||||
// create a log entry
|
||||
entry = sl.LogEntry()
|
||||
|
||||
// find the report path
|
||||
reportPath := filepath.Join(stagingDir, "report.txt")
|
||||
io.Println(reportPath)
|
||||
|
||||
// create the path
|
||||
report, err := exporter.Environment.Create(reportPath, environment.DefaultFilePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// and write out the report
|
||||
{
|
||||
_, err := sl.Report(report)
|
||||
return err
|
||||
}
|
||||
}, io, "Generating %s", Title)
|
||||
|
||||
// if we only requested staging
|
||||
// all that is left is to write the log entry
|
||||
if task.StagingOnly {
|
||||
logging.LogMessage(io, "Writing Log Entry")
|
||||
|
||||
// write out the log entry
|
||||
entry.Path = stagingDir
|
||||
entry.Packed = false
|
||||
exporter.ExporterLogger.Add(entry)
|
||||
|
||||
io.Printf("Wrote %s\n", stagingDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// package everything up as an archive!
|
||||
if err := logging.LogOperation(func() error {
|
||||
var count int64
|
||||
defer func() { io.Printf("Wrote %d byte(s) to %s\n", count, archivePath) }()
|
||||
|
||||
st := status.NewWithCompat(io.Stdout, 1)
|
||||
st.Start()
|
||||
defer st.Stop()
|
||||
|
||||
count, err = targz.Package(exporter.Environment, archivePath, stagingDir, func(dst, src string) {
|
||||
st.Set(0, dst)
|
||||
})
|
||||
|
||||
return err
|
||||
}, io, "Writing archive"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write out the log entry
|
||||
logging.LogMessage(io, "Writing Log Entry")
|
||||
entry.Path = archivePath
|
||||
entry.Packed = true
|
||||
exporter.ExporterLogger.Add(entry)
|
||||
|
||||
// and we're done!
|
||||
return nil
|
||||
}
|
||||
17
internal/component/exporter/log.go
Normal file
17
internal/component/exporter/log.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package exporter
|
||||
|
||||
import "github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
|
||||
func (backup *Backup) LogEntry() models.Export {
|
||||
return models.Export{
|
||||
Created: backup.StartTime,
|
||||
Slug: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (snapshot *Snapshot) LogEntry() models.Export {
|
||||
return models.Export{
|
||||
Created: snapshot.StartTime,
|
||||
Slug: snapshot.Instance.Slug,
|
||||
}
|
||||
}
|
||||
80
internal/component/exporter/logger/logger.go
Normal file
80
internal/component/exporter/logger/logger.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/sql"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
|
||||
"github.com/tkw1536/goprogram/lib/collection"
|
||||
)
|
||||
|
||||
// Logger is responsible for logging backups and snapshots
|
||||
type Logger struct {
|
||||
component.ComponentBase
|
||||
|
||||
SQL *sql.SQL
|
||||
}
|
||||
|
||||
func (*Logger) Name() string { return "snapshots-log" }
|
||||
|
||||
// For retrieves (and prunes) the ExportLog.
|
||||
// Slug determines if entries for Backups (empty slug)
|
||||
// or a specific Instance (non-empty slug) are returned.
|
||||
func (log *Logger) For(slug string) (exports []models.Export, err error) {
|
||||
exports, err = log.Log()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return collection.Filter(exports, func(s models.Export) bool {
|
||||
return s.Slug == slug
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Log retrieves (and prunes) all entries in the snapshot log.
|
||||
func (log *Logger) Log() ([]models.Export, error) {
|
||||
// query the table!
|
||||
table, err := log.SQL.QueryTable(false, models.ExportTable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// find all the exports
|
||||
var exports []models.Export
|
||||
res := table.Find(&exports)
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
}
|
||||
|
||||
// partition out the exports that have been deleted!
|
||||
parts := collection.Partition(exports, func(s models.Export) bool {
|
||||
_, err := log.Core.Environment.Stat(s.Path)
|
||||
return !environment.IsNotExist(err)
|
||||
})
|
||||
|
||||
// go and delete them!
|
||||
if len(parts[false]) > 0 {
|
||||
if err := table.Delete(parts[false]).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// return the ones that still exist
|
||||
return parts[true], nil
|
||||
}
|
||||
|
||||
// AddToExportLog adds the provided export to the log.
|
||||
func (log *Logger) Add(export models.Export) error {
|
||||
// find the table
|
||||
table, err := log.SQL.QueryTable(false, models.ExportTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// and save it!
|
||||
res := table.Create(&export)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
33
internal/component/exporter/manifest.go
Normal file
33
internal/component/exporter/manifest.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type WithManifest struct {
|
||||
Manifest []string
|
||||
}
|
||||
|
||||
func (wm *WithManifest) handleManifest(dest string) (chan<- string, func()) {
|
||||
manifest := make(chan string)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
for file := range manifest {
|
||||
// get the relative path to the root of the manifest.
|
||||
// nothing *should* go wrong, but in case it does, use the original path.
|
||||
path, err := filepath.Rel(dest, file)
|
||||
if err != nil {
|
||||
path = file
|
||||
}
|
||||
|
||||
// add the manifest
|
||||
wm.Manifest = append(wm.Manifest, path)
|
||||
}
|
||||
}()
|
||||
return manifest, func() {
|
||||
close(manifest)
|
||||
<-done
|
||||
}
|
||||
}
|
||||
55
internal/component/exporter/prune.go
Normal file
55
internal/component/exporter/prune.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
// ShouldPrune determines if a file with the provided modification time should be
|
||||
// removed from the export log.
|
||||
func (exporter *Exporter) ShouldPrune(modtime time.Time) bool {
|
||||
return time.Since(modtime) > time.Duration(exporter.Config.MaxBackupAge)*24*time.Hour
|
||||
}
|
||||
|
||||
// Prune prunes all old exports
|
||||
func (exporter *Exporter) PruneExports(io stream.IOStream) error {
|
||||
sPath := exporter.ArchivePath()
|
||||
|
||||
// list all the files
|
||||
entries, err := exporter.Core.Environment.ReadDir(sPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
// skip directories
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// grab info about the file
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if it should be pruned!
|
||||
if !exporter.ShouldPrune(info.ModTime()) {
|
||||
continue
|
||||
}
|
||||
|
||||
// assemble path, and then remove the file!
|
||||
path := filepath.Join(sPath, entry.Name())
|
||||
io.Printf("Removing %s cause it is older than %d days", path, exporter.Config.MaxBackupAge)
|
||||
|
||||
if err := exporter.Core.Environment.Remove(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// prune the snapshot log!
|
||||
_, err = exporter.ExporterLogger.Log()
|
||||
return err
|
||||
}
|
||||
110
internal/component/exporter/report.go
Normal file
110
internal/component/exporter/report.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/countwriter"
|
||||
)
|
||||
|
||||
func (snapshot Snapshot) String() string {
|
||||
var builder strings.Builder
|
||||
snapshot.Report(&builder)
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// Report writes a report from snapshot into w
|
||||
func (snapshot Snapshot) Report(w io.Writer) (int, error) {
|
||||
ww := countwriter.NewCountWriter(w)
|
||||
|
||||
encoder := json.NewEncoder(ww)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
io.WriteString(ww, "======= Begin Snapshot Report "+snapshot.Instance.Slug+" =======\n")
|
||||
|
||||
fmt.Fprintf(ww, "Slug: %s\n", snapshot.Instance.Slug)
|
||||
fmt.Fprintf(ww, "Dest: %s\n", snapshot.Description.Dest)
|
||||
|
||||
fmt.Fprintf(ww, "Start: %s\n", snapshot.StartTime)
|
||||
fmt.Fprintf(ww, "End: %s\n", snapshot.EndTime)
|
||||
io.WriteString(ww, "\n")
|
||||
|
||||
io.WriteString(ww, "======= Description =======\n")
|
||||
encoder.Encode(snapshot.Description)
|
||||
io.WriteString(ww, "\n")
|
||||
|
||||
io.WriteString(ww, "======= Instance =======\n")
|
||||
encoder.Encode(snapshot.Instance)
|
||||
io.WriteString(ww, "\n")
|
||||
|
||||
io.WriteString(ww, "======= Errors =======\n")
|
||||
fmt.Fprintf(ww, "Panic: %v\n", snapshot.ErrPanic)
|
||||
fmt.Fprintf(ww, "Start: %s\n", snapshot.ErrStart)
|
||||
fmt.Fprintf(ww, "Stop: %s\n", snapshot.ErrStop)
|
||||
|
||||
fmt.Fprintf(ww, "Whitebox: %s\n", snapshot.ErrWhitebox)
|
||||
fmt.Fprintf(ww, "Blackbox: %s\n", snapshot.ErrBlackbox)
|
||||
|
||||
io.WriteString(ww, "\n")
|
||||
|
||||
io.WriteString(ww, "======= Manifest =======\n")
|
||||
for _, file := range snapshot.Manifest {
|
||||
io.WriteString(ww, file+"\n")
|
||||
}
|
||||
|
||||
io.WriteString(ww, "\n")
|
||||
|
||||
io.WriteString(ww, "======= End Snapshot Report "+snapshot.Instance.Slug+"=======\n")
|
||||
|
||||
return ww.Sum()
|
||||
}
|
||||
|
||||
// Strings turns this backup into a string for the BackupReport.
|
||||
func (backup Backup) String() string {
|
||||
var builder strings.Builder
|
||||
backup.Report(&builder)
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// Report formats a report for this backup, and writes it into Writer.
|
||||
func (backup Backup) Report(w io.Writer) (int, error) {
|
||||
cw := countwriter.NewCountWriter(w)
|
||||
|
||||
encoder := json.NewEncoder(cw)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
io.WriteString(cw, "======= Backup =======\n")
|
||||
|
||||
fmt.Fprintf(cw, "Start: %s\n", backup.StartTime)
|
||||
fmt.Fprintf(cw, "End: %s\n", backup.EndTime)
|
||||
io.WriteString(cw, "\n")
|
||||
|
||||
io.WriteString(cw, "======= Description =======\n")
|
||||
encoder.Encode(backup.Description)
|
||||
io.WriteString(cw, "\n")
|
||||
|
||||
io.WriteString(cw, "======= Errors =======\n")
|
||||
fmt.Fprintf(cw, "Panic: %v\n", backup.ErrPanic)
|
||||
fmt.Fprintf(cw, "Component Errors: %v\n", backup.ComponentErrors)
|
||||
fmt.Fprintf(cw, "ConfigFileErr: %s\n", backup.ConfigFileErr)
|
||||
fmt.Fprintf(cw, "InstanceListErr: %s\n", backup.InstanceListErr)
|
||||
|
||||
io.WriteString(cw, "\n")
|
||||
|
||||
io.WriteString(cw, "======= Snapshots =======\n")
|
||||
for _, s := range backup.InstanceSnapshots {
|
||||
io.WriteString(cw, s.String())
|
||||
io.WriteString(cw, "\n")
|
||||
}
|
||||
|
||||
io.WriteString(cw, "======= Manifest =======\n")
|
||||
for _, file := range backup.Manifest {
|
||||
io.WriteString(cw, file+"\n")
|
||||
}
|
||||
|
||||
io.WriteString(cw, "\n")
|
||||
|
||||
return cw.Sum()
|
||||
}
|
||||
139
internal/component/exporter/snapshot.go
Normal file
139
internal/component/exporter/snapshot.go
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
package exporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/logging"
|
||||
"github.com/tkw1536/goprogram/lib/collection"
|
||||
"github.com/tkw1536/goprogram/status"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// SnapshotDescription is a description for a snapshot
|
||||
type SnapshotDescription struct {
|
||||
Dest string // destination path
|
||||
Keepalive bool // should we keep the instance alive while making the snapshot?
|
||||
}
|
||||
|
||||
// Snapshot represents the result of generating a snapshot
|
||||
type Snapshot struct {
|
||||
Description SnapshotDescription
|
||||
Instance models.Instance
|
||||
|
||||
// Start and End Time of the snapshot
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
|
||||
// Generic Panic that may have occured
|
||||
ErrPanic interface{}
|
||||
ErrStart error
|
||||
ErrStop error
|
||||
ErrWhitebox map[string]error
|
||||
ErrBlackbox map[string]error
|
||||
|
||||
// List of files included
|
||||
WithManifest
|
||||
}
|
||||
|
||||
// Snapshot creates a new snapshot of this instance into dest
|
||||
func (snapshots *Exporter) NewSnapshot(instance *wisski.WissKI, io stream.IOStream, desc SnapshotDescription) (snapshot Snapshot) {
|
||||
|
||||
logging.LogMessage(io, "Locking instance")
|
||||
if err := instance.TryLock(); err != nil {
|
||||
io.EPrintln(err)
|
||||
logging.LogMessage(io, "Aborting snapshot creation")
|
||||
|
||||
return Snapshot{
|
||||
ErrPanic: err,
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
logging.LogMessage(io, "Unlocking instance")
|
||||
instance.Unlock()
|
||||
}()
|
||||
|
||||
// setup the snapshot
|
||||
snapshot.Description = desc
|
||||
snapshot.Instance = instance.Instance
|
||||
|
||||
// capture anything critical, and write the end time
|
||||
defer func() {
|
||||
snapshot.ErrPanic = recover()
|
||||
}()
|
||||
|
||||
// do the create keeping track of time!
|
||||
logging.LogOperation(func() error {
|
||||
snapshot.StartTime = time.Now().UTC()
|
||||
|
||||
snapshot.ErrWhitebox = snapshot.makeParts(io, snapshots, instance, false)
|
||||
snapshot.ErrBlackbox = snapshot.makeParts(io, snapshots, instance, true)
|
||||
|
||||
snapshot.EndTime = time.Now().UTC()
|
||||
return nil
|
||||
}, io, "Writing snapshot files")
|
||||
|
||||
slices.Sort(snapshot.Manifest)
|
||||
return
|
||||
}
|
||||
|
||||
func (snapshot *Snapshot) makeParts(ios stream.IOStream, snapshots *Exporter, instance *wisski.WissKI, needsRunning bool) map[string]error {
|
||||
if !needsRunning && !snapshot.Description.Keepalive {
|
||||
stack := instance.Barrel()
|
||||
|
||||
logging.LogMessage(ios, "Stopping instance")
|
||||
snapshot.ErrStop = stack.Down(ios)
|
||||
|
||||
defer func() {
|
||||
logging.LogMessage(ios, "Starting instance")
|
||||
snapshot.ErrStart = stack.Up(ios)
|
||||
}()
|
||||
}
|
||||
// handle writing the manifest!
|
||||
manifest, done := snapshot.handleManifest(snapshot.Description.Dest)
|
||||
defer done()
|
||||
|
||||
// create a new status
|
||||
st := status.NewWithCompat(ios.Stdout, 0)
|
||||
st.Start()
|
||||
defer st.Stop()
|
||||
|
||||
// get all the components
|
||||
comps := collection.FilterClone(snapshots.Snapshotable, func(sc component.Snapshotable) bool {
|
||||
return sc.SnapshotNeedsRunning() == needsRunning
|
||||
})
|
||||
|
||||
results := make(map[string]error, len(comps))
|
||||
|
||||
errors := status.Group[component.Snapshotable, error]{
|
||||
PrefixString: func(item component.Snapshotable, index int) string {
|
||||
return fmt.Sprintf("[snapshot %q]: ", item.Name())
|
||||
},
|
||||
PrefixAlign: true,
|
||||
|
||||
Handler: func(sc component.Snapshotable, index int, writer io.Writer) error {
|
||||
return sc.Snapshot(
|
||||
instance.Instance,
|
||||
component.NewStagingContext(
|
||||
snapshots.Environment,
|
||||
stream.NewIOStream(writer, writer, nil, 0),
|
||||
filepath.Join(snapshot.Description.Dest, sc.SnapshotName()),
|
||||
manifest,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
||||
ResultString: status.DefaultErrorString[component.Snapshotable],
|
||||
}.Use(st, comps)
|
||||
|
||||
for i, wc := range comps {
|
||||
results[wc.Name()] = errors[i]
|
||||
}
|
||||
return results
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue