github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/plugins/juju-upgrade-mongo/upgrade.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"strings"
    13  	"text/template"
    14  	"time"
    15  
    16  	"github.com/juju/cmd"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/names"
    19  	"github.com/juju/replicaset"
    20  	"github.com/juju/utils"
    21  	"launchpad.net/gnuflag"
    22  
    23  	"github.com/juju/juju/api/highavailability"
    24  	"github.com/juju/juju/apiserver/params"
    25  	"github.com/juju/juju/cmd/modelcmd"
    26  	"github.com/juju/juju/juju"
    27  	"github.com/juju/juju/mongo"
    28  	"github.com/juju/juju/network"
    29  )
    30  
    31  func (c *upgradeMongoCommand) SetFlags(f *gnuflag.FlagSet) {
    32  	f.BoolVar(&c.local, "local", false, "this is a local provider")
    33  	c.Log.AddFlags(f)
    34  }
    35  
    36  func main() {
    37  	Main(os.Args)
    38  }
    39  
    40  // Main is the entry point for this plugins.
    41  func Main(args []string) {
    42  	ctx, err := cmd.DefaultContext()
    43  	if err != nil {
    44  		fmt.Fprintf(os.Stderr, "could not obtain context for command: %v\n", err)
    45  		os.Exit(2)
    46  	}
    47  	if err := juju.InitJujuXDGDataHome(); err != nil {
    48  		fmt.Fprintf(os.Stderr, "error: %s\n", err)
    49  		os.Exit(2)
    50  	}
    51  	os.Exit(cmd.Main(modelcmd.Wrap(&upgradeMongoCommand{}), ctx, args[1:]))
    52  }
    53  
    54  const upgradeDoc = `This command upgrades the version of mongo used to store the Juju model from 2.4 to 3.x`
    55  
    56  // MongoUpgradeClient defines the methods
    57  // on the client api that mongo upgrade will call.
    58  type MongoUpgradeClient interface {
    59  	Close() error
    60  	MongoUpgradeMode(mongo.Version) (params.MongoUpgradeResults, error)
    61  	ResumeHAReplicationAfterUpgrade([]replicaset.Member) error
    62  }
    63  
    64  type upgradeMongoCommand struct {
    65  	modelcmd.ModelCommandBase
    66  	Log      cmd.Log
    67  	local    bool
    68  	haClient MongoUpgradeClient
    69  }
    70  
    71  func (c *upgradeMongoCommand) Info() *cmd.Info {
    72  	return &cmd.Info{
    73  		Name:    "juju-upgrade-database",
    74  		Purpose: "Upgrade from mongo 2.4 to 3.x",
    75  		Args:    "",
    76  		Doc:     upgradeDoc,
    77  	}
    78  }
    79  
    80  // runViaJujuSSH will run arbitrary code in the remote machine.
    81  func runViaJujuSSH(machine, script string, stdout, stderr *bytes.Buffer) error {
    82  	cmd := exec.Command("ssh", []string{"-o StrictHostKeyChecking=no", fmt.Sprintf("ubuntu@%s", machine), "sudo -n bash -c " + utils.ShQuote(script)}...)
    83  	cmd.Stderr = stderr
    84  	cmd.Stdout = stdout
    85  	err := cmd.Run()
    86  	if err != nil {
    87  		return errors.Annotatef(err, "ssh command failed: (%q)", stderr.String())
    88  	}
    89  	return nil
    90  }
    91  
    92  // bufferPrinter is intended to print the output of a remote script
    93  // in real time.
    94  // the intention behind this is to provide the user with continuous
    95  // feedback while waiting a remote process that might take some time.
    96  func bufferPrinter(stdout *bytes.Buffer, closer chan int, verbose bool) {
    97  	for {
    98  		select {
    99  		case <-closer:
   100  			return
   101  		case <-time.After(500 * time.Millisecond):
   102  
   103  		}
   104  		line, err := stdout.ReadString(byte('\n'))
   105  		if err == nil || err == io.EOF {
   106  			fmt.Print(line)
   107  		}
   108  		if err != nil && err != io.EOF {
   109  			return
   110  		}
   111  
   112  	}
   113  }
   114  
   115  const (
   116  	jujuUpgradeScript = `
   117  /var/lib/juju/tools/machine-{{.MachineNumber}}/jujud upgrade-mongo --series {{.Series}} --machinetag 'machine-{{.MachineNumber}}'
   118  `
   119  	jujuUpgradeScriptMembers = `
   120  /var/lib/juju/tools/machine-{{.MachineNumber}}/jujud upgrade-mongo --series {{.Series}} --machinetag 'machine-{{.MachineNumber}}' --members '{{.Members}}'
   121  `
   122  	jujuSlaveUpgradeScript = `
   123  /var/lib/juju/tools/machine-{{.MachineNumber}}/jujud upgrade-mongo --series {{.Series}} --machinetag 'machine-{{.MachineNumber}}' --slave
   124  `
   125  )
   126  
   127  type upgradeScriptParams struct {
   128  	MachineNumber string
   129  	Series        string
   130  	Members       string
   131  }
   132  
   133  func (c *upgradeMongoCommand) Run(ctx *cmd.Context) error {
   134  	if err := c.Log.Start(ctx); err != nil {
   135  		return err
   136  	}
   137  
   138  	migratables, err := c.migratableMachines()
   139  	if err != nil {
   140  		return errors.Annotate(err, "cannot determine status servers")
   141  	}
   142  
   143  	addrs := make([]string, len(migratables.rsMembers))
   144  	for i, rsm := range migratables.rsMembers {
   145  		addrs[i] = rsm.Address
   146  	}
   147  	var members string
   148  	if len(addrs) > 0 {
   149  		members = strings.Join(addrs, ",")
   150  	}
   151  
   152  	var stdout, stderr bytes.Buffer
   153  	var closer chan int
   154  	closer = make(chan int, 1)
   155  	defer func() { closer <- 1 }()
   156  	go bufferPrinter(&stdout, closer, false)
   157  
   158  	t := template.New("").Funcs(template.FuncMap{
   159  		"shquote": utils.ShQuote,
   160  	})
   161  	var tmpl *template.Template
   162  	if members == "" {
   163  		tmpl = template.Must(t.Parse(jujuUpgradeScript))
   164  	} else {
   165  		tmpl = template.Must(t.Parse(jujuUpgradeScriptMembers))
   166  	}
   167  	var buf bytes.Buffer
   168  	upgradeParams := upgradeScriptParams{
   169  		migratables.master.machine.Id(),
   170  		migratables.master.series,
   171  		members,
   172  	}
   173  	if err = tmpl.Execute(&buf, upgradeParams); err != nil {
   174  		return errors.Annotate(err, "cannot build a script to perform the remote upgrade")
   175  	}
   176  
   177  	if err := runViaJujuSSH(migratables.master.ip.Value, buf.String(), &stdout, &stderr); err != nil {
   178  		return errors.Annotate(err, "migration to mongo 3 unsuccesful, your database is left in the same state.")
   179  	}
   180  	ts := template.New("")
   181  	tmpl = template.Must(ts.Parse(jujuSlaveUpgradeScript))
   182  	for _, m := range migratables.machines {
   183  		if m.ip.Value == migratables.master.ip.Value {
   184  			continue
   185  		}
   186  		var buf bytes.Buffer
   187  		upgradeParams := upgradeScriptParams{
   188  			m.machine.Id(),
   189  			m.series,
   190  			"",
   191  		}
   192  		if err := tmpl.Execute(&buf, upgradeParams); err != nil {
   193  			return errors.Annotate(err, "cannot build a script to perform the remote upgrade")
   194  		}
   195  		if err := runViaJujuSSH(m.ip.Value, buf.String(), &stdout, &stderr); err != nil {
   196  			return errors.Annotatef(err, "cannot migrate slave machine on %q", m.ip.Value)
   197  		}
   198  	}
   199  	return nil
   200  }
   201  
   202  type migratable struct {
   203  	machine names.MachineTag
   204  	ip      network.Address
   205  	result  int
   206  	series  string
   207  }
   208  
   209  type upgradeMongoParams struct {
   210  	master    migratable
   211  	machines  []migratable
   212  	rsMembers []replicaset.Member
   213  }
   214  
   215  func (c *upgradeMongoCommand) getHAClient() (MongoUpgradeClient, error) {
   216  	if c.haClient != nil {
   217  		return c.haClient, nil
   218  	}
   219  
   220  	root, err := c.NewAPIRoot()
   221  	if err != nil {
   222  		return nil, errors.Annotate(err, "cannot get API connection")
   223  	}
   224  
   225  	// NewClient does not return an error, so we'll return nil
   226  	return highavailability.NewClient(root), nil
   227  }
   228  
   229  func (c *upgradeMongoCommand) migratableMachines() (upgradeMongoParams, error) {
   230  	haClient, err := c.getHAClient()
   231  	if err != nil {
   232  		return upgradeMongoParams{}, err
   233  	}
   234  
   235  	defer haClient.Close()
   236  	results, err := haClient.MongoUpgradeMode(mongo.Mongo32wt)
   237  	if err != nil {
   238  		return upgradeMongoParams{}, errors.Annotate(err, "cannot enter mongo upgrade mode")
   239  	}
   240  	result := upgradeMongoParams{}
   241  
   242  	result.master = migratable{
   243  		ip:      results.Master.PublicAddress,
   244  		machine: names.NewMachineTag(results.Master.Tag),
   245  		series:  results.Master.Series,
   246  	}
   247  	result.machines = make([]migratable, len(results.Members))
   248  	for i, member := range results.Members {
   249  		result.machines[i] = migratable{
   250  			ip:      member.PublicAddress,
   251  			machine: names.NewMachineTag(member.Tag),
   252  			series:  member.Series,
   253  		}
   254  	}
   255  	result.rsMembers = make([]replicaset.Member, len(results.RsMembers))
   256  	for i, rsMember := range results.RsMembers {
   257  		result.rsMembers[i] = rsMember
   258  	}
   259  
   260  	return result, nil
   261  }