github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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  	"net/url"
    10  	"os"
    11  
    12  	"github.com/juju/description"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/loggo"
    15  	"github.com/juju/naturalsort"
    16  	"github.com/juju/version"
    17  	"gopkg.in/juju/charm.v6"
    18  
    19  	"github.com/juju/juju/core/leadership"
    20  	"github.com/juju/juju/core/migration"
    21  	"github.com/juju/juju/feature"
    22  	"github.com/juju/juju/resource"
    23  	"github.com/juju/juju/state"
    24  	"github.com/juju/juju/tools"
    25  )
    26  
    27  var logger = loggo.GetLogger("juju.migration")
    28  
    29  // StateExporter describes interface on state required to export a
    30  // model.
    31  type StateExporter interface {
    32  	// Export generates an abstract representation of a model.
    33  	Export() (description.Model, error)
    34  }
    35  
    36  // ExportModel creates a description.Model representation of the
    37  // active model for StateExporter (typically a *state.State), and
    38  // returns the serialized version. It provides the symmetric
    39  // functionality to ImportModel.
    40  func ExportModel(st StateExporter) ([]byte, error) {
    41  	model, err := st.Export()
    42  	if err != nil {
    43  		return nil, errors.Trace(err)
    44  	}
    45  	bytes, err := description.Serialize(model)
    46  	if err != nil {
    47  		return nil, errors.Trace(err)
    48  	}
    49  	return bytes, nil
    50  }
    51  
    52  // StateImporter describes the method needed to import a model
    53  // into the database.
    54  type StateImporter interface {
    55  	Import(model description.Model) (*state.Model, *state.State, error)
    56  }
    57  
    58  // ClaimerFunc is a function that returns a leadership claimer for the
    59  // model UUID passed.
    60  type ClaimerFunc func(string) (leadership.Claimer, error)
    61  
    62  // ImportModel deserializes a model description from the bytes, transforms
    63  // the model config based on information from the controller model, and then
    64  // imports that as a new database model.
    65  func ImportModel(importer StateImporter, getClaimer ClaimerFunc, bytes []byte) (*state.Model, *state.State, error) {
    66  	model, err := description.Deserialize(bytes)
    67  	if err != nil {
    68  		return nil, nil, errors.Trace(err)
    69  	}
    70  
    71  	dbModel, dbState, err := importer.Import(model)
    72  	if err != nil {
    73  		return nil, nil, errors.Trace(err)
    74  	}
    75  
    76  	config, err := dbState.ControllerConfig()
    77  	if err != nil {
    78  		return nil, nil, errors.Trace(err)
    79  	}
    80  
    81  	// If we're using legacy-leases we get the claimer from the new
    82  	// state - otherwise use the function passed in.
    83  	//
    84  	var claimer leadership.Claimer
    85  	if config.Features().Contains(feature.LegacyLeases) {
    86  		claimer = dbState.LeadershipClaimer()
    87  	} else {
    88  		claimer, err = getClaimer(dbModel.UUID())
    89  		if err != nil {
    90  			return nil, nil, errors.Annotate(err, "getting leadership claimer")
    91  		}
    92  	}
    93  
    94  	logger.Debugf("importing leadership")
    95  	for _, application := range model.Applications() {
    96  		if application.Leader() == "" {
    97  			continue
    98  		}
    99  		// When we import a new model, we need to give the leaders
   100  		// some time to settle. We don't want to have leader switches
   101  		// just because we migrated a model, so this time needs to be
   102  		// long enough to make sure we cover the time taken to migrate
   103  		// a reasonable sized model. We don't yet know how long this
   104  		// is going to be, but we need something.
   105  		// TODO(babbageclunk): Handle this better - maybe a way to
   106  		// suppress leadership expiries for a model until it's
   107  		// finished importing?
   108  		logger.Debugf("%q is the leader for %q", application.Leader(), application.Name())
   109  		err := claimer.ClaimLeadership(
   110  			application.Name(),
   111  			application.Leader(),
   112  			state.InitialLeaderClaimTime,
   113  		)
   114  		if err != nil {
   115  			return nil, nil, errors.Annotatef(
   116  				err,
   117  				"claiming leadership for %q",
   118  				application.Leader(),
   119  			)
   120  		}
   121  	}
   122  
   123  	return dbModel, dbState, nil
   124  }
   125  
   126  // CharmDownlaoder defines a single method that is used to download a
   127  // charm from the source controller in a migration.
   128  type CharmDownloader interface {
   129  	OpenCharm(*charm.URL) (io.ReadCloser, error)
   130  }
   131  
   132  // CharmUploader defines a single method that is used to upload a
   133  // charm to the target controller in a migration.
   134  type CharmUploader interface {
   135  	UploadCharm(*charm.URL, io.ReadSeeker) (*charm.URL, error)
   136  }
   137  
   138  // ToolsDownloader defines a single method that is used to download
   139  // tools from the source controller in a migration.
   140  type ToolsDownloader interface {
   141  	OpenURI(string, url.Values) (io.ReadCloser, error)
   142  }
   143  
   144  // ToolsUploader defines a single method that is used to upload tools
   145  // to the target controller in a migration.
   146  type ToolsUploader interface {
   147  	UploadTools(io.ReadSeeker, version.Binary, ...string) (tools.List, error)
   148  }
   149  
   150  // ResourceDownloader defines the interface for downloading resources
   151  // from the source controller during a migration.
   152  type ResourceDownloader interface {
   153  	OpenResource(string, string) (io.ReadCloser, error)
   154  }
   155  
   156  // ResourceUploader defines the interface for uploading resources into
   157  // the target controller during a migration.
   158  type ResourceUploader interface {
   159  	UploadResource(resource.Resource, io.ReadSeeker) error
   160  	SetPlaceholderResource(resource.Resource) error
   161  	SetUnitResource(string, resource.Resource) error
   162  }
   163  
   164  // UploadBinariesConfig provides all the configuration that the
   165  // UploadBinaries function needs to operate. To construct the config
   166  // with the default helper functions, use `NewUploadBinariesConfig`.
   167  type UploadBinariesConfig struct {
   168  	Charms          []string
   169  	CharmDownloader CharmDownloader
   170  	CharmUploader   CharmUploader
   171  
   172  	Tools           map[version.Binary]string
   173  	ToolsDownloader ToolsDownloader
   174  	ToolsUploader   ToolsUploader
   175  
   176  	Resources          []migration.SerializedModelResource
   177  	ResourceDownloader ResourceDownloader
   178  	ResourceUploader   ResourceUploader
   179  }
   180  
   181  // Validate makes sure that all the config values are non-nil.
   182  func (c *UploadBinariesConfig) Validate() error {
   183  	if c.CharmDownloader == nil {
   184  		return errors.NotValidf("missing CharmDownloader")
   185  	}
   186  	if c.CharmUploader == nil {
   187  		return errors.NotValidf("missing CharmUploader")
   188  	}
   189  	if c.ToolsDownloader == nil {
   190  		return errors.NotValidf("missing ToolsDownloader")
   191  	}
   192  	if c.ToolsUploader == nil {
   193  		return errors.NotValidf("missing ToolsUploader")
   194  	}
   195  	if c.ResourceDownloader == nil {
   196  		return errors.NotValidf("missing ResourceDownloader")
   197  	}
   198  	if c.ResourceUploader == nil {
   199  		return errors.NotValidf("missing ResourceUploader")
   200  	}
   201  	return nil
   202  }
   203  
   204  // UploadBinaries will send binaries stored in the source blobstore to
   205  // the target controller.
   206  func UploadBinaries(config UploadBinariesConfig) error {
   207  	if err := config.Validate(); err != nil {
   208  		return errors.Trace(err)
   209  	}
   210  	if err := uploadCharms(config); err != nil {
   211  		return errors.Trace(err)
   212  	}
   213  	if err := uploadTools(config); err != nil {
   214  		return errors.Trace(err)
   215  	}
   216  	if err := uploadResources(config); err != nil {
   217  		return errors.Trace(err)
   218  	}
   219  	return nil
   220  }
   221  
   222  func streamThroughTempFile(r io.Reader) (_ io.ReadSeeker, cleanup func(), err error) {
   223  	tempFile, err := ioutil.TempFile("", "juju-migrate-binary")
   224  	if err != nil {
   225  		return nil, nil, errors.Trace(err)
   226  	}
   227  	defer func() {
   228  		if err != nil {
   229  			os.Remove(tempFile.Name())
   230  		}
   231  	}()
   232  	_, err = io.Copy(tempFile, r)
   233  	if err != nil {
   234  		return nil, nil, errors.Trace(err)
   235  	}
   236  	tempFile.Seek(0, 0)
   237  	rmTempFile := func() {
   238  		filename := tempFile.Name()
   239  		tempFile.Close()
   240  		os.Remove(filename)
   241  	}
   242  
   243  	return tempFile, rmTempFile, nil
   244  }
   245  
   246  func uploadCharms(config UploadBinariesConfig) error {
   247  	// It is critical that charms are uploaded in ascending charm URL
   248  	// order so that charm revisions end up the same in the target as
   249  	// they were in the source.
   250  	naturalsort.Sort(config.Charms)
   251  
   252  	for _, charmURL := range config.Charms {
   253  		logger.Debugf("sending charm %s to target", charmURL)
   254  
   255  		curl, err := charm.ParseURL(charmURL)
   256  		if err != nil {
   257  			return errors.Annotate(err, "bad charm URL")
   258  		}
   259  
   260  		reader, err := config.CharmDownloader.OpenCharm(curl)
   261  		if err != nil {
   262  			return errors.Annotate(err, "cannot open charm")
   263  		}
   264  		defer reader.Close()
   265  
   266  		content, cleanup, err := streamThroughTempFile(reader)
   267  		if err != nil {
   268  			return errors.Trace(err)
   269  		}
   270  		defer cleanup()
   271  
   272  		if usedCurl, err := config.CharmUploader.UploadCharm(curl, content); err != nil {
   273  			return errors.Annotate(err, "cannot upload charm")
   274  		} else if usedCurl.String() != curl.String() {
   275  			// The target controller shouldn't assign a different charm URL.
   276  			return errors.Errorf("charm %s unexpectedly assigned %s", curl, usedCurl)
   277  		}
   278  	}
   279  	return nil
   280  }
   281  
   282  func uploadTools(config UploadBinariesConfig) error {
   283  	for v, uri := range config.Tools {
   284  		logger.Debugf("sending agent binaries to target: %s", v)
   285  
   286  		reader, err := config.ToolsDownloader.OpenURI(uri, nil)
   287  		if err != nil {
   288  			return errors.Annotate(err, "cannot open charm")
   289  		}
   290  		defer reader.Close()
   291  
   292  		content, cleanup, err := streamThroughTempFile(reader)
   293  		if err != nil {
   294  			return errors.Trace(err)
   295  		}
   296  		defer cleanup()
   297  
   298  		if _, err := config.ToolsUploader.UploadTools(content, v); err != nil {
   299  			return errors.Annotate(err, "cannot upload agent binaries")
   300  		}
   301  	}
   302  	return nil
   303  }
   304  
   305  func uploadResources(config UploadBinariesConfig) error {
   306  	for _, res := range config.Resources {
   307  		if res.ApplicationRevision.IsPlaceholder() {
   308  			// Resource placeholders created in the migration import rather
   309  			// than attempting to post empty resources.
   310  		} else {
   311  			err := uploadAppResource(config, res.ApplicationRevision)
   312  			if err != nil {
   313  				return errors.Trace(err)
   314  			}
   315  		}
   316  		for unitName, unitRev := range res.UnitRevisions {
   317  			if err := config.ResourceUploader.SetUnitResource(unitName, unitRev); err != nil {
   318  				return errors.Annotate(err, "cannot set unit resource")
   319  			}
   320  		}
   321  		// Each config.Resources element also contains a
   322  		// CharmStoreRevision field. This isn't especially important
   323  		// to migrate so is skipped for now.
   324  	}
   325  	return nil
   326  }
   327  
   328  func uploadAppResource(config UploadBinariesConfig, rev resource.Resource) error {
   329  	logger.Debugf("opening application resource for %s: %s", rev.ApplicationID, rev.Name)
   330  	reader, err := config.ResourceDownloader.OpenResource(rev.ApplicationID, rev.Name)
   331  	if err != nil {
   332  		return errors.Annotate(err, "cannot open resource")
   333  	}
   334  	defer reader.Close()
   335  
   336  	// TODO(menn0) - validate that the downloaded revision matches
   337  	// the expected metadata. Check revision and fingerprint.
   338  
   339  	content, cleanup, err := streamThroughTempFile(reader)
   340  	if err != nil {
   341  		return errors.Trace(err)
   342  	}
   343  	defer cleanup()
   344  
   345  	if err := config.ResourceUploader.UploadResource(rev, content); err != nil {
   346  		return errors.Annotate(err, "cannot upload resource")
   347  	}
   348  	return nil
   349  }