Last active
August 29, 2015 14:06
-
-
Save ossobuffo/9f18d534dcf4444a6a5f to your computer and use it in GitHub Desktop.
Workaround for Pantheon site-aliases timeouts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* Solves the timeout problem when the user is a "Team" member of too many | |
* Pantheon sites. In that case, when running | |
* drush pantheon-aliases | |
* you may encounter a timeout on the Terminus server, and your aliases file | |
* will not be updated. | |
* | |
* In this workaround, we leverage Terminus's API to fetch this info in smaller | |
* chunks. A side benefit is that we can now get aliases for all sites in our | |
* org, regardless of whether we are a Team member. | |
* | |
* This script presumes that you place the following in composer.json: | |
* { | |
* "require": { | |
* "guzzlehttp/guzzle": "~4.0" | |
* } | |
* } | |
* and then run composer install in that directory. | |
* | |
* This script will fail if you are not an org admin. | |
* | |
* Note that Pantheon currently places a limit on how many requests you | |
* can make within a certain time interval. At present, you can make up | |
* to 200 requests every 5 minutes. If you exceed this, you will be locked | |
* out and will have to wait 5 minutes, then re-authenticate. For this | |
* reason, we keep a count of how many requests are made, and when we get | |
* close to 200 we sleep for 5 minutes. | |
*/ | |
use GuzzleHttp\Client; | |
use GuzzleHttp\Cookie\CookieJar; | |
use GuzzleHttp\Cookie\SetCookie; | |
use GuzzleHttp\Exception\ClientException; | |
/** | |
* Helper function to create standardized Terminus URLs. | |
* | |
* @param string $realm | |
* @param string $uuid | |
* @param string $path | |
* @return string | |
*/ | |
function create_terminus_url($realm, $uuid, $path) { | |
return "https://terminus.getpantheon.com/terminus.php?$realm=$uuid&path=" . urlencode($path); | |
} | |
/** | |
* Makes sure we don't exceed our quota of 200 requests per 5 minutes. | |
*/ | |
function check_request_count() { | |
static $request_count = 0; | |
$request_count++; | |
if ($request_count > 190) { | |
echo "\n[Letting Terminus catch its breath...]\n"; | |
sleep(300); | |
$request_count = 0; | |
} | |
} | |
/** | |
* Logs user into terminus and stores the login cookie. | |
* | |
* @param GuzzleHttp\Cookie\CookieJar $jar | |
* @return string The UUID of the logged-in user. | |
*/ | |
function terminus_login(CookieJar &$jar) { | |
global $client; | |
global $email; | |
$cache_file = getenv('HOME') . '/.drush/cache/pantheon/terminus-current-session.cache'; | |
$email = NULL; | |
$uuid = NULL; | |
if (file_exists($cache_file)) { | |
$cache = @json_decode(file_get_contents($cache_file), TRUE); | |
if (is_array($cache) && array_key_exists('data', $cache)) { | |
if (array_key_exists('email', $cache['data'])) { | |
$email = $cache['data']['email']; | |
} | |
if (array_key_exists('user_uuid', $cache['data'])) { | |
$uuid = $cache['data']['user_uuid']; | |
} | |
if (array_key_exists('session_expire_time', $cache['data'])) { | |
if ($cache['data']['session_expire_time'] > time()) { | |
if (array_key_exists('session', $cache['data'])) { | |
list($key, $value) = explode('=', $cache['data']['session'], 2); | |
$cookie = new SetCookie(); | |
$cookie->setDomain('.terminus.getpantheon.com'); | |
$cookie->setName($key); | |
$cookie->setValue($value); | |
$cookie->setExpires($cache['data']['session_expire_time']); | |
$cookie->setSecure(TRUE); | |
$cookie->setHttpOnly(TRUE); | |
$jar->setCookie($cookie); | |
return $uuid; | |
} | |
} | |
} | |
} | |
} | |
$cache = [ | |
'cid' => 'terminus-current-session', | |
'data' => array( | |
'user_uuid' => $uuid, | |
'email' => $email, | |
'session_expire_time' => NULL, | |
'session' => NULL | |
) | |
]; | |
if (!isset($email)) { | |
$email = readline('Pantheon email: '); | |
} | |
$pass = readpass('Password: '); | |
// First, get form_build_id for Drupal login form. | |
// This involves ugly screen scraping. If you go to the page in question in a | |
// browser, the form's elements are set to visibility:hidden. | |
check_request_count(); | |
$request = $client->get('https://terminus.getpantheon.com/login', ['cookies' => $jar]); | |
$html = $request->getBody(); | |
$DOM = new DOMDocument; | |
@$DOM->loadHTML($html); | |
$login_form = $DOM->getElementById('atlas-login-form'); | |
foreach ($login_form->getElementsByTagName('input') as $input) { | |
if ($input->getAttribute('name') == 'form_build_id') { | |
$form_build_id = $input->getAttribute('value'); | |
break; | |
} | |
} | |
// POST to the login page using the form_build_id we scraped. | |
$login_data = array( | |
'email' => $email, | |
'password' => $pass, | |
'form_build_id' => $form_build_id, | |
'form_id' => 'atlas_login_form', | |
'op' => 'Login', | |
); | |
check_request_count(); | |
$request = $client->post('https://terminus.getpantheon.com/login', ['body' => $login_data, 'cookies' => $jar, 'allow_redirects' => false]); | |
$parts = explode('/', $request->getHeader('location')); | |
$uuid = end($parts); | |
$cache['data']['user_uid'] = $uuid; | |
foreach ($jar->toArray() as $cookie) { | |
if (substr($cookie['Name'], 0, 5) == 'SSESS') { | |
$cache['data']['session'] = $cookie['Name'] . '=' . $cookie['Value']; | |
$cache['data']['session_expire_time'] = $cookie['Expires']; | |
break; | |
} | |
} | |
file_put_contents($cache_file, json_encode($cache)); | |
return $uuid; | |
} | |
/** | |
* Reads a password from stdin without echoing it to the screen. | |
* | |
* @param string $prompt | |
* @return string | |
*/ | |
function readpass($prompt) { | |
echo $prompt; | |
$old_style = shell_exec('stty -g'); | |
shell_exec('stty -echo'); | |
$password = rtrim(fgets(STDIN), "\n"); | |
shell_exec("stty $old_style"); | |
echo "\n"; | |
return $password; | |
} | |
$HOME = getenv('HOME'); | |
define('ALIAS_FILE', "$HOME/.drush/pantheon.aliases.drushrc.php"); | |
$deleted_users = [ | |
// 'username' => 'uuid' | |
]; | |
// Autoload GuzzleHttp and its dependencies | |
require_once 'vendor/autoload.php'; | |
$client = new Client(['base_url' => 'https://terminus.getpantheon.com']); | |
$jar = new CookieJar(); | |
$user_uuid = terminus_login($jar); | |
// Get organizations of which you are a member | |
check_request_count(); | |
$request = $client->get(create_terminus_url('user', $user_uuid, 'organizations'), ['cookies' => $jar]); | |
$json = (string)$request->getBody(); | |
$orgs = json_decode($json, TRUE); | |
$org_uuids = array_keys($orgs); | |
// Initialize output file. We will append to this. | |
file_put_contents(ALIAS_FILE, "<?php\n"); | |
// Cycle through organizations | |
foreach ($org_uuids as $org_uuid) { | |
check_request_count(); | |
$request = $client->get(create_terminus_url('user', $user_uuid, 'organizations/'. $org_uuid .'/sites'), ['cookies' => $jar]); | |
$json = (string)$request->getBody(); | |
$sites = json_decode($json, TRUE); | |
$site_count = count($sites); | |
$i = 0; | |
// Cycle through sites in that organization | |
foreach ($sites as $site_uuid => $site_details) { | |
$i++; | |
$site_name = $site_details['name']; | |
$service_level = $site_details['service-level']; | |
echo "Processing [$i/$site_count] $site_name "; | |
check_request_count(); | |
$request = $client->get(create_terminus_url('site', $site_uuid, 'owner'), ['cookies' => $jar]); | |
$owner_uuid = json_decode($request->getBody(), TRUE); | |
check_request_count(); | |
$request = $client->get(create_terminus_url('site', $site_uuid, 'team'), ['cookies' => $jar]); | |
$info = json_decode($request->getBody(), TRUE); | |
$team_members = array_keys($info); | |
if ($owner_uuid != $user_uuid && !in_array($user_uuid, $team_members)) { | |
// Add self to team | |
$data = "{\"data\":{\"invited_by\":\"$user_uuid\"}}"; | |
$headers = array('Content-Type' => 'application/json', 'Content-Length' => strlen($data)); | |
$url = create_terminus_url('site', $site_uuid, 'team/' . rawurlencode($email)); | |
try { | |
check_request_count(); | |
$client->post($url, ['cookies' => $jar, 'headers' => $headers, 'body' => $data]); | |
echo "[added to team] "; | |
} | |
catch (ClientException $e) { | |
$response = $e->getResponse(); | |
echo "\n\n"; | |
echo "UUID: $user_uuid\n"; | |
print_r($info); | |
echo "\n"; | |
echo $e->getCode() . "\n"; | |
echo $response->getBody(); | |
echo "\n"; | |
die(); | |
} | |
$team_members[] = $user_uuid; | |
} | |
if (in_array($owner_uuid, $deleted_users)) { | |
$payload = $user_uuid; | |
$data = json_encode(array('data' => $payload)); | |
$headers = array('Content-Type' => 'application/json', 'Content-Length' => strlen($data)); | |
// Make self owner if current owner is in delete list | |
check_request_count(); | |
$client->put(create_terminus_url('site', $site_uuid, 'owner'), ['cookies' => $jar, 'headers' => $headers, 'body' => $data]); | |
echo "[made owner] "; | |
$owner_uuid = $user_uuid; | |
} | |
// Get environments | |
try { | |
check_request_count(); | |
$request = $client->get(create_terminus_url('site', $site_uuid, 'environments'), ['cookies' => $jar]); | |
} catch (Exception $e) { | |
die("Failed to request environments for site $site_name\n"); | |
} | |
$environments = json_decode((string)$request->getBody(), TRUE); | |
$env_names = array_keys($environments); | |
// Prune out environments that have not been set up yet | |
// (i.e. test or live instances that have never been cloned from dev). | |
check_request_count(); | |
$request = $client->get(create_terminus_url('site', $site_uuid, 'code-tips'), ['cookies' => $jar]); | |
$tips = json_decode((string)$request->getBody(), TRUE); | |
foreach ($env_names as $j => $env) { | |
$tips_env = ($env == 'dev') ? 'master' : $env; | |
if (!array_key_exists($tips_env, $tips)) { | |
unset($env_names[$j]); | |
} | |
} | |
$alias_file = $pdo_info = ''; | |
// Cycle through environments in this site | |
foreach ($env_names as $env_name) { | |
echo "$env_name "; | |
try { | |
check_request_count(); | |
$request = $client->get(create_terminus_url('site', $site_uuid, 'environments/' . $env_name . '/bindings?type=dbserver'), ['cookies' => $jar]); | |
} catch (Exception $e) { | |
die("\nFailed to fetch dbserver bindings for $site_name.$env_name\n"); | |
} | |
$db_details = json_decode((string)$request->getBody(), TRUE); | |
$db_details = reset($db_details); | |
// If an environment has never been configured, skip it. | |
// Missing port data seems to be the key indicator here. | |
if (!is_array($db_details) || !array_key_exists('port', $db_details)) { | |
continue; | |
} | |
$db_port = $db_details['port']; | |
$db_pass = $db_details['password']; | |
$db_user = $db_details['username']; | |
$db_name = $db_details['database']; | |
$alias_file .= "\$aliases['$site_name.$env_name'] = array(\n" | |
. " 'uri' => '$env_name-$site_name.devportal.apigee.com',\n" | |
. " 'db-url' => 'mysql://$db_user:$db_pass@dbserver.$env_name.$site_uuid.drush.in:$db_port/$db_name',\n" | |
. " 'db-allows-remote' => TRUE,\n" | |
. " 'remote-host' => 'appserver.$env_name.$site_uuid.drush.in',\n" | |
. " 'remote-user' => '$env_name.$site_uuid',\n" | |
. " 'ssh-options' => '-p 2222 -o \"AddressFamily inet\"',\n" | |
. " 'path-aliases' => array('%files' => 'code/sites/default/files', '%drush-script' => 'drush'),\n" | |
. " 'service-level => '$service_level',\n" | |
. ");\n"; | |
} | |
file_put_contents(ALIAS_FILE, $alias_file, FILE_APPEND); | |
echo "done.\n"; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment