1220 lines
52 KiB
PHP
Executable File
1220 lines
52 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* Cloud Manager - Huawei Cloud + Scaleway
|
|
* Gestion des serveurs, DNS, et ressources cloud
|
|
*/
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 0);
|
|
|
|
function getDB() {
|
|
static $pdo = null;
|
|
if ($pdo === null) {
|
|
$pdo = new PDO("pgsql:host=localhost;dbname=adx_system", "admin", "admin123",
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
}
|
|
return $pdo;
|
|
}
|
|
|
|
// Huawei Cloud Regions
|
|
$HUAWEI_REGIONS = [
|
|
'af-south-1' => 'Africa - Johannesburg',
|
|
'ap-southeast-1' => 'Asia Pacific - Hong Kong',
|
|
'ap-southeast-2' => 'Asia Pacific - Bangkok',
|
|
'ap-southeast-3' => 'Asia Pacific - Singapore',
|
|
'cn-east-2' => 'China - Shanghai',
|
|
'cn-east-3' => 'China - Shanghai 2',
|
|
'cn-north-1' => 'China - Beijing',
|
|
'cn-north-4' => 'China - Beijing 4',
|
|
'cn-south-1' => 'China - Guangzhou',
|
|
'cn-southwest-2' => 'China - Guiyang',
|
|
'eu-west-0' => 'Europe - Paris',
|
|
'eu-west-101' => 'Europe - Dublin',
|
|
'la-north-2' => 'Latin America - Mexico City',
|
|
'la-south-2' => 'Latin America - Santiago',
|
|
'me-east-1' => 'Middle East - Riyadh',
|
|
'na-mexico-1' => 'North America - Mexico',
|
|
'ru-northwest-2' => 'Russia - Moscow',
|
|
'sa-brazil-1' => 'South America - Sao Paulo',
|
|
'tr-west-1' => 'Turkey - Istanbul'
|
|
];
|
|
|
|
// Scaleway Regions & Zones
|
|
$SCALEWAY_REGIONS = [
|
|
'fr-par' => ['name' => 'France - Paris', 'zones' => ['fr-par-1', 'fr-par-2', 'fr-par-3']],
|
|
'nl-ams' => ['name' => 'Netherlands - Amsterdam', 'zones' => ['nl-ams-1', 'nl-ams-2', 'nl-ams-3']],
|
|
'pl-waw' => ['name' => 'Poland - Warsaw', 'zones' => ['pl-waw-1', 'pl-waw-2', 'pl-waw-3']]
|
|
];
|
|
|
|
// Scaleway Instance Types
|
|
$SCALEWAY_INSTANCES = [
|
|
'DEV1-S' => ['vcpus' => 2, 'ram' => 2, 'price' => 0.01],
|
|
'DEV1-M' => ['vcpus' => 3, 'ram' => 4, 'price' => 0.02],
|
|
'DEV1-L' => ['vcpus' => 4, 'ram' => 8, 'price' => 0.04],
|
|
'DEV1-XL' => ['vcpus' => 4, 'ram' => 12, 'price' => 0.06],
|
|
'GP1-XS' => ['vcpus' => 4, 'ram' => 16, 'price' => 0.10],
|
|
'GP1-S' => ['vcpus' => 8, 'ram' => 32, 'price' => 0.20],
|
|
'GP1-M' => ['vcpus' => 16, 'ram' => 64, 'price' => 0.40],
|
|
'GP1-L' => ['vcpus' => 32, 'ram' => 128, 'price' => 0.80],
|
|
'PLAY2-PICO' => ['vcpus' => 1, 'ram' => 1, 'price' => 0.004],
|
|
'PLAY2-NANO' => ['vcpus' => 1, 'ram' => 2, 'price' => 0.008],
|
|
'PLAY2-MICRO' => ['vcpus' => 2, 'ram' => 4, 'price' => 0.016],
|
|
'STARDUST1-S' => ['vcpus' => 1, 'ram' => 1, 'price' => 0.0025]
|
|
];
|
|
|
|
function ensureTables() {
|
|
$pdo = getDB();
|
|
|
|
// Huawei Accounts
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.huawei_accounts (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100),
|
|
access_key VARCHAR(255) NOT NULL,
|
|
secret_key VARCHAR(255) NOT NULL,
|
|
project_id VARCHAR(100),
|
|
region VARCHAR(50) DEFAULT 'eu-west-0',
|
|
status VARCHAR(50) DEFAULT 'active',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
|
|
// Huawei Servers
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.huawei_servers (
|
|
id SERIAL PRIMARY KEY,
|
|
account_id INTEGER,
|
|
server_id VARCHAR(100),
|
|
name VARCHAR(255),
|
|
region VARCHAR(50),
|
|
ip_address VARCHAR(50),
|
|
private_ip VARCHAR(50),
|
|
flavor VARCHAR(50),
|
|
status VARCHAR(50),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
|
|
// Scaleway Accounts
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.scaleway_accounts (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100),
|
|
access_key VARCHAR(255) NOT NULL,
|
|
secret_key VARCHAR(255) NOT NULL,
|
|
organization_id VARCHAR(100),
|
|
project_id VARCHAR(100),
|
|
default_zone VARCHAR(50) DEFAULT 'fr-par-1',
|
|
status VARCHAR(50) DEFAULT 'active',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
|
|
// Scaleway Servers
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.scaleway_servers (
|
|
id SERIAL PRIMARY KEY,
|
|
account_id INTEGER,
|
|
server_id VARCHAR(100),
|
|
name VARCHAR(255),
|
|
zone VARCHAR(50),
|
|
public_ip VARCHAR(50),
|
|
private_ip VARCHAR(50),
|
|
instance_type VARCHAR(50),
|
|
status VARCHAR(50),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
|
|
// Cloud IPs Pool
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS admin.cloud_ips (
|
|
id SERIAL PRIMARY KEY,
|
|
provider VARCHAR(50),
|
|
account_id INTEGER,
|
|
ip_address VARCHAR(50) NOT NULL,
|
|
region VARCHAR(50),
|
|
server_id VARCHAR(100),
|
|
ptr_record VARCHAR(255),
|
|
status VARCHAR(50) DEFAULT 'available',
|
|
assigned_to VARCHAR(255),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
}
|
|
|
|
// Huawei Cloud API
|
|
class HuaweiCloudAPI {
|
|
private $accessKey;
|
|
private $secretKey;
|
|
private $projectId;
|
|
private $region;
|
|
|
|
public function __construct($accountId = null) {
|
|
if ($accountId) {
|
|
$pdo = getDB();
|
|
$stmt = $pdo->prepare("SELECT * FROM admin.huawei_accounts WHERE id = ?");
|
|
$stmt->execute([$accountId]);
|
|
$acc = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
$this->accessKey = $acc['access_key'];
|
|
$this->secretKey = $acc['secret_key'];
|
|
$this->projectId = $acc['project_id'];
|
|
$this->region = $acc['region'];
|
|
}
|
|
}
|
|
|
|
public function setCredentials($accessKey, $secretKey, $projectId, $region) {
|
|
$this->accessKey = $accessKey;
|
|
$this->secretKey = $secretKey;
|
|
$this->projectId = $projectId;
|
|
$this->region = $region;
|
|
}
|
|
|
|
private function sign($method, $url, $headers, $body = '') {
|
|
$parsedUrl = parse_url($url);
|
|
$host = $parsedUrl['host'];
|
|
$path = $parsedUrl['path'] ?? '/';
|
|
$query = $parsedUrl['query'] ?? '';
|
|
|
|
$datetime = gmdate('Ymd\THis\Z');
|
|
$date = gmdate('Ymd');
|
|
|
|
$headers['Host'] = $host;
|
|
$headers['X-Sdk-Date'] = $datetime;
|
|
|
|
ksort($headers);
|
|
$signedHeaders = implode(';', array_map('strtolower', array_keys($headers)));
|
|
$canonicalHeaders = '';
|
|
foreach ($headers as $k => $v) {
|
|
$canonicalHeaders .= strtolower($k) . ':' . trim($v) . "\n";
|
|
}
|
|
|
|
$hashedPayload = hash('sha256', $body);
|
|
$canonicalRequest = "$method\n$path\n$query\n$canonicalHeaders\n$signedHeaders\n$hashedPayload";
|
|
$hashedCanonical = hash('sha256', $canonicalRequest);
|
|
|
|
$credentialScope = "$date/{$this->region}/ecs/sdk_request";
|
|
$stringToSign = "SDK-HMAC-SHA256\n$datetime\n$credentialScope\n$hashedCanonical";
|
|
|
|
$kDate = hash_hmac('sha256', $date, "SDK" . $this->secretKey, true);
|
|
$kRegion = hash_hmac('sha256', $this->region, $kDate, true);
|
|
$kService = hash_hmac('sha256', 'ecs', $kRegion, true);
|
|
$kSigning = hash_hmac('sha256', 'sdk_request', $kService, true);
|
|
$signature = hash_hmac('sha256', $stringToSign, $kSigning);
|
|
|
|
$authorization = "SDK-HMAC-SHA256 Credential={$this->accessKey}/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature";
|
|
$headers['Authorization'] = $authorization;
|
|
|
|
return $headers;
|
|
}
|
|
|
|
private function request($endpoint, $method = 'GET', $data = null) {
|
|
$baseUrl = "https://ecs.{$this->region}.myhuaweicloud.com";
|
|
$url = $baseUrl . $endpoint;
|
|
|
|
$headers = ['Content-Type' => 'application/json'];
|
|
$body = $data ? json_encode($data) : '';
|
|
|
|
$signedHeaders = $this->sign($method, $url, $headers, $body);
|
|
|
|
$ch = curl_init($url);
|
|
$curlHeaders = [];
|
|
foreach ($signedHeaders as $k => $v) {
|
|
$curlHeaders[] = "$k: $v";
|
|
}
|
|
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTPHEADER => $curlHeaders,
|
|
CURLOPT_CUSTOMREQUEST => $method
|
|
]);
|
|
|
|
if ($body && in_array($method, ['POST', 'PUT'])) {
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
|
}
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
return ['code' => $httpCode, 'data' => json_decode($response, true)];
|
|
}
|
|
|
|
public function listServers() {
|
|
return $this->request("/v1/{$this->projectId}/cloudservers/detail");
|
|
}
|
|
|
|
public function getServer($serverId) {
|
|
return $this->request("/v1/{$this->projectId}/cloudservers/$serverId");
|
|
}
|
|
|
|
public function createServer($name, $flavorId, $imageId, $vpcId, $subnetId, $securityGroupId) {
|
|
return $this->request("/v1/{$this->projectId}/cloudservers", 'POST', [
|
|
'server' => [
|
|
'name' => $name,
|
|
'flavorRef' => $flavorId,
|
|
'imageRef' => $imageId,
|
|
'vpcid' => $vpcId,
|
|
'nics' => [['subnet_id' => $subnetId]],
|
|
'security_groups' => [['id' => $securityGroupId]],
|
|
'publicip' => ['eip' => ['iptype' => '5_bgp', 'bandwidth' => ['size' => 10, 'sharetype' => 'PER']]]
|
|
]
|
|
]);
|
|
}
|
|
|
|
public function deleteServer($serverId) {
|
|
return $this->request("/v1/{$this->projectId}/cloudservers/delete", 'POST', [
|
|
'servers' => [['id' => $serverId]],
|
|
'delete_publicip' => true,
|
|
'delete_volume' => true
|
|
]);
|
|
}
|
|
|
|
public function listFlavors() {
|
|
return $this->request("/v1/{$this->projectId}/cloudservers/flavors");
|
|
}
|
|
|
|
public function listImages() {
|
|
$baseUrl = "https://ims.{$this->region}.myhuaweicloud.com";
|
|
// Simplified - returns common images
|
|
return ['data' => ['images' => [
|
|
['id' => 'ubuntu-20.04', 'name' => 'Ubuntu 20.04'],
|
|
['id' => 'ubuntu-22.04', 'name' => 'Ubuntu 22.04'],
|
|
['id' => 'centos-7', 'name' => 'CentOS 7'],
|
|
['id' => 'debian-11', 'name' => 'Debian 11']
|
|
]]];
|
|
}
|
|
|
|
public function serverAction($serverId, $action) {
|
|
$actions = [
|
|
'start' => ['os-start' => null],
|
|
'stop' => ['os-stop' => null],
|
|
'reboot' => ['reboot' => ['type' => 'SOFT']]
|
|
];
|
|
return $this->request("/v1/{$this->projectId}/cloudservers/$serverId/action", 'POST', $actions[$action] ?? []);
|
|
}
|
|
}
|
|
|
|
// Scaleway API
|
|
class ScalewayAPI {
|
|
private $accessKey;
|
|
private $secretKey;
|
|
private $organizationId;
|
|
private $projectId;
|
|
private $zone;
|
|
private $baseUrl = 'https://api.scaleway.com';
|
|
|
|
public function __construct($accountId = null) {
|
|
if ($accountId) {
|
|
$pdo = getDB();
|
|
$stmt = $pdo->prepare("SELECT * FROM admin.scaleway_accounts WHERE id = ?");
|
|
$stmt->execute([$accountId]);
|
|
$acc = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
$this->accessKey = $acc['access_key'];
|
|
$this->secretKey = $acc['secret_key'];
|
|
$this->organizationId = $acc['organization_id'];
|
|
$this->projectId = $acc['project_id'];
|
|
$this->zone = $acc['default_zone'];
|
|
}
|
|
}
|
|
|
|
public function setCredentials($accessKey, $secretKey, $orgId, $projectId, $zone = 'fr-par-1') {
|
|
$this->accessKey = $accessKey;
|
|
$this->secretKey = $secretKey;
|
|
$this->organizationId = $orgId;
|
|
$this->projectId = $projectId;
|
|
$this->zone = $zone;
|
|
}
|
|
|
|
public function setZone($zone) {
|
|
$this->zone = $zone;
|
|
}
|
|
|
|
private function request($endpoint, $method = 'GET', $data = null) {
|
|
$url = $this->baseUrl . $endpoint;
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTPHEADER => [
|
|
'X-Auth-Token: ' . $this->secretKey,
|
|
'Content-Type: application/json'
|
|
],
|
|
CURLOPT_CUSTOMREQUEST => $method
|
|
]);
|
|
|
|
if ($data && in_array($method, ['POST', 'PUT', 'PATCH'])) {
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
|
}
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
return ['code' => $httpCode, 'data' => json_decode($response, true)];
|
|
}
|
|
|
|
// Instances
|
|
public function listServers($zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/servers");
|
|
}
|
|
|
|
public function getServer($serverId, $zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/servers/$serverId");
|
|
}
|
|
|
|
public function createServer($name, $instanceType, $imageId, $zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/servers", 'POST', [
|
|
'name' => $name,
|
|
'commercial_type' => $instanceType,
|
|
'image' => $imageId,
|
|
'project' => $this->projectId,
|
|
'dynamic_ip_required' => true
|
|
]);
|
|
}
|
|
|
|
public function deleteServer($serverId, $zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
// First power off
|
|
$this->serverAction($serverId, 'poweroff', $z);
|
|
sleep(5);
|
|
return $this->request("/instance/v1/zones/$z/servers/$serverId", 'DELETE');
|
|
}
|
|
|
|
public function serverAction($serverId, $action, $zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/servers/$serverId/action", 'POST', [
|
|
'action' => $action
|
|
]);
|
|
}
|
|
|
|
// Images
|
|
public function listImages($zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/images?per_page=50");
|
|
}
|
|
|
|
// IPs
|
|
public function listIPs($zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/ips");
|
|
}
|
|
|
|
public function createIP($zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/ips", 'POST', [
|
|
'project' => $this->projectId
|
|
]);
|
|
}
|
|
|
|
public function deleteIP($ipId, $zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/ips/$ipId", 'DELETE');
|
|
}
|
|
|
|
public function updateReverseDNS($ipId, $reverse, $zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/ips/$ipId", 'PATCH', [
|
|
'reverse' => $reverse
|
|
]);
|
|
}
|
|
|
|
// Volumes
|
|
public function listVolumes($zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/volumes");
|
|
}
|
|
|
|
// Security Groups
|
|
public function listSecurityGroups($zone = null) {
|
|
$z = $zone ?? $this->zone;
|
|
return $this->request("/instance/v1/zones/$z/security_groups");
|
|
}
|
|
|
|
// Bulk Operations
|
|
public function bulkCreateServers($prefix, $count, $instanceType, $imageId, $zone = null) {
|
|
$results = [];
|
|
for ($i = 1; $i <= $count; $i++) {
|
|
$name = $prefix . '-' . str_pad($i, 3, '0', STR_PAD_LEFT);
|
|
$results[] = $this->createServer($name, $instanceType, $imageId, $zone);
|
|
usleep(500000); // 0.5s delay
|
|
}
|
|
return $results;
|
|
}
|
|
}
|
|
|
|
// API Handler
|
|
if (isset($_GET['action']) || isset($_POST['action'])) {
|
|
header('Content-Type: application/json');
|
|
ensureTables();
|
|
$action = $_GET['action'] ?? $_POST['action'];
|
|
$pdo = getDB();
|
|
|
|
try {
|
|
switch ($action) {
|
|
case 'stats':
|
|
echo json_encode(['success' => true, 'stats' => [
|
|
'huawei_accounts' => $pdo->query("SELECT COUNT(*) FROM admin.huawei_accounts")->fetchColumn(),
|
|
'huawei_servers' => $pdo->query("SELECT COUNT(*) FROM admin.huawei_servers")->fetchColumn(),
|
|
'scaleway_accounts' => $pdo->query("SELECT COUNT(*) FROM admin.scaleway_accounts")->fetchColumn(),
|
|
'scaleway_servers' => $pdo->query("SELECT COUNT(*) FROM admin.scaleway_servers")->fetchColumn(),
|
|
'total_ips' => $pdo->query("SELECT COUNT(*) FROM admin.cloud_ips")->fetchColumn(),
|
|
]]);
|
|
break;
|
|
|
|
case 'get_regions':
|
|
global $HUAWEI_REGIONS, $SCALEWAY_REGIONS;
|
|
echo json_encode(['success' => true, 'huawei' => $HUAWEI_REGIONS, 'scaleway' => $SCALEWAY_REGIONS]);
|
|
break;
|
|
|
|
case 'get_instance_types':
|
|
global $SCALEWAY_INSTANCES;
|
|
echo json_encode(['success' => true, 'scaleway' => $SCALEWAY_INSTANCES]);
|
|
break;
|
|
|
|
// ========== HUAWEI ==========
|
|
case 'hw_add_account':
|
|
$stmt = $pdo->prepare("INSERT INTO admin.huawei_accounts (name, access_key, secret_key, project_id, region) VALUES (?, ?, ?, ?, ?) RETURNING id");
|
|
$stmt->execute([
|
|
$_POST['name'] ?? 'Huawei Account',
|
|
$_POST['access_key'],
|
|
$_POST['secret_key'],
|
|
$_POST['project_id'],
|
|
$_POST['region'] ?? 'eu-west-0'
|
|
]);
|
|
echo json_encode(['success' => true, 'id' => $stmt->fetchColumn()]);
|
|
break;
|
|
|
|
case 'hw_list_accounts':
|
|
$accounts = $pdo->query("SELECT id, name, region, status, created_at FROM admin.huawei_accounts ORDER BY id")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['success' => true, 'accounts' => $accounts]);
|
|
break;
|
|
|
|
case 'hw_delete_account':
|
|
$pdo->exec("DELETE FROM admin.huawei_accounts WHERE id = " . intval($_POST['id']));
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'hw_list_servers':
|
|
$accountId = $_GET['account_id'] ?? 0;
|
|
$api = new HuaweiCloudAPI($accountId);
|
|
$result = $api->listServers();
|
|
|
|
if ($result['code'] == 200 && isset($result['data']['servers'])) {
|
|
foreach ($result['data']['servers'] as $srv) {
|
|
$ip = $srv['addresses'][array_key_first($srv['addresses'])][0]['addr'] ?? '';
|
|
$stmt = $pdo->prepare("INSERT INTO admin.huawei_servers (account_id, server_id, name, region, ip_address, status)
|
|
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING");
|
|
$stmt->execute([$accountId, $srv['id'], $srv['name'], $srv['OS-EXT-AZ:availability_zone'] ?? '', $ip, $srv['status']]);
|
|
}
|
|
}
|
|
|
|
echo json_encode(['success' => true, 'servers' => $result['data']['servers'] ?? []]);
|
|
break;
|
|
|
|
case 'hw_server_action':
|
|
$accountId = $_POST['account_id'] ?? 0;
|
|
$serverId = $_POST['server_id'] ?? '';
|
|
$act = $_POST['server_action'] ?? '';
|
|
|
|
$api = new HuaweiCloudAPI($accountId);
|
|
$result = $api->serverAction($serverId, $act);
|
|
echo json_encode(['success' => $result['code'] == 200]);
|
|
break;
|
|
|
|
case 'hw_delete_server':
|
|
$accountId = $_POST['account_id'] ?? 0;
|
|
$serverId = $_POST['server_id'] ?? '';
|
|
|
|
$api = new HuaweiCloudAPI($accountId);
|
|
$result = $api->deleteServer($serverId);
|
|
|
|
if ($result['code'] == 200) {
|
|
$pdo->exec("DELETE FROM admin.huawei_servers WHERE server_id = " . $pdo->quote($serverId));
|
|
}
|
|
|
|
echo json_encode(['success' => $result['code'] == 200]);
|
|
break;
|
|
|
|
// ========== SCALEWAY ==========
|
|
case 'scw_add_account':
|
|
$stmt = $pdo->prepare("INSERT INTO admin.scaleway_accounts (name, access_key, secret_key, organization_id, project_id, default_zone) VALUES (?, ?, ?, ?, ?, ?) RETURNING id");
|
|
$stmt->execute([
|
|
$_POST['name'] ?? 'Scaleway Account',
|
|
$_POST['access_key'],
|
|
$_POST['secret_key'],
|
|
$_POST['organization_id'],
|
|
$_POST['project_id'],
|
|
$_POST['zone'] ?? 'fr-par-1'
|
|
]);
|
|
echo json_encode(['success' => true, 'id' => $stmt->fetchColumn()]);
|
|
break;
|
|
|
|
case 'scw_list_accounts':
|
|
$accounts = $pdo->query("SELECT id, name, default_zone, status, created_at FROM admin.scaleway_accounts ORDER BY id")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['success' => true, 'accounts' => $accounts]);
|
|
break;
|
|
|
|
case 'scw_delete_account':
|
|
$pdo->exec("DELETE FROM admin.scaleway_accounts WHERE id = " . intval($_POST['id']));
|
|
echo json_encode(['success' => true]);
|
|
break;
|
|
|
|
case 'scw_list_servers':
|
|
$accountId = $_GET['account_id'] ?? 0;
|
|
$zone = $_GET['zone'] ?? null;
|
|
|
|
$api = new ScalewayAPI($accountId);
|
|
if ($zone) $api->setZone($zone);
|
|
$result = $api->listServers($zone);
|
|
|
|
if ($result['code'] == 200 && isset($result['data']['servers'])) {
|
|
foreach ($result['data']['servers'] as $srv) {
|
|
$ip = $srv['public_ip']['address'] ?? '';
|
|
$stmt = $pdo->prepare("INSERT INTO admin.scaleway_servers (account_id, server_id, name, zone, public_ip, instance_type, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING");
|
|
$stmt->execute([$accountId, $srv['id'], $srv['name'], $srv['zone'], $ip, $srv['commercial_type'], $srv['state']]);
|
|
}
|
|
}
|
|
|
|
echo json_encode(['success' => true, 'servers' => $result['data']['servers'] ?? []]);
|
|
break;
|
|
|
|
case 'scw_create_server':
|
|
$accountId = $_POST['account_id'] ?? 0;
|
|
$name = $_POST['name'] ?? 'pmta-' . uniqid();
|
|
$type = $_POST['instance_type'] ?? 'DEV1-S';
|
|
$image = $_POST['image'] ?? 'ubuntu_focal';
|
|
$zone = $_POST['zone'] ?? 'fr-par-1';
|
|
|
|
$api = new ScalewayAPI($accountId);
|
|
$result = $api->createServer($name, $type, $image, $zone);
|
|
|
|
if ($result['code'] == 201 && isset($result['data']['server'])) {
|
|
$srv = $result['data']['server'];
|
|
$stmt = $pdo->prepare("INSERT INTO admin.scaleway_servers (account_id, server_id, name, zone, instance_type, status) VALUES (?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([$accountId, $srv['id'], $srv['name'], $zone, $type, $srv['state']]);
|
|
|
|
// Power on
|
|
$api->serverAction($srv['id'], 'poweron', $zone);
|
|
}
|
|
|
|
echo json_encode(['success' => $result['code'] == 201, 'server' => $result['data']['server'] ?? null]);
|
|
break;
|
|
|
|
case 'scw_bulk_create':
|
|
$accountId = $_POST['account_id'] ?? 0;
|
|
$prefix = $_POST['prefix'] ?? 'pmta';
|
|
$count = min(50, intval($_POST['count'] ?? 1));
|
|
$type = $_POST['instance_type'] ?? 'DEV1-S';
|
|
$image = $_POST['image'] ?? 'ubuntu_focal';
|
|
$zone = $_POST['zone'] ?? 'fr-par-1';
|
|
|
|
$api = new ScalewayAPI($accountId);
|
|
$results = $api->bulkCreateServers($prefix, $count, $type, $image, $zone);
|
|
|
|
$success = 0;
|
|
foreach ($results as $r) {
|
|
if ($r['code'] == 201) {
|
|
$success++;
|
|
$srv = $r['data']['server'];
|
|
$stmt = $pdo->prepare("INSERT INTO admin.scaleway_servers (account_id, server_id, name, zone, instance_type, status) VALUES (?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([$accountId, $srv['id'], $srv['name'], $zone, $type, $srv['state']]);
|
|
$api->serverAction($srv['id'], 'poweron', $zone);
|
|
}
|
|
}
|
|
|
|
echo json_encode(['success' => true, 'created' => $success, 'total' => $count]);
|
|
break;
|
|
|
|
case 'scw_server_action':
|
|
$accountId = $_POST['account_id'] ?? 0;
|
|
$serverId = $_POST['server_id'] ?? '';
|
|
$act = $_POST['server_action'] ?? '';
|
|
$zone = $_POST['zone'] ?? null;
|
|
|
|
$api = new ScalewayAPI($accountId);
|
|
$result = $api->serverAction($serverId, $act, $zone);
|
|
echo json_encode(['success' => in_array($result['code'], [200, 202])]);
|
|
break;
|
|
|
|
case 'scw_delete_server':
|
|
$accountId = $_POST['account_id'] ?? 0;
|
|
$serverId = $_POST['server_id'] ?? '';
|
|
$zone = $_POST['zone'] ?? null;
|
|
|
|
$api = new ScalewayAPI($accountId);
|
|
$result = $api->deleteServer($serverId, $zone);
|
|
|
|
if (in_array($result['code'], [200, 204])) {
|
|
$pdo->exec("DELETE FROM admin.scaleway_servers WHERE server_id = " . $pdo->quote($serverId));
|
|
}
|
|
|
|
echo json_encode(['success' => in_array($result['code'], [200, 204])]);
|
|
break;
|
|
|
|
case 'scw_list_ips':
|
|
$accountId = $_GET['account_id'] ?? 0;
|
|
$zone = $_GET['zone'] ?? null;
|
|
|
|
$api = new ScalewayAPI($accountId);
|
|
$result = $api->listIPs($zone);
|
|
echo json_encode(['success' => true, 'ips' => $result['data']['ips'] ?? []]);
|
|
break;
|
|
|
|
case 'scw_create_ip':
|
|
$accountId = $_POST['account_id'] ?? 0;
|
|
$zone = $_POST['zone'] ?? null;
|
|
|
|
$api = new ScalewayAPI($accountId);
|
|
$result = $api->createIP($zone);
|
|
|
|
if ($result['code'] == 201 && isset($result['data']['ip'])) {
|
|
$ip = $result['data']['ip'];
|
|
$stmt = $pdo->prepare("INSERT INTO admin.cloud_ips (provider, account_id, ip_address, region) VALUES ('scaleway', ?, ?, ?)");
|
|
$stmt->execute([$accountId, $ip['address'], $zone]);
|
|
}
|
|
|
|
echo json_encode(['success' => $result['code'] == 201, 'ip' => $result['data']['ip'] ?? null]);
|
|
break;
|
|
|
|
case 'scw_set_ptr':
|
|
$accountId = $_POST['account_id'] ?? 0;
|
|
$ipId = $_POST['ip_id'] ?? '';
|
|
$reverse = $_POST['reverse'] ?? '';
|
|
$zone = $_POST['zone'] ?? null;
|
|
|
|
$api = new ScalewayAPI($accountId);
|
|
$result = $api->updateReverseDNS($ipId, $reverse, $zone);
|
|
echo json_encode(['success' => in_array($result['code'], [200, 201])]);
|
|
break;
|
|
|
|
case 'scw_list_images':
|
|
$accountId = $_GET['account_id'] ?? 0;
|
|
$zone = $_GET['zone'] ?? 'fr-par-1';
|
|
|
|
$api = new ScalewayAPI($accountId);
|
|
$result = $api->listImages($zone);
|
|
echo json_encode(['success' => true, 'images' => $result['data']['images'] ?? []]);
|
|
break;
|
|
|
|
default:
|
|
echo json_encode(['success' => false, 'error' => 'Unknown action']);
|
|
}
|
|
} catch (Exception $e) {
|
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
ensureTables();
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Cloud Manager - Huawei & Scaleway</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
:root{--bg:#0a0a0f;--bg2:#12121a;--bg3:#1a1a25;--primary:#6366f1;--success:#10b981;--warning:#f59e0b;--danger:#ef4444;--text:#e2e8f0;--text2:#94a3b8;--border:#2a2a3a;--huawei:#e4002b;--scaleway:#4f0599}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
|
.header{background:linear-gradient(135deg,var(--bg2),var(--bg3));border-bottom:1px solid var(--border);padding:1rem 2rem;display:flex;justify-content:space-between;align-items:center}
|
|
.header h1{font-size:1.3rem;display:flex;align-items:center;gap:.5rem}
|
|
.tabs{display:flex;gap:0;background:var(--bg3);border-radius:8px;padding:4px}
|
|
.tab{padding:.6rem 1.5rem;cursor:pointer;border-radius:6px;font-weight:600;font-size:.85rem;transition:all .2s;display:flex;align-items:center;gap:.5rem}
|
|
.tab:hover{background:rgba(255,255,255,.05)}
|
|
.tab.active{color:white}
|
|
.tab.huawei.active{background:var(--huawei)}
|
|
.tab.scaleway.active{background:var(--scaleway)}
|
|
.container{max-width:1500px;margin:0 auto;padding:1.5rem}
|
|
.stats{display:grid;grid-template-columns:repeat(5,1fr);gap:1rem;margin-bottom:1.5rem}
|
|
.stat{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1rem;text-align:center}
|
|
.stat .v{font-size:1.8rem;font-weight:700}.stat .l{color:var(--text2);font-size:.75rem}
|
|
.stat.huawei .v{color:var(--huawei)}.stat.scaleway .v{color:var(--scaleway)}
|
|
.panel{display:none}.panel.active{display:block}
|
|
.card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1.25rem;margin-bottom:1rem}
|
|
.card h3{margin-bottom:1rem;font-size:1rem;display:flex;align-items:center;gap:.5rem}
|
|
.form-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:1rem}
|
|
.form-group{display:flex;flex-direction:column;gap:.3rem}
|
|
.form-group label{font-size:.75rem;color:var(--text2)}
|
|
.form-group input,.form-group select{padding:.6rem .8rem;background:var(--bg3);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.85rem}
|
|
.btn{padding:.5rem 1rem;border:none;border-radius:6px;font-weight:600;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:.4rem;font-size:.8rem}
|
|
.btn:hover{transform:translateY(-1px);opacity:.9}
|
|
.btn-primary{background:var(--primary);color:white}
|
|
.btn-success{background:var(--success);color:white}
|
|
.btn-danger{background:var(--danger);color:white}
|
|
.btn-warning{background:var(--warning);color:black}
|
|
.btn-huawei{background:var(--huawei);color:white}
|
|
.btn-scaleway{background:var(--scaleway);color:white}
|
|
.btn-sm{padding:.35rem .7rem;font-size:.75rem}
|
|
table{width:100%;border-collapse:collapse;font-size:.8rem}
|
|
th,td{padding:.6rem;text-align:left;border-bottom:1px solid var(--border)}
|
|
th{color:var(--text2);font-weight:500;font-size:.7rem;text-transform:uppercase}
|
|
.badge{padding:.2rem .5rem;border-radius:10px;font-size:.65rem;font-weight:600}
|
|
.badge-running,.badge-active{background:rgba(16,185,129,.15);color:var(--success)}
|
|
.badge-stopped{background:rgba(239,68,68,.15);color:var(--danger)}
|
|
.badge-starting,.badge-stopping{background:rgba(245,158,11,.15);color:var(--warning)}
|
|
.empty{text-align:center;padding:2rem;color:var(--text2)}
|
|
.server-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}
|
|
.server-card{background:var(--bg3);border-radius:8px;padding:1rem;border-left:4px solid var(--primary)}
|
|
.server-card.running{border-left-color:var(--success)}
|
|
.server-card.stopped{border-left-color:var(--danger)}
|
|
.server-card h4{margin-bottom:.5rem;display:flex;justify-content:space-between;align-items:center}
|
|
.server-card p{font-size:.75rem;color:var(--text2);margin-bottom:.25rem}
|
|
.server-card .actions{margin-top:.75rem;display:flex;gap:.5rem}
|
|
.region-select{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.5rem;margin-bottom:1rem}
|
|
.region-opt{padding:.5rem .75rem;background:var(--bg3);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:.75rem;transition:all .2s}
|
|
.region-opt:hover,.region-opt.active{border-color:var(--primary);background:rgba(99,102,241,.1)}
|
|
.zone-badge{background:var(--bg);padding:.15rem .4rem;border-radius:3px;font-size:.6rem;margin-left:.25rem}
|
|
</style>
|
|
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1><i class="fas fa-cloud"></i> Cloud Manager</h1>
|
|
<div class="tabs">
|
|
<div class="tab huawei active" data-tab="huawei"><i class="fas fa-server"></i> Huawei Cloud</div>
|
|
<div class="tab scaleway" data-tab="scaleway"><i class="fas fa-server"></i> Scaleway</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="stats">
|
|
<div class="stat huawei"><div class="v" id="sHwAcc">0</div><div class="l">Huawei Accounts</div></div>
|
|
<div class="stat huawei"><div class="v" id="sHwSrv">0</div><div class="l">Huawei Servers</div></div>
|
|
<div class="stat scaleway"><div class="v" id="sScwAcc">0</div><div class="l">Scaleway Accounts</div></div>
|
|
<div class="stat scaleway"><div class="v" id="sScwSrv">0</div><div class="l">Scaleway Servers</div></div>
|
|
<div class="stat"><div class="v" id="sIPs">0</div><div class="l">Total IPs</div></div>
|
|
</div>
|
|
|
|
<!-- Huawei Panel -->
|
|
<div class="panel active" id="panel-huawei">
|
|
<div class="card">
|
|
<h3><i class="fas fa-plus" style="color:var(--huawei)"></i> Add Huawei Account</h3>
|
|
<div class="form-row">
|
|
<div class="form-group"><label>Account Name</label><input type="text" id="hwName" placeholder="My Huawei"></div>
|
|
<div class="form-group"><label>Access Key (AK)</label><input type="text" id="hwAK" placeholder="AK..."></div>
|
|
<div class="form-group"><label>Secret Key (SK)</label><input type="password" id="hwSK" placeholder="SK..."></div>
|
|
<div class="form-group"><label>Project ID</label><input type="text" id="hwProject" placeholder="project-id"></div>
|
|
<div class="form-group"><label>Region</label><select id="hwRegion"></select></div>
|
|
<div class="form-group"><label> </label><button class="btn btn-huawei" onclick="addHuaweiAccount()"><i class="fas fa-plus"></i> Add</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3><i class="fas fa-users"></i> Huawei Accounts</h3>
|
|
<table>
|
|
<thead><tr><th>ID</th><th>Name</th><th>Region</th><th>Status</th><th>Actions</th></tr></thead>
|
|
<tbody id="hwAccounts"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3><i class="fas fa-server"></i> Huawei Servers</h3>
|
|
<div id="hwServers" class="empty">Select an account to load servers</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scaleway Panel -->
|
|
<div class="panel" id="panel-scaleway">
|
|
<div class="card">
|
|
<h3><i class="fas fa-plus" style="color:var(--scaleway)"></i> Add Scaleway Account</h3>
|
|
<div class="form-row">
|
|
<div class="form-group"><label>Account Name</label><input type="text" id="scwName" placeholder="My Scaleway"></div>
|
|
<div class="form-group"><label>Access Key</label><input type="text" id="scwAK" placeholder="SCW..."></div>
|
|
<div class="form-group"><label>Secret Key</label><input type="password" id="scwSK" placeholder="Secret key"></div>
|
|
<div class="form-group"><label>Organization ID</label><input type="text" id="scwOrg" placeholder="org-id"></div>
|
|
<div class="form-group"><label>Project ID</label><input type="text" id="scwProject" placeholder="project-id"></div>
|
|
<div class="form-group"><label>Default Zone</label><select id="scwZone"></select></div>
|
|
<div class="form-group"><label> </label><button class="btn btn-scaleway" onclick="addScalewayAccount()"><i class="fas fa-plus"></i> Add</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3><i class="fas fa-users"></i> Scaleway Accounts</h3>
|
|
<table>
|
|
<thead><tr><th>ID</th><th>Name</th><th>Zone</th><th>Status</th><th>Actions</th></tr></thead>
|
|
<tbody id="scwAccounts"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card" id="scwCreateCard" style="display:none">
|
|
<h3><i class="fas fa-plus-circle" style="color:var(--scaleway)"></i> Create Servers</h3>
|
|
<div class="form-row">
|
|
<div class="form-group"><label>Name / Prefix</label><input type="text" id="scwSrvName" placeholder="pmta-001"></div>
|
|
<div class="form-group"><label>Instance Type</label><select id="scwSrvType"></select></div>
|
|
<div class="form-group"><label>Zone</label><select id="scwSrvZone"></select></div>
|
|
<div class="form-group"><label>Count (bulk)</label><input type="number" id="scwSrvCount" value="1" min="1" max="50"></div>
|
|
<div class="form-group"><label> </label><button class="btn btn-scaleway" onclick="createScalewayServer()"><i class="fas fa-plus"></i> Create</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3><i class="fas fa-server"></i> Scaleway Servers</h3>
|
|
<div class="form-row" style="margin-bottom:1rem">
|
|
<div class="form-group"><label>Filter by Zone</label><select id="scwFilterZone" onchange="loadScwServers()"><option value="">All Zones</option></select></div>
|
|
</div>
|
|
<div id="scwServers" class="server-grid empty">Select an account to load servers</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3><i class="fas fa-network-wired"></i> IPs Management</h3>
|
|
<button class="btn btn-scaleway btn-sm" onclick="createScalewayIP()"><i class="fas fa-plus"></i> Reserve New IP</button>
|
|
<table style="margin-top:1rem">
|
|
<thead><tr><th>IP Address</th><th>Zone</th><th>Server</th><th>Reverse DNS</th><th>Actions</th></tr></thead>
|
|
<tbody id="scwIPs"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let regions = {huawei: {}, scaleway: {}};
|
|
let instanceTypes = {};
|
|
let currentHwAccount = null;
|
|
let currentScwAccount = null;
|
|
|
|
// Tabs
|
|
document.querySelectorAll('.tab').forEach(t => {
|
|
t.onclick = () => {
|
|
document.querySelectorAll('.tab').forEach(x => x.classList.remove('active'));
|
|
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
t.classList.add('active');
|
|
document.getElementById('panel-' + t.dataset.tab).classList.add('active');
|
|
};
|
|
});
|
|
|
|
// Load initial data
|
|
function init() {
|
|
loadStats();
|
|
loadRegions();
|
|
loadInstanceTypes();
|
|
loadHuaweiAccounts();
|
|
loadScalewayAccounts();
|
|
}
|
|
|
|
function loadStats() {
|
|
fetch('?action=stats').then(r=>r.json()).then(d => {
|
|
if (d.success) {
|
|
document.getElementById('sHwAcc').textContent = d.stats.huawei_accounts;
|
|
document.getElementById('sHwSrv').textContent = d.stats.huawei_servers;
|
|
document.getElementById('sScwAcc').textContent = d.stats.scaleway_accounts;
|
|
document.getElementById('sScwSrv').textContent = d.stats.scaleway_servers;
|
|
document.getElementById('sIPs').textContent = d.stats.total_ips;
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadRegions() {
|
|
fetch('?action=get_regions').then(r=>r.json()).then(d => {
|
|
if (d.success) {
|
|
regions = {huawei: d.huawei, scaleway: d.scaleway};
|
|
|
|
// Huawei regions
|
|
let hwOpts = '';
|
|
for (const [k, v] of Object.entries(d.huawei)) {
|
|
hwOpts += `<option value="${k}">${v}</option>`;
|
|
}
|
|
document.getElementById('hwRegion').innerHTML = hwOpts;
|
|
|
|
// Scaleway zones
|
|
let scwOpts = '';
|
|
for (const [region, data] of Object.entries(d.scaleway)) {
|
|
for (const zone of data.zones) {
|
|
scwOpts += `<option value="${zone}">${data.name} - ${zone}</option>`;
|
|
}
|
|
}
|
|
document.getElementById('scwZone').innerHTML = scwOpts;
|
|
document.getElementById('scwSrvZone').innerHTML = scwOpts;
|
|
document.getElementById('scwFilterZone').innerHTML = '<option value="">All Zones</option>' + scwOpts;
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadInstanceTypes() {
|
|
fetch('?action=get_instance_types').then(r=>r.json()).then(d => {
|
|
if (d.success) {
|
|
instanceTypes = d.scaleway;
|
|
let opts = '';
|
|
for (const [type, specs] of Object.entries(d.scaleway)) {
|
|
opts += `<option value="${type}">${type} (${specs.vcpus}vCPU, ${specs.ram}GB, €${specs.price}/h)</option>`;
|
|
}
|
|
document.getElementById('scwSrvType').innerHTML = opts;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Huawei
|
|
function loadHuaweiAccounts() {
|
|
fetch('?action=hw_list_accounts').then(r=>r.json()).then(d => {
|
|
if (d.success) {
|
|
document.getElementById('hwAccounts').innerHTML = d.accounts.length ? d.accounts.map(a => `
|
|
<tr>
|
|
<td>#${a.id}</td>
|
|
<td>${a.name}</td>
|
|
<td>${regions.huawei[a.region] || a.region}</td>
|
|
<td><span class="badge badge-active">${a.status}</span></td>
|
|
<td>
|
|
<button class="btn btn-huawei btn-sm" onclick="loadHwServers(${a.id})"><i class="fas fa-server"></i> Servers</button>
|
|
<button class="btn btn-danger btn-sm" onclick="deleteHwAccount(${a.id})"><i class="fas fa-trash"></i></button>
|
|
</td>
|
|
</tr>
|
|
`).join('') : '<tr><td colspan="5" class="empty">No accounts</td></tr>';
|
|
}
|
|
});
|
|
}
|
|
|
|
function addHuaweiAccount() {
|
|
const fd = new FormData();
|
|
fd.append('action', 'hw_add_account');
|
|
fd.append('name', document.getElementById('hwName').value);
|
|
fd.append('access_key', document.getElementById('hwAK').value);
|
|
fd.append('secret_key', document.getElementById('hwSK').value);
|
|
fd.append('project_id', document.getElementById('hwProject').value);
|
|
fd.append('region', document.getElementById('hwRegion').value);
|
|
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
if (d.success) { alert('Account added!'); loadHuaweiAccounts(); loadStats(); }
|
|
else alert('Error: ' + d.error);
|
|
});
|
|
}
|
|
|
|
function deleteHwAccount(id) {
|
|
if (!confirm('Delete this account?')) return;
|
|
const fd = new FormData();
|
|
fd.append('action', 'hw_delete_account');
|
|
fd.append('id', id);
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
loadHuaweiAccounts(); loadStats();
|
|
});
|
|
}
|
|
|
|
function loadHwServers(accountId) {
|
|
currentHwAccount = accountId;
|
|
document.getElementById('hwServers').innerHTML = '<div class="empty"><i class="fas fa-spinner fa-spin"></i> Loading...</div>';
|
|
|
|
fetch('?action=hw_list_servers&account_id=' + accountId).then(r=>r.json()).then(d => {
|
|
if (d.success && d.servers.length) {
|
|
document.getElementById('hwServers').innerHTML = '<div class="server-grid">' + d.servers.map(s => {
|
|
const ip = s.addresses?.[Object.keys(s.addresses)[0]]?.[0]?.addr || '-';
|
|
return `
|
|
<div class="server-card ${s.status.toLowerCase()}">
|
|
<h4>${s.name} <span class="badge badge-${s.status.toLowerCase()}">${s.status}</span></h4>
|
|
<p><i class="fas fa-microchip"></i> ${s.flavor?.name || s.flavor?.id || '-'}</p>
|
|
<p><i class="fas fa-network-wired"></i> ${ip}</p>
|
|
<p><i class="fas fa-map-marker-alt"></i> ${s['OS-EXT-AZ:availability_zone'] || '-'}</p>
|
|
<div class="actions">
|
|
<button class="btn btn-success btn-sm" onclick="hwAction('${s.id}','start')"><i class="fas fa-play"></i></button>
|
|
<button class="btn btn-warning btn-sm" onclick="hwAction('${s.id}','stop')"><i class="fas fa-stop"></i></button>
|
|
<button class="btn btn-primary btn-sm" onclick="hwAction('${s.id}','reboot')"><i class="fas fa-sync"></i></button>
|
|
<button class="btn btn-danger btn-sm" onclick="deleteHwServer('${s.id}')"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
</div>`;
|
|
}).join('') + '</div>';
|
|
} else {
|
|
document.getElementById('hwServers').innerHTML = '<div class="empty">No servers found</div>';
|
|
}
|
|
});
|
|
}
|
|
|
|
function hwAction(serverId, action) {
|
|
const fd = new FormData();
|
|
fd.append('action', 'hw_server_action');
|
|
fd.append('account_id', currentHwAccount);
|
|
fd.append('server_id', serverId);
|
|
fd.append('server_action', action);
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
if (d.success) setTimeout(() => loadHwServers(currentHwAccount), 2000);
|
|
else alert('Action failed');
|
|
});
|
|
}
|
|
|
|
function deleteHwServer(serverId) {
|
|
if (!confirm('Delete this server?')) return;
|
|
const fd = new FormData();
|
|
fd.append('action', 'hw_delete_server');
|
|
fd.append('account_id', currentHwAccount);
|
|
fd.append('server_id', serverId);
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
loadHwServers(currentHwAccount); loadStats();
|
|
});
|
|
}
|
|
|
|
// Scaleway
|
|
function loadScalewayAccounts() {
|
|
fetch('?action=scw_list_accounts').then(r=>r.json()).then(d => {
|
|
if (d.success) {
|
|
document.getElementById('scwAccounts').innerHTML = d.accounts.length ? d.accounts.map(a => `
|
|
<tr>
|
|
<td>#${a.id}</td>
|
|
<td>${a.name}</td>
|
|
<td><span class="zone-badge">${a.default_zone}</span></td>
|
|
<td><span class="badge badge-active">${a.status}</span></td>
|
|
<td>
|
|
<button class="btn btn-scaleway btn-sm" onclick="selectScwAccount(${a.id})"><i class="fas fa-server"></i> Manage</button>
|
|
<button class="btn btn-danger btn-sm" onclick="deleteScwAccount(${a.id})"><i class="fas fa-trash"></i></button>
|
|
</td>
|
|
</tr>
|
|
`).join('') : '<tr><td colspan="5" class="empty">No accounts</td></tr>';
|
|
}
|
|
});
|
|
}
|
|
|
|
function addScalewayAccount() {
|
|
const fd = new FormData();
|
|
fd.append('action', 'scw_add_account');
|
|
fd.append('name', document.getElementById('scwName').value);
|
|
fd.append('access_key', document.getElementById('scwAK').value);
|
|
fd.append('secret_key', document.getElementById('scwSK').value);
|
|
fd.append('organization_id', document.getElementById('scwOrg').value);
|
|
fd.append('project_id', document.getElementById('scwProject').value);
|
|
fd.append('zone', document.getElementById('scwZone').value);
|
|
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
if (d.success) { alert('Account added!'); loadScalewayAccounts(); loadStats(); }
|
|
else alert('Error: ' + d.error);
|
|
});
|
|
}
|
|
|
|
function deleteScwAccount(id) {
|
|
if (!confirm('Delete this account?')) return;
|
|
const fd = new FormData();
|
|
fd.append('action', 'scw_delete_account');
|
|
fd.append('id', id);
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
loadScalewayAccounts(); loadStats();
|
|
});
|
|
}
|
|
|
|
function selectScwAccount(accountId) {
|
|
currentScwAccount = accountId;
|
|
document.getElementById('scwCreateCard').style.display = 'block';
|
|
loadScwServers();
|
|
loadScwIPs();
|
|
}
|
|
|
|
function loadScwServers() {
|
|
if (!currentScwAccount) return;
|
|
const zone = document.getElementById('scwFilterZone').value;
|
|
document.getElementById('scwServers').innerHTML = '<div class="empty"><i class="fas fa-spinner fa-spin"></i> Loading...</div>';
|
|
|
|
fetch(`?action=scw_list_servers&account_id=${currentScwAccount}&zone=${zone}`).then(r=>r.json()).then(d => {
|
|
if (d.success && d.servers.length) {
|
|
document.getElementById('scwServers').innerHTML = d.servers.map(s => `
|
|
<div class="server-card ${s.state}">
|
|
<h4>${s.name} <span class="badge badge-${s.state}">${s.state}</span></h4>
|
|
<p><i class="fas fa-microchip"></i> ${s.commercial_type}</p>
|
|
<p><i class="fas fa-network-wired"></i> ${s.public_ip?.address || 'No public IP'}</p>
|
|
<p><i class="fas fa-map-marker-alt"></i> ${s.zone}</p>
|
|
<div class="actions">
|
|
<button class="btn btn-success btn-sm" onclick="scwAction('${s.id}','poweron','${s.zone}')"><i class="fas fa-play"></i></button>
|
|
<button class="btn btn-warning btn-sm" onclick="scwAction('${s.id}','poweroff','${s.zone}')"><i class="fas fa-stop"></i></button>
|
|
<button class="btn btn-primary btn-sm" onclick="scwAction('${s.id}','reboot','${s.zone}')"><i class="fas fa-sync"></i></button>
|
|
<button class="btn btn-danger btn-sm" onclick="deleteScwServer('${s.id}','${s.zone}')"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
document.getElementById('scwServers').classList.remove('empty');
|
|
} else {
|
|
document.getElementById('scwServers').innerHTML = 'No servers found';
|
|
document.getElementById('scwServers').classList.add('empty');
|
|
}
|
|
});
|
|
}
|
|
|
|
function createScalewayServer() {
|
|
const count = parseInt(document.getElementById('scwSrvCount').value);
|
|
const action = count > 1 ? 'scw_bulk_create' : 'scw_create_server';
|
|
|
|
const fd = new FormData();
|
|
fd.append('action', action);
|
|
fd.append('account_id', currentScwAccount);
|
|
fd.append('name', document.getElementById('scwSrvName').value);
|
|
fd.append('prefix', document.getElementById('scwSrvName').value);
|
|
fd.append('instance_type', document.getElementById('scwSrvType').value);
|
|
fd.append('zone', document.getElementById('scwSrvZone').value);
|
|
fd.append('count', count);
|
|
fd.append('image', 'ubuntu_focal');
|
|
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
if (d.success) {
|
|
alert(count > 1 ? `Created ${d.created}/${d.total} servers` : 'Server created!');
|
|
loadScwServers();
|
|
loadStats();
|
|
} else {
|
|
alert('Error: ' + (d.error || 'Unknown'));
|
|
}
|
|
});
|
|
}
|
|
|
|
function scwAction(serverId, action, zone) {
|
|
const fd = new FormData();
|
|
fd.append('action', 'scw_server_action');
|
|
fd.append('account_id', currentScwAccount);
|
|
fd.append('server_id', serverId);
|
|
fd.append('server_action', action);
|
|
fd.append('zone', zone);
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
if (d.success) setTimeout(loadScwServers, 2000);
|
|
else alert('Action failed');
|
|
});
|
|
}
|
|
|
|
function deleteScwServer(serverId, zone) {
|
|
if (!confirm('Delete this server?')) return;
|
|
const fd = new FormData();
|
|
fd.append('action', 'scw_delete_server');
|
|
fd.append('account_id', currentScwAccount);
|
|
fd.append('server_id', serverId);
|
|
fd.append('zone', zone);
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
loadScwServers(); loadStats();
|
|
});
|
|
}
|
|
|
|
function loadScwIPs() {
|
|
if (!currentScwAccount) return;
|
|
fetch(`?action=scw_list_ips&account_id=${currentScwAccount}`).then(r=>r.json()).then(d => {
|
|
if (d.success) {
|
|
document.getElementById('scwIPs').innerHTML = d.ips.length ? d.ips.map(ip => `
|
|
<tr>
|
|
<td><strong>${ip.address}</strong></td>
|
|
<td><span class="zone-badge">${ip.zone}</span></td>
|
|
<td>${ip.server?.name || '-'}</td>
|
|
<td>${ip.reverse || '-'}</td>
|
|
<td>
|
|
<button class="btn btn-primary btn-sm" onclick="setScwPTR('${ip.id}','${ip.zone}')"><i class="fas fa-edit"></i> PTR</button>
|
|
</td>
|
|
</tr>
|
|
`).join('') : '<tr><td colspan="5" class="empty">No IPs</td></tr>';
|
|
}
|
|
});
|
|
}
|
|
|
|
function createScalewayIP() {
|
|
const zone = document.getElementById('scwSrvZone').value || 'fr-par-1';
|
|
const fd = new FormData();
|
|
fd.append('action', 'scw_create_ip');
|
|
fd.append('account_id', currentScwAccount);
|
|
fd.append('zone', zone);
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
if (d.success) { alert('IP created: ' + d.ip.address); loadScwIPs(); loadStats(); }
|
|
else alert('Error creating IP');
|
|
});
|
|
}
|
|
|
|
function setScwPTR(ipId, zone) {
|
|
const reverse = prompt('Enter reverse DNS (PTR record):');
|
|
if (!reverse) return;
|
|
const fd = new FormData();
|
|
fd.append('action', 'scw_set_ptr');
|
|
fd.append('account_id', currentScwAccount);
|
|
fd.append('ip_id', ipId);
|
|
fd.append('reverse', reverse);
|
|
fd.append('zone', zone);
|
|
fetch('', {method:'POST', body:fd}).then(r=>r.json()).then(d => {
|
|
if (d.success) { alert('PTR updated!'); loadScwIPs(); }
|
|
else alert('Error updating PTR');
|
|
});
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|