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