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 }