feat(snapshots): Implement volume shadow copy support on Windows (#3543)

* Implement volume shadow copy support on Windows

* Update go-vss version

* Fix unused variables

* Rename upload_actions*.go files

* Move vss settings to a separate policy section

* Handle existing shadow copy root

* Fix tests

* Fix lint issues

* Add cli policy test

* Add OS snapshot integration test

* Add GitHub Actions VSS test

* Fix "Incorrect function" error for root VSS snapshots

* Rename err to finalErr in createOSSnapshot

* Add OSSnapshotMode test

* Do not modify paths starting with \\?\ on Windows

* Allow warning messages in logfile tests

* Fix ignorefs not wrapping OS snapshot directory

* Retry VSS creation if another op was in progress

---------

Co-authored-by: Jarek Kowalski <jaak@jkowalski.net>
This commit is contained in:
Maxim Khitrov
2024-02-04 00:44:41 -05:00
committed by GitHub
parent 3fed193051
commit f62ef51700
23 changed files with 528 additions and 22 deletions

View File

@@ -24,6 +24,7 @@ type commandPolicySet struct {
policyLoggingFlags
policyRetentionFlags
policySchedulingFlags
policyOSSnapshotFlags
policyUploadFlags
}
@@ -39,6 +40,7 @@ func (c *commandPolicySet) setup(svc appServices, parent commandParent) {
c.policyLoggingFlags.setup(cmd)
c.policyRetentionFlags.setup(cmd)
c.policySchedulingFlags.setup(cmd)
c.policyOSSnapshotFlags.setup(cmd)
c.policyUploadFlags.setup(cmd)
cmd.Action(svc.repositoryWriterAction(c.run))
@@ -112,6 +114,10 @@ func (c *commandPolicySet) setPolicyFromFlags(ctx context.Context, p *policy.Pol
return errors.Wrap(err, "actions policy")
}
if err := c.setOSSnapshotPolicyFromFlags(ctx, &p.OSSnapshotPolicy, changeCount); err != nil {
return errors.Wrap(err, "OS snapshot policy")
}
if err := c.setLoggingPolicyFromFlags(ctx, &p.LoggingPolicy, changeCount); err != nil {
return errors.Wrap(err, "actions policy")
}

View File

@@ -0,0 +1,64 @@
package cli
import (
"context"
"github.com/alecthomas/kingpin/v2"
"github.com/pkg/errors"
"github.com/kopia/kopia/snapshot/policy"
)
type policyOSSnapshotFlags struct {
policyEnableVolumeShadowCopy string
}
func (c *policyOSSnapshotFlags) setup(cmd *kingpin.CmdClause) {
osSnapshotMode := []string{policy.OSSnapshotNeverString, policy.OSSnapshotAlwaysString, policy.OSSnapshotWhenAvailableString, inheritPolicyString}
cmd.Flag("enable-volume-shadow-copy", "Enable Volume Shadow Copy snapshots ('never', 'always', 'when-available', 'inherit')").PlaceHolder("MODE").EnumVar(&c.policyEnableVolumeShadowCopy, osSnapshotMode...)
}
func (c *policyOSSnapshotFlags) setOSSnapshotPolicyFromFlags(ctx context.Context, fp *policy.OSSnapshotPolicy, changeCount *int) error {
if err := applyPolicyOSSnapshotMode(ctx, "enable volume shadow copy", &fp.VolumeShadowCopy.Enable, c.policyEnableVolumeShadowCopy, changeCount); err != nil {
return errors.Wrap(err, "enable volume shadow copy")
}
return nil
}
func applyPolicyOSSnapshotMode(ctx context.Context, desc string, val **policy.OSSnapshotMode, str string, changeCount *int) error {
if str == "" {
// not changed
return nil
}
var mode policy.OSSnapshotMode
switch str {
case inheritPolicyString, defaultPolicyString:
*changeCount++
log(ctx).Infof(" - resetting %q to a default value inherited from parent.", desc)
*val = nil
return nil
case policy.OSSnapshotNeverString:
mode = policy.OSSnapshotNever
case policy.OSSnapshotAlwaysString:
mode = policy.OSSnapshotAlways
case policy.OSSnapshotWhenAvailableString:
mode = policy.OSSnapshotWhenAvailable
default:
return errors.Errorf("invalid %q mode %q", desc, str)
}
*changeCount++
log(ctx).Infof(" - setting %q to %v.", desc, mode)
*val = &mode
return nil
}

View File

@@ -0,0 +1,42 @@
package cli_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/internal/testutil"
"github.com/kopia/kopia/tests/testenv"
)
func TestSetOSSnapshotPolicy(t *testing.T) {
e := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, testenv.NewInProcRunner(t))
defer e.RunAndExpectSuccess(t, "repo", "disconnect")
e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir)
lines := e.RunAndExpectSuccess(t, "policy", "show", "--global")
lines = compressSpaces(lines)
require.Contains(t, lines, " Volume Shadow Copy: when-available (defined for this target)")
// make some directory we'll be setting policy on
td := testutil.TempDirectory(t)
lines = e.RunAndExpectSuccess(t, "policy", "show", td)
lines = compressSpaces(lines)
require.Contains(t, lines, " Volume Shadow Copy: when-available inherited from (global)")
e.RunAndExpectSuccess(t, "policy", "set", "--global", "--enable-volume-shadow-copy=always")
lines = e.RunAndExpectSuccess(t, "policy", "show", td)
lines = compressSpaces(lines)
require.Contains(t, lines, " Volume Shadow Copy: always inherited from (global)")
e.RunAndExpectSuccess(t, "policy", "set", "--enable-volume-shadow-copy=never", td)
lines = e.RunAndExpectSuccess(t, "policy", "show", td)
lines = compressSpaces(lines)
require.Contains(t, lines, " Volume Shadow Copy: never (defined for this target)")
}

View File

@@ -128,6 +128,8 @@ func printPolicy(out *textOutput, p *policy.Policy, def *policy.Definition) {
rows = append(rows, policyTableRow{})
rows = appendActionsPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendOSSnapshotPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendLoggingPolicyRows(rows, p, def)
out.printStdout("Policy for %v:\n\n%v\n", p.Target(), alignedPolicyTableRows(rows))
@@ -449,6 +451,15 @@ func appendActionCommandRows(rows []policyTableRow, h *policy.ActionCommand) []p
return rows
}
func appendOSSnapshotPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
rows = append(rows,
policyTableRow{"OS-level snapshot support:", "", ""},
policyTableRow{" Volume Shadow Copy:", p.OSSnapshotPolicy.VolumeShadowCopy.Enable.String(), definitionPointToString(p.Target(), def.OSSnapshotPolicy.VolumeShadowCopy.Enable)},
)
return rows
}
func valueOrNotSet(p *policy.OptionalInt) string {
if p == nil {
return "-"