Merge pull request #4764 from MichaelEischer/safe-keep-tag

Prevent unsafe uses of `forget --keep-tag`
This commit is contained in:
Michael Eischer 2024-05-24 20:51:20 +02:00 committed by GitHub
commit 3eeb6723cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1212 additions and 2149 deletions

View file

@ -0,0 +1,17 @@
Bugfix: Prevent `forget --keep-tags invalid` from deleting all snapshots
Running `forget --keep-tags invalid`, where the tag `invalid` does not
exist in the repository, would remove all snapshots. This is especially
problematic if the tag name contains a typo.
The `forget` command now fails with an error if all snapshots in a snapshot
group would be deleted. This prevents the above example from deleting all
snapshots.
It is possible to temporarily disable the new check by setting the environment variable
`RESTIC_FEATURES=safe-forget-keep-tags=false`. Note that this feature flag
will be removed in the next minor restic version.
https://github.com/restic/restic/issues/4568
https://github.com/restic/restic/pull/4764
https://forum.restic.net/t/delete-all-snapshots-in-one-command-is-this-feature-intentional/6923/3

View file

@ -0,0 +1,8 @@
Enhancement: Remove all snapshots using `forget --unsafe-allow-remove-all`
The forget command now supports the `--unsafe-allow-remove-all` option. It must
always be combined with a snapshot filter (by host, path or tag).
For example the command `forget --tag example --unsafe-allow-remove-all`,
removes all snapshots with tag `example`.
https://github.com/restic/restic/pull/4764

View file

@ -3,10 +3,12 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"strconv" "strconv"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/feature"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -91,6 +93,8 @@ type ForgetOptions struct {
WithinYearly restic.Duration WithinYearly restic.Duration
KeepTags restic.TagLists KeepTags restic.TagLists
UnsafeAllowRemoveAll bool
restic.SnapshotFilter restic.SnapshotFilter
Compact bool Compact bool
@ -120,6 +124,7 @@ func init() {
f.VarP(&forgetOptions.WithinMonthly, "keep-within-monthly", "", "keep monthly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot") f.VarP(&forgetOptions.WithinMonthly, "keep-within-monthly", "", "keep monthly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.VarP(&forgetOptions.WithinYearly, "keep-within-yearly", "", "keep yearly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot") f.VarP(&forgetOptions.WithinYearly, "keep-within-yearly", "", "keep yearly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)") f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
f.BoolVar(&forgetOptions.UnsafeAllowRemoveAll, "unsafe-allow-remove-all", false, "allow deleting all snapshots of a snapshot group")
initMultiSnapshotFilter(f, &forgetOptions.SnapshotFilter, false) initMultiSnapshotFilter(f, &forgetOptions.SnapshotFilter, false)
f.StringArrayVar(&forgetOptions.Hosts, "hostname", nil, "only consider snapshots with the given `hostname` (can be specified multiple times)") f.StringArrayVar(&forgetOptions.Hosts, "hostname", nil, "only consider snapshots with the given `hostname` (can be specified multiple times)")
@ -221,54 +226,62 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
Tags: opts.KeepTags, Tags: opts.KeepTags,
} }
if policy.Empty() && len(args) == 0 { if policy.Empty() {
printer.P("no policy was specified, no snapshots will be removed\n") if opts.UnsafeAllowRemoveAll {
if opts.SnapshotFilter.Empty() {
return errors.Fatal("--unsafe-allow-remove-all is not allowed unless a snapshot filter option is specified")
}
// UnsafeAllowRemoveAll together with snapshot filter is fine
} else {
return errors.Fatal("no policy was specified, no snapshots will be removed")
}
} }
if !policy.Empty() { printer.P("Applying Policy: %v\n", policy)
printer.P("Applying Policy: %v\n", policy)
for k, snapshotGroup := range snapshotGroups { for k, snapshotGroup := range snapshotGroups {
if gopts.Verbose >= 1 && !gopts.JSON { if gopts.Verbose >= 1 && !gopts.JSON {
err = PrintSnapshotGroupHeader(globalOptions.stdout, k) err = PrintSnapshotGroupHeader(globalOptions.stdout, k)
if err != nil { if err != nil {
return err
}
}
var key restic.SnapshotGroupKey
if json.Unmarshal([]byte(k), &key) != nil {
return err return err
} }
}
var fg ForgetGroup var key restic.SnapshotGroupKey
fg.Tags = key.Tags if json.Unmarshal([]byte(k), &key) != nil {
fg.Host = key.Hostname return err
fg.Paths = key.Paths }
keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy) var fg ForgetGroup
fg.Tags = key.Tags
fg.Host = key.Hostname
fg.Paths = key.Paths
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON { keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy)
printer.P("keep %d snapshots:\n", len(keep))
PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact)
printer.P("\n")
}
fg.Keep = asJSONSnapshots(keep)
if len(remove) != 0 && !gopts.Quiet && !gopts.JSON { if feature.Flag.Enabled(feature.SafeForgetKeepTags) && !policy.Empty() && len(keep) == 0 {
printer.P("remove %d snapshots:\n", len(remove)) return fmt.Errorf("refusing to delete last snapshot of snapshot group \"%v\"", key.String())
PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact) }
printer.P("\n") if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
} printer.P("keep %d snapshots:\n", len(keep))
fg.Remove = asJSONSnapshots(remove) PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact)
printer.P("\n")
}
fg.Keep = asJSONSnapshots(keep)
fg.Reasons = asJSONKeeps(reasons) if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
printer.P("remove %d snapshots:\n", len(remove))
PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact)
printer.P("\n")
}
fg.Remove = asJSONSnapshots(remove)
jsonGroups = append(jsonGroups, &fg) fg.Reasons = asJSONKeeps(reasons)
for _, sn := range remove { jsonGroups = append(jsonGroups, &fg)
removeSnIDs.Insert(*sn.ID())
} for _, sn := range remove {
removeSnIDs.Insert(*sn.ID())
} }
} }
} }

