From e8955ff103d11743fb43a78ca53198fdade705d7 Mon Sep 17 00:00:00 2001 From: Austin Kregel <5355937+austinkregel@users.noreply.github.com> Date: Sun, 12 May 2024 21:13:14 -0400 Subject: [PATCH] Initial commit --- .gitattributes | 13 + .gitignore | 9 + CHANGELOG.md | 32 +++ CODE_OF_CONDUCT.md | 74 ++++++ CONTRIBUTING.md | 32 +++ LICENSE.md | 21 ++ README.md | 82 ++++++ composer.json | 44 ++++ phpunit.xml.dist | 23 ++ src/LibvirtBasementServiceProvider.php | 22 ++ src/LibvirtService.php | 312 +++++++++++++++++++++++ src/Models/Server.php | 13 + tests/AbstractTestCase.php | 10 + tests/Integration/LibvirtServiceTest.php | 72 ++++++ 14 files changed, 759 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/LibvirtBasementServiceProvider.php create mode 100644 src/LibvirtService.php create mode 100644 src/Models/Server.php create mode 100644 tests/AbstractTestCase.php create mode 100644 tests/Integration/LibvirtServiceTest.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e991b7f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.github/PULL_REQUEST_TEMPLATE.md export-ignore +/.github/ISSUE_TEMPLATE.md export-ignore +/phpcs.xml.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore +/docs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f16734 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +build +composer.lock +vendor +phpcs.xml +phpunit.xml +.phpunit.result.cache +node_modules +npm-debug.log +*.log diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3971c27 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +All notable changes to `basement-common` will be documented in this file. + +Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. + +## NEXT - YYYY-MM-DD + +### Added + +#### v0.1.0 + - Updated requirements to >= Laravel 10 + - Rearranged .github + + + +#### v0.0.8 + +- Initial interfaces for domain and server services. +- Initial abstract classes for domains, regions, servers, sizes, and sshkeys. + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Removed +- Nothing + +### Security +- Nothing diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5557916 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at `austin@kregel.co`. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9233077 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [Github](https://github.com/austinkregel/basement-common). + + +## Pull Requests + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **Create feature branches** - Don't ask us to pull from your main branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + + +## Running Tests + +``` bash +$ composer test +``` + + +**Happy coding**! diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..82209c5 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2024 Austin Kregel <5355937+austinkregel@users.noreply.github.com> + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ffc714f --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# basement-common + +[![Latest Version on Packagist][ico-version]][link-packagist] +[![Software License][ico-license]](LICENSE.md) +[![Total Downloads][ico-downloads]][link-downloads] + +This package is a basic QEMU management interface using the interfaces from [the-basement/common](https://packagist.org/packages/the-basement/common). + +## Install + +Via Composer + +``` bash +$ composer require the-basement/libvirt +``` + +## Basic Usage + +Creating a VM in QEMU/KVM + +```php +// This assumes you have an ubuntu server image available from your KVM host +// This also assumes the default image location of the disks created by KVM. +// Both of these can be changed; disks that exist will not be overwritten +// disks that don't exist will be created. +$service = new TheBasement\Libvirt\LibvirtService(); +$service->createServer([ + 'name' => 'my-virtual-machine', + 'memory' => (string) (1024 * 1024), // 1G in KiB + 'cores' => 1, + 'threads' => 1, + 'iso_path' => '/var/lib/libvirt/iso/ubuntu-22.04.4-live-server-amd64.iso', + 'storage_pool' => 'default', + 'network_mac' => '', + 'video_ram' => '65536', // bytes of video ram + 'disk_path' => '/var/lib/libvirt/images/ubuntu22.04-2.qcow2', + 'disk_name' => 'ubuntu22.04-2.qcow2', + 'disk_capacity' => 10 * 1024 * 1024 * 1024, // 10 GB in bytes +]); + +// Gets all servers defined for the KVM +$servers = $service->findAllServers(); +``` + + + +## Change log + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Testing + +``` bash +$ composer test +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details. + +## Security + +If you discover any security related issues, please email security@austinkregel.com instead of using the issue tracker. + +## Credits + +- [Austin Kregel][link-author] +- [All Contributors][link-contributors] + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +[ico-version]: https://img.shields.io/packagist/v/the-basement/libvirt.svg?style=flat-square +[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square +[ico-code-quality]: https://img.shields.io/scrutinizer/g/the-basement/libvirt.svg?style=flat-square +[ico-downloads]: https://img.shields.io/packagist/dt/the-basement/libvirt.svg?style=flat-square + +[link-packagist]: https://packagist.org/packages/the-basement/libvirt +[link-downloads]: https://packagist.org/packages/the-basement/libvirt +[link-author]: https://github.com/austinkregel +[link-contributors]: ../../contributors diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..86b35dd --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name": "the-basement/libvirt", + "type": "library", + "description": "This package implements the domain service contract for the libvirt hypervisor.", + "keywords": [ + "thebasement.club", + "libvirt", + "hypervisor", + "kvm", + "qemu", + "virtualization" + ], + "homepage": "https://github.com/The-Basement-Club/basement-libvirt", + "license": "MIT", + "authors": [ + { + "name": "Austin Kregel", + "email": "5355937+austinkregel@users.noreply.github.com", + "homepage": "https://austinkregel.com", + "role": "Developer" + } + ], + "require": { + "php": ">=8.2", + "the-basement/common": "^0.1.1" + }, + "autoload": { + "psr-4": { + "TheBasement\\Libvirt\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "TheBasement\\Libvirt\\Tests\\": "tests" + } + }, + "config": { + "sort-packages": true + }, + "require-dev": { + "laravel/pint": "^1.15", + "phpunit/phpunit": "^11.1" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..f4034ae --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,23 @@ + + + + + + + + + + + + tests + + + + + + + + src/ + + + diff --git a/src/LibvirtBasementServiceProvider.php b/src/LibvirtBasementServiceProvider.php new file mode 100644 index 0000000..7e47c4a --- /dev/null +++ b/src/LibvirtBasementServiceProvider.php @@ -0,0 +1,22 @@ +app->make(ServerServiceFactory::class) + ->register('libvirt', LibvirtService::class); + } + + public function boot() + { + // + } +} \ No newline at end of file diff --git a/src/LibvirtService.php b/src/LibvirtService.php new file mode 100644 index 0000000..a52cf2f --- /dev/null +++ b/src/LibvirtService.php @@ -0,0 +1,312 @@ +resource = libvirt_connect($host, false); + } + + public function createServer(array $config): Serverlike + { + // Create the root element + $domain = new \SimpleXMLElement(''); + $domain->addAttribute('type', 'kvm'); + + // Add child elements + $domain->addChild('name', $config['name']); + $domain->addChild('memory', (string) $config['memory']); + if (isset($config['vcpu'])) { + $domain->addChild('vcpu', (string) ($config['vcpu'])); + } + + $os = $domain->addChild('os'); + $type = $os->addChild('type', 'hvm'); + $type->addAttribute('arch', 'x86_64'); + $type->addAttribute('machine', 'pc'); + + $os->addChild('boot') + ->addAttribute('dev', 'cdrom'); + + $features = $domain->addChild('features'); + $features->addChild('acpi'); + $features->addChild('apic'); + + $cpu = $domain->addChild('cpu'); + + $cpu->addAttribute('mode', 'host-passthrough'); + $cpu->addAttribute('check', 'none'); + $cpu->addAttribute('migratable', 'on'); + + $clock = $domain->addChild('clock'); + $clock->addAttribute('offset', 'utc'); + + $catchUpTimer = $clock->addChild('timer'); + $catchUpTimer->addAttribute('name', 'rtc'); + $catchUpTimer->addAttribute('tickpolicy', 'catchup'); + + $delayTimer = $clock->addChild('timer'); + $delayTimer->addAttribute('name', 'pit'); + $delayTimer->addAttribute('tickpolicy', 'delay'); + + if (isset($config['cores']) || isset($config['threads'])) { + $topology = $cpu->addChild('topology'); + $topology->addAttribute('sockets', '1'); + $topology->addAttribute('cores', (string) ($config['cores'] ?? 1)); + $topology->addAttribute('threads', (string) ($config['threads'] ?? 1)); + } + +// $domain->addChild('on_poweroff', 'destroy'); + $domain->addChild('on_reboot', 'restart'); +// $domain->addChild('on_crash', 'destroy'); + $pm = $domain->addChild('pm'); + $pm->addChild('suspend-to-mem')->addAttribute('enabled', 'no'); + $pm->addChild('suspend-to-disk')->addAttribute('enabled', 'no'); + + // Devices + $devices = $domain->addChild('devices'); + + // Add our disk + $disk = $devices->addChild('disk'); + $disk->addAttribute('type', 'file'); + $disk->addAttribute('device', 'disk'); + + $driver = $disk->addChild('driver'); + $driver->addAttribute('name', 'qemu'); + $driver->addAttribute('type', 'raw'); + $driver->addAttribute('discard', 'unmap'); + $disk->addChild('source')->addAttribute('file', $config['disk_path']); + $target = $disk->addChild('target'); + $target->addAttribute('dev', 'vda'); + $target->addAttribute('bus', 'virtio'); + + if (isset($config['iso_path'])) { + // Add our ISO as a CD + $cdrom = $devices->addChild('disk'); + $cdrom->addAttribute('type', 'file'); + $cdrom->addAttribute('device', 'cdrom'); + $iso = $cdrom->addChild('driver'); + $iso->addAttribute('name', 'qemu'); + $iso->addAttribute('type', 'raw'); + $cdrom->addChild('source')->addAttribute('file', $config['iso_path']); + $target = $cdrom->addChild('target'); + $target->addAttribute('dev', 'hda'); + $target->addAttribute('bus', 'sata'); + + $cdrom->addChild('readonly'); + } + + $graphics = $devices->addChild('graphics'); + $graphics->addAttribute('type', 'vnc'); + $graphics->addAttribute('port', '-1'); + $graphics->addAttribute('autoport', 'yes'); + + // Convert SimpleXMLElement object to XML string + + // Define the storage for the VM + $storagePool = libvirt_storagepool_lookup_by_name($this->resource, $config['storage_pool']); + $volume = $devices->addChild('volume'); + $volume->addChild('name', $config['disk_name']); + $volume->addChild('target')->addChild('path', $config['disk_path']); + $volume->addChild('capacity', (string) $config['disk_capacity']); // In KiB + + // This will provision the drive for us on the storage + libvirt_storagevolume_create_xml($storagePool, $volume->asXML()); + + $virtIoController = $devices->addChild('controller'); + $virtIoController->addAttribute('type', 'virtio-serial'); + $virtIoController->addAttribute('index', '0'); + + $interface = $devices->addChild('interface'); + $interface->addAttribute('type', 'network'); + // Dynamically set the mac? + if (isset($config['network_mac'])) { + $interface->addChild('mac')->addAttribute('address', $config['network_mac']); + } + $interface->addChild('source')->addAttribute('network', 'default'); + $interface->addChild('model')->addAttribute('type', 'virtio'); + + $console = $devices->addChild('console'); + $console->addAttribute('type', 'pty'); + $target = $console->addChild('target'); + $target->addAttribute('type', 'serial'); + $target->addAttribute('port', '0'); + + $channel = $devices->addChild('channel'); + $channel->addAttribute('type', 'unix'); + $target = $channel->addChild('target'); + $target->addAttribute('type', 'virtio'); + $target->addAttribute('name', 'org.qemu.guest_agent.0'); + + $graphics = $devices->addChild('graphics'); + $graphics->addAttribute('type', 'spice'); + $graphics->addAttribute('autoport', 'yes'); + + $graphics->addChild('listen')->addAttribute('type', 'address'); + $graphics->addChild('image')->addAttribute('compression', 'off'); + + $audio = $devices->addChild('audio'); + $audio->addAttribute('type', 'spice'); + $audio->addAttribute('id', '1'); + + $video = $devices->addChild('video'); + $model = $video->addChild('model'); + $model->addAttribute('type', 'qxl'); + $model->addAttribute('ram', $config['video_ram'] ?? '65536'); + $model->addAttribute('heads', '1'); + $model->addAttribute('primary', 'yes'); + + $devices->addChild('memballoon')->addAttribute('model', 'virtio'); + + $rng = $devices->addChild('rng'); + $rng->addAttribute('model', 'virtio'); + $rng->addChild('backend', '/dev/urandom')->addAttribute('model', 'random'); + + $keyboard = $devices->addChild('input'); + $keyboard->addAttribute('type', 'keyboard'); + $keyboard->addAttribute('bus', 'usb'); + + $mouse = $devices->addChild('input'); + $mouse->addAttribute('type', 'mouse'); + $mouse->addAttribute('bus', 'usb'); + + libvirt_domain_define_xml($this->resource, $domain->asXML()); + libvirt_domain_create(libvirt_domain_lookup_by_name($this->resource, $config['name'])); + // libvirt_domain_define_xml will --- Will save VM + return new Server(json_decode(json_encode($domain), true)); + } + + public function findAllRegions(): array + { + return [ + 'default' + ]; + } + + public function findAllSizes(): array + { + return libvirt_connect_get_machine_types($this->resource); + } + + public function findAllServers(): array + { + $vmNamesThatAreOn = libvirt_list_active_domains($this->resource); + + $vms = libvirt_connect_get_all_domain_stats($this->resource); + + $formattedArray = []; + foreach ($vms as $key => $vm) { + foreach ($vm as $k => $v) { + $formattedArray = Arr::add($formattedArray, $k, $v); + } + } + + return [ + 'data' => array_map(fn ($data, $key) => array_merge( + [ + 'id' => libvirt_domain_get_id($resource = libvirt_domain_lookup_by_name($this->resource, $key)), + 'name' => $key, + 'on' => in_array($key, $vmNamesThatAreOn), + 'state' => match ($data['state.state']) { + VIR_DOMAIN_NOSTATE => 'no state', + VIR_DOMAIN_RUNNING => 'running', + VIR_DOMAIN_BLOCKED => 'blocked', + VIR_DOMAIN_PAUSED => 'paused', + VIR_DOMAIN_SHUTDOWN => 'shutdown', + VIR_DOMAIN_SHUTOFF => 'shutoff', + VIR_DOMAIN_CRASHED => 'crashed', + VIR_DOMAIN_PMSUSPENDED => 'suspended', + }, + // bytes + 'memory' => ($info = libvirt_domain_get_info($resource))['memory'], + 'cpu_usage' => $info['cpuUsed'], + 'cpus' => $info['nrVirtCpu'], + 'disk' => array_values(array_filter($formattedArray['block'], 'is_array')), + ], + isset($formattedArray['balloon']) ? ['balloon' => $formattedArray['balloon']] : [], + isset($formattedArray['net']) ? ['net' => array_values(array_filter($formattedArray['net'], 'is_array'))] : [], + ), $vms, array_keys($vms)), + 'hypervisor' => libvirt_connect_get_information($this->resource), + 'storage_pool' => array_map(fn ($storagePoolName) => libvirt_storagepool_get_info(libvirt_storagepool_lookup_by_name($this->resource, $storagePoolName)), libvirt_list_storagepools($this->resource)), + ]; + } + + public function deleteServer(mixed $identifier): void + { + // We want to destroy all associated libvirt resources + $domain = libvirt_domain_lookup_by_name($this->resource, (string) $identifier); + // Lookup snapshots, disks, nic, etc and remove or destroy them + $disks = libvirt_domain_get_disk_devices($domain); + + // Attempt to shutdown the domain + libvirt_domain_shutdown($domain); + + // Wait for the domain to shut down + while (($info = libvirt_domain_get_info($domain)) && $info['state'] != VIR_DOMAIN_SHUTOFF) { + // Sleep for a bit to prevent high CPU usage + sleep(1); + } + + foreach ($disks as $disk) { + libvirt_domain_detach_device($domain, $disk); + libvirt_domain_disk_remove($domain, $disk); + sleep(1); + } + + libvirt_domain_destroy($domain); + libvirt_domain_undefine($domain); + } + + public function powerOnServer(int|string $identifier): void + { + $domain = libvirt_domain_lookup_by_name($this->resource, (string) $identifier); + libvirt_domain_create($domain); + } + + public function powerOffServer(int|string $identifier): void + { + $domain = libvirt_domain_lookup_by_name($this->resource, (string) $identifier); + libvirt_domain_shutdown($domain); + } + + public function shutdownServer(int|string $identifier): void + { + $domain = libvirt_domain_lookup_by_name($this->resource, (string) $identifier); + libvirt_domain_shutdown($domain); + } + + public function rebootServer(int|string $identifier): void + { + $domain = libvirt_domain_lookup_by_name($this->resource, $identifier); + libvirt_domain_reboot($domain); + } + + public function findAllSshkeys(): array + { + throw new NotImplementedException('SSH Key management is not supported'); + } + + public function createServerKey(array $config): SshKeylike + { + throw new NotImplementedException('SSH Key management is not supported'); + } + + public function removeServerKey($identifier): void + { + throw new NotImplementedException('SSH Key management is not supported'); + } +} diff --git a/src/Models/Server.php b/src/Models/Server.php new file mode 100644 index 0000000..80e62d4 --- /dev/null +++ b/src/Models/Server.php @@ -0,0 +1,13 @@ +libvirtService = new LibvirtService(); + } + + public function testCreateServer(): void + { + $config = [ + 'name' => 'myvm', + 'memory' => 1024, + 'cores' => 1, + 'threads' => 0, + 'disk_path' => '/path/to/disk.img', + 'iso_path' => '/path/to/iso.iso', + 'disk_name' => 'mydisk', + ]; + + $server = $this->libvirtService->createServer($config); + + $this->assertArrayHasKey('id', $server); + $this->assertArrayHasKey('name', $server); + $this->assertArrayHasKey('on', $server); + $this->assertArrayHasKey('state', $server); + $this->assertArrayHasKey('memory', $server); + $this->assertArrayHasKey('cpu_usage', $server); + $this->assertArrayHasKey('cpus', $server); + $this->assertArrayHasKey('disk', $server); + $this->assertArrayHasKey('balloon', $server); + $this->assertArrayHasKey('net', $server); + } + + public function testFindAllRegions(): void + { + $regions = $this->libvirtService->findAllRegions(); + + $this->assertIsArray($regions); + $this->assertNotEmpty($regions); + } + + public function testFindAllSizes(): void + { + $sizes = $this->libvirtService->findAllSizes(); + + $this->assertIsArray($sizes); + $this->assertNotEmpty($sizes); + } + + public function testFindAllServers(): void + { + $servers = $this->libvirtService->findAllServers(); + + $this->assertIsArray($servers); + $this->assertNotEmpty($servers); + $this->assertArrayHasKey('data', $servers); + } +} \ No newline at end of file