Skip to content

Commit

Permalink
Added support for actions that run before&after snapshot roots and be…
Browse files Browse the repository at this point in the history
…fore/after specific folders (#722)

* policy: add actions
* fs: added LocalFilesystemPath() which can optionally return local filesystem
  path (if entry is local)
* cli: added support for setting policy actions
* upload: support for executing actions before/after folder (non-inheritable)
  and before/after snapshots (inheritable)
* testing: end-to-end test for actions
* additional tests for actions with embedded scripts
  • Loading branch information
jkowalski committed Dec 21, 2020
1 parent 72a3372 commit 4f7d211
Show file tree
Hide file tree
Showing 20 changed files with 1,027 additions and 33 deletions.
7 changes: 6 additions & 1 deletion Makefile
@@ -1,6 +1,7 @@
COVERAGE_PACKAGES=github.com/kopia/kopia/repo/...,github.com/kopia/kopia/fs/...,github.com/kopia/kopia/snapshot/...
TEST_FLAGS?=
KOPIA_INTEGRATION_EXE=$(CURDIR)/dist/integration/kopia.exe
TESTING_ACTION_EXE=$(CURDIR)/dist/integration/testingaction.exe
FIO_DOCKER_TAG=ljishen/fio

export BOTO_PATH=$(CURDIR)/tools/.boto
Expand Down Expand Up @@ -219,8 +220,12 @@ vtest: $(gotestsum)
build-integration-test-binary:
go build -o $(KOPIA_INTEGRATION_EXE) -tags testing github.com/kopia/kopia

$(TESTING_ACTION_EXE): tests/testingaction/main.go
go build -o $(TESTING_ACTION_EXE) -tags testing github.com/kopia/kopia/tests/testingaction

integration-tests: export KOPIA_EXE ?= $(KOPIA_INTEGRATION_EXE)
integration-tests: build-integration-test-binary $(gotestsum)
integration-tests: export TESTING_ACTION_EXE ?= $(TESTING_ACTION_EXE)
integration-tests: build-integration-test-binary $(gotestsum) $(TESTING_ACTION_EXE)
$(GO_TEST) $(TEST_FLAGS) -count=1 -parallel $(PARALLEL) -timeout 3600s github.com/kopia/kopia/tests/end_to_end_test

endurance-tests: export KOPIA_EXE ?= $(KOPIA_INTEGRATION_EXE)
Expand Down
4 changes: 4 additions & 0 deletions cli/command_policy_set.go
Expand Up @@ -86,6 +86,10 @@ func setPolicyFromFlags(ctx context.Context, p *policy.Policy, changeCount *int)
return errors.Wrap(err, "scheduling policy")
}

if err := setActionsFromFlags(ctx, &p.Actions, changeCount); err != nil {
return errors.Wrap(err, "actions policy")
}

