diff --git a/cmd/info.go b/cmd/info.go index f6cb3b0..6df1d7c 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -58,6 +58,7 @@ func (i info) Run(context wisski_distillery.Context) error { context.Printf("GraphDB Password: %v\n", instance.GraphDBPassword) context.Printf("Running: %v\n", info.Running) + context.Printf("Locked: %v\n", info.Locked) context.Printf("Last Rebuild: %v\n", info.LastRebuild.String()) context.Printf("Skip Prefixes: %v\n", info.NoPrefixes) diff --git a/cmd/instance_lock.go b/cmd/instance_lock.go new file mode 100644 index 0000000..dee0d65 --- /dev/null +++ b/cmd/instance_lock.go @@ -0,0 +1,67 @@ +package cmd + +import ( + wisski_distillery "github.com/FAU-CDI/wisski-distillery" + "github.com/FAU-CDI/wisski-distillery/internal/core" + "github.com/tkw1536/goprogram/exit" +) + +// InstanceLock is then 'instance_lock' command +var InstanceLock wisski_distillery.Command = instanceLock{} + +type instanceLock struct { + Lock bool `short:"l" long:"lock" description:"Lock the provided WissKI instance"` + Unlock bool `short:"u" long:"unlock" description:"Unlock the provided WissKI instance"` + Positionals struct { + Slug string `positional-arg-name:"SLUG" required:"1-1" description:"slug of instance to lock or unlock"` + } `positional-args:"true"` +} + +func (instanceLock) Description() wisski_distillery.Description { + return wisski_distillery.Description{ + Requirements: core.Requirements{ + NeedsDistillery: true, + }, + Command: "instance_lock", + Description: "Locks or unlocks a WissKI instance", + } +} + +var errLockUnlockExcluded = exit.Error{ + Message: "Exactly one of `--lock` and `--unlock` must be provied", + ExitCode: exit.ExitCommandArguments, +} + +func (l instanceLock) AfterParse() error { + if l.Lock == l.Unlock { + return errLockUnlockExcluded + } + return nil +} + +var errNotUnlock = exit.Error{ + Message: "Unable to unlock instance: Not locked", + ExitCode: exit.ExitCommandArguments, +} + +func (l instanceLock) Run(context wisski_distillery.Context) error { + instance, err := context.Environment.Instances().WissKI(l.Positionals.Slug) + if err != nil { + return err + } + + if l.Unlock { + if !instance.Unlock() { + return errNotUnlock + } + context.Println("unlocked") + return nil + } + + if err := instance.TryLock(); err != nil { + return err + } + + context.Println("locked") + return nil +} diff --git a/cmd/purge.go b/cmd/purge.go index e5f5e5e..6989b49 100644 --- a/cmd/purge.go +++ b/cmd/purge.go @@ -102,6 +102,12 @@ func (p purge) Run(context wisski_distillery.Context) error { context.EPrintln(err) } + // remove the filesystem + logging.LogMessage(context.IOStream, "Remove lock data", instance.FilesystemBase) + if !instance.Unlock() { + context.EPrintln("instance was not locked") + } + logging.LogMessage(context.IOStream, "Instance %s has been purged", slug) return nil } diff --git a/cmd/wdcli/main.go b/cmd/wdcli/main.go index 626e38b..0dd0b19 100644 --- a/cmd/wdcli/main.go +++ b/cmd/wdcli/main.go @@ -37,6 +37,7 @@ func init() { // instance management wdcli.Register(cmd.Ls) wdcli.Register(cmd.Info) + wdcli.Register(cmd.InstanceLock) // instance tasks wdcli.Register(cmd.Shell) diff --git a/internal/component/info/html/instance.html b/internal/component/info/html/instance.html index 8e5952a..1ef9c9e 100644 --- a/internal/component/info/html/instance.html +++ b/internal/component/info/html/instance.html @@ -20,6 +20,7 @@ Excluded from Resolver: {{ .Info.NoPrefixes }}

Running: {{ .Info.Running }}
+ Locked: {{ .Info.Locked }}

Created: {{ .Instance.Created.Format "2006-01-02T15:04:05Z07:00" }}
diff --git a/internal/component/instances/wisski_lock.go b/internal/component/instances/wisski_lock.go new file mode 100644 index 0000000..12db452 --- /dev/null +++ b/internal/component/instances/wisski_lock.go @@ -0,0 +1,47 @@ +package instances + +import ( + "errors" + + "github.com/FAU-CDI/wisski-distillery/internal/models" +) + +var ErrLocked = errors.New("instance is locked") + +// TryLock attemps to lock this WissKI +// If this is not possible, returns ErrLocked +func (wisski WissKI) TryLock() error { + table, err := wisski.instances.SQL.QueryTable(true, models.LockTable) + if err != nil { + return ErrLocked + } + + result := table.FirstOrCreate(&models.Lock{}, models.Lock{Slug: wisski.Slug}) + locked := result.Error == nil && result.RowsAffected == 1 + + if !locked { + return ErrLocked + } + return nil +} + +func (wisski WissKI) IsLocked() (locked bool) { + table, err := wisski.instances.SQL.QueryTable(true, models.LockTable) + if err != nil { + return false + } + + // check if this instance is locked + table.Select("count(*) > 0").Where("slug = ?", wisski.Slug).Find(&locked) + return +} + +// Unlock unlocks this WissKI instance and returns if it succeeded +func (wisski WissKI) Unlock() bool { + table, err := wisski.instances.SQL.QueryTable(true, models.LockTable) + if err != nil { + return false + } + result := table.Where("slug = ?", wisski.Slug).Delete(&models.Lock{}) + return result.Error == nil && result.RowsAffected == 1 +} diff --git a/internal/component/instances/wisski_stack.go b/internal/component/instances/wisski_stack.go index cf5812c..ea80bb5 100644 --- a/internal/component/instances/wisski_stack.go +++ b/internal/component/instances/wisski_stack.go @@ -70,6 +70,11 @@ func (wisski *WissKI) setLastRebuild() error { // // It also logs the current time into the metadata belonging to this instance. func (wisski *WissKI) Build(stream stream.IOStream, start bool) error { + if err := wisski.TryLock(); err != nil { + return err + } + defer wisski.Unlock() + barrel := wisski.Barrel() var context component.InstallationContext diff --git a/internal/component/instances/wisski_status.go b/internal/component/instances/wisski_status.go index ce51912..19a2521 100644 --- a/internal/component/instances/wisski_status.go +++ b/internal/component/instances/wisski_status.go @@ -16,6 +16,8 @@ type WissKIInfo struct { Slug string // slug URL string // complete URL, including http(s) + Locked bool // Is this instance currently locked? + // Information about the running instance Running bool LastRebuild time.Time @@ -48,6 +50,12 @@ func (wisski *WissKI) Info(quick bool) (info WissKIInfo, err error) { return }) + // quick check if this instance is locked + group.Go(func() (err error) { + info.Locked = wisski.IsLocked() + return nil + }) + // slower checks for extra properties. // these might execute php code or require additional database queries. if !quick { diff --git a/internal/component/snapshots/snapshot.go b/internal/component/snapshots/snapshot.go index c83f92c..83e9eaa 100644 --- a/internal/component/snapshots/snapshot.go +++ b/internal/component/snapshots/snapshot.go @@ -44,6 +44,21 @@ type Snapshot struct { // Snapshot creates a new snapshot of this instance into dest func (snapshots *Manager) NewSnapshot(instance instances.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 diff --git a/internal/component/sql/update.go b/internal/component/sql/update.go index 4200095..2258d65 100644 --- a/internal/component/sql/update.go +++ b/internal/component/sql/update.go @@ -96,6 +96,11 @@ func (sql *SQL) Update(io stream.IOStream) error { &models.Export{}, models.ExportTable, }, + { + "lock", + &models.Lock{}, + models.LockTable, + }, } // migrate all of the tables! diff --git a/internal/models/lock.go b/internal/models/lock.go new file mode 100644 index 0000000..76ae07e --- /dev/null +++ b/internal/models/lock.go @@ -0,0 +1,11 @@ +package models + +// LockTable is the name of the table the 'Metadatum' model is stored in. +const LockTable = "locks" + +// Lock represents a log on WissKI Instances +type Lock struct { + Pk uint `gorm:"column:pk;primaryKey"` + + Slug string `gorm:"column:slug;not null"` // slug of instance +}