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