View file

@ -2,18 +2,65 @@ package main
import ( import (
"context" "context"
"path/filepath"
"strings"
"testing" "testing"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
) )
func testRunForget(t testing.TB, gopts GlobalOptions, args ...string) { func testRunForgetMayFail(gopts GlobalOptions, opts ForgetOptions, args ...string) error {
opts := ForgetOptions{}
pruneOpts := PruneOptions{ pruneOpts := PruneOptions{
MaxUnused: "5%", MaxUnused: "5%",
} }
rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
return runForget(context.TODO(), opts, pruneOpts, gopts, term, args) return runForget(context.TODO(), opts, pruneOpts, gopts, term, args)
})) })
}
func testRunForget(t testing.TB, gopts GlobalOptions, opts ForgetOptions, args ...string) {
rtest.OK(t, testRunForgetMayFail(gopts, opts, args...))
}
func TestRunForgetSafetyNet(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
testSetupBackupData(t, env)
opts := BackupOptions{
Host: "example",
}
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
testListSnapshots(t, env.gopts, 2)
// --keep-tags invalid
err := testRunForgetMayFail(env.gopts, ForgetOptions{
KeepTags: restic.TagLists{restic.TagList{"invalid"}},
GroupBy: restic.SnapshotGroupByOptions{Host: true, Path: true},
})
rtest.Assert(t, strings.Contains(err.Error(), `refusing to delete last snapshot of snapshot group "host example, path`), "wrong error message got %v", err)
// disallow `forget --unsafe-allow-remove-all`
err = testRunForgetMayFail(env.gopts, ForgetOptions{
UnsafeAllowRemoveAll: true,
})
rtest.Assert(t, strings.Contains(err.Error(), `--unsafe-allow-remove-all is not allowed unless a snapshot filter option is specified`), "wrong error message got %v", err)
// disallow `forget` without options
err = testRunForgetMayFail(env.gopts, ForgetOptions{})
rtest.Assert(t, strings.Contains(err.Error(), `no policy was specified, no snapshots will be removed`), "wrong error message got %v", err)
// `forget --host example --unsafe-allow-remmove-all` should work
testRunForget(t, env.gopts, ForgetOptions{
UnsafeAllowRemoveAll: true,
GroupBy: restic.SnapshotGroupByOptions{Host: true, Path: true},
SnapshotFilter: restic.SnapshotFilter{
Hosts: []string{opts.Host},
},
})
testListSnapshots(t, env.gopts, 0)
} }

