Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore 03 add state backup #1326

Merged
merged 14 commits into from Jan 14, 2015
11 changes: 11 additions & 0 deletions apiserver/params/backups.go
Expand Up @@ -6,6 +6,9 @@ package params
import (
"time"

"github.com/juju/names"

"github.com/juju/juju/instance"
"github.com/juju/juju/version"
)

Expand Down Expand Up @@ -67,3 +70,11 @@ type BackupsMetadataResult struct {
Hostname string
Version version.Number
}

// RestoreArgs holds the args to be used to call state/backups.Restore
type RestoreArgs struct {
PrivateAddress string
NewInstId instance.Id
NewInstTag names.Tag
NewInstSeries string
}
7 changes: 7 additions & 0 deletions juju/paths/export_test.go
@@ -0,0 +1,7 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package paths

var OsStat = &osStat
var ExecLookPath = &execLookPath
14 changes: 14 additions & 0 deletions juju/paths/package_test.go
@@ -0,0 +1,14 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package paths_test

import (
"testing"

gc "gopkg.in/check.v1"
)

func Test(t *testing.T) {
gc.TestingT(t)
}
28 changes: 28 additions & 0 deletions juju/paths/paths.go
Expand Up @@ -2,6 +2,10 @@ package paths

import (
"fmt"
"os"
"os/exec"

"github.com/juju/errors"

"github.com/juju/juju/version"
)
Expand Down Expand Up @@ -77,3 +81,27 @@ func MustSucceed(s string, e error) string {
}
return s
}

var osStat = os.Stat
var execLookPath = exec.LookPath

// mongorestorePath will look for mongorestore binary on the system
// and return it if mongorestore actually exists.
// it will look first for the juju provided one and if not found make a
// try at a system one.
func MongorestorePath() (string, error) {
// TODO (perrito666) this seems to be a package decission we should not
// rely on it and we should be aware of /usr/lib/juju if its something
// of ours.
const mongoRestoreFullPath string = "/usr/lib/juju/bin/mongorestore"

if _, err := osStat(mongoRestoreFullPath); err == nil {
return mongoRestoreFullPath, nil
}

path, err := execLookPath("mongorestore")
if err != nil {
return "", errors.Trace(err)
}
return path, nil
}
56 changes: 56 additions & 0 deletions juju/paths/paths_test.go
@@ -0,0 +1,56 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package paths_test

import (
"fmt"
"os"

jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"

"github.com/juju/juju/juju/paths"
"github.com/juju/juju/testing"
)

var _ = gc.Suite(&pathsSuite{})

type pathsSuite struct {
testing.BaseSuite
}

func (s *pathsSuite) TestMongorestorePathDefaultMongoExists(c *gc.C) {
calledWithPaths := []string{}
osStat := func(aPath string) (os.FileInfo, error) {
calledWithPaths = append(calledWithPaths, aPath)
return nil, nil
}
s.PatchValue(paths.OsStat, osStat)
mongoPath, err := paths.MongorestorePath()
c.Assert(err, jc.ErrorIsNil)
c.Assert(mongoPath, gc.Equals, "/usr/lib/juju/bin/mongorestore")
c.Assert(calledWithPaths, gc.DeepEquals, []string{"/usr/lib/juju/bin/mongorestore"})
}

func (s *pathsSuite) TestMongorestorePathNoDefaultMongo(c *gc.C) {
calledWithPaths := []string{}
osStat := func(aPath string) (os.FileInfo, error) {
calledWithPaths = append(calledWithPaths, aPath)
return nil, fmt.Errorf("sorry no mongo")
}
s.PatchValue(paths.OsStat, osStat)

calledWithLookup := []string{}
execLookPath := func(aLookup string) (string, error) {
calledWithLookup = append(calledWithLookup, aLookup)
return "/a/fake/mongo/path", nil
}
s.PatchValue(paths.ExecLookPath, execLookPath)

mongoPath, err := paths.MongorestorePath()
c.Assert(err, jc.ErrorIsNil)
c.Assert(mongoPath, gc.Equals, "/a/fake/mongo/path")
c.Assert(calledWithPaths, gc.DeepEquals, []string{"/usr/lib/juju/bin/mongorestore"})
c.Assert(calledWithLookup, gc.DeepEquals, []string{"mongorestore"})
}
134 changes: 133 additions & 1 deletion state/backups/backups.go
Expand Up @@ -36,12 +36,20 @@ connection.
package backups

