Rename snapshots.Manager => exporter.Exporter

This commit is contained in:
Tom Wiesing 2022-10-17 15:41:33 +02:00
parent 063f3f9b7d
commit 8d2855fdcb
No known key found for this signature in database
23 changed files with 105 additions and 100 deletions

View 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")
}

View 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
}

View 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
})
}

View 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,
}
}

View 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)
}

View 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
})
}

View 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
}

View 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,
}
}

View 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
}

View 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
}
}

View 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
}

View 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()
}

View 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
}