View file

@ -75,7 +75,7 @@ func createPrunableRepo(t *testing.T, env *testEnvironment) {
testListSnapshots(t, env.gopts, 3) testListSnapshots(t, env.gopts, 3)
testRunForgetJSON(t, env.gopts) testRunForgetJSON(t, env.gopts)
testRunForget(t, env.gopts, firstSnapshot.String()) testRunForget(t, env.gopts, ForgetOptions{}, firstSnapshot.String())
} }
func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) { func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
@ -129,7 +129,7 @@ func TestPruneWithDamagedRepository(t *testing.T) {
// create and delete snapshot to create unused blobs // create and delete snapshot to create unused blobs
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts) testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts)
firstSnapshot := testListSnapshots(t, env.gopts, 1)[0] firstSnapshot := testListSnapshots(t, env.gopts, 1)[0]
testRunForget(t, env.gopts, firstSnapshot.String()) testRunForget(t, env.gopts, ForgetOptions{}, firstSnapshot.String())
oldPacks := listPacks(env.gopts, t) oldPacks := listPacks(env.gopts, t)

View file

@ -62,7 +62,7 @@ func TestRepairSnapshotsWithLostData(t *testing.T) {
testRunCheckMustFail(t, env.gopts) testRunCheckMustFail(t, env.gopts)
// repository must be ok after removing the broken snapshots // repository must be ok after removing the broken snapshots
testRunForget(t, env.gopts, snapshotIDs[0].String(), snapshotIDs[1].String()) testRunForget(t, env.gopts, ForgetOptions{}, snapshotIDs[0].String(), snapshotIDs[1].String())
testListSnapshots(t, env.gopts, 2) testListSnapshots(t, env.gopts, 2)
_, err := testRunCheckOutput(env.gopts, false) _, err := testRunCheckOutput(env.gopts, false)
rtest.OK(t, err) rtest.OK(t, err)
@ -86,7 +86,7 @@ func TestRepairSnapshotsWithLostTree(t *testing.T) {
// remove tree for foo/bar and the now completely broken first snapshot // remove tree for foo/bar and the now completely broken first snapshot
removePacks(env.gopts, t, restic.NewIDSet(oldPacks...)) removePacks(env.gopts, t, restic.NewIDSet(oldPacks...))
testRunForget(t, env.gopts, oldSnapshot[0].String()) testRunForget(t, env.gopts, ForgetOptions{}, oldSnapshot[0].String())
testRunCheckMustFail(t, env.gopts) testRunCheckMustFail(t, env.gopts)
// repair // repair

View file

@ -182,7 +182,9 @@ The ``forget`` command accepts the following policy options:
- ``--keep-yearly n`` for the last ``n`` years which have one or more - ``--keep-yearly n`` for the last ``n`` years which have one or more
snapshots, keep only the most recent one for each year. snapshots, keep only the most recent one for each year.
- ``--keep-tag`` keep all snapshots which have all tags specified by - ``--keep-tag`` keep all snapshots which have all tags specified by
this option (can be specified multiple times). this option (can be specified multiple times). The ``forget`` command will
exit with an error if all snapshots in a snapshot group would be removed
as none of them have the specified tags.
- ``--keep-within duration`` keep all snapshots having a timestamp within - ``--keep-within duration`` keep all snapshots having a timestamp within
the specified duration of the latest snapshot, where ``duration`` is a the specified duration of the latest snapshot, where ``duration`` is a
number of years, months, days, and hours. E.g. ``2y5m7d3h`` will keep all number of years, months, days, and hours. E.g. ``2y5m7d3h`` will keep all
@ -336,12 +338,23 @@ year and yearly for the last 75 years, you can instead specify ``forget
--keep-within-yearly 75y`` (note that `1w` is not a recognized duration, so --keep-within-yearly 75y`` (note that `1w` is not a recognized duration, so
you will have to specify `7d` instead). you will have to specify `7d` instead).
Removing all snapshots
======================
For safety reasons, restic refuses to act on an "empty" policy. For example, For safety reasons, restic refuses to act on an "empty" policy. For example,
if one were to specify ``--keep-last 0`` to forget *all* snapshots in the if one were to specify ``--keep-last 0`` to forget *all* snapshots in the
repository, restic will respond that no snapshots will be removed. To delete repository, restic will respond that no snapshots will be removed. To delete
all snapshots, use ``--keep-last 1`` and then finally remove the last snapshot all snapshots, use ``--keep-last 1`` and then finally remove the last snapshot
manually (by passing the ID to ``forget``). manually (by passing the ID to ``forget``).
Since restic 0.17.0, it is possible to delete all snapshots for a specific
host, tag or path using the ``--unsafe-allow-remove-all`` option. The option
must always be combined with a snapshot filter (by host, path or tag).
For example the command ``forget --tag example --unsafe-allow-remove-all``
removes all snapshots with tag ``example``.
Security considerations in append-only mode Security considerations in append-only mode
=========================================== ===========================================

View file

@ -9,6 +9,7 @@ const (
DeprecateLegacyIndex FlagName = "deprecate-legacy-index" DeprecateLegacyIndex FlagName = "deprecate-legacy-index"
DeprecateS3LegacyLayout FlagName = "deprecate-s3-legacy-layout" DeprecateS3LegacyLayout FlagName = "deprecate-s3-legacy-layout"
DeviceIDForHardlinks FlagName = "device-id-for-hardlinks" DeviceIDForHardlinks FlagName = "device-id-for-hardlinks"
SafeForgetKeepTags FlagName = "safe-forget-keep-tags"
) )
func init() { func init() {
@ -17,5 +18,6 @@ func init() {
DeprecateLegacyIndex: {Type: Beta, Description: "disable support for index format used by restic 0.1.0. Use `restic repair index` to update the index if necessary."}, DeprecateLegacyIndex: {Type: Beta, Description: "disable support for index format used by restic 0.1.0. Use `restic repair index` to update the index if necessary."},
DeprecateS3LegacyLayout: {Type: Beta, Description: "disable support for S3 legacy layout used up to restic 0.7.0. Use `RESTIC_FEATURES=deprecate-s3-legacy-layout=false restic migrate s3_layout` to migrate your S3 repository if necessary."}, DeprecateS3LegacyLayout: {Type: Beta, Description: "disable support for S3 legacy layout used up to restic 0.7.0. Use `RESTIC_FEATURES=deprecate-s3-legacy-layout=false restic migrate s3_layout` to migrate your S3 repository if necessary."},
DeviceIDForHardlinks: {Type: Alpha, Description: "store deviceID only for hardlinks to reduce metadata changes for example when using btrfs subvolumes. Will be removed in a future restic version after repository format 3 is available"}, DeviceIDForHardlinks: {Type: Alpha, Description: "store deviceID only for hardlinks to reduce metadata changes for example when using btrfs subvolumes. Will be removed in a future restic version after repository format 3 is available"},
SafeForgetKeepTags: {Type: Beta, Description: "prevent deleting all snapshots if the tag passed to `forget --keep-tags tagname` does not exist"},
}) })
} }

