mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 08:42:41 +01:00
Allow AlmanacHost blueprints to build a meaningful CommandInterface
Summary: Ref T9253. Provide a meaningful command interface for Almanac hosts. Test Plan: Configued and leased a real host (`sbuild001.phacility.net`) and ran a command on it. ``` $ ./bin/drydock command --lease 90 -- ls / bin boot core dev etc home initrd.img lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var vmlinuz ``` Reviewers: chad, hach-que Reviewed By: chad, hach-que Maniphest Tasks: T9253 Differential Revision: https://secure.phabricator.com/D14126
This commit is contained in:
parent
9a270efe8a
commit
6a0eb9d84b
10 changed files with 169 additions and 113 deletions
|
@ -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',
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -3,59 +3,21 @@
|
|||
/**
|
||||
* @task lease Lease Acquisition
|
||||
* @task resource Resource Allocation
|
||||
* @task interface Resource Interfaces
|
||||
* @task log Logging
|
||||
*/
|
||||
abstract class DrydockBlueprintImplementation extends Phobject {
|
||||
|
||||
private $activeResource;
|
||||
private $activeLease;
|
||||
private $instance;
|
||||
|
||||
abstract public function getType();
|
||||
abstract public function getInterface(
|
||||
DrydockResource $resource,
|
||||
DrydockLease $lease,
|
||||
$type);
|
||||
|
||||
abstract public function isEnabled();
|
||||
|
||||
abstract public function getBlueprintName();
|
||||
abstract public function getDescription();
|
||||
|
||||
public function getBlueprintClass() {
|
||||
return get_class($this);
|
||||
}
|
||||
|
||||
protected function loadLease($lease_id) {
|
||||
// TODO: Get rid of this?
|
||||
$query = id(new DrydockLeaseQuery())
|
||||
->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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
final class DrydockLocalCommandInterface extends DrydockCommandInterface {
|
||||
|
||||
public function getExecFuture($command) {
|
||||
$argv = func_get_args();
|
||||
$argv = $this->applyWorkingDirectoryToArgv($argv);
|
||||
|
||||
return newv('ExecFuture', $argv);
|
||||
}
|
||||
|
||||
}
|
|
@ -2,67 +2,57 @@
|
|||
|
||||
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');
|
||||
|
||||
$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(),
|
||||
$this->credential = PassphraseSSHKey::loadFromPHID(
|
||||
$credential_phid,
|
||||
PhabricatorUser::getOmnipotentUser());
|
||||
}
|
||||
|
||||
return $this->credential;
|
||||
}
|
||||
|
||||
public function setConnectTimeout($timeout) {
|
||||
$this->connectTimeout = $timeout;
|
||||
return $this;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
final class DrydockManagementCommandWorkflow
|
||||
extends DrydockManagementWorkflow {
|
||||
|
||||
protected function didConstruct() {
|
||||
$this
|
||||
->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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 )------------------------- */
|
||||
|
||||
|
|
Loading…
Reference in a new issue