Skip to content

Instantly share code, notes, and snippets.

@darioguarascio
Last active October 22, 2015 18:11
Show Gist options
  • Save darioguarascio/21e7c188b467154e013b to your computer and use it in GitHub Desktop.
Save darioguarascio/21e7c188b467154e013b to your computer and use it in GitHub Desktop.
Laravel command to add/remove/list a domain IPs part of the CloudFlare round-robin ip Failover system
<?php
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use \Requests;
/*
|--------------------------------------------------------------------------
| Cloudflare domain IPv4 A-type manager
|--------------------------------------------------------------------------
|
| This Laravel command performs actions on domains only (ie: example.com
| but not sub.example.com) adding, deleting or listing associated ips.
|
| `php artisan cloudflar:failover example.com list`
|
| `php artisan cloudflar:failover example.com add 127.0.0.1`
|
| `php artisan cloudflar:failover example.com delete 99.99.99.99`
*/
class CloudflareFailover extends Command {
/**
* The console command name.
*
* @var string
*/
protected $name = 'cloudflare:failover';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add or delete IP from Cloudflare domain';
/**
* Cloudflare API Endpoint
*
* @return void
*/
protected $apiEndpoint = "https://www.cloudflare.com/api_json.html";
public function __construct()
{
parent::__construct();
Log::listen(function($level, $message, $context)
{
$this->info(sprintf("[%s] %s", $level, $message));
});
}
private function _storeSet($key,$value) {
Cache::forever('cloudflare.'.$key, $value);
}
private function _storeGet($key, $default = 0) {
return Cache::get('cloudflare.'.$key, $default);
}
/**
* Cloudflare user email as optional argument or read from config file
*
* @return string
*/
private function _cfEmail() {
return $this->option('email') ? $this->option('email') : Config::get('cloudflare.email');
}
/**
* Cloudflare user token as optional argument or read from config file
* Token can be found here: https://www.cloudflare.com/my-account
*
* @return string
*/
private function _cfToken() {
return $this->option('token') ? $this->option('token') : Config::get('cloudflare.token');
}
/**
* Fail threshold - Number of checks to tolerate before taking action
*
* @return string
*/
private function _cfFails() {
return $this->option('fails') ? $this->option('fails') : Config::get('cloudflare.fail-threshold');
}
/**
* Add an A record to a domain
*
* @return array
*/
private function _addRecord($host, $ip) {
$query = array(
"email" => $this->_cfEmail(),
"tkn" => $this->_cfToken(),
"z" => $host,
"a" => "rec_new",
"type" => "A",
"name" => $host,
"content" => $ip,
"ttl" => 3600,
);
try {
$req = Requests::get($this->apiEndpoint.'?'.http_build_query($query, '', '&'));
$data = json_decode($req->body);
return array(
$data->result,
$data->msg );
} catch (Exception $e) {
throw $e;
}
}
/**
* Delete an A record from a domain
*
* @return array
*/
private function _delRecord($host, $id) {
$query = array(
"email" => $this->_cfEmail(),
"tkn" => $this->_cfToken(),
"z" => $host,
"a" => "rec_delete",
"id" => $id
);
try {
$req = Requests::get($this->apiEndpoint.'?'.http_build_query($query, '', '&'));
$data = json_decode($req->body);
return array(
$data->result,
$data->msg );
} catch (Exception $e) {
throw $e;
}
}
/**
* Get all the A-type record for a domain
*
* @return array
*/
private function _getRecords($host) {
$query = array(
"email" => $this->_cfEmail(),
"tkn" => $this->_cfToken(),
"z" => $host,
"a" => "rec_load_all"
);
try {
$result = array();
$req = Requests::get($this->apiEndpoint.'?'.http_build_query($query, '', '&'));
$data = json_decode($req->body);
foreach ($data->response->recs->objs as $entry) {
if ($entry->name == $host && $entry->type == 'A') {
$result[ $entry->rec_id ] = $entry->content;
}
}
return $result;
} catch (Exception $e) {
throw $e;
}
}
protected function defaultCheck($ip) {
Log::debug('Running defaultCheck('.$ip.') => TRUE');
return true;
}
protected function randomCheck($ip) {
$result = rand(0,1);
Log::debug('Running randomCheck('.$ip.') => ' . $result);
return (bool) $result;
}
protected function falseCheck($ip) {
Log::debug('Running randomCheck('.$ip.') => FALSE');
return false;
}
protected function addToPool($ip) {
$target = $this->argument('target');
$result = $this->_addRecord($target, $ip);
if ($result[0] == 'success') {
Log::debug(sprintf("Ip %s added to %s", $ip, $target));
return true;
} else {
Log::debug($result[1]);
return false;
}
}
protected function deleteFromPool($ip) {
$target = $this->argument('target');
$list = $this->_getRecords($target);
if (count($list) == 1 && !$this->option('unsafe')) {
$this->error(sprintf("%s contains only 1 ip, and it will not be deleted. To force deletion, use --unsafe option.", $target));
}
else {
foreach ($list as $id => $addr) {
if ($addr == $ip) {
$result = $this->_delRecord($target, $id);
if ($result[0] == 'success') {
Log::debug(sprintf("Ip %s deleted from %s", $ip, $target) );
return true;
} else {
Log::debug($this->error($result[1]));
return false;
}
return;
}
}
Log::debug(sprintf("Ip %s not found in the pool for %s", $ip, $target));
return true;
}
}
private function getFailedChecks($ip){
$k = sprintf("%s.failedChecks.%s", $this->argument('target'), $ip);
return $this->_storeGet($k, 0);
}
private function setFailedChecks($ip, $n){
$k = sprintf("%s.failedChecks.%s", $this->argument('target'), $ip);
return $this->_storeSet($k, $n);
}
private function setRemovedState($ip, $state = 1){
$k = sprintf("%s.removed.%s", $this->argument('target'), $ip);
return $this->_storeSet($k, $state);
}
private function getRemovedState($ip){
$k = sprintf("%s.removed.%s", $this->argument('target'), $ip);
return $this->_storeGet($k,0);
}
/**
* Execute the console command.
*
* @return void
*/
public function fire()
{
$run = $this->_storeGet('fire');
$this->_storeSet('fire', ++$run);
Log::info(sprintf("Run %s times", $run ));
$target = $this->argument('target');
switch ($this->argument('action')) {
case "delete":
$ip = $this->argument('arg_ip');
$this->deleteFromPool($ip);
break;
case "add":
$ip = $this->argument('arg_ip');
break;
case "check":
$list = $this->option('checkIp');
$check = array($this, $this->option('checkMethod') . 'Check' );
foreach ($list as $addr) {
Log::info('Start checking ip: ' . $addr . ' using ' . $this->option('checkMethod') );
$result = call_user_func_array($check, array( $addr ));
$failedChecks = $this->getFailedChecks($addr);
if (!$result) {
$failedChecks++;
Log::warning(sprintf("Check for ip %s failed %s times", $addr, $failedChecks));
if ($failedChecks > $this->_cfFails()) {
if ($this->getRemovedState($addr)) {
Log::critical(sprintf("Ip %s still removed from the pool after %s checks", $addr, $failedChecks));
} else {
Log::alert(sprintf("Ip %s fails passed the threshold, removing from CloudFlare pool", $addr, $failedChecks));
$del = $this->deleteFromPool($addr);
if ($del === true ){
$this->setRemovedState($addr);
} else {
Log::error($del);
}
}
}
$this->setFailedChecks($addr, $failedChecks);
} else {
if ($failedChecks > 0) {
Log::notice(sprintf("Check for ip %s returned OK after %s checks.", $addr, $failedChecks));
if ($this->getRemovedState($addr)) {
Log::alert(sprintf("Adding %s to %s pool...", $addr, $target));
if ($this->addToPool($addr)) {
$this->setRemovedState($addr, 0);
}
}
}
$this->setFailedChecks($addr, 0);
}
}
break;
case "list":
default:
$list = $this->_getRecords($target);
if (count($list) == 0) {
$this->info(sprintf("%s has no ips", $target));
} else {
foreach ($list as $id => $ip) {
$this->info(sprintf("%s has %s", $target, $ip));
}
}
}
}
/**
* Get the console command arguments.
* @return array
*/
protected function getArguments()
{
return array(
array('target', InputArgument::REQUIRED, 'Target domain on CloudFlare'),
array('action', InputArgument::OPTIONAL, 'Command to perform (add, delete, list)', 'list'),
array('arg_ip', InputArgument::OPTIONAL, 'Ip to add or delete', null),
);
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return array(
array('unsafe', null, InputOption::VALUE_OPTIONAL, 'On deletions, skip checking if the ip is the only one in the pool', false),
array('email', null, InputOption::VALUE_OPTIONAL, 'Cloudflare email address', false),
array('token', null, InputOption::VALUE_OPTIONAL, 'Cloudflare auth token', false),
array('fails', null, InputOption::VALUE_OPTIONAL, 'Fails threshold', false),
array('checkMethod', null, InputOption::VALUE_OPTIONAL, 'IP Check method', 'default'),
array('checkIp', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'IP to check', array()),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment