Updates to work with metadata service
This commit is contained in:
parent
92fd23b18c
commit
e9d1a9ccc1
@ -11,7 +11,7 @@ during the snapshot start which will break the database.
|
|||||||
## Usage
|
## Usage
|
||||||
After the required configuration settings are set up in the `.env` file, simply executing `php backup.php` will
|
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
|
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
|
snapshots. If the configuration setting `ENABLE` is set to `false` the script will not perform any actions. To override
|
||||||
the `ENABLE` setting, `--force` can be used. The script will also respond to `--help` to outline the usage.
|
the `ENABLE` setting, `--force` can be used. The script will also respond to `--help` to outline the usage.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|||||||
@ -13,7 +13,6 @@ use Carbon\Carbon;
|
|||||||
* Class Dates
|
* Class Dates
|
||||||
*
|
*
|
||||||
* @author David Fairbanks <david@makerdave.com>
|
* @author David Fairbanks <david@makerdave.com>
|
||||||
* @package Fairbanks\Kizarian\Utilities
|
|
||||||
* @version 2.0
|
* @version 2.0
|
||||||
*/
|
*/
|
||||||
class Dates {
|
class Dates {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Ec2Backup.php
|
* Ec2Backup.php
|
||||||
*
|
*
|
||||||
* @copyright 2023 Fairbanks Publishing LLC
|
* @copyright 2025 Fairbanks Publishing LLC
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
@ -10,21 +10,19 @@ namespace App;
|
|||||||
use Aws\Ec2\Ec2Client;
|
use Aws\Ec2\Ec2Client;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Ec2Snapshot
|
* Class Ec2Backup
|
||||||
*
|
*
|
||||||
* @author David Fairbanks <david@makerdave.com>
|
* @author David Fairbanks <david@makerdave.com>
|
||||||
* @version 2.0
|
* @version 3.0
|
||||||
*/
|
*/
|
||||||
class Ec2Backup
|
class Ec2Backup
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var Ec2Client
|
|
||||||
*/
|
|
||||||
private Ec2Client $ec2Client;
|
private Ec2Client $ec2Client;
|
||||||
|
private array $options = [
|
||||||
/**
|
'noPrune' => false,
|
||||||
* @var boolean
|
'force' => false,
|
||||||
*/
|
'dryRun' => false,
|
||||||
|
];
|
||||||
private bool $enable = true;
|
private bool $enable = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,21 +32,26 @@ class Ec2Backup
|
|||||||
*/
|
*/
|
||||||
private int $maxBackupCount = 4;
|
private int $maxBackupCount = 4;
|
||||||
|
|
||||||
public function __construct(Ec2Client $ec2Client) {
|
public function __construct(Ec2Client $ec2Client, array $options = [])
|
||||||
|
{
|
||||||
$this->ec2Client = $ec2Client;
|
$this->ec2Client = $ec2Client;
|
||||||
|
foreach ($this->options as $key => $default) {
|
||||||
|
if (array_key_exists($key, $options) && is_bool($options[$key])) {
|
||||||
|
$this->options[$key] = $options[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->enable = boolean(config('ENABLE', true));
|
$this->enable = boolean(config('ENABLE', true));
|
||||||
|
|
||||||
$this->maxBackupCount = (int)config('MAX_SNAPSHOT_COUNT', 4);
|
$this->maxBackupCount = (int) config('MAX_SNAPSHOT_COUNT', 4);
|
||||||
if($this->maxBackupCount <= 0)
|
if ($this->maxBackupCount <= 0) {
|
||||||
$this->maxBackupCount = 4;
|
$this->maxBackupCount = 4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $options=[]): void
|
public function create(): void
|
||||||
{
|
{
|
||||||
$options = array_merge(['noPrune' => false, 'force' => false], $options);
|
if (! $this->enable && ! $this->options['force']) {
|
||||||
|
|
||||||
if (!$this->enable && !$options['force']) {
|
|
||||||
app_echo('EC2 Backup is disabled in environment');
|
app_echo('EC2 Backup is disabled in environment');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -63,30 +66,31 @@ class Ec2Backup
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$machine = MachineDetails::getDetails();
|
$machine = MachineDetails::getDetails();
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
app_echo("Error getting machine details: {$e->getMessage()}");
|
app_echo("Error getting machine details: {$e->getMessage()}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($machine['type'] != 'ec2') {
|
if ($machine->type != 'ec2') {
|
||||||
app_echo('Instance type is wrong to do an EC2 backup', $machine);
|
app_echo('Instance type is wrong to do an EC2 backup', (array) $machine);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$volumes = $this->getVolumes($machine['instanceId']);
|
$volumes = $this->getVolumes($machine->instanceId);
|
||||||
$tags = $this->getTags($machine['instanceId']);
|
$tags = $this->getTags($machine->instanceId);
|
||||||
|
|
||||||
foreach ($volumes as $volume) {
|
foreach ($volumes as $volume) {
|
||||||
if ($options['noPrune']) {
|
if ($this->options['noPrune']) {
|
||||||
$pruneWording = '(pruning disabled)';
|
$pruneWording = '(pruning disabled)';
|
||||||
} else {
|
} else {
|
||||||
$pruneCount = $this->pruneBackups($volume['volumeId']);
|
$pruneCount = $this->pruneBackups($volume['volumeId']);
|
||||||
$pruneWording = "and pruned {$pruneCount} old snapshots";
|
$pruneWording = "and pruned {$pruneCount} old snapshots";
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = (isset($tags['Name'])) ? $tags['Name'] : $machine['instanceId'];
|
$name = (isset($tags['Name'])) ? $tags['Name'] : $machine->instanceId;
|
||||||
if (count($volumes) > 1)
|
if (count($volumes) > 1) {
|
||||||
$name .= " ({$volume['device']})";
|
$name .= " ({$volume['device']})";
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->backup(['volumeId' => $volume['volumeId'], 'name' => $name])) {
|
if ($this->backup(['volumeId' => $volume['volumeId'], 'name' => $name])) {
|
||||||
app_echo("Successfully started snapshot for {$volume['volumeId']} {$pruneWording}");
|
app_echo("Successfully started snapshot for {$volume['volumeId']} {$pruneWording}");
|
||||||
@ -101,16 +105,16 @@ class Ec2Backup
|
|||||||
try {
|
try {
|
||||||
$result = $this->ec2Client->describeVolumes(
|
$result = $this->ec2Client->describeVolumes(
|
||||||
[
|
[
|
||||||
'DryRun' => false,
|
'DryRun' => false,
|
||||||
'Filters' => [
|
'Filters' => [
|
||||||
[
|
[
|
||||||
'Name' => 'attachment.instance-id',
|
'Name' => 'attachment.instance-id',
|
||||||
'Values' => [$instanceId]
|
'Values' => [$instanceId]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
app_echo('Error getting volumes: ' . $e->getMessage());
|
app_echo('Error getting volumes: ' . $e->getMessage());
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -131,16 +135,16 @@ class Ec2Backup
|
|||||||
try {
|
try {
|
||||||
$result = $this->ec2Client->describeTags(
|
$result = $this->ec2Client->describeTags(
|
||||||
[
|
[
|
||||||
'DryRun' => false,
|
'DryRun' => false,
|
||||||
'Filters' => [
|
'Filters' => [
|
||||||
[
|
[
|
||||||
'Name' => 'resource-id',
|
'Name' => 'resource-id',
|
||||||
'Values' => [$instanceId]
|
'Values' => [$instanceId]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
app_echo('Error getting instance tags: ' . $e->getMessage());
|
app_echo('Error getting instance tags: ' . $e->getMessage());
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -158,24 +162,25 @@ class Ec2Backup
|
|||||||
try {
|
try {
|
||||||
$result = $this->ec2Client->describeSnapshots(
|
$result = $this->ec2Client->describeSnapshots(
|
||||||
[
|
[
|
||||||
'DryRun' => false,
|
'DryRun' => false,
|
||||||
'Filters' => [
|
'Filters' => [
|
||||||
[
|
[
|
||||||
'Name' => 'volume-id',
|
'Name' => 'volume-id',
|
||||||
'Values' => [$volumeId],
|
'Values' => [$volumeId],
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
app_echo('Error getting current snapshots: ' . $e->getMessage());
|
app_echo('Error getting current snapshots: ' . $e->getMessage());
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$snapshots = [];
|
$snapshots = [];
|
||||||
foreach ($result['Snapshots'] as $snapshot) {
|
foreach ($result['Snapshots'] as $snapshot) {
|
||||||
if ($snapshot['State'] != 'completed')
|
if ($snapshot['State'] != 'completed') {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$snapshots[$snapshot['SnapshotId']] = [
|
$snapshots[$snapshot['SnapshotId']] = [
|
||||||
'started' => Dates::makeCarbon($snapshot['StartTime']),
|
'started' => Dates::makeCarbon($snapshot['StartTime']),
|
||||||
@ -184,7 +189,7 @@ class Ec2Backup
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
uasort($snapshots, function($a, $b) {
|
uasort($snapshots, function ($a, $b) {
|
||||||
if ($a['started'] == $b['started']) {
|
if ($a['started'] == $b['started']) {
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
@ -204,7 +209,7 @@ class Ec2Backup
|
|||||||
}
|
}
|
||||||
|
|
||||||
$prune = array_slice($backups, 0, count($backups) - $this->maxBackupCount);
|
$prune = array_slice($backups, 0, count($backups) - $this->maxBackupCount);
|
||||||
if(empty($prune)) {
|
if (empty($prune)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,11 +218,11 @@ class Ec2Backup
|
|||||||
try {
|
try {
|
||||||
$this->ec2Client->deleteSnapshot(
|
$this->ec2Client->deleteSnapshot(
|
||||||
[
|
[
|
||||||
'DryRun' => false,
|
'DryRun' => $this->options['dryRun'],
|
||||||
'SnapshotId' => $snapshotId
|
'SnapshotId' => $snapshotId
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
app_echo('Error pruning snapshot: ' . $e->getMessage());
|
app_echo('Error pruning snapshot: ' . $e->getMessage());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -230,7 +235,7 @@ class Ec2Backup
|
|||||||
|
|
||||||
public function backup(array $params): bool
|
public function backup(array $params): bool
|
||||||
{
|
{
|
||||||
if (!isset($params['volumeId'])) {
|
if (! isset($params['volumeId'])) {
|
||||||
app_echo('Volume ID is not set in backup parameters');
|
app_echo('Volume ID is not set in backup parameters');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -239,7 +244,7 @@ class Ec2Backup
|
|||||||
|
|
||||||
$tags = [['Key' => 'Name', 'Value' => $name]];
|
$tags = [['Key' => 'Name', 'Value' => $name]];
|
||||||
$snapTags = json_decode(config('TAGS', '[]'), true);
|
$snapTags = json_decode(config('TAGS', '[]'), true);
|
||||||
if (is_array($snapTags) && !empty($snapTags)) {
|
if (is_array($snapTags) && ! empty($snapTags)) {
|
||||||
foreach ($snapTags as $key => $value) {
|
foreach ($snapTags as $key => $value) {
|
||||||
$tags[] = ['Key' => $key, 'Value' => $value];
|
$tags[] = ['Key' => $key, 'Value' => $value];
|
||||||
}
|
}
|
||||||
@ -248,18 +253,18 @@ class Ec2Backup
|
|||||||
try {
|
try {
|
||||||
$this->ec2Client->createSnapshot(
|
$this->ec2Client->createSnapshot(
|
||||||
[
|
[
|
||||||
'Description' => sprintf('%s Backup %s', $name, date('Y-m-d')),
|
'Description' => sprintf('%s Backup %s', $name, date('Y-m-d')),
|
||||||
'DryRun' => false,
|
'DryRun' => $this->options['dryRun'],
|
||||||
'TagSpecifications' => [
|
'TagSpecifications' => [
|
||||||
[
|
[
|
||||||
'ResourceType' => 'snapshot',
|
'ResourceType' => 'snapshot',
|
||||||
'Tags' => $tags
|
'Tags' => $tags
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'VolumeId' => $params['volumeId']
|
'VolumeId' => $params['volumeId']
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
app_echo($e->getMessage());
|
app_echo($e->getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,46 +2,38 @@
|
|||||||
/**
|
/**
|
||||||
* MachineDetails.php
|
* MachineDetails.php
|
||||||
*
|
*
|
||||||
* @copyright 2023 Fairbanks Publishing LLC
|
* @copyright 2025 Fairbanks Publishing LLC
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class MachineDetails
|
* Class MachineDetails
|
||||||
*
|
*
|
||||||
* @author David Fairbanks <david@makerdave.com>
|
* @author David Fairbanks <david@makerdave.com>
|
||||||
* @version 2.0
|
* @version 3.0
|
||||||
*/
|
*/
|
||||||
class MachineDetails
|
class MachineDetails
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var MachineDetails
|
|
||||||
*/
|
|
||||||
protected static MachineDetails $instance;
|
protected static MachineDetails $instance;
|
||||||
|
|
||||||
/**
|
protected string|null $type = null;
|
||||||
* @var array
|
protected string|null $machineId = null;
|
||||||
*/
|
protected string|null $instanceId = null;
|
||||||
protected static array $details = [
|
protected string|null $region = null;
|
||||||
'type' => null,
|
protected string|null $privateIp = null;
|
||||||
'id' => null,
|
protected string|null $publicIp = null;
|
||||||
'machineId' => null,
|
|
||||||
'instanceId' => null,
|
|
||||||
'region' => null,
|
|
||||||
'publicIp' => null,
|
|
||||||
'privateIp' => null,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
private Client|null $metadataServiceClient = null;
|
||||||
* PHP config setting for default_socket_timeout to reset to after doing file_get_contents()
|
private string|null $metadataServiceToken = null;
|
||||||
* @var int|null
|
private CarbonInterface|null $metadataServiceTokenDate = null;
|
||||||
*/
|
|
||||||
protected ?int $socketTimeout = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return MachineDetails
|
|
||||||
*/
|
|
||||||
public static function getInstance(): MachineDetails
|
public static function getInstance(): MachineDetails
|
||||||
{
|
{
|
||||||
if (!isset(static::$instance)) {
|
if (!isset(static::$instance)) {
|
||||||
@ -51,162 +43,75 @@ class MachineDetails
|
|||||||
return static::$instance;
|
return static::$instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static function getDetails(): object
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function getDetails(): array
|
|
||||||
{
|
{
|
||||||
if(self::$instance === null)
|
if(! isset(self::$instance))
|
||||||
self::$instance = self::getInstance();
|
self::$instance = self::getInstance();
|
||||||
|
|
||||||
return self::$details;
|
return self::$instance->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function get(): object
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public static function id(): ?string
|
|
||||||
{
|
{
|
||||||
if(self::$instance === null)
|
return (object) [
|
||||||
self::$instance = self::getInstance();
|
'type' => $this->type,
|
||||||
|
'machineId' => $this->machineId,
|
||||||
return self::$details['id'];
|
'instanceId' => $this->instanceId,
|
||||||
}
|
'region' => $this->region,
|
||||||
|
'privateIp' => $this->privateIp,
|
||||||
/**
|
'publicIp' => $this->publicIp,
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public static function machineId(): ?string
|
|
||||||
{
|
|
||||||
if(self::$instance === null)
|
|
||||||
self::$instance = self::getInstance();
|
|
||||||
|
|
||||||
return self::$details['machineId'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function fullMachineId(): string
|
|
||||||
{
|
|
||||||
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)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function publicIp(): string|null
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public static function region(): ?string
|
|
||||||
{
|
{
|
||||||
if(self::$instance === null)
|
if (! empty($this->publicIp)) {
|
||||||
self::$instance = self::getInstance();
|
return $this->publicIp;
|
||||||
|
|
||||||
return self::$details['region'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public static function publicIp(): ?string
|
|
||||||
{
|
|
||||||
if(self::$instance === null)
|
|
||||||
self::$instance = self::getInstance();
|
|
||||||
|
|
||||||
if(self::$details['publicIp'] === null) {
|
|
||||||
self::$details['publicIp'] = match (self::$details['type']) {
|
|
||||||
'ec2' => self::getEc2PublicIp(),
|
|
||||||
default => self::getLocalPublicIp(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::$details['privateIp'];
|
$this->ensureMetadataService();
|
||||||
}
|
if (empty($this->metadataServiceToken)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
try {
|
||||||
* @return string|null
|
$response = $this->metadataServiceClient->request(
|
||||||
*/
|
'GET',
|
||||||
public static function privateIp(): ?string
|
'meta-data/public-ipv4',
|
||||||
{
|
[
|
||||||
if(self::$instance === null)
|
'headers' => [
|
||||||
self::$instance = self::getInstance();
|
'X-aws-ec2-metadata-token' => $this->metadataServiceToken,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return self::$details['privateIp'];
|
$this->publicIp = $response->getBody()->getContents();
|
||||||
}
|
} catch (GuzzleException|Exception $e) {
|
||||||
|
app_echo(get_class($e) . ' getting IMDS V2 instance details: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
/* Private methods */
|
return $this->publicIp;
|
||||||
|
|
||||||
private static function getLocalPublicIp(): string
|
|
||||||
{
|
|
||||||
return '127.0.0.1';
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function getEc2PublicIp(): false|string
|
|
||||||
{
|
|
||||||
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() {
|
private function __construct() {
|
||||||
$type = config('MACHINE_TYPE');
|
if ($this->isEc2()) {
|
||||||
if($type === null || empty($type)) {
|
$this->type = 'ec2';
|
||||||
$type = $this->determineMachineType();
|
$this->instanceIdentity();
|
||||||
}
|
} else {
|
||||||
|
$this->type = 'unknown';
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function isEc2(): bool
|
||||||
* Make clone magic method private, so nobody can clone instance.
|
{
|
||||||
*/
|
$uname = php_uname();
|
||||||
|
|
||||||
|
return preg_match('/ip-\d{2,3}-\d{1,3}-\d{1,3}-\d{1,3}\s.*-aws/', $uname);
|
||||||
|
}
|
||||||
|
|
||||||
private function __clone() {}
|
private function __clone() {}
|
||||||
|
|
||||||
private function determineMachineType(): string
|
private function instanceIdentity(): void
|
||||||
{
|
|
||||||
$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(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => 'dev',
|
|
||||||
'machineId' => 'dev',
|
|
||||||
'region' => 'local',
|
|
||||||
'publicIp' => '127.0.0.1',
|
|
||||||
'privateIp' => '127.0.0.1',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getEc2Data(): array
|
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* curl "http://169.254.169.254/latest/dynamic/instance-identity/document"
|
* curl "http://169.254.169.254/latest/dynamic/instance-identity/document"
|
||||||
@ -228,40 +133,74 @@ class MachineDetails
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$this->shortenTimeout();
|
$this->ensureMetadataService();
|
||||||
$json = @file_get_contents('http://169.254.169.254/latest/dynamic/instance-identity/document');
|
if (empty($this->metadataServiceToken)) {
|
||||||
$this->resetTimeout();
|
return;
|
||||||
|
|
||||||
$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']))
|
try {
|
||||||
$data['machineId'] = substr($data['instanceId'], 2);
|
$response = $this->metadataServiceClient->request(
|
||||||
|
'GET',
|
||||||
|
'dynamic/instance-identity/document',
|
||||||
|
[
|
||||||
|
'headers' => [
|
||||||
|
'X-aws-ec2-metadata-token' => $this->metadataServiceToken,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
$out = [];
|
$json = $response->getBody()->getContents();
|
||||||
foreach(self::$details as $key => $v) {
|
} catch (GuzzleException|Exception $e) {
|
||||||
$out[$key] = (isset($data[$key])) ? $data[$key] : $v;
|
app_echo(get_class($e) . ' getting IMDS V2 instance details: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $out;
|
$data = json_decode($json);
|
||||||
|
|
||||||
|
$this->instanceId = $data->instanceId;
|
||||||
|
$this->region = $data->region;
|
||||||
|
$this->privateIp = $data->privateIp;
|
||||||
|
|
||||||
|
if (! empty($data->instanceId)) {
|
||||||
|
$this->machineId = substr($data->instanceId, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function shortenTimeout(): void
|
private function ensureMetadataService(): void
|
||||||
{
|
{
|
||||||
if($this->socketTimeout === null) {
|
if ($this->metadataServiceClient === null) {
|
||||||
$this->socketTimeout = ini_get('default_socket_timeout');
|
$this->metadataServiceClient = new Client(['base_uri' => 'http://169.254.169.254/latest/']);
|
||||||
}
|
}
|
||||||
|
|
||||||
ini_set('default_socket_timeout', 2);
|
if ($this->metadataServiceToken !== null && $this->metadataServiceTokenDate !== null
|
||||||
}
|
&& $this->metadataServiceTokenDate->isAfter(Carbon::now()->addSeconds(-60))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
private function resetTimeout(): void
|
$this->metadataServiceToken = null;
|
||||||
{
|
$this->metadataServiceTokenDate = Carbon::now();
|
||||||
if($this->socketTimeout !== null) {
|
|
||||||
ini_set('default_socket_timeout', $this->socketTimeout);
|
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-dynamic-data-retrieval.html
|
||||||
|
// https://stackoverflow.com/a/74334921/667613
|
||||||
|
// TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
|
||||||
|
// curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/dynamic/instance-identity/document
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->metadataServiceClient->request(
|
||||||
|
'PUT',
|
||||||
|
'api/token',
|
||||||
|
[
|
||||||
|
'headers' => [
|
||||||
|
'X-aws-ec2-metadata-token-ttl-seconds' => 60,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->metadataServiceToken = $response->getBody()->getContents();
|
||||||
|
} catch (GuzzleException|Exception $e) {
|
||||||
|
app_echo(get_class($e) . ' getting IMDS V2 token: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,64 +5,6 @@
|
|||||||
* @copyright (c) 2018, Fairbanks Publishing
|
* @copyright (c) 2018, Fairbanks Publishing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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): array
|
|
||||||
{
|
|
||||||
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 = []): array
|
|
||||||
{
|
|
||||||
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')) {
|
if(!function_exists('boolean')) {
|
||||||
/**
|
/**
|
||||||
* Convert value to boolean
|
* Convert value to boolean
|
||||||
|
|||||||
11
backup.php
11
backup.php
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
$arguments = getopt('fph', ['force', 'no-prune', 'help']);
|
$arguments = getopt('fph', ['force', 'no-prune', 'dry-run', 'help']);
|
||||||
if(isset($arguments['h']) || isset($arguments['help'])) {
|
if(isset($arguments['h']) || isset($arguments['help'])) {
|
||||||
echo <<<hereDoc
|
echo <<<hereDoc
|
||||||
Create and rotate EBS snapshots on AWS for EC2 instances.
|
Create and rotate EBS snapshots on AWS for EC2 instances.
|
||||||
@ -11,6 +11,7 @@ Usage:
|
|||||||
Options:
|
Options:
|
||||||
-f, --force Override the ENABLE environment variable
|
-f, --force Override the ENABLE environment variable
|
||||||
-p, --no-prune Prevent the removal of old snapshots
|
-p, --no-prune Prevent the removal of old snapshots
|
||||||
|
--dry-run Do not actually create the snapshot
|
||||||
|
|
||||||
hereDoc;
|
hereDoc;
|
||||||
exit;
|
exit;
|
||||||
@ -18,7 +19,8 @@ hereDoc;
|
|||||||
|
|
||||||
$options = [
|
$options = [
|
||||||
'force' => isset($arguments['f']) || isset($arguments['force']),
|
'force' => isset($arguments['f']) || isset($arguments['force']),
|
||||||
'noPrune' => isset($arguments['p']) || isset($arguments['no-prune'])
|
'noPrune' => isset($arguments['p']) || isset($arguments['no-prune']),
|
||||||
|
'dryRun' => isset($arguments['dry-run']),
|
||||||
];
|
];
|
||||||
|
|
||||||
require_once(__DIR__ . '/vendor/autoload.php');
|
require_once(__DIR__ . '/vendor/autoload.php');
|
||||||
@ -31,7 +33,7 @@ $dotenv->required('AWS_REGION')->notEmpty();
|
|||||||
$dotenv->ifPresent('MAX_SNAPSHOT_COUNT')->isInteger();
|
$dotenv->ifPresent('MAX_SNAPSHOT_COUNT')->isInteger();
|
||||||
$dotenv->ifPresent('ENABLE')->isBoolean();
|
$dotenv->ifPresent('ENABLE')->isBoolean();
|
||||||
|
|
||||||
$ec2Client = new \Aws\Ec2\Ec2Client([
|
$ec2Client = new Aws\Ec2\Ec2Client([
|
||||||
'credentials' => [
|
'credentials' => [
|
||||||
'key' => config('AWS_KEY'),
|
'key' => config('AWS_KEY'),
|
||||||
'secret' => config('AWS_SECRET')
|
'secret' => config('AWS_SECRET')
|
||||||
@ -39,6 +41,5 @@ $ec2Client = new \Aws\Ec2\Ec2Client([
|
|||||||
'region' => config('AWS_REGION'),
|
'region' => config('AWS_REGION'),
|
||||||
'version' => config('AWS_VERSION', 'latest')
|
'version' => config('AWS_VERSION', 'latest')
|
||||||
]);
|
]);
|
||||||
$ec2Backup = new \App\Ec2Backup($ec2Client);
|
$ec2Backup = new App\Ec2Backup($ec2Client, $options);
|
||||||
|
|
||||||
$ec2Backup->create();
|
$ec2Backup->create();
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"aws/aws-sdk-php": "^3.209",
|
"aws/aws-sdk-php": "^3.308.0",
|
||||||
"nesbot/carbon": "^2.68",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
|
"nesbot/carbon": "3.8.4.0",
|
||||||
"vlucas/phpdotenv": "^5.5"
|
"vlucas/phpdotenv": "^5.5"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
756
composer.lock
generated
756
composer.lock
generated
File diff suppressed because it is too large
Load Diff
12
metadata.php
Normal file
12
metadata.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
$machine = App\MachineDetails::getDetails();
|
||||||
|
echo json_encode($machine, JSON_PRETTY_PRINT) . PHP_EOL;
|
||||||
Loading…
x
Reference in New Issue
Block a user