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  }