diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3a2a76ba6a..a9e43d215e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -838,7 +838,6 @@ phutil_register_library_map(array( 'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php', 'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php', 'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php', - 'DrydockLocalCommandInterface' => 'applications/drydock/interface/command/DrydockLocalCommandInterface.php', 'DrydockLog' => 'applications/drydock/storage/DrydockLog.php', 'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php', 'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php', @@ -846,6 +845,7 @@ phutil_register_library_map(array( 'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php', 'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php', 'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php', + 'DrydockManagementCommandWorkflow' => 'applications/drydock/management/DrydockManagementCommandWorkflow.php', 'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php', 'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php', 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', @@ -4555,7 +4555,6 @@ phutil_register_library_map(array( 'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockLeaseStatus' => 'DrydockConstants', 'DrydockLeaseViewController' => 'DrydockLeaseController', - 'DrydockLocalCommandInterface' => 'DrydockCommandInterface', 'DrydockLog' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', @@ -4566,6 +4565,7 @@ phutil_register_library_map(array( 'DrydockLogQuery' => 'DrydockQuery', 'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementCommandWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', diff --git a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php index aea46edd71..37fe18efab 100644 --- a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php @@ -119,11 +119,37 @@ final class DrydockAlmanacServiceHostBlueprintImplementation } public function getInterface( + DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease, $type) { - // TODO: Actually do stuff here, this needs work and currently makes this - // entire exercise pointless. + + $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) { + // TODO: This is probably a permanent failure, destroy this resource? + 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() { diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php index 3a51f65ad1..b3983d250d 100644 --- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php @@ -1,61 +1,23 @@ setViewer(PhabricatorUser::getOmnipotentUser()) - ->withIDs(array($lease_id)) - ->execute(); - - $lease = idx($query, $lease_id); - - if (!$lease) { - throw new Exception(pht("No such lease '%d'!", $lease_id)); - } - - return $lease; - } - - protected function getInstance() { - if (!$this->instance) { - throw new Exception( - pht('Attach the blueprint instance to the implementation.')); - } - - return $this->instance; - } - - public function attachInstance(DrydockBlueprint $instance) { - $this->instance = $instance; - return $this; - } - public function getFieldSpecifications() { return array(); } @@ -105,6 +67,7 @@ abstract class DrydockBlueprintImplementation extends Phobject { DrydockResource $resource, DrydockLease $lease); + final public function releaseLease( DrydockBlueprint $blueprint, DrydockResource $resource, @@ -236,6 +199,16 @@ abstract class DrydockBlueprintImplementation extends Phobject { DrydockLease $lease); +/* -( Resource Interfaces )------------------------------------------------ */ + + + abstract public function getInterface( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease, + $type); + + /* -( Logging )------------------------------------------------------------ */ @@ -308,7 +281,7 @@ abstract class DrydockBlueprintImplementation extends Phobject { $this->log( pht( "Blueprint '%s': Created New Template", - $this->getBlueprintClass())); + get_class($this))); return $resource; } diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php index 86207bb73f..34593bba3a 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -122,18 +122,11 @@ final class DrydockWorkingCopyBlueprintImplementation } public function getInterface( + DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease, $type) { - - switch ($type) { - case 'command': - return $this - ->loadLease($resource->getAttribute('lease.host')) - ->getInterface($type); - } - - throw new Exception(pht("No interface of type '%s'.", $type)); + // TODO: This blueprint doesn't work at all. } } diff --git a/src/applications/drydock/interface/DrydockInterface.php b/src/applications/drydock/interface/DrydockInterface.php index 631481f7eb..3b4863e1f4 100644 --- a/src/applications/drydock/interface/DrydockInterface.php +++ b/src/applications/drydock/interface/DrydockInterface.php @@ -2,12 +2,12 @@ abstract class DrydockInterface extends Phobject { - private $config; + private $config = array(); abstract public function getInterfaceType(); - final public function setConfiguration(array $config) { - $this->config = $config; + final public function setConfig($key, $value) { + $this->config[$key] = $value; return $this; } diff --git a/src/applications/drydock/interface/command/DrydockCommandInterface.php b/src/applications/drydock/interface/command/DrydockCommandInterface.php index e2239c1045..8034e0f732 100644 --- a/src/applications/drydock/interface/command/DrydockCommandInterface.php +++ b/src/applications/drydock/interface/command/DrydockCommandInterface.php @@ -2,6 +2,8 @@ abstract class DrydockCommandInterface extends DrydockInterface { + const INTERFACE_TYPE = 'command'; + private $workingDirectory; public function setWorkingDirectory($working_directory) { @@ -14,7 +16,7 @@ abstract class DrydockCommandInterface extends DrydockInterface { } final public function getInterfaceType() { - return 'command'; + return self::INTERFACE_TYPE; } final public function exec($command) { @@ -38,7 +40,7 @@ abstract class DrydockCommandInterface extends DrydockInterface { protected function applyWorkingDirectoryToArgv(array $argv) { if ($this->getWorkingDirectory() !== null) { $cmd = $argv[0]; - $cmd = "(cd %s; {$cmd})"; + $cmd = "(cd %s && {$cmd})"; $argv = array_merge( array($cmd), array($this->getWorkingDirectory()), diff --git a/src/applications/drydock/interface/command/DrydockLocalCommandInterface.php b/src/applications/drydock/interface/command/DrydockLocalCommandInterface.php deleted file mode 100644 index e791214733..0000000000 --- a/src/applications/drydock/interface/command/DrydockLocalCommandInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -applyWorkingDirectoryToArgv($argv); - - return newv('ExecFuture', $argv); - } - -} diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php index d75dcfb780..1aab14b57b 100644 --- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php +++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php @@ -2,35 +2,19 @@ final class DrydockSSHCommandInterface extends DrydockCommandInterface { - private $passphraseSSHKey; + private $credential; private $connectTimeout; - private function openCredentialsIfNotOpen() { - if ($this->passphraseSSHKey !== null) { - return; + private function loadCredential() { + if ($this->credential === null) { + $credential_phid = $this->getConfig('credentialPHID'); + + $this->credential = PassphraseSSHKey::loadFromPHID( + $credential_phid, + PhabricatorUser::getOmnipotentUser()); } - $credential = id(new PassphraseCredentialQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withIDs(array($this->getConfig('credential'))) - ->needSecrets(true) - ->executeOne(); - - if ($credential === null) { - throw new Exception( - pht( - 'There is no credential with ID %d.', - $this->getConfig('credential'))); - } - - if ($credential->getProvidesType() !== - PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE) { - throw new Exception(pht('Only private key credentials are supported.')); - } - - $this->passphraseSSHKey = PassphraseSSHKey::loadFromPHID( - $credential->getPHID(), - PhabricatorUser::getOmnipotentUser()); + return $this->credential; } public function setConnectTimeout($timeout) { @@ -39,30 +23,36 @@ final class DrydockSSHCommandInterface extends DrydockCommandInterface { } public function getExecFuture($command) { - $this->openCredentialsIfNotOpen(); + $credential = $this->loadCredential(); $argv = func_get_args(); $argv = $this->applyWorkingDirectoryToArgv($argv); $full_command = call_user_func_array('csprintf', $argv); - $command_timeout = ''; - if ($this->connectTimeout !== null) { - $command_timeout = csprintf( - '-o %s', - 'ConnectTimeout='.$this->connectTimeout); + $flags = array(); + $flags[] = '-o'; + $flags[] = 'LogLevel=quiet'; + + $flags[] = '-o'; + $flags[] = 'StrictHostKeyChecking=no'; + + $flags[] = '-o'; + $flags[] = 'UserKnownHostsFile=/dev/null'; + + $flags[] = '-o'; + $flags[] = 'BatchMode=yes'; + + if ($this->connectTimeout) { + $flags[] = '-o'; + $flags[] = 'ConnectTimeout='.$this->connectTimeout; } return new ExecFuture( - 'ssh '. - '-o LogLevel=quiet '. - '-o StrictHostKeyChecking=no '. - '-o UserKnownHostsFile=/dev/null '. - '-o BatchMode=yes '. - '%C -p %s -i %P %P@%s -- %s', - $command_timeout, + 'ssh %Ls -l %P -p %s -i %P %s -- %s', + $flags, + $credential->getUsernameEnvelope(), $this->getConfig('port'), - $this->passphraseSSHKey->getKeyfileEnvelope(), - $this->passphraseSSHKey->getUsernameEnvelope(), + $credential->getKeyfileEnvelope(), $this->getConfig('host'), $full_command); } diff --git a/src/applications/drydock/management/DrydockManagementCommandWorkflow.php b/src/applications/drydock/management/DrydockManagementCommandWorkflow.php new file mode 100644 index 0000000000..bc66966f8c --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementCommandWorkflow.php @@ -0,0 +1,66 @@ +setName('command') + ->setSynopsis(pht('Run a command on a leased resource.')) + ->setArguments( + array( + array( + 'name' => 'lease', + 'param' => 'id', + 'help' => pht('Lease ID.'), + ), + array( + 'name' => 'argv', + 'wildcard' => true, + 'help' => pht('Command to execute.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $lease_id = $args->getArg('lease'); + if (!$lease_id) { + throw new PhutilArgumentUsageException( + pht( + 'Use %s to specify a lease.', + '--lease')); + } + + $argv = $args->getArg('argv'); + if (!$argv) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a command to run.')); + } + + $lease = id(new DrydockLeaseQuery()) + ->setViewer($this->getViewer()) + ->withIDs(array($lease_id)) + ->executeOne(); + if (!$lease) { + throw new Exception( + pht( + 'Unable to load lease with ID "%s"!', + $lease_id)); + } + + // TODO: Check lease state, etc. + + $interface = $lease->getInterface(DrydockCommandInterface::INTERFACE_TYPE); + + list($stdout, $stderr) = call_user_func_array( + array($interface, 'execx'), + array('%Ls', $argv)); + + fprintf(STDOUT, $stdout); + fprintf(STDERR, $stderr); + + return 0; + } + +} diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php index 21a3933ccc..9e09ee744c 100644 --- a/src/applications/drydock/storage/DrydockBlueprint.php +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -173,6 +173,24 @@ final class DrydockBlueprint extends DrydockDAO 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 )------------------------- */