github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/machine_upgradeseries.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 "sort" 9 "time" 10 11 "github.com/juju/collections/set" 12 "github.com/juju/errors" 13 "github.com/juju/mgo/v3" 14 "github.com/juju/mgo/v3/bson" 15 "github.com/juju/mgo/v3/txn" 16 jujutxn "github.com/juju/txn/v3" 17 18 "github.com/juju/juju/core/model" 19 stateerrors "github.com/juju/juju/state/errors" 20 ) 21 22 // upgradeSeriesLockDoc holds the attributes relevant to lock a machine during a 23 // series update of a machine 24 type upgradeSeriesLockDoc struct { 25 Id string `bson:"machine-id"` 26 ToBase string `bson:"to-base"` 27 FromBase string `bson:"from-base"` 28 MachineStatus model.UpgradeSeriesStatus `bson:"machine-status"` 29 Messages []UpgradeSeriesMessage `bson:"messages"` 30 TimeStamp time.Time `bson:"timestamp"` 31 UnitStatuses map[string]UpgradeSeriesUnitStatus `bson:"unit-statuses"` 32 } 33 34 type UpgradeSeriesUnitStatus struct { 35 Status model.UpgradeSeriesStatus 36 Timestamp time.Time 37 } 38 39 // UpgradeSeriesMessage holds a message detailing why the upgrade series status 40 // was updated. This format of this message should be a single sentence similar 41 // to logging message. The string is accompanied by a timestamp and a boolean 42 // value indicating whether or not the message has been observed by a client. 43 type UpgradeSeriesMessage struct { 44 Message string `bson:"message"` 45 Timestamp time.Time `bson:"timestamp"` 46 Seen bool `bson:"seen"` 47 } 48 49 func newUpgradeSeriesMessage(name string, message string, timestamp time.Time) UpgradeSeriesMessage { 50 taggedMessage := fmt.Sprintf("%s %s", name, message) 51 return UpgradeSeriesMessage{ 52 Message: taggedMessage, 53 Timestamp: timestamp, 54 Seen: false, 55 } 56 } 57 58 // CreateUpgradeSeriesLock create a prepare lock for series upgrade. If 59 // this item exists in the database for a given machine it indicates that a 60 // machine's operating system is being upgraded from one series to another; 61 // for example, from xenial to bionic. 62 func (m *Machine) CreateUpgradeSeriesLock(unitNames []string, toBase Base) error { 63 buildTxn := func(attempt int) ([]txn.Op, error) { 64 if attempt > 0 { 65 if err := m.Refresh(); err != nil { 66 return nil, errors.Trace(err) 67 } 68 } 69 locked, err := m.IsLockedForSeriesUpgrade() 70 if err != nil { 71 return nil, errors.Trace(err) 72 } 73 if locked { 74 return nil, errors.AlreadyExistsf("upgrade series lock for machine %q", m) 75 } 76 if err = m.isStillAlive(); err != nil { 77 return nil, errors.Trace(err) 78 } 79 // Exit early if the Machine base doesn't need to change. 80 fromBase := m.Base().String() 81 if fromBase == toBase.String() { 82 return nil, errors.Trace(errors.Errorf("machine %s already at base %s", m.Id(), toBase.String())) 83 } 84 // If the units have changed, the verification is no longer valid. 85 changed, err := m.unitsHaveChanged(unitNames) 86 if err != nil { 87 return nil, errors.Trace(err) 88 } 89 if changed { 90 return nil, errors.Errorf("Units have changed, please retry (%v)", unitNames) 91 } 92 data := m.prepareUpgradeBaseLock(unitNames, toBase) 93 return createUpgradeSeriesLockTxnOps(m.doc.Id, data), nil 94 } 95 err := m.st.db().Run(buildTxn) 96 if err != nil { 97 err = onAbort(err, stateerrors.ErrDead) 98 logger.Errorf("cannot prepare series upgrade for machine %q: %v", m, err) 99 return err 100 } 101 return nil 102 } 103 104 // IsParentLockedForSeriesUpgrade determines if a machine is a container who's 105 // parent is locked for series upgrade. 106 func (m *Machine) IsParentLockedForSeriesUpgrade() (bool, error) { 107 parentId, isContainer := m.ParentId() 108 if !isContainer { 109 return false, nil 110 } 111 112 parent, err := m.st.Machine(parentId) 113 if err != nil { 114 return false, errors.Trace(err) 115 } 116 117 locked, err := parent.IsLockedForSeriesUpgrade() 118 return locked, errors.Trace(err) 119 } 120 121 // IsLockedForSeriesUpgrade determines if a machine is locked for upgrade series. 122 func (m *Machine) IsLockedForSeriesUpgrade() (bool, error) { 123 _, err := m.getUpgradeSeriesLock() 124 if err == nil { 125 return true, nil 126 } 127 if errors.IsNotFound(err) { 128 return false, nil 129 } 130 return false, errors.Trace(err) 131 } 132 133 func (m *Machine) unitsHaveChanged(unitNames []string) (bool, error) { 134 curUnits, err := m.Units() 135 if err != nil { 136 return true, err 137 } 138 if len(curUnits) == 0 && len(unitNames) == 0 { 139 return false, nil 140 } 141 if len(curUnits) != len(unitNames) { 142 return true, nil 143 } 144 curUnitSet := set.NewStrings() 145 for _, unit := range curUnits { 146 curUnitSet.Add(unit.Name()) 147 } 148 unitNameSet := set.NewStrings(unitNames...) 149 return !unitNameSet.Difference(curUnitSet).IsEmpty(), nil 150 } 151 152 func (m *Machine) prepareUpgradeBaseLock(unitNames []string, toBase Base) *upgradeSeriesLockDoc { 153 // We want to put the unit statuses in to a prepared started state and only 154 // the machine status should be in a validate state. As we're only 155 // validating the machine and not each individual unit. 156 timestamp := bson.Now() 157 unitStatuses := make(map[string]UpgradeSeriesUnitStatus, len(unitNames)) 158 for _, name := range unitNames { 159 unitStatuses[name] = UpgradeSeriesUnitStatus{ 160 Status: model.UpgradeSeriesPrepareStarted, Timestamp: timestamp, 161 } 162 } 163 164 message := fmt.Sprintf("validation of upgrade base from %q to %q", m.Base().String(), toBase.String()) 165 updateMessage := newUpgradeSeriesMessage(m.Tag().String(), message, timestamp) 166 return &upgradeSeriesLockDoc{ 167 Id: m.Id(), 168 ToBase: toBase.String(), 169 FromBase: m.Base().String(), 170 MachineStatus: model.UpgradeSeriesValidate, 171 UnitStatuses: unitStatuses, 172 TimeStamp: timestamp, 173 Messages: []UpgradeSeriesMessage{ 174 updateMessage, 175 }, 176 } 177 } 178 179 func createUpgradeSeriesLockTxnOps(machineDocId string, data *upgradeSeriesLockDoc) []txn.Op { 180 return []txn.Op{ 181 { 182 C: machinesC, 183 Id: machineDocId, 184 Assert: isAliveDoc, 185 }, 186 { 187 C: machineUpgradeSeriesLocksC, 188 Id: machineDocId, 189 Assert: txn.DocMissing, 190 Insert: data, 191 }, 192 } 193 } 194 195 // UpgradeSeriesTarget returns the base 196 // that the machine is being upgraded to. 197 func (m *Machine) UpgradeSeriesTarget() (string, error) { 198 lock, err := m.getUpgradeSeriesLock() 199 if err != nil { 200 return "", errors.Trace(err) 201 } 202 return lock.ToBase, nil 203 } 204 205 // StartUpgradeSeriesUnitCompletion notifies units that an upgrade-machine 206 // workflow is ready for its "completion" phase. 207 func (m *Machine) StartUpgradeSeriesUnitCompletion(message string) error { 208 buildTxn := func(attempt int) ([]txn.Op, error) { 209 if attempt > 0 { 210 if err := m.Refresh(); err != nil { 211 return nil, errors.Trace(err) 212 } 213 } 214 if err := m.isStillAlive(); err != nil { 215 return nil, errors.Trace(err) 216 } 217 lock, err := m.getUpgradeSeriesLock() 218 if err != nil { 219 return nil, err 220 } 221 if lock.MachineStatus != model.UpgradeSeriesCompleteStarted { 222 return nil, fmt.Errorf("machine %q can not complete its unit, the machine has not yet been marked as completed", m.Id()) 223 } 224 timestamp := bson.Now() 225 lock.Messages = append(lock.Messages, newUpgradeSeriesMessage(m.Tag().String(), message, timestamp)) 226 lock.TimeStamp = timestamp 227 changeCount := 0 228 for unitName, us := range lock.UnitStatuses { 229 if us.Status == model.UpgradeSeriesCompleteStarted { 230 continue 231 } 232 us.Status = model.UpgradeSeriesCompleteStarted 233 us.Timestamp = timestamp 234 lock.UnitStatuses[unitName] = us 235 changeCount++ 236 } 237 if changeCount == 0 { 238 return nil, jujutxn.ErrNoOperations 239 } 240 return startUpgradeSeriesUnitCompletionTxnOps(m.doc.Id, lock), nil 241 } 242 err := m.st.db().Run(buildTxn) 243 if err != nil { 244 err = onAbort(err, stateerrors.ErrDead) 245 return err 246 } 247 return nil 248 } 249 250 func startUpgradeSeriesUnitCompletionTxnOps(machineDocID string, lock *upgradeSeriesLockDoc) []txn.Op { 251 statusField := "unit-statuses" 252 return []txn.Op{ 253 { 254 C: machinesC, 255 Id: machineDocID, 256 Assert: isAliveDoc, 257 }, 258 { 259 C: machineUpgradeSeriesLocksC, 260 Id: machineDocID, 261 Assert: bson.D{{"machine-status", model.UpgradeSeriesCompleteStarted}}, 262 Update: bson.D{{"$set", bson.D{ 263 {statusField, lock.UnitStatuses}, 264 {"timestamp", lock.TimeStamp}, 265 {"messages", lock.Messages}}}}, 266 }, 267 } 268 } 269 270 // CompleteUpgradeSeries notifies units and machines that an upgrade series is 271 // ready for its "completion" phase. 272 func (m *Machine) CompleteUpgradeSeries() error { 273 buildTxn := func(attempt int) ([]txn.Op, error) { 274 if attempt > 0 { 275 if err := m.Refresh(); err != nil { 276 return nil, errors.Trace(err) 277 } 278 } 279 if err := m.isStillAlive(); err != nil { 280 return nil, errors.Trace(err) 281 } 282 readyForCompletion, err := m.isReadyForCompletion() 283 if err != nil { 284 return nil, errors.Trace(err) 285 } 286 if !readyForCompletion { 287 return nil, fmt.Errorf("machine %q can not complete, it is either not prepared or already completed", m.Id()) 288 } 289 timestamp := bson.Now() 290 message := newUpgradeSeriesMessage(m.Tag().String(), "complete phase started", timestamp) 291 return completeUpgradeSeriesTxnOps(m.doc.Id, timestamp, message), nil 292 } 293 err := m.st.db().Run(buildTxn) 294 if err != nil { 295 err = onAbort(err, stateerrors.ErrDead) 296 return err 297 } 298 return nil 299 } 300 301 func (m *Machine) isReadyForCompletion() (bool, error) { 302 lock, err := m.getUpgradeSeriesLock() 303 if err != nil { 304 return false, err 305 } 306 return lock.MachineStatus == model.UpgradeSeriesPrepareCompleted, nil 307 } 308 309 func completeUpgradeSeriesTxnOps(machineDocID string, timestamp time.Time, message UpgradeSeriesMessage) []txn.Op { 310 return []txn.Op{ 311 { 312 C: machinesC, 313 Id: machineDocID, 314 Assert: isAliveDoc, 315 }, 316 { 317 C: machineUpgradeSeriesLocksC, 318 Id: machineDocID, 319 Assert: bson.D{{"machine-status", model.UpgradeSeriesPrepareCompleted}}, 320 Update: bson.D{ 321 {"$set", bson.D{ 322 {"machine-status", model.UpgradeSeriesCompleteStarted}, 323 {"timestamp", timestamp}, 324 }}, 325 {"$push", bson.D{{"messages", message}}}}, 326 }, 327 } 328 } 329 330 func (m *Machine) RemoveUpgradeSeriesLock() error { 331 buildTxn := func(attempt int) ([]txn.Op, error) { 332 if attempt > 0 { 333 if err := m.Refresh(); err != nil { 334 return nil, errors.Trace(err) 335 } 336 } 337 locked, err := m.IsLockedForSeriesUpgrade() 338 if err != nil { 339 return nil, errors.Trace(err) 340 } 341 if !locked { 342 return nil, jujutxn.ErrNoOperations 343 } 344 return removeUpgradeSeriesLockTxnOps(m.doc.Id), nil 345 } 346 err := m.st.db().Run(buildTxn) 347 if err != nil { 348 err = onAbort(err, stateerrors.ErrDead) 349 return err 350 } 351 return nil 352 } 353 354 func removeUpgradeSeriesLockTxnOps(machineDocId string) []txn.Op { 355 return []txn.Op{ 356 { 357 C: machineUpgradeSeriesLocksC, 358 Id: machineDocId, 359 Assert: txn.DocExists, 360 Remove: true, 361 }, 362 } 363 } 364 365 func (m *Machine) UpgradeSeriesStatus() (model.UpgradeSeriesStatus, error) { 366 coll, closer := m.st.db().GetCollection(machineUpgradeSeriesLocksC) 367 defer closer() 368 369 var lock upgradeSeriesLockDoc 370 err := coll.FindId(m.Id()).One(&lock) 371 if err == mgo.ErrNotFound { 372 return "", errors.NotFoundf("upgrade series lock for machine %q", m.Id()) 373 } 374 if err != nil { 375 return "", errors.Trace(err) 376 } 377 378 return lock.MachineStatus, errors.Trace(err) 379 } 380 381 // UpgradeSeriesUnitStatuses returns the unit statuses 382 // from the upgrade-series lock for this machine. 383 func (m *Machine) UpgradeSeriesUnitStatuses() (map[string]UpgradeSeriesUnitStatus, error) { 384 lock, err := m.getUpgradeSeriesLock() 385 if err != nil { 386 return nil, errors.Trace(err) 387 } 388 return lock.UnitStatuses, nil 389 } 390 391 // SetUpgradeSeriesUnitStatus sets the status of a series upgrade for a unit. 392 func (m *Machine) SetUpgradeSeriesUnitStatus(unitName string, status model.UpgradeSeriesStatus, message string) error { 393 buildTxn := func(attempt int) ([]txn.Op, error) { 394 if attempt > 0 { 395 if err := m.Refresh(); err != nil { 396 return nil, errors.Trace(err) 397 } 398 } 399 if err := m.isStillAlive(); err != nil { 400 return nil, errors.Trace(err) 401 } 402 canUpdate, err := m.verifyUnitUpgradeSeriesStatus(unitName, status) 403 if err != nil { 404 return nil, errors.Trace(err) 405 } 406 if !canUpdate { 407 return nil, jujutxn.ErrNoOperations 408 } 409 timestamp := bson.Now() 410 updateMessage := newUpgradeSeriesMessage(unitName, message, timestamp) 411 return setUpgradeSeriesTxnOps(m.doc.Id, unitName, status, timestamp, updateMessage) 412 } 413 err := m.st.db().Run(buildTxn) 414 if err != nil { 415 err = onAbort(err, stateerrors.ErrDead) 416 return err 417 } 418 return nil 419 } 420 421 // verifyUnitUpgradeSeriesStatus returns a boolean indicating whether or not it 422 // is safe to update the UpgradeSeriesStatus of a lock. 423 func (m *Machine) verifyUnitUpgradeSeriesStatus(unitName string, status model.UpgradeSeriesStatus) (bool, error) { 424 lock, err := m.getUpgradeSeriesLock() 425 if err != nil { 426 return false, errors.Trace(err) 427 } 428 us, ok := lock.UnitStatuses[unitName] 429 if !ok { 430 return false, errors.NotFoundf(unitName) 431 } 432 433 fsm, err := model.NewUpgradeSeriesFSM(model.UpgradeSeriesGraph(), us.Status) 434 if err != nil { 435 return false, errors.Trace(err) 436 } 437 return fsm.TransitionTo(status), nil 438 } 439 440 // [TODO](externalreality): move some/all of these parameters into an argument structure. 441 func setUpgradeSeriesTxnOps( 442 machineDocID, 443 unitName string, 444 status model.UpgradeSeriesStatus, 445 timestamp time.Time, 446 message UpgradeSeriesMessage, 447 ) ([]txn.Op, error) { 448 statusField := "unit-statuses" 449 unitStatusField := fmt.Sprintf("%s.%s.status", statusField, unitName) 450 unitTimestampField := fmt.Sprintf("%s.%s.timestamp", statusField, unitName) 451 return []txn.Op{ 452 { 453 C: machinesC, 454 Id: machineDocID, 455 Assert: isAliveDoc, 456 }, 457 { 458 C: machineUpgradeSeriesLocksC, 459 Id: machineDocID, 460 Assert: bson.D{{"$and", []bson.D{ 461 {{statusField, bson.D{{"$exists", true}}}}, // if it doesn't exist something is wrong 462 {{unitStatusField, bson.D{{"$ne", status}}}}}}}, 463 Update: bson.D{ 464 {"$set", bson.D{ 465 {unitStatusField, status}, 466 {"timestamp", timestamp}, 467 {unitTimestampField, timestamp}}}, 468 {"$push", bson.D{{"messages", message}}}, 469 }, 470 }, 471 }, nil 472 } 473 474 // SetUpgradeSeriesStatus sets the status of the machine in 475 // the upgrade-series lock. 476 func (m *Machine) SetUpgradeSeriesStatus(status model.UpgradeSeriesStatus, message string) error { 477 buildTxn := func(attempt int) ([]txn.Op, error) { 478 if attempt > 0 { 479 if err := m.Refresh(); err != nil { 480 return nil, errors.Trace(err) 481 } 482 } 483 if err := m.isStillAlive(); err != nil { 484 return nil, errors.Trace(err) 485 } 486 statusSet, err := m.isMachineUpgradeSeriesStatusSet(status) 487 if err != nil { 488 return nil, errors.Trace(err) 489 } 490 if statusSet { 491 return nil, jujutxn.ErrNoOperations 492 } 493 timestamp := bson.Now() 494 upgradeSeriesMessage := newUpgradeSeriesMessage(m.Tag().String(), message, timestamp) 495 return setMachineUpgradeSeriesTxnOps(m.doc.Id, status, upgradeSeriesMessage, timestamp), nil 496 } 497 err := m.st.db().Run(buildTxn) 498 if err != nil { 499 err = onAbort(err, stateerrors.ErrDead) 500 return err 501 } 502 return nil 503 } 504 505 // GetUpgradeSeriesMessages returns all 'unseen' upgrade series 506 // notifications sorted by timestamp. 507 func (m *Machine) GetUpgradeSeriesMessages() ([]string, bool, error) { 508 lock, err := m.getUpgradeSeriesLock() 509 if errors.IsNotFound(err) { 510 // If the lock is not found here then there are no more messages 511 return nil, true, nil 512 } 513 if err != nil { 514 return nil, false, errors.Trace(err) 515 } 516 // finished means that a subsequent call to this method, while the 517 // Machine Lock is of a similar Machine Status, would return no 518 // additional messages (notifications). Since the value of this variable 519 // is returned, callers may choose to close streams or stop watchers 520 // based on this information. 521 finished := lock.MachineStatus == model.UpgradeSeriesCompleted || 522 lock.MachineStatus == model.UpgradeSeriesPrepareCompleted 523 // Filter seen messages 524 unseenMessages := make([]UpgradeSeriesMessage, 0) 525 for _, upgradeSeriesMessage := range lock.Messages { 526 if !upgradeSeriesMessage.Seen { 527 unseenMessages = append(unseenMessages, upgradeSeriesMessage) 528 } 529 } 530 if len(unseenMessages) == 0 { 531 return []string{}, finished, nil 532 } 533 sort.Slice(unseenMessages, func(i, j int) bool { 534 return unseenMessages[i].Timestamp.Before(unseenMessages[j].Timestamp) 535 }) 536 messages := make([]string, 0) 537 for _, unseenMessage := range unseenMessages { 538 messages = append(messages, unseenMessage.Message) 539 } 540 err = m.SetUpgradeSeriesMessagesAsSeen(lock.Messages) 541 if err != nil { 542 return nil, false, errors.Trace(err) 543 } 544 return messages, finished, nil 545 } 546 547 // SetUpgradeSeriesMessagesAsSeen marks a given upgrade series messages as 548 // having been seen by a client of the API. 549 func (m *Machine) SetUpgradeSeriesMessagesAsSeen(messages []UpgradeSeriesMessage) error { 550 buildTxn := func(attempt int) ([]txn.Op, error) { 551 if attempt > 0 { 552 if err := m.Refresh(); err != nil { 553 return nil, errors.Trace(err) 554 } 555 } 556 if len(messages) == 0 { 557 return nil, jujutxn.ErrNoOperations 558 } 559 if err := m.isStillAlive(); err != nil { 560 return nil, errors.Trace(err) 561 } 562 return setUpgradeSeriesMessageTxnOps(m.doc.Id, messages, true), nil 563 } 564 err := m.st.db().Run(buildTxn) 565 if err != nil { 566 err = onAbort(err, stateerrors.ErrDead) 567 return err 568 } 569 return nil 570 } 571 572 func setUpgradeSeriesMessageTxnOps(machineDocID string, messages []UpgradeSeriesMessage, seen bool) []txn.Op { 573 ops := []txn.Op{ 574 { 575 C: machinesC, 576 Id: machineDocID, 577 Assert: isAliveDoc, 578 }, 579 } 580 fields := bson.D{} 581 for i := range messages { 582 field := fmt.Sprintf("messages.%d.seen", i) 583 fields = append(fields, bson.DocElem{Name: field, Value: seen}) 584 } 585 ops = append(ops, txn.Op{ 586 C: machineUpgradeSeriesLocksC, 587 Id: machineDocID, 588 Update: bson.D{{"$set", fields}}, 589 }) 590 return ops 591 } 592 593 func (m *Machine) isMachineUpgradeSeriesStatusSet(status model.UpgradeSeriesStatus) (bool, error) { 594 lock, err := m.getUpgradeSeriesLock() 595 if err != nil { 596 return false, err 597 } 598 return lock.MachineStatus == status, nil 599 } 600 601 func (m *Machine) getUpgradeSeriesLock() (*upgradeSeriesLockDoc, error) { 602 lock, err := m.st.getUpgradeSeriesLock(m.Id()) 603 return lock, errors.Trace(err) 604 } 605 606 func (st *State) getUpgradeSeriesLock(machineID string) (*upgradeSeriesLockDoc, error) { 607 coll, closer := st.db().GetCollection(machineUpgradeSeriesLocksC) 608 defer closer() 609 610 var lock upgradeSeriesLockDoc 611 err := coll.FindId(machineID).One(&lock) 612 if err == mgo.ErrNotFound { 613 return nil, errors.NotFoundf("upgrade lock for machine %q", machineID) 614 } 615 if err != nil { 616 return nil, errors.Annotatef(err, "retrieving upgrade series lock for machine %v", machineID) 617 } 618 return &lock, nil 619 } 620 621 func setMachineUpgradeSeriesTxnOps( 622 machineDocID string, status model.UpgradeSeriesStatus, message UpgradeSeriesMessage, timestamp time.Time, 623 ) []txn.Op { 624 field := "machine-status" 625 626 return []txn.Op{ 627 { 628 C: machinesC, 629 Id: machineDocID, 630 Assert: isAliveDoc, 631 }, 632 { 633 C: machineUpgradeSeriesLocksC, 634 Id: machineDocID, 635 Update: bson.D{ 636 {"$set", bson.D{{field, status}, {"timestamp", timestamp}}}, 637 {"$push", bson.D{{"messages", message}}}, 638 }, 639 }, 640 } 641 } 642 643 // upgradeSeriesMachineIds returns the IDs of all machines 644 // currently locked for series-upgrade. 645 func (st *State) upgradeSeriesMachineIds() ([]string, error) { 646 coll, closer := st.db().GetCollection(machineUpgradeSeriesLocksC) 647 defer closer() 648 649 var locks []struct { 650 Id string `bson:"machine-id"` 651 } 652 if err := coll.Find(nil).Select(bson.M{"machine-id": 1}).All(&locks); err != nil { 653 return nil, errors.Trace(err) 654 } 655 656 ids := make([]string, len(locks)) 657 for i, l := range locks { 658 ids[i] = l.Id 659 } 660 return ids, nil 661 } 662 663 // HasUpgradeSeriesLocks returns true if there are any upgrade machine locks. 664 func (st *State) HasUpgradeSeriesLocks() (bool, error) { 665 ids, err := st.upgradeSeriesMachineIds() 666 if err != nil { 667 return false, errors.Trace(err) 668 } 669 return len(ids) > 0, nil 670 }