diff --git a/conf/default.conf.php b/conf/default.conf.php index 19dd22e12b..d6f3e6293d 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -671,6 +671,14 @@ return array( // projects that want to expose an activity feed on the project homepage. 'feed.public' => false, + +// -- Drydock --------------------------------------------------------------- // + + // If you want to use Drydock's builtin EC2 Blueprints, configure your AWS + // EC2 credentials here. + 'amazon-ec2.access-key' => null, + 'amazon-ec2.secret-key' => null, + // -- Customization --------------------------------------------------------- // // Paths to additional phutil libraries to load. diff --git a/resources/sql/patches/099.drydock.sql b/resources/sql/patches/099.drydock.sql new file mode 100644 index 0000000000..da269a658a --- /dev/null +++ b/resources/sql/patches/099.drydock.sql @@ -0,0 +1,29 @@ +CREATE DATABASE IF NOT EXISTS phabricator_drydock; + +CREATE TABLE phabricator_drydock.drydock_resource ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) BINARY NOT NULL, + name VARCHAR(255) NOT NULL, + ownerPHID varchar(64) BINARY, + status INT UNSIGNED NOT NULL, + blueprintClass VARCHAR(255) NOT NULL, + type VARCHAR(64) NOT NULL, + attributes LONGBLOB NOT NULL, + capabilities LONGBLOB NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY (phid) +) ENGINE=InnoDB; + +CREATE TABLE phabricator_drydock.drydock_lease ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) BINARY NOT NULL, + resourceID INT UNSIGNED, + status INT UNSIGNED NOT NULL, + until INT UNSIGNED, + ownerPHID VARCHAR(64) BINARY, + attributes LONGBLOB NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY (phid) +) ENGINE=InnoDB; diff --git a/scripts/drydock/drydock_control.php b/scripts/drydock/drydock_control.php new file mode 100755 index 0000000000..edaa84e5ee --- /dev/null +++ b/scripts/drydock/drydock_control.php @@ -0,0 +1,42 @@ +#!/usr/bin/env php +setResourceType('host'); +$lease = $allocator->allocate(); + +$i_file = $lease->getInterface('command'); + +list($stdout) = $i_file->execx('ls / ; echo -- ; uptime ; echo -- ; uname -n'); +echo $stdout; + + +$lease->release(); + + +//$i_http = $lease->getInterface('httpd'); +//echo $i_http->getURI('/index.html')."\n"; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 1812e8915a..8d31e0fa2d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -311,6 +311,25 @@ phutil_register_library_map(array( 'DiffusionSymbolController' => 'applications/diffusion/controller/symbol', 'DiffusionSymbolQuery' => 'applications/diffusion/query/symbol', 'DiffusionView' => 'applications/diffusion/view/base', + 'DrydockAllocator' => 'applications/drydock/allocator/resource', + 'DrydockBlueprint' => 'applications/drydock/blueprint/base', + 'DrydockCommandInterface' => 'applications/drydock/interface/command/base', + 'DrydockConstants' => 'applications/drydock/constants/base', + 'DrydockController' => 'applications/drydock/controller/base', + 'DrydockDAO' => 'applications/drydock/storage/base', + 'DrydockEC2HostBlueprint' => 'applications/drydock/blueprint/ec2host', + 'DrydockInterface' => 'applications/drydock/interface/base', + 'DrydockLease' => 'applications/drydock/storage/lease', + 'DrydockLeaseListController' => 'applications/drydock/controller/leaselist', + 'DrydockLeaseStatus' => 'applications/drydock/constants/leasestatus', + 'DrydockLocalCommandInterface' => 'applications/drydock/interface/command/local', + 'DrydockLocalHostBlueprint' => 'applications/drydock/blueprint/localhost', + 'DrydockRemoteHostBlueprint' => 'applications/drydock/blueprint/remotehost', + 'DrydockResource' => 'applications/drydock/storage/resource', + 'DrydockResourceAllocateController' => 'applications/drydock/controller/resourceallocate', + 'DrydockResourceListController' => 'applications/drydock/controller/resourcelist', + 'DrydockResourceStatus' => 'applications/drydock/constants/resourcestatus', + 'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/ssh', 'HeraldAction' => 'applications/herald/storage/action', 'HeraldActionConfig' => 'applications/herald/config/action', 'HeraldAllRulesController' => 'applications/herald/controller/all', @@ -1039,6 +1058,21 @@ phutil_register_library_map(array( 'DiffusionSvnRequest' => 'DiffusionRequest', 'DiffusionSymbolController' => 'DiffusionController', 'DiffusionView' => 'AphrontView', + 'DrydockCommandInterface' => 'DrydockInterface', + 'DrydockController' => 'PhabricatorController', + 'DrydockDAO' => 'PhabricatorLiskDAO', + 'DrydockEC2HostBlueprint' => 'DrydockRemoteHostBlueprint', + 'DrydockLease' => 'DrydockDAO', + 'DrydockLeaseListController' => 'DrydockController', + 'DrydockLeaseStatus' => 'DrydockConstants', + 'DrydockLocalCommandInterface' => 'DrydockCommandInterface', + 'DrydockLocalHostBlueprint' => 'DrydockBlueprint', + 'DrydockRemoteHostBlueprint' => 'DrydockBlueprint', + 'DrydockResource' => 'DrydockDAO', + 'DrydockResourceAllocateController' => 'DrydockController', + 'DrydockResourceListController' => 'DrydockController', + 'DrydockResourceStatus' => 'DrydockConstants', + 'DrydockSSHCommandInterface' => 'DrydockCommandInterface', 'HeraldAction' => 'HeraldDAO', 'HeraldAllRulesController' => 'HeraldController', 'HeraldApplyTranscript' => 'HeraldDAO', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index f420b3bfa7..195e80872f 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -380,6 +380,18 @@ class AphrontDefaultApplicationConfiguration '/calendar/' => array( '$' => 'PhabricatorCalendarBrowseController', ), + + '/drydock/' => array( + '$' => 'DrydockResourceListController', + 'resource/$' => 'DrydockResourceListController', + 'resource/allocate/$' => 'DrydockResourceAllocateController', + 'host/' => array( + '$' => 'DrydockHostListController', + 'edit/$' => 'DrydockHostEditController', + 'edit/(?P\d+)/$' => 'DrydockhostEditController', + ), + 'lease/$' => 'DrydockLeaseListController', + ), ); } diff --git a/src/applications/drydock/allocator/resource/DrydockAllocator.php b/src/applications/drydock/allocator/resource/DrydockAllocator.php new file mode 100644 index 0000000000..f82b8fd1cb --- /dev/null +++ b/src/applications/drydock/allocator/resource/DrydockAllocator.php @@ -0,0 +1,88 @@ +resourceType = $resource_type; + return $this; + } + + public function getResourceType() { + return $this->resourceType; + } + + public function getPendingLease() { + if (!$this->lease) { + $lease = new DrydockLease(); + $lease->setStatus(DrydockLeaseStatus::STATUS_PENDING); + $lease->save(); + + $this->lease = $lease; + } + return $lease; + } + + public function allocate() { + $type = $this->getResourceType(); + + $candidates = id(new DrydockResource())->loadAllWhere( + 'type = %s AND status = %s', + $type, + DrydockResourceStatus::STATUS_OPEN); + + if ($candidates) { + shuffle($candidates); + $resource = head($candidates); + } else { + $blueprints = DrydockBlueprint::getAllBlueprintsForResource($type); + + foreach ($blueprints as $key => $blueprint) { + if (!$blueprint->canAllocateResources()) { + unset($blueprints[$key]); + continue; + } + } + + if (!$blueprints) { + throw new Exception( + "There are no valid existing '{$type}' resources, and no valid ". + "blueprints to build new ones."); + } + + // TODO: Rank intelligently. + shuffle($blueprints); + + $blueprint = head($blueprints); + $resource = $blueprint->allocateResource(); + } + + $lease = $this->getPendingLease(); + $lease->setResourceID($resource->getID()); + $lease->setStatus(DrydockLeaseStatus::STATUS_ACTIVE); + $lease->save(); + + $lease->attachResource($resource); + + return $lease; + } + +} diff --git a/src/applications/drydock/allocator/resource/__init__.php b/src/applications/drydock/allocator/resource/__init__.php new file mode 100644 index 0000000000..f057679f5c --- /dev/null +++ b/src/applications/drydock/allocator/resource/__init__.php @@ -0,0 +1,18 @@ +setType('class') + ->setAncestorClass('DrydockBlueprint') + ->selectAndLoadSymbols(); + $list = ipull($blueprints, 'name', 'name'); + foreach ($list as $class_name => $ignored) { + $reflection = new ReflectionClass($class_name); + if ($reflection->isAbstract()) { + continue; + } + $list[$class_name] = newv($class_name, array()); + } + } + + return $list; + } + + public static function getAllBlueprintsForResource($type) { + static $groups = null; + if ($groups === null) { + $groups = mgroup(self::getAllBlueprints(), 'getType'); + } + return idx($groups, $type, array()); + } + +} diff --git a/src/applications/drydock/blueprint/base/__init__.php b/src/applications/drydock/blueprint/base/__init__.php new file mode 100644 index 0000000000..4ea31b3c9d --- /dev/null +++ b/src/applications/drydock/blueprint/base/__init__.php @@ -0,0 +1,13 @@ +setBlueprintClass(get_class($this)); + $resource->setType($this->getType()); + $resource->setStatus(DrydockResourceStatus::STATUS_PENDING); + $resource->setName('EC2 Host'); + $resource->save(); + + $resource->setStatus(DrydockResourceStatus::STATUS_ALLOCATING); + $resource->save(); + + $xml = $this->executeEC2Query( + 'RunInstances', + array( + 'ImageId' => 'ami-c7c99982', + 'MinCount' => 1, + 'MaxCount' => 1, + 'KeyName' => 'ec2wc', + 'SecurityGroupId.1' => 'sg-6bffff2e', + 'InstanceType' => 't1.micro', + )); + + $instance_id = (string)$xml->instancesSet[0]->item[0]->instanceId[0]; + + echo "instance id: ".$instance_id."\n"; + + $n = 1; + do { + $xml = $this->executeEC2Query( + 'DescribeInstances', + array( + 'InstanceId.1' => $instance_id, + )); + + var_dump($xml); + + $instance = $xml->reservationSet[0]->item[0]->instancesSet[0]->item[0]; + + $state = (string)$instance->instanceState[0]->name; + + echo "State = {$state}\n"; + + if ($state == 'pending') { + sleep(min($n++, 15)); + } else if ($state == 'running') { + break; + } else { + // TODO: Communicate this failure. + $resource->setStatus(DrydockResourceStatus::STATUS_BROKEN); + $resource->save(); + } + } while (true); + + + $n = 1; + do { + $xml = $this->executeEC2Query( + 'DescribeInstanceStatus', + array( + 'InstanceId' => $instance_id, + )); + + var_dump($xml); + + $item = $xml->instanceStatusSet[0]->item[0]; + + $system_status = (string)$item->systemStatus->status[0]; + $instance_status = (string)$item->instanceStatus->status[0]; + + if (($system_status == 'initializing') || + ($instance_status == 'initializing')) { + sleep(min($n++, 15)); + } else if (($system_status == 'ok') && + ($instance_status == 'ok')) { + break; + } else { + // TODO: Communicate this failure. + $resource->setStatus(DrydockResourceStatus::STATUS_BROKEN); + $resource->save(); + } + } while (true); + + // TODO: This is a fuzz factor because sshd doesn't come up immediately + // once EC2 reports the machine reachable. Validate that SSH is actually + // responsive. + sleep(120); + + $resource->setAttributes( + array( + 'host' => (string)$instance->dnsName, + 'user' => 'ec2-user', + 'ssh-keyfile' => '/Users/epriestley/.ssh/id_ec2w', + )); + $resource->setName($resource->getName().' ('.$instance->dnsName.')'); + $resource->setStatus(DrydockResourceStatus::STATUS_OPEN); + $resource->save(); + + return $resource; + } + + public function getInterface( + DrydockResource $resource, + DrydockLease $lease, + $type) { + + switch ($type) { + case 'command': + $ssh = new DrydockSSHCommandInterface(); + $ssh->setConfiguration( + array( + 'host' => $resource->getAttribute('host'), + 'user' => $resource->getAttribute('user'), + 'ssh-keyfile' => $resource->getAttribute('ssh-keyfile'), + )); + return $ssh; + } + + throw new Exception("No interface of type '{$type}'."); + } + + private function executeEC2Query($action, array $params) { + $future = new PhutilAWSEC2Future(); + $future->setAWSKeys( + PhabricatorEnv::getEnvConfig('amazon-ec2.access-key'), + PhabricatorEnv::getEnvConfig('amazon-ec2.secret-key')); + $future->setRawAWSQuery($action, $params); + return $future->resolve(); + } + +} diff --git a/src/applications/drydock/blueprint/ec2host/__init__.php b/src/applications/drydock/blueprint/ec2host/__init__.php new file mode 100644 index 0000000000..9bc6905978 --- /dev/null +++ b/src/applications/drydock/blueprint/ec2host/__init__.php @@ -0,0 +1,18 @@ +setConfiguration( + array( + 'host' => 'secure.phabricator.com', + 'user' => 'ec2-user', + 'ssh-keyfile' => '/Users/epriestley/.ssh/id_ec2w', + )); + return $ssh; + } + + throw new Exception("No interface of type '{$type}'."); + } + +} diff --git a/src/applications/drydock/blueprint/remotehost/__init__.php b/src/applications/drydock/blueprint/remotehost/__init__.php new file mode 100644 index 0000000000..d26b36bf49 --- /dev/null +++ b/src/applications/drydock/blueprint/remotehost/__init__.php @@ -0,0 +1,13 @@ + 'Pending', + self::STATUS_ACTIVE => 'Active', + self::STATUS_RELEASED => 'Released', + self::STATUS_BROKEN => 'Broken', + self::STATUS_EXPIRED => 'Expired', + ); + + return idx($map, $status, 'Unknown'); + } + +} diff --git a/src/applications/drydock/constants/leasestatus/__init__.php b/src/applications/drydock/constants/leasestatus/__init__.php new file mode 100644 index 0000000000..389548a125 --- /dev/null +++ b/src/applications/drydock/constants/leasestatus/__init__.php @@ -0,0 +1,14 @@ + 'Pending', + self::STATUS_ALLOCATING => 'Pending', + self::STATUS_OPEN => 'Open', + self::STATUS_CLOSED => 'Closed', + self::STATUS_BROKEN => 'Broken', + self::STATUS_DESTROYED => 'Destroyed', + ); + + return idx($map, $status, 'Unknown'); + } + +} diff --git a/src/applications/drydock/constants/resourcestatus/__init__.php b/src/applications/drydock/constants/resourcestatus/__init__.php new file mode 100644 index 0000000000..6fca697315 --- /dev/null +++ b/src/applications/drydock/constants/resourcestatus/__init__.php @@ -0,0 +1,14 @@ +buildStandardPageView(); + + $page->setApplicationName('Drydock'); + $page->setBaseURI('/drydock/'); + $page->setTitle(idx($data, 'title')); + $page->setGlyph("\xE2\x98\x82"); + + $page->appendChild($view); + + $help_uri = PhabricatorEnv::getDoclink('article/Drydock_User_Guide.html'); + $page->setTabs( + array( + 'help' => array( + 'name' => 'Help', + 'href' => $help_uri, + ), + ), null); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + + final protected function buildSideNav($selected) { + $items = array( + 'resourcelist' => array( + 'href' => '/drydock/resource/', + 'name' => 'Resources', + ), + 'leaselist' => array( + 'href' => '/drydock/lease/', + 'name' => 'Leases', + ), + ); + + $nav = new AphrontSideNavView(); + foreach ($items as $key => $info) { + $nav->addNavItem( + phutil_render_tag( + 'a', + array( + 'href' => $info['href'], + 'class' => ($key == $selected ? 'aphront-side-nav-selected' : null), + ), + phutil_escape_html($info['name']))); + } + + return $nav; + } + +} diff --git a/src/applications/drydock/controller/base/__init__.php b/src/applications/drydock/controller/base/__init__.php new file mode 100644 index 0000000000..2c0763732e --- /dev/null +++ b/src/applications/drydock/controller/base/__init__.php @@ -0,0 +1,18 @@ +getRequest(); + $user = $request->getUser(); + + $nav = $this->buildSideNav('leaselist'); + + $pager = new AphrontPagerView(); + $pager->setURI(new PhutilURI('/drydock/lease/'), 'page'); + + $data = id(new DrydockLease())->loadAllWhere( + '1 = 1 ORDER BY id DESC LIMIT %d, %d', + $pager->getOffset(), + $pager->getPageSize() + 1); + $data = $pager->sliceResults($data); + + $phids = mpull($data, 'getOwnerPHID'); + $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); + + $resource_ids = mpull($data, 'getResourceID'); + $resources = array(); + if ($resource_ids) { + $resources = id(new DrydockResource())->loadAllWhere( + 'id IN (%Ld)', + $resource_ids); + } + + $rows = array(); + foreach ($data as $lease) { + $resource = idx($resources, $lease->getResourceID()); + $rows[] = array( + $lease->getID(), + DrydockLeaseStatus::getNameForStatus($lease->getStatus()), + ($lease->getOwnerPHID() + ? $handles[$lease->getOwnerPHID()]->renderLink() + : null), + $lease->getResourceID(), + ($resource + ? phutil_escape_html($resource->getName()) + : null), + phabricator_datetime($lease->getDateCreated(), $user), + ); + } + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + 'ID', + 'Status', + 'Owner', + 'Resource ID', + 'Resource', + 'Created', + )); + $table->setColumnClasses( + array( + '', + '', + '', + '', + 'wide pri', + 'right', + )); + + $panel = new AphrontPanelView(); + $panel->setHeader('Drydock Leases'); + + $panel->appendChild($table); + $panel->appendChild($pager); + + $nav->appendChild($panel); + return $this->buildStandardPageResponse( + $nav, + array( + 'title' => 'Leases', + )); + + } + +} diff --git a/src/applications/drydock/controller/leaselist/__init__.php b/src/applications/drydock/controller/leaselist/__init__.php new file mode 100644 index 0000000000..0b396d6e59 --- /dev/null +++ b/src/applications/drydock/controller/leaselist/__init__.php @@ -0,0 +1,24 @@ +getRequest(); + $user = $request->getUser(); + + $resource = new DrydockResource(); + + $json = new PhutilJSON(); + + $err_attributes = true; + $err_capabilities = true; + + $json_attributes = $json->encodeFormatted($resource->getAttributes()); + $json_capabilities = $json->encodeFormatted($resource->getCapabilities()); + + $errors = array(); + + if ($request->isFormPost()) { + $raw_attributes = $request->getStr('attributes'); + $attributes = json_decode($raw_attributes, true); + if (!is_array($attributes)) { + $err_attributes = 'Invalid'; + $errors[] = 'Enter attributes as a valid JSON object.'; + $json_attributes = $raw_attributes; + } else { + $resource->setAttributes($attributes); + $json_attributes = $json->encodeFormatted($attributes); + $err_attributes = null; + } + + $raw_capabilities = $request->getStr('capabilities'); + $capabilities = json_decode($raw_capabilities, true); + if (!is_array($capabilities)) { + $err_capabilities = 'Invalid'; + $errors[] = 'Enter capabilities as a valid JSON object.'; + $json_capabilities = $raw_capabilities; + } else { + $resource->setCapabilities($capabilities); + $json_capabilities = $json->encodeFormatted($capabilities); + $err_capabilities = null; + } + + $resource->setBlueprintClass($request->getStr('blueprint')); + $resource->setType($resource->getBlueprint()->getType()); + $resource->setOwnerPHID($user->getPHID()); + $resource->setName($request->getStr('name')); + + if (!$errors) { + $resource->save(); + return id(new AphrontRedirectResponse()) + ->setURI('/drydock/resource/'); + } + } + + $error_view = null; + if ($errors) { + $error_view = new AphrontErrorView(); + $error_view->setTitle('Form Errors'); + $error_view->setErrors($errors); + } + + + $blueprints = id(new PhutilSymbolLoader()) + ->setType('class') + ->setAncestorClass('DrydockBlueprint') + ->selectAndLoadSymbols(); + $blueprints = ipull($blueprints, 'name', 'name'); + $panel = new AphrontPanelView(); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->setHeader('Allocate Drydock Resource'); + + $form = id(new AphrontFormView()) + ->setUser($request->getUser()) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Name') + ->setName('name') + ->setValue($resource->getName())) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Blueprint') + ->setOptions($blueprints) + ->setName('blueprint') + ->setValue($resource->getBlueprintClass())) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Attributes') + ->setName('attributes') + ->setValue($json_attributes) + ->setError($err_attributes) + ->setCaption('Specify attributes in JSON.')) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Capabilities') + ->setName('capabilities') + ->setValue($json_capabilities) + ->setError($err_capabilities) + ->setCaption('Specify capabilities in JSON.')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Allocate Resource')); + + $panel->appendChild($form); + + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + ), + array( + 'title' => 'Allocate Resource', + )); + + } + +} diff --git a/src/applications/drydock/controller/resourceallocate/__init__.php b/src/applications/drydock/controller/resourceallocate/__init__.php new file mode 100644 index 0000000000..a6ac464a84 --- /dev/null +++ b/src/applications/drydock/controller/resourceallocate/__init__.php @@ -0,0 +1,25 @@ +getRequest(); + $user = $request->getUser(); + + $nav = $this->buildSideNav('resourcelist'); + + $pager = new AphrontPagerView(); + $pager->setURI(new PhutilURI('/drydock/resource/'), 'page'); + + $data = id(new DrydockResource())->loadAllWhere( + '1 = 1 ORDER BY id DESC LIMIT %d, %d', + $pager->getOffset(), + $pager->getPageSize() + 1); + $data = $pager->sliceResults($data); + + $phids = mpull($data, 'getOwnerPHID'); + $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); + + $rows = array(); + foreach ($data as $resource) { + $rows[] = array( + $resource->getID(), + ($resource->getOwnerPHID() + ? $handles[$resource->getOwnerPHID()]->renderLink() + : null), + phutil_escape_html($resource->getType()), + DrydockResourceStatus::getNameForStatus($resource->getStatus()), + phutil_escape_html(nonempty($resource->getName(), 'Unnamed')), + phabricator_datetime($resource->getDateCreated(), $user), + ); + } + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + 'ID', + 'Owner', + 'Type', + 'Status', + 'Resource', + 'Created', + )); + $table->setColumnClasses( + array( + '', + '', + '', + '', + 'pri wide', + 'right', + )); + + $panel = new AphrontPanelView(); + $panel->setHeader('Drydock Resources'); + + $panel->addButton( + phutil_render_tag( + 'a', + array( + 'href' => '/drydock/resource/allocate/', + 'class' => 'green button', + ), + 'Allocate Resource')); + + $panel->appendChild($table); + $panel->appendChild($pager); + + $nav->appendChild($panel); + + return $this->buildStandardPageResponse( + $nav, + array( + 'title' => 'Resources', + )); + + } + +} diff --git a/src/applications/drydock/controller/resourcelist/__init__.php b/src/applications/drydock/controller/resourcelist/__init__.php new file mode 100644 index 0000000000..664e95c888 --- /dev/null +++ b/src/applications/drydock/controller/resourcelist/__init__.php @@ -0,0 +1,23 @@ +config = $config; + return $this; + } + + final protected function getConfig($key, $default = null) { + return idx($this->config, $key, $default); + } + +} diff --git a/src/applications/drydock/interface/base/__init__.php b/src/applications/drydock/interface/base/__init__.php new file mode 100644 index 0000000000..9f14fe82b5 --- /dev/null +++ b/src/applications/drydock/interface/base/__init__.php @@ -0,0 +1,12 @@ +resolve(); + } + + final public function execx($command) { + $argv = func_get_args(); + $exec = call_user_func_array( + array($this, 'getExecFuture'), + $argv); + return $exec->resolvex(); + } + + abstract public function getExecFuture($command); + +} diff --git a/src/applications/drydock/interface/command/base/__init__.php b/src/applications/drydock/interface/command/base/__init__.php new file mode 100644 index 0000000000..31fad4f8f1 --- /dev/null +++ b/src/applications/drydock/interface/command/base/__init__.php @@ -0,0 +1,12 @@ +getConfig('ssh-keyfile'), + $this->getConfig('user'), + $this->getConfig('host'), + $full_command); + } + +} diff --git a/src/applications/drydock/interface/command/ssh/__init__.php b/src/applications/drydock/interface/command/ssh/__init__.php new file mode 100644 index 0000000000..1e9c4983eb --- /dev/null +++ b/src/applications/drydock/interface/command/ssh/__init__.php @@ -0,0 +1,15 @@ + true, + self::CONFIG_SERIALIZATION => array( + 'attributes' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorPHIDConstants::PHID_TYPE_DRYL); + } + + public function getInterface($type) { + return $this->getResource()->getInterface($this, $type); + } + + public function getResource() { + $this->assertActive(); + if ($this->resource === null) { + throw new Exception("Resource is not yet loaded."); + } + return $this->resource; + } + + public function attachResource(DrydockResource $resource) { + $this->assertActive(); + $this->resource = $resource; + return $this; + } + + public function loadResource() { + $this->assertActive(); + return id(new DrydockResource())->loadOneWhere( + 'id = %d', + $this->getResourceID()); + } + + public function release() { + + // TODO: Insert a cleanup task into the taskmaster queue. + + $this->setStatus(DrydockLeaseStatus::STATUS_RELEASED); + $this->save(); + + $this->resource = null; + + return $this; + } + + private function assertActive() { + if ($this->status != DrydockLeaseStatus::STATUS_ACTIVE) { + throw new Exception( + "Lease is not active! You can not interact with resources through ". + "an inactive lease."); + } + } + +} diff --git a/src/applications/drydock/storage/lease/__init__.php b/src/applications/drydock/storage/lease/__init__.php new file mode 100644 index 0000000000..c515524f64 --- /dev/null +++ b/src/applications/drydock/storage/lease/__init__.php @@ -0,0 +1,18 @@ + true, + self::CONFIG_SERIALIZATION => array( + 'attributes' => self::SERIALIZATION_JSON, + 'capabilities' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorPHIDConstants::PHID_TYPE_DRYR); + } + + public function getAttribute($key, $default = null) { + return idx($this->attributes, $key, $default); + } + + 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() { + if (empty($this->blueprint)) { + $this->blueprint = newv($this->blueprintClass, array()); + } + return $this->blueprint; + } + +} diff --git a/src/applications/drydock/storage/resource/__init__.php b/src/applications/drydock/storage/resource/__init__.php new file mode 100644 index 0000000000..9a3ec16bf9 --- /dev/null +++ b/src/applications/drydock/storage/resource/__init__.php @@ -0,0 +1,16 @@ +