View file

@ -24,7 +24,7 @@ type SnapshotFilter struct {
TimestampLimit time.Time TimestampLimit time.Time
} }
func (f *SnapshotFilter) empty() bool { func (f *SnapshotFilter) Empty() bool {
return len(f.Hosts)+len(f.Tags)+len(f.Paths) == 0 return len(f.Hosts)+len(f.Tags)+len(f.Paths) == 0
} }
@ -173,7 +173,7 @@ func (f *SnapshotFilter) FindAll(ctx context.Context, be Lister, loader LoaderUn
} }
// Give the user some indication their filters are not used. // Give the user some indication their filters are not used.
if !usedFilter && !f.empty() { if !usedFilter && !f.Empty() {
return fn("filters", nil, errors.Errorf("explicit snapshot ids are given")) return fn("filters", nil, errors.Errorf("explicit snapshot ids are given"))
} }
return nil return nil

View file

@ -66,6 +66,20 @@ type SnapshotGroupKey struct {
Tags []string `json:"tags"` Tags []string `json:"tags"`
} }
func (s *SnapshotGroupKey) String() string {
var parts []string
if s.Hostname != "" {
parts = append(parts, fmt.Sprintf("host %v", s.Hostname))
}
if len(s.Paths) != 0 {
parts = append(parts, fmt.Sprintf("path %v", s.Paths))
}
if len(s.Tags) != 0 {
parts = append(parts, fmt.Sprintf("tags %v", s.Tags))
}
return strings.Join(parts, ", ")
}
// GroupSnapshots takes a list of snapshots and a grouping criteria and creates // GroupSnapshots takes a list of snapshots and a grouping criteria and creates
// a grouped list of snapshots. // a grouped list of snapshots.
func GroupSnapshots(snapshots Snapshots, groupBy SnapshotGroupByOptions) (map[string]Snapshots, bool, error) { func GroupSnapshots(snapshots Snapshots, groupBy SnapshotGroupByOptions) (map[string]Snapshots, bool, error) {

View file

@ -94,7 +94,11 @@ func (e ExpirePolicy) String() (s string) {
s += fmt.Sprintf("all snapshots within %s of the newest", e.Within) s += fmt.Sprintf("all snapshots within %s of the newest", e.Within)
} }
s = "keep " + s if s == "" {
s = "remove"
} else {
s = "keep " + s
}
return s return s
} }
@ -186,16 +190,6 @@ func ApplyPolicy(list Snapshots, p ExpirePolicy) (keep, remove Snapshots, reason
// sort newest snapshots first // sort newest snapshots first
sort.Stable(list) sort.Stable(list)
if p.Empty() {
for _, sn := range list {
reasons = append(reasons, KeepReason{
Snapshot: sn,
Matches: []string{"policy is empty"},
})
}
return list, remove, reasons
}
if len(list) == 0 { if len(list) == 0 {
return list, nil, nil return list, nil, nil
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -507,7 +507,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -518,7 +520,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -529,7 +533,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -540,7 +546,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -551,7 +559,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -562,7 +572,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -573,7 +585,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -584,7 +598,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -595,7 +611,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -606,7 +624,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -617,7 +637,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -628,7 +650,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -639,7 +663,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -650,7 +676,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -661,7 +689,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -672,7 +702,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -683,7 +715,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -694,7 +728,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -705,7 +741,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -716,7 +754,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -727,7 +767,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -738,7 +780,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -749,7 +793,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -760,7 +806,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -771,7 +819,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -782,7 +832,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -793,7 +845,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -804,7 +858,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -815,7 +871,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -826,7 +884,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -837,7 +897,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -848,7 +910,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -859,7 +923,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -870,7 +936,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -881,7 +949,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -892,7 +962,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -903,7 +975,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -914,7 +988,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -925,7 +1001,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -936,7 +1014,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -947,7 +1027,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -958,7 +1040,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -969,7 +1053,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -980,7 +1066,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -991,7 +1079,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1002,7 +1092,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1013,7 +1105,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1024,7 +1118,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1035,7 +1131,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1046,7 +1144,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1057,7 +1157,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1068,7 +1170,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1079,7 +1183,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1090,7 +1196,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1101,7 +1209,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1112,7 +1222,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1127,7 +1239,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1141,7 +1255,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1155,7 +1271,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1169,7 +1287,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1183,7 +1303,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1197,7 +1319,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1211,7 +1335,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1225,7 +1351,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1239,7 +1367,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1253,7 +1383,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1267,7 +1399,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1281,7 +1415,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1295,7 +1431,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1309,7 +1447,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1323,7 +1463,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1334,7 +1476,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1345,7 +1489,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1356,7 +1502,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1367,7 +1515,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1378,7 +1528,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1389,7 +1541,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1400,7 +1554,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1411,7 +1567,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1422,7 +1580,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1433,7 +1593,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1444,7 +1606,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1455,7 +1619,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1466,7 +1632,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1477,7 +1645,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1488,7 +1658,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1499,7 +1671,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1510,7 +1684,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1521,7 +1697,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -1532,7 +1710,9 @@
"matches": [ "matches": [
"hourly snapshot" "hourly snapshot"
], ],
"counters": {"Hourly": -1} "counters": {
"hourly": -1
}
} }
] ]
} }

