github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/jujud/upgrade_mongo.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  	"fmt"
     8  	"io/ioutil"
     9  	"net"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/juju/clock"
    17  	"github.com/juju/cmd"
    18  	"github.com/juju/errors"
    19  	"github.com/juju/gnuflag"
    20  	"github.com/juju/packaging/manager"
    21  	"github.com/juju/replicaset"
    22  	"github.com/juju/retry"
    23  	"github.com/juju/utils"
    24  	"github.com/juju/utils/fs"
    25  	"gopkg.in/juju/names.v2"
    26  	"gopkg.in/mgo.v2"
    27  	"gopkg.in/mgo.v2/bson"
    28  
    29  	"github.com/juju/juju/agent"
    30  	jujucmd "github.com/juju/juju/cmd"
    31  	"github.com/juju/juju/juju/paths"
    32  	"github.com/juju/juju/mongo"
    33  	"github.com/juju/juju/service"
    34  	"github.com/juju/juju/service/common"
    35  	"github.com/juju/juju/worker/peergrouper"
    36  )
    37  
    38  // KeyUpgradeBackup is the config key used to store information about
    39  // the backup made when upgrading mongo.
    40  const KeyUpgradeBackup = "mongo-upgrade-backup"
    41  
    42  func createTempDir() (string, error) {
    43  	return ioutil.TempDir("", "")
    44  }
    45  
    46  var defaultCallArgs = retry.CallArgs{
    47  	Attempts: 60,
    48  	Delay:    time.Second,
    49  	Clock:    clock.WallClock,
    50  }
    51  
    52  // NewUpgradeMongoCommand returns a new UpgradeMongo command initialized with
    53  // the default helper functions.
    54  func NewUpgradeMongoCommand() *UpgradeMongoCommand {
    55  	return &UpgradeMongoCommand{
    56  		stat:                 os.Stat,
    57  		remove:               os.RemoveAll,
    58  		mkdir:                os.Mkdir,
    59  		runCommand:           utils.RunCommand,
    60  		dialAndLogin:         dialAndLogin,
    61  		satisfyPrerequisites: satisfyPrerequisites,
    62  		createTempDir:        createTempDir,
    63  		discoverService:      service.DiscoverService,
    64  		fsCopy:               fs.Copy,
    65  		osGetenv:             os.Getenv,
    66  
    67  		callArgs:                    defaultCallArgs,
    68  		mongoStart:                  mongo.StartService,
    69  		mongoStop:                   mongo.StopService,
    70  		mongoRestart:                mongo.ReStartService,
    71  		mongoEnsureServiceInstalled: mongo.EnsureServiceInstalled,
    72  		mongoDialInfo:               mongo.DialInfo,
    73  		initiateMongoServer:         peergrouper.InitiateMongoServer,
    74  		replicasetAdd:               replicaAddCall,
    75  		replicasetRemove:            replicaRemoveCall,
    76  	}
    77  }
    78  
    79  type statFunc func(string) (os.FileInfo, error)
    80  type removeFunc func(string) error
    81  type mkdirFunc func(string, os.FileMode) error
    82  type createTempDirFunc func() (string, error)
    83  type discoverService func(string, common.Conf) (service.Service, error)
    84  type fsCopyFunc func(string, string) error
    85  type osGetenv func(string) string
    86  
    87  type utilsRun func(command string, args ...string) (output string, err error)
    88  
    89  type mgoSession interface {
    90  	Close()
    91  }
    92  
    93  type mgoDb interface {
    94  	Run(interface{}, interface{}) error
    95  }
    96  
    97  type dialAndLogger func(*mongo.MongoInfo, retry.CallArgs) (mgoSession, mgoDb, error)
    98  
    99  type requisitesSatisfier func(string) error
   100  
   101  type mongoService func() error
   102  type mongoEnsureService func(string, int, int, bool, mongo.Version, bool, mongo.MemoryProfile) error
   103  type mongoDialInfo func(mongo.Info, mongo.DialOpts) (*mgo.DialInfo, error)
   104  
   105  type initiateMongoServerFunc func(peergrouper.InitiateMongoParams) error
   106  
   107  type replicaAddFunc func(mgoSession, ...replicaset.Member) error
   108  type replicaRemoveFunc func(mgoSession, ...string) error
   109  
   110  // UpgradeMongoCommand represents a jujud upgrade-mongo command.
   111  type UpgradeMongoCommand struct {
   112  	cmd.CommandBase
   113  	machineTag     string
   114  	series         string
   115  	configFilePath string
   116  	agentConfig    agent.ConfigSetterWriter
   117  	tmpDir         string
   118  	backupPath     string
   119  	rollback       bool
   120  	slave          bool
   121  	members        string
   122  
   123  	// utils used by this struct.
   124  	callArgs             retry.CallArgs
   125  	stat                 statFunc
   126  	remove               removeFunc
   127  	mkdir                mkdirFunc
   128  	runCommand           utilsRun
   129  	dialAndLogin         dialAndLogger
   130  	satisfyPrerequisites requisitesSatisfier
   131  	createTempDir        createTempDirFunc
   132  	discoverService      discoverService
   133  	fsCopy               fsCopyFunc
   134  	osGetenv             osGetenv
   135  
   136  	// mongo related utils.
   137  	mongoStart                  mongoService
   138  	mongoStop                   mongoService
   139  	mongoRestart                mongoService
   140  	mongoEnsureServiceInstalled mongoEnsureService
   141  	mongoDialInfo               mongoDialInfo
   142  	initiateMongoServer         initiateMongoServerFunc
   143  	replicasetAdd               replicaAddFunc
   144  	replicasetRemove            replicaRemoveFunc
   145  }
   146  
   147  // Info returns a decription of the command.
   148  func (*UpgradeMongoCommand) Info() *cmd.Info {
   149  	return jujucmd.Info(&cmd.Info{
   150  		Name:    "upgrade-mongo",
   151  		Purpose: "upgrade state server to mongo 3",
   152  	})
   153  }
   154  
   155  // SetFlags adds the flags for this command to the passed gnuflag.FlagSet.
   156  func (u *UpgradeMongoCommand) SetFlags(f *gnuflag.FlagSet) {
   157  	f.StringVar(&u.machineTag, "machinetag", "machine-0", "unique tag identifier for machine to be upgraded")
   158  	f.StringVar(&u.series, "series", "", "series for the machine")
   159  	f.StringVar(&u.configFilePath, "configfile", "", "path to the config file")
   160  	f.StringVar(&u.members, "members", "", "a comma separated list of replicaset member ips")
   161  	f.BoolVar(&u.rollback, "rollback", false, "rollback a previous attempt at upgrading that was cut in the process")
   162  	f.BoolVar(&u.slave, "slave", false, "this is a slave machine in a replicaset")
   163  }
   164  
   165  // Init initializes the command for running.
   166  func (u *UpgradeMongoCommand) Init(args []string) error {
   167  	return nil
   168  }
   169  
   170  // Run migrates an environment to mongo 3.
   171  func (u *UpgradeMongoCommand) Run(ctx *cmd.Context) error {
   172  	return u.run()
   173  }
   174  
   175  func (u *UpgradeMongoCommand) run() (err error) {
   176  	dataDir, err := paths.DataDir(u.series)
   177  	if err != nil {
   178  		return errors.Annotatef(err, "cannot determine data dir for %q", u.series)
   179  	}
   180  	if u.configFilePath == "" {
   181  		machineTag, err := names.ParseMachineTag(u.machineTag)
   182  		if err != nil {
   183  			return errors.Annotatef(err, "%q is not a valid machine tag", u.machineTag)
   184  		}
   185  		u.configFilePath = agent.ConfigPath(dataDir, machineTag)
   186  	}
   187  	u.agentConfig, err = agent.ReadConfig(u.configFilePath)
   188  	if err != nil {
   189  		return errors.Annotatef(err, "cannot read config file in %q", u.configFilePath)
   190  	}
   191  
   192  	current := u.agentConfig.MongoVersion()
   193  
   194  	agentServiceName := u.agentConfig.Value(agent.AgentServiceName)
   195  	if agentServiceName == "" {
   196  		// For backwards compatibility, handle lack of AgentServiceName.
   197  		agentServiceName = u.osGetenv("UPSTART_JOB")
   198  	}
   199  	if agentServiceName == "" {
   200  		return errors.New("cannot determine juju service name")
   201  	}
   202  	svc, err := u.discoverService(agentServiceName, common.Conf{})
   203  	if err != nil {
   204  		return errors.Annotate(err, "cannot determine juju service")
   205  	}
   206  	if err := svc.Stop(); err != nil {
   207  		return errors.Annotate(err, "cannot stop juju to begin migration")
   208  	}
   209  	defer func() {
   210  		svcErr := svc.Start()
   211  		if err != nil {
   212  			err = errors.Annotatef(err, "failed upgrade and juju start after rollbacking upgrade: %v", svcErr)
   213  		} else {
   214  			err = errors.Annotate(svcErr, "could not start juju after upgrade")
   215  		}
   216  	}()
   217  	if !u.slave {
   218  		defer u.replicaAdd()
   219  	}
   220  	if u.rollback {
   221  		origin := u.agentConfig.Value(KeyUpgradeBackup)
   222  		if origin == "" {
   223  			return errors.New("no available backup")
   224  		}
   225  		return u.rollbackCopyBackup(dataDir, origin)
   226  	}
   227  
   228  	u.tmpDir, err = u.createTempDir()
   229  	if err != nil {
   230  		return errors.Annotate(err, "could not create a temporary directory for the migration")
   231  	}
   232  
   233  	logger.Infof("begin migration to mongo 3")
   234  
   235  	if err := u.satisfyPrerequisites(u.series); err != nil {
   236  		return errors.Annotate(err, "cannot satisfy pre-requisites for the migration")
   237  	}
   238  	if current == mongo.Mongo24 || current == mongo.MongoUpgrade {
   239  		if u.slave {
   240  			return u.upgradeSlave(dataDir)
   241  		}
   242  		u.replicaRemove()
   243  		if err := u.maybeUpgrade24to26(dataDir); err != nil {
   244  			defer func() {
   245  				if u.backupPath == "" {
   246  					return
   247  				}
   248  				logger.Infof("will roll back after failed 2.6 upgrade")
   249  				if err := u.rollbackCopyBackup(dataDir, u.backupPath); err != nil {
   250  					logger.Errorf("could not rollback the upgrade: %v", err)
   251  				}
   252  			}()
   253  			return errors.Annotate(err, "cannot upgrade from mongo 2.4 to 2.6")
   254  		}
   255  		current = mongo.Mongo26
   256  	}
   257  	if current == mongo.Mongo26 || current.StorageEngine != mongo.WiredTiger {
   258  		if err := u.maybeUpgrade26to3x(dataDir); err != nil {
   259  			defer func() {
   260  				if u.backupPath == "" {
   261  					return
   262  				}
   263  				logger.Infof("will roll back after failed 3.0 upgrade")
   264  				if err := u.rollbackCopyBackup(dataDir, u.backupPath); err != nil {
   265  					logger.Errorf("could not rollback the upgrade: %v", err)
   266  				}
   267  			}()
   268  			return errors.Annotate(err, "cannot upgrade from mongo 2.6 to 3")
   269  		}
   270  	}
   271  	return nil
   272  }
   273  
   274  func replicaRemoveCall(session mgoSession, addrs ...string) error {
   275  	mSession := session.(*mgo.Session)
   276  	if err := replicaset.Remove(mSession, addrs...); err != nil {
   277  		return errors.Annotate(err, "cannot resume HA")
   278  	}
   279  	return nil
   280  }
   281  
   282  func (u *UpgradeMongoCommand) replicaRemove() error {
   283  	if u.members == "" {
   284  		return nil
   285  	}
   286  	info, ok := u.agentConfig.MongoInfo()
   287  	if !ok {
   288  		return errors.New("cannot get mongo info from agent config to resume HA")
   289  	}
   290  
   291  	session, _, err := u.dialAndLogin(info, u.callArgs)
   292  	if err != nil {
   293  		return errors.Annotate(err, "error dialing mongo to resume HA")
   294  	}
   295  	defer session.Close()
   296  	addrs := strings.Split(u.members, ",")
   297  
   298  	if err := u.replicasetRemove(session, addrs...); err != nil {
   299  		return errors.Annotate(err, "cannot resume HA")
   300  	}
   301  	return nil
   302  }
   303  
   304  func replicaAddCall(session mgoSession, members ...replicaset.Member) error {
   305  	mSession := session.(*mgo.Session)
   306  	if err := replicaset.Add(mSession, members...); err != nil {
   307  		return errors.Annotate(err, "cannot resume HA")
   308  	}
   309  	return nil
   310  }
   311  
   312  func (u *UpgradeMongoCommand) replicaAdd() error {
   313  	if u.members == "" {
   314  		return nil
   315  	}
   316  	info, ok := u.agentConfig.MongoInfo()
   317  	if !ok {
   318  		return errors.New("cannot get mongo info from agent config to resume HA")
   319  	}
   320  
   321  	session, _, err := u.dialAndLogin(info, u.callArgs)
   322  	if err != nil {
   323  		return errors.Annotate(err, "error dialing mongo to resume HA")
   324  	}
   325  	defer session.Close()
   326  	addrs := strings.Split(u.members, ",")
   327  	members := make([]replicaset.Member, len(addrs))
   328  	for i, addr := range addrs {
   329  		members[i] = replicaset.Member{Address: addr}
   330  	}
   331  
   332  	if err := u.replicasetAdd(session, members...); err != nil {
   333  		return errors.Annotate(err, "cannot resume HA")
   334  	}
   335  	return nil
   336  }
   337  
   338  // UpdateService will re-write the service scripts for mongo and restart it.
   339  func (u *UpgradeMongoCommand) UpdateService(auth bool) error {
   340  	var oplogSize int
   341  	if oplogSizeString := u.agentConfig.Value(agent.MongoOplogSize); oplogSizeString != "" {
   342  		var err error
   343  		if oplogSize, err = strconv.Atoi(oplogSizeString); err != nil {
   344  			return errors.Annotatef(err, "invalid oplog size: %q", oplogSizeString)
   345  		}
   346  	}
   347  
   348  	var numaCtlPolicy bool
   349  	if numaCtlString := u.agentConfig.Value(agent.NUMACtlPreference); numaCtlString != "" {
   350  		var err error
   351  		if numaCtlPolicy, err = strconv.ParseBool(numaCtlString); err != nil {
   352  			return errors.Annotatef(err, "invalid numactl preference: %q", numaCtlString)
   353  		}
   354  	}
   355  	ssi, _ := u.agentConfig.StateServingInfo()
   356  
   357  	err := u.mongoEnsureServiceInstalled(u.agentConfig.DataDir(),
   358  		ssi.StatePort,
   359  		oplogSize,
   360  		numaCtlPolicy,
   361  		u.agentConfig.MongoVersion(),
   362  		auth,
   363  		mongo.MemoryProfileLow)
   364  	return errors.Annotate(err, "cannot ensure mongodb service script is properly installed")
   365  }
   366  
   367  func (u *UpgradeMongoCommand) maybeUpgrade24to26(dataDir string) error {
   368  	logger.Infof("backing up 2.4 MongoDB")
   369  	var err error
   370  	u.backupPath, err = u.copyBackupMongo("24", dataDir)
   371  	if err != nil {
   372  		return errors.Annotate(err, "could not do pre migration backup")
   373  	}
   374  
   375  	logger.Infof("stopping 2.4 MongoDB")
   376  	if err := u.mongoStop(); err != nil {
   377  		return errors.Annotate(err, "cannot stop mongo to perform 2.6 upgrade step")
   378  	}
   379  
   380  	// Run the not-so-optional --upgrade step on mongodb 2.6.
   381  	if err := u.mongo26UpgradeStep(dataDir); err != nil {
   382  		return errors.Annotate(err, "cannot run mongo 2.6 with --upgrade")
   383  	}
   384  
   385  	u.agentConfig.SetMongoVersion(mongo.Mongo26)
   386  	if err := u.agentConfig.Write(); err != nil {
   387  		return errors.Annotate(err, "could not update mongo version in agent.config")
   388  	}
   389  
   390  	if err := u.UpdateService(true); err != nil {
   391  		return errors.Annotate(err, "cannot update mongo service to use mongo 2.6")
   392  	}
   393  
   394  	logger.Infof("starting 2.6 MongoDB")
   395  	if err := u.mongoStart(); err != nil {
   396  		return errors.Annotate(err, "cannot start mongo 2.6 to upgrade auth schema")
   397  	}
   398  
   399  	info, ok := u.agentConfig.MongoInfo()
   400  	if !ok {
   401  		return errors.New("cannot get mongo info from agent config")
   402  	}
   403  
   404  	session, db, err := u.dialAndLogin(info, u.callArgs)
   405  	if err != nil {
   406  		return errors.Annotate(err, "error dialing mongo to upgrade auth schema")
   407  	}
   408  	defer session.Close()
   409  
   410  	var res bson.M
   411  	res = make(bson.M)
   412  	err = db.Run("authSchemaUpgrade", &res)
   413  	if err != nil {
   414  		return errors.Annotate(err, "cannot upgrade auth schema")
   415  	}
   416  
   417  	if res["ok"].(float64) != 1 {
   418  		return errors.Errorf("cannot upgrade auth schema :%s", res["message"])
   419  	}
   420  	session.Close()
   421  	if err := u.mongoRestart(); err != nil {
   422  		return errors.Annotate(err, "cannot restart mongodb 2.6 service")
   423  	}
   424  	return nil
   425  }
   426  
   427  func (u *UpgradeMongoCommand) maybeUpgrade26to3x(dataDir string) error {
   428  	jujuMongoPath := filepath.Dir(mongo.JujuMongodPath(mongo.Mongo26))
   429  	password := u.agentConfig.OldPassword()
   430  	ssi, _ := u.agentConfig.StateServingInfo()
   431  	port := ssi.StatePort
   432  	current := u.agentConfig.MongoVersion()
   433  
   434  	logger.Infof("backing up 2.6 MongoDB")
   435  	if current == mongo.Mongo26 {
   436  		// TODO(perrito666) dont ignore out if debug-log was used.
   437  		_, err := u.mongoDump(jujuMongoPath, password, "30", port)
   438  		if err != nil {
   439  			return errors.Annotate(err, "could not do pre migration backup")
   440  		}
   441  		logger.Infof("pre 3.x migration dump ready.")
   442  		if err := u.mongoStop(); err != nil {
   443  			return errors.Annotate(err, "cannot stop mongo to update to mongo 3")
   444  		}
   445  		logger.Infof("mongo stopped")
   446  
   447  		// Initially, mongo 3, no wired tiger so we can do a pre-migration dump.
   448  		mongoNoWiredTiger := mongo.Mongo32wt
   449  		mongoNoWiredTiger.StorageEngine = mongo.MMAPV1
   450  		u.agentConfig.SetMongoVersion(mongoNoWiredTiger)
   451  		if err := u.agentConfig.Write(); err != nil {
   452  			return errors.Annotate(err, "could not update mongo version in agent.config")
   453  		}
   454  		logger.Infof("new mongo version set-up to %q", mongoNoWiredTiger.String())
   455  
   456  		if err := u.UpdateService(true); err != nil {
   457  			return errors.Annotate(err, "cannot update service script")
   458  		}
   459  		logger.Infof("service startup scripts up to date")
   460  
   461  		if err := u.mongoStart(); err != nil {
   462  			return errors.Annotate(err, "cannot start mongo 3 to do a pre-tiger migration dump")
   463  		}
   464  		logger.Infof("started mongo")
   465  		current = mongo.Mongo32wt
   466  	}
   467  
   468  	if current.Major > 2 {
   469  		jujuMongoPath = filepath.Dir(mongo.JujuMongodPath(current))
   470  		_, err := u.mongoDump(jujuMongoPath, password, "Tiger", port)
   471  		if err != nil {
   472  			return errors.Annotate(err, "could not do the tiger migration export")
   473  		}
   474  		logger.Infof("dumped to change storage")
   475  
   476  		if err := u.mongoStop(); err != nil {
   477  			return errors.Annotate(err, "cannot stop mongo to update to wired tiger")
   478  		}
   479  		logger.Infof("mongo stopped before storage migration")
   480  		if err := u.removeOldDb(u.agentConfig.DataDir()); err != nil {
   481  			return errors.Annotate(err, "cannot prepare the new db location for wired tiger")
   482  		}
   483  		logger.Infof("old db files removed")
   484  
   485  		// Mongo, with wired tiger
   486  		u.agentConfig.SetMongoVersion(mongo.Mongo32wt)
   487  		if err := u.agentConfig.Write(); err != nil {
   488  			return errors.Annotate(err, "could not update mongo version in agent.config")
   489  		}
   490  		logger.Infof("wired tiger set in agent.config")
   491  
   492  		if err := u.UpdateService(false); err != nil {
   493  			return errors.Annotate(err, "cannot update service script to use wired tiger")
   494  		}
   495  		logger.Infof("service startup script up to date")
   496  
   497  		info, ok := u.agentConfig.MongoInfo()
   498  		if !ok {
   499  			return errors.New("cannot get mongo info from agent config")
   500  		}
   501  
   502  		logger.Infof("will create dialinfo for new mongo")
   503  		//TODO(perrito666) make this into its own function
   504  		dialOpts := mongo.DialOpts{}
   505  		dialInfo, err := u.mongoDialInfo(info.Info, dialOpts)
   506  		if err != nil {
   507  			return errors.Annotate(err, "cannot obtain dial info")
   508  		}
   509  
   510  		if err := u.mongoStart(); err != nil {
   511  			return errors.Annotate(err, "cannot start mongo 3 to restart replicaset")
   512  		}
   513  		logger.Infof("mongo started")
   514  
   515  		// perhaps statehost port?
   516  		// we need localhost, since there is no admin user
   517  		peerHostPort := net.JoinHostPort("localhost", fmt.Sprint(ssi.StatePort))
   518  		err = u.initiateMongoServer(peergrouper.InitiateMongoParams{
   519  			DialInfo:       dialInfo,
   520  			MemberHostPort: peerHostPort,
   521  		})
   522  		if err != nil {
   523  			return errors.Annotate(err, "cannot initiate replicaset")
   524  		}
   525  		logger.Infof("mongo initiated")
   526  
   527  		// blobstorage might fail to restore in certain versions of
   528  		// mongorestore because of a bug in mongorestore
   529  		// that breaks gridfs restoration https://jira.mongodb.org/browse/TOOLS-939
   530  		err = u.mongoRestore(jujuMongoPath, "", "Tiger", nil, port, true, 100)
   531  		if err != nil {
   532  			return errors.Annotate(err, "cannot restore the db.")
   533  		}
   534  		logger.Infof("mongo restored into the new storage")
   535  
   536  		if err := u.UpdateService(true); err != nil {
   537  			return errors.Annotate(err, "cannot update service script post wired tiger migration")
   538  		}
   539  		logger.Infof("service scripts up to date")
   540  
   541  		if err := u.mongoRestart(); err != nil {
   542  			return errors.Annotate(err, "cannot restart mongo service after upgrade")
   543  		}
   544  		logger.Infof("mongo restarted")
   545  	}
   546  	return nil
   547  }
   548  
   549  // dialAndLogin returns a mongo session logged in as a user with administrative
   550  // privileges
   551  func dialAndLogin(mongoInfo *mongo.MongoInfo, callArgs retry.CallArgs) (mgoSession, mgoDb, error) {
   552  	var session *mgo.Session
   553  	opts := mongo.DefaultDialOpts()
   554  	callArgs.Func = func() error {
   555  		// Try to connect, retry a few times until the db comes up.
   556  		var err error
   557  		session, err = mongo.DialWithInfo(*mongoInfo, opts)
   558  		if err == nil {
   559  			return nil
   560  		}
   561  		logger.Errorf("cannot open mongo connection: %v", err)
   562  		return err
   563  	}
   564  	if err := retry.Call(callArgs); err != nil {
   565  		return nil, nil, errors.Annotate(err, "error dialing mongo to resume HA")
   566  	}
   567  	admin := session.DB("admin")
   568  	if mongoInfo.Tag != nil {
   569  		if err := admin.Login(mongoInfo.Tag.String(), mongoInfo.Password); err != nil {
   570  			return nil, nil, errors.Annotatef(err, "cannot log in to admin database as %q", mongoInfo.Tag)
   571  		}
   572  	} else if mongoInfo.Password != "" {
   573  		if err := admin.Login(mongo.AdminUser, mongoInfo.Password); err != nil {
   574  			return nil, nil, errors.Annotate(err, "cannot log in to admin database")
   575  		}
   576  	}
   577  	return session, admin, nil
   578  }
   579  
   580  func mongo26UpgradeStepCall(runCommand utilsRun, dataDir string) error {
   581  	updateArgs := []string{"--dbpath", mongo.DbDir(dataDir), "--replSet", "juju", "--upgrade"}
   582  	out, err := runCommand(mongo.JujuMongod24Path, updateArgs...)
   583  	logger.Infof(out)
   584  	if err != nil {
   585  		return errors.Annotate(err, "cannot upgrade mongo 2.4 data")
   586  	}
   587  	return nil
   588  }
   589  
   590  func (u *UpgradeMongoCommand) mongo26UpgradeStep(dataDir string) error {
   591  	return mongo26UpgradeStepCall(u.runCommand, dataDir)
   592  }
   593  
   594  func removeOldDbCall(dataDir string, stat statFunc, remove removeFunc, mkdir mkdirFunc) error {
   595  	dbPath := filepath.Join(dataDir, "db")
   596  
   597  	fi, err := stat(dbPath)
   598  	if err != nil {
   599  		return errors.Annotatef(err, "cannot stat %q", dbPath)
   600  	}
   601  
   602  	if err := remove(dbPath); err != nil {
   603  		return errors.Annotatef(err, "cannot recursively remove %q", dbPath)
   604  	}
   605  	if err := mkdir(dbPath, fi.Mode()); err != nil {
   606  		return errors.Annotatef(err, "cannot re-create %q", dbPath)
   607  	}
   608  	return nil
   609  }
   610  
   611  func (u *UpgradeMongoCommand) removeOldDb(dataDir string) error {
   612  	return removeOldDbCall(dataDir, u.stat, u.remove, u.mkdir)
   613  }
   614  
   615  func satisfyPrerequisites(operatingsystem string) error {
   616  	// CentOS and OpenSUSE are  not currently supported by our mongo package.
   617  	if operatingsystem == "centos7" {
   618  		return errors.New("centos7 is still not suported by this upgrade")
   619  	} else if operatingsystem == "opensuseleap" {
   620  		return errors.New("openSUSE Leap is still not suported by this upgrade")
   621  	}
   622  
   623  	pacman, err := manager.NewPackageManager(operatingsystem)
   624  	if err != nil {
   625  		return errors.Annotatef(err, "cannot obtain package manager for %q", operatingsystem)
   626  	}
   627  
   628  	if err := pacman.InstallPrerequisite(); err != nil {
   629  		return err
   630  	}
   631  
   632  	if err := pacman.Install("juju-mongodb2.6"); err != nil {
   633  		return errors.Annotate(err, "cannot install juju-mongodb2.6")
   634  	}
   635  	// JujuMongoPackage represents a version of mongo > 3.1 .
   636  	if err := pacman.Install(mongo.JujuMongoPackage); err != nil {
   637  		return errors.Annotatef(err, "cannot install %v", mongo.JujuMongoPackage)
   638  	}
   639  	if err := pacman.Install(mongo.JujuMongoToolsPackage); err != nil {
   640  		return errors.Annotatef(err, "cannot install %v", mongo.JujuMongoToolsPackage)
   641  	}
   642  	return nil
   643  }
   644  
   645  func mongoDumpCall(
   646  	runCommand utilsRun, tmpDir, mongoPath, adminPassword, migrationName string,
   647  	statePort int, callArgs retry.CallArgs,
   648  ) (string, error) {
   649  	mongodump := filepath.Join(mongoPath, "mongodump")
   650  	dumpParams := []string{
   651  		"--ssl",
   652  		"-u", "admin",
   653  		"-p", adminPassword,
   654  		"--port", strconv.Itoa(statePort),
   655  		"--host", "localhost",
   656  		"--out", filepath.Join(tmpDir, fmt.Sprintf("migrateTo%sdump", migrationName)),
   657  	}
   658  	var out string
   659  	callArgs.Func = func() error {
   660  		var err error
   661  		out, err = runCommand(mongodump, dumpParams...)
   662  		if err == nil {
   663  			return nil
   664  		}
   665  		logger.Errorf("cannot dump db %v: %s", err, out)
   666  		return err
   667  	}
   668  	if err := retry.Call(callArgs); err != nil {
   669  		logger.Errorf(out)
   670  		return out, errors.Annotate(err, "cannot dump mongo db")
   671  	}
   672  	return out, nil
   673  }
   674  
   675  func (u *UpgradeMongoCommand) mongoDump(mongoPath, adminPassword, migrationName string, statePort int) (string, error) {
   676  	return mongoDumpCall(u.runCommand, u.tmpDir, mongoPath, adminPassword, migrationName, statePort, u.callArgs)
   677  }
   678  
   679  func mongoRestoreCall(runCommand utilsRun, tmpDir, mongoPath, adminPassword, migrationName string,
   680  	dbs []string, statePort int, invalidSSL bool, batchSize int, callArgs retry.CallArgs) error {
   681  	mongorestore := filepath.Join(mongoPath, "mongorestore")
   682  	restoreParams := []string{
   683  		"--ssl",
   684  		"--port", strconv.Itoa(statePort),
   685  		"--host", "localhost",
   686  	}
   687  
   688  	if invalidSSL {
   689  		restoreParams = append(restoreParams, "--sslAllowInvalidCertificates")
   690  	}
   691  	if batchSize > 0 {
   692  		restoreParams = append(restoreParams, "--batchSize", strconv.Itoa(batchSize))
   693  	}
   694  	if adminPassword != "" {
   695  		restoreParams = append(restoreParams, "-u", "admin", "-p", adminPassword)
   696  	}
   697  	var out string
   698  	if len(dbs) == 0 || dbs == nil {
   699  		restoreParams = append(restoreParams, filepath.Join(tmpDir, fmt.Sprintf("migrateTo%sdump", migrationName)))
   700  		restoreCallArgs := callArgs
   701  		restoreCallArgs.Func = func() error {
   702  			var err error
   703  			out, err = runCommand(mongorestore, restoreParams...)
   704  			if err == nil {
   705  				return nil
   706  			}
   707  			logger.Errorf("cannot restore %v: %s", err, out)
   708  			return err
   709  		}
   710  		if err := retry.Call(restoreCallArgs); err != nil {
   711  			err := errors.Annotatef(err, "cannot restore dbs got: %s", out)
   712  			logger.Errorf("%#v", err)
   713  			return err
   714  		}
   715  	}
   716  	for i := range dbs {
   717  		restoreDbParams := append(restoreParams,
   718  			fmt.Sprintf("--db=%s", dbs[i]),
   719  			filepath.Join(tmpDir, fmt.Sprintf("migrateTo%sdump", migrationName), dbs[i]))
   720  		restoreCallArgs := callArgs
   721  		restoreCallArgs.Func = func() error {
   722  			var err error
   723  			out, err = runCommand(mongorestore, restoreDbParams...)
   724  			if err == nil {
   725  				return nil
   726  			}
   727  			logger.Errorf("cannot restore db %q: %v: got %s", dbs[i], err, out)
   728  			return err
   729  		}
   730  		if err := retry.Call(restoreCallArgs); err != nil {
   731  			return errors.Annotatef(err, "cannot restore db %q got: %s", dbs[i], out)
   732  		}
   733  		logger.Infof("Successfully restored db %q", dbs[i])
   734  	}
   735  	return nil
   736  }
   737  
   738  func (u *UpgradeMongoCommand) mongoRestore(mongoPath, adminPassword, migrationName string, dbs []string, statePort int, invalidSSL bool, batchSize int) error {
   739  	return mongoRestoreCall(
   740  		u.runCommand, u.tmpDir, mongoPath, adminPassword, migrationName, dbs,
   741  		statePort, invalidSSL, batchSize, u.callArgs,
   742  	)
   743  }
   744  
   745  // copyBackupMongo will make a copy of mongo db by copying the db
   746  // directory, this is safer than a dump.
   747  func (u *UpgradeMongoCommand) copyBackupMongo(targetVersion, dataDir string) (string, error) {
   748  	tmpDir, err := u.createTempDir()
   749  	if err != nil {
   750  		return "", errors.Annotate(err, "cannot create a working directory for backing up mongo")
   751  	}
   752  	if err := u.mongoStop(); err != nil {
   753  		return "", errors.Annotate(err, "cannot stop mongo to backup")
   754  	}
   755  	defer u.mongoStart()
   756  
   757  	dbPath := filepath.Join(dataDir, "db")
   758  	fi, err := u.stat(dbPath)
   759  
   760  	target := filepath.Join(tmpDir, targetVersion)
   761  	if err := u.mkdir(target, fi.Mode()); err != nil {
   762  		return "", errors.Annotate(err, "cannot create target folder for backup")
   763  	}
   764  	targetDb := filepath.Join(target, "db")
   765  
   766  	u.agentConfig.SetValue(KeyUpgradeBackup, targetDb)
   767  	if err := u.agentConfig.Write(); err != nil {
   768  		return "", errors.Annotate(err, "cannot write agent config backup information")
   769  	}
   770  
   771  	if err := u.fsCopy(dbPath, targetDb); err != nil {
   772  		// TODO, delete what was copied
   773  		return "", errors.Annotate(err, "cannot backup mongo database")
   774  	}
   775  	return targetDb, nil
   776  }
   777  
   778  func (u *UpgradeMongoCommand) rollbackCopyBackup(dataDir, origin string) error {
   779  	if err := u.mongoStop(); err != nil {
   780  		return errors.Annotate(err, "cannot stop mongo to rollback")
   781  	}
   782  	defer u.mongoStart()
   783  
   784  	dbDir := filepath.Join(dataDir, "db")
   785  	if err := u.remove(dbDir); err != nil {
   786  		return errors.Annotate(err, "could not remove the existing folder to rollback")
   787  	}
   788  
   789  	if err := fs.Copy(origin, dbDir); err != nil {
   790  		return errors.Annotate(err, "cannot rollback mongo database")
   791  	}
   792  	if err := u.rollbackAgentConfig(); err != nil {
   793  		return errors.Annotate(err, "cannot roo back agent configuration")
   794  	}
   795  	return errors.Annotate(u.UpdateService(true), "cannot rollback service script")
   796  }
   797  
   798  // rollbackAgentconfig rolls back the config value for mongo version
   799  // to its original one and corrects the entry in stop mongo until.
   800  func (u *UpgradeMongoCommand) rollbackAgentConfig() error {
   801  	u.agentConfig.SetMongoVersion(mongo.Mongo24)
   802  	return errors.Annotate(u.agentConfig.Write(), "could not rollback mongo version in agent.config")
   803  }
   804  
   805  func (u *UpgradeMongoCommand) upgradeSlave(dataDir string) error {
   806  	if err := u.satisfyPrerequisites(u.series); err != nil {
   807  		return errors.Annotate(err, "cannot satisfy pre-requisites for the migration")
   808  	}
   809  	if err := u.mongoStop(); err != nil {
   810  		return errors.Annotate(err, "cannot stop mongo to upgrade mongo slave")
   811  	}
   812  	defer u.mongoStart()
   813  	if err := u.removeOldDb(dataDir); err != nil {
   814  		return errors.Annotate(err, "cannot remove existing slave db")
   815  	}
   816  	// Mongo 3, with wired tiger
   817  	u.agentConfig.SetMongoVersion(mongo.Mongo32wt)
   818  	if err := u.agentConfig.Write(); err != nil {
   819  		return errors.Annotate(err, "could not update mongo version in agent.config")
   820  	}
   821  	logger.Infof("wired tiger set in agent.config")
   822  
   823  	if err := u.UpdateService(false); err != nil {
   824  		return errors.Annotate(err, "cannot update service script to use wired tiger")
   825  	}
   826  	logger.Infof("service startup script up to date")
   827  	return nil
   828  }