Last active
December 24, 2015 12:19
-
-
Save J7mbo/6796657 to your computer and use it in GitHub Desktop.
An Instance controller for aws. Not hard.
This file contains hidden or 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 | |
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