// It's not really a list, just optional boolean, last one wins.
for _, inherit := range *policySetInherit {
*changeCount++
Expand Down
116 changes: 116 additions & 0 deletions cli/command_policy_set_actions.go
@@ -0,0 +1,116 @@
package cli

import (
"context"
"encoding/csv"
"fmt"
"io/ioutil"
"strings"

"github.com/pkg/errors"

"github.com/kopia/kopia/snapshot/policy"
)

const maxScriptLength = 32000

var (
policySetBeforeFolderActionCommand = policySetCommand.Flag("before-folder-action", "Path to before-folder action command ('none' to remove)").Default("-").PlaceHolder("COMMAND").String()
policySetAfterFolderActionCommand = policySetCommand.Flag("after-folder-action", "Path to after-folder action command ('none' to remove)").Default("-").PlaceHolder("COMMAND").String()
policySetBeforeSnapshotRootActionCommand = policySetCommand.Flag("before-snapshot-root-action", "Path to before-snapshot-root action command ('none' to remove or 'inherit')").Default("-").PlaceHolder("COMMAND").String()
policySetAfterSnapshotRootActionCommand = policySetCommand.Flag("after-snapshot-root-action", "Path to after-snapshot-root action command ('none' to remove or 'inherit')").Default("-").PlaceHolder("COMMAND").String()
policySetActionCommandTimeout = policySetCommand.Flag("action-command-timeout", "Max time allowed for a action to run in seconds").Default("5m").Duration()
policySetActionCommandMode = policySetCommand.Flag("action-command-mode", "Action command mode").Default("essential").Enum("essential", "optional", "async")
policySetPersistActionScript = policySetCommand.Flag("persist-action-script", "Persist action script").Bool()
)

func setActionsFromFlags(ctx context.Context, p *policy.ActionsPolicy, changeCount *int) error {
if err := setActionCommandFromFlags(ctx, "before-folder", &p.BeforeFolder, *policySetBeforeFolderActionCommand, changeCount); err != nil {
return errors.Wrap(err, "invalid before-folder-action")
}

if err := setActionCommandFromFlags(ctx, "after-folder", &p.AfterFolder, *policySetAfterFolderActionCommand, changeCount); err != nil {
return errors.Wrap(err, "invalid after-folder-action")
}

if err := setActionCommandFromFlags(ctx, "before-snapshot-root", &p.BeforeSnapshotRoot, *policySetBeforeSnapshotRootActionCommand, changeCount); err != nil {
return errors.Wrap(err, "invalid before-snapshot-root-action")
}

if err := setActionCommandFromFlags(ctx, "after-snapshot-root", &p.AfterSnapshotRoot, *policySetAfterSnapshotRootActionCommand, changeCount); err != nil {
return errors.Wrap(err, "invalid after-snapshot-root-action")
}

return nil
}

func setActionCommandFromFlags(ctx context.Context, actionName string, cmd **policy.ActionCommand, value string, changeCount *int) error {
if value == "-" {
// not set
return nil
}

if value == "" {
log(ctx).Infof(" - removing %v action", actionName)

*changeCount++

*cmd = nil

return nil
}

*cmd = &policy.ActionCommand{
TimeoutSeconds: int(policySetActionCommandTimeout.Seconds()),
Mode: *policySetActionCommandMode,
}

*changeCount++

if *policySetPersistActionScript {
script, err := ioutil.ReadFile(value) //nolint:gosec
if err != nil {
return err
}

if len(script) > maxScriptLength {
return errors.Errorf("action script file (%v) too long: %v, max allowed %d", value, len(script), maxScriptLength)
}

log(ctx).Infof(" - setting %v (%v) action script from file %v (%v bytes) with timeout %v", actionName, *policySetActionCommandMode, value, len(script), *policySetActionCommandTimeout)

(*cmd).Script = string(script)

return nil
}

// parse path as CSV as if space was the separator, this automatically takes care of quotations
r := csv.NewReader(strings.NewReader(value))
r.Comma = ' ' // space

fields, err := r.Read()
if err != nil {
return errors.Wrapf(err, "error parsing %v command", actionName)
}

(*cmd).Command = fields[0]
(*cmd).Arguments = fields[1:]

if len((*cmd).Arguments) == 0 {
log(ctx).Infof(" - setting %v (%v) action command to %v and timeout %v", actionName, *policySetActionCommandMode, quoteArguments((*cmd).Command), *policySetActionCommandTimeout)
} else {
log(ctx).Infof(" - setting %v (%v) action command to %v with arguments %v and timeout %v", actionName, *policySetActionCommandMode, quoteArguments((*cmd).Command), quoteArguments((*cmd).Arguments...), *policySetActionCommandTimeout)
}

return nil
}

func quoteArguments(s ...string) string {
var result []string

for _, v := range s {
result = append(result, fmt.Sprintf("\"%v\"", v))
}

return strings.Join(result, " ")
}
100 changes: 79 additions & 21 deletions cli/command_policy_show.go
Expand Up @@ -3,11 +3,13 @@ package cli
import (
"context"
"fmt"
"strings"

"github.com/pkg/errors"

"github.com/kopia/kopia/internal/units"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/snapshot"
"github.com/kopia/kopia/snapshot/policy"
)

Expand Down Expand Up @@ -44,10 +46,10 @@ func showPolicy(ctx context.Context, rep repo.Repository) error {
return nil
}

func getDefinitionPoint(parents []*policy.Policy, match func(p *policy.Policy) bool) string {
for i, p := range parents {
func getDefinitionPoint(target snapshot.SourceInfo, parents []*policy.Policy, match func(p *policy.Policy) bool) string {
for _, p := range parents {
if match(p) {
if i == 0 {
if p.Target() == target {
return "(defined for this target)"
}

Expand Down Expand Up @@ -84,38 +86,40 @@ func printPolicy(p *policy.Policy, parents []*policy.Policy) {
printSchedulingPolicy(p, parents)
printStdout("\n")
printCompressionPolicy(p, parents)
printStdout("\n")
printActions(p, parents)
}

func printRetentionPolicy(p *policy.Policy, parents []*policy.Policy) {
printStdout("Retention:\n")
printStdout(" Annual snapshots: %3v %v\n",
valueOrNotSet(p.RetentionPolicy.KeepAnnual),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.RetentionPolicy.KeepAnnual != nil
}))
printStdout(" Monthly snapshots: %3v %v\n",
valueOrNotSet(p.RetentionPolicy.KeepMonthly),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.RetentionPolicy.KeepMonthly != nil
}))
printStdout(" Weekly snapshots: %3v %v\n",
valueOrNotSet(p.RetentionPolicy.KeepWeekly),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.RetentionPolicy.KeepWeekly != nil
}))
printStdout(" Daily snapshots: %3v %v\n",
valueOrNotSet(p.RetentionPolicy.KeepDaily),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.RetentionPolicy.KeepDaily != nil
}))
printStdout(" Hourly snapshots: %3v %v\n",
valueOrNotSet(p.RetentionPolicy.KeepHourly),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.RetentionPolicy.KeepHourly != nil
}))
printStdout(" Latest snapshots: %3v %v\n",
valueOrNotSet(p.RetentionPolicy.KeepLatest),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.RetentionPolicy.KeepLatest != nil
}))
}
Expand All @@ -125,7 +129,7 @@ func printFilesPolicy(p *policy.Policy, parents []*policy.Policy) {

printStdout(" Ignore cache directories: %5v %v\n",
p.FilesPolicy.IgnoreCacheDirectoriesOrDefault(true),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.FilesPolicy.IgnoreCacheDirs != nil
}))

