Skip to content
This repository has been archived by the owner on Dec 12, 2023. It is now read-only.

Backup #81

Merged
merged 23 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Eigenlayer is a setup wizard for EigenLayer Node Software. The tool installs, ma
- [Updating with explicit version](#updating-with-explicit-version)
- [Updating with commit hash](#updating-with-commit-hash)
- [Updating options](#updating-options)
- [Backup](#backup)
- [List backups](#list-backups)
- [Uninstalling AVS Node Software](#uninstalling-avs-node-software)
- [List installed instances](#list-installed-instances)
- [Run an AVS instance](#run-an-avs-instance)
Expand Down Expand Up @@ -263,6 +265,37 @@ eigenlayer update mock-avs-default a3406616b848164358fdd24465b8eecda5f5ae34

The `--no-prompt` flag is available to skip the options prompt, also the dynamic flags `--option.<option-name>` are available to set the option values, like in the `install` command.

## Backup

To backup an installed AVS Node Software, use the `eigenlayer backup` command with the AVS instance ID as an argument, as follows:

```bash
eigenlayer backup mock-avs-default
```

Output:

```bash
INFO[0000] Backing up instance mock-avs-default
INFO[0000] Backing up instance data...
INFO[0000] Backup created with id: mock-avs-default-1696337650
```

## List backups

To list all the backups, use the `eigenlayer backup ls` command, as follows:

```bash
eigenlayer backup ls
```

Output:

```bash
AVS Instance ID TIMESTAMP SIZE (GB)
mock-avs-default 2023-10-01 08:00:00 0.000009
```

## Uninstalling AVS Node Software

When uninstalling AVS Node Software, it is stopped, disconnected from the Monitoring Stack, and removed from the data directory. To uninstall AVS Node Software, use the `eigenlayer uninstall` command with the AVS instance ID as an argument, as follows:
Expand Down
34 changes: 34 additions & 0 deletions cli/backup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cli

import (
"github.com/NethermindEth/eigenlayer/pkg/daemon"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func BackupCmd(d daemon.Daemon) *cobra.Command {
AntiD2ta marked this conversation as resolved.
Show resolved Hide resolved
var instanceId string
cmd := cobra.Command{
Use: "backup <instance-id>",
Short: "Backup an instance",
Long: "Backup an instance saving the data into a tarball file. To list backups, use 'eigenlayer backup ls'.",
Args: cobra.MinimumNArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
instanceId = args[0]
},
RunE: func(cmd *cobra.Command, args []string) error {
backupId, err := d.Backup(instanceId)
if err != nil {
return err
}
log.Info("Backup created with id: ", backupId)
return nil
},
}

// Add ls subcommand
lsCmd := BackupLsCmd(d)
cmd.AddCommand(lsCmd)

return &cmd
}
52 changes: 52 additions & 0 deletions cli/backup_ls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cli

import (
"fmt"
"io"
"text/tabwriter"
"time"

"github.com/NethermindEth/eigenlayer/pkg/daemon"
"github.com/spf13/cobra"
"kythe.io/kythe/go/util/datasize"
)

func BackupLsCmd(d daemon.Daemon) *cobra.Command {
cmd := cobra.Command{
Use: "ls",
Short: "List backups",
AntiD2ta marked this conversation as resolved.
Show resolved Hide resolved
Long: "List backups showing all backups and their details.",
RunE: func(cmd *cobra.Command, args []string) error {
backups, err := d.BackupList()
if err != nil {
return err
}
printBackupTable(backups, cmd.OutOrStdout())
return nil
},
}
return &cmd
}

func printBackupTable(backups []daemon.BackupInfo, out io.Writer) {
w := tabwriter.NewWriter(out, 0, 0, 4, ' ', 0)
fmt.Fprintln(w, "AVS Instance ID\tTIMESTAMP\tSIZE\t")
for _, b := range backups {
fmt.Fprintln(w, backupTableItem{
instance: b.Instance,
timestamp: b.Timestamp.Format(time.DateTime),
size: datasize.Size(b.SizeBytes).String(),
})
}
w.Flush()
}

type backupTableItem struct {
instance string
timestamp string
size string
}

func (b backupTableItem) String() string {
return fmt.Sprintf("%s\t%s\t%s\t", b.instance, b.timestamp, b.size)
}
1 change: 1 addition & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func RootCmd(d daemon.Daemon, p prompter.Prompter) *cobra.Command {
LogsCmd(d),
InitMonitoringCmd(d),
CleanMonitoringCmd(d),
BackupCmd(d),
UpdateCmd(d, p),
)
cmd.CompletionOptions.DisableDefaultCmd = true
Expand Down
11 changes: 10 additions & 1 deletion cli/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func UpdateCmd(d daemon.Daemon, p prompter.Prompter) *cobra.Command {
version string
commit string
noPrompt bool
backup bool
help bool
yes bool
)
Expand Down Expand Up @@ -158,7 +159,14 @@ Options of the new version can be specified using the --option.<option-name> fla
}
}

// TODO: backup current instance
// Backup instance
if backup {
backupId, err := d.Backup(instanceId)
if err != nil {
return err
}
log.Info("Backup created with id: ", backupId)
}

// Uninstall current instance
err = uninstallPackage(d, instanceId)
Expand Down Expand Up @@ -197,6 +205,7 @@ Options of the new version can be specified using the --option.<option-name> fla

cmd.Flags().BoolVar(&noPrompt, "no-prompt", false, "disable command prompts, and all options should be passed using command flags.")
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "skip confirmation prompts.")
cmd.Flags().BoolVar(&backup, "backup", false, "backup current instance before updating.")
return &cmd
}

Expand Down
99 changes: 99 additions & 0 deletions cli/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"fmt"
"testing"
"time"

daemonMock "github.com/NethermindEth/eigenlayer/cli/mocks"
prompterMock "github.com/NethermindEth/eigenlayer/cli/prompter/mocks"
Expand Down Expand Up @@ -183,6 +184,104 @@ func TestUpdate(t *testing.T) {
)
},
},
{
name: "update with backup",
args: []string{instanceId, "--backup"},
err: nil,
mocker: func(ctrl *gomock.Controller, d *daemonMock.MockDaemon, p *prompterMock.MockPrompter) {
oldOption := daemonMock.NewMockOption(ctrl)
newOption := daemonMock.NewMockOption(ctrl)
mergedOption := daemonMock.NewMockOption(ctrl)

oldOption.EXPECT().IsSet().Return(true)
oldOption.EXPECT().Value().Return("old-value", nil)
oldOption.EXPECT().Name().Return("old-option").Times(3)
mergedOption.EXPECT().IsSet().Return(true).Times(2)
mergedOption.EXPECT().Value().Return("old-value", nil)
mergedOption.EXPECT().Name().Return("old-option").Times(2)
mergedOption.EXPECT().Default().Return("default-value")
mergedOption.EXPECT().Help().Return("option help")

gomock.InOrder(
d.EXPECT().PullUpdate(instanceId, daemon.PullTarget{}).Return(daemon.PullUpdateResult{
Name: "mock-avs",
Tag: "default",
Url: common.MockAvsPkg.Repo(),
Profile: "option-returner",
OldVersion: "v5.4.0",
NewVersion: common.MockAvsPkg.Version(),
OldCommit: "b64c50c15e53ae7afebbdbe210b834d1ee471043",
NewCommit: common.MockAvsPkg.CommitHash(),
HasPlugin: true,
OldOptions: []daemon.Option{oldOption},
NewOptions: []daemon.Option{newOption},
MergedOptions: []daemon.Option{mergedOption},
HardwareRequirements: daemon.HardwareRequirements{
MinCPUCores: 2,
MinRAM: 2048,
MinFreeSpace: 5120,
StopIfRequirementsAreNotMet: true,
},
}, nil),
d.EXPECT().Backup(instanceId).Return(fmt.Sprintf("%s-%d", instanceId, time.Now().Unix()), nil),
d.EXPECT().Uninstall(instanceId).Return(nil),
d.EXPECT().Install(daemon.InstallOptions{
Name: "mock-avs",
Tag: "default",
URL: common.MockAvsPkg.Repo(),
Profile: "option-returner",
Version: common.MockAvsPkg.Version(),
Commit: common.MockAvsPkg.CommitHash(),
Options: []daemon.Option{mergedOption},
}).Return(instanceId, nil),
p.EXPECT().Confirm("Run the new instance now?").Return(true, nil),
d.EXPECT().Run(instanceId),
)
},
},
{
name: "update backup error",
args: []string{instanceId, "--backup"},
err: assert.AnError,
mocker: func(ctrl *gomock.Controller, d *daemonMock.MockDaemon, p *prompterMock.MockPrompter) {
oldOption := daemonMock.NewMockOption(ctrl)
newOption := daemonMock.NewMockOption(ctrl)
mergedOption := daemonMock.NewMockOption(ctrl)

oldOption.EXPECT().IsSet().Return(true)
oldOption.EXPECT().Value().Return("old-value", nil)
oldOption.EXPECT().Name().Return("old-option").Times(3)
mergedOption.EXPECT().IsSet().Return(true).Times(2)
mergedOption.EXPECT().Value().Return("old-value", nil)
mergedOption.EXPECT().Name().Return("old-option").Times(2)
mergedOption.EXPECT().Default().Return("default-value")
mergedOption.EXPECT().Help().Return("option help")

gomock.InOrder(
d.EXPECT().PullUpdate(instanceId, daemon.PullTarget{}).Return(daemon.PullUpdateResult{
Name: "mock-avs",
Tag: "default",
Url: common.MockAvsPkg.Repo(),
Profile: "option-returner",
OldVersion: "v5.4.0",
NewVersion: common.MockAvsPkg.Version(),
OldCommit: "b64c50c15e53ae7afebbdbe210b834d1ee471043",
NewCommit: common.MockAvsPkg.CommitHash(),
HasPlugin: true,
OldOptions: []daemon.Option{oldOption},
NewOptions: []daemon.Option{newOption},
MergedOptions: []daemon.Option{mergedOption},
HardwareRequirements: daemon.HardwareRequirements{
MinCPUCores: 2,
MinRAM: 2048,
MinFreeSpace: 5120,
StopIfRequirementsAreNotMet: true,
},
}, nil),
d.EXPECT().Backup(instanceId).Return("", assert.AnError),
)
},
},
{
name: "invalid arguments, instance id is required",
args: []string{},
Expand Down
6 changes: 5 additions & 1 deletion cmd/eigenlayer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/NethermindEth/eigenlayer/cli"
"github.com/NethermindEth/eigenlayer/cli/prompter"
"github.com/NethermindEth/eigenlayer/internal/backup"
"github.com/NethermindEth/eigenlayer/internal/commands"
"github.com/NethermindEth/eigenlayer/internal/compose"
"github.com/NethermindEth/eigenlayer/internal/data"
Expand Down Expand Up @@ -63,8 +64,11 @@ func main() {
log.Fatal(err)
}

// Backup manager
backupMgr := backup.NewBackupManager(fs, dataDir, dockerManager)

// Initialize daemon
daemon, err := daemon.NewEgnDaemon(dataDir, composeManager, dockerManager, monitoringManager, locker)
daemon, err := daemon.NewEgnDaemon(dataDir, composeManager, dockerManager, monitoringManager, backupMgr, locker)
if err != nil {
log.Fatal(err)
}
Expand Down
83 changes: 83 additions & 0 deletions e2e/backup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package e2e

import (
"regexp"
"testing"
"time"

"github.com/NethermindEth/eigenlayer/internal/common"
"github.com/stretchr/testify/assert"
)

func TestBackupInstance(t *testing.T) {
// Test context
var (
backupErr error
start time.Time
)
// Build test case
e2eTest := newE2ETestCase(
t,
// Arrange
func(t *testing.T, egnPath string) error {
start = time.Now()
err := buildMockAvsImagesLatest(t)
if err != nil {
return err
}
// Install latest version
return runCommand(t, egnPath, "install", "--profile", "option-returner", "--no-prompt", "--yes", "--version", common.MockAvsPkg.Version(), "--option.test-option-hidden", "12345678", "--option.test-option-enum-hidden", "option3", common.MockAvsPkg.Repo())
},
// Act
func(t *testing.T, egnPath string) {
backupErr = runCommand(t, egnPath, "backup", "mock-avs-default")
},
// Assert
func(t *testing.T) {
assert.NoError(t, backupErr, "backup command should succeed")
checkBackupExist(t, "mock-avs-default", start, time.Now())
},
)
// Run test case
e2eTest.run()
}

func TestBackupList(t *testing.T) {
// Test context
var (
out []byte
backupErr error
)
// Build test case
e2eTest := newE2ETestCase(
t,
// Arrange
func(t *testing.T, egnPath string) error {
err := buildMockAvsImagesLatest(t)
if err != nil {
return err
}
// Install latest version
err = runCommand(t, egnPath, "install", "--profile", "option-returner", "--no-prompt", "--yes", "--version", common.MockAvsPkg.Version(), "--option.test-option-hidden", "12345678", "--option.test-option-enum-hidden", "option3", common.MockAvsPkg.Repo())
if err != nil {
return err
}
return runCommand(t, egnPath, "backup", "mock-avs-default")
},
// Act
func(t *testing.T, egnPath string) {
out, backupErr = runCommandOutput(t, egnPath, "backup", "ls")
},
// Assert
func(t *testing.T) {
t.Log(string(out))
assert.NoError(t, backupErr, "backup ls command should succeed")
assert.Regexp(t, regexp.MustCompile(
`AVS Instance ID TIMESTAMP SIZE
mock-avs-default .* 9KiB`),
string(out))
},
)
// Run test case
e2eTest.run()
}
Loading