Skip to content

Instantly share code, notes, and snippets.

@ossobuffo
Last active August 29, 2015 14:06
Show Gist options
  • Save ossobuffo/9f18d534dcf4444a6a5f to your computer and use it in GitHub Desktop.
Save ossobuffo/9f18d534dcf4444a6a5f to your computer and use it in GitHub Desktop.
Workaround for Pantheon site-aliases timeouts
<?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