Expand All @@ -137,7 +141,7 @@ func printFilesPolicy(p *policy.Policy, parents []*policy.Policy) {

for _, rule := range p.FilesPolicy.IgnoreRules {
rule := rule
printStdout(" %-30v %v\n", rule, getDefinitionPoint(parents, func(pol *policy.Policy) bool {
printStdout(" %-30v %v\n", rule, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return containsString(pol.FilesPolicy.IgnoreRules, rule)
}))
}
Expand All @@ -148,22 +152,22 @@ func printFilesPolicy(p *policy.Policy, parents []*policy.Policy) {

for _, dotFile := range p.FilesPolicy.DotIgnoreFiles {
dotFile := dotFile
printStdout(" %-30v %v\n", dotFile, getDefinitionPoint(parents, func(pol *policy.Policy) bool {
printStdout(" %-30v %v\n", dotFile, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return containsString(pol.FilesPolicy.DotIgnoreFiles, dotFile)
}))
}

if maxSize := p.FilesPolicy.MaxFileSize; maxSize > 0 {
printStdout(" Ignore files above: %10v %v\n",
units.BytesStringBase2(maxSize),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.FilesPolicy.MaxFileSize != 0
}))
}

printStdout(" Scan one filesystem only: %5v %v\n",
p.FilesPolicy.OneFileSystemOrDefault(false),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.FilesPolicy.OneFileSystem != nil
}))
}
Expand All @@ -173,13 +177,13 @@ func printErrorHandlingPolicy(p *policy.Policy, parents []*policy.Policy) {

printStdout(" Ignore file read errors: %5v %v\n",
p.ErrorHandlingPolicy.IgnoreFileErrorsOrDefault(false),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.ErrorHandlingPolicy.IgnoreFileErrors != nil
}))

