diff --git a/resources/sql/autopatches/20180827.drydock.01.acquired.sql b/resources/sql/autopatches/20180827.drydock.01.acquired.sql new file mode 100644 index 0000000000..55948391c9 --- /dev/null +++ b/resources/sql/autopatches/20180827.drydock.01.acquired.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_lease + ADD acquiredEpoch INT UNSIGNED; diff --git a/resources/sql/autopatches/20180827.drydock.02.activated.sql b/resources/sql/autopatches/20180827.drydock.02.activated.sql new file mode 100644 index 0000000000..552f7b6b24 --- /dev/null +++ b/resources/sql/autopatches/20180827.drydock.02.activated.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_lease + ADD activatedEpoch INT UNSIGNED; diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php index 91a911277f..18e1d0088d 100644 --- a/src/applications/drydock/controller/DrydockLeaseViewController.php +++ b/src/applications/drydock/controller/DrydockLeaseViewController.php @@ -1,178 +1,202 @@ getViewer(); $id = $request->getURIData('id'); $lease = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needUnconsumedCommands(true) ->executeOne(); if (!$lease) { return new Aphront404Response(); } $id = $lease->getID(); $lease_uri = $this->getApplicationURI("lease/{$id}/"); $title = pht('Lease %d', $lease->getID()); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon('fa-link') ->setStatus( $lease->getStatusIcon(), $lease->getStatusColor(), $lease->getStatusDisplayName()); if ($lease->isReleasing()) { $header->addTag( id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setIcon('fa-exclamation-triangle') ->setColor('red') ->setName('Releasing')); } $curtain = $this->buildCurtain($lease); $properties = $this->buildPropertyListView($lease); $log_query = id(new DrydockLogQuery()) ->withLeasePHIDs(array($lease->getPHID())); $logs = $this->buildLogBox( $log_query, $this->getApplicationURI("lease/{$id}/logs/query/all/")); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title, $lease_uri); $crumbs->setBorder(true); $locks = $this->buildLocksTab($lease->getPHID()); $commands = $this->buildCommandsTab($lease->getPHID()); $tab_group = id(new PHUITabGroupView()) ->addTab( id(new PHUITabView()) ->setName(pht('Properties')) ->setKey('properties') ->appendChild($properties)) ->addTab( id(new PHUITabView()) ->setName(pht('Slot Locks')) ->setKey('locks') ->appendChild($locks)) ->addTab( id(new PHUITabView()) ->setName(pht('Commands')) ->setKey('commands') ->appendChild($commands)); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Properties')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addTabGroup($tab_group); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $object_box, $logs, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } private function buildCurtain(DrydockLease $lease) { $viewer = $this->getViewer(); $curtain = $this->newCurtainView($lease); $id = $lease->getID(); $can_release = $lease->canRelease(); if ($lease->isReleasing()) { $can_release = false; } $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $lease, PhabricatorPolicyCapability::CAN_EDIT); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Release Lease')) ->setIcon('fa-times') ->setHref($this->getApplicationURI("/lease/{$id}/release/")) ->setWorkflow(true) ->setDisabled(!$can_release || !$can_edit)); return $curtain; } private function buildPropertyListView( DrydockLease $lease) { $viewer = $this->getViewer(); $view = new PHUIPropertyListView(); $view->addProperty( pht('Resource Type'), $lease->getResourceType()); $owner_phid = $lease->getOwnerPHID(); if ($owner_phid) { $owner_display = $viewer->renderHandle($owner_phid); } else { $owner_display = phutil_tag('em', array(), pht('No Owner')); } $view->addProperty(pht('Owner'), $owner_display); $authorizing_phid = $lease->getAuthorizingPHID(); if ($authorizing_phid) { $authorizing_display = $viewer->renderHandle($authorizing_phid); } else { $authorizing_display = phutil_tag('em', array(), pht('None')); } $view->addProperty(pht('Authorized By'), $authorizing_display); $resource_phid = $lease->getResourcePHID(); if ($resource_phid) { $resource_display = $viewer->renderHandle($resource_phid); } else { $resource_display = phutil_tag('em', array(), pht('No Resource')); } $view->addProperty(pht('Resource'), $resource_display); $until = $lease->getUntil(); if ($until) { $until_display = phabricator_datetime($until, $viewer); } else { $until_display = phutil_tag('em', array(), pht('Never')); } $view->addProperty(pht('Expires'), $until_display); + $acquired_epoch = $lease->getAcquiredEpoch(); + $activated_epoch = $lease->getActivatedEpoch(); + + if ($acquired_epoch) { + $acquired_display = phabricator_datetime($acquired_epoch, $viewer); + } else { + if ($activated_epoch) { + $acquired_display = phutil_tag( + 'em', + array(), + pht('Activated on Acquisition')); + } else { + $acquired_display = phutil_tag('em', array(), pht('Not Acquired')); + } + } + $view->addProperty(pht('Acquired'), $acquired_display); + + if ($activated_epoch) { + $activated_display = phabricator_datetime($activated_epoch, $viewer); + } else { + $activated_display = phutil_tag('em', array(), pht('Not Activated')); + } + $view->addProperty(pht('Activated'), $activated_display); + $attributes = $lease->getAttributes(); if ($attributes) { $view->addSectionHeader( pht('Attributes'), 'fa-list-ul'); foreach ($attributes as $key => $value) { $view->addProperty($key, $value); } } return $view; } } diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index 4cee7a5f17..866bb21b37 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -1,518 +1,538 @@ setPHID($lease->generatePHID()); return $lease; } /** * Flag this lease to be released when its destructor is called. This is * mostly useful if you have a script which acquires, uses, and then releases * a lease, as you don't need to explicitly handle exceptions to properly * release the lease. */ public function setReleaseOnDestruction($release) { $this->releaseOnDestruction = $release; return $this; } public function __destruct() { if (!$this->releaseOnDestruction) { return; } if (!$this->canRelease()) { return; } $actor = PhabricatorUser::getOmnipotentUser(); $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); $command = DrydockCommand::initializeNewCommand($actor) ->setTargetPHID($this->getPHID()) ->setAuthorPHID($drydock_phid) ->setCommand(DrydockCommand::COMMAND_RELEASE) ->save(); $this->scheduleUpdate(); } + public function setStatus($status) { + if ($status == DrydockLeaseStatus::STATUS_ACQUIRED) { + if (!$this->getAcquiredEpoch()) { + $this->setAcquiredEpoch(PhabricatorTime::getNow()); + } + } + + if ($status == DrydockLeaseStatus::STATUS_ACTIVE) { + if (!$this->getActivatedEpoch()) { + $this->setActivatedEpoch(PhabricatorTime::getNow()); + } + } + + return parent::setStatus($status); + } + public function getLeaseName() { return pht('Lease %d', $this->getID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attributes' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'text32', 'until' => 'epoch?', 'resourceType' => 'text128', 'ownerPHID' => 'phid?', 'resourcePHID' => 'phid?', + 'acquiredEpoch' => 'epoch?', + 'activatedEpoch' => 'epoch?', ), self::CONFIG_KEY_SCHEMA => array( 'key_resource' => array( 'columns' => array('resourcePHID', 'status'), ), 'key_status' => array( 'columns' => array('status'), ), ), ) + parent::getConfiguration(); } public function setAttribute($key, $value) { $this->attributes[$key] = $value; return $this; } public function getAttribute($key, $default = null) { return idx($this->attributes, $key, $default); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(DrydockLeasePHIDType::TYPECONST); } public function getInterface($type) { return $this->getResource()->getInterface($this, $type); } public function getResource() { return $this->assertAttached($this->resource); } public function attachResource(DrydockResource $resource = null) { $this->resource = $resource; return $this; } public function hasAttachedResource() { return ($this->resource !== null); } public function getUnconsumedCommands() { return $this->assertAttached($this->unconsumedCommands); } public function attachUnconsumedCommands(array $commands) { $this->unconsumedCommands = $commands; return $this; } public function isReleasing() { foreach ($this->getUnconsumedCommands() as $command) { if ($command->getCommand() == DrydockCommand::COMMAND_RELEASE) { return true; } } return false; } public function queueForActivation() { if ($this->getID()) { throw new Exception( pht('Only new leases may be queued for activation!')); } if (!$this->getAuthorizingPHID()) { throw new Exception( pht( 'Trying to queue a lease for activation without an authorizing '. 'object. Use "%s" to specify the PHID of the authorizing object. '. 'The authorizing object must be approved to use the allowed '. 'blueprints.', 'setAuthorizingPHID()')); } if (!$this->getAllowedBlueprintPHIDs()) { throw new Exception( pht( 'Trying to queue a lease for activation without any allowed '. 'Blueprints. Use "%s" to specify allowed blueprints. The '. 'authorizing object must be approved to use the allowed blueprints.', 'setAllowedBlueprintPHIDs()')); } $this ->setStatus(DrydockLeaseStatus::STATUS_PENDING) ->save(); $this->scheduleUpdate(); $this->logEvent(DrydockLeaseQueuedLogType::LOGCONST); return $this; } public function setActivateWhenAcquired($activate) { $this->activateWhenAcquired = true; return $this; } public function needSlotLock($key) { $this->slotLocks[] = $key; return $this; } public function acquireOnResource(DrydockResource $resource) { $expect_status = DrydockLeaseStatus::STATUS_PENDING; $actual_status = $this->getStatus(); if ($actual_status != $expect_status) { throw new Exception( pht( 'Trying to acquire a lease on a resource which is in the wrong '. 'state: status must be "%s", actually "%s".', $expect_status, $actual_status)); } if ($this->activateWhenAcquired) { $new_status = DrydockLeaseStatus::STATUS_ACTIVE; } else { $new_status = DrydockLeaseStatus::STATUS_ACQUIRED; } if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) { if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) { throw new Exception( pht( 'Trying to acquire an active lease on a pending resource. '. 'You can not immediately activate leases on resources which '. 'need time to start up.')); } } // Before we associate the lease with the resource, we lock the resource // and reload it to make sure it is still pending or active. If we don't // do this, the resource may have just been reclaimed. (Once we acquire // the resource that stops it from being released, so we're nearly safe.) $resource_phid = $resource->getPHID(); $hash = PhabricatorHash::digestForIndex($resource_phid); $lock_key = 'drydock.resource:'.$hash; $lock = PhabricatorGlobalLock::newLock($lock_key); try { $lock->lock(15); } catch (Exception $ex) { throw new DrydockResourceLockException( pht( 'Failed to acquire lock for resource ("%s") while trying to '. 'acquire lease ("%s").', $resource->getPHID(), $this->getPHID())); } $resource->reload(); if (($resource->getStatus() !== DrydockResourceStatus::STATUS_ACTIVE) && ($resource->getStatus() !== DrydockResourceStatus::STATUS_PENDING)) { throw new DrydockAcquiredBrokenResourceException( pht( 'Trying to acquire lease ("%s") on a resource ("%s") in the '. 'wrong status ("%s").', $this->getPHID(), $resource->getPHID(), $resource->getStatus())); } $caught = null; try { $this->openTransaction(); try { DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); $this->slotLocks = array(); } catch (DrydockSlotLockException $ex) { $this->killTransaction(); $this->logEvent( DrydockSlotLockFailureLogType::LOGCONST, array( 'locks' => $ex->getLockMap(), )); throw $ex; } $this ->setResourcePHID($resource->getPHID()) ->attachResource($resource) ->setStatus($new_status) ->save(); $this->saveTransaction(); } catch (Exception $ex) { $caught = $ex; } $lock->unlock(); if ($caught) { throw $caught; } $this->isAcquired = true; $this->logEvent(DrydockLeaseAcquiredLogType::LOGCONST); if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) { $this->didActivate(); } return $this; } public function isAcquiredLease() { return $this->isAcquired; } public function activateOnResource(DrydockResource $resource) { $expect_status = DrydockLeaseStatus::STATUS_ACQUIRED; $actual_status = $this->getStatus(); if ($actual_status != $expect_status) { throw new Exception( pht( 'Trying to activate a lease which has the wrong status: status '. 'must be "%s", actually "%s".', $expect_status, $actual_status)); } if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) { // TODO: Be stricter about this? throw new Exception( pht( 'Trying to activate a lease on a pending resource.')); } $this->openTransaction(); try { DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); $this->slotLocks = array(); } catch (DrydockSlotLockException $ex) { $this->killTransaction(); $this->logEvent( DrydockSlotLockFailureLogType::LOGCONST, array( 'locks' => $ex->getLockMap(), )); throw $ex; } $this ->setStatus(DrydockLeaseStatus::STATUS_ACTIVE) ->save(); $this->saveTransaction(); $this->isActivated = true; $this->didActivate(); return $this; } public function isActivatedLease() { return $this->isActivated; } public function scheduleUpdate($epoch = null) { PhabricatorWorker::scheduleTask( 'DrydockLeaseUpdateWorker', array( 'leasePHID' => $this->getPHID(), 'isExpireTask' => ($epoch !== null), ), array( 'objectPHID' => $this->getPHID(), 'delayUntil' => ($epoch ? (int)$epoch : null), )); } public function setAwakenTaskIDs(array $ids) { $this->setAttribute('internal.awakenTaskIDs', $ids); return $this; } public function setAllowedBlueprintPHIDs(array $phids) { $this->setAttribute('internal.blueprintPHIDs', $phids); return $this; } public function getAllowedBlueprintPHIDs() { return $this->getAttribute('internal.blueprintPHIDs', array()); } private function didActivate() { $viewer = PhabricatorUser::getOmnipotentUser(); $need_update = false; $this->logEvent(DrydockLeaseActivatedLogType::LOGCONST); $commands = id(new DrydockCommandQuery()) ->setViewer($viewer) ->withTargetPHIDs(array($this->getPHID())) ->withConsumed(false) ->execute(); if ($commands) { $need_update = true; } if ($need_update) { $this->scheduleUpdate(); } $expires = $this->getUntil(); if ($expires) { $this->scheduleUpdate($expires); } $this->awakenTasks(); } public function logEvent($type, array $data = array()) { $log = id(new DrydockLog()) ->setEpoch(PhabricatorTime::getNow()) ->setType($type) ->setData($data); $log->setLeasePHID($this->getPHID()); $resource_phid = $this->getResourcePHID(); if ($resource_phid) { $resource = $this->getResource(); $log->setResourcePHID($resource->getPHID()); $log->setBlueprintPHID($resource->getBlueprintPHID()); } return $log->save(); } /** * Awaken yielded tasks after a state change. * * @return this */ public function awakenTasks() { $awaken_ids = $this->getAttribute('internal.awakenTaskIDs'); if (is_array($awaken_ids) && $awaken_ids) { PhabricatorWorker::awakenTaskIDs($awaken_ids); } return $this; } public function getURI() { $id = $this->getID(); return "/drydock/lease/{$id}/"; } /* -( Status )------------------------------------------------------------- */ public function getStatusObject() { return DrydockLeaseStatus::newStatusObject($this->getStatus()); } public function getStatusIcon() { return $this->getStatusObject()->getIcon(); } public function getStatusColor() { return $this->getStatusObject()->getColor(); } public function getStatusDisplayName() { return $this->getStatusObject()->getDisplayName(); } public function isActivating() { return $this->getStatusObject()->isActivating(); } public function isActive() { return $this->getStatusObject()->isActive(); } public function canRelease() { if (!$this->getID()) { return false; } return $this->getStatusObject()->canRelease(); } public function canReceiveCommands() { return $this->getStatusObject()->canReceiveCommands(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($this->getResource()) { return $this->getResource()->getPolicy($capability); } // TODO: Implement reasonable policies. return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->getResource()) { return $this->getResource()->hasAutomaticCapability($capability, $viewer); } return false; } public function describeAutomaticCapability($capability) { return pht('Leases inherit policies from the resources they lease.'); } }