Page MenuHomePhabricator

D10547.id25334.diff
No OneTemporary

D10547.id25334.diff

diff --git a/resources/windows/zeroconf.ps1 b/resources/windows/sshzeroconf.ps1
rename from resources/windows/zeroconf.ps1
rename to resources/windows/sshzeroconf.ps1
diff --git a/resources/windows/winrmzeroconf.ps1 b/resources/windows/winrmzeroconf.ps1
new file mode 100644
--- /dev/null
+++ b/resources/windows/winrmzeroconf.ps1
@@ -0,0 +1,32 @@
+# Allow running scripts on this system
+Set-ExecutionPolicy -Force Bypass
+
+# Enable plain HTTP WinRM remoting
+Set-WSManInstance WinRM/Config/Service/Auth -ValueSet @{Basic = $true}
+Set-WSManInstance WinRM/Config/Service -ValueSet @{AllowUnencrypted = $true}
+Set-WSManInstance WinRM/Config/Client -ValueSet @{TrustedHosts="*"}
+
+# Prevent Powershell from using stupid defaults.
+Set-Item -ErrorAction SilentlyContinue `
+ WSMAN:localhost\Shell\MaxMemoryPerShellMB 100000000
+Set-Item -ErrorAction SilentlyContinue `
+ WSMAN:localhost\Shell\MaxShellsPerUser 10000
+
+Push-Location WSMAN:localhost\Plugin\
+foreach ($dir in Get-ChildItem) {
+ $name = $dir.Name
+ try {
+ Set-Item -ErrorAction SilentlyContinue `
+ -Path "$name\Quotas\MaxConcurrentCommandsPerShell" 100000000
+ } catch [System.InvalidOperationException] {
+ }
+ try {
+ Set-Item -ErrorAction SilentlyContinue `
+ -Path "$name\Quotas\MaxMemoryPerShellMB" 10000
+ } catch [System.InvalidOperationException] {
+ }
+}
+Pop-Location
+
+# Reload remoting configuration.
+Restart-Service winrm
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -646,7 +646,9 @@
'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php',
'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php',
'DrydockSchemaSpec' => 'applications/drydock/storage/DrydockSchemaSpec.php',
+ 'DrydockSetupCheckWinRM' => 'applications/drydock/check/DrydockSetupCheckWinRM.php',
'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php',
+ 'DrydockWinRMCommandInterface' => 'applications/drydock/interface/command/DrydockWinRMCommandInterface.php',
'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php',
'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php',
'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php',
@@ -3493,7 +3495,9 @@
'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface',
'DrydockSSHCommandInterface' => 'DrydockCommandInterface',
'DrydockSchemaSpec' => 'PhabricatorConfigSchemaSpec',
+ 'DrydockSetupCheckWinRM' => 'PhabricatorSetupCheck',
'DrydockWebrootInterface' => 'DrydockInterface',
+ 'DrydockWinRMCommandInterface' => 'DrydockCommandInterface',
'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation',
'FeedConduitAPIMethod' => 'ConduitAPIMethod',
'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod',
diff --git a/src/applications/drydock/blueprint/DrydockAmazonEC2HostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAmazonEC2HostBlueprintImplementation.php
--- a/src/applications/drydock/blueprint/DrydockAmazonEC2HostBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockAmazonEC2HostBlueprintImplementation.php
@@ -95,10 +95,33 @@
->withPHIDs(array($this->getDetail('keypair')))
->executeOne();
+ if ($credential === null) {
+ throw new Exception('Specified credential does not exist!');
+ }
+
$this->log(pht(
'Using credential %d to allocate.',
$credential->getID()));
+ $winrm_auth_id = null;
+ if ($this->getDetail('protocol') === 'winrm') {
+ $winrm_auth = id(new PassphraseCredentialQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withPHIDs(array($this->getDetail('winrm-auth')))
+ ->executeOne();
+
+ if ($winrm_auth === null) {
+ throw new Exception(
+ 'Specified credential for WinRM auth does not exist!');
+ }
+
+ $winrm_auth_id = $winrm_auth->getID();
+
+ $this->log(pht(
+ 'Using credential %d to authenticate over WinRM.',
+ $winrm_auth->getID()));
+ }
+
try {
$existing_keys = $this->getAWSEC2Future()
->setRawAWSQuery(
@@ -151,11 +174,11 @@
$i++;
}
- if (!$this->getDetail('skip-ssh-setup-windows')) {
+ if (!$this->getDetail('skip-setup-windows')) {
if ($this->getDetail('platform') === 'windows') {
$this->log(pht('Enabled SSH automatic configuration for Windows.'));
$settings['UserData'] = id(new WindowsZeroConf())
- ->getEncodedUserData($credential);
+ ->getEncodedUserData($credential, $this->getDetail('protocol'));
}
}
@@ -428,8 +451,10 @@
->setAttributes(array(
'instance-id' => $instance_id,
'platform' => $this->getDetail('platform'),
+ 'protocol' => $this->getDetail('protocol'),
'path' => $this->getDetail('storage-path'),
'credential' => $credential->getID(),
+ 'winrm-auth' => $winrm_auth_id,
'aws-status' => 'Instance Requested'))
->save();
@@ -638,37 +663,46 @@
// Update address and port attributes.
$resource->setAttribute('host', $address);
- $resource->setAttribute('port', 22);
+ if ($resource->getAttribute('protocol') === 'winrm') {
+ $resource->setAttribute('port', 5985);
+ } else {
+ $resource->setAttribute('port', 22);
+ }
$resource->save();
+ $protocol_name = '';
+ if ($resource->getAttribute('protocol') === 'winrm') {
+ $protocol_name = 'WinRM';
+ } else {
+ $protocol_name = 'SSH';
+ }
+
$this->log(pht(
- 'Waiting for a successful SSH connection'));
-
- // Wait until we get a successful SSH connection.
- $ssh = id(new DrydockSSHCommandInterface())
- ->setConfiguration(array(
- 'host' => $resource->getAttribute('host'),
- 'port' => $resource->getAttribute('port'),
- 'credential' => $resource->getAttribute('credential'),
- 'platform' => $resource->getAttribute('platform')));
- $ssh->setConnectTimeout(60);
+ 'Waiting for a successful %s connection', $protocol_name));
+
+ // Wait until we get a successful connection.
+ $ssh = $this->getInterface($resource, $lease, 'command');
$ssh->setExecTimeout(60);
+ if ($resource->getAttribute('protocol') === 'ssh') {
+ $ssh->setConnectTimeout(60);
+ }
$resource->setAttribute(
'aws-status',
- 'Waiting for successful SSH connection');
+ 'Waiting for successful %s connection', $protocol_name);
$resource->save();
while (true) {
try {
$this->log(pht(
- 'Attempting to connect to \'%s\' via SSH',
- $instance_id));
+ 'Attempting to connect to \'%s\' via %s',
+ $instance_id,
+ $protocol_name));
$ssh_future = $ssh->getExecFuture('echo "test"');
$ssh_future->resolvex();
if ($ssh_future->getWasKilledByTimeout()) {
- throw new Exception('SSH execution timed out.');
+ throw new Exception('%s execution timed out.', $protocol_name);
}
break;
@@ -688,7 +722,8 @@
$instance_state = (string)$instance->instanceState->name;
$this->log(pht(
- 'SSH connection not yet ready; instance is in state \'%s\'',
+ '%s connection not yet ready; instance is in state \'%s\'',
+ $protocol_name,
$instance_state));
if ($instance_state === 'shutting-down' ||
@@ -696,8 +731,9 @@
$this->log(pht(
'Instance has ended up in state \'%s\' while waiting for an '.
- 'SSH connection',
- $instance_state));
+ '%s connection',
+ $instance_state,
+ $protocol_name));
// Deallocate and release the public IP address if we allocated one.
if ($resource->getAttribute('eip-allocated')) {
@@ -950,13 +986,25 @@
switch ($type) {
case 'command':
- return id(new DrydockSSHCommandInterface())
- ->setConfiguration(array(
- 'host' => $resource->getAttribute('host'),
- 'port' => $resource->getAttribute('port'),
- 'credential' => $resource->getAttribute('credential'),
- 'platform' => $resource->getAttribute('platform')))
- ->setWorkingDirectory($lease->getAttribute('path'));
+ if ($resource->getAttribute('protocol') === 'ssh') {
+ return id(new DrydockSSHCommandInterface())
+ ->setConfiguration(array(
+ 'host' => $resource->getAttribute('host'),
+ 'port' => $resource->getAttribute('port'),
+ 'credential' => $resource->getAttribute('credential'),
+ 'platform' => $resource->getAttribute('platform')))
+ ->setWorkingDirectory($lease->getAttribute('path'));
+ } else if ($resource->getAttribute('protocol') === 'winrm') {
+ return id(new DrydockWinRMCommandInterface())
+ ->setConfiguration(array(
+ 'host' => $resource->getAttribute('host'),
+ 'port' => $resource->getAttribute('port'),
+ 'credential' => $resource->getAttribute('winrm-auth'),
+ 'platform' => $resource->getAttribute('platform')))
+ ->setWorkingDirectory($lease->getAttribute('path'));
+ } else {
+ throw new Exception('Unsupported protocol for remoting');
+ }
case 'filesystem':
return id(new DrydockSFTPFilesystemInterface())
->setConfiguration(array(
@@ -1007,6 +1055,22 @@
'required' => true,
'caption' => pht('e.g. %s or %s', 'windows', 'linux')
),
+ 'protocol' => array(
+ 'name' => pht('Protocol'),
+ 'type' => 'select',
+ 'required' => true,
+ 'options' => array(
+ 'ssh' => 'SSH',
+ 'winrm' => 'WinRM (Windows platform only)'),
+ ),
+ 'winrm-auth' => array(
+ 'name' => pht('WinRM Credentials'),
+ 'type' => 'credential',
+ 'credential.provides'
+ => PassphraseCredentialTypePassword::PROVIDES_TYPE,
+ 'caption' => pht(
+ 'This is only required if the protocol is set to WinRM.')
+ ),
'subnet-id' => array(
'name' => pht('VPC Subnet'),
'type' => 'text',
@@ -1049,13 +1113,14 @@
'connect to the machine on it\'s private IP address (because of '.
'firewall rules).'),
),
- 'skip-ssh-setup-windows' => array(
- 'name' => pht('Skip SSH setup on Windows'),
+ 'skip-setup-windows' => array(
+ 'name' => pht('Skip setup on Windows'),
'type' => 'bool',
'caption' => pht(
- 'If SSH is already configured on a Windows AMI, check this option. '.
- 'By default, Phabricator will automatically install and configure '.
- 'SSH on the Windows image.')
+ 'If SSH or WinRM is already configured on a Windows AMI, check '.
+ 'this option. By default, Phabricator will automatically install '.
+ 'and configure SSH or WinRM on the Windows image (depending on the '.
+ 'protocol chosen).')
),
'spot' => array(
'name' => pht('Spot Instances'),
diff --git a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
--- a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
@@ -52,17 +52,34 @@
// we have all the information we need.
PhutilTypeSpec::checkMap(
$resource->getAttributesForTypeSpec(
- array('platform', 'host', 'port', 'credential', 'path')),
+ array('platform', 'protocol', 'host', 'port', 'credential', 'path')),
array(
'platform' => 'string',
+ 'protocol' => 'string',
'host' => 'string',
'port' => 'string', // Value is a string from the command line
'credential' => 'string',
'path' => 'string',
));
$v_platform = $resource->getAttribute('platform');
+ $v_protocol = $resource->getAttribute('protocol');
$v_path = $resource->getAttribute('path');
+ // Verify the provided protocol.
+ if ($v_platform === 'windows') {
+ if ($v_protocol !== 'ssh' && $v_protocol !== 'winrm') {
+ throw new Exception(
+ 'Invalid protocol set for Windows platform; '.
+ 'expected \'ssh\' or \'winrm\'.');
+ }
+ } else {
+ if ($v_protocol !== 'ssh') {
+ throw new Exception(
+ 'Invalid protocol set for UNIX platform; '.
+ 'expected \'ssh\'.');
+ }
+ }
+
// Similar to DrydockLocalHostBlueprint, we create a folder
// on the remote host that the lease can use.
@@ -99,7 +116,12 @@
switch ($type) {
case 'command':
- return id(new DrydockSSHCommandInterface())
+ $interface = new DrydockSSHCommandInterface();
+ if ($resource->getAttribute('protocol') === 'winrm') {
+ $interface = new DrydockWinRMCommandInterface();
+ }
+
+ return $interface
->setConfiguration(array(
'host' => $resource->getAttribute('host'),
'port' => $resource->getAttribute('port'),
diff --git a/src/applications/drydock/blueprint/windows/WindowsZeroConf.php b/src/applications/drydock/blueprint/windows/WindowsZeroConf.php
--- a/src/applications/drydock/blueprint/windows/WindowsZeroConf.php
+++ b/src/applications/drydock/blueprint/windows/WindowsZeroConf.php
@@ -1,8 +1,8 @@
<?php
/**
- * Responsible for configuring and automatically installing SSH on Windows
- * EC2 instances when they start.
+ * Responsible for configuring and automatically installing SSH or WinRM
+ * on Windows EC2 instances when they start.
*/
final class WindowsZeroConf extends Phobject {
@@ -10,14 +10,21 @@
return base64_encode($this->getUserData($credential));
}
- private function getZeroConfScript() {
+ private function getSSHZeroConfScript() {
$file =
dirname(phutil_get_library_root('phabricator')).
- '/resources/windows/zeroconf.ps1';
+ '/resources/windows/sshzeroconf.ps1';
return Filesystem::readFile($file);
}
- private function getUserData(PassphraseCredential $credential) {
+ private function getWinRMZeroConfScript() {
+ $file =
+ dirname(phutil_get_library_root('phabricator')).
+ '/resources/windows/winrmzeroconf.ps1';
+ return Filesystem::readFile($file);
+ }
+
+ private function getUserData(PassphraseCredential $credential, $protocol) {
$type = PassphraseCredentialType::getTypeByConstant(
$credential->getCredentialType());
@@ -25,34 +32,61 @@
throw new Exception(pht('Credential has invalid type "%s"!', $type));
}
- if (!$type->hasPublicKey()) {
- throw new Exception(pht('Credential has no public key!'));
- }
+ if ($protocol === 'ssh') {
+ if (!$type->hasPublicKey()) {
+ throw new Exception(pht('Credential has no public key!'));
+ }
- $username = $credential->getUsername();
- $publickey = $type->getPublicKey(
- PhabricatorUser::getOmnipotentUser(),
- $credential);
- $publickey = trim($publickey);
+ $username = $credential->getUsername();
+ $publickey = $type->getPublicKey(
+ PhabricatorUser::getOmnipotentUser(),
+ $credential);
+ $publickey = trim($publickey);
- $username = str_replace('"', '`"', $username);
- $publickey = str_replace('"', '`"', $publickey);
+ $username = str_replace('"', '`"', $username);
+ $publickey = str_replace('"', '`"', $publickey);
- $start = <<<EOF
+ $start = <<<EOF
<powershell>
\$username = "$username";
\$publickey = "$publickey";
EOF;
- $script = $this->getZeroConfScript();
+ $script = $this->getZeroConfScript('ssh');
+
+ $end = <<<EOF
+
+</powershell>
+EOF;
+
+ return $start.$script.$end;
+ } else if ($protocol === 'winrm') {
+ $username = $credential->getUsername();
+ $password = $credential->getSecret();
+
+ $username = str_replace('"', '`"', $username);
+ $password = str_replace('"', '`"', $password);
+
+ $start = <<<EOF
+<powershell>
+\$username = "$username";
+\$password = "$password";
+
+EOF;
+
+ $script = $this->getZeroConfScript('winrm');
- $end = <<<EOF
+ $end = <<<EOF
</powershell>
EOF;
- return $start.$script.$end;
+ return $start.$script.$end;
+
+ } else {
+ throw new Exception('Unknown protocol for automatic setup');
+ }
}
}
diff --git a/src/applications/drydock/check/DrydockSetupCheckWinRM.php b/src/applications/drydock/check/DrydockSetupCheckWinRM.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/check/DrydockSetupCheckWinRM.php
@@ -0,0 +1,41 @@
+<?php
+
+final class DrydockSetupCheckWinRM extends PhabricatorSetupCheck {
+
+ protected function executeChecks() {
+
+ $drydock_app = 'PhabricatorDrydockApplication';
+ if (!PhabricatorApplication::isClassInstalled($drydock_app)) {
+ return;
+ }
+
+ if (!Filesystem::binaryExists('winrm')) {
+ $preamble = pht(
+ "The 'winrm' binary could not be found. This utility is used to ".
+ "run commands on remote Windows machines when they are leased through ".
+ "Drydock.\n\n".
+ "You will most likely need to download and compile it from ".
+ "%s, using the Go compiler. Once you have, place the binary ".
+ "somewhere in your %s.",
+ phutil_tag(
+ 'a',
+ array('href' => 'https://github.com/masterzen/winrm'),
+ 'https://github.com/masterzen/winrm'),
+ phutil_tag('tt', array(), 'PATH'));
+
+ $message = pht(
+ 'You only need this binary if you are leasing Windows hosts in '.
+ 'Drydock or Harbormaster. If you don\'t need to run commands on '.
+ 'Windows machines, you can safely ignore this message.');
+
+ $this->newIssue('bin.winrm')
+ ->setShortName(pht("'%s' Missing", 'winrm'))
+ ->setName(pht("Missing '%s' Binary", 'winrm'))
+ ->setSummary(
+ pht("The '%s' binary could not be located or executed.", 'winrm'))
+ ->setMessage(pht("%s\n\n%s", $preamble, $message));
+ }
+
+ }
+
+}
diff --git a/src/applications/drydock/interface/command/DrydockWinRMCommandInterface.php b/src/applications/drydock/interface/command/DrydockWinRMCommandInterface.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/interface/command/DrydockWinRMCommandInterface.php
@@ -0,0 +1,76 @@
+<?php
+
+final class DrydockWinRMCommandInterface extends DrydockCommandInterface {
+
+ private $passphraseWinRMPassword;
+ private $execTimeout;
+
+ private function openCredentialsIfNotOpen() {
+ if ($this->passphraseWinRMPassword !== null) {
+ return;
+ }
+
+ $credential = id(new PassphraseCredentialQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withIDs(array($this->getConfig('credential')))
+ ->needSecrets(true)
+ ->executeOne();
+
+ if ($credential->getProvidesType() !==
+ PassphraseCredentialTypePassword::PROVIDES_TYPE) {
+ throw new Exception('Only password credentials are supported.');
+ }
+
+ $this->passphraseWinRMPassword = PassphrasePasswordKey::loadFromPHID(
+ $credential->getPHID(),
+ PhabricatorUser::getOmnipotentUser());
+ }
+
+ public function setExecTimeout($timeout) {
+ $this->execTimeout = $timeout;
+ return $this;
+ }
+
+ public function getExecFuture($command) {
+ $this->openCredentialsIfNotOpen();
+
+ $argv = func_get_args();
+
+ $change_directory = '';
+ if ($this->getWorkingDirectory() !== null) {
+ $change_directory .= 'cd '.$this->getWorkingDirectory().' & ';
+ }
+
+ // Encode the command to run under Powershell.
+ $command = id(new PhutilCommandString($argv))
+ ->setEscapingMode(PhutilCommandString::MODE_POWERSHELL);
+
+ // When Microsoft says "Unicode" they don't mean UTF-8.
+ $command = mb_convert_encoding($command, 'UTF-16LE');
+ $command = base64_encode($command);
+
+ $powershell =
+ 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
+ $powershell .=
+ ' -ExecutionPolicy Bypass'.
+ ' -NonInteractive'.
+ ' -InputFormat Text'.
+ ' -OutputFormat Text'.
+ ' -EncodedCommand '.$command;
+
+ $future = new ExecFuture(
+ 'winrm '.
+ '-hostname=%s '.
+ '-username=%P '.
+ '-password=%P '.
+ '-port=%s '.
+ '%s',
+ $this->getConfig('host'),
+ $this->passphraseWinRMPassword->getUsernameEnvelope(),
+ $this->passphraseWinRMPassword->getPasswordEnvelope(),
+ $this->getConfig('port'),
+ $change_directory.$powershell);
+ $future->setTimeout($this->execTimeout);
+ return $future;
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Thu, Apr 3, 6:32 AM (2 w, 6 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7715744
Default Alt Text
D10547.id25334.diff (20 KB)

Event Timeline