github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/enableha.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "fmt" 8 "strconv" 9 10 "github.com/juju/errors" 11 "github.com/juju/mgo/v3" 12 "github.com/juju/mgo/v3/bson" 13 "github.com/juju/mgo/v3/txn" 14 "github.com/juju/names/v5" 15 "github.com/juju/replicaset/v3" 16 jujutxn "github.com/juju/txn/v3" 17 "github.com/juju/utils/v3" 18 "github.com/juju/version/v2" 19 20 "github.com/juju/juju/core/constraints" 21 "github.com/juju/juju/core/controller" 22 "github.com/juju/juju/core/instance" 23 "github.com/juju/juju/environs/bootstrap" 24 "github.com/juju/juju/mongo" 25 stateerrors "github.com/juju/juju/state/errors" 26 "github.com/juju/juju/tools" 27 ) 28 29 func isController(mdoc *machineDoc) bool { 30 for _, j := range mdoc.Jobs { 31 if j == JobManageModel { 32 return true 33 } 34 } 35 return false 36 } 37 38 var errControllerNotAllowed = errors.New("controller jobs specified but not allowed") 39 40 func (st *State) getVotingControllerCount() (int, error) { 41 controllerNodesColl, closer := st.db().GetCollection(controllerNodesC) 42 defer closer() 43 44 return controllerNodesColl.Find(bson.M{"wants-vote": true}).Count() 45 } 46 47 // maintainControllersOps returns a set of operations that will maintain 48 // the controller information when controllers with the given ids 49 // are added. If bootstrapOnly is true, there can be only one id = 0; 50 // (this is a special case to allow adding the bootstrap node). 51 func (st *State) maintainControllersOps(newIds []string, bootstrapOnly bool) ([]txn.Op, error) { 52 if len(newIds) == 0 { 53 return nil, nil 54 } 55 currentControllerIds, err := st.ControllerIds() 56 if err != nil { 57 return nil, errors.Annotate(err, "cannot get controller info") 58 } 59 if bootstrapOnly { 60 // Allow bootstrap machine only. 61 if len(newIds) != 1 || newIds[0] != "0" { 62 return nil, errControllerNotAllowed 63 } 64 if len(currentControllerIds) > 0 { 65 return nil, errors.New("controllers already exist") 66 } 67 } 68 ops := []txn.Op{{ 69 C: controllersC, 70 Id: modelGlobalKey, 71 Assert: bson.D{ 72 {"controller-ids", bson.D{{"$size", len(currentControllerIds)}}}, 73 }, 74 Update: bson.D{ 75 {"$addToSet", 76 bson.D{ 77 {"controller-ids", bson.D{{"$each", newIds}}}, 78 }, 79 }, 80 }, 81 }} 82 return ops, nil 83 } 84 85 // EnableHA adds controller machines as necessary to make 86 // the number of live controllers equal to numControllers. The given 87 // constraints and series will be attached to any new machines. 88 // If placement is not empty, any new machines which may be required are started 89 // according to the specified placement directives until the placement list is 90 // exhausted; thereafter any new machines are started according to the constraints and series. 91 // MachineID is the id of the machine where the apiserver is running. 92 func (st *State) EnableHA( 93 numControllers int, cons constraints.Value, base Base, placement []string, 94 ) (ControllersChanges, error) { 95 96 if numControllers < 0 || (numControllers != 0 && numControllers%2 != 1) { 97 return ControllersChanges{}, errors.New("number of controllers must be odd and non-negative") 98 } 99 if numControllers > controller.MaxPeers { 100 return ControllersChanges{}, errors.Errorf("controller count is too large (allowed %d)", controller.MaxPeers) 101 } 102 var change ControllersChanges 103 buildTxn := func(attempt int) ([]txn.Op, error) { 104 desiredControllerCount := numControllers 105 votingCount, err := st.getVotingControllerCount() 106 if err != nil { 107 return nil, errors.Trace(err) 108 } 109 if desiredControllerCount == 0 { 110 // Make sure we go to add odd number of desired voters. Even if HA was currently at 2 desired voters 111 desiredControllerCount = votingCount + (votingCount+1)%2 112 if desiredControllerCount <= 1 { 113 desiredControllerCount = 3 114 } 115 } 116 if votingCount > desiredControllerCount { 117 return nil, errors.New("cannot remove controllers with enable-ha, use remove-machine and chose the controller(s) to remove") 118 } 119 120 controllerIds, err := st.ControllerIds() 121 if err != nil { 122 return nil, errors.Trace(err) 123 } 124 intent, err := st.enableHAIntentions(controllerIds, placement) 125 if err != nil { 126 return nil, err 127 } 128 voteCount := 0 129 for _, m := range intent.maintain { 130 if m.WantsVote() { 131 voteCount++ 132 } 133 } 134 if voteCount == desiredControllerCount { 135 return nil, jujutxn.ErrNoOperations 136 } 137 138 if n := desiredControllerCount - voteCount; n < len(intent.convert) { 139 intent.convert = intent.convert[:n] 140 } 141 voteCount += len(intent.convert) 142 143 intent.newCount = desiredControllerCount - voteCount 144 145 logger.Infof("%d new machines; converting %v", intent.newCount, intent.convert) 146 147 var ops []txn.Op 148 ops, change, err = st.enableHAIntentionOps(intent, cons, base) 149 return ops, err 150 } 151 if err := st.db().Run(buildTxn); err != nil { 152 err = errors.Annotatef(err, "failed to enable HA with %d controllers", numControllers) 153 return ControllersChanges{}, err 154 } 155 return change, nil 156 } 157 158 // Change in controllers after the ensure availability txn has committed. 159 type ControllersChanges struct { 160 Added []string 161 Removed []string 162 Maintained []string 163 Converted []string 164 } 165 166 // enableHAIntentionOps returns operations to fulfil the desired intent. 167 func (st *State) enableHAIntentionOps( 168 intent *enableHAIntent, 169 cons constraints.Value, 170 base Base, 171 ) ([]txn.Op, ControllersChanges, error) { 172 var ops []txn.Op 173 var change ControllersChanges 174 175 // TODO(wallyworld) - only need until we transition away from enable-ha 176 controllerApp, err := st.Application(bootstrap.ControllerApplicationName) 177 if err != nil && !errors.IsNotFound(err) { 178 return nil, ControllersChanges{}, errors.Trace(err) 179 } 180 181 for _, m := range intent.convert { 182 ops = append(ops, convertControllerOps(m)...) 183 change.Converted = append(change.Converted, m.Id()) 184 // Add a controller charm unit to the promoted machine. 185 if controllerApp != nil { 186 unitName, unitOps, err := controllerApp.addUnitOps("", AddUnitParams{machineID: m.Id()}, nil) 187 if err != nil { 188 return nil, ControllersChanges{}, errors.Trace(err) 189 } 190 ops = append(ops, unitOps...) 191 addToMachineOp := txn.Op{ 192 C: machinesC, 193 Id: m.doc.DocID, 194 Assert: txn.DocExists, 195 Update: bson.D{{"$addToSet", bson.D{{"principals", unitName}}}, {"$set", bson.D{{"clean", false}}}}, 196 } 197 ops = append(ops, addToMachineOp) 198 } 199 } 200 201 // Use any placement directives that have been provided when adding new 202 // machines, until the directives have been all used up. 203 // Ignore constraints for provided machines. 204 placementCount := 0 205 getPlacementConstraints := func() (string, constraints.Value) { 206 if placementCount >= len(intent.placement) { 207 return "", cons 208 } 209 result := intent.placement[placementCount] 210 placementCount++ 211 return result, constraints.Value{} 212 } 213 214 var controllerIds []string 215 for i := 0; i < intent.newCount; i++ { 216 placement, cons := getPlacementConstraints() 217 template := MachineTemplate{ 218 Base: base, 219 Jobs: []MachineJob{ 220 JobHostUnits, 221 JobManageModel, 222 }, 223 Constraints: cons, 224 Placement: placement, 225 } 226 // Set up the new controller to have a controller charm unit. 227 // The unit itself is created below. 228 var controllerUnitName string 229 if controllerApp != nil { 230 controllerUnitName, err = controllerApp.newUnitName() 231 if err != nil { 232 return nil, ControllersChanges{}, errors.Trace(err) 233 } 234 template.Dirty = true 235 template.principals = []string{controllerUnitName} 236 } 237 mdoc, addOps, err := st.addMachineOps(template) 238 if err != nil { 239 return nil, ControllersChanges{}, errors.Trace(err) 240 } 241 if isController(mdoc) { 242 controllerIds = append(controllerIds, mdoc.Id) 243 } 244 ops = append(ops, addOps...) 245 change.Added = append(change.Added, mdoc.Id) 246 if controllerApp != nil { 247 _, unitOps, err := controllerApp.addUnitOps("", AddUnitParams{ 248 UnitName: &controllerUnitName, 249 machineID: mdoc.Id, 250 }, nil) 251 if err != nil { 252 return nil, ControllersChanges{}, errors.Trace(err) 253 } 254 ops = append(ops, unitOps...) 255 } 256 } 257 258 for _, m := range intent.maintain { 259 change.Maintained = append(change.Maintained, m.Id()) 260 } 261 ssOps, err := st.maintainControllersOps(controllerIds, false) 262 if err != nil { 263 return nil, ControllersChanges{}, errors.Annotate(err, "cannot prepare machine add operations") 264 } 265 ops = append(ops, ssOps...) 266 return ops, change, nil 267 } 268 269 type enableHAIntent struct { 270 newCount int 271 placement []string 272 273 maintain []ControllerNode 274 convert []*Machine 275 } 276 277 // enableHAIntentions returns what we would like 278 // to do to maintain the availability of the existing servers 279 // mentioned in the given info, including: 280 // 281 // gathering available, non-voting machines that may be promoted; 282 func (st *State) enableHAIntentions(controllerIds []string, placement []string) (*enableHAIntent, error) { 283 var intent enableHAIntent 284 for _, s := range placement { 285 // TODO(natefinch): Unscoped placements can end up here, though they 286 // should not. We should fix up the CLI to always add a scope, 287 // then we can remove the need to deal with unscoped placements. 288 289 // Append unscoped placements to the intentions. 290 // These will be used if/when adding new controllers is required. 291 // These placements will be interpreted as availability zones. 292 p, err := instance.ParsePlacement(s) 293 if err == instance.ErrPlacementScopeMissing { 294 intent.placement = append(intent.placement, s) 295 continue 296 } 297 298 // Placements for machines are "consumed" by appending such machines as 299 // candidates for promotion to controllers. 300 if err == nil && p.Scope == instance.MachineScope { 301 if names.IsContainerMachine(p.Directive) { 302 return nil, errors.New("container placement directives not supported") 303 } 304 305 m, err := st.Machine(p.Directive) 306 if err != nil { 307 return nil, errors.Annotatef(err, "can't find machine for placement directive %q", s) 308 } 309 if m.IsManager() { 310 return nil, errors.Errorf("machine for placement directive %q is already a controller", s) 311 } 312 intent.convert = append(intent.convert, m) 313 continue 314 } 315 return nil, errors.Errorf("unsupported placement directive %q", s) 316 } 317 318 for _, id := range controllerIds { 319 node, err := st.ControllerNode(id) 320 if err != nil { 321 return nil, err 322 } 323 logger.Infof("controller %q, wants vote %v, has vote %v", id, node.WantsVote(), node.HasVote()) 324 if node.WantsVote() { 325 intent.maintain = append(intent.maintain, node) 326 } 327 } 328 logger.Infof("initial intentions: maintain %v; convert: %v", 329 intent.maintain, intent.convert) 330 return &intent, nil 331 } 332 333 func convertControllerOps(m *Machine) []txn.Op { 334 return []txn.Op{{ 335 C: machinesC, 336 Id: m.doc.DocID, 337 Update: bson.D{ 338 {"$addToSet", bson.D{{"jobs", JobManageModel}}}, 339 }, 340 Assert: bson.D{{"jobs", bson.D{{"$nin", []MachineJob{JobManageModel}}}}}, 341 }, { 342 C: controllersC, 343 Id: modelGlobalKey, 344 Update: bson.D{ 345 {"$addToSet", bson.D{ 346 {"controller-ids", m.doc.Id}, 347 }}, 348 }, 349 }, 350 addControllerNodeOp(m.st, m.doc.Id, false), 351 } 352 } 353 354 func (st *State) getControllerNodeDoc(id string) (*controllerNodeDoc, error) { 355 controllerNodesColl, closer := st.db().GetCollection(controllerNodesC) 356 defer closer() 357 358 cdoc := &controllerNodeDoc{} 359 docId := st.docID(id) 360 err := controllerNodesColl.FindId(docId).One(cdoc) 361 362 switch err { 363 case nil: 364 return cdoc, nil 365 case mgo.ErrNotFound: 366 return nil, errors.NotFoundf("controller node %s", id) 367 default: 368 return nil, errors.Annotatef(err, "cannot get controller node %s", id) 369 } 370 } 371 372 func (st *State) removeControllerReferenceOps(cid string, controllerIds []string) []txn.Op { 373 return []txn.Op{{ 374 C: machinesC, 375 Id: st.docID(cid), 376 Update: bson.D{ 377 {"$pull", bson.D{{"jobs", JobManageModel}}}, 378 }, 379 }, { 380 C: controllersC, 381 Id: modelGlobalKey, 382 Assert: bson.D{{"controller-ids", controllerIds}}, 383 Update: bson.D{{"$pull", bson.D{{"controller-ids", cid}}}}, 384 }, { 385 C: controllerNodesC, 386 Id: st.docID(cid), 387 Assert: bson.D{ 388 {"wants-vote", false}, 389 {"has-vote", false}, 390 }, 391 }} 392 } 393 394 // ControllerNode represents an instance of a HA controller. 395 type ControllerNode interface { 396 Id() string 397 Tag() names.Tag 398 Refresh() error 399 WantsVote() bool 400 HasVote() bool 401 SetHasVote(hasVote bool) error 402 Watch() NotifyWatcher 403 SetMongoPassword(password string) error 404 } 405 406 // ControllerIds returns the ids of the controller nodes. 407 func (st *State) ControllerIds() ([]string, error) { 408 controllerInfo, err := st.ControllerInfo() 409 if err != nil { 410 return nil, errors.Annotatef(err, "reading controller info") 411 } 412 return controllerInfo.ControllerIds, nil 413 } 414 415 // SafeControllerIds returns the ids of the controller nodes 416 // by looking for the newer "controller-ids" attribute but falling 417 // back to the legacy "machineids" if needed. 418 // This method is only used when preparing for upgrades since 2.6 419 // or earlier used "machineids". 420 func (st *State) SafeControllerIds() ([]string, error) { 421 session := st.session.Copy() 422 defer session.Close() 423 424 db := session.DB(jujuDB) 425 controllers := db.C(controllersC) 426 427 var info bson.M 428 err := controllers.Find(bson.D{{"_id", modelGlobalKey}}).One(&info) 429 if err == mgo.ErrNotFound { 430 return nil, errors.NotFoundf("controllers document") 431 } 432 if err != nil { 433 return nil, errors.Annotatef(err, "cannot get controllers document") 434 } 435 var ids []interface{} 436 var ok bool 437 if ids, ok = info["controller-ids"].([]interface{}); !ok { 438 ids, ok = info["machineids"].([]interface{}) 439 } 440 if !ok { 441 return nil, nil 442 } 443 result := make([]string, len(ids)) 444 for i, id := range ids { 445 result[i] = id.(string) 446 } 447 return result, nil 448 } 449 450 // ControllerNode returns the controller node with the given id. 451 func (st *State) ControllerNode(id string) (ControllerNode, error) { 452 cdoc, err := st.getControllerNodeDoc(id) 453 if err != nil { 454 return nil, errors.Trace(err) 455 } 456 return &controllerNode{*cdoc, st}, nil 457 } 458 459 // AddControllerNode creates a new controller node. 460 func (st *State) AddControllerNode() (*controllerNode, error) { 461 seq, err := sequence(st, "controller") 462 if err != nil { 463 return nil, err 464 } 465 id := strconv.Itoa(seq) 466 doc := controllerNodeDoc{ 467 DocID: st.docID(id), 468 WantsVote: true, 469 } 470 471 currentInfo, err := st.ControllerInfo() 472 if err != nil && !errors.IsNotFound(err) { 473 return nil, errors.Trace(err) 474 } 475 ops := []txn.Op{addControllerNodeOp(st, id, false)} 476 ssOps, err := st.maintainControllersOps([]string{id}, currentInfo == nil) 477 if err != nil { 478 return nil, errors.Trace(err) 479 } 480 ops = append(ops, ssOps...) 481 if err := st.db().RunTransaction(ops); err != nil { 482 return nil, errors.Annotate(err, "cannot add controller node") 483 } 484 return &controllerNode{doc: doc, st: st}, nil 485 } 486 487 // HAPrimaryMachine returns machine tag for a controller machine 488 // that has a mongo instance that is primary in replicaset. 489 func (st *State) HAPrimaryMachine() (names.MachineTag, error) { 490 nodeID := -1 491 // Current status of replicaset contains node state. 492 // Here we determine node id of the primary node. 493 replicaStatus, err := replicaset.CurrentStatus(st.MongoSession()) 494 if err != nil { 495 return names.MachineTag{}, errors.Trace(err) 496 } 497 for _, m := range replicaStatus.Members { 498 if m.State == replicaset.PrimaryState { 499 nodeID = m.Id 500 } 501 } 502 if nodeID == -1 { 503 return names.MachineTag{}, errors.NotFoundf("HA primary machine") 504 } 505 506 // Current members collection of replicaset contains additional 507 // information for the nodes, including machine IDs. 508 ms, err := replicaset.CurrentMembers(st.MongoSession()) 509 if err != nil { 510 return names.MachineTag{}, errors.Trace(err) 511 } 512 for _, m := range ms { 513 if m.Id == nodeID { 514 if machineID, ok := m.Tags["juju-machine-id"]; ok { 515 return names.NewMachineTag(machineID), nil 516 } 517 } 518 } 519 return names.MachineTag{}, errors.NotFoundf("HA primary machine") 520 } 521 522 // ControllerNodes returns all the controller nodes. 523 func (st *State) ControllerNodes() ([]*controllerNode, error) { 524 controllerNodesColl, closer := st.db().GetCollection(controllerNodesC) 525 defer closer() 526 527 var docs []controllerNodeDoc 528 err := controllerNodesColl.Find(nil).All(&docs) 529 if err != nil { 530 return nil, errors.Trace(err) 531 } 532 result := make([]*controllerNode, len(docs)) 533 for i, doc := range docs { 534 result[i] = &controllerNode{doc, st} 535 } 536 return result, nil 537 } 538 539 type controllerNode struct { 540 doc controllerNodeDoc 541 st *State 542 } 543 544 type controllerNodeDoc struct { 545 DocID string `bson:"_id"` 546 HasVote bool `bson:"has-vote"` 547 WantsVote bool `bson:"wants-vote"` 548 PasswordHash string `bson:"password-hash"` 549 AgentVersion *tools.Tools `bson:"agent-version,omitempty"` 550 } 551 552 // Id returns the controller id. 553 func (c *controllerNode) Id() string { 554 return c.st.localID(c.doc.DocID) 555 } 556 557 // Life is always alive for controller nodes. 558 // This API is used when a controller agent attempts 559 // to connect to a controller, currently only for CAAS. 560 // IAAS models still connect to machines as controllers. 561 // TODO(controlleragent) - model life on controller nodes 562 func (c *controllerNode) Life() Life { 563 return Alive 564 } 565 566 // IsManager is always true for controller nodes. 567 func (c *controllerNode) IsManager() bool { 568 return true 569 } 570 571 // Tag returns the controller tag. 572 func (c *controllerNode) Tag() names.Tag { 573 return names.NewControllerAgentTag(c.Id()) 574 } 575 576 func (c *controllerNode) SetMongoPassword(password string) error { 577 return mongo.SetAdminMongoPassword(c.st.session, c.Tag().String(), password) 578 } 579 580 // SetPassword implements Authenticator. 581 func (c *controllerNode) SetPassword(password string) error { 582 if len(password) < utils.MinAgentPasswordLength { 583 return errors.Errorf("password is only %d bytes long, and is not a valid Agent password", len(password)) 584 } 585 passwordHash := utils.AgentPasswordHash(password) 586 ops := []txn.Op{{ 587 C: controllerNodesC, 588 Id: c.doc.DocID, 589 Update: bson.D{{"$set", bson.D{{"password-hash", passwordHash}}}}, 590 }} 591 err := c.st.db().RunTransaction(ops) 592 if err != nil { 593 return fmt.Errorf("cannot set password of controller node %q: %v", c.Id(), err) 594 } 595 c.doc.PasswordHash = passwordHash 596 return nil 597 } 598 599 // PasswordValid implements Authenticator. 600 func (c *controllerNode) PasswordValid(password string) bool { 601 agentHash := utils.AgentPasswordHash(password) 602 return agentHash == c.doc.PasswordHash 603 } 604 605 func (c *controllerNode) AgentTools() (*tools.Tools, error) { 606 if c.doc.AgentVersion == nil { 607 return nil, errors.NotFoundf("agent binaries for controller %v", c) 608 } 609 agentVersion := *c.doc.AgentVersion 610 return &agentVersion, nil 611 } 612 613 func (c *controllerNode) SetAgentVersion(v version.Binary) (err error) { 614 defer errors.DeferredAnnotatef(&err, "cannot set agent version for controller %s", c.Id()) 615 if err := checkVersionValidity(v); err != nil { 616 return err 617 } 618 binaryVersion := &tools.Tools{Version: v} 619 ops := []txn.Op{{ 620 C: controllerNodesC, 621 Id: c.doc.DocID, 622 Assert: notDeadDoc, 623 Update: bson.D{{"$set", bson.D{{"agent-version", binaryVersion}}}}, 624 }} 625 // A "raw" transaction is needed here because this function gets 626 // called before database migrations have run so we don't 627 // necessarily want the model UUID added to the id. 628 if err := c.st.runRawTransaction(ops); err != nil { 629 return errors.Trace(err) 630 } 631 c.doc.AgentVersion = binaryVersion 632 return nil 633 } 634 635 // Refresh reloads the controller state. 636 func (c *controllerNode) Refresh() error { 637 id := c.st.localID(c.doc.DocID) 638 cdoc, err := c.st.getControllerNodeDoc(id) 639 if err != nil { 640 if errors.IsNotFound(err) { 641 return err 642 } 643 return errors.Annotatef(err, "cannot refresh controller node %v", c) 644 } 645 c.doc = *cdoc 646 return nil 647 } 648 649 // Watch returns a watcher for observing changes to a node. 650 func (c *controllerNode) Watch() NotifyWatcher { 651 return newEntityWatcher(c.st, controllerNodesC, c.doc.DocID) 652 } 653 654 // WantsVote reports whether the controller 655 // that wants to take part in peer voting. 656 func (c *controllerNode) WantsVote() bool { 657 return c.doc.WantsVote 658 } 659 660 // HasVote reports whether that controller is currently a voting 661 // member of the replica set. 662 func (c *controllerNode) HasVote() bool { 663 return c.doc.HasVote 664 } 665 666 // SetHasVote sets whether the controller is currently a voting 667 // member of the replica set. It should only be called 668 // from the worker that maintains the replica set. 669 func (c *controllerNode) SetHasVote(hasVote bool) error { 670 buildTxn := func(attempt int) ([]txn.Op, error) { 671 if attempt > 0 { 672 if err := c.Refresh(); err != nil { 673 return nil, err 674 } 675 } 676 677 var ops []txn.Op 678 // Check the host entity life (machine on IAAS models). 679 host, err := c.st.Machine(c.Id()) 680 if err != nil && !errors.IsNotFound(err) { 681 return nil, errors.Trace(err) 682 } 683 if err == nil { 684 if host.Life() == Dead { 685 return nil, stateerrors.ErrDead 686 } 687 ops = []txn.Op{{ 688 C: machinesC, 689 Id: host.doc.DocID, 690 Assert: notDeadDoc, 691 }} 692 } 693 ops = append(ops, c.setHasVoteOps(hasVote)...) 694 return ops, nil 695 } 696 if err := c.st.db().Run(buildTxn); err != nil { 697 return errors.Trace(err) 698 } 699 return nil 700 } 701 702 func (c *controllerNode) setHasVoteOps(hasVote bool) []txn.Op { 703 return []txn.Op{{ 704 C: controllerNodesC, 705 Id: c.doc.DocID, 706 Assert: txn.DocExists, 707 Update: bson.D{{"$set", bson.D{{"has-vote", hasVote}}}}, 708 }} 709 } 710 711 func setControllerWantsVoteOp(st *State, id string, wantsVote bool) txn.Op { 712 return txn.Op{ 713 C: controllerNodesC, 714 Id: st.docID(id), 715 Assert: txn.DocExists, 716 Update: bson.D{{"$set", bson.D{{"wants-vote", wantsVote}}}}, 717 } 718 } 719 720 type controllerReference interface { 721 Id() string 722 Refresh() error 723 WantsVote() bool 724 HasVote() bool 725 } 726 727 // RemoveControllerReference will unregister Controller from being part of the set of Controllers. 728 // It must not have or want to vote, and it must not be the last controller. 729 func (st *State) RemoveControllerReference(c controllerReference) error { 730 logger.Infof("removing controller machine %q", c.Id()) 731 buildTxn := func(attempt int) ([]txn.Op, error) { 732 if attempt != 0 { 733 // Something changed, make sure we're still up to date 734 if err := c.Refresh(); err != nil { 735 return nil, errors.Trace(err) 736 } 737 } 738 if c.WantsVote() { 739 return nil, errors.Errorf("controller %s cannot be removed as it still wants to vote", c.Id()) 740 } 741 if c.HasVote() { 742 return nil, errors.Errorf("controller %s cannot be removed as it still has a vote", c.Id()) 743 } 744 controllerIds, err := st.ControllerIds() 745 if err != nil { 746 return nil, errors.Trace(err) 747 } 748 if len(controllerIds) <= 1 { 749 return nil, errors.Errorf("controller %s cannot be removed as it is the last controller", c.Id()) 750 } 751 return st.removeControllerReferenceOps(c.Id(), controllerIds), nil 752 } 753 if err := st.db().Run(buildTxn); err != nil { 754 return errors.Trace(err) 755 } 756 return nil 757 } 758 759 func addControllerNodeOp(mb modelBackend, id string, hasVote bool) txn.Op { 760 doc := &controllerNodeDoc{ 761 DocID: mb.docID(id), 762 HasVote: hasVote, 763 WantsVote: true, 764 } 765 return txn.Op{ 766 C: controllerNodesC, 767 Id: doc.DocID, 768 Assert: txn.DocMissing, 769 Insert: doc, 770 } 771 } 772 773 func removeControllerNodeOp(mb modelBackend, id string) txn.Op { 774 return txn.Op{ 775 C: controllerNodesC, 776 Id: mb.docID(id), 777 Remove: true, 778 } 779 }