.+\.(?:css|js))$'
=> 'CelerityResourceController',
),
);
}
public function buildRequest() {
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($_GET + $_POST);
$request->setApplicationConfiguration($this);
return $request;
}
public function handleException(Exception $ex) {
// Always log the unhandled exception.
phlog($ex);
$class = phutil_escape_html(get_class($ex));
$message = phutil_escape_html($ex->getMessage());
if (PhabricatorEnv::getEnvConfig('phabricator.show-stack-traces')) {
$trace = $this->renderStackTrace($ex->getTrace());
} else {
$trace = null;
}
$content =
''.
'
'.$message.'
'.
$trace.
'
';
$user = $this->getRequest()->getUser();
if (!$user) {
// If we hit an exception very early, we won't have a user.
$user = new PhabricatorUser();
}
$dialog = new AphrontDialogView();
$dialog
->setTitle('Unhandled Exception ("'.$class.'")')
->setClass('aphront-exception-dialog')
->setUser($user)
->appendChild($content);
if ($this->getRequest()->isAjax()) {
$dialog->addCancelButton('/', 'Close');
}
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
return $response;
}
public function willSendResponse(AphrontResponse $response) {
$request = $this->getRequest();
$response->setRequest($request);
if ($response instanceof AphrontDialogResponse) {
if (!$request->isAjax()) {
$view = new PhabricatorStandardPageView();
$view->setRequest($request);
$view->appendChild(
''.
$response->buildResponseString().
'
');
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
} else {
return id(new AphrontAjaxResponse())
->setContent(array(
'dialog' => $response->buildResponseString(),
));
}
} else if ($response instanceof AphrontRedirectResponse) {
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'redirect' => $response->getURI(),
));
}
}
return $response;
}
public function build404Controller() {
return array(new Phabricator404Controller($this->getRequest()), array());
}
public function buildRedirectController($uri) {
return array(
new PhabricatorRedirectController($this->getRequest()),
array(
'uri' => $uri,
));
}
private function renderStackTrace($trace) {
$libraries = PhutilBootloader::getInstance()->getAllLibraries();
// TODO: Make this configurable?
$host = 'https://secure.phabricator.com';
$browse = array(
'arcanist' =>
$host.'/diffusion/ARC/browse/origin:master/src/',
'phutil' =>
$host.'/diffusion/PHU/browse/origin:master/src/',
'phabricator' =>
$host.'/diffusion/P/browse/origin:master/src/',
);
$rows = array();
$depth = count($trace);
foreach ($trace as $part) {
$lib = null;
$file = idx($part, 'file');
$relative = $file;
foreach ($libraries as $library) {
$root = phutil_get_library_root($library);
if (Filesystem::isDescendant($file, $root)) {
$lib = $library;
$relative = Filesystem::readablePath($file, $root);
break;
}
}
$where = '';
if (isset($part['class'])) {
$where .= $part['class'].'::';
}
if (isset($part['function'])) {
$where .= $part['function'].'()';
}
if ($file) {
if (isset($browse[$lib])) {
$file_name = phutil_render_tag(
'a',
array(
'href' => $browse[$lib].$relative.'$'.$part['line'],
'title' => $file,
'target' => '_blank',
),
phutil_escape_html($relative));
} else {
$file_name = phutil_render_tag(
'span',
array(
'title' => $file,
),
phutil_escape_html($relative));
}
$file_name = $file_name.' : '.(int)$part['line'];
} else {
$file_name = '(Internal)';
}
$rows[] = array(
$depth--,
phutil_escape_html($lib),
$file_name,
phutil_escape_html($where),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Depth',
'Library',
'File',
'Where',
));
$table->setColumnClasses(
array(
'n',
'',
'',
'wide',
));
return
''.
''.
$table->render().
'
';
}
}
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 @@
+