View file

@ -74,10 +74,15 @@
"matches": [ "matches": [
"daily snapshot", "daily snapshot",
"weekly snapshot", "weekly snapshot",
"monthly snapshot", "monthly snapshot",
"yearly snapshot" "yearly snapshot"
], ],
"counters": {"Daily": 2, "Weekly": 1, "Monthly": -1, "Yearly": -1} "counters": {
"daily": 2,
"weekly": 1,
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -89,7 +94,11 @@
"daily snapshot", "daily snapshot",
"weekly snapshot" "weekly snapshot"
], ],
"counters": {"Daily": 1, "Monthly": -1, "Yearly": -1} "counters": {
"daily": 1,
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -100,7 +109,10 @@
"matches": [ "matches": [
"daily snapshot" "daily snapshot"
], ],
"counters": {"Monthly": -1, "Yearly": -1} "counters": {
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -112,7 +124,10 @@
"monthly snapshot", "monthly snapshot",
"yearly snapshot" "yearly snapshot"
], ],
"counters": {"Monthly": -1, "Yearly": -1} "counters": {
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -123,7 +138,10 @@
"matches": [ "matches": [
"monthly snapshot" "monthly snapshot"
], ],
"counters": {"Monthly": -1, "Yearly": -1} "counters": {
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -134,7 +152,10 @@
"matches": [ "matches": [
"monthly snapshot" "monthly snapshot"
], ],
"counters": {"Monthly": -1, "Yearly": -1} "counters": {
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -145,7 +166,10 @@
"matches": [ "matches": [
"monthly snapshot" "monthly snapshot"
], ],
"counters": {"Monthly": -1, "Yearly": -1} "counters": {
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -157,7 +181,10 @@
"monthly snapshot", "monthly snapshot",
"yearly snapshot" "yearly snapshot"
], ],
"counters": {"Monthly": -1, "Yearly": -1} "counters": {
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -171,7 +198,10 @@
"matches": [ "matches": [
"monthly snapshot" "monthly snapshot"
], ],
"counters": {"Monthly": -1, "Yearly": -1} "counters": {
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -182,7 +212,10 @@
"matches": [ "matches": [
"monthly snapshot" "monthly snapshot"
], ],
"counters": {"Monthly": -1, "Yearly": -1} "counters": {
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -193,7 +226,10 @@
"matches": [ "matches": [
"monthly snapshot" "monthly snapshot"
], ],
"counters": {"Monthly": -1, "Yearly": -1} "counters": {
"monthly": -1,
"yearly": -1
}
}, },
{ {
"snapshot": { "snapshot": {
@ -205,7 +241,10 @@
"monthly snapshot", "monthly snapshot",
"yearly snapshot" "yearly snapshot"
], ],
"counters": {"Monthly": -1, "Yearly": -1} "counters": {
"monthly": -1,
"yearly": -1
}
} }
] ]
} }