Skip to content

Instantly share code, notes, and snippets.

@J7mbo
Last active December 24, 2015 12:19
Show Gist options
  • Save J7mbo/6796657 to your computer and use it in GitHub Desktop.
Save J7mbo/6796657 to your computer and use it in GitHub Desktop.
An Instance controller for aws. Not hard.
<?php
namespace Classes;
class InstanceController
{
/**
* Ubuntu 10.04 LTS AMI
*
* @link <http://cloud-images.ubuntu.com/locator/ec2/>
*/
//const AMI_ID = 'amiid';
/**
* Lisa Build AMI (uses 10.04 LTS, set up and everything working)
*/
const AMI_ID = 'amiid';
/**
* Instance type
*
* @link <http://aws.amazon.com/ec2/instance-types/>
*/
const AMI_TYPE = 'c1.medium';
/**
* KeyPair to give to newly created Instances
*/
const KEY_NAME = 'aws-control';
/**
* Name of the cert (.pem) file in resources/ to use
*/
const CERT_NAME = 'aws-control-php';
/**
* SecurityGroup to give to newly created Instances
*/
const SECUR_NAME = 'Standard-ssh';
/**
* Username to login to Instances with (defaults usually ubuntu)
*/
const LOGIN_NAME = 'ubuntu';
/**
* Name tag to give to new Instances (mainly for viewing in aws console)
*/
const BOX_NAME = 'aws-control-ebs';
/**
* @var \Aws\Ec2\Ec2Client Client from AWS SDK for PHP 2
*/
private $client;
/**
* @var \Monolog\Logger Logger for exceptions
*/
private $logger;
/**
* @var \Classes\InstanceProvider Factory to create Instance objects
*/
private $provider;
/**
* @var array Internal state representation of instances on Ec2
*/
private $instances = array();
/**
* @var array Internal state representation of elastic ip's
*/
private $elasticIps = array();
/**
* Constructor
*
* @param \Aws\Ec2\Ec2Client $client Client from AWS SDK for PHP 2
*
* @return void
*/
public function __construct(\Aws\Ec2\Ec2Client $client, \Monolog\Logger $logger, \Classes\InstanceProvider $provider)
{
$this->client = $client;
$this->logger = $logger;
$this->provider = $provider;
$this->refreshData();
}
/**
* List all instances according to a filter if required
*
* @param string $filter Example string 'running' or 'terminated'
*
* @return array An array of Instance objects matching filter criteria
*/
public function listInstances($filter = false)
{
return $filter ? array_filter($this->instances, function($instance) use ($filter) {
return $instance->getState() === $filter;
}) : $this->instances;
}
/**
* List all elastic ips according to a filter if required
*
* @param string $inUse If false, return only those not in use
*
* @return array An array of ip data matching filter criteria
*/
public function listElasticIps($inUse = false)
{
return $inUse ? array_filter($this->elasticIps, function($ip) {
return $ip['id'] !== false;
}) : $this->elasticIps;
}
/**
* Retrieve a specific Instance by it's id
*
* @param string $id The instance id to retrieve
*
* @return Instance An instance of an Instance
*/
public function getInstance($id)
{
return $this->instanceExists($id) ? $this->instances[$id] : false;
}
/**
* Start a new Ec2 instance and refresh the internal state representations
*
* A new instance is only created when the internal list of available
* elastic ips has an empty ip in there. <br> Therefore, if the aws console
* only has five available elastic ips, only five instances max can exist
* at any one time.
*
* @note Waits until instance running (can take between 20 seconds to a min)
*
* @return mixed If successful, Instance object, else false
*/
public function createInstance()
{
$ips = array_filter($this->elasticIps, function($ip) {
return $ip['id'] === false;
});
if (!empty($ips))
{
$this->logger->addNotice(sprintf('Creating new %s Instance with ImageId: %s, Security: %s and KeyPair: %s.', self::AMI_TYPE, self::AMI_ID, self::SECUR_NAME, self::KEY_NAME));
$instance = $this->client->runInstances(array(
'ImageId' => self::AMI_ID,
'InstanceType' => self::AMI_TYPE,
'MinCount' => 1,
'MaxCount' => 1,
'SecurityGroups' => array(self::SECUR_NAME),
'KeyName' => self::KEY_NAME
))->toArray();
if (isset($instance['Instances'][0]['InstanceId']))
{
$id = $instance['Instances'][0]['InstanceId'];
$this->associateIp($id);
$this->client->createTags(array(
'Resources' => array($id),
'Tags' => array(
array('Key' => 'Name', 'Value' => self::BOX_NAME)
)
));
$this->logger->addNotice(sprintf('Successfully created new instance with id: %s.', $id));
return $this->getInstance($id);
}
else
{
$this->logger->addError("Unable to create new instance. Data returned from S3 does not contain expected data.");
return false;
}
}
else
{
$this->logger->addError("No free elastic ip's exist, so not creating a new instance.");
return false;
}
}
/**
* Start a stopped ec2 instance
*
* @param Instance $instance An instance of the Instance class containing an id
*
* @return mixed False on failure, string from ec2 on success
*/
public function startInstance(Instance $instance)
{
$id = $instance->getId();
$this->logger->addNotice(sprintf('Starting existing instance with id: %s.', $id));
$result = ($this->instanceExists($id) && ($this->instances[$id]->getState() !== 'running') ? $this->client->startInstances(array('InstanceIds' => array($id))) : false);
$result ? $this->logger->addNotice(sprintf('Existing instance started with id: %s.', $id)) && $this->associateIp($id) : $this->logger->addWarning(sprintf('Unable to start existing instance with id: %s', $id));
return $result;
}
/**
* Stop a running ec2 instance
*
* @param Instance $instance An instance of the Instance class containing an id
*
* @return mixed False on failure, string from ec2 on success
*/
public function stopInstance(Instance $instance)
{
$id = $instance->getId();
$result = ($this->instanceExists($id) && ($this->instances[$id]->getState() === 'running') ? $this->client->stopInstances(array('InstanceIds' => array($id))) : false);
$result ? $this->logger->addNotice(sprintf('Stopping existing instance with id: %s.', $id)) : $this->logger->addError(sprintf('Unable to stop running instance with id: %s.', $id));
$this->refreshData();
return $result;
}
/**
* Send a command to an ec2 instance to execute
*
* @param Instance $instance An instance of the Instance class containing an id
* @param string $key The filepath of the keyfile to import
* @param string $command The command to send to the instance
*
* @return mixed False on failure, command output on success
*/
public function sendCommandToInstance(Instance $instance, $key, $command)
{
$id = $instance->getId();
$ip = $instance->getIp();
$key = new \Crypt_RSA();
$key->loadKey(file_get_contents(sprintf('%s/resources/%s.pem', dirname(dirname(__DIR__)), self::CERT_NAME)));
$ssh = new \Net_SSH2($ip);
$result = $ssh->login(self::LOGIN_NAME, $key) ? $ssh->exec($command) : false;
if ($result)
{
$this->logger->addNotice(sprintf('Command sent to instance with id %s: %s.', $id, $command));
$ssh->disconnect();
}
else
{
$this->logger->addError(sprintf('Command Failed: "%s" sent to instance: %s'));
}
unset($ssh);
return $result;
}
/**
* Helper function to determine whether or not the internal state
* representation of instances contains the given unique id
*
* @param string $id The instance id to check the existance of
*
* @return boolean True if the instance exists, false if it doesn't
*/
private function instanceExists($id)
{
$this->refreshData();
return array_key_exists($id, $this->instances);
}
/**
* Helper function to associate an elastic ip with an instance by it's id
*
* @param string $id The instance id to give the ip to
* @param string $ip The ip address to give to the instance
*
* @note If no $ip provided, random available used
*
* @return mixed False on failure, string from ec2 on success
*/
private function associateIp($id, $ip = false)
{
$result = false;
$this->refreshData();
if ($this->instanceExists($id))
{
if (!$ip)
{
$ips = array_filter($this->elasticIps, function($ip) {
return $ip['id'] === false;
});
if ($ips)
{
$ip = $ips[array_rand($ips)]['ip'];
}
}
try
{
$this->logger->addNotice(sprintf('Waiting for response from Instance with id: %s...', $id));
$this->client->waitUntilInstanceRunning(array(
'InstanceIds' => array($id)
));
}
catch (Exception $e)
{
$this->logger->addError(sprintf('Couldn\'t associate elastic ip: %s with instance id: %s.', $ip, $id));
return false;
}
$result = $this->client->associateAddress(array(
'InstanceId' => $id,
'PublicIp' => $ip
));
if ($result)
{
$this->logger->addNotice(sprintf('Associated elastic ip: %s with instance id: %s.', $ip, $id));
}
$this->refreshData();
}
return $result;
}
/**
* Helper function to disassociate an elastic ip with an instance by it's id
*
* @param string $id The instance Id to disassociate an IP with
*
* @return mixed False on failure, string from ec2 on success
*/
private function disassociateIp($id)
{
$result = false;
$ip = $this->getInstance($id)->getIp();
if ($ip)
{
$result = $this->client->disassociateAddress(array(
'PublicIp' => $ip
));
if ($result)
{
$this->logger->addNotice(sprintf('Disassociated elastic ip: %s from instance id: %s.', $ip, $id));
}
$this->refreshData();
}
return $result;
}
/**
* Sends a command to s3 to list instances, formats them, then creates a
* new Instance object containing the internal state representation of
* each instance
*
* @return void
*/
private function refreshData()
{
$data = $this->client->DescribeInstances(array(
'Filters' => array(
array(
'Name' => 'key-name',
'Values' => array(self::KEY_NAME)
),
array(
'Name' => 'instance-state-name',
'Values' => array('pending', 'running', 'shutting-down', 'stopping', 'stopped')
)
)
))->toArray();
foreach ($data as $key)
{
if (is_array($key))
{
foreach ($key as $boxes)
{
$box = $boxes['Instances'][0];
$instance = $this->provider->build($box['InstanceId']);
$instance->setAmi($box['ImageId']);
$instance->setState($box['State']['Name']);
$instance->setType($box['InstanceType']);
$instance->setLaunchTime($box['LaunchTime']);
$instance->setSecurity($box['KeyName']);
$instance->setIP($box['PublicDnsName']);
$this->instances[$box['InstanceId']] = $instance;
}
}
}
$ips = $this->client->describeAddresses()->toArray();
$count = count($ips['Addresses']);
for ($i = 0; $i < $count; $i++)
{
$this->elasticIps[$i]['ip'] = $ips['Addresses'][$i]['PublicIp'];
$this->elasticIps[$i]['id'] = empty($ips['Addresses'][$i]['InstanceId']) ? false : $ips['Addresses'][$i]['InstanceId'];
}
/** Debug Stats **/
$running = array_filter($this->instances, function($instance) {
return in_array($instance->getState(), array('running', 'pending'));
});
$stopped = array_filter($this->instances, function($instance) {
return in_array($instance->getState(), array('stopped', 'stopping'));
});
$this->logger->debug(sprintf('%d instances running. %d instances stopped.', count($running), count($stopped)));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment