github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/modelsummaries.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/mgo/v3/bson"
    12  	"github.com/juju/names/v5"
    13  	"github.com/juju/version/v2"
    14  
    15  	corebase "github.com/juju/juju/core/base"
    16  	"github.com/juju/juju/core/instance"
    17  	"github.com/juju/juju/core/permission"
    18  	"github.com/juju/juju/core/status"
    19  	"github.com/juju/juju/environs/config"
    20  	"github.com/juju/juju/mongo"
    21  	"github.com/juju/juju/mongo/utils"
    22  )
    23  
    24  // UserAccessInfo contains just the information about a single user's access to a model and when they last connected.
    25  type UserAccessInfo struct {
    26  	permission.UserAccess
    27  	LastConnection *time.Time
    28  }
    29  
    30  // MachineModelInfo contains the summary information about a machine for a given model.
    31  type MachineModelInfo struct {
    32  	Id         string
    33  	Hardware   *instance.HardwareCharacteristics
    34  	InstanceId string
    35  	Status     string
    36  }
    37  
    38  // ModelSummary describe interesting information for a given model. This is meant to match the values that a user wants
    39  // to see as part of either show-model or models.
    40  type ModelSummary struct {
    41  	Name           string
    42  	UUID           string
    43  	Type           ModelType
    44  	Owner          string
    45  	ControllerUUID string
    46  	IsController   bool
    47  	Life           Life
    48  
    49  	CloudTag           string
    50  	CloudRegion        string
    51  	CloudCredentialTag string
    52  
    53  	// SLA contains the information about the SLA for the model, if set.
    54  	SLALevel string
    55  	SLAOwner string
    56  
    57  	// Needs Config()
    58  	ProviderType  string
    59  	DefaultSeries string
    60  	DefaultBase   corebase.Base
    61  	AgentVersion  *version.Number
    62  
    63  	// Needs Statuses collection
    64  	Status status.StatusInfo
    65  
    66  	// Access is the access level the supplied user has on this model
    67  	Access permission.Access
    68  	// UserLastConnection is the last time this user has accessed this model
    69  	UserLastConnection *time.Time
    70  
    71  	MachineCount int64
    72  	CoreCount    int64
    73  	UnitCount    int64
    74  
    75  	// Needs Migration collection
    76  	// Do we need all the Migration fields?
    77  	// Migration needs to be a pointer as we may not always have one.
    78  	Migration ModelMigration
    79  }
    80  
    81  // modelSummaryProcessor provides the working space for extracting details for models that a user has access to.
    82  type modelSummaryProcessor struct {
    83  	st          *State
    84  	summaries   []ModelSummary
    85  	user        names.UserTag
    86  	isSuperuser bool
    87  	indexByUUID map[string]int
    88  	modelUUIDs  []string
    89  }
    90  
    91  func newProcessorFromModelDocs(st *State, modelDocs []modelDoc, user names.UserTag, isSuperuser bool) *modelSummaryProcessor {
    92  	p := &modelSummaryProcessor{
    93  		st:          st,
    94  		user:        user,
    95  		isSuperuser: isSuperuser,
    96  	}
    97  	p.summaries = make([]ModelSummary, len(modelDocs))
    98  	p.indexByUUID = make(map[string]int, len(modelDocs))
    99  	p.modelUUIDs = make([]string, len(modelDocs))
   100  	for i, doc := range modelDocs {
   101  		var cloudCred string
   102  		if names.IsValidCloudCredential(doc.CloudCredential) {
   103  			cloudCred = names.NewCloudCredentialTag(doc.CloudCredential).String()
   104  		}
   105  		p.summaries[i] = ModelSummary{
   106  			Name:               doc.Name,
   107  			UUID:               doc.UUID,
   108  			Type:               doc.Type,
   109  			Life:               doc.Life,
   110  			Owner:              doc.Owner,
   111  			ControllerUUID:     doc.ControllerUUID,
   112  			IsController:       doc.UUID == st.modelTag.Id(),
   113  			SLALevel:           string(doc.SLA.Level),
   114  			SLAOwner:           doc.SLA.Owner,
   115  			CloudTag:           names.NewCloudTag(doc.Cloud).String(),
   116  			CloudRegion:        doc.CloudRegion,
   117  			CloudCredentialTag: cloudCred,
   118  		}
   119  		p.indexByUUID[doc.UUID] = i
   120  		p.modelUUIDs[i] = doc.UUID
   121  	}
   122  	return p
   123  }
   124  
   125  func (p *modelSummaryProcessor) fillInFromConfig() error {
   126  	// We use the raw settings because we are reading across model UUIDs
   127  	rawSettings, closer := p.st.database.GetRawCollection(settingsC)
   128  	defer closer()
   129  
   130  	settingIds := make([]string, len(p.modelUUIDs))
   131  	for i, uuid := range p.modelUUIDs {
   132  		settingIds[i] = uuid + ":" + modelGlobalKey
   133  	}
   134  	query := rawSettings.Find(bson.M{"_id": bson.M{"$in": settingIds}})
   135  	var doc settingsDoc
   136  	iter := query.Iter()
   137  	defer iter.Close()
   138  	for iter.Next(&doc) {
   139  		idx, ok := p.indexByUUID[doc.ModelUUID]
   140  		if !ok {
   141  			// How could it return a doc that we don't have?
   142  			continue
   143  		}
   144  
   145  		cfg, err := config.New(config.NoDefaults, doc.Settings)
   146  		if err != nil {
   147  			// err on one model should kill all the other ones?
   148  			return errors.Trace(err)
   149  		}
   150  		detail := &(p.summaries[idx])
   151  		detail.ProviderType = cfg.Type()
   152  		detail.DefaultBase = config.PreferredBase(cfg)
   153  
   154  		// TODO(stickupkid): Ensure we fill in the default series for now, we
   155  		// can switch that out later.
   156  		if detail.DefaultSeries, err = corebase.GetSeriesFromBase(detail.DefaultBase); err != nil {
   157  			return errors.Trace(err)
   158  		}
   159  
   160  		if agentVersion, exists := cfg.AgentVersion(); exists {
   161  			detail.AgentVersion = &agentVersion
   162  		}
   163  	}
   164  	if err := iter.Close(); err != nil {
   165  		return errors.Trace(err)
   166  	}
   167  	return nil
   168  }
   169  
   170  func (p *modelSummaryProcessor) fillInFromStatus() error {
   171  	// We use the raw statuses because otherwise it filters by model-uuid
   172  	rawStatus, closer := p.st.database.GetRawCollection(statusesC)
   173  	defer closer()
   174  	statusIds := make([]string, len(p.modelUUIDs))
   175  	for i, uuid := range p.modelUUIDs {
   176  		statusIds[i] = uuid + ":" + modelGlobalKey
   177  	}
   178  	// TODO(jam): 2017-11-27 Track remaining and error if we're missing any
   179  	query := rawStatus.Find(bson.M{"_id": bson.M{"$in": statusIds}})
   180  	var doc statusDoc
   181  	iter := query.Iter()
   182  	defer iter.Close()
   183  	for iter.Next(&doc) {
   184  		idx, ok := p.indexByUUID[doc.ModelUUID]
   185  		if !ok {
   186  			// missing?
   187  			continue
   188  		}
   189  		p.summaries[idx].Status = status.StatusInfo{
   190  			Status:  doc.Status,
   191  			Message: doc.StatusInfo,
   192  			Data:    utils.UnescapeKeys(doc.StatusData),
   193  			Since:   unixNanoToTime(doc.Updated),
   194  		}
   195  	}
   196  	if err := iter.Close(); err != nil {
   197  		return errors.Trace(err)
   198  	}
   199  	return nil
   200  }
   201  
   202  func (p *modelSummaryProcessor) fillInPermissions(permissionIds []string) error {
   203  	// permissionsC is a global collection, so can be accessed from any state
   204  	perms, closer := p.st.db().GetCollection(permissionsC)
   205  	defer closer()
   206  	query := perms.Find(bson.M{"_id": bson.M{"$in": permissionIds}})
   207  	iter := query.Iter()
   208  	defer iter.Close()
   209  
   210  	var doc permissionDoc
   211  	for iter.Next(&doc) {
   212  		var modelUUID string
   213  		if strings.HasPrefix(doc.ObjectGlobalKey, modelGlobalKey+"#") {
   214  			modelUUID = doc.ObjectGlobalKey[2:]
   215  		} else {
   216  			// Invalid ObjectGlobalKey
   217  			continue
   218  		}
   219  		modelIdx, ok := p.indexByUUID[modelUUID]
   220  		if !ok {
   221  			// How did we get a document that isn't in our list of documents?
   222  			// TODO(jam) 2017-11-27, probably should be treated at least as a logged warning
   223  			continue
   224  		}
   225  		details := &p.summaries[modelIdx]
   226  		access := permission.Access(doc.Access)
   227  		if err := access.Validate(); err == nil {
   228  			details.Access = access
   229  		}
   230  	}
   231  	if err := iter.Close(); err != nil {
   232  		return errors.Trace(err)
   233  	}
   234  	return nil
   235  }
   236  
   237  func (p *modelSummaryProcessor) fillInMachineSummary() error {
   238  	machines, closer := p.st.db().GetRawCollection(machinesC)
   239  	defer closer()
   240  	query := machines.Find(bson.M{
   241  		"model-uuid": bson.M{"$in": p.modelUUIDs},
   242  		"life":       Alive,
   243  	})
   244  	query.Select(bson.M{"life": 1, "model-uuid": 1, "_id": 1, "machineid": 1})
   245  	iter := query.Iter()
   246  	defer iter.Close()
   247  	var doc machineDoc
   248  	machineIds := make([]string, 0)
   249  	for iter.Next(&doc) {
   250  		if doc.Life != Alive {
   251  			continue
   252  		}
   253  		idx, ok := p.indexByUUID[doc.ModelUUID]
   254  		if !ok {
   255  			continue
   256  		}
   257  		// There was a lot of data that was collected from things like Machine.Status.
   258  		// However, if we're just aggregating the counts, we don't care about any of that.
   259  		details := &p.summaries[idx]
   260  		// CAAS models don't have machines.
   261  		if details.Type == ModelTypeCAAS {
   262  			continue
   263  		}
   264  		details.MachineCount++
   265  		machineIds = append(machineIds, doc.ModelUUID+":"+doc.Id)
   266  	}
   267  	if err := iter.Close(); err != nil {
   268  		return errors.Trace(err)
   269  	}
   270  	instances, closer2 := p.st.db().GetRawCollection(instanceDataC)
   271  	defer closer2()
   272  	query = instances.Find(bson.M{"_id": bson.M{"$in": machineIds}})
   273  	query.Select(bson.M{"cpucores": 1, "model-uuid": 1})
   274  	iter = query.Iter()
   275  	defer iter.Close()
   276  	var instData instanceData
   277  	for iter.Next(&instData) {
   278  		idx, ok := p.indexByUUID[instData.ModelUUID]
   279  		if !ok {
   280  			continue
   281  		}
   282  		details := &p.summaries[idx]
   283  		if instData.CpuCores != nil {
   284  			details.CoreCount += int64(*instData.CpuCores)
   285  		}
   286  	}
   287  	if err := iter.Close(); err != nil {
   288  		return errors.Trace(err)
   289  	}
   290  	return nil
   291  }
   292  
   293  func (p *modelSummaryProcessor) fillInApplicationSummary() error {
   294  	units, closer := p.st.db().GetRawCollection(unitsC)
   295  	defer closer()
   296  	query := units.Find(bson.M{
   297  		"model-uuid": bson.M{"$in": p.modelUUIDs},
   298  		"life":       Alive,
   299  	})
   300  	query.Select(bson.M{"life": 1, "model-uuid": 1})
   301  	iter := query.Iter()
   302  	defer iter.Close()
   303  	var doc unitDoc
   304  	for iter.Next(&doc) {
   305  		if doc.Life != Alive {
   306  			continue
   307  		}
   308  		idx, ok := p.indexByUUID[doc.ModelUUID]
   309  		if !ok {
   310  			continue
   311  		}
   312  		details := &p.summaries[idx]
   313  		details.UnitCount++
   314  	}
   315  	if err := iter.Close(); err != nil {
   316  		return errors.Trace(err)
   317  	}
   318  	return nil
   319  }
   320  
   321  func (p *modelSummaryProcessor) fillInMigration() error {
   322  	// For now, we just potato the Migration information. Its a little unfortunate, but the expectation is that most
   323  	// models won't have been migrated, and thus the table is mostly empty anyway.
   324  	// It might be possible to do it differently with an aggregation and $first queries.
   325  	// $first appears to have been available since Mongo 2.4.
   326  	// Migrations is a global collection so can be accessed from any State
   327  	migrations, closer := p.st.db().GetCollection(migrationsC)
   328  	defer closer()
   329  	pipe := migrations.Pipe([]bson.M{
   330  		{"$match": bson.M{"model-uuid": bson.M{"$in": p.modelUUIDs}}},
   331  		{"$sort": bson.M{"model-uuid": 1, "attempt": -1}},
   332  		{"$group": bson.M{
   333  			"_id":   "$model-uuid",
   334  			"docid": bson.M{"$first": "$_id"},
   335  			// TODO(jam): 2017-11-27 Do we need all of these, do we care about anything but doc _id?
   336  			"attempt":           bson.M{"$first": "$attempt"},
   337  			"initiated-by":      bson.M{"$first": "$initiated-by"},
   338  			"target-controller": bson.M{"$first": "$target-controller"},
   339  			"target-addrs":      bson.M{"$first": "$target-addrs"},
   340  			"target-cacert":     bson.M{"$first": "$target-cacert"},
   341  			"target-entity":     bson.M{"$first": "$target-entity"},
   342  		}},
   343  		// We grouped on model-uuid, but need to project back to normal fields
   344  		{"$project": bson.M{
   345  			"_id":               "$docid",
   346  			"model-uuid":        "$_id",
   347  			"attempt":           1,
   348  			"initiated-by":      1,
   349  			"target-controller": 1,
   350  			"target-addrs":      1,
   351  			"target-cacert":     1,
   352  			"target-entity":     1,
   353  		}},
   354  	})
   355  	pipe.Batch(100)
   356  	var iter mongo.Iterator = pipe.Iter()
   357  	defer iter.Close()
   358  	modelMigDocs := make(map[string]modelMigDoc)
   359  	docIds := make([]string, 0)
   360  	var doc modelMigDoc
   361  	for iter.Next(&doc) {
   362  		if _, ok := p.indexByUUID[doc.ModelUUID]; !ok {
   363  			continue
   364  		}
   365  		modelMigDocs[doc.Id] = doc
   366  		docIds = append(docIds, doc.Id)
   367  	}
   368  	if err := iter.Close(); err != nil {
   369  		return errors.Trace(err)
   370  	}
   371  	// Now look up the status documents and join them together
   372  	migStatus, closer2 := p.st.db().GetCollection(migrationsStatusC)
   373  	defer closer2()
   374  	query := migStatus.Find(bson.M{"_id": bson.M{"$in": docIds}})
   375  	query.Batch(100)
   376  	iter = query.Iter()
   377  	defer iter.Close()
   378  	var statusDoc modelMigStatusDoc
   379  	for iter.Next(&statusDoc) {
   380  		doc, ok := modelMigDocs[statusDoc.Id]
   381  		if !ok {
   382  			continue
   383  		}
   384  		idx, ok := p.indexByUUID[doc.ModelUUID]
   385  		if !ok {
   386  			continue
   387  		}
   388  		details := &p.summaries[idx]
   389  		// TODO(jam): 2017-11-27 Can we make modelMigration *not* accept a State object so that we know we won't potato
   390  		// more stuff in the future?
   391  		details.Migration = &modelMigration{
   392  			doc:       doc,
   393  			statusDoc: statusDoc,
   394  			st:        p.st,
   395  		}
   396  	}
   397  	if err := iter.Close(); err != nil {
   398  		return errors.Trace(err)
   399  	}
   400  	return nil
   401  }
   402  
   403  // fillInJustUser fills in the Access rights for this user on every model (but not other users).
   404  // We will use this information later to determine whether it is reasonable to include the information from other models.
   405  func (p *modelSummaryProcessor) fillInJustUser() error {
   406  	// Note: Even for Superuser we track the individual Access for each model.
   407  	// TODO(jam): 2017-11-27 ensure that we have appropriate indexes so that users that aren't "admin" and only see a couple
   408  	// models don't do a COLLSCAN on the table.
   409  	username := strings.ToLower(p.user.Name())
   410  	var permissionIds []string
   411  	for _, modelUUID := range p.modelUUIDs {
   412  		permId := permissionID(modelKey(modelUUID), userGlobalKey(username))
   413  		permissionIds = append(permissionIds, permId)
   414  	}
   415  	if err := p.fillInPermissions(permissionIds); err != nil {
   416  		return errors.Trace(err)
   417  	}
   418  	return nil
   419  }
   420  
   421  func (p *modelSummaryProcessor) fillInLastAccess() error {
   422  	// We fill in the last access only for the requesting user.
   423  	lastAccessIds := make([]string, len(p.modelUUIDs))
   424  	suffix := ":" + strings.ToLower(p.user.Name())
   425  	for i, modelUUID := range p.modelUUIDs {
   426  		lastAccessIds[i] = modelUUID + suffix
   427  	}
   428  	lastConnections, closer := p.st.db().GetRawCollection(modelUserLastConnectionC)
   429  	defer closer()
   430  	query := lastConnections.Find(bson.M{"_id": bson.M{"$in": lastAccessIds}})
   431  	query.Select(bson.M{"_id": 1, "model-uuid": 1, "last-connection": 1})
   432  	query.Batch(100)
   433  	iter := query.Iter()
   434  	defer iter.Close()
   435  	var connInfo modelUserLastConnectionDoc
   436  	for iter.Next(&connInfo) {
   437  		idx, ok := p.indexByUUID[connInfo.ModelUUID]
   438  		if !ok {
   439  			continue
   440  		}
   441  		details := &p.summaries[idx]
   442  		t := connInfo.LastConnection
   443  		details.UserLastConnection = &t
   444  	}
   445  	if err := iter.Close(); err != nil {
   446  		return errors.Trace(err)
   447  	}
   448  	// Note: We don't care if there are lastAccessIds that are not found, because its possible the user never
   449  	// actually connected to a model they were given access to.
   450  	return nil
   451  }
   452  
   453  // fillInStatusBasedOnCloudCredentialValidity fills in the Status on every model (if credential is invalid).
   454  func (p *modelSummaryProcessor) fillInStatusBasedOnCloudCredentialValidity() error {
   455  	credentialModels := map[names.CloudCredentialTag][]string{}
   456  	for _, model := range p.summaries {
   457  		if model.CloudCredentialTag == "" {
   458  			continue
   459  		}
   460  		tag, err := names.ParseCloudCredentialTag(model.CloudCredentialTag)
   461  		if err != nil {
   462  			logger.Warningf("could not parse cloud credential tag %v for model%v: %v", model.CloudCredentialTag, model.UUID, err)
   463  			// Don't stop the rest of the models
   464  			continue
   465  		}
   466  		summaries, ok := credentialModels[tag]
   467  		if !ok {
   468  			summaries = []string{}
   469  		}
   470  		credentialModels[tag] = append(summaries, model.UUID)
   471  	}
   472  	if len(credentialModels) != 0 {
   473  		if err := p.substituteModelStatusForInvalidCredentials(credentialModels); err != nil {
   474  			return errors.Trace(err)
   475  		}
   476  	}
   477  	return nil
   478  }
   479  
   480  func (p *modelSummaryProcessor) substituteModelStatusForInvalidCredentials(credentials map[names.CloudCredentialTag][]string) error {
   481  	var ids []string
   482  	for tag := range credentials {
   483  		ids = append(ids, cloudCredentialDocID(tag))
   484  	}
   485  	// cloudCredentialsC is a global collection, so can be accessed from any state
   486  	perms, closer := p.st.db().GetCollection(cloudCredentialsC)
   487  	defer closer()
   488  	query := perms.Find(bson.M{"_id": bson.M{"$in": ids}})
   489  	iter := query.Iter()
   490  	defer iter.Close()
   491  
   492  	var doc cloudCredentialDoc
   493  	for iter.Next(&doc) {
   494  		if doc.Invalid {
   495  			tag, err := doc.cloudCredentialTag()
   496  			if err != nil {
   497  				logger.Warningf("could not get cloud credential tag %v: %v", doc.DocID, err)
   498  				// Don't stop the rest of the models
   499  				continue
   500  			}
   501  			for _, uuid := range credentials[tag] {
   502  				idx, ok := p.indexByUUID[uuid]
   503  				if !ok {
   504  					continue
   505  				}
   506  				details := &p.summaries[idx]
   507  				details.Status = modelStatusInvalidCredential(doc.InvalidReason)
   508  			}
   509  		}
   510  	}
   511  	if err := iter.Close(); err != nil {
   512  		return errors.Trace(err)
   513  	}
   514  	return nil
   515  }