github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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  	"crypto/rand"
     8  	"fmt"
     9  	"io"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/juju/cmd"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/gnuflag"
    17  	"github.com/juju/utils"
    18  
    19  	"github.com/juju/juju/api/backups"
    20  	"github.com/juju/juju/apiserver/params"
    21  	"github.com/juju/juju/cloud"
    22  	"github.com/juju/juju/cmd/juju/common"
    23  	"github.com/juju/juju/cmd/modelcmd"
    24  	"github.com/juju/juju/constraints"
    25  	"github.com/juju/juju/controller"
    26  	"github.com/juju/juju/environs"
    27  	"github.com/juju/juju/environs/bootstrap"
    28  	"github.com/juju/juju/environs/config"
    29  	"github.com/juju/juju/environs/sync"
    30  	"github.com/juju/juju/jujuclient"
    31  	"github.com/juju/juju/version"
    32  )
    33  
    34  // NewRestoreCommand returns a command used to restore a backup.
    35  func NewRestoreCommand() cmd.Command {
    36  	restoreCmd := &restoreCommand{}
    37  	restoreCmd.newEnvironFunc = environs.New
    38  	restoreCmd.getRebootstrapParamsFunc = restoreCmd.getRebootstrapParams
    39  	restoreCmd.newAPIClientFunc = func() (RestoreAPI, error) {
    40  		return restoreCmd.newClient()
    41  	}
    42  	restoreCmd.getArchiveFunc = getArchive
    43  	restoreCmd.waitForAgentFunc = common.WaitForAgentInitialisation
    44  	return modelcmd.Wrap(restoreCmd)
    45  }
    46  
    47  // restoreCommand is a subcommand of backups that implement the restore behavior
    48  // it is invoked with "juju restore-backup".
    49  type restoreCommand struct {
    50  	CommandBase
    51  	constraints    constraints.Value
    52  	constraintsStr string
    53  	filename       string
    54  	backupId       string
    55  	bootstrap      bool
    56  	buildAgent     bool
    57  
    58  	newAPIClientFunc         func() (RestoreAPI, error)
    59  	newEnvironFunc           func(environs.OpenParams) (environs.Environ, error)
    60  	getRebootstrapParamsFunc func(*cmd.Context, string, *params.BackupsMetadataResult) (*restoreBootstrapParams, error)
    61  	getArchiveFunc           func(string) (ArchiveReader, *params.BackupsMetadataResult, error)
    62  	waitForAgentFunc         func(ctx *cmd.Context, c *modelcmd.ModelCommandBase, controllerName, hostedModelName string) error
    63  }
    64  
    65  // RestoreAPI is used to invoke various API calls.
    66  type RestoreAPI interface {
    67  	// Close is taken from io.Closer.
    68  	Close() error
    69  
    70  	// Restore is taken from backups.Client.
    71  	Restore(backupId string, newClient backups.ClientConnection) error
    72  
    73  	// RestoreReader is taken from backups.Client.
    74  	RestoreReader(r io.ReadSeeker, meta *params.BackupsMetadataResult, newClient backups.ClientConnection) error
    75  }
    76  
    77  var restoreDoc = `
    78  Restores a backup that was previously created with "juju create-backup".
    79  
    80  This command creates a new controller and arranges for it to replace
    81  the previous controller for a model.  It does *not* restore
    82  an existing server to a previous state, but instead creates a new server
    83  with equivalent state.  As part of restore, all known instances are
    84  configured to treat the new controller as their master.
    85  
    86  The given constraints will be used to choose the new instance.
    87  
    88  If the provided state cannot be restored, this command will fail with
    89  an appropriate message.  For instance, if the existing bootstrap
    90  instance is already running then the command will fail with a message
    91  to that effect.
    92  `
    93  
    94  var BootstrapFunc = bootstrap.Bootstrap
    95  
    96  // Info returns the content for --help.
    97  func (c *restoreCommand) Info() *cmd.Info {
    98  	return &cmd.Info{
    99  		Name:    "restore-backup",
   100  		Purpose: "Restore from a backup archive to a new controller.",
   101  		Args:    "",
   102  		Doc:     strings.TrimSpace(restoreDoc),
   103  	}
   104  }
   105  
   106  // SetFlags handles known option flags.
   107  func (c *restoreCommand) SetFlags(f *gnuflag.FlagSet) {
   108  	c.CommandBase.SetFlags(f)
   109  	f.StringVar(&c.constraintsStr, "constraints", "", "set model constraints")
   110  	f.BoolVar(&c.bootstrap, "b", false, "Bootstrap a new state machine")
   111  	f.StringVar(&c.filename, "file", "", "Provide a file to be used as the backup.")
   112  	f.StringVar(&c.backupId, "id", "", "Provide the name of the backup to be restored")
   113  	f.BoolVar(&c.buildAgent, "build-agent", false, "Build binary agent if bootstraping a new machine")
   114  }
   115  
   116  // Init is where the preconditions for this commands can be checked.
   117  func (c *restoreCommand) Init(args []string) error {
   118  	if c.filename == "" && c.backupId == "" {
   119  		return errors.Errorf("you must specify either a file or a backup id.")
   120  	}
   121  	if c.filename != "" && c.backupId != "" {
   122  		return errors.Errorf("you must specify either a file or a backup id but not both.")
   123  	}
   124  	if c.backupId != "" && c.bootstrap {
   125  		return errors.Errorf("it is not possible to rebootstrap and restore from an id.")
   126  	}
   127  
   128  	var err error
   129  	if c.filename != "" {
   130  		c.filename, err = filepath.Abs(c.filename)
   131  		if err != nil {
   132  			return errors.Trace(err)
   133  		}
   134  	}
   135  	return nil
   136  }
   137  
   138  type restoreBootstrapParams struct {
   139  	ControllerConfig controller.Config
   140  	Cloud            environs.CloudSpec
   141  	CredentialName   string
   142  	AdminSecret      string
   143  	ModelConfig      *config.Config
   144  }
   145  
   146  // getRebootstrapParams returns the params for rebootstrapping the
   147  // specified controller.
   148  func (c *restoreCommand) getRebootstrapParams(
   149  	ctx *cmd.Context, controllerName string, meta *params.BackupsMetadataResult,
   150  ) (*restoreBootstrapParams, error) {
   151  	// TODO(axw) delete this and -b. We will update bootstrap with a flag
   152  	// to specify a restore file. When we do that, we'll need to extract
   153  	// the CA cert from the backup, and we'll need to reset the password
   154  	// after restore so the admin user can login. We also need to store
   155  	// things like the admin-secret, controller certificate etc with the
   156  	// backup.
   157  	store := c.ClientStore()
   158  	controllerDetails, err := store.ControllerByName(controllerName)
   159  	if err != nil {
   160  		return nil, errors.Trace(err)
   161  	}
   162  	config, params, err := modelcmd.NewGetBootstrapConfigParamsFunc(ctx, store)(controllerName)
   163  	if err != nil {
   164  		return nil, errors.Trace(err)
   165  	}
   166  	provider, err := environs.Provider(config.CloudType)
   167  	if err != nil {
   168  		return nil, errors.Trace(err)
   169  	}
   170  	cfg, err := provider.PrepareConfig(*params)
   171  	if err != nil {
   172  		return nil, errors.Trace(err)
   173  	}
   174  
   175  	// Get the local admin user so we can use the password as the admin secret.
   176  	// TODO(axw) check that account.User is environs.AdminUser.
   177  	var adminSecret string
   178  	account, err := store.AccountDetails(controllerName)
   179  	if err == nil {
   180  		adminSecret = account.Password
   181  	} else if errors.IsNotFound(err) {
   182  		// No relevant local admin user so generate a new secret.
   183  		buf := make([]byte, 16)
   184  		if _, err := io.ReadFull(rand.Reader, buf); err != nil {
   185  			return nil, errors.Annotate(err, "generating new admin secret")
   186  		}
   187  		adminSecret = fmt.Sprintf("%x", buf)
   188  	} else {
   189  		return nil, errors.Trace(err)
   190  	}
   191  
   192  	// Turn on safe mode so that the newly bootstrapped instance
   193  	// will not destroy all the instances it does not know about.
   194  	// Also set the admin secret and ca cert info.
   195  	cfg, err = cfg.Apply(map[string]interface{}{
   196  		"provisioner-safe-mode": true,
   197  	})
   198  	if err != nil {
   199  		return nil, errors.Annotatef(err, "cannot enable provisioner-safe-mode")
   200  	}
   201  
   202  	controllerCfg := make(controller.Config)
   203  	for k, v := range config.ControllerConfig {
   204  		controllerCfg[k] = v
   205  	}
   206  	controllerCfg[controller.ControllerUUIDKey] = controllerDetails.ControllerUUID
   207  	controllerCfg[controller.CACertKey] = meta.CACert
   208  
   209  	return &restoreBootstrapParams{
   210  		controllerCfg,
   211  		params.Cloud,
   212  		config.Credential,
   213  		adminSecret,
   214  		cfg,
   215  	}, nil
   216  }
   217  
   218  // rebootstrap will bootstrap a new server in safe-mode (not killing any other agent)
   219  // if there is no current server available to restore to.
   220  func (c *restoreCommand) rebootstrap(ctx *cmd.Context, meta *params.BackupsMetadataResult) error {
   221  	params, err := c.getRebootstrapParamsFunc(ctx, c.ControllerName(), meta)
   222  	if err != nil {
   223  		return errors.Trace(err)
   224  	}
   225  
   226  	cloudParam, err := cloud.CloudByName(params.Cloud.Name)
   227  	if errors.IsNotFound(err) {
   228  		provider, err := environs.Provider(params.Cloud.Type)
   229  		if errors.IsNotFound(err) {
   230  			return errors.NewNotFound(nil, fmt.Sprintf("unknown cloud %q, please try %q", params.Cloud.Name, "juju update-clouds"))
   231  		} else if err != nil {
   232  			return errors.Trace(err)
   233  		}
   234  		detector, ok := provider.(environs.CloudRegionDetector)
   235  		if !ok {
   236  			return errors.Errorf("provider %q does not support detecting regions", params.Cloud.Type)
   237  		}
   238  		var cloudEndpoint string
   239  		regions, err := detector.DetectRegions()
   240  		if errors.IsNotFound(err) {
   241  			// It's not an error to have no regions. If the
   242  			// provider does not support regions, then we
   243  			// reinterpret the supplied region name as the
   244  			// cloud's endpoint. This enables the user to
   245  			// supply, for example, maas/<IP> or manual/<IP>.
   246  			if params.Cloud.Region != "" {
   247  				cloudEndpoint = params.Cloud.Region
   248  			}
   249  		} else if err != nil {
   250  			return errors.Annotatef(err, "detecting regions for %q cloud provider", params.Cloud.Type)
   251  		}
   252  		schemas := provider.CredentialSchemas()
   253  		authTypes := make([]cloud.AuthType, 0, len(schemas))
   254  		for authType := range schemas {
   255  			authTypes = append(authTypes, authType)
   256  		}
   257  		cloudParam = &cloud.Cloud{
   258  			Type:      params.Cloud.Type,
   259  			AuthTypes: authTypes,
   260  			Endpoint:  cloudEndpoint,
   261  			Regions:   regions,
   262  		}
   263  	} else if err != nil {
   264  		return errors.Trace(err)
   265  	}
   266  
   267  	env, err := c.newEnvironFunc(environs.OpenParams{
   268  		Cloud:  params.Cloud,
   269  		Config: params.ModelConfig,
   270  	})
   271  	if err != nil {
   272  		return errors.Annotate(err, "opening environ for rebootstrapping")
   273  	}
   274  
   275  	instanceIds, err := env.ControllerInstances(params.ControllerConfig.ControllerUUID())
   276  	if err != nil && errors.Cause(err) != environs.ErrNotBootstrapped {
   277  		return errors.Annotatef(err, "cannot determine controller instances")
   278  	}
   279  	if len(instanceIds) > 0 {
   280  		inst, err := env.Instances(instanceIds)
   281  		if err == nil {
   282  			return errors.Errorf("old bootstrap instance %q still seems to exist; will not replace", inst)
   283  		}
   284  		if err != environs.ErrNoInstances {
   285  			return errors.Annotatef(err, "cannot detect whether old instance is still running")
   286  		}
   287  	}
   288  
   289  	// We require a hosted model config to bootstrap. We'll fill in some defaults
   290  	// just to get going. The restore will clear the initial state.
   291  	hostedModelUUID, err := utils.NewUUID()
   292  	if err != nil {
   293  		return errors.Trace(err)
   294  	}
   295  	hostedModelConfig := map[string]interface{}{
   296  		"name":         "default",
   297  		config.UUIDKey: hostedModelUUID.String(),
   298  	}
   299  
   300  	// We may have previous controller metadata. We need to replace that so it
   301  	// will contain the new CA Cert and UUID required to connect to the newly
   302  	// bootstrapped controller API.
   303  	store := c.ClientStore()
   304  	details := jujuclient.ControllerDetails{
   305  		ControllerUUID: params.ControllerConfig.ControllerUUID(),
   306  		CACert:         meta.CACert,
   307  		Cloud:          params.Cloud.Name,
   308  		CloudRegion:    params.Cloud.Region,
   309  	}
   310  	err = store.UpdateController(c.ControllerName(), details)
   311  	if err != nil {
   312  		return errors.Trace(err)
   313  	}
   314  
   315  	bootVers := version.Current
   316  	args := bootstrap.BootstrapParams{
   317  		Cloud:               *cloudParam,
   318  		CloudName:           params.Cloud.Name,
   319  		CloudRegion:         params.Cloud.Region,
   320  		CloudCredentialName: params.CredentialName,
   321  		CloudCredential:     params.Cloud.Credential,
   322  		ModelConstraints:    c.constraints,
   323  		BuildAgent:          c.buildAgent,
   324  		BuildAgentTarball:   sync.BuildAgentTarball,
   325  		ControllerConfig:    params.ControllerConfig,
   326  		HostedModelConfig:   hostedModelConfig,
   327  		BootstrapSeries:     meta.Series,
   328  		AgentVersion:        &bootVers,
   329  		AdminSecret:         params.AdminSecret,
   330  		CAPrivateKey:        meta.CAPrivateKey,
   331  		DialOpts: environs.BootstrapDialOpts{
   332  			Timeout:        time.Second * bootstrap.DefaultBootstrapSSHTimeout,
   333  			RetryDelay:     time.Second * bootstrap.DefaultBootstrapSSHRetryDelay,
   334  			AddressesDelay: time.Second * bootstrap.DefaultBootstrapSSHAddressesDelay,
   335  		},
   336  	}
   337  	if err := BootstrapFunc(modelcmd.BootstrapContext(ctx), env, args); err != nil {
   338  		return errors.Annotatef(err, "cannot bootstrap new instance")
   339  	}
   340  
   341  	// New controller is bootstrapped, so now record the API address so
   342  	// we can connect.
   343  	apiPort := params.ControllerConfig.APIPort()
   344  	err = common.SetBootstrapEndpointAddress(store, c.ControllerName(), bootVers, apiPort, env)
   345  	if err != nil {
   346  		return errors.Trace(err)
   347  	}
   348  
   349  	// To avoid race conditions when running scripted bootstraps, wait
   350  	// for the controller's machine agent to be ready to accept commands
   351  	// before exiting this bootstrap command.
   352  	return c.waitForAgentFunc(ctx, &c.ModelCommandBase, c.ControllerName(), "default")
   353  }
   354  
   355  func (c *restoreCommand) newClient() (*backups.Client, error) {
   356  	client, err := c.NewAPIClient()
   357  	if err != nil {
   358  		return nil, errors.Trace(err)
   359  	}
   360  	backupsClient, ok := client.(*backups.Client)
   361  	if !ok {
   362  		return nil, errors.Errorf("invalid client for backups")
   363  	}
   364  	return backupsClient, nil
   365  }
   366  
   367  // Run is the entry point for this command.
   368  func (c *restoreCommand) Run(ctx *cmd.Context) error {
   369  	var err error
   370  	c.constraints, err = common.ParseConstraints(ctx, c.constraintsStr)
   371  	if err != nil {
   372  		return err
   373  	}
   374  
   375  	if c.Log != nil {
   376  		if err := c.Log.Start(ctx); err != nil {
   377  			return err
   378  		}
   379  	}
   380  
   381  	var archive ArchiveReader
   382  	var meta *params.BackupsMetadataResult
   383  	target := c.backupId
   384  	if c.filename != "" {
   385  		// Read archive specified by the filename;
   386  		// we'll need the info later regardless if
   387  		// we need it now to rebootstrap.
   388  		target = c.filename
   389  		var err error
   390  		archive, meta, err = c.getArchiveFunc(c.filename)
   391  		if err != nil {
   392  			return errors.Trace(err)
   393  		}
   394  		defer archive.Close()
   395  
   396  		if c.bootstrap {
   397  			if err := c.rebootstrap(ctx, meta); err != nil {
   398  				return errors.Trace(err)
   399  			}
   400  		}
   401  	}
   402  
   403  	client, err := c.newAPIClientFunc()
   404  	if err != nil {
   405  		return errors.Trace(err)
   406  	}
   407  	defer client.Close()
   408  
   409  	// We have a backup client, now use the relevant method
   410  	// to restore the backup.
   411  	if c.filename != "" {
   412  		err = client.RestoreReader(archive, meta, c.newClient)
   413  	} else {
   414  		err = client.Restore(c.backupId, c.newClient)
   415  	}
   416  	if err != nil {
   417  		return errors.Trace(err)
   418  	}
   419  	fmt.Fprintf(ctx.Stdout, "restore from %q completed\n", target)
   420  	return nil
   421  }