Initial commit
Like to add configuration to not backup a volume mounted to a particular location.
This commit is contained in:
commit
40616f8346
16
.env.example
Normal file
16
.env.example
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Rename this file to .env and change the values as required
|
||||||
|
ENABLE=true
|
||||||
|
# The count is the old snapshots
|
||||||
|
# A value of 4 will keep 5 snapshots on AWS
|
||||||
|
# Latest and the last four
|
||||||
|
MAX_SNAPSHOT_COUNT=4
|
||||||
|
# JSON encoded key value pair of tags to assign to the snapshot
|
||||||
|
# The Name tag is automatically carried over from the instance
|
||||||
|
TAGS='{"Project":"Backup"}'
|
||||||
|
AWS_KEY=
|
||||||
|
AWS_SECRET=
|
||||||
|
AWS_REGION=
|
||||||
|
AWS_VERSION=latest
|
||||||
|
|
||||||
|
# Optional overrides (limited effects)
|
||||||
|
MACHINE_TYPE=ec2
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
vendor
|
||||||
|
|
||||||
|
.idea
|
||||||
92
README.md
Normal file
92
README.md
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# Automatic EC2 Volume Back-Up (Snapshot)
|
||||||
|
|
||||||
|
Stand-alone script designed to run via cron or systemd to back-up EC2 volumes on a regular schedule.
|
||||||
|
|
||||||
|
All volumes associated with the EC2 instance are backed up.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
After the required configuration settings are set up in the `.env` file, simply executing `php backup.php` will
|
||||||
|
perform the backup. The script has additional options such as `--no-prune` to prevent the script from removing old
|
||||||
|
snapshots. If the configuration setting `ENABLE` is set to `false` the script will no perform any actions. To override
|
||||||
|
the `ENABLE` setting, `--force` can be used. The script will also respond to `--help` to outline the usage.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Copy the repository (via `git clone` or upload files) to your server. My recommendation is to install in your home
|
||||||
|
directory (ec2-user or ubuntu).
|
||||||
|
|
||||||
|
1. From this directory, run `composer install`
|
||||||
|
2. Copy (or rename/move) the `.env.example` file to the same directory
|
||||||
|
3. Add the required AWS credentials (see below) to the `.env` file
|
||||||
|
4. Modify the `TAGS` value in `.env`
|
||||||
|
|
||||||
|
You will want to set up this script to run on a regular schedule. For instance, I run this every day. Examples using
|
||||||
|
Crontab and SystemD are outlined below.
|
||||||
|
|
||||||
|
### Requires IAM permission
|
||||||
|
It is **STRONGLY** recommended to create an IAM use specifically for programmatic use. The script requires
|
||||||
|
AWS credentials to be stored in a hidden file.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "VisualEditor0",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"ec2:DescribeVolumeStatus",
|
||||||
|
"ec2:DeleteSnapshot",
|
||||||
|
"ec2:DescribeTags",
|
||||||
|
"ec2:DescribeSnapshotAttribute",
|
||||||
|
"ec2:DescribeVolumes",
|
||||||
|
"ec2:CreateSnapshot",
|
||||||
|
"ec2:CreateTags",
|
||||||
|
"ec2:DescribeSnapshots",
|
||||||
|
"ec2:DescribeVolumeAttribute"
|
||||||
|
],
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schedule Execution via Cron
|
||||||
|
The following example will run the backup script every day at 3am UTC (server time) and send script outputs
|
||||||
|
to `backup.log` in the home directory.
|
||||||
|
|
||||||
|
Be sure to change the path to the script and your log file.
|
||||||
|
|
||||||
|
`0 3 * * * /usr/bin/env php /home/ubuntu/backup/backup.php >> /home/ubuntu/backup.log 2>&1`
|
||||||
|
|
||||||
|
### Schedule Execution via SystemD
|
||||||
|
The following example will run the backup script every day at 3am UTC (server time). Script outputs are handled
|
||||||
|
directly by SystemD and are available through `journalctl`.
|
||||||
|
|
||||||
|
Create the two following files (requires root permission). Be sure to change the path to the script.
|
||||||
|
|
||||||
|
/etc/systemd/system/ec2-backup.service
|
||||||
|
```unit file (systemd)
|
||||||
|
[Unit]
|
||||||
|
Description=Create and rotate EBS snapshots on AWS for EC2 instances
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/env php /home/ubuntu/backup/backup.php
|
||||||
|
User=ubuntu
|
||||||
|
```
|
||||||
|
|
||||||
|
/etc/systemd/system/ec2-backup.timer
|
||||||
|
```unit file (systemd)
|
||||||
|
[Unit]
|
||||||
|
Description=Run ec2-backup.service every day
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=03:00
|
||||||
|
AccuracySec=1h
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
```
|
||||||
|
|
||||||
|
To activate the schedule, execute the command (noting the sudo use)
|
||||||
|
`sudo systemctl start ec2-backup.timer`
|
||||||
50
app/Dates.php
Normal file
50
app/Dates.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Date Helper
|
||||||
|
*
|
||||||
|
* @package Fairbanks\Kizarian\Utilities
|
||||||
|
* @copyright (c) 2018, Fairbanks Publishing
|
||||||
|
* @license Proprietary
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Dates
|
||||||
|
*
|
||||||
|
* @author David Fairbanks <david@makerdave.com>
|
||||||
|
* @package Fairbanks\Kizarian\Utilities
|
||||||
|
* @version 2.0
|
||||||
|
*/
|
||||||
|
class Dates {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $date
|
||||||
|
* @param null|Carbon $default
|
||||||
|
*
|
||||||
|
* @return Carbon
|
||||||
|
*/
|
||||||
|
public static function makeCarbon($date, Carbon $default=null)
|
||||||
|
{
|
||||||
|
if(is_object($date) && $date instanceof Carbon) {
|
||||||
|
return $date;
|
||||||
|
} elseif(is_object($date) && $date instanceof \DateTime) {
|
||||||
|
return Carbon::instance($date);
|
||||||
|
} elseif(is_object($date)) {
|
||||||
|
return Carbon::parse($date->date, $date->timezone);
|
||||||
|
} elseif(is_array($date)) {
|
||||||
|
return Carbon::parse($date['date'], $date['timezone']);
|
||||||
|
} elseif(is_numeric($date)) {
|
||||||
|
return Carbon::createFromTimestamp($date);
|
||||||
|
} elseif(is_string($date)) {
|
||||||
|
return Carbon::parse($date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($default !== null)
|
||||||
|
return $default;
|
||||||
|
|
||||||
|
return Carbon::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
269
app/Ec2Backup.php
Normal file
269
app/Ec2Backup.php
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* (file name)
|
||||||
|
*
|
||||||
|
* @copyright (c) 2019, Fairbanks Publishing
|
||||||
|
* @license Proprietary
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Aws\Ec2\Ec2Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Ec2Snapshot
|
||||||
|
*
|
||||||
|
* @author David Fairbanks <david@makerdave.com>
|
||||||
|
* @version 2.0
|
||||||
|
*/
|
||||||
|
class Ec2Backup
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Ec2Client
|
||||||
|
*/
|
||||||
|
private $ec2Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
private $enable = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of completed backups to keep
|
||||||
|
* This is the number BEFORE a backup is started
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $maxBackupCount = 4;
|
||||||
|
|
||||||
|
public function __construct(Ec2Client $ec2Client) {
|
||||||
|
$this->ec2Client = $ec2Client;
|
||||||
|
|
||||||
|
$this->enable = boolean(config('ENABLE', true));
|
||||||
|
|
||||||
|
$this->maxBackupCount = (int)config('MAX_SNAPSHOT_COUNT', 4);
|
||||||
|
if($this->maxBackupCount <= 0)
|
||||||
|
$this->maxBackupCount = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(array $options=[])
|
||||||
|
{
|
||||||
|
$options = array_merge(['noPrune' => false, 'force' => false], $options);
|
||||||
|
|
||||||
|
if($this->enable == false && $options['force'] != true) {
|
||||||
|
app_echo('EC2 Backup is disabled in environment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* is this an ec2 instance
|
||||||
|
* get volume ID
|
||||||
|
* get list of snapshots
|
||||||
|
* prune old snapshots
|
||||||
|
* create new snapshot
|
||||||
|
*/
|
||||||
|
|
||||||
|
try {
|
||||||
|
$machine = MachineDetails::getDetails();
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
app_echo("Error getting machine details: {$e->getMessage()}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($machine['type'] != 'ec2') {
|
||||||
|
app_echo('Instance type is wrong to do an EC2 backup', $machine);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$volumes = $this->getVolumes($machine['instanceId']);
|
||||||
|
$tags = $this->getTags($machine['instanceId']);
|
||||||
|
|
||||||
|
foreach($volumes as $volume) {
|
||||||
|
if($options['noPrune'] == true) {
|
||||||
|
$pruneWording = '(pruning disabled)';
|
||||||
|
} else {
|
||||||
|
$pruneCount = $this->pruneBackups($volume['volumeId']);
|
||||||
|
$pruneWording = "and pruned {$pruneCount} old snapshots";
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = (isset($tags['Name'])) ? $tags['Name'] : $machine['instanceId'];
|
||||||
|
if(count($volumes) > 1)
|
||||||
|
$name .= " ({$volume['device']})";
|
||||||
|
|
||||||
|
$r = $this->backup(['volumeId' => $volume['volumeId'], 'name' => $name]);
|
||||||
|
if($r == true) {
|
||||||
|
app_echo("Successfully started snapshot for {$volume['volumeId']} {$pruneWording}");
|
||||||
|
} else {
|
||||||
|
app_echo("Error starting snapshot for {$volume['volumeId']} {$pruneWording}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVolumes($instanceId)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->ec2Client->describeVolumes(
|
||||||
|
[
|
||||||
|
'DryRun' => false,
|
||||||
|
'Filters' => [
|
||||||
|
[
|
||||||
|
'Name' => 'attachment.instance-id',
|
||||||
|
'Values' => [$instanceId]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
app_echo('Error getting volumes: ' . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$volumes = [];
|
||||||
|
foreach($result['Volumes'] as $volume) {
|
||||||
|
$volumes[$volume['VolumeId']] = [
|
||||||
|
'device' => $volume['Attachments'][0]['Device'],
|
||||||
|
'volumeId' => $volume['VolumeId']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $volumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTags($instanceId)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->ec2Client->describeTags(
|
||||||
|
[
|
||||||
|
'DryRun' => false,
|
||||||
|
'Filters' => [
|
||||||
|
[
|
||||||
|
'Name' => 'resource-id',
|
||||||
|
'Values' => [$instanceId]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
app_echo('Error getting instance tags: ' . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$tags = [];
|
||||||
|
foreach($result['Tags'] as $tag) {
|
||||||
|
$tags[$tag['Key']] = $tag['Value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBackups($volumeId)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->ec2Client->describeSnapshots(
|
||||||
|
[
|
||||||
|
'DryRun' => false,
|
||||||
|
'Filters' => [
|
||||||
|
[
|
||||||
|
'Name' => 'volume-id',
|
||||||
|
'Values' => [$volumeId],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
app_echo('Error getting current snapshots: ' . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshots = [];
|
||||||
|
foreach($result['Snapshots'] as $snapshot) {
|
||||||
|
if($snapshot['State'] != 'completed')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$snapshots[$snapshot['SnapshotId']] = [
|
||||||
|
'started' => Dates::makeCarbon($snapshot['StartTime']),
|
||||||
|
'description' => $snapshot['Description'],
|
||||||
|
'snapshotId' => $snapshot['SnapshotId']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
uasort($snapshots, function($a, $b) {
|
||||||
|
if($a['started'] == $b['started']) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return $a['started'] > $b['started'] ? 1 : 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $snapshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pruneBackups($volumeId)
|
||||||
|
{
|
||||||
|
$backups = $this->getBackups($volumeId);
|
||||||
|
|
||||||
|
if(count($backups) <= $this->maxBackupCount)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
$prune = array_slice($backups, 0, count($backups) - $this->maxBackupCount);
|
||||||
|
if(empty($prune))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
foreach($prune as $snapshotId => $snapshot) {
|
||||||
|
try {
|
||||||
|
$this->ec2Client->deleteSnapshot(
|
||||||
|
[
|
||||||
|
'DryRun' => false,
|
||||||
|
'SnapshotId' => $snapshotId
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
app_echo('Error pruning snapshot: ' . $e->getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function backup(array $params)
|
||||||
|
{
|
||||||
|
if(!isset($params['volumeId'])) {
|
||||||
|
app_echo('Volume ID is not set in backup parameters');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = (isset($params['name'])) ? $params['name'] : $params['volumeId'];
|
||||||
|
|
||||||
|
$tags = [['Key' => 'Name', 'Value' => $name]];
|
||||||
|
$snapTags = json_decode(config('TAGS', '[]'), true);
|
||||||
|
if(is_array($snapTags) && !empty($snapTags)) {
|
||||||
|
foreach($snapTags as $key => $value) {
|
||||||
|
$tags[] = ['Key' => $key, 'Value' => $value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->ec2Client->createSnapshot(
|
||||||
|
[
|
||||||
|
'Description' => sprintf('%s Backup %s', $name, date('Y-m-d')),
|
||||||
|
'DryRun' => false,
|
||||||
|
'TagSpecifications' => [
|
||||||
|
[
|
||||||
|
'ResourceType' => 'snapshot',
|
||||||
|
'Tags' => $tags
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'VolumeId' => $params['volumeId']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
app_echo($e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
271
app/MachineDetails.php
Normal file
271
app/MachineDetails.php
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Machine Details for Logging
|
||||||
|
*
|
||||||
|
* @copyright (c) 2017, Fairbanks Publishing
|
||||||
|
* @license Proprietary
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class MachineDetails
|
||||||
|
*
|
||||||
|
* @author David Fairbanks <david@makerdave.com>
|
||||||
|
* @version 2.0
|
||||||
|
*/
|
||||||
|
class MachineDetails
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var MachineDetails
|
||||||
|
*/
|
||||||
|
protected static $instance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected static $details = [
|
||||||
|
'type' => null,
|
||||||
|
'id' => null,
|
||||||
|
'machineId' => null,
|
||||||
|
'instanceId' => null,
|
||||||
|
'region' => null,
|
||||||
|
'publicIp' => null,
|
||||||
|
'privateIp' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHP config setting for default_socket_timeout to reset to after doing file_get_contents()
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $socketTimeout = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return MachineDetails
|
||||||
|
*/
|
||||||
|
public static function getInstance()
|
||||||
|
{
|
||||||
|
if (!isset(static::$instance)) {
|
||||||
|
static::$instance = new static;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getDetails()
|
||||||
|
{
|
||||||
|
if(self::$instance === null)
|
||||||
|
self::$instance = self::getInstance();
|
||||||
|
|
||||||
|
return self::$details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static function id()
|
||||||
|
{
|
||||||
|
if(self::$instance === null)
|
||||||
|
self::$instance = self::getInstance();
|
||||||
|
|
||||||
|
return self::$details['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static function machineId()
|
||||||
|
{
|
||||||
|
if(self::$instance === null)
|
||||||
|
self::$instance = self::getInstance();
|
||||||
|
|
||||||
|
return self::$details['machineId'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function fullMachineId()
|
||||||
|
{
|
||||||
|
if(self::$instance === null)
|
||||||
|
self::$instance = self::getInstance();
|
||||||
|
|
||||||
|
$id = [
|
||||||
|
config('MACHINE_TYPE'),
|
||||||
|
self::$details['region'],
|
||||||
|
self::$details['machineId']
|
||||||
|
];
|
||||||
|
|
||||||
|
return implode('-', array_unique(array_filter($id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static function region()
|
||||||
|
{
|
||||||
|
if(self::$instance === null)
|
||||||
|
self::$instance = self::getInstance();
|
||||||
|
|
||||||
|
return self::$details['region'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static function publicIp()
|
||||||
|
{
|
||||||
|
if(self::$instance === null)
|
||||||
|
self::$instance = self::getInstance();
|
||||||
|
|
||||||
|
if(self::$details['publicIp'] === null) {
|
||||||
|
switch(self::$details['type']) {
|
||||||
|
case 'ec2' :
|
||||||
|
self::$details['publicIp'] = self::getEc2PublicIp();
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
self::$details['publicIp'] = self::getLocalPublicIp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$details['privateIp'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static function privateIp()
|
||||||
|
{
|
||||||
|
if(self::$instance === null)
|
||||||
|
self::$instance = self::getInstance();
|
||||||
|
|
||||||
|
return self::$details['privateIp'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Private methods */
|
||||||
|
|
||||||
|
private static function getLocalPublicIp()
|
||||||
|
{
|
||||||
|
return '127.0.0.1';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getEc2PublicIp()
|
||||||
|
{
|
||||||
|
if(self::$instance === null)
|
||||||
|
self::$instance = self::getInstance();
|
||||||
|
|
||||||
|
self::$instance->shortenTimeout();
|
||||||
|
$ip = @file_get_contents('http://169.254.169.254/latest/meta-data/public-ipv4');
|
||||||
|
self::$instance->resetTimeout();
|
||||||
|
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
$type = config('MACHINE_TYPE');
|
||||||
|
if($type === null || empty($type)) {
|
||||||
|
$type = $this->determineMachineType();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch($type) {
|
||||||
|
case 'ec2' :
|
||||||
|
self::$details['type'] = 'ec2';
|
||||||
|
self::$details = array_merge(self::$details, $this->getEc2Data());
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
self::$details['type'] = 'local';
|
||||||
|
self::$details = array_merge(self::$details, $this->getLocalData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make clone magic method private, so nobody can clone instance.
|
||||||
|
*/
|
||||||
|
private function __clone() {}
|
||||||
|
|
||||||
|
private function determineMachineType()
|
||||||
|
{
|
||||||
|
$this->shortenTimeout();
|
||||||
|
$hostname = @file_get_contents('http://169.254.169.254/latest/meta-data/hostname');
|
||||||
|
$this->resetTimeout();
|
||||||
|
|
||||||
|
if(strpos($hostname, 'ec2') !== false) {
|
||||||
|
return 'ec2';
|
||||||
|
} else {
|
||||||
|
return 'local';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLocalData()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => 'dev',
|
||||||
|
'machineId' => 'dev',
|
||||||
|
'region' => 'local',
|
||||||
|
'publicIp' => '127.0.0.1',
|
||||||
|
'privateIp' => '127.0.0.1',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getEc2Data()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* curl "http://169.254.169.254/latest/dynamic/instance-identity/document"
|
||||||
|
* {
|
||||||
|
* "privateIp" : "172.31.59.103",
|
||||||
|
* "devpayProductCodes" : null,
|
||||||
|
* "availabilityZone" : "us-east-1b",
|
||||||
|
* "version" : "2010-08-31",
|
||||||
|
* "instanceId" : "i-0a1233...",
|
||||||
|
* "billingProducts" : null,
|
||||||
|
* "pendingTime" : "2017-05-12T15:21:57Z",
|
||||||
|
* "instanceType" : "t2.micro",
|
||||||
|
* "accountId" : "393...",
|
||||||
|
* "architecture" : "x86_64",
|
||||||
|
* "kernelId" : null,
|
||||||
|
* "ramdiskId" : null,
|
||||||
|
* "imageId" : "ami-9a...",
|
||||||
|
* "region" : "us-east-1"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
$this->shortenTimeout();
|
||||||
|
$json = @file_get_contents('http://169.254.169.254/latest/dynamic/instance-identity/document');
|
||||||
|
$this->resetTimeout();
|
||||||
|
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
|
||||||
|
if(!is_array($data) || empty($data)) {
|
||||||
|
throw new \Exception('Invalid machine details from http://169.254.169.254. Assumption is this is not an EC2 instance.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($data['instanceId']))
|
||||||
|
$data['machineId'] = substr($data['instanceId'], 2);
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach(self::$details as $key => $v) {
|
||||||
|
$out[$key] = (isset($data[$key])) ? $data[$key] : $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shortenTimeout()
|
||||||
|
{
|
||||||
|
if($this->socketTimeout === null) {
|
||||||
|
$this->socketTimeout = ini_get('default_socket_timeout');
|
||||||
|
}
|
||||||
|
|
||||||
|
ini_set('default_socket_timeout', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resetTimeout()
|
||||||
|
{
|
||||||
|
if($this->socketTimeout !== null) {
|
||||||
|
ini_set('default_socket_timeout', $this->socketTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
app/functions.php
Normal file
160
app/functions.php
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Global helper functions
|
||||||
|
*
|
||||||
|
* @copyright (c) 2018, Fairbanks Publishing
|
||||||
|
* @license Proprietary
|
||||||
|
*/
|
||||||
|
|
||||||
|
if(!function_exists('array_combine_safe')) {
|
||||||
|
/**
|
||||||
|
* Same affect as array_combine but does not error if the two arrays are different lengths
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @param array $values
|
||||||
|
* @param null $default
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
function array_combine_safe(array $keys = [], array $values = [], $default = null)
|
||||||
|
{
|
||||||
|
if(empty($keys)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
|
||||||
|
foreach($keys as $index => $key) {
|
||||||
|
$out[$key] = (isset($values[$index])) ? $values[$index] : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('array_limit_keys')) {
|
||||||
|
/**
|
||||||
|
* Filter input array to only have the keys provided
|
||||||
|
*
|
||||||
|
* Only ensures first level of supplied array
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @param array $input
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
function array_limit_keys(array $keys = [], array $input = [])
|
||||||
|
{
|
||||||
|
if(empty($keys)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
//return array_filter($input, function($item) use ($keys) {
|
||||||
|
// return in_array($item, $keys);
|
||||||
|
//}, ARRAY_FILTER_USE_KEY);
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach($keys as $key) {
|
||||||
|
if(array_key_exists($key, $input)) {
|
||||||
|
$out[$key] = $input[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('boolean')) {
|
||||||
|
/**
|
||||||
|
* Convert value to boolean
|
||||||
|
*
|
||||||
|
* Extends boolval() to look at more words as being TRUE.
|
||||||
|
* Strings and numbers default to FALSE.
|
||||||
|
* Notable differences from boolval() docs:
|
||||||
|
* +----------+-----------+-----------+
|
||||||
|
* | val | boolval() | boolean() |
|
||||||
|
* +----------+-----------+-----------+
|
||||||
|
* | 0 | false | false |
|
||||||
|
* | 42 | true | false |
|
||||||
|
* | 0 | false | false |
|
||||||
|
* | 4.2 | true | false |
|
||||||
|
* | 1 | true | true |
|
||||||
|
* | NULL | false | false |
|
||||||
|
* | "" | false | false |
|
||||||
|
* | "string" | true | false |
|
||||||
|
* | "0" | false | false |
|
||||||
|
* | "1" | true | true |
|
||||||
|
* | "yes" | true | true |
|
||||||
|
* | "no" | true | false |
|
||||||
|
* | "y" | true | true |
|
||||||
|
* | "n" | true | false |
|
||||||
|
* | "true" | true | true |
|
||||||
|
* | "false" | true | false |
|
||||||
|
* | [1,2] | true | true |
|
||||||
|
* | [] | false | false |
|
||||||
|
* | stdClass | true | true |
|
||||||
|
* +----------+-----------+-----------+
|
||||||
|
*
|
||||||
|
* @param boolean|int|string|null $var
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
function boolean($var)
|
||||||
|
{
|
||||||
|
if(is_bool($var)) {
|
||||||
|
return ($var == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(is_string($var)) {
|
||||||
|
$var = strtolower($var);
|
||||||
|
|
||||||
|
switch($var) {
|
||||||
|
case 'true' :
|
||||||
|
case 'on' :
|
||||||
|
case 'yes' :
|
||||||
|
case 'y' :
|
||||||
|
case '1' :
|
||||||
|
return true;
|
||||||
|
default :
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(is_numeric($var)) {
|
||||||
|
return ($var == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return boolval($var);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('config')) {
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
* @param scalar $default
|
||||||
|
* @return scalar
|
||||||
|
*/
|
||||||
|
function config($key, $default = null)
|
||||||
|
{
|
||||||
|
if(array_key_exists($key, $_ENV)) {
|
||||||
|
return $_ENV[$key];
|
||||||
|
} else {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('app_echo')) {
|
||||||
|
/**
|
||||||
|
* @param string $message
|
||||||
|
* @param array $context
|
||||||
|
*/
|
||||||
|
function app_echo($message, $context = [])
|
||||||
|
{
|
||||||
|
if(is_array($context) && !empty($context)) {
|
||||||
|
$message .= ' ' . json_encode($context);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo trim($message) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
44
backup.php
Normal file
44
backup.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$arguments = getopt('fph', ['force', 'no-prune', 'help']);
|
||||||
|
if(isset($arguments['h']) || isset($arguments['help'])) {
|
||||||
|
echo <<<hereDoc
|
||||||
|
Create and rotate EBS snapshots on AWS for EC2 instances.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
{$argv[0]} [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-f, --force Override the ENABLE environment variable
|
||||||
|
-p, --no-prune Prevent the removal of old snapshots
|
||||||
|
|
||||||
|
hereDoc;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'force' => isset($arguments['f']) || isset($arguments['force']),
|
||||||
|
'noPrune' => isset($arguments['p']) || isset($arguments['no-prune'])
|
||||||
|
];
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/vendor/autoload.php');
|
||||||
|
|
||||||
|
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
||||||
|
$dotenv->load();
|
||||||
|
$dotenv->required('AWS_KEY')->notEmpty();
|
||||||
|
$dotenv->required('AWS_SECRET')->notEmpty();
|
||||||
|
$dotenv->required('AWS_REGION')->notEmpty();
|
||||||
|
$dotenv->ifPresent('MAX_SNAPSHOT_COUNT')->isInteger();
|
||||||
|
$dotenv->ifPresent('ENABLE')->isBoolean();
|
||||||
|
|
||||||
|
$ec2Client = new \Aws\Ec2\Ec2Client([
|
||||||
|
'credentials' => [
|
||||||
|
'key' => config('AWS_KEY'),
|
||||||
|
'secret' => config('AWS_SECRET')
|
||||||
|
],
|
||||||
|
'region' => config('AWS_REGION'),
|
||||||
|
'version' => config('AWS_VERSION', 'latest')
|
||||||
|
]);
|
||||||
|
$ec2Backup = new \App\Ec2Backup($ec2Client);
|
||||||
|
|
||||||
|
$ec2Backup->create();
|
||||||
21
composer.json
Normal file
21
composer.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"aws/aws-sdk-php": "^3.173",
|
||||||
|
"nesbot/carbon": "^2.45",
|
||||||
|
"vlucas/phpdotenv": "^5.3"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"app/functions.php"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
1272
composer.lock
generated
Normal file
1272
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user