github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/controller/migrationmaster/facade_test.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package migrationmaster_test
     5  
     6  import (
     7  	"fmt"
     8  	"time"
     9  
    10  	"github.com/juju/description/v5"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/names/v5"
    13  	jc "github.com/juju/testing/checkers"
    14  	"github.com/juju/utils/v3"
    15  	"github.com/juju/version/v2"
    16  	"go.uber.org/mock/gomock"
    17  	gc "gopkg.in/check.v1"
    18  	"gopkg.in/macaroon.v2"
    19  
    20  	"github.com/juju/juju/apiserver/common"
    21  	apiservererrors "github.com/juju/juju/apiserver/errors"
    22  	"github.com/juju/juju/apiserver/facade"
    23  	"github.com/juju/juju/apiserver/facades/controller/migrationmaster"
    24  	"github.com/juju/juju/apiserver/facades/controller/migrationmaster/mocks"
    25  	apiservertesting "github.com/juju/juju/apiserver/testing"
    26  	"github.com/juju/juju/controller"
    27  	coremigration "github.com/juju/juju/core/migration"
    28  	"github.com/juju/juju/core/network"
    29  	"github.com/juju/juju/core/presence"
    30  	environscloudspec "github.com/juju/juju/environs/cloudspec"
    31  	"github.com/juju/juju/rpc/params"
    32  	"github.com/juju/juju/state"
    33  	coretesting "github.com/juju/juju/testing"
    34  	jujuversion "github.com/juju/juju/version"
    35  )
    36  
    37  type Suite struct {
    38  	coretesting.BaseSuite
    39  
    40  	controllerBackend *mocks.MockControllerState
    41  	backend           *mocks.MockBackend
    42  	precheckBackend   *mocks.MockPrecheckBackend
    43  
    44  	controllerUUID string
    45  	modelUUID      string
    46  	model          description.Model
    47  	resources      *common.Resources
    48  	authorizer     apiservertesting.FakeAuthorizer
    49  	cloudSpec      environscloudspec.CloudSpec
    50  }
    51  
    52  var _ = gc.Suite(&Suite{})
    53  
    54  func (s *Suite) SetUpTest(c *gc.C) {
    55  	s.BaseSuite.SetUpTest(c)
    56  
    57  	s.controllerUUID = utils.MustNewUUID().String()
    58  	s.modelUUID = utils.MustNewUUID().String()
    59  
    60  	s.model = description.NewModel(description.ModelArgs{
    61  		Type:               "iaas",
    62  		Config:             map[string]interface{}{"uuid": s.modelUUID},
    63  		Owner:              names.NewUserTag("admin"),
    64  		LatestToolsVersion: jujuversion.Current,
    65  	})
    66  
    67  	s.resources = common.NewResources()
    68  	s.AddCleanup(func(*gc.C) { s.resources.StopAll() })
    69  
    70  	s.authorizer = apiservertesting.FakeAuthorizer{Controller: true}
    71  	s.cloudSpec = environscloudspec.CloudSpec{Type: "lxd"}
    72  }
    73  
    74  func (s *Suite) TestNotController(c *gc.C) {
    75  	s.authorizer.Controller = false
    76  
    77  	api, err := s.makeAPI()
    78  	c.Assert(api, gc.IsNil)
    79  	c.Assert(err, gc.Equals, apiservererrors.ErrPerm)
    80  }
    81  
    82  func (s *Suite) TestWatch(c *gc.C) {
    83  	ctrl := s.setupMocks(c)
    84  	defer ctrl.Finish()
    85  
    86  	// Watcher with an initial event in the pipe.
    87  	w := mocks.NewMockNotifyWatcher(ctrl)
    88  	w.EXPECT().Stop().Return(nil).AnyTimes()
    89  
    90  	ch := make(chan struct{}, 1)
    91  	ch <- struct{}{}
    92  	w.EXPECT().Changes().Return(ch).Times(2)
    93  
    94  	s.backend.EXPECT().WatchForMigration().Return(w)
    95  
    96  	result := s.mustMakeAPI(c).Watch()
    97  	c.Assert(result.Error, gc.IsNil)
    98  
    99  	resource := s.resources.Get(result.NotifyWatcherId)
   100  	watcher, _ := resource.(state.NotifyWatcher)
   101  	c.Assert(watcher, gc.NotNil)
   102  
   103  	select {
   104  	case <-watcher.Changes():
   105  		c.Fatalf("initial event not consumed")
   106  	case <-time.After(coretesting.ShortWait):
   107  	}
   108  }
   109  
   110  func (s *Suite) TestMigrationStatus(c *gc.C) {
   111  	ctrl := s.setupMocks(c)
   112  	defer ctrl.Finish()
   113  
   114  	password := "secret"
   115  
   116  	mig := mocks.NewMockModelMigration(ctrl)
   117  
   118  	mac, err := macaroon.New([]byte(password), []byte("id"), "location", macaroon.LatestVersion)
   119  	c.Assert(err, jc.ErrorIsNil)
   120  
   121  	targetInfo := coremigration.TargetInfo{
   122  		ControllerTag: names.NewControllerTag(s.controllerUUID),
   123  		Addrs:         []string{"1.1.1.1:1", "2.2.2.2:2"},
   124  		CACert:        "trust me",
   125  		AuthTag:       names.NewUserTag("admin"),
   126  		Password:      password,
   127  		Macaroons:     []macaroon.Slice{{mac}},
   128  	}
   129  
   130  	exp := mig.EXPECT()
   131  	exp.TargetInfo().Return(&targetInfo, nil)
   132  	exp.Phase().Return(coremigration.IMPORT, nil)
   133  	exp.ModelUUID().Return(s.modelUUID)
   134  	exp.Id().Return("ID")
   135  	now := time.Now()
   136  	exp.PhaseChangedTime().Return(now)
   137  
   138  	s.backend.EXPECT().LatestMigration().Return(mig, nil)
   139  
   140  	api := s.mustMakeAPI(c)
   141  	status, err := api.MigrationStatus()
   142  	c.Assert(err, jc.ErrorIsNil)
   143  
   144  	c.Check(status, gc.DeepEquals, params.MasterMigrationStatus{
   145  		Spec: params.MigrationSpec{
   146  			ModelTag: names.NewModelTag(s.modelUUID).String(),
   147  			TargetInfo: params.MigrationTargetInfo{
   148  				ControllerTag: names.NewControllerTag(s.controllerUUID).String(),
   149  				Addrs:         []string{"1.1.1.1:1", "2.2.2.2:2"},
   150  				CACert:        "trust me",
   151  				AuthTag:       names.NewUserTag("admin").String(),
   152  				Password:      password,
   153  				Macaroons:     `[[{"l":"location","i":"id","s64":"qYAr8nQmJzPWKDppxigFtWaNv0dbzX7cJaligz98LLo"}]]`,
   154  			},
   155  		},
   156  		MigrationId:      "ID",
   157  		Phase:            "IMPORT",
   158  		PhaseChangedTime: now,
   159  	})
   160  }
   161  
   162  func (s *Suite) TestModelInfo(c *gc.C) {
   163  	defer s.setupMocks(c).Finish()
   164  
   165  	exp := s.backend.EXPECT()
   166  	exp.ModelUUID().Return("model-uuid")
   167  	exp.ModelName().Return("model-name", nil)
   168  	exp.ModelOwner().Return(names.NewUserTag("owner"), nil)
   169  	exp.AgentVersion().Return(version.MustParse("1.2.3"), nil)
   170  
   171  	mod, err := s.mustMakeAPI(c).ModelInfo()
   172  	c.Assert(err, jc.ErrorIsNil)
   173  
   174  	c.Assert(mod.UUID, gc.Equals, "model-uuid")
   175  	c.Assert(mod.Name, gc.Equals, "model-name")
   176  	c.Assert(mod.OwnerTag, gc.Equals, names.NewUserTag("owner").String())
   177  	c.Assert(mod.AgentVersion, gc.Equals, version.MustParse("1.2.3"))
   178  }
   179  
   180  func (s *Suite) TestSourceControllerInfo(c *gc.C) {
   181  	defer s.setupMocks(c).Finish()
   182  
   183  	exp := s.backend.EXPECT()
   184  	exp.AllLocalRelatedModels().Return([]string{"related-model-uuid"}, nil)
   185  	s.backend.EXPECT().ControllerConfig().Return(controller.Config{
   186  		controller.ControllerUUIDKey: coretesting.ControllerTag.Id(),
   187  		controller.ControllerName:    "mycontroller",
   188  		controller.CACertKey:         "cacert",
   189  	}, nil)
   190  	apiAddr := []network.SpaceHostPorts{{{
   191  		SpaceAddress: network.SpaceAddress{
   192  			MachineAddress: network.MachineAddress{Value: "10.0.0.1"},
   193  		},
   194  		NetPort: 666,
   195  	}}}
   196  	s.controllerBackend.EXPECT().APIHostPortsForClients().Return(apiAddr, nil)
   197  
   198  	info, err := s.mustMakeAPI(c).SourceControllerInfo()
   199  	c.Assert(err, jc.ErrorIsNil)
   200  
   201  	c.Assert(info, jc.DeepEquals, params.MigrationSourceInfo{
   202  		LocalRelatedModels: []string{"related-model-uuid"},
   203  		ControllerTag:      coretesting.ControllerTag.String(),
   204  		ControllerAlias:    "mycontroller",
   205  		Addrs:              []string{"10.0.0.1:666"},
   206  		CACert:             "cacert",
   207  	})
   208  }
   209  
   210  func (s *Suite) TestSetPhase(c *gc.C) {
   211  	ctrl := s.setupMocks(c)
   212  	defer ctrl.Finish()
   213  
   214  	mig := mocks.NewMockModelMigration(ctrl)
   215  	mig.EXPECT().SetPhase(coremigration.ABORT).Return(nil)
   216  
   217  	s.backend.EXPECT().LatestMigration().Return(mig, nil)
   218  
   219  	err := s.mustMakeAPI(c).SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"})
   220  	c.Assert(err, jc.ErrorIsNil)
   221  
   222  }
   223  
   224  func (s *Suite) TestSetPhaseNoMigration(c *gc.C) {
   225  	defer s.setupMocks(c).Finish()
   226  
   227  	s.backend.EXPECT().LatestMigration().Return(nil, errors.New("boom"))
   228  
   229  	err := s.mustMakeAPI(c).SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"})
   230  	c.Assert(err, gc.ErrorMatches, "could not get migration: boom")
   231  }
   232  
   233  func (s *Suite) TestSetPhaseBadPhase(c *gc.C) {
   234  	err := s.mustMakeAPI(c).SetPhase(params.SetMigrationPhaseArgs{Phase: "wat"})
   235  	c.Assert(err, gc.ErrorMatches, `invalid phase: "wat"`)
   236  }
   237  
   238  func (s *Suite) TestSetPhaseError(c *gc.C) {
   239  	ctrl := s.setupMocks(c)
   240  	defer ctrl.Finish()
   241  
   242  	mig := mocks.NewMockModelMigration(ctrl)
   243  	mig.EXPECT().SetPhase(coremigration.ABORT).Return(errors.New("blam"))
   244  
   245  	s.backend.EXPECT().LatestMigration().Return(mig, nil)
   246  
   247  	err := s.mustMakeAPI(c).SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"})
   248  	c.Assert(err, gc.ErrorMatches, "failed to set phase: blam")
   249  }
   250  
   251  func (s *Suite) TestSetStatusMessage(c *gc.C) {
   252  	ctrl := s.setupMocks(c)
   253  	defer ctrl.Finish()
   254  
   255  	mig := mocks.NewMockModelMigration(ctrl)
   256  	mig.EXPECT().SetStatusMessage("foo").Return(nil)
   257  
   258  	s.backend.EXPECT().LatestMigration().Return(mig, nil)
   259  
   260  	err := s.mustMakeAPI(c).SetStatusMessage(params.SetMigrationStatusMessageArgs{Message: "foo"})
   261  	c.Assert(err, jc.ErrorIsNil)
   262  }
   263  
   264  func (s *Suite) TestSetStatusMessageNoMigration(c *gc.C) {
   265  	defer s.setupMocks(c).Finish()
   266  
   267  	s.backend.EXPECT().LatestMigration().Return(nil, errors.New("boom"))
   268  
   269  	err := s.mustMakeAPI(c).SetStatusMessage(params.SetMigrationStatusMessageArgs{Message: "foo"})
   270  	c.Assert(err, gc.ErrorMatches, "could not get migration: boom")
   271  }
   272  
   273  func (s *Suite) TestSetStatusMessageError(c *gc.C) {
   274  	ctrl := s.setupMocks(c)
   275  	defer ctrl.Finish()
   276  
   277  	mig := mocks.NewMockModelMigration(ctrl)
   278  	mig.EXPECT().SetStatusMessage("foo").Return(errors.New("blam"))
   279  
   280  	s.backend.EXPECT().LatestMigration().Return(mig, nil)
   281  
   282  	err := s.mustMakeAPI(c).SetStatusMessage(params.SetMigrationStatusMessageArgs{Message: "foo"})
   283  	c.Assert(err, gc.ErrorMatches, "failed to set status message: blam")
   284  }
   285  
   286  func (s *Suite) TestPrechecksModelError(c *gc.C) {
   287  	defer s.setupMocks(c).Finish()
   288  
   289  	s.precheckBackend.EXPECT().Model().Return(nil, errors.New("boom"))
   290  
   291  	err := s.mustMakeAPI(c).Prechecks(params.PrechecksArgs{TargetControllerVersion: version.MustParse("2.9.32")})
   292  	c.Assert(err, gc.ErrorMatches, "retrieving model: boom")
   293  }
   294  
   295  func (s *Suite) TestProcessRelations(c *gc.C) {
   296  	api := s.mustMakeAPI(c)
   297  	err := api.ProcessRelations(params.ProcessRelations{ControllerAlias: "foo"})
   298  	c.Assert(err, jc.ErrorIsNil)
   299  }
   300  
   301  func (s *Suite) TestExportIAAS(c *gc.C) {
   302  	s.assertExport(c, "iaas")
   303  }
   304  
   305  func (s *Suite) TestExportCAAS(c *gc.C) {
   306  	s.model = description.NewModel(description.ModelArgs{
   307  		Type:               "caas",
   308  		Config:             map[string]interface{}{"uuid": s.modelUUID},
   309  		Owner:              names.NewUserTag("admin"),
   310  		LatestToolsVersion: jujuversion.Current,
   311  	})
   312  	s.assertExport(c, "caas")
   313  }
   314  
   315  func (s *Suite) assertExport(c *gc.C, modelType string) {
   316  	defer s.setupMocks(c).Finish()
   317  
   318  	app := s.model.AddApplication(description.ApplicationArgs{
   319  		Tag:      names.NewApplicationTag("foo"),
   320  		CharmURL: "ch:foo-0",
   321  	})
   322  
   323  	const tools0 = "2.0.0-ubuntu-amd64"
   324  	const tools1 = "2.0.1-ubuntu-amd64"
   325  	m := s.model.AddMachine(description.MachineArgs{Id: names.NewMachineTag("9")})
   326  	m.SetTools(description.AgentToolsArgs{
   327  		Version: version.MustParseBinary(tools1),
   328  	})
   329  
   330  	res := app.AddResource(description.ResourceArgs{Name: "bin"})
   331  	appRev := res.SetApplicationRevision(description.ResourceRevisionArgs{
   332  		Revision:       2,
   333  		Type:           "file",
   334  		Path:           "bin.tar.gz",
   335  		Description:    "who knows",
   336  		Origin:         "upload",
   337  		FingerprintHex: "abcd",
   338  		Size:           123,
   339  		Timestamp:      time.Now(),
   340  		Username:       "bob",
   341  	})
   342  	csRev := res.SetCharmStoreRevision(description.ResourceRevisionArgs{
   343  		Revision:       3,
   344  		Type:           "file",
   345  		Path:           "fink.tar.gz",
   346  		Description:    "knows who",
   347  		Origin:         "store",
   348  		FingerprintHex: "deaf",
   349  		Size:           321,
   350  		Timestamp:      time.Now(),
   351  		Username:       "xena",
   352  	})
   353  
   354  	unit := app.AddUnit(description.UnitArgs{
   355  		Tag: names.NewUnitTag("foo/0"),
   356  	})
   357  	unit.SetTools(description.AgentToolsArgs{
   358  		Version: version.MustParseBinary(tools0),
   359  	})
   360  	unitRes := unit.AddResource(description.UnitResourceArgs{
   361  		Name: "bin",
   362  		RevisionArgs: description.ResourceRevisionArgs{
   363  			Revision:       1,
   364  			Type:           "file",
   365  			Path:           "bin.tar.gz",
   366  			Description:    "nose knows",
   367  			Origin:         "upload",
   368  			FingerprintHex: "beef",
   369  			Size:           222,
   370  			Timestamp:      time.Now(),
   371  			Username:       "bambam",
   372  		},
   373  	})
   374  	unitRev := unitRes.Revision()
   375  
   376  	s.backend.EXPECT().Export(map[string]string{}).Return(s.model, nil)
   377  
   378  	serialized, err := s.mustMakeAPI(c).Export()
   379  	c.Assert(err, jc.ErrorIsNil)
   380  
   381  	// We don't want to tie this test the serialisation output (that's
   382  	// tested elsewhere). Just check that at least one thing we expect
   383  	// is in the serialised output.
   384  	c.Check(string(serialized.Bytes), jc.Contains, jujuversion.Current.String())
   385  
   386  	c.Check(serialized.Charms, gc.DeepEquals, []string{"ch:foo-0"})
   387  	if modelType == "caas" {
   388  		c.Check(serialized.Tools, gc.HasLen, 0)
   389  	} else {
   390  		c.Check(serialized.Tools, jc.SameContents, []params.SerializedModelTools{
   391  			{tools0, "/tools/" + tools0},
   392  			{tools1, "/tools/" + tools1},
   393  		})
   394  	}
   395  	c.Check(serialized.Resources, gc.DeepEquals, []params.SerializedModelResource{{
   396  		Application: "foo",
   397  		Name:        "bin",
   398  		ApplicationRevision: params.SerializedModelResourceRevision{
   399  			Revision:       appRev.Revision(),
   400  			Type:           appRev.Type(),
   401  			Path:           appRev.Path(),
   402  			Description:    appRev.Description(),
   403  			Origin:         appRev.Origin(),
   404  			FingerprintHex: appRev.FingerprintHex(),
   405  			Size:           appRev.Size(),
   406  			Timestamp:      appRev.Timestamp(),
   407  			Username:       appRev.Username(),
   408  		},
   409  		CharmStoreRevision: params.SerializedModelResourceRevision{
   410  			Revision:       csRev.Revision(),
   411  			Type:           csRev.Type(),
   412  			Path:           csRev.Path(),
   413  			Description:    csRev.Description(),
   414  			Origin:         csRev.Origin(),
   415  			FingerprintHex: csRev.FingerprintHex(),
   416  			Size:           csRev.Size(),
   417  			Timestamp:      csRev.Timestamp(),
   418  			Username:       csRev.Username(),
   419  		},
   420  		UnitRevisions: map[string]params.SerializedModelResourceRevision{
   421  			"foo/0": {
   422  				Revision:       unitRev.Revision(),
   423  				Type:           unitRev.Type(),
   424  				Path:           unitRev.Path(),
   425  				Description:    unitRev.Description(),
   426  				Origin:         unitRev.Origin(),
   427  				FingerprintHex: unitRev.FingerprintHex(),
   428  				Size:           unitRev.Size(),
   429  				Timestamp:      unitRev.Timestamp(),
   430  				Username:       unitRev.Username(),
   431  			},
   432  		},
   433  	}})
   434  }
   435  
   436  func (s *Suite) TestReap(c *gc.C) {
   437  	ctrl := s.setupMocks(c)
   438  	defer ctrl.Finish()
   439  
   440  	mig := mocks.NewMockModelMigration(ctrl)
   441  
   442  	exp := s.backend.EXPECT()
   443  	exp.LatestMigration().Return(mig, nil)
   444  
   445  	// Reaping should set the migration phase to DONE - otherwise
   446  	// there's a race between the migrationmaster worker updating the
   447  	// phase and being stopped because the model's gone. This leaves
   448  	// the migration as active in the source controller, which will
   449  	// prevent the model from being migrated back.
   450  	exp.RemoveExportingModelDocs().Return(nil)
   451  	mig.EXPECT().SetPhase(coremigration.DONE).Return(nil)
   452  
   453  	err := s.mustMakeAPI(c).Reap()
   454  	c.Check(err, jc.ErrorIsNil)
   455  
   456  }
   457  
   458  func (s *Suite) TestReapError(c *gc.C) {
   459  	ctrl := s.setupMocks(c)
   460  	defer ctrl.Finish()
   461  
   462  	mig := mocks.NewMockModelMigration(ctrl)
   463  
   464  	s.backend.EXPECT().LatestMigration().Return(mig, nil)
   465  	s.backend.EXPECT().RemoveExportingModelDocs().Return(errors.New("boom"))
   466  
   467  	err := s.mustMakeAPI(c).Reap()
   468  	c.Check(err, gc.ErrorMatches, "boom")
   469  }
   470  
   471  func (s *Suite) TestWatchMinionReports(c *gc.C) {
   472  	ctrl := s.setupMocks(c)
   473  	defer ctrl.Finish()
   474  
   475  	// Watcher with an initial event in the pipe.
   476  	w := mocks.NewMockNotifyWatcher(ctrl)
   477  	w.EXPECT().Stop().Return(nil).AnyTimes()
   478  
   479  	ch := make(chan struct{}, 1)
   480  	ch <- struct{}{}
   481  	w.EXPECT().Changes().Return(ch).Times(2)
   482  
   483  	mig := mocks.NewMockModelMigration(ctrl)
   484  	mig.EXPECT().WatchMinionReports().Return(w, nil)
   485  
   486  	s.backend.EXPECT().LatestMigration().Return(mig, nil)
   487  
   488  	result := s.mustMakeAPI(c).WatchMinionReports()
   489  	c.Assert(result.Error, gc.IsNil)
   490  
   491  	resource := s.resources.Get(result.NotifyWatcherId)
   492  	watcher, _ := resource.(state.NotifyWatcher)
   493  	c.Assert(watcher, gc.NotNil)
   494  
   495  	select {
   496  	case <-watcher.Changes():
   497  		c.Fatalf("initial event not consumed")
   498  	case <-time.After(coretesting.ShortWait):
   499  	}
   500  }
   501  
   502  func (s *Suite) TestMinionReports(c *gc.C) {
   503  	ctrl := s.setupMocks(c)
   504  	defer ctrl.Finish()
   505  
   506  	// Report 16 unknowns.
   507  	// These are in reverse order in order to test sorting.
   508  	unknown := make([]names.Tag, 0, 16)
   509  	for i := cap(unknown) - 1; i >= 0; i-- {
   510  		unknown = append(unknown, names.NewMachineTag(fmt.Sprintf("%d", i)))
   511  	}
   512  	m50c0 := names.NewMachineTag("50/lxd/0")
   513  	m50c1 := names.NewMachineTag("50/lxd/1")
   514  	m50 := names.NewMachineTag("50")
   515  	m51 := names.NewMachineTag("51")
   516  	m52 := names.NewMachineTag("52")
   517  	u0 := names.NewUnitTag("foo/0")
   518  	u1 := names.NewUnitTag("foo/1")
   519  
   520  	mig := mocks.NewMockModelMigration(ctrl)
   521  
   522  	exp := mig.EXPECT()
   523  	exp.Id().Return("ID")
   524  	exp.Phase().Return(coremigration.IMPORT, nil)
   525  	exp.MinionReports().Return(&state.MinionReports{
   526  		Succeeded: []names.Tag{m50, m51, u0},
   527  		Failed:    []names.Tag{u1, m52, m50c1, m50c0},
   528  		Unknown:   unknown,
   529  	}, nil)
   530  
   531  	s.backend.EXPECT().LatestMigration().Return(mig, nil)
   532  
   533  	reports, err := s.mustMakeAPI(c).MinionReports()
   534  	c.Assert(err, jc.ErrorIsNil)
   535  
   536  	// Expect the sample of unknowns to be in order and be limited to
   537  	// the first 10.
   538  	expectedSample := make([]string, 0, 10)
   539  	for i := 0; i < cap(expectedSample); i++ {
   540  		expectedSample = append(expectedSample, names.NewMachineTag(fmt.Sprintf("%d", i)).String())
   541  	}
   542  	c.Assert(reports, gc.DeepEquals, params.MinionReports{
   543  		MigrationId:   "ID",
   544  		Phase:         "IMPORT",
   545  		SuccessCount:  3,
   546  		UnknownCount:  len(unknown),
   547  		UnknownSample: expectedSample,
   548  		Failed: []string{
   549  			// Note sorting.
   550  			m50c0.String(),
   551  			m50c1.String(),
   552  			m52.String(),
   553  			u1.String(),
   554  		},
   555  	})
   556  }
   557  
   558  func (s *Suite) TestMinionReportTimeout(c *gc.C) {
   559  	ctrl := s.setupMocks(c)
   560  	defer ctrl.Finish()
   561  
   562  	timeout := "30s"
   563  
   564  	s.backend.EXPECT().ControllerConfig().Return(controller.Config{
   565  		controller.MigrationMinionWaitMax: timeout,
   566  	}, nil)
   567  
   568  	res, err := s.mustMakeAPI(c).MinionReportTimeout()
   569  	c.Assert(err, jc.ErrorIsNil)
   570  	c.Assert(res.Error, gc.IsNil)
   571  	c.Check(res.Result, gc.Equals, timeout)
   572  }
   573  
   574  func (s *Suite) setupMocks(c *gc.C) *gomock.Controller {
   575  	ctrl := gomock.NewController(c)
   576  
   577  	s.controllerBackend = mocks.NewMockControllerState(ctrl)
   578  	s.backend = mocks.NewMockBackend(ctrl)
   579  	s.precheckBackend = mocks.NewMockPrecheckBackend(ctrl)
   580  	return ctrl
   581  }
   582  
   583  func (s *Suite) mustMakeAPI(c *gc.C) *migrationmaster.API {
   584  	api, err := s.makeAPI()
   585  	c.Assert(err, jc.ErrorIsNil)
   586  	return api
   587  }
   588  
   589  func (s *Suite) makeAPI() (*migrationmaster.API, error) {
   590  	return migrationmaster.NewAPI(
   591  		s.controllerBackend,
   592  		s.backend,
   593  		s.precheckBackend,
   594  		nil, // pool
   595  		s.resources,
   596  		s.authorizer,
   597  		&stubPresence{},
   598  		func(names.ModelTag) (environscloudspec.CloudSpec, error) { return s.cloudSpec, nil },
   599  		stubLeadership{},
   600  	)
   601  }
   602  
   603  type stubPresence struct{}
   604  
   605  func (f *stubPresence) ModelPresence(modelUUID string) facade.ModelPresence {
   606  	return f
   607  }
   608  
   609  func (f *stubPresence) AgentStatus(agent string) (presence.Status, error) {
   610  	return presence.Alive, nil
   611  }
   612  
   613  type stubLeadership struct{}
   614  
   615  func (stubLeadership) Leaders() (map[string]string, error) {
   616  	return map[string]string{}, nil
   617  }