launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/cmd/plugins/juju-restore/restore.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package main 5 6 import ( 7 "archive/tar" 8 "bytes" 9 "compress/gzip" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "os" 14 "os/exec" 15 "os/signal" 16 "path" 17 "text/template" 18 19 "github.com/loggo/loggo" 20 "launchpad.net/gnuflag" 21 "launchpad.net/goyaml" 22 23 "launchpad.net/juju-core/cmd" 24 "launchpad.net/juju-core/constraints" 25 "launchpad.net/juju-core/environs" 26 "launchpad.net/juju-core/environs/bootstrap" 27 "launchpad.net/juju-core/environs/config" 28 "launchpad.net/juju-core/environs/configstore" 29 "launchpad.net/juju-core/instance" 30 "launchpad.net/juju-core/juju" 31 _ "launchpad.net/juju-core/provider/all" 32 "launchpad.net/juju-core/state" 33 "launchpad.net/juju-core/state/api" 34 "launchpad.net/juju-core/utils" 35 ) 36 37 func main() { 38 Main(os.Args) 39 } 40 41 func Main(args []string) { 42 if err := juju.InitJujuHome(); err != nil { 43 fmt.Fprintf(os.Stderr, "error: %s\n", err) 44 os.Exit(2) 45 } 46 os.Exit(cmd.Main(&restoreCommand{}, cmd.DefaultContext(), args[1:])) 47 } 48 49 var logger = loggo.GetLogger("juju.plugins.restore") 50 51 const restoreDoc = ` 52 Restore restores a backup created with juju backup 53 by creating a new juju bootstrap instance and arranging 54 it so that the existing instances in the environment 55 talk to it. 56 57 It verifies that the existing bootstrap instance is 58 not running. The given constraints will be used 59 to choose the new instance. 60 ` 61 62 type restoreCommand struct { 63 cmd.EnvCommandBase 64 Log cmd.Log 65 Constraints constraints.Value 66 backupFile string 67 showDescription bool 68 } 69 70 func (c *restoreCommand) Info() *cmd.Info { 71 return &cmd.Info{ 72 Name: "juju-restore", 73 Purpose: "Restore a backup made with juju backup", 74 Args: "<backupfile.tar.gz>", 75 Doc: restoreDoc, 76 } 77 } 78 79 func (c *restoreCommand) SetFlags(f *gnuflag.FlagSet) { 80 c.EnvCommandBase.SetFlags(f) 81 f.Var(constraints.ConstraintsValue{&c.Constraints}, "constraints", "set environment constraints") 82 f.BoolVar(&c.showDescription, "description", false, "show the purpose of this plugin") 83 c.Log.AddFlags(f) 84 } 85 86 func (c *restoreCommand) Init(args []string) error { 87 if c.showDescription { 88 return cmd.CheckEmpty(args) 89 } 90 if len(args) == 0 { 91 return fmt.Errorf("no backup file specified") 92 } 93 c.backupFile = args[0] 94 return cmd.CheckEmpty(args[1:]) 95 } 96 97 var updateBootstrapMachineTemplate = mustParseTemplate(` 98 set -e -x 99 tar xzf juju-backup.tgz 100 test -d juju-backup 101 102 initctl stop jujud-machine-0 103 104 initctl stop juju-db 105 rm -r /var/lib/juju /var/log/juju 106 tar -C / -xvp -f juju-backup/root.tar 107 mkdir -p /var/lib/juju/db 108 export LC_ALL=C 109 mongorestore --drop --dbpath /var/lib/juju/db juju-backup/dump 110 initctl start juju-db 111 112 mongoEval() { 113 mongo --ssl -u {{.Creds.Tag}} -p {{.Creds.Password | shquote}} localhost:37017/juju --eval "$1" 114 } 115 # wait for mongo to come up after starting the juju-db upstart service. 116 for i in $(seq 1 60) 117 do 118 mongoEval ' ' && break 119 sleep 2 120 done 121 mongoEval ' 122 db = db.getSiblingDB("juju") 123 db.machines.update({_id: "0"}, {$set: {instanceid: '{{.NewInstanceId | printf "%q" | shquote}}' } }) 124 db.instanceData.update({_id: "0"}, {$set: {instanceid: '{{.NewInstanceId | printf "%q"| shquote}}' } }) 125 ' 126 initctl start jujud-machine-0 127 `) 128 129 func updateBootstrapMachineScript(instanceId instance.Id, creds credentials) string { 130 return execTemplate(updateBootstrapMachineTemplate, struct { 131 NewInstanceId instance.Id 132 Creds credentials 133 }{instanceId, creds}) 134 } 135 136 func (c *restoreCommand) Run(ctx *cmd.Context) error { 137 if c.showDescription { 138 fmt.Fprintf(ctx.Stdout, "%s\n", c.Info().Purpose) 139 return nil 140 } 141 if err := c.Log.Start(ctx); err != nil { 142 return err 143 } 144 creds, err := extractCreds(c.backupFile) 145 if err != nil { 146 return fmt.Errorf("cannot extract credentials from backup file: %v", err) 147 } 148 progress("extracted credentials from backup file") 149 store, err := configstore.Default() 150 if err != nil { 151 return err 152 } 153 cfg, _, err := environs.ConfigForName(c.EnvName, store) 154 if err != nil { 155 return err 156 } 157 env, err := rebootstrap(cfg, c.Constraints) 158 if err != nil { 159 return fmt.Errorf("cannot re-bootstrap environment: %v", err) 160 } 161 progress("connecting to newly bootstrapped instance") 162 conn, err := juju.NewAPIConn(env, api.DefaultDialOpts()) 163 if err != nil { 164 return fmt.Errorf("cannot connect to bootstrap instance: %v", err) 165 } 166 progress("restoring bootstrap machine") 167 newInstId, machine0Addr, err := restoreBootstrapMachine(conn, c.backupFile, creds) 168 if err != nil { 169 return fmt.Errorf("cannot restore bootstrap machine: %v", err) 170 } 171 progress("restored bootstrap machine") 172 // Update the environ state to point to the new instance. 173 if err := bootstrap.SaveState(env.Storage(), &bootstrap.BootstrapState{ 174 StateInstances: []instance.Id{newInstId}, 175 }); err != nil { 176 return fmt.Errorf("cannot update environ bootstrap state storage: %v", err) 177 } 178 // Construct our own state info rather than using juju.NewConn so 179 // that we can avoid storage eventual-consistency issues 180 // (and it's faster too). 181 caCert, ok := cfg.CACert() 182 if !ok { 183 return fmt.Errorf("configuration has no CA certificate") 184 } 185 progress("opening state") 186 st, err := state.Open(&state.Info{ 187 Addrs: []string{fmt.Sprintf("%s:%d", machine0Addr, cfg.StatePort())}, 188 CACert: caCert, 189 Tag: creds.Tag, 190 Password: creds.Password, 191 }, state.DefaultDialOpts()) 192 if err != nil { 193 return fmt.Errorf("cannot open state: %v", err) 194 } 195 progress("updating all machines") 196 if err := updateAllMachines(st, machine0Addr); err != nil { 197 return fmt.Errorf("cannot update machines: %v", err) 198 } 199 return nil 200 } 201 202 func progress(f string, a ...interface{}) { 203 fmt.Printf("%s\n", fmt.Sprintf(f, a...)) 204 } 205 206 func rebootstrap(cfg *config.Config, cons constraints.Value) (environs.Environ, error) { 207 progress("re-bootstrapping environment") 208 // Turn on safe mode so that the newly bootstrapped instance 209 // will not destroy all the instances it does not know about. 210 cfg, err := cfg.Apply(map[string]interface{}{ 211 "provisioner-safe-mode": true, 212 }) 213 if err != nil { 214 return nil, fmt.Errorf("cannot enable provisioner-safe-mode: %v", err) 215 } 216 env, err := environs.New(cfg) 217 if err != nil { 218 return nil, err 219 } 220 state, err := bootstrap.LoadState(env.Storage()) 221 if err != nil { 222 return nil, fmt.Errorf("cannot retrieve environment storage; perhaps the environment was not bootstrapped: %v", err) 223 } 224 if len(state.StateInstances) == 0 { 225 return nil, fmt.Errorf("no instances found on bootstrap state; perhaps the environment was not bootstrapped") 226 } 227 if len(state.StateInstances) > 1 { 228 return nil, fmt.Errorf("restore does not support HA juju configurations yet") 229 } 230 inst, err := env.Instances(state.StateInstances) 231 if err == nil { 232 return nil, fmt.Errorf("old bootstrap instance %q still seems to exist; will not replace", inst) 233 } 234 if err != environs.ErrNoInstances { 235 return nil, fmt.Errorf("cannot detect whether old instance is still running: %v", err) 236 } 237 // Remove the storage so that we can bootstrap without the provider complaining. 238 if err := env.Storage().Remove(bootstrap.StateFile); err != nil { 239 return nil, fmt.Errorf("cannot remove %q from storage: %v", bootstrap.StateFile, err) 240 } 241 242 // TODO If we fail beyond here, then we won't have a state file and 243 // we won't be able to re-run this script because it fails without it. 244 // We could either try to recreate the file if we fail (which is itself 245 // error-prone) or we could provide a --no-check flag to make 246 // it go ahead anyway without the check. 247 248 if err := bootstrap.Bootstrap(bootstrapContext{}, env, cons); err != nil { 249 return nil, fmt.Errorf("cannot bootstrap new instance: %v", err) 250 } 251 return env, nil 252 } 253 254 func restoreBootstrapMachine(conn *juju.APIConn, backupFile string, creds credentials) (newInstId instance.Id, addr string, err error) { 255 addr, err = conn.State.Client().PublicAddress("0") 256 if err != nil { 257 return "", "", fmt.Errorf("cannot get public address of bootstrap machine: %v", err) 258 } 259 status, err := conn.State.Client().Status(nil) 260 if err != nil { 261 return "", "", fmt.Errorf("cannot get environment status: %v", err) 262 } 263 info, ok := status.Machines["0"] 264 if !ok { 265 return "", "", fmt.Errorf("cannot find bootstrap machine in status") 266 } 267 newInstId = instance.Id(info.InstanceId) 268 269 progress("copying backup file to bootstrap host") 270 if err := scp(backupFile, addr, "~/juju-backup.tgz"); err != nil { 271 return "", "", fmt.Errorf("cannot copy backup file to bootstrap instance: %v", err) 272 } 273 progress("updating bootstrap machine") 274 if err := ssh(addr, updateBootstrapMachineScript(newInstId, creds)); err != nil { 275 return "", "", fmt.Errorf("update script failed: %v", err) 276 } 277 return newInstId, addr, nil 278 } 279 280 type credentials struct { 281 Tag string 282 Password string 283 } 284 285 func extractCreds(backupFile string) (credentials, error) { 286 f, err := os.Open(backupFile) 287 if err != nil { 288 return credentials{}, err 289 } 290 defer f.Close() 291 gzr, err := gzip.NewReader(f) 292 if err != nil { 293 return credentials{}, fmt.Errorf("cannot unzip %q: %v", backupFile, err) 294 } 295 defer gzr.Close() 296 outerTar, err := findFileInTar(gzr, "juju-backup/root.tar") 297 if err != nil { 298 return credentials{}, err 299 } 300 agentConf, err := findFileInTar(outerTar, "var/lib/juju/agents/machine-0/agent.conf") 301 if err != nil { 302 return credentials{}, err 303 } 304 data, err := ioutil.ReadAll(agentConf) 305 if err != nil { 306 return credentials{}, fmt.Errorf("failed to read agent config file: %v", err) 307 } 308 var conf interface{} 309 if err := goyaml.Unmarshal(data, &conf); err != nil { 310 return credentials{}, fmt.Errorf("cannot unmarshal agent config file: %v", err) 311 } 312 m, ok := conf.(map[interface{}]interface{}) 313 if !ok { 314 return credentials{}, fmt.Errorf("config file unmarshalled to %T not %T", conf, m) 315 } 316 password, ok := m["statepassword"].(string) 317 if !ok || password == "" { 318 return credentials{}, fmt.Errorf("agent password not found in configuration") 319 } 320 return credentials{ 321 Tag: "machine-0", 322 Password: password, 323 }, nil 324 } 325 326 func findFileInTar(r io.Reader, name string) (io.Reader, error) { 327 tarr := tar.NewReader(r) 328 for { 329 hdr, err := tarr.Next() 330 if err != nil { 331 return nil, fmt.Errorf("%q not found: %v", name, err) 332 } 333 if path.Clean(hdr.Name) == name { 334 return tarr, nil 335 } 336 } 337 } 338 339 var agentAddressTemplate = mustParseTemplate(` 340 set -exu 341 cd /var/lib/juju/agents 342 for agent in * 343 do 344 initctl stop jujud-$agent 345 sed -i.old -r "/^(stateaddresses|apiaddresses):/{ 346 n 347 s/- .*(:[0-9]+)/- {{.Address}}\1/ 348 }" $agent/agent.conf 349 if [[ $agent = unit-* ]] 350 then 351 sed -i -r 's/change-version: [0-9]+$/change-version: 0/' $agent/state/relations/*/* || true 352 fi 353 initctl start jujud-$agent 354 done 355 sed -i -r 's/^(:syslogtag, startswith, "juju-" @)(.*)(:[0-9]+.*)$/\1{{.Address}}\3/' /etc/rsyslog.d/*-juju*.conf 356 `) 357 358 // setAgentAddressScript generates an ssh script argument to update state addresses 359 func setAgentAddressScript(stateAddr string) string { 360 return execTemplate(agentAddressTemplate, struct { 361 Address string 362 }{stateAddr}) 363 } 364 365 // updateAllMachines finds all machines and resets the stored state address 366 // in each of them. The address does not include the port. 367 func updateAllMachines(st *state.State, stateAddr string) error { 368 machines, err := st.AllMachines() 369 if err != nil { 370 return err 371 } 372 pendingMachineCount := 0 373 done := make(chan error) 374 for _, machine := range machines { 375 // A newly resumed state server requires no updating, and more 376 // than one state server is not yet support by this plugin. 377 if machine.IsManager() || machine.Life() == state.Dead { 378 continue 379 } 380 pendingMachineCount++ 381 machine := machine 382 go func() { 383 err := runMachineUpdate(machine, setAgentAddressScript(stateAddr)) 384 if err != nil { 385 logger.Errorf("failed to update machine %s: %v", machine, err) 386 } else { 387 progress("updated machine %s", machine) 388 } 389 done <- err 390 }() 391 } 392 err = nil 393 for ; pendingMachineCount > 0; pendingMachineCount-- { 394 if updateErr := <-done; updateErr != nil && err == nil { 395 err = fmt.Errorf("machine update failed") 396 } 397 } 398 return err 399 } 400 401 // runMachineUpdate connects via ssh to the machine and runs the update script 402 func runMachineUpdate(m *state.Machine, sshArg string) error { 403 progress("updating machine: %v\n", m) 404 addr := instance.SelectPublicAddress(m.Addresses()) 405 if addr == "" { 406 return fmt.Errorf("no appropriate public address found") 407 } 408 return ssh(addr, sshArg) 409 } 410 411 func ssh(addr string, script string) error { 412 args := []string{ 413 "-l", "ubuntu", 414 "-T", 415 "-o", "StrictHostKeyChecking no", 416 "-o", "PasswordAuthentication no", 417 addr, 418 "sudo -n bash -c " + utils.ShQuote(script), 419 } 420 cmd := exec.Command("ssh", args...) 421 logger.Debugf("ssh command: %s %q", cmd.Path, cmd.Args) 422 data, err := cmd.CombinedOutput() 423 if err != nil { 424 return fmt.Errorf("ssh command failed: %v (%q)", err, data) 425 } 426 progress("ssh command succeeded: %q", data) 427 return nil 428 } 429 430 func scp(file, host, destFile string) error { 431 cmd := exec.Command( 432 "scp", 433 "-B", 434 "-q", 435 "-o", "StrictHostKeyChecking no", 436 "-o", "PasswordAuthentication no", 437 file, 438 "ubuntu@"+host+":"+destFile) 439 logger.Debugf("scp command: %s %q", cmd.Path, cmd.Args) 440 out, err := cmd.CombinedOutput() 441 if err == nil { 442 return nil 443 } 444 if _, ok := err.(*exec.ExitError); ok { 445 return fmt.Errorf("scp failed: %s", out) 446 } 447 return err 448 } 449 450 func mustParseTemplate(templ string) *template.Template { 451 t := template.New("").Funcs(template.FuncMap{ 452 "shquote": utils.ShQuote, 453 }) 454 return template.Must(t.Parse(templ)) 455 } 456 457 func execTemplate(tmpl *template.Template, data interface{}) string { 458 var buf bytes.Buffer 459 err := tmpl.Execute(&buf, data) 460 if err != nil { 461 panic(fmt.Errorf("template error: %v", err)) 462 } 463 return buf.String() 464 } 465 466 type bootstrapContext struct{} 467 468 func (bootstrapContext) Stdin() io.Reader { 469 return os.Stdin 470 } 471 472 func (bootstrapContext) Stdout() io.Writer { 473 return os.Stdout 474 } 475 476 func (bootstrapContext) Stderr() io.Writer { 477 return os.Stderr 478 } 479 480 func (bootstrapContext) InterruptNotify(c chan<- os.Signal) { 481 signal.Notify(c, os.Interrupt) 482 } 483 484 func (bootstrapContext) StopInterruptNotify(c chan<- os.Signal) { 485 signal.Stop(c) 486 }