github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/state/modelmigration.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"fmt"
     8  	"strconv"
     9  	"time"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/names"
    13  	"gopkg.in/mgo.v2"
    14  	"gopkg.in/mgo.v2/bson"
    15  	"gopkg.in/mgo.v2/txn"
    16  
    17  	"github.com/juju/juju/core/migration"
    18  )
    19  
    20  // This file contains functionality for managing the state documents
    21  // used by Juju to track model migrations.
    22  
    23  // ModelMigration represents the state of an migration attempt for a
    24  // model.
    25  type ModelMigration interface {
    26  	// Id returns a unique identifier for the model migration.
    27  	Id() string
    28  
    29  	// ModelUUID returns the UUID for the model being migrated.
    30  	ModelUUID() string
    31  
    32  	// Attempt returns the migration attempt identifier. This
    33  	// increments for each migration attempt for the model.
    34  	Attempt() (int, error)
    35  
    36  	// StartTime returns the time when the migration was started.
    37  	StartTime() time.Time
    38  
    39  	// SuccessTime returns the time when the migration reached
    40  	// SUCCESS.
    41  	SuccessTime() time.Time
    42  
    43  	// EndTime returns the time when the migration reached DONE or
    44  	// REAPFAILED.
    45  	EndTime() time.Time
    46  
    47  	// Phase returns the migration's phase.
    48  	Phase() (migration.Phase, error)
    49  
    50  	// PhaseChangedTime returns the time when the migration's phase
    51  	// last changed.
    52  	PhaseChangedTime() time.Time
    53  
    54  	// StatusMessage returns human readable text about the current
    55  	// progress of the migration.
    56  	StatusMessage() string
    57  
    58  	// InitiatedBy returns username the initiated the migration.
    59  	InitiatedBy() string
    60  
    61  	// TargetInfo returns the details required to connect to the
    62  	// migration's target controller.
    63  	TargetInfo() (*migration.TargetInfo, error)
    64  
    65  	// SetPhase sets the phase of the migration. An error will be
    66  	// returned if the new phase does not follow the current phase or
    67  	// if the migration is no longer active.
    68  	SetPhase(nextPhase migration.Phase) error
    69  
    70  	// SetStatusMessage sets some human readable text about the
    71  	// current progress of the migration.
    72  	SetStatusMessage(text string) error
    73  
    74  	// Refresh updates the contents of the ModelMigration from the
    75  	// underlying state.
    76  	Refresh() error
    77  }
    78  
    79  // modelMigration is an implementation of ModelMigration.
    80  type modelMigration struct {
    81  	st        *State
    82  	doc       modelMigDoc
    83  	statusDoc modelMigStatusDoc
    84  }
    85  
    86  // modelMigDoc holds parameters of a migration attempt for a
    87  // model. These are written into migrationsC.
    88  type modelMigDoc struct {
    89  	// Id holds migration document key. It has the format
    90  	// "uuid:sequence".
    91  	Id string `bson:"_id"`
    92  
    93  	// The UUID of the model being migrated.
    94  	ModelUUID string `bson:"model-uuid"`
    95  
    96  	// InitiatedBy holds the username of the user that triggered the
    97  	// migration. It should be in "user@domain" format.
    98  	InitiatedBy string `bson:"initiated-by"`
    99  
   100  	// TargetController holds the UUID of the target controller.
   101  	TargetController string `bson:"target-controller"`
   102  
   103  	// TargetAddrs holds the host:port values for the target API
   104  	// server.
   105  	TargetAddrs []string `bson:"target-addrs"`
   106  
   107  	// TargetCACert holds the certificate to validate the target API
   108  	// server's TLS certificate.
   109  	TargetCACert string `bson:"target-cacert"`
   110  
   111  	// TargetAuthTag holds a string representation of the tag to
   112  	// authenticate to the target controller with.
   113  	TargetAuthTag string `bson:"target-entity"`
   114  
   115  	// TargetPassword holds the password to use with TargetAuthTag
   116  	// when authenticating.
   117  	TargetPassword string `bson:"target-password"`
   118  }
   119  
   120  // modelMigStatusDoc tracks the progress of a migration attempt for a
   121  // model. These are written into migrationsStatusC.
   122  //
   123  // There is exactly one document in migrationsStatusC for each
   124  // document in migrationsC. Separating them allows for watching
   125  // for new model migrations without being woken up for each model
   126  // migration status change.
   127  type modelMigStatusDoc struct {
   128  	// These are the same as the ids as migrationsC.
   129  	// "uuid:sequence".
   130  	Id string `bson:"_id"`
   131  
   132  	// StartTime holds the time the migration started (stored as per
   133  	// UnixNano).
   134  	StartTime int64 `bson:"start-time"`
   135  
   136  	// StartTime holds the time the migration reached the SUCCESS
   137  	// phase (stored as per UnixNano).
   138  	SuccessTime int64 `bson:"success-time"`
   139  
   140  	// EndTime holds the time the migration reached a terminal (end)
   141  	// phase (stored as per UnixNano).
   142  	EndTime int64 `bson:"end-time"`
   143  
   144  	// Phase holds the current migration phase. This should be one of
   145  	// the string representations of the core/migrations.Phase
   146  	// constants.
   147  	Phase string `bson:"phase"`
   148  
   149  	// PhaseChangedTime holds the time that Phase last changed (stored
   150  	// as per UnixNano).
   151  	PhaseChangedTime int64 `bson:"phase-changed-time"`
   152  
   153  	// StatusMessage holds a human readable message about the
   154  	// migration's progress.
   155  	StatusMessage string `bson:"status-message"`
   156  }
   157  
   158  // Id implements ModelMigration.
   159  func (mig *modelMigration) Id() string {
   160  	return mig.doc.Id
   161  }
   162  
   163  // ModelUUID implements ModelMigration.
   164  func (mig *modelMigration) ModelUUID() string {
   165  	return mig.doc.ModelUUID
   166  }
   167  
   168  // Attempt implements ModelMigration.
   169  func (mig *modelMigration) Attempt() (int, error) {
   170  	attempt, err := strconv.Atoi(mig.st.localID(mig.doc.Id))
   171  	if err != nil {
   172  		// This really shouldn't happen.
   173  		return -1, errors.Errorf("invalid migration id: %v", mig.doc.Id)
   174  	}
   175  	return attempt, nil
   176  }
   177  
   178  // StartTime implements ModelMigration.
   179  func (mig *modelMigration) StartTime() time.Time {
   180  	return unixNanoToTime0(mig.statusDoc.StartTime)
   181  }
   182  
   183  // SuccessTime implements ModelMigration.
   184  func (mig *modelMigration) SuccessTime() time.Time {
   185  	return unixNanoToTime0(mig.statusDoc.SuccessTime)
   186  }
   187  
   188  // EndTime implements ModelMigration.
   189  func (mig *modelMigration) EndTime() time.Time {
   190  	return unixNanoToTime0(mig.statusDoc.EndTime)
   191  }
   192  
   193  // Phase implements ModelMigration.
   194  func (mig *modelMigration) Phase() (migration.Phase, error) {
   195  	phase, ok := migration.ParsePhase(mig.statusDoc.Phase)
   196  	if !ok {
   197  		return phase, errors.Errorf("invalid phase in DB: %v", mig.statusDoc.Phase)
   198  	}
   199  	return phase, nil
   200  }
   201  
   202  // PhaseChangedTime implements ModelMigration.
   203  func (mig *modelMigration) PhaseChangedTime() time.Time {
   204  	return unixNanoToTime0(mig.statusDoc.PhaseChangedTime)
   205  }
   206  
   207  // StatusMessage implements ModelMigration.
   208  func (mig *modelMigration) StatusMessage() string {
   209  	return mig.statusDoc.StatusMessage
   210  }
   211  
   212  // InitiatedBy implements ModelMigration.
   213  func (mig *modelMigration) InitiatedBy() string {
   214  	return mig.doc.InitiatedBy
   215  }
   216  
   217  // TargetInfo implements ModelMigration.
   218  func (mig *modelMigration) TargetInfo() (*migration.TargetInfo, error) {
   219  	authTag, err := names.ParseUserTag(mig.doc.TargetAuthTag)
   220  	if err != nil {
   221  		return nil, errors.Trace(err)
   222  	}
   223  	return &migration.TargetInfo{
   224  		ControllerTag: names.NewModelTag(mig.doc.TargetController),
   225  		Addrs:         mig.doc.TargetAddrs,
   226  		CACert:        mig.doc.TargetCACert,
   227  		AuthTag:       authTag,
   228  		Password:      mig.doc.TargetPassword,
   229  	}, nil
   230  }
   231  
   232  // SetPhase implements ModelMigration.
   233  func (mig *modelMigration) SetPhase(nextPhase migration.Phase) error {
   234  	now := GetClock().Now().UnixNano()
   235  
   236  	phase, err := mig.Phase()
   237  	if err != nil {
   238  		return errors.Trace(err)
   239  	}
   240  
   241  	if nextPhase == phase {
   242  		return nil // Already at that phase. Nothing to do.
   243  	}
   244  	if !phase.CanTransitionTo(nextPhase) {
   245  		return errors.Errorf("illegal phase change: %s -> %s", phase, nextPhase)
   246  	}
   247  
   248  	nextDoc := mig.statusDoc
   249  	nextDoc.Phase = nextPhase.String()
   250  	nextDoc.PhaseChangedTime = now
   251  	update := bson.M{
   252  		"phase":              nextDoc.Phase,
   253  		"phase-changed-time": now,
   254  	}
   255  	if nextPhase == migration.SUCCESS {
   256  		nextDoc.SuccessTime = now
   257  		update["success-time"] = now
   258  	}
   259  	var ops []txn.Op
   260  	if nextPhase.IsTerminal() {
   261  		nextDoc.EndTime = now
   262  		update["end-time"] = now
   263  		ops = append(ops, txn.Op{
   264  			C:      migrationsActiveC,
   265  			Id:     mig.doc.ModelUUID,
   266  			Assert: txn.DocExists,
   267  			Remove: true,
   268  		})
   269  	}
   270  
   271  	ops = append(ops, txn.Op{
   272  		C:      migrationsStatusC,
   273  		Id:     mig.statusDoc.Id,
   274  		Update: bson.M{"$set": update},
   275  		// Ensure phase hasn't changed underneath us
   276  		Assert: bson.M{"phase": mig.statusDoc.Phase},
   277  	})
   278  
   279  	if err := mig.st.runTransaction(ops); err == txn.ErrAborted {
   280  		return errors.New("phase already changed")
   281  	} else if err != nil {
   282  		return errors.Annotate(err, "failed to update phase")
   283  	}
   284  
   285  	mig.statusDoc = nextDoc
   286  	return nil
   287  }
   288  
   289  // SetStatusMessage implements ModelMigration.
   290  func (mig *modelMigration) SetStatusMessage(text string) error {
   291  	ops := []txn.Op{{
   292  		C:      migrationsStatusC,
   293  		Id:     mig.statusDoc.Id,
   294  		Update: bson.M{"$set": bson.M{"status-message": text}},
   295  		Assert: txn.DocExists,
   296  	}}
   297  	if err := mig.st.runTransaction(ops); err != nil {
   298  		return errors.Annotate(err, "failed to set migration status")
   299  	}
   300  	mig.statusDoc.StatusMessage = text
   301  	return nil
   302  }
   303  
   304  // Refresh implements ModelMigration.
   305  func (mig *modelMigration) Refresh() error {
   306  	// Only the status document is updated. The modelMigDoc is static
   307  	// after creation.
   308  	statusColl, closer := mig.st.getCollection(migrationsStatusC)
   309  	defer closer()
   310  	var statusDoc modelMigStatusDoc
   311  	err := statusColl.FindId(mig.doc.Id).One(&statusDoc)
   312  	if err == mgo.ErrNotFound {
   313  		return errors.NotFoundf("migration status")
   314  	} else if err != nil {
   315  		return errors.Annotate(err, "migration status lookup failed")
   316  	}
   317  
   318  	mig.statusDoc = statusDoc
   319  	return nil
   320  }
   321  
   322  // ModelMigrationSpec holds the information required to create a
   323  // ModelMigration instance.
   324  type ModelMigrationSpec struct {
   325  	InitiatedBy names.UserTag
   326  	TargetInfo  migration.TargetInfo
   327  }
   328  
   329  // Validate returns an error if the ModelMigrationSpec contains bad
   330  // data. Nil is returned otherwise.
   331  func (spec *ModelMigrationSpec) Validate() error {
   332  	if !names.IsValidUser(spec.InitiatedBy.Id()) {
   333  		return errors.NotValidf("InitiatedBy")
   334  	}
   335  	return spec.TargetInfo.Validate()
   336  }
   337  
   338  // CreateModelMigration initialises state that tracks a model
   339  // migration. It will return an error if there is already a
   340  // model migration in progress.
   341  func (st *State) CreateModelMigration(spec ModelMigrationSpec) (ModelMigration, error) {
   342  	if st.IsController() {
   343  		return nil, errors.New("controllers can't be migrated")
   344  	}
   345  	if err := spec.Validate(); err != nil {
   346  		return nil, errors.Trace(err)
   347  	}
   348  	if err := checkTargetController(st, spec.TargetInfo.ControllerTag); err != nil {
   349  		return nil, errors.Trace(err)
   350  	}
   351  
   352  	now := GetClock().Now().UnixNano()
   353  	modelUUID := st.ModelUUID()
   354  	var doc modelMigDoc
   355  	var statusDoc modelMigStatusDoc
   356  	buildTxn := func(int) ([]txn.Op, error) {
   357  		model, err := st.Model()
   358  		if err != nil {
   359  			return nil, errors.Annotate(err, "failed to load model")
   360  		}
   361  		if model.Life() != Alive {
   362  			return nil, errors.New("model is not alive")
   363  		}
   364  
   365  		if isActive, err := st.IsModelMigrationActive(); err != nil {
   366  			return nil, errors.Trace(err)
   367  		} else if isActive {
   368  			return nil, errors.New("already in progress")
   369  		}
   370  
   371  		seq, err := st.sequence("modelmigration")
   372  		if err != nil {
   373  			return nil, errors.Trace(err)
   374  		}
   375  
   376  		id := fmt.Sprintf("%s:%d", modelUUID, seq)
   377  		doc = modelMigDoc{
   378  			Id:               id,
   379  			ModelUUID:        modelUUID,
   380  			InitiatedBy:      spec.InitiatedBy.Id(),
   381  			TargetController: spec.TargetInfo.ControllerTag.Id(),
   382  			TargetAddrs:      spec.TargetInfo.Addrs,
   383  			TargetCACert:     spec.TargetInfo.CACert,
   384  			TargetAuthTag:    spec.TargetInfo.AuthTag.String(),
   385  			TargetPassword:   spec.TargetInfo.Password,
   386  		}
   387  		statusDoc = modelMigStatusDoc{
   388  			Id:               id,
   389  			StartTime:        now,
   390  			Phase:            migration.QUIESCE.String(),
   391  			PhaseChangedTime: now,
   392  		}
   393  		return []txn.Op{{
   394  			C:      migrationsC,
   395  			Id:     doc.Id,
   396  			Assert: txn.DocMissing,
   397  			Insert: &doc,
   398  		}, {
   399  			C:      migrationsStatusC,
   400  			Id:     statusDoc.Id,
   401  			Assert: txn.DocMissing,
   402  			Insert: &statusDoc,
   403  		}, {
   404  			C:      migrationsActiveC,
   405  			Id:     modelUUID,
   406  			Assert: txn.DocMissing,
   407  			Insert: bson.M{"id": doc.Id},
   408  		}, model.assertActiveOp(),
   409  		}, nil
   410  	}
   411  	if err := st.run(buildTxn); err != nil {
   412  		return nil, errors.Annotate(err, "failed to create migration")
   413  	}
   414  
   415  	return &modelMigration{
   416  		doc:       doc,
   417  		statusDoc: statusDoc,
   418  		st:        st,
   419  	}, nil
   420  }
   421  
   422  func checkTargetController(st *State, targetControllerTag names.ModelTag) error {
   423  	currentController, err := st.ControllerModel()
   424  	if err != nil {
   425  		return errors.Annotate(err, "failed to load existing controller model")
   426  	}
   427  	if targetControllerTag == currentController.ModelTag() {
   428  		return errors.New("model already attached to target controller")
   429  	}
   430  	return nil
   431  }
   432  
   433  // GetModelMigration returns the most recent ModelMigration for a
   434  // model (if any).
   435  func (st *State) GetModelMigration() (ModelMigration, error) {
   436  	migColl, closer := st.getCollection(migrationsC)
   437  	defer closer()
   438  
   439  	query := migColl.Find(bson.M{"model-uuid": st.ModelUUID()})
   440  	query = query.Sort("-_id").Limit(1)
   441  	var doc modelMigDoc
   442  	err := query.One(&doc)
   443  	if err == mgo.ErrNotFound {
   444  		return nil, errors.NotFoundf("migration")
   445  	} else if err != nil {
   446  		return nil, errors.Annotate(err, "migration lookup failed")
   447  	}
   448  
   449  	statusColl, closer := st.getCollection(migrationsStatusC)
   450  	defer closer()
   451  	var statusDoc modelMigStatusDoc
   452  	err = statusColl.FindId(doc.Id).One(&statusDoc)
   453  	if err != nil {
   454  		return nil, errors.Annotate(err, "failed to find status document")
   455  	}
   456  
   457  	return &modelMigration{
   458  		doc:       doc,
   459  		statusDoc: statusDoc,
   460  		st:        st,
   461  	}, nil
   462  }
   463  
   464  // IsModelMigrationActive return true if a migration is in progress for
   465  // the model associated with the State.
   466  func (st *State) IsModelMigrationActive() (bool, error) {
   467  	active, closer := st.getCollection(migrationsActiveC)
   468  	defer closer()
   469  	n, err := active.FindId(st.ModelUUID()).Count()
   470  	if err != nil {
   471  		return false, errors.Trace(err)
   472  	}
   473  	return n > 0, nil
   474  }
   475  
   476  func unixNanoToTime0(i int64) time.Time {
   477  	if i == 0 {
   478  		return time.Time{}
   479  	}
   480  	return time.Unix(0, i)
   481  }