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 }