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 }