github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/backups/restore.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package backups
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/gnuflag"
    15  	"gopkg.in/juju/names.v2"
    16  
    17  	"github.com/juju/juju/api/backups"
    18  	"github.com/juju/juju/api/base"
    19  	"github.com/juju/juju/apiserver/params"
    20  	jujucmd "github.com/juju/juju/cmd"
    21  	"github.com/juju/juju/cmd/juju/controller"
    22  	"github.com/juju/juju/cmd/modelcmd"
    23  	"github.com/juju/juju/environs/bootstrap"
    24  )
    25  
    26  // NewRestoreCommand returns a command used to restore a backup.
    27  func NewRestoreCommand() cmd.Command {
    28  	c := &restoreCommand{}
    29  	c.getModelStatusAPI = func() (ModelStatusAPI, error) { return c.NewModelManagerAPIClient() }
    30  	return modelcmd.Wrap(c)
    31  }
    32  
    33  // restoreCommand is a subcommand of backups that implement the restore behavior
    34  // it is invoked with "juju restore-backup".
    35  type restoreCommand struct {
    36  	CommandBase
    37  	getModelStatusAPI func() (ModelStatusAPI, error)
    38  
    39  	Filename string
    40  	BackupId string
    41  }
    42  
    43  // RestoreAPI is used to invoke various API calls.
    44  type RestoreAPI interface {
    45  	// Close is taken from io.Closer.
    46  	Close() error
    47  
    48  	// Restore is taken from backups.Client.
    49  	Restore(backupId string, newClient backups.ClientConnection) error
    50  
    51  	// RestoreReader is taken from backups.Client.
    52  	RestoreReader(r io.ReadSeeker, meta *params.BackupsMetadataResult, newClient backups.ClientConnection) error
    53  }
    54  
    55  // ModelStatusAPI is used to invoke common.ModelStatus
    56  // The interface is used to facilitate testing.
    57  //
    58  //go:generate mockgen -package backups_test -destination modelstatusapi_mock_test.go github.com/juju/juju/cmd/juju/backups ModelStatusAPI
    59  type ModelStatusAPI interface {
    60  	Close() error
    61  	ModelStatus(tags ...names.ModelTag) ([]base.ModelStatus, error)
    62  }
    63  
    64  var restoreDoc = `
    65  Restores the Juju state database backup that was previously created with
    66  "juju create-backup", returning an existing controller to a previous state.
    67  
    68  Note: Only the database will be restored.  Juju will not change the existing
    69  environment to match the restored database, e.g. no units, relations, nor
    70  machines will be added or removed during the restore process.
    71  
    72  Note: Extra care is needed to restore in an HA environment, please see
    73  https://docs.jujucharms.com/stable/controllers-backup for more information.
    74  
    75  If the provided state cannot be restored, this command will fail with
    76  an explanation.
    77  `
    78  
    79  // Info returns the content for --help.
    80  func (c *restoreCommand) Info() *cmd.Info {
    81  	return jujucmd.Info(&cmd.Info{
    82  		Name:    "restore-backup",
    83  		Purpose: "Restore from a backup archive to the existing controller.",
    84  		Args:    "",
    85  		Doc:     strings.TrimSpace(restoreDoc),
    86  	})
    87  }
    88  
    89  // SetFlags handles known option flags.
    90  func (c *restoreCommand) SetFlags(f *gnuflag.FlagSet) {
    91  	c.CommandBase.SetFlags(f)
    92  	f.StringVar(&c.Filename, "file", "", "Provide a file to be used as the backup")
    93  	f.StringVar(&c.BackupId, "id", "", "Provide the name of the backup to be restored")
    94  }
    95  
    96  // Init is where the preconditions for this command can be checked.
    97  func (c *restoreCommand) Init(args []string) error {
    98  	if c.Filename == "" && c.BackupId == "" {
    99  		return errors.Errorf("you must specify either a file or a backup id.")
   100  	}
   101  	if c.Filename != "" && c.BackupId != "" {
   102  		return errors.Errorf("you must specify either a file or a backup id but not both.")
   103  	}
   104  
   105  	if c.Filename != "" {
   106  		var err error
   107  		c.Filename, err = filepath.Abs(c.Filename)
   108  		if err != nil {
   109  			return errors.Trace(err)
   110  		}
   111  	}
   112  
   113  	return nil
   114  }
   115  
   116  func (c *restoreCommand) modelStatus() (string, []base.ModelStatus, error) {
   117  	modelUUIDs, err := c.ModelUUIDs([]string{bootstrap.ControllerModelName})
   118  	if err != nil {
   119  		return "", nil, errors.Annotatef(err, "cannot get controller model uuid")
   120  	}
   121  	if len(modelUUIDs) != 1 {
   122  		return "", nil, errors.New("cannot get controller model uuid")
   123  	}
   124  	controllerModelUUID := modelUUIDs[0]
   125  
   126  	modelClient, err := c.getModelStatusAPI()
   127  	if err != nil {
   128  		return "", nil, errors.Annotatef(err, "cannot get model status client")
   129  	}
   130  	defer modelClient.Close()
   131  
   132  	modelStatus, err := modelClient.ModelStatus(names.NewModelTag(controllerModelUUID))
   133  	if err != nil {
   134  		return "", nil, errors.Annotatef(err, "cannot refresh controller model")
   135  	}
   136  	if len(modelStatus) != 1 {
   137  		return "", nil, errors.New("could not find controller model status")
   138  	}
   139  
   140  	return controllerModelUUID, modelStatus, nil
   141  }
   142  
   143  func (c *restoreCommand) newClient() (*backups.Client, error) {
   144  	client, err := c.NewAPIClient()
   145  	if err != nil {
   146  		return nil, errors.Trace(err)
   147  	}
   148  	backupsClient, ok := client.(*backups.Client)
   149  	if !ok {
   150  		return nil, errors.Errorf("invalid client for backups")
   151  	}
   152  	return backupsClient, nil
   153  }
   154  
   155  // Run is the entry point for this command.
   156  func (c *restoreCommand) Run(ctx *cmd.Context) error {
   157  	if c.Log != nil {
   158  		if err := c.Log.Start(ctx); err != nil {
   159  			return err
   160  		}
   161  	}
   162  
   163  	// Don't allow restore in an HA environment
   164  	controllerModelUUID, modelStatus, err := c.modelStatus()
   165  	if err != nil {
   166  		return errors.Trace(err)
   167  	}
   168  	activeCount, _ := controller.ControllerMachineCounts(controllerModelUUID, modelStatus)
   169  	if activeCount > 1 {
   170  		return errors.Errorf("unable to restore backup in HA configuration.  For help see https://docs.jujucharms.com/stable/controllers-backup")
   171  	}
   172  
   173  	var archive ArchiveReader
   174  	var meta *params.BackupsMetadataResult
   175  	target := c.BackupId
   176  	if c.Filename != "" {
   177  		// Read archive specified by the Filename
   178  		target = c.Filename
   179  		var err error
   180  		archive, meta, err = getArchive(c.Filename)
   181  		if err != nil {
   182  			return errors.Trace(err)
   183  		}
   184  		defer archive.Close()
   185  	}
   186  
   187  	client, err := c.NewAPIClient()
   188  	if err != nil {
   189  		return errors.Trace(err)
   190  	}
   191  	defer client.Close()
   192  
   193  	// We have a backup client, now use the relevant method
   194  	// to restore the backup.
   195  	if c.Filename != "" {
   196  		err = client.RestoreReader(archive, meta, c.newClient)
   197  	} else {
   198  		err = client.Restore(c.BackupId, c.newClient)
   199  	}
   200  	if err != nil {
   201  		return errors.Trace(err)
   202  	}
   203  	fmt.Fprintf(ctx.Stdout, "restore from %q completed\n", target)
   204  	return nil
   205  }