Skip to content

Commit

Permalink
Merge pull request juju#1592 from perrito666/restore_06_restore_subco…
Browse files Browse the repository at this point in the history
…mmand_and_api

Restore 05 and 06 restore subcommand and api

This PR introduces the Client side API calls for restore and the command line sub-command.

(Review request: http://reviews.vapour.ws/r/922/)
  • Loading branch information
jujubot committed Feb 23, 2015
2 parents 6dd6152 + ae7fed4 commit 7e456d1
Show file tree
Hide file tree
Showing 13 changed files with 536 additions and 73 deletions.
172 changes: 172 additions & 0 deletions api/backups/restore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package backups

import (
"io"
"time"

"github.com/juju/errors"
"github.com/juju/utils"

"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/rpc"
)

var (
// restoreStrategy is the attempt strategy for api server calls re-attempts in case
// the server is upgrading.
restoreStrategy = utils.AttemptStrategy{
Delay: 10 * time.Second,
Min: 10,
}
)

// ClientConnection type represents a function capable of spawning a new Client connection
// it is used to pass around connection factories when necessary.
// TODO(perrito666) This is a workaround for lp:1399722 .
type ClientConnection func() (*Client, func() error, error)

// closerfunc is a function that allows you to close a client connection.
type closerFunc func() error

func prepareAttempt(client *Client, closer closerFunc) (error, error) {
var remoteError error
defer closer()
err := client.facade.FacadeCall("PrepareRestore", nil, &remoteError)
return err, remoteError
}

func prepareRestore(newClient ClientConnection) error {
var err, remoteError error

// PrepareRestore puts the server into a state that only allows
// for restore to be called. This is to avoid the data loss if
// users try to perform actions that are going to be overwritten
// by restore.
for a := restoreStrategy.Start(); a.Next(); {
logger.Debugf("Will attempt to call 'PrepareRestore'")
client, clientCloser, clientErr := newClient()
if clientErr != nil {
return errors.Trace(clientErr)
}
if err, remoteError = prepareAttempt(client, clientCloser); err == nil {
return nil
}
if !params.IsCodeUpgradeInProgress(remoteError) {
return errors.Annotatef(err, "could not start prepare restore mode, server returned: %v", remoteError)
}
}
return errors.Annotatef(err, "could not start restore process: %v", remoteError)
}

// RestoreReader restores the contents of backupFile as backup.
func (c *Client) RestoreReader(r io.Reader, meta *params.BackupsMetadataResult, newClient ClientConnection) error {
if err := prepareRestore(newClient); err != nil {
return errors.Trace(err)
}
logger.Debugf("Server is now in 'about to restore' mode, proceeding to upload the backup file")

// Upload.
backupId, err := c.Upload(r, *meta)
if err != nil {
finishErr := finishRestore(newClient)
logger.Errorf("could not exit restoring status: %v", finishErr)
return errors.Annotatef(err, "cannot upload backup file")
}
return c.restore(backupId, newClient)
}

// Restore performs restore using a backup id corresponding to a backup stored in the server.
func (c *Client) Restore(backupId string, newClient ClientConnection) error {
if err := prepareRestore(newClient); err != nil {
return errors.Trace(err)
}
logger.Debugf("Server in 'about to restore' mode")
return c.restore(backupId, newClient)
}

func restoreAttempt(client *Client, closer closerFunc, restoreArgs params.RestoreArgs) (error, error) {
var remoteError error
defer closer()
err := client.facade.FacadeCall("Restore", restoreArgs, &remoteError)
return err, remoteError
}

// restore is responsible for triggering the whole restore process in a remote
// machine. The backup information for the process should already be in the
// server and loaded in the backup storage under the backupId id.
// It takes backupId as the identifier for the remote backup file and a
// client connection factory newClient (newClient should no longer be
// necessary when lp:1399722 is sorted out).
func (c *Client) restore(backupId string, newClient ClientConnection) error {
var err, remoteError error

// Restore
restoreArgs := params.RestoreArgs{
BackupId: backupId,
}

for a := restoreStrategy.Start(); a.Next(); {
logger.Debugf("Attempting Restore of %q", backupId)
restoreClient, restoreClientCloser, err := newClient()
if err != nil {
return errors.Trace(err)
}

err, remoteError = restoreAttempt(restoreClient, restoreClientCloser, restoreArgs)

// This signals that Restore almost certainly finished and
// triggered Exit.
if err == rpc.ErrShutdown && remoteError == nil {
break
}
if err != nil && !params.IsCodeUpgradeInProgress(remoteError) {
finishErr := finishRestore(newClient)
logger.Errorf("could not exit restoring status: %v", finishErr)
return errors.Annotatef(err, "cannot perform restore: %v", remoteError)
}
}
if err != rpc.ErrShutdown {
finishErr := finishRestore(newClient)
logger.Errorf("could not exit restoring status: %v", finishErr)
return errors.Annotatef(err, "cannot perform restore: %v", remoteError)
}

err = finishRestore(newClient)
if err != nil {
return errors.Annotatef(err, "could not finish restore process: %v", remoteError)
}
return nil
}

func finishAttempt(client *Client, closer closerFunc) (error, error) {
var remoteError error
defer closer()
err := client.facade.FacadeCall("finishRestore", nil, &remoteError)
return err, remoteError
}

// finishRestore since Restore call will end up with a reset
// state server, finish restore will check that the the newly
// placed state server has the mark of restore complete.
// upstart should have restarted the api server so we reconnect.
func finishRestore(newClient ClientConnection) error {
var err, remoteError error
for a := restoreStrategy.Start(); a.Next(); {
logger.Debugf("Attempting finishRestore")
finishClient, finishClientCloser, err := newClient()
if err != nil {
return errors.Trace(err)
}

if err, remoteError = finishAttempt(finishClient, finishClientCloser); err == nil {
return nil
}
if !params.IsCodeUpgradeInProgress(remoteError) {
return errors.Annotatef(err, "cannot complete restore: %v", remoteError)
}
}
return errors.Annotatef(err, "cannot complete restore: %v", remoteError)
}
9 changes: 8 additions & 1 deletion apiserver/backups/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (a *API) Restore(p params.RestoreArgs) error {
defer closer.Close()

// Obtain the address of current machine, where we will be performing restore.
machine, err := a.st.Machine(p.Machine)
machine, err := a.st.Machine(a.machineID)
if err != nil {
return errors.Trace(err)
}
Expand All @@ -32,6 +32,11 @@ func (a *API) Restore(p params.RestoreArgs) error {
return errors.Errorf("machine %q has no internal address", machine)
}

publicAddress := network.SelectPublicAddress(machine.Addresses())
if publicAddress == "" {
return errors.Errorf("machine %q has no public address", machine)
}

info, err := a.st.RestoreInfoSetter()
if err != nil {
return errors.Trace(err)
Expand All @@ -54,6 +59,7 @@ func (a *API) Restore(p params.RestoreArgs) error {
// Restore
restoreArgs := backups.RestoreArgs{
PrivateAddress: addr,
PublicAddress: publicAddress,
NewInstId: instanceId,
NewInstTag: machine.Tag(),
NewInstSeries: machine.Series(),
Expand Down Expand Up @@ -87,6 +93,7 @@ func (a *API) FinishRestore() error {
}
currentStatus := info.Status()
if currentStatus != state.RestoreFinished {
info.SetStatus(state.RestoreFailed)
return errors.Errorf("Restore did not finish succesfuly")
}
logger.Infof("Succesfully restored")
Expand Down
5 changes: 1 addition & 4 deletions apiserver/params/backups.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,8 @@ type BackupsMetadataResult struct {
Version version.Number
}

// RestoreArgs Holds the backup file or id and the machine to
// be used for the restore process.
// RestoreArgs Holds the backup file or id
type RestoreArgs struct {
// BackupId holds the id of the backup in server if any
BackupId string
// Machine holds the machine where the backup is going to be restored
Machine string
}
72 changes: 72 additions & 0 deletions cmd/juju/backups/backups.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ package backups
import (
"fmt"
"io"
"os"

"github.com/juju/cmd"
"github.com/juju/errors"

"github.com/juju/juju/api/backups"
apiserverbackups "github.com/juju/juju/apiserver/backups"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/cmd/envcmd"
statebackups "github.com/juju/juju/state/backups"
)

var backupsDoc = `
Expand Down Expand Up @@ -44,6 +47,7 @@ func NewCommand() cmd.Command {
backupsCmd.Register(envcmd.Wrap(&DownloadCommand{}))
backupsCmd.Register(envcmd.Wrap(&UploadCommand{}))
backupsCmd.Register(envcmd.Wrap(&RemoveCommand{}))
backupsCmd.Register(envcmd.Wrap(&RestoreCommand{}))
return &backupsCmd
}

Expand All @@ -63,6 +67,10 @@ type APIClient interface {
Upload(ar io.Reader, meta params.BackupsMetadataResult) (string, error)
// Remove removes the stored backup.
Remove(id string) error
// Restore will restore a backup with the given id into the state server.
Restore(string, backups.ClientConnection) error
// Restore will restore a backup file into the state server.
RestoreReader(io.Reader, *params.BackupsMetadataResult, backups.ClientConnection) error
}

// CommandBase is the base type for backups sub-commands.
Expand Down Expand Up @@ -100,3 +108,67 @@ func (c *CommandBase) dumpMetadata(ctx *cmd.Context, result *params.BackupsMetad
fmt.Fprintf(ctx.Stdout, "created on host: %q\n", result.Hostname)
fmt.Fprintf(ctx.Stdout, "juju version: %v\n", result.Version)
}

func getArchive(filename string) (rc io.ReadCloser, metaResult *params.BackupsMetadataResult, err error) {
defer func() {
if err != nil && rc != nil {
rc.Close()
}
}()
archive, err := os.Open(filename)
rc = archive
if err != nil {
return nil, nil, errors.Trace(err)
}

// Extract the metadata.
ad, err := statebackups.NewArchiveDataReader(archive)
if err != nil {
return nil, nil, errors.Trace(err)
}
_, err = archive.Seek(0, os.SEEK_SET)
if err != nil {
return nil, nil, errors.Trace(err)
}
meta, err := ad.Metadata()
if err != nil {
if !errors.IsNotFound(err) {
return nil, nil, errors.Trace(err)
}
meta, err = statebackups.BuildMetadata(archive)
if err != nil {
return nil, nil, errors.Trace(err)
}
}
// Make sure the file info is set.
fileMeta, err := statebackups.BuildMetadata(archive)
if err != nil {
return nil, nil, errors.Trace(err)
}
if meta.Size() == int64(0) {
if err := meta.SetFileInfo(fileMeta.Size(), "", ""); err != nil {
return nil, nil, errors.Trace(err)
}
}
if meta.Checksum() == "" {
err := meta.SetFileInfo(0, fileMeta.Checksum(), fileMeta.ChecksumFormat())
if err != nil {
return nil, nil, errors.Trace(err)
}
}
if meta.Finished == nil || meta.Finished.IsZero() {
meta.Finished = fileMeta.Finished
}
_, err = archive.Seek(0, os.SEEK_SET)
if err != nil {
return nil, nil, errors.Trace(err)
}

// Pack the metadata into a result.
// TODO(perrito666) change the identity of ResultfromMetadata to
// return a pointer.
mResult := apiserverbackups.ResultFromMetadata(meta)
metaResult = &mResult

return archive, metaResult, nil
}
1 change: 1 addition & 0 deletions cmd/juju/backups/backups_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var expectedSubCommmandNames = []string{
"info",
"list",
"remove",
"restore",
"upload",
}

Expand Down
9 changes: 9 additions & 0 deletions cmd/juju/backups/package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"

apibackups "github.com/juju/juju/api/backups"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/cmd/juju/backups"
jujutesting "github.com/juju/juju/testing"
Expand Down Expand Up @@ -228,3 +229,11 @@ func (c *fakeAPIClient) Remove(id string) error {
func (c *fakeAPIClient) Close() error {
return nil
}

func (c *fakeAPIClient) RestoreReader(io.Reader, *params.BackupsMetadataResult, apibackups.ClientConnection) error {
return nil
}

func (c *fakeAPIClient) Restore(string, apibackups.ClientConnection) error {
return nil
}
Loading

0 comments on commit 7e456d1

Please sign in to comment.