baseUrl = $_ENV['LXD_API_URL'] ?? 'https://localhost:8443'; } private function curlInit($url, $method = '') { $ch = curl_init($url); // Paths to client certificate and key for TLS authentication $clientCert = $_ENV['LXD_CLIENT_CERT'] ?? '/etc/ssl/lxdapp/client.crt'; $clientKey = $_ENV['LXD_CLIENT_KEY'] ?? '/etc/ssl/lxdapp/client.key'; curl_setopt($ch, CURLOPT_SSLCERT, $clientCert); curl_setopt($ch, CURLOPT_SSLKEY, $clientKey); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); if($method) curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); return $ch; } /** * Sends an HTTP request to the LXD API. * * @param string $method HTTP method (GET, POST, PUT, etc.) * @param string $endpoint API endpoint * @param array $body Request body (optional) * @return array Response from the API * @throws Exception */ private function request(string $method, string $endpoint, array $body = []): array { $ch = $this->curlInit("{$this->baseUrl}{$endpoint}", $method); if (!empty($body)) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); // Check if curl_exec failed if (curl_errno($ch)) { throw new Exception("Curl error: " . curl_error($ch)); } curl_close($ch); if (!$response) { throw new Exception("LXD API error or not reachable."); } $json = json_decode($response, true); if ($httpCode >= 400) { throw new Exception("LXD API Error: " . ($json['error'] ?? 'Unknown')); } return $json; } /** * Sends a GET request and returns raw (non-JSON) response. * * @param string $endpoint API endpoint * @return string Raw response body * @throws Exception */ private function requestRaw(string $endpoint): string { $url = "{$this->baseUrl}{$endpoint}"; $ch = $this->curlInit($url); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $curlError = curl_error($ch); curl_close($ch); if ($response === false || $response === null) { throw new \Exception("Raw LXD API call failed: $curlError"); } if ($httpCode >= 400) { throw new \Exception("LXD raw API returned HTTP $httpCode"); } return $response; } /** * Retrieves the status of a container. * * @param string $name Container name * @return array|null Container status or null if an error occurs */ public function getContainerState(string $name) { try { return $this->request('GET', "/1.0/instances/$name/state"); } catch (\Throwable $e) { return null; // echo 'Error: ' . $e->getMessage(); } } /** * Checks if a container exists. * * @param string $name Container name * @return bool True if the container exists, false otherwise */ public function containerExists(string $name): bool { return $this->getContainerState($name) !== null; } /** * Starts a container. * * @param string $name Container name * @return array Response from the API * @throws Exception */ public function startContainer(string $name): array { $startResponse = $this->request('PUT', "/1.0/instances/$name/state", [ "action" => "start", "timeout" => 30, "force" => true ]); if (!isset($startResponse['operation'])) { throw new \Exception("Failed to start container, please try again."); } return $startResponse; } /** * Stops a container. * * @param string $name Container name * @return array Response from the API */ public function stopContainer(string $name): array { $response = $this->request('PUT', "/1.0/instances/$name/state", [ "action" => "stop", "timeout" => 30, "force" => true ]); return $response; } /** * Retrieves the IPv4 address of a container. * * @param string $name Container name * @return string|null IPv4 address or null if not found */ public function getContainerIP($name) { $container = $this->getContainerState($name); return $this->getIPv4FromMetadata($container['metadata']); } /** * Extracts the IPv4 address from container metadata. * * @param array $metadata Container metadata * @return string|null IPv4 address or null if not found */ public function getIPv4FromMetadata(array $metadata): ?string { if ( isset($metadata['network']['eth0']['addresses']) && is_array($metadata['network']['eth0']['addresses']) ) { foreach ($metadata['network']['eth0']['addresses'] as $addr) { if (isset($addr['family']) && $addr['family'] === 'inet' && isset($addr['address'])) { return $addr['address']; } } } return null; } // public function isServiceRunning(string $container, string $service): string // { // $lxcPath = $_ENV['LXC_PATH'] ?: 'lxc'; // $cmd = "$lxcPath exec {$container} -- systemctl is-active {$service} 2>&1"; // $output = shell_exec($cmd); // return trim($output); // } public function getContainerServiceStatus(string $container, string $service): string { // Step 1: Prepare exec command $execPayload = [ "command" => ["systemctl", "is-active", $service], "wait-for-websocket" => false, "record-output" => true, "interactive" => false ]; // Step 2: Start exec operation $execResponse = $this->request('POST', "/1.0/instances/{$container}/exec", $execPayload); $operationUrl = $execResponse['operation'] ?? null; if (!$operationUrl) { throw new \Exception("Failed to create exec operation"); } // Step 3: Wait for operation to complete $waitResponse = $this->request('GET', "{$operationUrl}/wait?timeout=10"); $metadata = $waitResponse['metadata']['metadata'] ?? []; $outputPaths = $metadata['output'] ?? []; $stdoutPath = $outputPaths['1'] ?? null; if (!$stdoutPath) { throw new \Exception("No stdout path returned by LXD"); } // Step 4: Fetch raw stdout output $rawOutput = $this->requestRaw($stdoutPath); return trim($rawOutput); // Expected: 'active', 'inactive', etc. } }