Hetzner Service, DTOs, provision script
This commit is contained in:
13
app/Data/ServerProviders/Image.php
Normal file
13
app/Data/ServerProviders/Image.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Data\ServerProviders;
|
||||
|
||||
class Image
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $name,
|
||||
public string $osFlavor,
|
||||
public string $osVersion,
|
||||
) {}
|
||||
}
|
||||
13
app/Data/ServerProviders/Location.php
Normal file
13
app/Data/ServerProviders/Location.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Data\ServerProviders;
|
||||
|
||||
class Location
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $name,
|
||||
public string $country,
|
||||
public string $city,
|
||||
) {}
|
||||
}
|
||||
22
app/Data/ServerProviders/ServerType.php
Normal file
22
app/Data/ServerProviders/ServerType.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Data\ServerProviders;
|
||||
|
||||
class ServerType
|
||||
{
|
||||
/**
|
||||
* @param string $name The name of the server type
|
||||
* @param int $cores The number of cores
|
||||
* @param int $memory The amount of memory in MB
|
||||
* @param int $disk The amount of disk space in GB
|
||||
*/
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $name,
|
||||
public int $cores,
|
||||
public int $memory,
|
||||
public int $disk,
|
||||
public float $priceMonthly,
|
||||
public float $priceHourly,
|
||||
) {}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Service extends Model
|
||||
{
|
||||
@@ -13,4 +14,9 @@ class Service extends Model
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
public function slices(): HasMany
|
||||
{
|
||||
return $this->hasMany(Slice::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Slice extends Model
|
||||
{
|
||||
//
|
||||
public function service(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Service::class);
|
||||
}
|
||||
}
|
||||
|
||||
89
app/Services/ServerProviders/HetznerService.php
Normal file
89
app/Services/ServerProviders/HetznerService.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ServerProviders;
|
||||
|
||||
use App\Data\ServerProviders\Image;
|
||||
use App\Data\ServerProviders\Location;
|
||||
use App\Data\ServerProviders\ServerType;
|
||||
use App\Http\Integrations\Connectors\HetznerConnector;
|
||||
use App\Http\Integrations\Requests\Hetzner\Images\ListImagesRequest;
|
||||
use App\Http\Integrations\Requests\Hetzner\Locations\ListLocationsRequest;
|
||||
use App\Http\Integrations\Requests\Hetzner\ServerTypes\ListServerTypesRequest;
|
||||
use Exception;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class HetznerService implements ServerProviderService
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->connector = new HetznerConnector;
|
||||
}
|
||||
|
||||
public function createServer(
|
||||
string $name,
|
||||
string $serverType,
|
||||
string $location,
|
||||
string $image,
|
||||
): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function listServerTypes(): Collection
|
||||
{
|
||||
$response = $this->connector->send(new ListServerTypesRequest);
|
||||
|
||||
if ($response->status() !== 200) {
|
||||
throw new Exception('Failed to fetch server types from Hetzner');
|
||||
}
|
||||
|
||||
return collect($response->json('server_types'))->where('deprecated', false)->where('architecture', 'x86')->map(function ($serverType) {
|
||||
return new ServerType(
|
||||
id: $serverType['id'],
|
||||
name: $serverType['name'],
|
||||
cores: $serverType['cores'],
|
||||
memory: $serverType['memory'] * 1024,
|
||||
disk: $serverType['disk'],
|
||||
priceMonthly: $serverType['prices'][0]['monthly']['gross'] ?? 0,
|
||||
priceHourly: $serverType['prices'][0]['hourly']['gross'] ?? 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function listLocations(): Collection
|
||||
{
|
||||
$response = $this->connector->send(new ListLocationsRequest);
|
||||
|
||||
if ($response->status() !== 200) {
|
||||
throw new Exception('Failed to fetch locations from Hetzner');
|
||||
}
|
||||
|
||||
return collect($response->json('locations'))->map(function ($location) {
|
||||
return new Location(
|
||||
id: $location['id'],
|
||||
name: $location['name'],
|
||||
country: $location['country'],
|
||||
city: $location['city'],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function listImages(): Collection
|
||||
{
|
||||
$response = $this->connector->send(new ListImagesRequest(
|
||||
architecture: 'x86',
|
||||
));
|
||||
|
||||
if ($response->status() !== 200) {
|
||||
throw new Exception('Failed to fetch images from Hetzner');
|
||||
}
|
||||
|
||||
return collect($response->json('images'))->where('os_flavor', 'ubuntu')->map(function ($image) {
|
||||
return new Image(
|
||||
id: $image['id'],
|
||||
name: $image['description'],
|
||||
osFlavor: $image['os_flavor'],
|
||||
osVersion: $image['os_version'],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
24
app/Services/ServerProviders/ServerProviderService.php
Normal file
24
app/Services/ServerProviders/ServerProviderService.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ServerProviders;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Saloon\Http\Connector;
|
||||
|
||||
interface ServerProviderService
|
||||
{
|
||||
protected Connector $connector;
|
||||
|
||||
public function createServer(
|
||||
string $name,
|
||||
string $serverType,
|
||||
string $location,
|
||||
string $image,
|
||||
): bool;
|
||||
|
||||
public function listServerTypes(): Collection;
|
||||
|
||||
public function listLocations(): Collection;
|
||||
|
||||
public function listImages(): Collection;
|
||||
}
|
||||
121
provision.sh
Normal file
121
provision.sh
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
|
||||
apt_wait() {
|
||||
while fuser /var/lib/dpkg/lock >/dev/null 2>&1; do
|
||||
echo "Waiting: dpkg/lock is locked..."
|
||||
sleep 5
|
||||
done
|
||||
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do
|
||||
echo "Waiting: dpkg/lock-frontend is locked..."
|
||||
sleep 5
|
||||
done
|
||||
while fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do
|
||||
echo "Waiting: lists/lock is locked..."
|
||||
sleep 5
|
||||
done
|
||||
if [ -f /var/log/unattended-upgrades/unattended-upgrades.log ]; then
|
||||
while fuser /var/log/unattended-upgrades/unattended-upgrades.log >/dev/null 2>&1; do
|
||||
echo "Waiting: unattended-upgrades is locked..."
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
apt_wait
|
||||
|
||||
# Make sure we're up to date
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt update
|
||||
apt_wait
|
||||
apt upgrade -y
|
||||
apt_wait
|
||||
apt install unzip curl fail2ban ufw -y
|
||||
|
||||
# No password logins
|
||||
sed -i "/PasswordAuthentication yes/d" /etc/ssh/sshd_config
|
||||
echo "" | sudo tee -a /etc/ssh/sshd_config
|
||||
echo "" | sudo tee -a /etc/ssh/sshd_config
|
||||
echo "PasswordAuthentication no" | sudo tee -a /etc/ssh/sshd_config
|
||||
|
||||
# Restart SSH
|
||||
ssh-keygen -A service ssh restart
|
||||
|
||||
# UTC
|
||||
ln -sf /usr/share/zoneinfo/UTC /etc/localtime
|
||||
|
||||
# Create The Root SSH Directory If Necessary
|
||||
if [ ! -d /root/.ssh ]; then
|
||||
mkdir -p /root/.ssh
|
||||
touch /root/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
# Setup Keystone User
|
||||
useradd keystone
|
||||
mkdir -p /home/keystone/.ssh
|
||||
mkdir -p /home/keystone/.keystone
|
||||
adduser keystone sudo
|
||||
|
||||
# Setup Bash For Keystone User
|
||||
chsh -s /bin/bash keystone
|
||||
cp /root/.profile /home/keystone/.profile
|
||||
cp /root/.bashrc /home/keystone/.bashrc
|
||||
|
||||
# Set The Sudo Password For Keystone
|
||||
PASSWORD=$(mkpasswd [!sudo_password!])
|
||||
usermod --password $PASSWORD keystone
|
||||
|
||||
# Build Formatted Keys & Copy Keys To Keystone
|
||||
cat >/root/.ssh/authorized_keys <<EOF
|
||||
# Keystone
|
||||
@TODO INJECT KEY HERE
|
||||
EOF
|
||||
|
||||
cp /root/.ssh/authorized_keys /home/keystone/.ssh/authorized_keys
|
||||
|
||||
# Create The Server SSH Key
|
||||
ssh-keygen -f /home/keystone/.ssh/id_ed25519 -t ed25519 -N ''
|
||||
|
||||
# Setup Keystone Home Directory Permissions
|
||||
chown -R keystone:keystone /home/keystone
|
||||
chmod -R 755 /home/keystone
|
||||
chmod 700 /home/keystone/.ssh/id_rsa
|
||||
|
||||
# Setup UFW Firewall
|
||||
ufw allow 22
|
||||
# ufw allow 80 # only if web
|
||||
# ufw allow 443
|
||||
ufw --force enable
|
||||
|
||||
# Add Keystone User To www-data Group
|
||||
usermod -a -G www-data keystone
|
||||
id keystone
|
||||
groups keystone
|
||||
|
||||
# Install bun and pm2
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
source $HOME/.bashrc
|
||||
bun add -g pm2
|
||||
ln -s $(which bun) /usr/bin/node # (official) workaround for not having node
|
||||
$(pm2 startup | sed 's/^sudo //') | su - keystone -c "bash"
|
||||
|
||||
# Setup Unattended Security Upgrades
|
||||
apt-get install -y --force-yes unattended-upgrades
|
||||
cat >/etc/apt/apt.conf.d/50unattended-upgrades <<EOF
|
||||
Unattended-Upgrade::Allowed-Origins {
|
||||
"Ubuntu focal-security";
|
||||
};
|
||||
Unattended-Upgrade::Package-Blacklist {
|
||||
//
|
||||
};
|
||||
EOF
|
||||
|
||||
cat >/etc/apt/apt.conf.d/10periodic <<EOF
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||
APT::Periodic::AutocleanInterval "7";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
EOF
|
||||
|
||||
|
||||
# Callback that the server is installed
|
||||
curl --insecure --data "event_id=878&server_id=390&sudo_password=[!sudo_password!]&db_password=[!db_password!]&recipe_id=" https://keystone.test/provisioning/callback/app
|
||||
@@ -18,12 +18,11 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<div class="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
|
||||
<div>The plan:</div>
|
||||
<div>Organisations have projects</div>
|
||||
<div>Projects have environments</div>
|
||||
<div>Environments have servers</div>
|
||||
<div>Organisations have applications</div>
|
||||
<div>Applications have environments</div>
|
||||
<div>Servers have services</div>
|
||||
<div>Servers can have services</div>
|
||||
<div>Services have slices, this could be a database table or a client in a websocket server</div>
|
||||
<div>Environments have SLICES</div>
|
||||
<div>Also servers need to be provisioned</div>
|
||||
<div>Users have SSH keys</div>
|
||||
<div>Keystone has its own SSH keys</div>
|
||||
|
||||
Reference in New Issue
Block a user