import (
"fmt"
"io"
"time"

"github.com/juju/errors"
"github.com/juju/loggo"
"github.com/juju/names"
"github.com/juju/utils/filestorage"

"github.com/juju/juju/agent"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/juju/paths"
"github.com/juju/juju/network"
"github.com/juju/juju/state"
)

const (
Expand Down Expand Up @@ -82,7 +90,6 @@ func StoreArchive(stor filestorage.FileStorage, meta *Metadata, file io.Reader)

// Backups is an abstraction around all juju backup-related functionality.
type Backups interface {

// Create creates and stores a new juju backup archive. It updates
// the provided metadata.
Create(meta *Metadata, paths *Paths, dbInfo *DBInfo) error
Expand All @@ -98,6 +105,9 @@ type Backups interface {

// Remove deletes the backup from storage.
Remove(id string) error

// Restore updates juju's state to the contents of the backup archive.
Restore(backupId string, args params.RestoreArgs) error
}

type backups struct {
Expand Down Expand Up @@ -206,3 +216,125 @@ func (b *backups) List() ([]*Metadata, error) {
func (b *backups) Remove(id string) error {
return errors.Trace(b.storage.Remove(id))
}

// Restore handles either returning or creating a state server to a backed up status:
// * extracts the content of the given backup file and:
// * runs mongorestore with the backed up mongo dump
// * updates and writes configuration files
// * updates existing db entries to make sure they hold no references to
// old instances
// * updates config in all agents.
func (b *backups) Restore(backupId string, args params.RestoreArgs) error {
meta, backupReader, err := b.Get(backupId)
if err != nil {
return errors.Annotatef(err, "could not fetch backup %q", backupId)
}

defer backupReader.Close()

workspace, err := NewArchiveWorkspaceReader(backupReader)
if err != nil {
return errors.Annotate(err, "cannot unpack backup file")
}
defer workspace.Close()

// TODO(perrito666) Create a compatibility table of sorts.
version := meta.Origin.Version
backupMachine := names.NewMachineTag(meta.Origin.Machine)

// delete all the files to be replaced
if err := PrepareMachineForRestore(); err != nil {
return errors.Annotate(err, "cannot delete existing files")
}

if err := workspace.UnpackFilesBundle(filesystemRoot()); err != nil {
return errors.Annotate(err, "cannot obtain system files from backup")
}

if err := updateBackupMachineTag(backupMachine, args.NewInstTag); err != nil {
return errors.Annotate(err, "cannot update paths to reflect current machine id")
}

var agentConfig agent.ConfigSetterWriter
datadir, err := paths.DataDir(args.NewInstSeries)
if err != nil {
return errors.Annotate(err, "cannot determine DataDir for the restored machine")
}
agentConfigFile := agent.ConfigPath(datadir, args.NewInstTag)
if agentConfig, err = agent.ReadConfig(agentConfigFile); err != nil {
return errors.Annotate(err, "cannot load agent config from disk")
}
ssi, ok := agentConfig.StateServingInfo()
if !ok {
return errors.Errorf("cannot determine state serving info")
}
agentConfig.SetValue("tag", args.NewInstTag.String())
APIHostPort := network.HostPort{
Address: network.Address{
Value: args.PrivateAddress,
Type: network.DeriveAddressType(args.PrivateAddress),
},
Port: ssi.APIPort}
agentConfig.SetAPIHostPorts([][]network.HostPort{{APIHostPort}})
if err := agentConfig.Write(); err != nil {
return errors.Annotate(err, "cannot write new agent configuration")
}

// Restore mongodb from backup
if err := placeNewMongo(workspace.DBDumpDir, version); err != nil {
return errors.Annotate(err, "error restoring state from backup")
}

// Re-start replicaset with the new value for server address
dialInfo, err := newDialInfo(args.PrivateAddress, agentConfig)
if err != nil {
return errors.Annotate(err, "cannot produce dial information")
}

memberHostPort := fmt.Sprintf("%s:%d", args.PrivateAddress, ssi.StatePort)
err = resetReplicaSet(dialInfo, memberHostPort)
if err != nil {
return errors.Annotate(err, "cannot reset replicaSet")
}

err = updateMongoEntries(args.NewInstId, args.NewInstTag.Id(), dialInfo)
if err != nil {
return errors.Annotate(err, "cannot update mongo entries")
}

// From here we work with the restored state server
mgoInfo, ok := agentConfig.MongoInfo()
if !ok {
return errors.Errorf("cannot retrieve info to connect to mongo")
}

st, err := newStateConnection(mgoInfo)
if err != nil {
return errors.Trace(err)
}
defer st.Close()

// update all agents known to the new state server.
// TODO(perrito666): We should never stop process because of this.
// updateAllMachines will not return errors for individual
// agent update failures
machines, err := st.AllMachines()
if err != nil {
return errors.Trace(err)
}
if err = updateAllMachines(args.PrivateAddress, machines); err != nil {
return errors.Annotate(err, "cannot update agents")
}

info, err := st.EnsureRestoreInfo()

if err != nil {
return errors.Trace(err)
}

// Mark restoreInfo as Finished so upon restart of the apiserver
// the client can reconnect and determine if we where succesful.
err = info.SetStatus(state.RestoreFinished)

return errors.Annotate(err, "failed to set status to finished")
}
54 changes: 54 additions & 0 deletions state/backups/db.go
Expand Up @@ -12,9 +12,12 @@ import (
"github.com/juju/errors"
"github.com/juju/utils/set"

"github.com/juju/juju/agent"
"github.com/juju/juju/juju/paths"
"github.com/juju/juju/mongo"
"github.com/juju/juju/state/imagestorage"
"github.com/juju/juju/utils"
"github.com/juju/juju/version"
)

// db is a surrogate for the proverbial DB layer abstraction that we
Expand Down Expand Up @@ -212,3 +215,54 @@ func listDatabases(dumpDir string) (set.Strings, error) {
}
return databases, nil
}

// mongoRestoreArgsForVersion returns a string slice containing the args to be used
// to call mongo restore since these can change depending on the backup method.
// Version 0: a dump made with --db, stopping the state server.
// Version 1: a dump made with --oplog with a running state server.
// TODO (perrito666) change versions to use metadata version
func mongoRestoreArgsForVersion(ver version.Number, dumpPath string) ([]string, error) {
dbDir := filepath.Join(agent.DefaultDataDir, "db")
switch {
case ver.Major == 1 && ver.Minor < 22:
return []string{"--drop", "--dbpath", dbDir, dumpPath}, nil
case ver.Major == 1 && ver.Minor >= 22:
return []string{"--drop", "--oplogReplay", "--dbpath", dbDir, dumpPath}, nil
default:
return nil, errors.Errorf("this backup file is incompatible with the current version of juju")
}
}

var restorePath = paths.MongorestorePath
var restoreArgsForVersion = mongoRestoreArgsForVersion

// placeNewMongo tries to use mongorestore to replace an existing
// mongo with the dump in newMongoDumpPath returns an error if its not possible.
func placeNewMongo(newMongoDumpPath string, ver version.Number) error {
mongoRestore, err := restorePath()
if err != nil {
return errors.Annotate(err, "mongorestore not available")
}

mgoRestoreArgs, err := restoreArgsForVersion(ver, newMongoDumpPath)
if err != nil {
return errors.Errorf("cannot restore this backup version")
}
err = runCommand("initctl", "stop", mongo.ServiceName(""))
if err != nil {
return errors.Annotate(err, "failed to stop mongo")
}

err = runCommand(mongoRestore, mgoRestoreArgs...)

if err != nil {
return errors.Annotate(err, "failed to restore database dump")
}

err = runCommand("initctl", "start", mongo.ServiceName(""))
if err != nil {
return errors.Annotate(err, "failed to start mongo")
}

return nil
}