Anmelden
DE
<?phpnamespace App\Xapi;/** * The XVA format of XAPI's exports: a tar archive whose first member is ova.xml (the VM's record and its disks, as XML-RPC values), * then each disk in 1 MiB members "Ref:<n>/00000000", "Ref:<n>/00000000.checksum"... Here: what XenCenter needs to read the VM's * name before an import (readHeader(), the first 512-byte header and ova.xml only, never the whole file), and a small writer the * demo pool uses to answer /export. */final class Xva{    /** One tar member: a 512-byte ustar header, the data, padding to 512. */    public static function member(string $name, string $data, int $mtime = 0): string    {        $header = str_pad($name, 100, "\0") . str_pad('0000644', 7, '0', STR_PAD_LEFT) . "\0" . "0000000\0" . "0000000\0"            . str_pad(decoct(strlen($data)), 11, '0', STR_PAD_LEFT) . "\0" . str_pad(decoct($mtime), 11, '0', STR_PAD_LEFT) . "\0"            . '        ' . '0' . str_repeat("\0", 100) . "ustar\0" . '00' . str_repeat("\0", 32) . str_repeat("\0", 32)            . str_repeat("\0", 8) . str_repeat("\0", 8) . str_repeat("\0", 155);        $header = str_pad($header, 512, "\0");        $sum = 0;        for ($i = 0; $i < 512; $i++) {            $sum += ord($header[$i]);        }        $header = substr_replace($header, str_pad(decoct($sum), 6, '0', STR_PAD_LEFT) . "\0 ", 148, 8);        $pad = (512 - strlen($data) % 512) % 512;        return $header . $data . str_repeat("\0", $pad);    }    /** The end of a tar archive: two empty blocks. */    public static function end(): string    {        return str_repeat("\0", 1024);    }    /**     * The ova.xml of a VM: the "objects" list XAPI writes (class, id, snapshot = the record), the fields an import needs.     *     * @param array<string, mixed> $vm     * @param list<array{id: string, name: string, size: float}> $disks     */    public static function ovaXml(array $vm, array $disks): string    {        $member = fn (string $name, string $value): string => '<member><name>' . $name . '</name><value>' . htmlspecialchars($value, ENT_XML1) . '</value></member>';        $objects = '<value><struct>' . $member('class', 'VM') . $member('id', 'Ref:0') . '<member><name>snapshot</name><value><struct>'            . $member('name_label', (string)($vm['name_label'] ?? 'VM')) . $member('name_description', (string)($vm['name_description'] ?? ''))            . $member('memory_static_max', sprintf('%.0F', (float)($vm['memory_static_max'] ?? 0))) . $member('VCPUs_max', (string)(int)($vm['VCPUs_max'] ?? 1))            . $member('power_state', 'Halted') . $member('is_a_template', 'false') . '</struct></value></member></struct></value>';        foreach ($disks as $disk) {            $objects .= '<value><struct>' . $member('class', 'VDI') . $member('id', $disk['id']) . '<member><name>snapshot</name><value><struct>'                . $member('name_label', $disk['name']) . $member('virtual_size', sprintf('%.0F', $disk['size'])) . '</struct></value></member></struct></value>';        }        return '<?xml version="1.0" ?><value><struct><member><name>version</name><value><struct>' . $member('hostname', 'xcp-ng')            . $member('product_version', '8.3.0') . $member('xapi_major', '24') . $member('export_vsn', '2')            . '</struct></value></member><member><name>objects</name><value><array><data>' . $objects . '</data></array></value></member></struct></value>';    }    /**     * What ova.xml says of the VM (name_label, name_description, memory_static_max, VCPUs_max, disks: [[name, virtual_size]]), or null     * when it is not an XVA. $head: the first bytes of the file (ova.xml is the first member: a few kilobytes).     *     * @return array<string, mixed>|null     */    public static function readHeader(string $head): ?array    {        if (str_starts_with($head, "\x1f\x8b") && function_exists('inflate_init')) {            // an export made with use_compression: the start of the gzip stream is enough for ova.xml            $inflate = inflate_init(ZLIB_ENCODING_GZIP);            $head = $inflate === false ? '' : (string)@inflate_add($inflate, $head, ZLIB_SYNC_FLUSH);        }        if (strlen($head) < 512 || rtrim(substr($head, 0, 100), "\0") !== 'ova.xml') {            return null;        }        $size = (int)octdec(trim(substr($head, 124, 12), "\0 "));        $xml = substr($head, 512, $size);        $objects = preg_split('#<member><name>class</name>#', $xml) ?: [];        $out = null;        $disks = [];        foreach (array_slice($objects, 1) as $object) {            $class = preg_match('#^<value>([^<]*)</value>#', $object, $m) ? $m[1] : '';            $field = fn (string $name): ?string => preg_match('#<member><name>' . $name . '</name><value>(?:<string>)?([^<]*)#', $object, $f) ? html_entity_decode($f[1], ENT_XML1) : null;            if ($class === 'VM' && $out === null) {                $out = ['name_label' => (string)$field('name_label'), 'name_description' => (string)$field('name_description'),                    'memory_static_max' => (float)$field('memory_static_max'), 'VCPUs_max' => (int)$field('VCPUs_max')];            } elseif ($class === 'VDI') {                $disks[] = [(string)$field('name_label'), (float)$field('virtual_size')];            }        }        if ($out === null) {            return null;        }        return $out + ['disks' => $disks];    }    /** The first bytes of a file, enough for readHeader() (ova.xml stays small). */    public static function headOf(string $file, int $bytes = 262144): string    {        $handle = @fopen($file, 'rb');        if ($handle === false) {            return '';        }        $head = (string)fread($handle, $bytes);        fclose($handle);        return $head;    }}