github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/migration/migration.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package migration
     5  
     6  import (
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	"github.com/juju/utils/set"
    14  	"github.com/juju/version"
    15  	"gopkg.in/juju/charm.v6-unstable"
    16  	"gopkg.in/mgo.v2"
    17  
    18  	"github.com/juju/juju/api"
    19  	"github.com/juju/juju/core/description"
    20  	"github.com/juju/juju/environs"
    21  	"github.com/juju/juju/environs/config"
    22  	"github.com/juju/juju/state"
    23  	"github.com/juju/juju/state/binarystorage"
    24  	"github.com/juju/juju/state/storage"
    25  	"github.com/juju/juju/tools"
    26  )
    27  
    28  var logger = loggo.GetLogger("juju.migration")
    29  
    30  // StateExporter describes interface on state required to export a
    31  // model.
    32  type StateExporter interface {
    33  	// Export generates an abstract representation of a model.
    34  	Export() (description.Model, error)
    35  }
    36  
    37  // ExportModel creates a description.Model representation of the
    38  // active model for StateExporter (typically a *state.State), and
    39  // returns the serialized version. It provides the symmetric
    40  // functionality to ImportModel.
    41  func ExportModel(st StateExporter) ([]byte, error) {
    42  	model, err := st.Export()
    43  	if err != nil {
    44  		return nil, errors.Trace(err)
    45  	}
    46  	bytes, err := description.Serialize(model)
    47  	if err != nil {
    48  		return nil, errors.Trace(err)
    49  	}
    50  	return bytes, nil
    51  }
    52  
    53  // ImportModel deserializes a model description from the bytes, transforms
    54  // the model config based on information from the controller model, and then
    55  // imports that as a new database model.
    56  func ImportModel(st *state.State, bytes []byte) (*state.Model, *state.State, error) {
    57  	model, err := description.Deserialize(bytes)
    58  	if err != nil {
    59  		return nil, nil, errors.Trace(err)
    60  	}
    61  
    62  	controllerModel, err := st.ControllerModel()
    63  	if err != nil {
    64  		return nil, nil, errors.Trace(err)
    65  	}
    66  
    67  	controllerConfig, err := controllerModel.Config()
    68  	if err != nil {
    69  		return nil, nil, errors.Trace(err)
    70  	}
    71  
    72  	model.UpdateConfig(controllerValues(controllerConfig))
    73  
    74  	if err := updateConfigFromProvider(model, controllerConfig); err != nil {
    75  		return nil, nil, errors.Trace(err)
    76  	}
    77  
    78  	dbModel, dbState, err := st.Import(model)
    79  	if err != nil {
    80  		return nil, nil, errors.Trace(err)
    81  	}
    82  	return dbModel, dbState, nil
    83  }
    84  
    85  func controllerValues(config *config.Config) map[string]interface{} {
    86  	result := make(map[string]interface{})
    87  
    88  	result["state-port"] = config.StatePort()
    89  	result["api-port"] = config.APIPort()
    90  	result["controller-uuid"] = config.ControllerUUID()
    91  	// We ignore the second bool param from the CACert check as if there
    92  	// wasn't a CACert, there is no way we'd be importing a new model
    93  	// into the controller
    94  	result["ca-cert"], _ = config.CACert()
    95  	return result
    96  }
    97  
    98  func updateConfigFromProvider(model description.Model, controllerConfig *config.Config) error {
    99  	newConfig, err := config.New(config.NoDefaults, model.Config())
   100  	if err != nil {
   101  		return errors.Trace(err)
   102  	}
   103  
   104  	provider, err := environs.New(newConfig)
   105  	if err != nil {
   106  		return errors.Trace(err)
   107  	}
   108  
   109  	updater, ok := provider.(environs.MigrationConfigUpdater)
   110  	if !ok {
   111  		return nil
   112  	}
   113  
   114  	model.UpdateConfig(updater.MigrationConfigUpdate(controllerConfig))
   115  	return nil
   116  }
   117  
   118  // UploadBackend define the methods on *state.State that are needed for
   119  // uploading the tools and charms from the current controller to a different
   120  // controller.
   121  type UploadBackend interface {
   122  	Charm(*charm.URL) (*state.Charm, error)
   123  	ModelUUID() string
   124  	MongoSession() *mgo.Session
   125  	ToolsStorage() (binarystorage.StorageCloser, error)
   126  }
   127  
   128  // CharmUploader defines a simple single method interface that is used to
   129  // upload a charm to the target controller
   130  type CharmUploader interface {
   131  	UploadCharm(*charm.URL, io.ReadSeeker) (*charm.URL, error)
   132  }
   133  
   134  // ToolsUploader defines a simple single method interface that is used to
   135  // upload tools to the target controller
   136  type ToolsUploader interface {
   137  	UploadTools(io.ReadSeeker, version.Binary, ...string) (tools.List, error)
   138  }
   139  
   140  // UploadBinariesConfig provides all the configuration that the UploadBinaries
   141  // function needs to operate. The functions are configurable for testing
   142  // purposes. To construct the config with the default functions, use
   143  // `NewUploadBinariesConfig`.
   144  type UploadBinariesConfig struct {
   145  	State  UploadBackend
   146  	Model  description.Model
   147  	Target api.Connection
   148  
   149  	GetCharmUploader func(api.Connection) CharmUploader
   150  	GetToolsUploader func(api.Connection) ToolsUploader
   151  
   152  	GetStateStorage     func(UploadBackend) storage.Storage
   153  	GetCharmStoragePath func(UploadBackend, *charm.URL) (string, error)
   154  }
   155  
   156  // NewUploadBinariesConfig constructs a `UploadBinariesConfig` with the default
   157  // functions to get the uploaders for the target api connection, and functions
   158  // used to get the charm data out of the database.
   159  func NewUploadBinariesConfig(backend UploadBackend, model description.Model, target api.Connection) UploadBinariesConfig {
   160  	return UploadBinariesConfig{
   161  		State:  backend,
   162  		Model:  model,
   163  		Target: target,
   164  
   165  		GetCharmUploader:    getCharmUploader,
   166  		GetStateStorage:     getStateStorage,
   167  		GetToolsUploader:    getToolsUploader,
   168  		GetCharmStoragePath: getCharmStoragePath,
   169  	}
   170  }
   171  
   172  // Validate makes sure that all the config values are non-nil.
   173  func (c *UploadBinariesConfig) Validate() error {
   174  	if c.State == nil {
   175  		return errors.NotValidf("missing UploadBackend")
   176  	}
   177  	if c.Model == nil {
   178  		return errors.NotValidf("missing Model")
   179  	}
   180  	if c.Target == nil {
   181  		return errors.NotValidf("missing Target")
   182  	}
   183  	if c.GetCharmUploader == nil {
   184  		return errors.NotValidf("missing GetCharmUploader")
   185  	}
   186  	if c.GetStateStorage == nil {
   187  		return errors.NotValidf("missing GetStateStorage")
   188  	}
   189  	if c.GetToolsUploader == nil {
   190  		return errors.NotValidf("missing GetToolsUploader")
   191  	}
   192  	if c.GetCharmStoragePath == nil {
   193  		return errors.NotValidf("missing GetCharmStoragePath")
   194  	}
   195  	return nil
   196  }
   197  
   198  // UploadBinaries will send binaries stored in the source blobstore to
   199  // the target controller.
   200  func UploadBinaries(config UploadBinariesConfig) error {
   201  	if err := config.Validate(); err != nil {
   202  		return errors.Trace(err)
   203  	}
   204  	if err := uploadTools(config); err != nil {
   205  		return errors.Trace(err)
   206  	}
   207  
   208  	if err := uploadCharms(config); err != nil {
   209  		return errors.Trace(err)
   210  	}
   211  
   212  	return nil
   213  }
   214  
   215  func getStateStorage(backend UploadBackend) storage.Storage {
   216  	return storage.NewStorage(backend.ModelUUID(), backend.MongoSession())
   217  }
   218  
   219  func getToolsUploader(target api.Connection) ToolsUploader {
   220  	return target.Client()
   221  }
   222  
   223  func getCharmUploader(target api.Connection) CharmUploader {
   224  	return target.Client()
   225  }
   226  
   227  func uploadTools(config UploadBinariesConfig) error {
   228  	storage, err := config.State.ToolsStorage()
   229  	if err != nil {
   230  		return errors.Trace(err)
   231  	}
   232  	defer storage.Close()
   233  
   234  	usedVersions := getUsedToolsVersions(config.Model)
   235  	toolsUploader := config.GetToolsUploader(config.Target)
   236  
   237  	for toolsVersion := range usedVersions {
   238  		logger.Debugf("send tools version %s to target", toolsVersion)
   239  		_, reader, err := storage.Open(toolsVersion.String())
   240  		if err != nil {
   241  			return errors.Trace(err)
   242  		}
   243  		defer reader.Close()
   244  
   245  		content, cleanup, err := streamThroughTempFile(reader)
   246  		if err != nil {
   247  			return errors.Trace(err)
   248  		}
   249  		defer cleanup()
   250  
   251  		// UploadTools encapsulates the HTTP POST necessary to send the tools
   252  		// to the target API server.
   253  		if _, err := toolsUploader.UploadTools(content, toolsVersion); err != nil {
   254  			return errors.Trace(err)
   255  		}
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  func streamThroughTempFile(r io.Reader) (_ io.ReadSeeker, cleanup func(), err error) {
   262  	tempFile, err := ioutil.TempFile("", "juju-tools")
   263  	if err != nil {
   264  		return nil, nil, errors.Trace(err)
   265  	}
   266  	defer func() {
   267  		if err != nil {
   268  			os.Remove(tempFile.Name())
   269  		}
   270  	}()
   271  	_, err = io.Copy(tempFile, r)
   272  	if err != nil {
   273  		return nil, nil, errors.Trace(err)
   274  	}
   275  	tempFile.Seek(0, 0)
   276  	rmTempFile := func() {
   277  		filename := tempFile.Name()
   278  		tempFile.Close()
   279  		os.Remove(filename)
   280  	}
   281  
   282  	return tempFile, rmTempFile, nil
   283  }
   284  
   285  func getUsedToolsVersions(model description.Model) map[version.Binary]bool {
   286  	// Iterate through the model for all tools, and make a map of them.
   287  	usedVersions := make(map[version.Binary]bool)
   288  	// It is most likely that the preconditions will limit the number of
   289  	// tools versions in use, but that is not depended on here.
   290  	for _, machine := range model.Machines() {
   291  		addToolsVersionForMachine(machine, usedVersions)
   292  	}
   293  
   294  	for _, service := range model.Services() {
   295  		for _, unit := range service.Units() {
   296  			tools := unit.Tools()
   297  			usedVersions[tools.Version()] = true
   298  		}
   299  	}
   300  	return usedVersions
   301  }
   302  
   303  func addToolsVersionForMachine(machine description.Machine, usedVersions map[version.Binary]bool) {
   304  	tools := machine.Tools()
   305  	usedVersions[tools.Version()] = true
   306  	for _, container := range machine.Containers() {
   307  		addToolsVersionForMachine(container, usedVersions)
   308  	}
   309  }
   310  
   311  func uploadCharms(config UploadBinariesConfig) error {
   312  	storage := config.GetStateStorage(config.State)
   313  	usedCharms := getUsedCharms(config.Model)
   314  	charmUploader := config.GetCharmUploader(config.Target)
   315  
   316  	for _, charmUrl := range usedCharms.Values() {
   317  		logger.Debugf("send charm %s to target", charmUrl)
   318  
   319  		curl, err := charm.ParseURL(charmUrl)
   320  		if err != nil {
   321  			return errors.Annotate(err, "bad charm URL")
   322  		}
   323  
   324  		path, err := config.GetCharmStoragePath(config.State, curl)
   325  		if err != nil {
   326  			return errors.Trace(err)
   327  		}
   328  
   329  		reader, _, err := storage.Get(path)
   330  		if err != nil {
   331  			return errors.Annotate(err, "cannot get charm from storage")
   332  		}
   333  		defer reader.Close()
   334  
   335  		content, cleanup, err := streamThroughTempFile(reader)
   336  		if err != nil {
   337  			return errors.Trace(err)
   338  		}
   339  		defer cleanup()
   340  
   341  		if _, err := charmUploader.UploadCharm(curl, content); err != nil {
   342  			return errors.Annotate(err, "cannot upload charm")
   343  		}
   344  	}
   345  	return nil
   346  }
   347  
   348  func getUsedCharms(model description.Model) set.Strings {
   349  	result := set.NewStrings()
   350  	for _, service := range model.Services() {
   351  		result.Add(service.CharmURL())
   352  	}
   353  	return result
   354  }
   355  
   356  func getCharmStoragePath(st UploadBackend, curl *charm.URL) (string, error) {
   357  	ch, err := st.Charm(curl)
   358  	if err != nil {
   359  		return "", errors.Annotate(err, "cannot get charm from state")
   360  	}
   361  
   362  	return ch.StoragePath(), nil
   363  }
   364  
   365  // PrecheckBackend is implemented by *state.State but defined as an interface
   366  // for easier testing.
   367  type PrecheckBackend interface {
   368  	NeedsCleanup() (bool, error)
   369  }
   370  
   371  // Precheck checks the database state to make sure that the preconditions
   372  // for model migration are met.
   373  func Precheck(backend PrecheckBackend) error {
   374  	cleanupNeeded, err := backend.NeedsCleanup()
   375  	if err != nil {
   376  		return errors.Annotate(err, "precheck cleanups")
   377  	}
   378  	if cleanupNeeded {
   379  		return errors.New("precheck failed: cleanup needed")
   380  	}
   381  	return nil
   382  }