diff --git a/resources/sql/autopatches/20151001.drydock.rname.1.sql b/resources/sql/autopatches/20151001.drydock.rname.1.sql new file mode 100644 index 000000000..3dbd66c57 --- /dev/null +++ b/resources/sql/autopatches/20151001.drydock.rname.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_resource + DROP name; diff --git a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php index 9a7147462..e45802060 100644 --- a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php @@ -1,288 +1,298 @@ <?php final class DrydockAlmanacServiceHostBlueprintImplementation extends DrydockBlueprintImplementation { private $services; private $freeBindings; public function isEnabled() { $almanac_app = 'PhabricatorAlmanacApplication'; return PhabricatorApplication::isClassInstalled($almanac_app); } public function getBlueprintName() { return pht('Almanac Hosts'); } public function getDescription() { return pht( 'Allows Drydock to lease existing hosts defined in an Almanac service '. 'pool.'); } public function canAnyBlueprintEverAllocateResourceForLease( DrydockLease $lease) { return true; } public function canEverAllocateResourceForLease( DrydockBlueprint $blueprint, DrydockLease $lease) { $services = $this->loadServices($blueprint); $bindings = $this->loadAllBindings($services); if (!$bindings) { // If there are no devices bound to the services for this blueprint, // we can not allocate resources. return false; } return true; } public function canAllocateResourceForLease( DrydockBlueprint $blueprint, DrydockLease $lease) { // We will only allocate one resource per unique device bound to the // services for this blueprint. Make sure we have a free device somewhere. $free_bindings = $this->loadFreeBindings($blueprint); if (!$free_bindings) { return false; } return true; } public function allocateResource( DrydockBlueprint $blueprint, DrydockLease $lease) { $free_bindings = $this->loadFreeBindings($blueprint); shuffle($free_bindings); $exceptions = array(); foreach ($free_bindings as $binding) { $device = $binding->getDevice(); $device_name = $device->getName(); $binding_phid = $binding->getPHID(); - $resource = $this->newResourceTemplate($blueprint, $device_name) + $resource = $this->newResourceTemplate($blueprint) ->setActivateWhenAllocated(true) + ->setAttribute('almanacDeviceName', $device_name) ->setAttribute('almanacServicePHID', $binding->getServicePHID()) ->setAttribute('almanacBindingPHID', $binding_phid) ->needSlotLock("almanac.host.binding({$binding_phid})"); try { return $resource->allocateResource(); } catch (Exception $ex) { $exceptions[] = $ex; } } throw new PhutilAggregateException( pht('Unable to allocate any binding as a resource.'), $exceptions); } public function destroyResource( DrydockBlueprint $blueprint, DrydockResource $resource) { // We don't create anything when allocating hosts, so we don't need to do // any cleanup here. return; } + public function getResourceName( + DrydockBlueprint $blueprint, + DrydockResource $resource) { + $device_name = $resource->getAttribute( + 'almanacDeviceName', + pht('<Unknown>')); + return pht('Host (%s)', $device_name); + } + public function canAcquireLeaseOnResource( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { if (!DrydockSlotLock::isLockFree($this->getLeaseSlotLock($resource))) { return false; } return true; } public function acquireLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { $lease ->setActivateWhenAcquired(true) ->needSlotLock($this->getLeaseSlotLock($resource)) ->acquireOnResource($resource); } public function didReleaseLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // Almanac hosts stick around indefinitely so we don't need to recycle them // if they don't have any leases. return; } public function destroyLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // We don't create anything when activating a lease, so we don't need to // throw anything away. return; } private function getLeaseSlotLock(DrydockResource $resource) { $resource_phid = $resource->getPHID(); return "almanac.host.lease({$resource_phid})"; } public function getType() { return 'host'; } public function getInterface( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease, $type) { $viewer = PhabricatorUser::getOmnipotentUser(); switch ($type) { case DrydockCommandInterface::INTERFACE_TYPE: $credential_phid = $blueprint->getFieldValue('credentialPHID'); $binding_phid = $resource->getAttribute('almanacBindingPHID'); $binding = id(new AlmanacBindingQuery()) ->setViewer($viewer) ->withPHIDs(array($binding_phid)) ->executeOne(); if (!$binding) { throw new Exception( pht( 'Unable to load binding "%s" to create command interface.', $binding_phid)); } $interface = $binding->getInterface(); return id(new DrydockSSHCommandInterface()) ->setConfig('credentialPHID', $credential_phid) ->setConfig('host', $interface->getAddress()) ->setConfig('port', $interface->getPort()); } } public function getFieldSpecifications() { return array( 'almanacServicePHIDs' => array( 'name' => pht('Almanac Services'), 'type' => 'datasource', 'datasource.class' => 'AlmanacServiceDatasource', 'datasource.parameters' => array( 'serviceClasses' => $this->getAlmanacServiceClasses(), ), 'required' => true, ), 'credentialPHID' => array( 'name' => pht('Credentials'), 'type' => 'credential', 'credential.provides' => PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE, 'credential.type' => PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE, ), ) + parent::getFieldSpecifications(); } private function loadServices(DrydockBlueprint $blueprint) { if (!$this->services) { $service_phids = $blueprint->getFieldValue('almanacServicePHIDs'); if (!$service_phids) { throw new Exception( pht( 'This blueprint ("%s") does not define any Almanac Service PHIDs.', $blueprint->getBlueprintName())); } $viewer = PhabricatorUser::getOmnipotentUser(); $services = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withPHIDs($service_phids) ->withServiceClasses($this->getAlmanacServiceClasses()) ->needBindings(true) ->execute(); $services = mpull($services, null, 'getPHID'); if (count($services) != count($service_phids)) { $missing_phids = array_diff($service_phids, array_keys($services)); throw new Exception( pht( 'Some of the Almanac Services defined by this blueprint '. 'could not be loaded. They may be invalid, no longer exist, '. 'or be of the wrong type: %s.', implode(', ', $missing_phids))); } $this->services = $services; } return $this->services; } private function loadAllBindings(array $services) { assert_instances_of($services, 'AlmanacService'); $bindings = array_mergev(mpull($services, 'getBindings')); return mpull($bindings, null, 'getPHID'); } private function loadFreeBindings(DrydockBlueprint $blueprint) { if ($this->freeBindings === null) { $viewer = PhabricatorUser::getOmnipotentUser(); $pool = id(new DrydockResourceQuery()) ->setViewer($viewer) ->withBlueprintPHIDs(array($blueprint->getPHID())) ->withStatuses( array( DrydockResourceStatus::STATUS_PENDING, DrydockResourceStatus::STATUS_ACTIVE, DrydockResourceStatus::STATUS_RELEASED, )) ->execute(); $allocated_phids = array(); foreach ($pool as $resource) { $allocated_phids[] = $resource->getAttribute('almanacDevicePHID'); } $allocated_phids = array_fuse($allocated_phids); $services = $this->loadServices($blueprint); $bindings = $this->loadAllBindings($services); $free = array(); foreach ($bindings as $binding) { if (empty($allocated_phids[$binding->getPHID()])) { $free[] = $binding; } } $this->freeBindings = $free; } return $this->freeBindings; } private function getAlmanacServiceClasses() { return array( 'AlmanacDrydockPoolServiceType', ); } } diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php index b7e39a49a..5ec2d60ae 100644 --- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php @@ -1,299 +1,309 @@ <?php /** * @task lease Lease Acquisition * @task resource Resource Allocation * @task interface Resource Interfaces * @task log Logging */ abstract class DrydockBlueprintImplementation extends Phobject { abstract public function getType(); abstract public function isEnabled(); abstract public function getBlueprintName(); abstract public function getDescription(); public function getFieldSpecifications() { return array(); } /* -( Lease Acquisition )-------------------------------------------------- */ /** * Enforce basic checks on lease/resource compatibility. Allows resources to * reject leases if they are incompatible, even if the resource types match. * * For example, if a resource represents a 32-bit host, this method might * reject leases that need a 64-bit host. The blueprint might also reject * a resource if the lease needs 8GB of RAM and the resource only has 6GB * free. * * This method should not acquire locks or expect anything to be locked. This * is a coarse compatibility check between a lease and a resource. * * @param DrydockBlueprint Concrete blueprint to allocate for. * @param DrydockResource Candidiate resource to allocate the lease on. * @param DrydockLease Pending lease that wants to allocate here. * @return bool True if the resource and lease are compatible. * @task lease */ abstract public function canAcquireLeaseOnResource( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease); /** * Acquire a lease. Allows resources to peform setup as leases are brought * online. * * If acquisition fails, throw an exception. * * @param DrydockBlueprint Blueprint which built the resource. * @param DrydockResource Resource to acquire a lease on. * @param DrydockLease Requested lease. * @return void * @task lease */ abstract public function acquireLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease); /** * @return void * @task lease */ public function activateLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { throw new PhutilMethodNotImplementedException(); } /** * React to a lease being released. * * This callback is primarily useful for automatically releasing resources * once all leases are released. * * @param DrydockBlueprint Blueprint which built the resource. * @param DrydockResource Resource a lease was released on. * @param DrydockLease Recently released lease. * @return void * @task lease */ abstract public function didReleaseLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease); /** * Destroy any temporary data associated with a lease. * * If a lease creates temporary state while held, destroy it here. * * @param DrydockBlueprint Blueprint which built the resource. * @param DrydockResource Resource the lease is acquired on. * @param DrydockLease The lease being destroyed. * @return void * @task lease */ abstract public function destroyLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease); /* -( Resource Allocation )------------------------------------------------ */ /** * Enforce fundamental implementation/lease checks. Allows implementations to * reject a lease which no concrete blueprint can ever satisfy. * * For example, if a lease only builds ARM hosts and the lease needs a * PowerPC host, it may be rejected here. * * This is the earliest rejection phase, and followed by * @{method:canEverAllocateResourceForLease}. * * This method should not actually check if a resource can be allocated * right now, or even if a blueprint which can allocate a suitable resource * really exists, only if some blueprint may conceivably exist which could * plausibly be able to build a suitable resource. * * @param DrydockLease Requested lease. * @return bool True if some concrete blueprint of this implementation's * type might ever be able to build a resource for the lease. * @task resource */ abstract public function canAnyBlueprintEverAllocateResourceForLease( DrydockLease $lease); /** * Enforce basic blueprint/lease checks. Allows blueprints to reject a lease * which they can not build a resource for. * * This is the second rejection phase. It follows * @{method:canAnyBlueprintEverAllocateResourceForLease} and is followed by * @{method:canAllocateResourceForLease}. * * This method should not check if a resource can be built right now, only * if the blueprint as configured may, at some time, be able to build a * suitable resource. * * @param DrydockBlueprint Blueprint which may be asked to allocate a * resource. * @param DrydockLease Requested lease. * @return bool True if this blueprint can eventually build a suitable * resource for the lease, as currently configured. * @task resource */ abstract public function canEverAllocateResourceForLease( DrydockBlueprint $blueprint, DrydockLease $lease); /** * Enforce basic availability limits. Allows blueprints to reject resource * allocation if they are currently overallocated. * * This method should perform basic capacity/limit checks. For example, if * it has a limit of 6 resources and currently has 6 resources allocated, * it might reject new leases. * * This method should not acquire locks or expect locks to be acquired. This * is a coarse check to determine if the operation is likely to succeed * right now without needing to acquire locks. * * It is expected that this method will sometimes return `true` (indicating * that a resource can be allocated) but find that another allocator has * eaten up free capacity by the time it actually tries to build a resource. * This is normal and the allocator will recover from it. * * @param DrydockBlueprint The blueprint which may be asked to allocate a * resource. * @param DrydockLease Requested lease. * @return bool True if this blueprint appears likely to be able to allocate * a suitable resource. * @task resource */ abstract public function canAllocateResourceForLease( DrydockBlueprint $blueprint, DrydockLease $lease); /** * Allocate a suitable resource for a lease. * * This method MUST acquire, hold, and manage locks to prevent multiple * allocations from racing. World state is not locked before this method is * called. Blueprints are entirely responsible for any lock handling they * need to perform. * * @param DrydockBlueprint The blueprint which should allocate a resource. * @param DrydockLease Requested lease. * @return DrydockResource Allocated resource. * @task resource */ abstract public function allocateResource( DrydockBlueprint $blueprint, DrydockLease $lease); /** * @task resource */ public function activateResource( DrydockBlueprint $blueprint, DrydockResource $resource) { throw new PhutilMethodNotImplementedException(); } /** * Destroy any temporary data associated with a resource. * * If a resource creates temporary state when allocated, destroy that state * here. For example, you might shut down a virtual host or destroy a working * copy on disk. * * @param DrydockBlueprint Blueprint which built the resource. * @param DrydockResource Resource being destroyed. * @return void * @task resource */ abstract public function destroyResource( DrydockBlueprint $blueprint, DrydockResource $resource); + /** + * Get a human readable name for a resource. + * + * @param DrydockBlueprint Blueprint which built the resource. + * @param DrydockResource Resource to get the name of. + * @return string Human-readable resource name. + * @task resource + */ + abstract public function getResourceName( + DrydockBlueprint $blueprint, + DrydockResource $resource); + + /* -( Resource Interfaces )------------------------------------------------ */ abstract public function getInterface( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease, $type); /* -( Logging )------------------------------------------------------------ */ public static function getAllBlueprintImplementations() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->execute(); } public static function getNamedImplementation($class) { return idx(self::getAllBlueprintImplementations(), $class); } - protected function newResourceTemplate( - DrydockBlueprint $blueprint, - $name) { + protected function newResourceTemplate(DrydockBlueprint $blueprint) { $resource = id(new DrydockResource()) ->setBlueprintPHID($blueprint->getPHID()) ->attachBlueprint($blueprint) ->setType($this->getType()) - ->setStatus(DrydockResourceStatus::STATUS_PENDING) - ->setName($name); + ->setStatus(DrydockResourceStatus::STATUS_PENDING); // Pre-allocate the resource PHID. $resource->setPHID($resource->generatePHID()); return $resource; } protected function newLease(DrydockBlueprint $blueprint) { return id(new DrydockLease()); } protected function requireActiveLease(DrydockLease $lease) { $lease_status = $lease->getStatus(); switch ($lease_status) { case DrydockLeaseStatus::STATUS_ACQUIRED: // TODO: Temporary failure. throw new Exception(pht('Lease still activating.')); case DrydockLeaseStatus::STATUS_ACTIVE: return; default: // TODO: Permanent failure. throw new Exception(pht('Lease in bad state.')); } } } diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php index a61af96eb..1c6b71a29 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -1,344 +1,349 @@ <?php final class DrydockWorkingCopyBlueprintImplementation extends DrydockBlueprintImplementation { public function isEnabled() { return true; } public function getBlueprintName() { return pht('Working Copy'); } public function getDescription() { return pht('Allows Drydock to check out working copies of repositories.'); } public function canAnyBlueprintEverAllocateResourceForLease( DrydockLease $lease) { return true; } public function canEverAllocateResourceForLease( DrydockBlueprint $blueprint, DrydockLease $lease) { return true; } public function canAllocateResourceForLease( DrydockBlueprint $blueprint, DrydockLease $lease) { return true; } public function canAcquireLeaseOnResource( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { $need_map = $lease->getAttribute('repositories.map'); if (!is_array($need_map)) { return false; } $have_map = $resource->getAttribute('repositories.map'); if (!is_array($have_map)) { return false; } $have_as = ipull($have_map, 'phid'); $need_as = ipull($need_map, 'phid'); foreach ($need_as as $need_directory => $need_phid) { if (empty($have_as[$need_directory])) { // This resource is missing a required working copy. return false; } if ($have_as[$need_directory] != $need_phid) { // This resource has a required working copy, but it contains // the wrong repository. return false; } unset($have_as[$need_directory]); } if ($have_as && $lease->getAttribute('repositories.strict')) { // This resource has extra repositories, but the lease is strict about // which repositories are allowed to exist. return false; } if (!DrydockSlotLock::isLockFree($this->getLeaseSlotLock($resource))) { return false; } return true; } public function acquireLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { $lease ->needSlotLock($this->getLeaseSlotLock($resource)) ->acquireOnResource($resource); } private function getLeaseSlotLock(DrydockResource $resource) { $resource_phid = $resource->getPHID(); return "workingcopy.lease({$resource_phid})"; } public function allocateResource( DrydockBlueprint $blueprint, DrydockLease $lease) { - $resource = $this->newResourceTemplate( - $blueprint, - pht('Working Copy')); + $resource = $this->newResourceTemplate($blueprint); $resource_phid = $resource->getPHID(); $host_lease = $this->newLease($blueprint) ->setResourceType('host') ->setOwnerPHID($resource_phid) ->setAttribute('workingcopy.resourcePHID', $resource_phid) ->queueForActivation(); // TODO: Add some limits to the number of working copies we can have at // once? $map = $lease->getAttribute('repositories.map'); foreach ($map as $key => $value) { $map[$key] = array_select_keys( $value, array( 'phid', )); } return $resource ->setAttribute('repositories.map', $map) ->setAttribute('host.leasePHID', $host_lease->getPHID()) ->allocateResource(); } public function activateResource( DrydockBlueprint $blueprint, DrydockResource $resource) { $lease = $this->loadHostLease($resource); $this->requireActiveLease($lease); $command_type = DrydockCommandInterface::INTERFACE_TYPE; $interface = $lease->getInterface($command_type); // TODO: Make this configurable. $resource_id = $resource->getID(); $root = "/var/drydock/workingcopy-{$resource_id}"; $map = $resource->getAttribute('repositories.map'); $repositories = $this->loadRepositories(ipull($map, 'phid')); foreach ($map as $directory => $spec) { // TODO: Validate directory isn't goofy like "/etc" or "../../lol" // somewhere? $repository = $repositories[$spec['phid']]; $path = "{$root}/repo/{$directory}/"; // TODO: Run these in parallel? $interface->execx( 'git clone -- %s %s', (string)$repository->getCloneURIObject(), $path); } $resource ->setAttribute('workingcopy.root', $root) ->activateResource(); } public function destroyResource( DrydockBlueprint $blueprint, DrydockResource $resource) { $lease = $this->loadHostLease($resource); // Destroy the lease on the host. $lease->releaseOnDestruction(); if ($lease->isActive()) { // Destroy the working copy on disk. $command_type = DrydockCommandInterface::INTERFACE_TYPE; $interface = $lease->getInterface($command_type); $root_key = 'workingcopy.root'; $root = $resource->getAttribute($root_key); if (strlen($root)) { $interface->execx('rm -rf -- %s', $root); } } } + public function getResourceName( + DrydockBlueprint $blueprint, + DrydockResource $resource) { + return pht('Working Copy'); + } + + public function activateLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { $host_lease = $this->loadHostLease($resource); $command_type = DrydockCommandInterface::INTERFACE_TYPE; $interface = $host_lease->getInterface($command_type); $map = $lease->getAttribute('repositories.map'); $root = $resource->getAttribute('workingcopy.root'); $default = null; foreach ($map as $directory => $spec) { $cmd = array(); $arg = array(); $cmd[] = 'cd %s'; $arg[] = "{$root}/repo/{$directory}/"; $cmd[] = 'git clean -d --force'; $cmd[] = 'git fetch'; $commit = idx($spec, 'commit'); $branch = idx($spec, 'branch'); if ($commit !== null) { $cmd[] = 'git reset --hard %s'; $arg[] = $commit; } else if ($branch !== null) { $cmd[] = 'git checkout %s'; $arg[] = $branch; $cmd[] = 'git reset --hard origin/%s'; $arg[] = $branch; } else { $cmd[] = 'git reset --hard HEAD'; } $cmd = implode(' && ', $cmd); $argv = array_merge(array($cmd), $arg); $result = call_user_func_array( array($interface, 'execx'), $argv); if (idx($spec, 'default')) { $default = $directory; } } if ($default === null) { $default = head_key($map); } // TODO: Use working storage? $lease->setAttribute('workingcopy.default', "{$root}/repo/{$default}/"); $lease->activateOnResource($resource); } public function didReleaseLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // We leave working copies around even if there are no leases on them, // since the cost to maintain them is nearly zero but rebuilding them is // moderately expensive and it's likely that they'll be reused. return; } public function destroyLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // When we activate a lease we just reset the working copy state and do // not create any new state, so we don't need to do anything special when // destroying a lease. return; } public function getType() { return 'working-copy'; } public function getInterface( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease, $type) { switch ($type) { case DrydockCommandInterface::INTERFACE_TYPE: $host_lease = $this->loadHostLease($resource); $command_interface = $host_lease->getInterface($type); $path = $lease->getAttribute('workingcopy.default'); $command_interface->setWorkingDirectory($path); return $command_interface; } } private function loadRepositories(array $phids) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); foreach ($phids as $phid) { if (empty($repositories[$phid])) { throw new Exception( pht( 'Repository PHID "%s" does not exist.', $phid)); } } foreach ($repositories as $repository) { $repository_vcs = $repository->getVersionControlSystem(); switch ($repository_vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: break; default: throw new Exception( pht( 'Repository ("%s") has unsupported VCS ("%s").', $repository->getPHID(), $repository_vcs)); } } return $repositories; } private function loadHostLease(DrydockResource $resource) { $viewer = PhabricatorUser::getOmnipotentUser(); $lease_phid = $resource->getAttribute('host.leasePHID'); $lease = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withPHIDs(array($lease_phid)) ->executeOne(); if (!$lease) { throw new Exception( pht( 'Unable to load lease ("%s").', $lease_phid)); } return $lease; } } diff --git a/src/applications/drydock/controller/DrydockLeaseController.php b/src/applications/drydock/controller/DrydockLeaseController.php index d5a633545..48082b872 100644 --- a/src/applications/drydock/controller/DrydockLeaseController.php +++ b/src/applications/drydock/controller/DrydockLeaseController.php @@ -1,62 +1,62 @@ <?php abstract class DrydockLeaseController extends DrydockController { private $resource; public function setResource($resource) { $this->resource = $resource; return $this; } public function getResource() { return $this->resource; } public function buildSideNavView() { $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); $engine = id(new DrydockLeaseSearchEngine()) ->setViewer($this->getRequest()->getUser()); if ($this->getResource()) { $engine->setResource($this->getResource()); } $engine->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $resource = $this->getResource(); if ($resource) { $id = $resource->getID(); $crumbs->addTextCrumb( pht('Resources'), $this->getApplicationURI('resource/')); $crumbs->addTextCrumb( - $resource->getName(), + $resource->getResourceName(), $this->getApplicationURI("resource/{$id}/")); $crumbs->addTextCrumb( pht('Leases'), $this->getApplicationURI("resource/{$id}/leases/")); } else { $crumbs->addTextCrumb( pht('Leases'), $this->getApplicationURI('lease/')); } return $crumbs; } } diff --git a/src/applications/drydock/controller/DrydockLogController.php b/src/applications/drydock/controller/DrydockLogController.php index 5ae87e6aa..704cbb53b 100644 --- a/src/applications/drydock/controller/DrydockLogController.php +++ b/src/applications/drydock/controller/DrydockLogController.php @@ -1,119 +1,119 @@ <?php abstract class DrydockLogController extends DrydockController { private $blueprint; private $resource; private $lease; public function setBlueprint(DrydockBlueprint $blueprint) { $this->blueprint = $blueprint; return $this; } public function getBlueprint() { return $this->blueprint; } public function setResource(DrydockResource $resource) { $this->resource = $resource; return $this; } public function getResource() { return $this->resource; } public function setLease(DrydockLease $lease) { $this->lease = $lease; return $this; } public function getLease() { return $this->lease; } public function buildSideNavView() { $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); $engine = id(new DrydockLogSearchEngine()) ->setViewer($this->getRequest()->getUser()); $blueprint = $this->getBlueprint(); if ($blueprint) { $engine->setBlueprint($blueprint); } $resource = $this->getResource(); if ($resource) { $engine->setResource($resource); } $lease = $this->getLease(); if ($lease) { $engine->setLease($lease); } $engine->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $blueprint = $this->getBlueprint(); $resource = $this->getResource(); $lease = $this->getLease(); if ($blueprint) { $id = $blueprint->getID(); $crumbs->addTextCrumb( pht('Blueprints'), $this->getApplicationURI('blueprint/')); $crumbs->addTextCrumb( $blueprint->getBlueprintName(), $this->getApplicationURI("blueprint/{$id}/")); $crumbs->addTextCrumb( pht('Logs'), $this->getApplicationURI("blueprint/{$id}/logs/")); } else if ($resource) { $id = $resource->getID(); $crumbs->addTextCrumb( pht('Resources'), $this->getApplicationURI('resource/')); $crumbs->addTextCrumb( - $resource->getName(), + $resource->getResourceName(), $this->getApplicationURI("resource/{$id}/")); $crumbs->addTextCrumb( pht('Logs'), $this->getApplicationURI("resource/{$id}/logs/")); } else if ($lease) { $id = $lease->getID(); $crumbs->addTextCrumb( pht('Leases'), $this->getApplicationURI('lease/')); $crumbs->addTextCrumb( $lease->getLeaseName(), $this->getApplicationURI("lease/{$id}/")); $crumbs->addTextCrumb( pht('Logs'), $this->getApplicationURI("lease/{$id}/logs/")); } return $crumbs; } } diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php index f97081e67..4809cf970 100644 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -1,183 +1,186 @@ <?php final class DrydockResourceViewController extends DrydockResourceController { public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); $resource = id(new DrydockResourceQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needUnconsumedCommands(true) ->executeOne(); if (!$resource) { return new Aphront404Response(); } - $title = pht('Resource %s %s', $resource->getID(), $resource->getName()); + $title = pht( + 'Resource %s %s', + $resource->getID(), + $resource->getResourceName()); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setPolicyObject($resource) ->setHeader($title); if ($resource->isReleasing()) { $header->setStatus('fa-exclamation-triangle', 'red', pht('Releasing')); } $actions = $this->buildActionListView($resource); $properties = $this->buildPropertyListView($resource, $actions); $id = $resource->getID(); $resource_uri = $this->getApplicationURI("resource/{$id}/"); $log_query = id(new DrydockLogQuery()) ->withResourcePHIDs(array($resource->getPHID())); $log_box = $this->buildLogBox( $log_query, $this->getApplicationURI("resource/{$id}/logs/query/all/")); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Resource %d', $resource->getID())); $locks = $this->buildLocksTab($resource->getPHID()); $commands = $this->buildCommandsTab($resource->getPHID()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties, pht('Properties')) ->addPropertyList($locks, pht('Slot Locks')) ->addPropertyList($commands, pht('Commands')); $lease_box = $this->buildLeaseBox($resource); return $this->buildApplicationPage( array( $crumbs, $object_box, $lease_box, $log_box, ), array( 'title' => $title, )); } private function buildActionListView(DrydockResource $resource) { $viewer = $this->getViewer(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($resource); $can_release = $resource->canRelease(); if ($resource->isReleasing()) { $can_release = false; } $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $resource, PhabricatorPolicyCapability::CAN_EDIT); $uri = '/resource/'.$resource->getID().'/release/'; $uri = $this->getApplicationURI($uri); $view->addAction( id(new PhabricatorActionView()) ->setHref($uri) ->setName(pht('Release Resource')) ->setIcon('fa-times') ->setWorkflow(true) ->setDisabled(!$can_release || !$can_edit)); return $view; } private function buildPropertyListView( DrydockResource $resource, PhabricatorActionListView $actions) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setActionList($actions); $status = $resource->getStatus(); $status = DrydockResourceStatus::getNameForStatus($status); $view->addProperty( pht('Status'), $status); $until = $resource->getUntil(); if ($until) { $until_display = phabricator_datetime($until, $viewer); } else { $until_display = phutil_tag('em', array(), pht('Never')); } $view->addProperty(pht('Expires'), $until_display); $view->addProperty( pht('Resource Type'), $resource->getType()); $view->addProperty( pht('Blueprint'), $viewer->renderHandle($resource->getBlueprintPHID())); $attributes = $resource->getAttributes(); if ($attributes) { $view->addSectionHeader( pht('Attributes'), 'fa-list-ul'); foreach ($attributes as $key => $value) { $view->addProperty($key, $value); } } return $view; } private function buildLeaseBox(DrydockResource $resource) { $viewer = $this->getViewer(); $leases = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withResourcePHIDs(array($resource->getPHID())) ->withStatuses( array( DrydockLeaseStatus::STATUS_PENDING, DrydockLeaseStatus::STATUS_ACQUIRED, DrydockLeaseStatus::STATUS_ACTIVE, )) ->setLimit(100) ->execute(); $id = $resource->getID(); $leases_uri = "resource/{$id}/leases/query/all/"; $leases_uri = $this->getApplicationURI($leases_uri); $lease_header = id(new PHUIHeaderView()) ->setHeader(pht('Active Leases')) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setHref($leases_uri) ->setIconFont('fa-search') ->setText(pht('View All Leases'))); $lease_list = id(new DrydockLeaseListView()) ->setUser($viewer) ->setLeases($leases) ->render() ->setNoDataString(pht('This resource has no active leases.')); return id(new PHUIObjectBoxView()) ->setHeader($lease_header) ->setObjectList($lease_list); } } diff --git a/src/applications/drydock/phid/DrydockResourcePHIDType.php b/src/applications/drydock/phid/DrydockResourcePHIDType.php index 6b266ff16..9eb85e756 100644 --- a/src/applications/drydock/phid/DrydockResourcePHIDType.php +++ b/src/applications/drydock/phid/DrydockResourcePHIDType.php @@ -1,42 +1,42 @@ <?php final class DrydockResourcePHIDType extends PhabricatorPHIDType { const TYPECONST = 'DRYR'; public function getTypeName() { return pht('Drydock Resource'); } public function newObject() { return new DrydockResource(); } protected function buildQueryForObjects( PhabricatorObjectQuery $query, array $phids) { return id(new DrydockResourceQuery()) ->withPHIDs($phids); } public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects) { foreach ($handles as $phid => $handle) { $resource = $objects[$phid]; $id = $resource->getID(); $handle->setName( pht( 'Resource %d: %s', $id, - $resource->getName())); + $resource->getResourceName())); $handle->setURI("/drydock/resource/{$id}/"); } } } diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php index 429e5c297..a0d440e4e 100644 --- a/src/applications/drydock/storage/DrydockBlueprint.php +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -1,325 +1,335 @@ <?php /** * @task resource Allocating Resources * @task lease Acquiring Leases */ final class DrydockBlueprint extends DrydockDAO implements PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface, PhabricatorCustomFieldInterface { protected $className; protected $blueprintName; protected $viewPolicy; protected $editPolicy; protected $details = array(); protected $isDisabled; private $implementation = self::ATTACHABLE; private $customFields = self::ATTACHABLE; private $fields = null; public static function initializeNewBlueprint(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorDrydockApplication')) ->executeOne(); $view_policy = $app->getPolicy( DrydockDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( DrydockDefaultEditCapability::CAPABILITY); return id(new DrydockBlueprint()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setBlueprintName('') ->setIsDisabled(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'className' => 'text255', 'blueprintName' => 'sort255', 'isDisabled' => 'bool', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DrydockBlueprintPHIDType::TYPECONST); } public function getImplementation() { return $this->assertAttached($this->implementation); } public function attachImplementation(DrydockBlueprintImplementation $impl) { $this->implementation = $impl; return $this; } public function hasImplementation() { return ($this->implementation !== self::ATTACHABLE); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getFieldValue($key) { $key = "std:drydock:core:{$key}"; $fields = $this->loadCustomFields(); $field = idx($fields, $key); if (!$field) { throw new Exception( pht( 'Unknown blueprint field "%s"!', $key)); } return $field->getBlueprintFieldValue(); } private function loadCustomFields() { if ($this->fields === null) { $field_list = PhabricatorCustomField::getObjectFields( $this, PhabricatorCustomField::ROLE_VIEW); $field_list->readFieldsFromStorage($this); $this->fields = $field_list->getFields(); } return $this->fields; } /* -( Allocating Resources )----------------------------------------------- */ /** * @task resource */ public function canEverAllocateResourceForLease(DrydockLease $lease) { return $this->getImplementation()->canEverAllocateResourceForLease( $this, $lease); } /** * @task resource */ public function canAllocateResourceForLease(DrydockLease $lease) { return $this->getImplementation()->canAllocateResourceForLease( $this, $lease); } /** * @task resource */ public function allocateResource(DrydockLease $lease) { return $this->getImplementation()->allocateResource( $this, $lease); } /** * @task resource */ public function activateResource(DrydockResource $resource) { return $this->getImplementation()->activateResource( $this, $resource); } /** * @task resource */ public function destroyResource(DrydockResource $resource) { $this->getImplementation()->destroyResource( $this, $resource); return $this; } + /** + * @task resource + */ + public function getResourceName(DrydockResource $resource) { + return $this->getImplementation()->getResourceName( + $this, + $resource); + } + + /* -( Acquiring Leases )--------------------------------------------------- */ /** * @task lease */ public function canAcquireLeaseOnResource( DrydockResource $resource, DrydockLease $lease) { return $this->getImplementation()->canAcquireLeaseOnResource( $this, $resource, $lease); } /** * @task lease */ public function acquireLease( DrydockResource $resource, DrydockLease $lease) { return $this->getImplementation()->acquireLease( $this, $resource, $lease); } /** * @task lease */ public function activateLease( DrydockResource $resource, DrydockLease $lease) { return $this->getImplementation()->activateLease( $this, $resource, $lease); } /** * @task lease */ public function didReleaseLease( DrydockResource $resource, DrydockLease $lease) { $this->getImplementation()->didReleaseLease( $this, $resource, $lease); return $this; } /** * @task lease */ public function destroyLease( DrydockResource $resource, DrydockLease $lease) { $this->getImplementation()->destroyLease( $this, $resource, $lease); return $this; } public function getInterface( DrydockResource $resource, DrydockLease $lease, $type) { $interface = $this->getImplementation() ->getInterface($this, $resource, $lease, $type); if (!$interface) { throw new Exception( pht( 'Unable to build resource interface of type "%s".', $type)); } return $interface; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DrydockBlueprintEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new DrydockBlueprintTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return array(); } public function getCustomFieldBaseClass() { return 'DrydockBlueprintCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php index 1d442620e..7aad1064f 100644 --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -1,311 +1,320 @@ <?php final class DrydockResource extends DrydockDAO implements PhabricatorPolicyInterface { protected $id; protected $phid; protected $blueprintPHID; protected $status; protected $until; protected $type; - protected $name; protected $attributes = array(); protected $capabilities = array(); protected $ownerPHID; private $blueprint = self::ATTACHABLE; private $unconsumedCommands = self::ATTACHABLE; private $isAllocated = false; private $isActivated = false; private $activateWhenAllocated = false; private $slotLocks = array(); protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attributes' => self::SERIALIZATION_JSON, 'capabilities' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( - 'name' => 'text255', 'ownerPHID' => 'phid?', 'status' => 'text32', 'type' => 'text64', 'until' => 'epoch?', ), self::CONFIG_KEY_SCHEMA => array( 'key_type' => array( 'columns' => array('type', 'status'), ), 'key_blueprint' => array( 'columns' => array('blueprintPHID', 'status'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(DrydockResourcePHIDType::TYPECONST); } + public function getResourceName() { + return $this->getBlueprint()->getResourceName($this); + } + public function getAttribute($key, $default = null) { return idx($this->attributes, $key, $default); } public function getAttributesForTypeSpec(array $attribute_names) { return array_select_keys($this->attributes, $attribute_names); } public function setAttribute($key, $value) { $this->attributes[$key] = $value; return $this; } public function getCapability($key, $default = null) { return idx($this->capbilities, $key, $default); } public function getInterface(DrydockLease $lease, $type) { return $this->getBlueprint()->getInterface($this, $lease, $type); } public function getBlueprint() { return $this->assertAttached($this->blueprint); } public function attachBlueprint(DrydockBlueprint $blueprint) { $this->blueprint = $blueprint; return $this; } 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 setActivateWhenAllocated($activate) { $this->activateWhenAllocated = $activate; return $this; } public function needSlotLock($key) { $this->slotLocks[] = $key; return $this; } public function allocateResource() { if ($this->getID()) { throw new Exception( pht( 'Trying to allocate a resource which has already been persisted. '. 'Only new resources may be allocated.')); } + // We expect resources to have a pregenerated PHID, as they should have + // been created by a call to DrydockBlueprint->newResourceTemplate(). + if (!$this->getPHID()) { + throw new Exception( + pht( + 'Trying to allocate a resource with no generated PHID. Use "%s" to '. + 'create new resource templates.', + 'newResourceTemplate()')); + } + $expect_status = DrydockResourceStatus::STATUS_PENDING; $actual_status = $this->getStatus(); if ($actual_status != $expect_status) { throw new Exception( pht( 'Trying to allocate a resource from the wrong status. Status must '. 'be "%s", actually "%s".', $expect_status, $actual_status)); } if ($this->activateWhenAllocated) { $new_status = DrydockResourceStatus::STATUS_ACTIVE; } else { $new_status = DrydockResourceStatus::STATUS_PENDING; } - $phid = $this->generatePHID(); - $this->openTransaction(); try { try { - DrydockSlotLock::acquireLocks($phid, $this->slotLocks); + DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); $this->slotLocks = array(); } catch (DrydockSlotLockException $ex) { $this->logEvent( DrydockSlotLockFailureLogType::LOGCONST, array( 'locks' => $ex->getLockMap(), )); throw $ex; } $this - ->setPHID($phid) ->setStatus($new_status) ->save(); } catch (Exception $ex) { $this->killTransaction(); throw $ex; } $this->saveTransaction(); $this->isAllocated = true; if ($new_status == DrydockResourceStatus::STATUS_ACTIVE) { $this->didActivate(); } return $this; } public function isAllocatedResource() { return $this->isAllocated; } public function activateResource() { if (!$this->getID()) { throw new Exception( pht( 'Trying to activate a resource which has not yet been persisted.')); } $expect_status = DrydockResourceStatus::STATUS_PENDING; $actual_status = $this->getStatus(); if ($actual_status != $expect_status) { throw new Exception( pht( 'Trying to activate a resource from the wrong status. Status must '. 'be "%s", actually "%s".', $expect_status, $actual_status)); } $this->openTransaction(); $this ->setStatus(DrydockResourceStatus::STATUS_ACTIVE) ->save(); DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); $this->slotLocks = array(); $this->saveTransaction(); $this->isActivated = true; $this->didActivate(); return $this; } public function isActivatedResource() { return $this->isActivated; } public function canRelease() { switch ($this->getStatus()) { case DrydockResourceStatus::STATUS_RELEASED: case DrydockResourceStatus::STATUS_DESTROYED: return false; default: return true; } } public function scheduleUpdate($epoch = null) { PhabricatorWorker::scheduleTask( 'DrydockResourceUpdateWorker', array( 'resourcePHID' => $this->getPHID(), 'isExpireTask' => ($epoch !== null), ), array( 'objectPHID' => $this->getPHID(), 'delayUntil' => ($epoch ? (int)$epoch : null), )); } private function didActivate() { $viewer = PhabricatorUser::getOmnipotentUser(); $need_update = false; $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); } } public function canReceiveCommands() { switch ($this->getStatus()) { case DrydockResourceStatus::STATUS_RELEASED: case DrydockResourceStatus::STATUS_BROKEN: case DrydockResourceStatus::STATUS_DESTROYED: return false; default: return true; } } public function logEvent($type, array $data = array()) { $log = id(new DrydockLog()) ->setEpoch(PhabricatorTime::getNow()) ->setType($type) ->setData($data); $log->setResourcePHID($this->getPHID()); $log->setBlueprintPHID($this->getBlueprintPHID()); return $log->save(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBlueprint()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBlueprint()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Resources inherit the policies of their blueprints.'); } } diff --git a/src/applications/drydock/view/DrydockLeaseListView.php b/src/applications/drydock/view/DrydockLeaseListView.php index d3507546a..8c161b00c 100644 --- a/src/applications/drydock/view/DrydockLeaseListView.php +++ b/src/applications/drydock/view/DrydockLeaseListView.php @@ -1,59 +1,50 @@ <?php final class DrydockLeaseListView extends AphrontView { private $leases; public function setLeases(array $leases) { assert_instances_of($leases, 'DrydockLease'); $this->leases = $leases; return $this; } public function render() { $leases = $this->leases; $viewer = $this->getUser(); $view = new PHUIObjectItemListView(); foreach ($leases as $lease) { $item = id(new PHUIObjectItemView()) ->setUser($viewer) ->setHeader($lease->getLeaseName()) ->setHref('/drydock/lease/'.$lease->getID().'/'); - if ($lease->hasAttachedResource()) { - $resource = $lease->getResource(); - - $resource_href = '/drydock/resource/'.$resource->getID().'/'; - $resource_name = $resource->getName(); - + $resource_phid = $lease->getResourcePHID(); + if ($resource_phid) { $item->addAttribute( - phutil_tag( - 'a', - array( - 'href' => $resource_href, - ), - $resource_name)); + $viewer->renderHandle($resource_phid)); } $status = DrydockLeaseStatus::getNameForStatus($lease->getStatus()); $item->addAttribute($status); $item->setEpoch($lease->getDateCreated()); // TODO: Tailor this for clarity. if ($lease->isActivating()) { $item->setStatusIcon('fa-dot-circle-o yellow'); } else if ($lease->isActive()) { $item->setStatusIcon('fa-dot-circle-o green'); } else { $item->setStatusIcon('fa-dot-circle-o red'); } $view->addItem($item); } return $view; } } diff --git a/src/applications/drydock/view/DrydockResourceListView.php b/src/applications/drydock/view/DrydockResourceListView.php index 9b7706c38..739b464bd 100644 --- a/src/applications/drydock/view/DrydockResourceListView.php +++ b/src/applications/drydock/view/DrydockResourceListView.php @@ -1,49 +1,50 @@ <?php final class DrydockResourceListView extends AphrontView { private $resources; public function setResources(array $resources) { assert_instances_of($resources, 'DrydockResource'); $this->resources = $resources; return $this; } public function render() { $resources = $this->resources; $viewer = $this->getUser(); $view = new PHUIObjectItemListView(); foreach ($resources as $resource) { - $name = pht('Resource %d', $resource->getID()).': '.$resource->getName(); + $id = $resource->getID(); $item = id(new PHUIObjectItemView()) - ->setHref('/drydock/resource/'.$resource->getID().'/') - ->setHeader($name); + ->setHref("/drydock/resource/{$id}/") + ->setObjectName(pht('Resource %d', $id)) + ->setHeader($resource->getResourceName()); $status = DrydockResourceStatus::getNameForStatus($resource->getStatus()); $item->addAttribute($status); switch ($resource->getStatus()) { case DrydockResourceStatus::STATUS_PENDING: $item->setStatusIcon('fa-dot-circle-o yellow'); break; case DrydockResourceStatus::STATUS_ACTIVE: $item->setStatusIcon('fa-dot-circle-o green'); break; case DrydockResourceStatus::STATUS_DESTROYED: $item->setStatusIcon('fa-times-circle-o black'); break; default: $item->setStatusIcon('fa-dot-circle-o red'); break; } $view->addItem($item); } return $view; } } diff --git a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php index f5ecc8389..4ac6a9775 100644 --- a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php +++ b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php @@ -1,731 +1,733 @@ <?php /** * @task update Updating Leases * @task command Processing Commands * @task allocator Drydock Allocator * @task acquire Acquiring Leases * @task activate Activating Leases * @task release Releasing Leases * @task break Breaking Leases * @task destroy Destroying Leases */ final class DrydockLeaseUpdateWorker extends DrydockWorker { protected function doWork() { $lease_phid = $this->getTaskDataValue('leasePHID'); $hash = PhabricatorHash::digestForIndex($lease_phid); $lock_key = 'drydock.lease:'.$hash; $lock = PhabricatorGlobalLock::newLock($lock_key) ->lock(1); try { $lease = $this->loadLease($lease_phid); $this->handleUpdate($lease); } catch (Exception $ex) { $lock->unlock(); throw $ex; } $lock->unlock(); } /* -( Updating Leases )---------------------------------------------------- */ /** * @task update */ private function handleUpdate(DrydockLease $lease) { try { $this->updateLease($lease); } catch (Exception $ex) { if ($this->isTemporaryException($ex)) { $this->yieldLease($lease, $ex); } else { $this->breakLease($lease, $ex); } } } /** * @task update */ private function updateLease(DrydockLease $lease) { $this->processLeaseCommands($lease); $lease_status = $lease->getStatus(); switch ($lease_status) { case DrydockLeaseStatus::STATUS_PENDING: $this->executeAllocator($lease); break; case DrydockLeaseStatus::STATUS_ACQUIRED: $this->activateLease($lease); break; case DrydockLeaseStatus::STATUS_ACTIVE: // Nothing to do. break; case DrydockLeaseStatus::STATUS_RELEASED: case DrydockLeaseStatus::STATUS_BROKEN: $this->destroyLease($lease); break; case DrydockLeaseStatus::STATUS_DESTROYED: break; } $this->yieldIfExpiringLease($lease); } /** * @task update */ private function yieldLease(DrydockLease $lease, Exception $ex) { $duration = $this->getYieldDurationFromException($ex); $lease->logEvent( DrydockLeaseActivationYieldLogType::LOGCONST, array( 'duration' => $duration, )); throw new PhabricatorWorkerYieldException($duration); } /* -( Processing Commands )------------------------------------------------ */ /** * @task command */ private function processLeaseCommands(DrydockLease $lease) { if (!$lease->canReceiveCommands()) { return; } $this->checkLeaseExpiration($lease); $commands = $this->loadCommands($lease->getPHID()); foreach ($commands as $command) { if (!$lease->canReceiveCommands()) { break; } $this->processLeaseCommand($lease, $command); $command ->setIsConsumed(true) ->save(); } } /** * @task command */ private function processLeaseCommand( DrydockLease $lease, DrydockCommand $command) { switch ($command->getCommand()) { case DrydockCommand::COMMAND_RELEASE: $this->releaseLease($lease); break; } } /* -( Drydock Allocator )-------------------------------------------------- */ /** * Find or build a resource which can satisfy a given lease request, then * acquire the lease. * * @param DrydockLease Requested lease. * @return void * @task allocator */ private function executeAllocator(DrydockLease $lease) { $blueprints = $this->loadBlueprintsForAllocatingLease($lease); // If we get nothing back, that means no blueprint is defined which can // ever build the requested resource. This is a permanent failure, since // we don't expect to succeed no matter how many times we try. if (!$blueprints) { throw new PhabricatorWorkerPermanentFailureException( pht( 'No active Drydock blueprint exists which can ever allocate a '. 'resource for lease "%s".', $lease->getPHID())); } // First, try to find a suitable open resource which we can acquire a new // lease on. $resources = $this->loadResourcesForAllocatingLease($blueprints, $lease); // If no resources exist yet, see if we can build one. if (!$resources) { $usable_blueprints = $this->removeOverallocatedBlueprints( $blueprints, $lease); // If we get nothing back here, some blueprint claims it can eventually // satisfy the lease, just not right now. This is a temporary failure, // and we expect allocation to succeed eventually. if (!$usable_blueprints) { $lease->logEvent( DrydockLeaseWaitingForResourcesLogType::LOGCONST, array( 'blueprintPHIDs' => mpull($blueprints, 'getPHID'), )); throw new PhabricatorWorkerYieldException(15); } $usable_blueprints = $this->rankBlueprints($usable_blueprints, $lease); $exceptions = array(); foreach ($usable_blueprints as $blueprint) { try { $resources[] = $this->allocateResource($blueprint, $lease); // Bail after allocating one resource, we don't need any more than // this. break; } catch (Exception $ex) { $exceptions[] = $ex; } } if (!$resources) { throw new PhutilAggregateException( pht( 'All blueprints failed to allocate a suitable new resource when '. 'trying to allocate lease "%s".', $lease->getPHID()), $exceptions); } // NOTE: We have not acquired the lease yet, so it is possible that the // resource we just built will be snatched up by some other lease before // we can. This is not problematic: we'll retry a little later and should // suceed eventually. } $resources = $this->rankResources($resources, $lease); $exceptions = array(); $allocated = false; foreach ($resources as $resource) { try { $this->acquireLease($resource, $lease); $allocated = true; break; } catch (Exception $ex) { $exceptions[] = $ex; } } if (!$allocated) { throw new PhutilAggregateException( pht( 'Unable to acquire lease "%s" on any resouce.', $lease->getPHID()), $exceptions); } } /** * Get all the @{class:DrydockBlueprintImplementation}s which can possibly * build a resource to satisfy a lease. * * This method returns blueprints which might, at some time, be able to * build a resource which can satisfy the lease. They may not be able to * build that resource right now. * * @param DrydockLease Requested lease. * @return list<DrydockBlueprintImplementation> List of qualifying blueprint * implementations. * @task allocator */ private function loadBlueprintImplementationsForAllocatingLease( DrydockLease $lease) { $impls = DrydockBlueprintImplementation::getAllBlueprintImplementations(); $keep = array(); foreach ($impls as $key => $impl) { // Don't use disabled blueprint types. if (!$impl->isEnabled()) { continue; } // Don't use blueprint types which can't allocate the correct kind of // resource. if ($impl->getType() != $lease->getResourceType()) { continue; } if (!$impl->canAnyBlueprintEverAllocateResourceForLease($lease)) { continue; } $keep[$key] = $impl; } return $keep; } /** * Get all the concrete @{class:DrydockBlueprint}s which can possibly * build a resource to satisfy a lease. * * @param DrydockLease Requested lease. * @return list<DrydockBlueprint> List of qualifying blueprints. * @task allocator */ private function loadBlueprintsForAllocatingLease( DrydockLease $lease) { $viewer = $this->getViewer(); $impls = $this->loadBlueprintImplementationsForAllocatingLease($lease); if (!$impls) { return array(); } $blueprints = id(new DrydockBlueprintQuery()) ->setViewer($viewer) ->withBlueprintClasses(array_keys($impls)) ->withDisabled(false) ->execute(); $keep = array(); foreach ($blueprints as $key => $blueprint) { if (!$blueprint->canEverAllocateResourceForLease($lease)) { continue; } $keep[$key] = $blueprint; } return $keep; } /** * Load a list of all resources which a given lease can possibly be * allocated against. * * @param list<DrydockBlueprint> Blueprints which may produce suitable * resources. * @param DrydockLease Requested lease. * @return list<DrydockResource> Resources which may be able to allocate * the lease. * @task allocator */ private function loadResourcesForAllocatingLease( array $blueprints, DrydockLease $lease) { assert_instances_of($blueprints, 'DrydockBlueprint'); $viewer = $this->getViewer(); $resources = id(new DrydockResourceQuery()) ->setViewer($viewer) ->withBlueprintPHIDs(mpull($blueprints, 'getPHID')) ->withTypes(array($lease->getResourceType())) ->withStatuses( array( DrydockResourceStatus::STATUS_PENDING, DrydockResourceStatus::STATUS_ACTIVE, )) ->execute(); $keep = array(); foreach ($resources as $key => $resource) { $blueprint = $resource->getBlueprint(); if (!$blueprint->canAcquireLeaseOnResource($resource, $lease)) { continue; } $keep[$key] = $resource; } return $keep; } /** * Remove blueprints which are too heavily allocated to build a resource for * a lease from a list of blueprints. * * @param list<DrydockBlueprint> List of blueprints. * @return list<DrydockBlueprint> List with blueprints that can not allocate * a resource for the lease right now removed. * @task allocator */ private function removeOverallocatedBlueprints( array $blueprints, DrydockLease $lease) { assert_instances_of($blueprints, 'DrydockBlueprint'); $keep = array(); foreach ($blueprints as $key => $blueprint) { if (!$blueprint->canAllocateResourceForLease($lease)) { continue; } $keep[$key] = $blueprint; } return $keep; } /** * Rank blueprints by suitability for building a new resource for a * particular lease. * * @param list<DrydockBlueprint> List of blueprints. * @param DrydockLease Requested lease. * @return list<DrydockBlueprint> Ranked list of blueprints. * @task allocator */ private function rankBlueprints(array $blueprints, DrydockLease $lease) { assert_instances_of($blueprints, 'DrydockBlueprint'); // TODO: Implement improvements to this ranking algorithm if they become // available. shuffle($blueprints); return $blueprints; } /** * Rank resources by suitability for allocating a particular lease. * * @param list<DrydockResource> List of resources. * @param DrydockLease Requested lease. * @return list<DrydockResource> Ranked list of resources. * @task allocator */ private function rankResources(array $resources, DrydockLease $lease) { assert_instances_of($resources, 'DrydockResource'); // TODO: Implement improvements to this ranking algorithm if they become // available. shuffle($resources); return $resources; } /** * Perform an actual resource allocation with a particular blueprint. * * @param DrydockBlueprint The blueprint to allocate a resource from. * @param DrydockLease Requested lease. * @return DrydockResource Allocated resource. * @task allocator */ private function allocateResource( DrydockBlueprint $blueprint, DrydockLease $lease) { $resource = $blueprint->allocateResource($lease); $this->validateAllocatedResource($blueprint, $resource, $lease); // If this resource was allocated as a pending resource, queue a task to // activate it. if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) { PhabricatorWorker::scheduleTask( 'DrydockResourceUpdateWorker', array( 'resourcePHID' => $resource->getPHID(), ), array( 'objectPHID' => $resource->getPHID(), )); } return $resource; } /** * Check that the resource a blueprint allocated is roughly the sort of * object we expect. * * @param DrydockBlueprint Blueprint which built the resource. * @param wild Thing which the blueprint claims is a valid resource. * @param DrydockLease Lease the resource was allocated for. * @return void * @task allocator */ private function validateAllocatedResource( DrydockBlueprint $blueprint, $resource, DrydockLease $lease) { if (!($resource instanceof DrydockResource)) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: %s must '. 'return an object of type %s or throw, but returned something else.', $blueprint->getBlueprintName(), $blueprint->getClassName(), 'allocateResource()', 'DrydockResource')); } if (!$resource->isAllocatedResource()) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: %s '. 'must actually allocate the resource it returns.', $blueprint->getBlueprintName(), $blueprint->getClassName(), 'allocateResource()')); } $resource_type = $resource->getType(); $lease_type = $lease->getResourceType(); if ($resource_type !== $lease_type) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: it '. 'built a resource of type "%s" to satisfy a lease requesting a '. 'resource of type "%s".', $blueprint->getBlueprintName(), $blueprint->getClassName(), $resource_type, $lease_type)); } } /* -( Acquiring Leases )--------------------------------------------------- */ /** * Perform an actual lease acquisition on a particular resource. * * @param DrydockResource Resource to acquire a lease on. * @param DrydockLease Lease to acquire. * @return void * @task acquire */ private function acquireLease( DrydockResource $resource, DrydockLease $lease) { $blueprint = $resource->getBlueprint(); $blueprint->acquireLease($resource, $lease); $this->validateAcquiredLease($blueprint, $resource, $lease); // If this lease has been acquired but not activated, queue a task to // activate it. if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACQUIRED) { PhabricatorWorker::scheduleTask( __CLASS__, array( 'leasePHID' => $lease->getPHID(), ), array( 'objectPHID' => $lease->getPHID(), )); } } /** * Make sure that a lease was really acquired properly. * * @param DrydockBlueprint Blueprint which created the resource. * @param DrydockResource Resource which was acquired. * @param DrydockLease The lease which was supposedly acquired. * @return void * @task acquire */ private function validateAcquiredLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { if (!$lease->isAcquiredLease()) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: it '. 'returned from "%s" without acquiring a lease.', $blueprint->getBlueprintName(), $blueprint->getClassName(), 'acquireLease()')); } $lease_phid = $lease->getResourcePHID(); $resource_phid = $resource->getPHID(); if ($lease_phid !== $resource_phid) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: it '. 'returned from "%s" with a lease acquired on the wrong resource.', $blueprint->getBlueprintName(), $blueprint->getClassName(), 'acquireLease()')); } } /* -( Activating Leases )-------------------------------------------------- */ /** * @task activate */ private function activateLease(DrydockLease $lease) { $resource = $lease->getResource(); if (!$resource) { throw new Exception( pht('Trying to activate lease with no resource.')); } $resource_status = $resource->getStatus(); if ($resource_status == DrydockResourceStatus::STATUS_PENDING) { throw new PhabricatorWorkerYieldException(15); } if ($resource_status != DrydockResourceStatus::STATUS_ACTIVE) { throw new Exception( pht( 'Trying to activate lease on a dead resource (in status "%s").', $resource_status)); } // NOTE: We can race resource destruction here. Between the time we // performed the read above and now, the resource might have closed, so // we may activate leases on dead resources. At least for now, this seems // fine: a resource dying right before we activate a lease on it should not // be distinguisahble from a resource dying right after we activate a lease // on it. We end up with an active lease on a dead resource either way, and // can not prevent resources dying from lightning strikes. $blueprint = $resource->getBlueprint(); $blueprint->activateLease($resource, $lease); $this->validateActivatedLease($blueprint, $resource, $lease); } /** * @task activate */ private function validateActivatedLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { if (!$lease->isActivatedLease()) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: it '. 'returned from "%s" without activating a lease.', $blueprint->getBlueprintName(), $blueprint->getClassName(), 'acquireLease()')); } } /* -( Releasing Leases )--------------------------------------------------- */ /** * @task release */ private function releaseLease(DrydockLease $lease) { $lease ->setStatus(DrydockLeaseStatus::STATUS_RELEASED) ->save(); $lease->logEvent(DrydockLeaseReleasedLogType::LOGCONST); $resource = $lease->getResource(); $blueprint = $resource->getBlueprint(); $blueprint->didReleaseLease($resource, $lease); $this->destroyLease($lease); } /* -( Breaking Leases )---------------------------------------------------- */ /** * @task break */ protected function breakLease(DrydockLease $lease, Exception $ex) { switch ($lease->getStatus()) { case DrydockLeaseStatus::STATUS_BROKEN: case DrydockLeaseStatus::STATUS_RELEASED: case DrydockLeaseStatus::STATUS_DESTROYED: throw new PhutilProxyException( pht( 'Unexpected failure while destroying lease ("%s").', $lease->getPHID()), $ex); } $lease ->setStatus(DrydockLeaseStatus::STATUS_BROKEN) ->save(); - $lease->scheduleDestruction(); + $lease->scheduleUpdate(); $lease->logEvent( DrydockLeaseActivationFailureLogType::LOGCONST, array( 'class' => get_class($ex), 'message' => $ex->getMessage(), )); throw new PhabricatorWorkerPermanentFailureException( pht( 'Permanent failure while activating lease ("%s"): %s', $lease->getPHID(), $ex->getMessage())); } /* -( Destroying Leases )-------------------------------------------------- */ /** * @task destroy */ private function destroyLease(DrydockLease $lease) { $resource = $lease->getResource(); - $blueprint = $resource->getBlueprint(); - $blueprint->destroyLease($resource, $lease); + if ($resource) { + $blueprint = $resource->getBlueprint(); + $blueprint->destroyLease($resource, $lease); + } DrydockSlotLock::releaseLocks($lease->getPHID()); $lease ->setStatus(DrydockLeaseStatus::STATUS_DESTROYED) ->save(); $lease->logEvent(DrydockLeaseDestroyedLogType::LOGCONST); } }