printStdout(" Ignore directory read errors: %5v %v\n",
p.ErrorHandlingPolicy.IgnoreDirectoryErrorsOrDefault(false),
getDefinitionPoint(parents, func(pol *policy.Policy) bool {
getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.ErrorHandlingPolicy.IgnoreDirectoryErrors != nil
}))
}
Expand All @@ -190,7 +194,7 @@ func printSchedulingPolicy(p *policy.Policy, parents []*policy.Policy) {
any := false

if p.SchedulingPolicy.Interval() != 0 {
printStdout(" Snapshot interval: %10v %v\n", p.SchedulingPolicy.Interval(), getDefinitionPoint(parents, func(pol *policy.Policy) bool {
printStdout(" Snapshot interval: %10v %v\n", p.SchedulingPolicy.Interval(), getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.SchedulingPolicy.Interval() != 0
}))

Expand All @@ -202,7 +206,7 @@ func printSchedulingPolicy(p *policy.Policy, parents []*policy.Policy) {

for _, tod := range p.SchedulingPolicy.TimesOfDay {
tod := tod
printStdout(" %9v %v\n", tod, getDefinitionPoint(parents, func(pol *policy.Policy) bool {
printStdout(" %9v %v\n", tod, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
for _, t := range pol.SchedulingPolicy.TimesOfDay {
if t == tod {
return true
Expand All @@ -224,7 +228,7 @@ func printSchedulingPolicy(p *policy.Policy, parents []*policy.Policy) {
func printCompressionPolicy(p *policy.Policy, parents []*policy.Policy) {
if p.CompressionPolicy.CompressorName != "" && p.CompressionPolicy.CompressorName != "none" {
printStdout("Compression:\n")
printStdout(" Compressor: %q %v\n", p.CompressionPolicy.CompressorName, getDefinitionPoint(parents, func(pol *policy.Policy) bool {
printStdout(" Compressor: %q %v\n", p.CompressionPolicy.CompressorName, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.CompressionPolicy.CompressorName != ""
}))
} else {
Expand All @@ -238,7 +242,7 @@ func printCompressionPolicy(p *policy.Policy, parents []*policy.Policy) {

for _, rule := range p.CompressionPolicy.OnlyCompress {
rule := rule
printStdout(" %-30v %v\n", rule, getDefinitionPoint(parents, func(pol *policy.Policy) bool {
printStdout(" %-30v %v\n", rule, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return containsString(pol.CompressionPolicy.OnlyCompress, rule)
}))
}
Expand All @@ -248,7 +252,7 @@ func printCompressionPolicy(p *policy.Policy, parents []*policy.Policy) {

for _, rule := range p.CompressionPolicy.NeverCompress {
rule := rule
printStdout(" %-30v %v\n", rule, getDefinitionPoint(parents, func(pol *policy.Policy) bool {
printStdout(" %-30v %v\n", rule, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return containsString(pol.CompressionPolicy.NeverCompress, rule)
}))
}
Expand All @@ -269,6 +273,60 @@ func printCompressionPolicy(p *policy.Policy, parents []*policy.Policy) {
}
}

func printActions(p *policy.Policy, parents []*policy.Policy) {
var anyActions bool

if h := p.Actions.BeforeSnapshotRoot; h != nil {
printStdout("Run command before snapshot root: %v\n", getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.Actions.BeforeSnapshotRoot == h
}))

printActionCommand(h)

anyActions = true
}

if h := p.Actions.AfterSnapshotRoot; h != nil {
printStdout("Run command after snapshot root: %v\n", getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
return pol.Actions.AfterSnapshotRoot == h
}))
printActionCommand(h)

anyActions = true
}

if h := p.Actions.BeforeFolder; h != nil {
printStdout("Run command before this folder: (non-inheritable)\n")

printActionCommand(h)

anyActions = true
}

if h := p.Actions.AfterFolder; h != nil {
printStdout("Run command after this folder: (non-inheritable)\n")
printActionCommand(h)

anyActions = true
}

if !anyActions {
printStdout("No actions defined.\n")
}
}

func printActionCommand(h *policy.ActionCommand) {
if h.Script != "" {
printStdout(" Embedded Script: %q\n", h.Script)
} else {
printStdout(" Command: %v %v\n", h.Command, strings.Join(h.Arguments, " "))
}

printStdout(" Mode: %v\n", h.Mode)
printStdout(" Timeout: %v\n", h.TimeoutSeconds)
printStdout("\n")
}

func valueOrNotSet(p *int) string {
if p == nil {
return "-"
Expand Down
1 change: 1 addition & 0 deletions fs/entry.go
Expand Up @@ -14,6 +14,7 @@ type Entry interface {
os.FileInfo
Owner() OwnerInfo
Device() DeviceInfo
LocalFilesystemPath() string // returns full local filesystem path or "" if not a local filesystem
}

// OwnerInfo describes owner of a filesystem entry.
Expand Down

0 comments on commit 4f7d211

Please sign in to comment.