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  }