├── .styleci.yml ├── src ├── Exception │ ├── ServerException.php │ ├── OperationException.php │ ├── InvalidEndpointException.php │ ├── ClientConnectionException.php │ ├── SourceImageException.php │ ├── NotFoundException.php │ ├── ConflictException.php │ ├── AuthenticationFailedException.php │ └── BadRequestException.php ├── Endpoint │ ├── Containers.php │ ├── VirtualMachines.php │ ├── Instances.php │ ├── Resources.php │ ├── Storage │ │ ├── Resources.php │ │ └── Volumes.php │ ├── Warnings │ │ └── Status.php │ ├── Cluster.php │ ├── Cluster │ │ └── Members.php │ ├── Warnings.php │ ├── Instance │ │ ├── Logs.php │ │ ├── Files.php │ │ ├── Backups.php │ │ └── Snapshots.php │ ├── Storage.php │ ├── Host.php │ ├── Networks.php │ ├── Operations.php │ ├── Projects.php │ ├── Certificates.php │ ├── Images │ │ └── Aliases.php │ ├── AbstractEndpoint.php │ ├── Profiles.php │ ├── Images.php │ └── InstaceBase.php ├── HttpClient │ ├── Plugin │ │ ├── PathTrimEnd.php │ │ ├── PathPrepend.php │ │ └── LxdExceptionThrower.php │ └── Message │ │ └── ResponseMediator.php └── Client.php ├── .editorconfig ├── docs ├── host.md ├── networks.md ├── operations.md ├── certificates.md ├── profiles.md ├── images.md ├── configuration.md └── containers.md ├── phpunit.xml ├── LICENSE.md ├── CONTRIBUTING.md ├── README.md ├── composer.json ├── CONDUCT.md └── CHANGELOG.md /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | -------------------------------------------------------------------------------- /src/Exception/ServerException.php: -------------------------------------------------------------------------------- 1 | client->hasVms() ? "/instances/" : "/containers/"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Endpoint/Resources.php: -------------------------------------------------------------------------------- 1 | get($this->getEndpoint()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/host.md: -------------------------------------------------------------------------------- 1 | ### LXD server information 2 | 3 | > NOTE: If you haven't setup your LXD server, read [configuration.md](configuration.md) 4 | 5 | To get information on the LXD server: 6 | 7 | ``` 8 | host->info(); 11 | ``` 12 | 13 | Test if this client is trusted by the LXD server 14 | 15 | ``` 16 | host->trusted()) { 19 | echo 'trusted'; 20 | } else { 21 | echo 'not trusted'; 22 | } 23 | 24 | ``` 25 | -------------------------------------------------------------------------------- /src/Endpoint/Storage/Resources.php: -------------------------------------------------------------------------------- 1 | get($this->getEndpoint().$name.'/resources'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/networks.md: -------------------------------------------------------------------------------- 1 | ### Networks 2 | 3 | > NOTE: If you haven't setup your LXD server, read [configuration.md](configuration.md) 4 | 5 | Get list of networks: 6 | 7 | ``` 8 | networks->all(); 11 | ``` 12 | 13 | Get network information: 14 | 15 | ``` 16 | networks->info('lxdbr0'); 19 | ``` 20 | 21 | > TODO: 22 | > - define a new network 23 | > - replace the network information 24 | > - update the network information 25 | > - rename a network 26 | > - remove a network 27 | -------------------------------------------------------------------------------- /src/Exception/NotFoundException.php: -------------------------------------------------------------------------------- 1 | message, $request, $response, $previous); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exception/ConflictException.php: -------------------------------------------------------------------------------- 1 | message, $request, $response, $previous); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/operations.md: -------------------------------------------------------------------------------- 1 | ### Operations 2 | 3 | > NOTE: If you haven't setup your LXD server, read [configuration.md](configuration.md) 4 | 5 | Get all operations: 6 | 7 | ``` 8 | operations->all(); 11 | ``` 12 | 13 | Get operation information: 14 | 15 | ``` 16 | operations->info($uuid); 19 | ``` 20 | 21 | Cancel an operation: 22 | 23 | ``` 24 | operations->cancel($uuid); 27 | ``` 28 | 29 | Wait for an operation to finish: 30 | 31 | ``` 32 | operations->wait($uuid, $timeout); 35 | ``` 36 | -------------------------------------------------------------------------------- /src/Exception/AuthenticationFailedException.php: -------------------------------------------------------------------------------- 1 | message, $request, $response, $previous); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Endpoint/Warnings/Status.php: -------------------------------------------------------------------------------- 1 | "new"]; 17 | return $this->put($this->getEndpoint() . $uuid, $data); 18 | } 19 | 20 | public function acknowledge(string $uuid) 21 | { 22 | $data = ["status"=>"acknowledged"]; 23 | return $this->put($this->getEndpoint() . $uuid, $data); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/certificates.md: -------------------------------------------------------------------------------- 1 | ### Certificates 2 | 3 | > NOTE: If you haven't setup your LXD server, read [configuration.md](configuration.md) 4 | 5 | List of trusted certificates: 6 | 7 | ``` 8 | certificates->all(); 11 | ``` 12 | 13 | Add a new trusted certificate: 14 | 15 | ``` 16 | certificates->add(file_get_contents(__DIR__.'/client.pem'), 'Super secret password'); 19 | ``` 20 | 21 | Get trusted certificate information: 22 | 23 | ``` 24 | certificates->info($fingerprint); 27 | ``` 28 | 29 | Remove trusted certificate: 30 | 31 | ``` 32 | certificates->remove('xxxxxxxx'); 35 | ``` 36 | -------------------------------------------------------------------------------- /src/Exception/BadRequestException.php: -------------------------------------------------------------------------------- 1 | getBody()->getContents(), true); 16 | 17 | $message = json_last_error() !== 0 ? $this->fallbackMessage : $content["error"]; 18 | 19 | parent::__construct($message, $request, $response, $previous); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/HttpClient/Plugin/PathTrimEnd.php: -------------------------------------------------------------------------------- 1 | trim = $trim; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise 28 | { 29 | $trimPath = rtrim($request->getUri()->getPath(), $this->trim); 30 | $uri = $request->getUri()->withPath($trimPath); 31 | $request = $request->withUri($uri); 32 | 33 | return $next($request); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/HttpClient/Plugin/PathPrepend.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class PathPrepend implements Plugin 15 | { 16 | private $path; 17 | 18 | /** 19 | * @param string $path 20 | */ 21 | public function __construct($path) 22 | { 23 | $this->path = $path; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise 30 | { 31 | $currentPath = $request->getUri()->getPath(); 32 | $uri = $request->getUri()->withPath($this->path.$currentPath); 33 | $request = $request->withUri($uri); 34 | 35 | return $next($request); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Endpoint/Cluster.php: -------------------------------------------------------------------------------- 1 | get($this->getEndpoint()); 26 | } 27 | 28 | public function __get($endpoint) 29 | { 30 | $class = __NAMESPACE__.'\\Cluster\\'.ucfirst($endpoint); 31 | 32 | if (class_exists($class)) { 33 | return new $class($this->client); 34 | } else { 35 | throw new InvalidEndpointException( 36 | 'Endpoint '.$class.', not implemented.' 37 | ); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ashley Hood 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/HttpClient/Message/ResponseMediator.php: -------------------------------------------------------------------------------- 1 | getBody()->__toString(); 16 | 17 | if (strpos($response->getHeaderLine('Content-Type'), 'application/json') === 0) { 18 | $content = json_decode($body, true); 19 | 20 | if (json_last_error() === JSON_ERROR_NONE) { 21 | if ($response->getStatusCode() >= 100 && $response->getStatusCode() <= 111) { 22 | return $content; 23 | } 24 | 25 | return $content['metadata']; 26 | } 27 | } 28 | 29 | return $body; 30 | } 31 | 32 | /** 33 | * Get the value for a single header 34 | * @param ResponseInterface $response 35 | * @param string $name 36 | * 37 | * @return string|null 38 | */ 39 | public static function getHeader(ResponseInterface $response, $name) 40 | { 41 | $headers = $response->getHeader($name); 42 | return array_shift($headers); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Endpoint/Cluster/Members.php: -------------------------------------------------------------------------------- 1 | $recursion 23 | ]; 24 | 25 | $members = $this->get($this->getEndpoint(), $config); 26 | if ($recursion == 0) { 27 | foreach ($members as &$member) { 28 | $member = str_replace('/' . $this->client->getApiVersion() . $this->getEndpoint(), '', $member); 29 | } 30 | } 31 | 32 | return $members; 33 | } 34 | 35 | public function info(string $name) 36 | { 37 | return $this->get($this->getEndpoint() . "$name"); 38 | } 39 | 40 | public function rename(string $name, string $newName) 41 | { 42 | return $this->post($this->getEndpoint() . "$name", ["server_name" => $newName]); 43 | } 44 | 45 | public function remove(string $name) 46 | { 47 | return $this->delete($this->getEndpoint() . "$name"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Endpoint/Warnings.php: -------------------------------------------------------------------------------- 1 | $recursion 21 | ]; 22 | 23 | if (!empty($project)) { 24 | $config["project"] = $project; 25 | } 26 | 27 | 28 | return $this->get($this->getEndpoint(), $config); 29 | } 30 | 31 | public function info(string $uuid) 32 | { 33 | return $this->get($this->getEndpoint() . $uuid); 34 | } 35 | 36 | public function remove(string $uuid) 37 | { 38 | return $this->delete($this->getEndpoint() . $uuid); 39 | } 40 | 41 | public function __get($endpoint) 42 | { 43 | $class = __NAMESPACE__.'\\Warnings\\'.ucfirst($endpoint); 44 | 45 | if (class_exists($class)) { 46 | return new $class($this->client); 47 | } else { 48 | throw new InvalidEndpointException( 49 | 'Endpoint '.$class.', not implemented.' 50 | ); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [OSS Gitlab](https://git.oss.place/opensaucesystems/php-lxd). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **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. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /docs/profiles.md: -------------------------------------------------------------------------------- 1 | ### Profiles 2 | 3 | > NOTE: If you haven't setup your LXD server, read [configuration.md](configuration.md) 4 | 5 | Get all profiles: 6 | 7 | ``` 8 | profiles->all(); 11 | ``` 12 | 13 | Get profile information: 14 | 15 | ``` 16 | profiles->info('plan-one'); 19 | ``` 20 | 21 | Create a new profile: 22 | 23 | ``` 24 | "2GB"]; 27 | $devices = [ 28 | "kvm" => [ 29 | "type" => "unix-char", 30 | "path" => "/dev/kvm" 31 | ], 32 | ]; 33 | 34 | $lxd->profiles->create('profile-name', 'Profile description', $config, $devices); 35 | 36 | ``` 37 | 38 | Update profile information: 39 | 40 | ``` 41 | "4GB"]; 44 | $devices = [ 45 | "kvm" => [ 46 | "type" => "unix-char", 47 | "path" => "/dev/kvm" 48 | ], 49 | ]; 50 | 51 | $lxd->profiles->update('profile-name', 'New profile description', $config, $devices); 52 | 53 | ``` 54 | 55 | Replace profile information: 56 | 57 | ``` 58 | "4GB"]; 61 | $devices = [ 62 | "kvm" => [ 63 | "type" => "unix-char", 64 | "path" => "/dev/kvm" 65 | ], 66 | ]; 67 | 68 | $lxd->profiles->replace('profile-name', 'New profile description', $config, $devices); 69 | 70 | ``` 71 | 72 | Rename a profile: 73 | 74 | ``` 75 | profiles->rename('profile-name', 'new-profile-name'); 78 | ``` 79 | 80 | Remove a profile: 81 | 82 | ``` 83 | profiles->remove('profile-name'); 86 | ``` 87 | -------------------------------------------------------------------------------- /src/Endpoint/Instance/Logs.php: -------------------------------------------------------------------------------- 1 | endpoint; 14 | } 15 | 16 | public function setEndpoint(string $endpoint) 17 | { 18 | $this->endpoint = $endpoint; 19 | } 20 | 21 | /** 22 | * List of logs for a container 23 | * 24 | * @param string $name Name of container 25 | * @return array 26 | */ 27 | public function all($name) 28 | { 29 | $logs = []; 30 | 31 | foreach ($this->get($this->getEndpoint().$name.'/logs/') as $log) { 32 | $logs[] = str_replace( 33 | '/'.$this->client->getApiVersion().$this->getEndpoint().$name.'/logs/', 34 | '', 35 | $log 36 | ); 37 | } 38 | 39 | return $logs; 40 | } 41 | 42 | /** 43 | * Get the contents of a particular log file 44 | * 45 | * @param string $name Name of container 46 | * @param string $log Name of log 47 | * @return object 48 | */ 49 | public function read($name, $log) 50 | { 51 | return $this->get($this->getEndpoint().$name.'/logs/'.$log); 52 | } 53 | 54 | /** 55 | * Remove a particular log file 56 | * 57 | * @param string $name Name of container 58 | * @param string $log Name of log 59 | * @return object 60 | */ 61 | public function remove($name, $log) 62 | { 63 | return $this->delete($this->getEndpoint().$name.'/logs/'.$log); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/images.md: -------------------------------------------------------------------------------- 1 | ### Images 2 | 3 | > NOTE: If you haven't setup your LXD server, read [configuration.md](configuration.md) 4 | 5 | Get all images: 6 | 7 | ``` 8 | images->all(); 11 | 12 | ``` 13 | 14 | Get image information: 15 | 16 | ``` 17 | images->info('xxxxxxxx')); 20 | ``` 21 | 22 | Create image from remote LXD image server: 23 | 24 | ``` 25 | images->createFromRemote( 28 | 'https://images.linuxcontainers.org:8443', 29 | [ 30 | 'alias' => 'ubuntu/xenial/amd64', 31 | ] 32 | ); 33 | ``` 34 | 35 | Create image from snapshot: 36 | 37 | ``` 38 | images->createFromSnapshot('container-name', 'snap0'); 41 | ``` 42 | 43 | Remove image: 44 | 45 | ``` 46 | images->remove('xxxxxxxx'); 49 | ``` 50 | 51 | ### Aliases 52 | 53 | > NOTE: If you haven't setup your LXD server, read [configuration.md](configuration.md) 54 | 55 | Get all aliases: 56 | 57 | ``` 58 | images->aliases->all(); 61 | ``` 62 | 63 | Get alias description and target 64 | 65 | ``` 66 | images->aliases->info('ubuntu/xenial/amd64'); 69 | ``` 70 | 71 | Create an alias: 72 | 73 | ``` 74 | images->aliases->create('xxxxxxxx', 'alias-name', 'Alias description'); 77 | ``` 78 | 79 | Replaces the alias target or description: 80 | 81 | ``` 82 | images->aliases->replace('alias-name', 'xxxxxxxx', 'New description'); 85 | ``` 86 | 87 | Rename an alias: 88 | 89 | ``` 90 | images->aliases->rename('alias-name', 'new-alias-name'); 93 | ``` 94 | 95 | Remove alias: 96 | 97 | ``` 98 | images->aliases->remove('ubuntu/xenial/amd64'); 101 | ``` 102 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | ### Setup a LXD for remote access 2 | 3 | On the LXD server you will need to allow access over the network by setting the following configs: 4 | 5 | lxc config set core.https_address [::]:8443 6 | lxc config set core.trust_password 'Super secret password' 7 | 8 | ### Connect to LXD server 9 | 10 | To connect to the LXD server you will need a certificate for the client. 11 | 12 | Here is how to create one in PHP: 13 | 14 | ``` 15 | "UK", 20 | "stateOrProvinceName" => "Isle Of Wight", 21 | "localityName" => "Cowes", 22 | "organizationName" => "Open Sauce Systems", 23 | "organizationalUnitName" => "Dev", 24 | "commonName" => "127.0.0.1", 25 | "emailAddress" => "info@opensauce.systems" 26 | ); 27 | 28 | // Generate certificate 29 | $privkey = openssl_pkey_new(); 30 | $cert = openssl_csr_new($dn, $privkey); 31 | $cert = openssl_csr_sign($cert, null, $privkey, 365); 32 | 33 | // Generate strings 34 | openssl_x509_export($cert, $certString); 35 | openssl_pkey_export($privkey, $privkeyString); 36 | 37 | // Save to file 38 | $pemFile = __DIR__.'/client.pem'; 39 | file_put_contents($pemFile, $certString.$privkeyString); 40 | 41 | ``` 42 | 43 | Once you have a ssl certificate, you can use this to connect to the LXD server: 44 | 45 | ``` 46 | require "vendor/autoload.php"; 47 | 48 | use GuzzleHttp\Client as GuzzleClient; 49 | use Http\Adapter\Guzzle6\Client as GuzzleAdapter; 50 | 51 | $config = [ 52 | 'verify' => false, 53 | 'cert' => [ 54 | __DIR__.'/client.pem', 55 | '' 56 | ] 57 | ]; 58 | 59 | $guzzle = new GuzzleClient($config); 60 | $adapter = new GuzzleAdapter($guzzle); 61 | 62 | $lxd = new \Opensaucesystems\Lxd\Client($adapter); 63 | 64 | $lxd->setUrl('https://lxd.example.com:8443'); 65 | 66 | ``` 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Fork 2 | 3 | This is a fork of https://opensauce.systems project, but with some of the new 4 | api's added. 5 | 6 | Thanks for the work 7 | 8 | # php-lxd 9 | 10 | [![Latest Version on Packagist][ico-version]][link-packagist] 11 | [![Software License][ico-license]](LICENSE.md) 12 | [![Build Status][ico-travis]][link-travis] 13 | [![Total Downloads][ico-downloads]][link-downloads] 14 | 15 | A PHP library for interacting with the LXD REST API. 16 | 17 | ## Install 18 | 19 | Via Composer 20 | 21 | ``` bash 22 | $ composer require dhope0000/lxd 23 | ``` 24 | 25 | ## Usage 26 | 27 | See the [`docs`](./docs) for more information. 28 | 29 | ## Change log 30 | 31 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 32 | 33 | ## Testing 34 | 35 | ``` bash 36 | $ composer test 37 | ``` 38 | 39 | ## Contributing 40 | 41 | Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details. 42 | 43 | ## Security 44 | 45 | If you discover any security related issues, please email ashley@opensauce.systems instead of using the issue tracker. 46 | 47 | ## Credits 48 | 49 | - [Ashley Hood][link-author] 50 | - [All Contributors][link-contributors] 51 | 52 | ## License 53 | 54 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 55 | 56 | [ico-version]: https://img.shields.io/packagist/v/dhope0000/lxd.svg?style=flat-square 57 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 58 | [ico-travis]: https://img.shields.io/travis/ashleyhood/php-lxd/master.svg?style=flat-square 59 | [ico-downloads]: https://img.shields.io/packagist/dt/dhope0000/lxd.svg?style=flat-square 60 | 61 | [link-packagist]: https://packagist.org/packages/dhope0000/lxd 62 | [link-travis]: https://travis-ci.org/ashleyhood/php-lxd 63 | [link-downloads]: https://packagist.org/packages/dhope0000/lxd 64 | [link-author]: https://opensauce.systems 65 | [link-contributors]: ../../contributors 66 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dhope0000/lxd", 3 | "type": "library", 4 | "description": "PHP-based API wrapper for LXD REST API.", 5 | "keywords": [ 6 | "opensaucesystems", 7 | "wrapper", 8 | "api", 9 | "client", 10 | "lxd" 11 | ], 12 | "homepage": "https://git.oss.place/opensaucesystems/lxd", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Ashley Hood", 17 | "email": "ashley@opensauce.systems", 18 | "homepage": "https://www.opensauce.systems", 19 | "role": "Developer" 20 | }, 21 | { 22 | "name": "Daniel Hope", 23 | "email": "dhope0000@gmail.com", 24 | "role": "Developer" 25 | } 26 | ], 27 | "require": { 28 | "php": "^7.2.5|~8.0", 29 | "psr/http-message": "^1.0", 30 | "php-http/httplug": "^2.4.1", 31 | "php-http/discovery": "^1.20", 32 | "php-http/client-implementation": "^1.0", 33 | "php-http/client-common": "^2.7.2", 34 | "php-http/cache-plugin": "^1.6" 35 | }, 36 | "require-dev": { 37 | "phpunit/phpunit": "4.*", 38 | "mockery/mockery": "^0.9.5", 39 | "php-http/guzzle7-adapter": "^1.0", 40 | "guzzlehttp/psr7": "^1.2", 41 | "php-http/mock-client": "^1.6.1", 42 | "squizlabs/php_codesniffer": "^2.6" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Opensaucesystems\\Lxd\\": "src" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Opensaucesystems\\Lxd\\Tests\\": "tests" 52 | } 53 | }, 54 | "scripts": { 55 | "test": [ 56 | "@test-phpcs", 57 | "@test-phpunit" 58 | ], 59 | "test-phpunit": "phpunit --configuration phpunit.xml", 60 | "test-phpcs": "phpcs -v -s --standard=PSR2 src tests" 61 | }, 62 | "extra": { 63 | "branch-alias": { 64 | "dev-master": "1.0-dev" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Endpoint/Storage.php: -------------------------------------------------------------------------------- 1 | $recursion 16 | ]; 17 | 18 | $pools = $this->get($this->getEndpoint(), $config); 19 | if ($recursion == 0) { 20 | foreach ($pools as &$pool) { 21 | $pool = str_replace('/' . $this->client->getApiVersion() . $this->getEndpoint(), '', $pool); 22 | } 23 | } 24 | return $pools; 25 | } 26 | 27 | public function info(string $name) 28 | { 29 | return $this->get($this->getEndpoint() . $name); 30 | } 31 | 32 | public function create(string $name, string $driver, array $config) 33 | { 34 | $pool = [ 35 | "name" => $name, 36 | "driver" => $driver, 37 | "config" => $config 38 | ]; 39 | 40 | if (empty($config)) { 41 | unset($pool["config"]); 42 | } 43 | 44 | return $this->post($this->getEndpoint(), $pool); 45 | } 46 | 47 | public function replace(string $name, array $config) 48 | { 49 | return $this->put($this->getEndpoint() . $name, ["config" => $config]); 50 | } 51 | 52 | public function update(string $name, array $config) 53 | { 54 | return $this->patch($this->getEndpoint() . $name, ["config" => $config]); 55 | } 56 | 57 | public function remove(string $name) 58 | { 59 | return $this->delete($this->getEndpoint() . $name); 60 | } 61 | 62 | public function __get($endpoint) 63 | { 64 | $class = __NAMESPACE__ . '\\Storage\\' . ucfirst($endpoint); 65 | 66 | if (class_exists($class)) { 67 | return new $class($this->client); 68 | } else { 69 | throw new InvalidEndpointException( 70 | 'Endpoint ' . $class . ', not implemented.' 71 | ); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/HttpClient/Plugin/LxdExceptionThrower.php: -------------------------------------------------------------------------------- 1 | then( 27 | function (ResponseInterface $response) { 28 | // Successful response, just return it. 29 | return $response; 30 | }, 31 | function (\Throwable $e) use ($request) { 32 | if ($e instanceof HttpException) { 33 | $response = $e->getResponse(); 34 | $status = $response->getStatusCode(); 35 | 36 | switch ($status) { 37 | case 400: 38 | throw new BadRequestException($request, $response, $e); 39 | case 401: 40 | throw new OperationException($request, $response, $e); 41 | case 403: 42 | throw new AuthenticationFailedException($request, $response, $e); 43 | case 404: 44 | throw new NotFoundException($request, $response, $e); 45 | case 409: 46 | throw new ConflictException($request, $response, $e); 47 | } 48 | } 49 | 50 | // Rethrow unhandled exceptions 51 | throw $e; 52 | } 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | * Other unethical or unprofessional conduct. 15 | 16 | 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. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community in a direct capacity. Personal views, beliefs and values of individuals do not necessarily reflect those of the organisation or affiliated individuals and organisations. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 21 | 22 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) 23 | -------------------------------------------------------------------------------- /src/Endpoint/Storage/Volumes.php: -------------------------------------------------------------------------------- 1 | get($this->getEndpoint() . $pool . '/volumes'); 17 | } 18 | 19 | /** 20 | * $path for /1.0/storage-pools/default/volumes/custom/test would be custom/test 21 | */ 22 | public function info(string $pool, string $path) 23 | { 24 | $config = [ 25 | "project" => $this->client->getProject() 26 | ]; 27 | 28 | return $this->get($this->getEndpoint() . $pool . '/volumes/' . $path, $config); 29 | } 30 | 31 | public function create(string $pool, string $name, array $config) 32 | { 33 | $opts['name'] = $name; 34 | $opts["config"] = $config; 35 | 36 | $httpConfig = [ 37 | "project" => $this->client->getProject() 38 | ]; 39 | 40 | return $this->post($this->getEndpoint() . $pool . '/volumes/custom', $opts, $httpConfig); 41 | } 42 | 43 | public function createCustomVolumeFromFile(string $pool, string $name, $fileContents, string $type = "iso", bool $wait = false) 44 | { 45 | $headers = [ 46 | "Content-Type" => "application/octet-stream", 47 | "X-LXD-name" => $name, 48 | "X-LXD-type" => $type 49 | ]; 50 | $queryParams = ["project" => $this->client->getProject()]; 51 | 52 | $response = $this->post($this->getEndpoint() . $pool . '/volumes/custom', $fileContents, $queryParams, $headers); 53 | 54 | if ($wait) { 55 | $response = $this->client->operations->wait($response['id']); 56 | } 57 | 58 | return $response; 59 | } 60 | 61 | public function remove(string $pool, string $name) 62 | { 63 | $httpConfig = [ 64 | "project" => $this->client->getProject() 65 | ]; 66 | 67 | return $this->delete($this->getEndpoint() . $pool . '/volumes/' . $name, [], $httpConfig); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Endpoint/Host.php: -------------------------------------------------------------------------------- 1 | client = $client; 21 | } 22 | 23 | /** 24 | * Server configuration and environment information 25 | * 26 | * @return object 27 | */ 28 | public function info() 29 | { 30 | return $this->get($this->getEndpoint()); 31 | } 32 | 33 | /** 34 | * Does the server trust the client 35 | * 36 | * @return bool 37 | */ 38 | public function trusted() 39 | { 40 | $info = $this->info(); 41 | 42 | return $info['auth'] === 'trusted' ? true : false; 43 | } 44 | 45 | /** 46 | * Updates the server configuration or other properties 47 | * 48 | * Example: Change trust password 49 | * $info = $lxd->info(); 50 | * $info['config']['core.trust_password'] = "my-new-password"; 51 | * $lxd->update($config); 52 | * 53 | * @param object $config replaces any existing config with the provided one 54 | * @return object 55 | */ 56 | // public function update($config) 57 | // { 58 | // $data['config'] = $config; 59 | // $response = $this->patch($this->getEndpoint(), $config); 60 | 61 | // return $this->info(); 62 | // } 63 | 64 | /** 65 | * Replaces the server configuration or other properties 66 | * 67 | * Example: Change image updates 68 | * $info = $lxd->info(); 69 | * $info['config']['images.auto_update_interval'] = '24'; 70 | * $lxd->update($info['config']); 71 | * 72 | * @param object $config replaces any existing config with the provided one 73 | * @return 74 | */ 75 | public function replace($config) 76 | { 77 | $data['config'] = $config; 78 | $response = $this->put($this->getEndpoint(), $data); 79 | 80 | return $this->info(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Endpoint/Networks.php: -------------------------------------------------------------------------------- 1 | $this->client->getProject(), 23 | "recursion" => $recursion 24 | ]; 25 | 26 | $networks = $this->get($this->getEndpoint(), $config); 27 | 28 | if ($recursion == 0) { 29 | foreach ($networks as &$network) { 30 | $network = str_replace('/' . $this->client->getApiVersion() . $this->getEndpoint(), '', $network); 31 | } 32 | } 33 | 34 | return $networks; 35 | } 36 | 37 | /** 38 | * Show information on a network 39 | * 40 | * @param string $name name of network 41 | * @return object 42 | */ 43 | public function info($name) 44 | { 45 | $config = [ 46 | "project" => $this->client->getProject() 47 | ]; 48 | 49 | return $this->get($this->getEndpoint() . $name, $config); 50 | } 51 | 52 | /** 53 | * Create a network 54 | * 55 | * @param string $name name of network 56 | * @param array $config configuration of the network (Optional) 57 | * @return object 58 | */ 59 | public function create(string $name, string $description = "", array $config = [], $type = "") 60 | { 61 | $data = []; 62 | 63 | $data["name"] = $name; 64 | $data["description"] = $description; 65 | if (!empty($config)) { 66 | $data["config"] = $config; 67 | } 68 | $data["type"] = $type; 69 | 70 | $config = [ 71 | "project" => $this->client->getProject() 72 | ]; 73 | 74 | return $this->post($this->getEndpoint(), $data, $config); 75 | } 76 | 77 | /** 78 | * Delete network 79 | * @param string $name name of network 80 | * @return object 81 | */ 82 | public function remove($name) 83 | { 84 | $config = [ 85 | "project" => $this->client->getProject() 86 | ]; 87 | 88 | return $this->delete($this->getEndpoint() . $name, $config); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Endpoint/Operations.php: -------------------------------------------------------------------------------- 1 | $this->client->getProject() 27 | ]; 28 | 29 | foreach ($this->get($this->getEndpoint(), $config) as $key => $operation) { 30 | $operations[$key] = str_replace('/'.$this->client->getApiVersion().$this->getEndpoint(), '', $operation); 31 | } 32 | 33 | return $operations; 34 | } 35 | 36 | /** 37 | * Get information on a certificate 38 | * 39 | * @param string $uuid UUID of background operation 40 | * @return array 41 | */ 42 | public function info($uuid) 43 | { 44 | $config = [ 45 | "project"=>$this->client->getProject() 46 | ]; 47 | 48 | return $this->get($this->getEndpoint().$uuid, $config); 49 | } 50 | 51 | /** 52 | * Cancel an operation 53 | * 54 | * Calling this will change the state to "cancelling" 55 | * rather than actually removing the entry 56 | * 57 | * @param string $uuid UUID of background operation 58 | */ 59 | public function cancel($uuid) 60 | { 61 | $config = [ 62 | "project"=>$this->client->getProject() 63 | ]; 64 | 65 | return $this->delete($this->getEndpoint().$uuid, $config); 66 | } 67 | 68 | /** 69 | * Wait for an operation to finish 70 | * 71 | * @param string $uuid UUID of background operation 72 | * @param int $timeout Max time to wait 73 | * @return array 74 | */ 75 | public function wait($uuid, ?int $timeout = null) 76 | { 77 | $config = [ 78 | "project"=>$this->client->getProject() 79 | ]; 80 | 81 | $endpoint = $this->getEndpoint().$uuid.'/wait'; 82 | 83 | if (is_numeric($timeout) && $timeout > 0) { 84 | $config['timeout'] = $timeout; 85 | } 86 | 87 | return $this->get($endpoint, $config); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Endpoint/Instance/Files.php: -------------------------------------------------------------------------------- 1 | endpoint; 14 | } 15 | 16 | public function setEndpoint(string $endpoint) 17 | { 18 | $this->endpoint = $endpoint; 19 | } 20 | 21 | /** 22 | * Read the contents of a file in a container 23 | * 24 | * @param string $name Name of container 25 | * @param string $filepath Full path to a file within the container 26 | * @return object 27 | */ 28 | public function read($name, $filepath) 29 | { 30 | $config = [ 31 | "project"=>$this->client->getProject(), 32 | "path"=>$filepath 33 | ]; 34 | 35 | return $this->get($this->getEndpoint().$name.'/files', $config); 36 | } 37 | 38 | /** 39 | * Write to a file in a container 40 | * 41 | * 42 | * @param string $name Name of container 43 | * @param string $filepath Path to the output file in the container 44 | * @param string $data Data to write to the file 45 | * @return object 46 | */ 47 | public function write($name, $filepath, $data, $uid = null, $gid = null, $mode = null, $type = "file") 48 | { 49 | $headers = []; 50 | 51 | if (is_numeric($uid)) { 52 | $headers['X-LXD-uid'] = $uid; 53 | } 54 | 55 | if (is_numeric($gid)) { 56 | $headers['X-LXD-gid'] = $gid; 57 | } 58 | 59 | if (is_numeric($mode)) { 60 | $headers['X-LXD-mode'] = $mode; 61 | } 62 | 63 | if (is_string($type)) { 64 | $headers['X-LXD-type'] = $type; 65 | } 66 | 67 | $config = [ 68 | "project"=>$this->client->getProject(), 69 | "path"=>$filepath 70 | ]; 71 | 72 | return $this->post($this->getEndpoint().$name.'/files', $data, $config, $headers); 73 | } 74 | 75 | /** 76 | * Delete a file in a container 77 | * 78 | * @param string $name Name of container 79 | * @param string $filepath Full path to a file within the container 80 | * @return object 81 | */ 82 | public function remove($name, $filepath) 83 | { 84 | $config = [ 85 | "project"=>$this->client->getProject(), 86 | "path"=>$filepath 87 | ]; 88 | return $this->delete($this->getEndpoint().$name.'/files', $config); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Endpoint/Projects.php: -------------------------------------------------------------------------------- 1 | $recursion 22 | ]; 23 | $projects = $this->get($this->getEndpoint(), $config); 24 | if ($recursion == 0) { 25 | foreach ($projects as &$project) { 26 | $project = str_replace('/' . $this->client->getApiVersion() . $this->getEndpoint(), '', $project); 27 | $project = str_replace('?project=' . $this->client->getProject(), '', $project); 28 | } 29 | } 30 | 31 | return $projects; 32 | } 33 | 34 | public function create(string $name, string $description = "", array $config = []) 35 | { 36 | $project = []; 37 | $project["name"] = $name; 38 | $project["description"] = $description; 39 | $project["config"] = empty($config) ? $this->defaultProjectConfig() : $config; 40 | 41 | return $this->post($this->getEndpoint(), $project); 42 | } 43 | 44 | public function info(string $name) 45 | { 46 | return $this->get($this->getEndpoint() . $name); 47 | } 48 | 49 | public function replace(string $name, string $description = "", array $config = []) 50 | { 51 | $project = []; 52 | $project["description"] = $description; 53 | $project["config"] = empty($config) ? $this->defaultProjectConfig() : $config; 54 | 55 | return $this->put($this->getEndpoint() . $name, $project); 56 | } 57 | 58 | public function update(string $name, string $description = "", array $config = []) 59 | { 60 | $project = []; 61 | if (!empty($description)) { 62 | $project["description"] = $description; 63 | } 64 | $project["config"] = empty($config) ? $this->defaultProjectConfig() : $config; 65 | return $this->patch($this->getEndpoint() . $name, $project); 66 | } 67 | 68 | public function rename(string $name, string $newName) 69 | { 70 | $config = ["name" => $newName]; 71 | return $this->post($this->getEndpoint() . $name, $config); 72 | } 73 | 74 | public function remove(string $name) 75 | { 76 | return $this->delete($this->getEndpoint() . $name); 77 | } 78 | 79 | private function defaultProjectConfig() 80 | { 81 | return [ 82 | "features.images" => "true", 83 | "features.profiles" => "true", 84 | ]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Endpoint/Certificates.php: -------------------------------------------------------------------------------- 1 | get($this->getEndpoint()) as $certificate) { 24 | $certificates[] = str_replace('/' . $this->client->getApiVersion() . $this->getEndpoint(), '', $certificate); 25 | } 26 | 27 | return $certificates; 28 | } 29 | 30 | /** 31 | * Show information on a certificate 32 | * 33 | * @param string $fingerprint Fingerprint of certificate 34 | * @return object 35 | */ 36 | public function info($fingerprint) 37 | { 38 | return $this->get($this->getEndpoint() . $fingerprint); 39 | } 40 | 41 | /** 42 | * Add a new trusted certificate to the server 43 | * 44 | * Example: Add trusted certificate 45 | * $lxd->certificates->add(file_get_contents('/tmp/lxd_client.crt')); 46 | * 47 | * Example: Add trusted certificate from untrusted client 48 | * $lxd->certificates->add(file_get_contents('/tmp/lxd_client.crt'), 'secret'); 49 | * 50 | * @param string $certificate Certificate contents in PEM format 51 | * @param string $password Password for untrusted client 52 | * @param string $name Name for the certificate. If nothing is provided, the host in the TLS header for 53 | * the request is used. 54 | * @param string $token The join token to use in modern LXD (leave password as null) 55 | * @return string fingerprint of certificate 56 | */ 57 | public function add($certificate, ?string $password = null, ?string $name = null, ?string $token = null) 58 | { 59 | // Convert PEM certificate to DER certificate 60 | $begin = "CERTIFICATE-----"; 61 | $end = "-----END"; 62 | $pem_data = substr($certificate, strpos($certificate, $begin) + strlen($begin)); 63 | $pem_data = substr($pem_data, 0, strpos($pem_data, $end)); 64 | $der = base64_decode($pem_data); 65 | 66 | $fingerprint = hash('sha256', $der); 67 | 68 | $options = []; 69 | $options['type'] = 'client'; 70 | $options['certificate'] = base64_encode($der); 71 | 72 | if ($password !== null) { 73 | $options['password'] = $password; 74 | } 75 | 76 | if ($token !== null) { 77 | $options['trust_token'] = $token; 78 | } 79 | 80 | if ($name !== null) { 81 | $options['name'] = $name; 82 | } 83 | 84 | $this->post($this->getEndpoint(), $options); 85 | 86 | return $fingerprint; 87 | } 88 | 89 | /** 90 | * Remove a trusted certificate 91 | * 92 | * @param string $fingerprint Fingerprint of certificate 93 | */ 94 | public function remove($fingerprint) 95 | { 96 | $this->delete($this->getEndpoint() . $fingerprint); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Endpoint/Images/Aliases.php: -------------------------------------------------------------------------------- 1 | $this->client->getProject() 25 | ]; 26 | 27 | foreach ($this->get($this->getEndpoint(), $config) as $alias) { 28 | $aliases[] = str_replace('/'.$this->client->getApiVersion().$this->getEndpoint(), '', $alias); 29 | } 30 | 31 | return $aliases; 32 | } 33 | 34 | /** 35 | * Get information on an alias 36 | * 37 | * @param string $name Name of container 38 | * @return object 39 | */ 40 | public function info($name) 41 | { 42 | $config = [ 43 | "project"=>$this->client->getProject() 44 | ]; 45 | 46 | return $this->get($this->getEndpoint().$name, $config); 47 | } 48 | 49 | /** 50 | * Create an alias of an image 51 | * 52 | * @param string $fingerprint Fingerprint of image 53 | * @param string $aliasName Name of alias 54 | * @param string $description Description of alias 55 | */ 56 | public function create($fingerprint, $aliasName, $description = '') 57 | { 58 | $opts['target'] = $fingerprint; 59 | $opts['name'] = $aliasName; 60 | $opts['description'] = $description; 61 | 62 | $config = [ 63 | "project"=>$this->client->getProject() 64 | ]; 65 | 66 | return $this->post($this->getEndpoint(), $opts, $config); 67 | } 68 | 69 | /** 70 | * Replace an image alias 71 | * 72 | * Example: Replace alias "ubuntu/xenial/amd64" to point to image "097..." 73 | * $lxd->images->aliases->update( 74 | * 'test', 75 | * 'd02d6cf5a494df1c88144c7cbfec47b6d010a79baf18975a7c17abbf31cbae40', 76 | * 'new description' 77 | * ); 78 | * 79 | * @param string $name Name of alias 80 | * @param string $fingerprint Fingerprint of image 81 | * @param string $description Description of alias 82 | * @return object 83 | */ 84 | public function replace($name, $fingerprint, $description = '') 85 | { 86 | $opts['target'] = $fingerprint; 87 | $opts['description'] = $description; 88 | 89 | $config = [ 90 | "project"=>$this->client->getProject() 91 | ]; 92 | 93 | return $this->put($this->getEndpoint().$name, $opts, $config); 94 | } 95 | 96 | /** 97 | * Rename an alias 98 | * 99 | * @param string $name Name of container 100 | * @param string $newName Name of new alias 101 | * @return object 102 | */ 103 | public function rename($name, $newName) 104 | { 105 | $opts['name'] = $newName; 106 | 107 | $config = [ 108 | "project"=>$this->client->getProject() 109 | ]; 110 | 111 | return $this->post($this->getEndpoint().$name, $opts, $config); 112 | } 113 | 114 | /** 115 | * Delete an alias 116 | * 117 | * @param string $name Name of alias 118 | * @return object 119 | */ 120 | public function remove($name) 121 | { 122 | $config = [ 123 | "project"=>$this->client->getProject() 124 | ]; 125 | 126 | return $this->delete($this->getEndpoint().$name, $config); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `php-lxd` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 6 | 7 | # [1.2.0] 8 | ## Fixed 9 | Derpciation warnings caused by PHP 8.4 10 | 11 | # [1.1.0] 12 | ## Changed 13 | Require PHP >= 7.2.5 and support PHP8 in composer.json 14 | 15 | ## Fixed 16 | Setting recursion > 0 was str_replace on array, breaking in PHP8 17 | 18 | # [1.0.0] 19 | 20 | ## Changed 21 | Bump to guzzle7 22 | Fix spelling mistake in LxdExceptionThrower class name 23 | 24 | # [0.26.0] 25 | 26 | ## Added 27 | Method to create volume from file 28 | 29 | # [0.25.0] 30 | 31 | ## Added 32 | Token paramater when adding new certificate to LXD 33 | 34 | # [0.24.0] 35 | 36 | ## Fixed 37 | Cant restore snapshot from VM 38 | 39 | ## Dev 40 | Add some dynamic properties and return types to satisfy phpstan 41 | 42 | # [0.23.0] 43 | 44 | ## Changed 45 | - Handle 400 responses from LXD (the generic error response) this may impact 46 | exisiting code. 47 | 48 | # [0.22.1] 49 | 50 | ## Changed 51 | Don't set "config" and/or "device" properties when replacing profile 52 | 53 | 54 | # [0.22.0] 55 | 56 | ## Added 57 | - Recursion parameter for all project method 58 | 59 | ## Fixed 60 | - If source host is using the socket when migrating an instance, use host 61 | environment address instead of client url 62 | 63 | # [0.21.0] 64 | 65 | ## Added 66 | - Recursion parameter for getting snapshots 67 | - Recursion parameter for getting networks 68 | 69 | ## Fixed 70 | - Cant create storage pool when sending empty config 71 | 72 | # [0.20.0] 73 | ## Added 74 | - Create storage pool volume 75 | - Delete storage pool volume 76 | 77 | # [0.19.1] 78 | 79 | ## Added 80 | - Optional target project parameter to copy instance 81 | 82 | # [0.19.0] 83 | 84 | ## Added 85 | - Support for the warnings API 86 | 87 | # [0.18.2] 88 | 89 | ## Change 90 | - Support recursion param on get all pools 91 | 92 | # [0.18.1] 93 | 94 | ## Added 95 | - Get volume info 96 | 97 | # [0.18.0] 98 | 99 | ## Added 100 | - Recursion parameter for getting images (#17) 101 | 102 | ## Changed 103 | - Make networks project aware (#16) 104 | 105 | 106 | # [0.17.0] 107 | 108 | ## Added 109 | - Instance files are now project aware 110 | - Add the timeout parameter correctly when waiting for an operation (@dhzavann) 111 | 112 | ## Changed 113 | - Delay check if vms are supported until needed (@TonyBogdanov) 114 | 115 | # [0.16.4] 116 | 117 | ## Added 118 | - Recursion parameter for getting profiles 119 | 120 | # [0.16.3] 121 | 122 | ## Added 123 | - Recursion parameter for getting cluster members 124 | 125 | # [0.16.2] 126 | 127 | ## Fixed 128 | - Cant load project info because path is wrong 129 | 130 | # [0.16.1] 131 | 132 | ## Added 133 | - Support passing alias param when creating image 134 | - Use "instance" instead of "container" when creating image from "container" 135 | 136 | # [0.16.0] 137 | 138 | ## Added 139 | - Recursion parameter to get all instances / containers / vms 140 | 141 | # [0.15.2] 142 | 143 | ## Added 144 | - Added optional target parameter for creating instance 145 | 146 | # [0.15.1] 147 | 148 | ## Fixed 149 | - Files method using the wrong param for headers 150 | 151 | # [0.15.0] 152 | 153 | ## Added 154 | - Added an instance class that you should use instead of containers, it will 155 | fall back to the `/containers` endpoint if your host doesn't support `/instances` 156 | which is the agnostic way of dealing with both containers and virtual machines 157 | 158 | 159 | # [0.14.0] 160 | 161 | ## Added 162 | - Provide backup as source type & file to create container from backup file 163 | 164 | ## [0.13.2] 165 | 166 | ## Fixed 167 | - not being able to cache responses due to bad namespaces 168 | 169 | 170 | ## [0.13.1] 171 | 172 | ## Added 173 | - Delete container file (#11 @TonyBogdanov) 174 | 175 | ## [0.13.0] 176 | 177 | ## Added 178 | - Some of the cluster endpoints 179 | 180 | ## [0.12.3] 181 | 182 | ## Fixed 183 | - Migrating snapshot of runnin container (#9) 184 | 185 | ## [0.12.2] 186 | 187 | ## Added 188 | - Support for using container snapshot as source for migration 189 | 190 | ## [0.12.1] 191 | 192 | ### Added 193 | - Backup export method 194 | 195 | ## [0.12.0] 196 | 197 | ### Added 198 | - Backup endpoints 199 | 200 | ## [0.6.0] - 2017-01-23 201 | 202 | ### Added 203 | - Documentation 204 | - container migration 205 | 206 | ### Deprecated 207 | - Nothing 208 | 209 | ### Fixed 210 | - Nothing 211 | 212 | ### Removed 213 | - Nothing 214 | 215 | ### Security 216 | - Nothing 217 | -------------------------------------------------------------------------------- /src/Endpoint/Instance/Backups.php: -------------------------------------------------------------------------------- 1 | endpoint; 14 | } 15 | 16 | public function setEndpoint(string $endpoint) 17 | { 18 | $this->endpoint = $endpoint; 19 | } 20 | 21 | 22 | /** 23 | * Get all backups for a particular container 24 | * @param string $container Container name 25 | * @return object 26 | */ 27 | public function all(string $container) 28 | { 29 | $backups = []; 30 | 31 | $config = [ 32 | "project"=>$this->client->getProject() 33 | ]; 34 | 35 | foreach ($this->get($this->getEndpoint().$container.'/backups/', $config) as $backup) { 36 | $backup = str_replace( 37 | '/'.$this->client->getApiVersion().$this->getEndpoint().$container.'/backups/', 38 | '', 39 | $backup 40 | ); 41 | $backup = str_replace("?project=".$config["project"], "", $backup); 42 | $backups[] = $backup; 43 | } 44 | 45 | return $backups; 46 | } 47 | /** 48 | * Get info for a container backup 49 | * @param string $container Container name 50 | * @param string $name Backup name 51 | * @return object 52 | */ 53 | public function info(string $container, string $name) 54 | { 55 | $config = [ 56 | "project"=>$this->client->getProject() 57 | ]; 58 | 59 | return $this->get($this->getEndpoint().$container.'/backups/'.$name, $config); 60 | } 61 | /** 62 | * Create a backup for a container 63 | * @param string $container Name of the container 64 | * @param string $name Name of the backup 65 | * @param array $opts Options for the backup 66 | * @param bool $wait Wait for the backup operation to finish 67 | * @return object 68 | */ 69 | public function create(string $container, string $name, array $opts, $wait = false) 70 | { 71 | $opts = array_merge([ 72 | "name"=>$name 73 | ], $opts); 74 | 75 | $config = [ 76 | "project"=>$this->client->getProject() 77 | ]; 78 | 79 | $response = $this->post($this->getEndpoint().$container.'/backups', $opts, $config); 80 | 81 | if ($wait) { 82 | $response = $this->client->operations->wait($response['id']); 83 | } 84 | 85 | return $response; 86 | } 87 | /** 88 | * Rename a container backup 89 | * @param string $container Name of the container 90 | * @param string $name Name of the backup 91 | * @param string $newBackup New name for the backup 92 | * @param bool $wait Wait for the rename operation to finish 93 | * @return object 94 | */ 95 | public function rename(string $container, string $name, string $newBackup, $wait = false) 96 | { 97 | $opts = [ 98 | "name"=>$newBackup 99 | ]; 100 | 101 | $config = [ 102 | "project"=>$this->client->getProject() 103 | ]; 104 | 105 | $response = $this->post($this->getEndpoint().$container.'/backups/'.$name, $opts, $config); 106 | 107 | 108 | if ($wait) { 109 | $response = $this->client->operations->wait($response['id']); 110 | } 111 | 112 | return $response; 113 | } 114 | /** 115 | * Remove a container backup 116 | * @param string $container Name of a container 117 | * @param string $name Name of the backup 118 | * @param bool $wait Wait for the delete operation to finish 119 | * @return object 120 | */ 121 | public function remove(string $container, string $name, $wait = false) 122 | { 123 | $config = [ 124 | "project"=>$this->client->getProject() 125 | ]; 126 | 127 | $response = $this->delete($this->getEndpoint().$container.'/backups/'.$name, $config); 128 | 129 | if ($wait) { 130 | $response = $this->client->operations->wait($response['id']); 131 | } 132 | 133 | return $response; 134 | } 135 | /** 136 | * Download a backup 137 | * @param string $container Name of a container 138 | * @param string $name Name of the backup 139 | * @return object 140 | */ 141 | public function export(string $container, string $name) 142 | { 143 | $config = [ 144 | "project"=>$this->client->getProject() 145 | ]; 146 | 147 | $response = $this->get($this->getEndpoint().$container.'/backups/'.$name . "/export", $config); 148 | 149 | return $response; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Endpoint/AbstractEndpoint.php: -------------------------------------------------------------------------------- 1 | client = $client; 15 | } 16 | 17 | abstract protected function getEndpoint(); 18 | 19 | /** 20 | * Send a GET request with query parameters. 21 | * 22 | * @param string $path Request path. 23 | * @param array $parameters GET parameters. 24 | * @param array $requestHeaders Request Headers. 25 | * 26 | * @return array|string 27 | */ 28 | protected function get($path, array $parameters = [], array $requestHeaders = []) 29 | { 30 | $response = $this->client->getHttpClient()->get( 31 | $this->buildPath($path, $parameters), 32 | $requestHeaders 33 | ); 34 | 35 | return ResponseMediator::getContent($response); 36 | } 37 | 38 | /** 39 | * Send a POST request with JSON-encoded data. 40 | * 41 | * @param string $path Request path. 42 | * @param array|string $data POST data to be JSON encoded. 43 | * @param array $parameters POST parameters. 44 | * @param array $requestHeaders Request headers. 45 | */ 46 | protected function post($path, $data = [], array $parameters = [], array $requestHeaders = []) 47 | { 48 | $response = $this->client->getHttpClient()->post( 49 | $this->buildPath($path, $parameters), 50 | $requestHeaders, 51 | $this->createJsonBody($data) 52 | ); 53 | 54 | return ResponseMediator::getContent($response); 55 | } 56 | 57 | /** 58 | * Send a PUT request with JSON-encoded data. 59 | * 60 | * @param string $path Request path. 61 | * @param array|string $data POST data to be JSON encoded. 62 | * @param array $parameters POST parameters. 63 | * @param array $requestHeaders Request headers. 64 | */ 65 | protected function put($path, $data = [], array $parameters = [], array $requestHeaders = []) 66 | { 67 | $response = $this->client->getHttpClient()->put( 68 | $this->buildPath($path, $parameters), 69 | $requestHeaders, 70 | $this->createJsonBody($data) 71 | ); 72 | 73 | return ResponseMediator::getContent($response); 74 | } 75 | 76 | /** 77 | * Send a PATCH request with JSON-encoded data. 78 | * 79 | * @param string $path Request path. 80 | * @param array|string $data POST data to be JSON encoded. 81 | * @param array $parameters POST parameters. 82 | * @param array $requestHeaders Request headers. 83 | */ 84 | protected function patch($path, $data = [], array $parameters = [], array $requestHeaders = []) 85 | { 86 | $response = $this->client->getHttpClient()->patch( 87 | $this->buildPath($path, $parameters), 88 | $requestHeaders, 89 | $this->createJsonBody($data) 90 | ); 91 | 92 | return ResponseMediator::getContent($response); 93 | } 94 | 95 | /** 96 | * Send a DELETE request with query parameters. 97 | * 98 | * @param string $path Request path. 99 | * @param array $parameters GET parameters. 100 | * @param array $requestHeaders Request Headers. 101 | * 102 | * @return array|string 103 | */ 104 | protected function delete($path, array $parameters = [], array $requestHeaders = []) 105 | { 106 | $response = $this->client->getHttpClient()->delete( 107 | $this->buildPath($path, $parameters), 108 | $requestHeaders 109 | ); 110 | 111 | return ResponseMediator::getContent($response); 112 | } 113 | 114 | /** 115 | * Create a JSON encoded version of an array. 116 | * 117 | * @param array|string $data Request data 118 | * 119 | * @return null|string 120 | */ 121 | protected function createJsonBody($data) 122 | { 123 | if (is_array($data)) { 124 | return (count($data) === 0) ? null : json_encode($data, empty($data) ? JSON_FORCE_OBJECT : 0); 125 | } else { 126 | return $data; 127 | } 128 | } 129 | 130 | /** 131 | * Build URI with query parameters. 132 | * 133 | * @param string $path Request path. 134 | * @param array $data Request data. 135 | * 136 | * @return string 137 | */ 138 | protected function buildPath($path, array $parameters) 139 | { 140 | if (count($parameters) > 0) { 141 | $path .= '?'.http_build_query($parameters); 142 | } 143 | 144 | return $path; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Endpoint/Instance/Snapshots.php: -------------------------------------------------------------------------------- 1 | endpoint; 14 | } 15 | 16 | public function setEndpoint(string $endpoint) 17 | { 18 | $this->endpoint = $endpoint; 19 | } 20 | /** 21 | * List of snapshots for a container 22 | * 23 | * @param string $name Name of container 24 | * @return array 25 | */ 26 | public function all($name, $recursion = 0) 27 | { 28 | 29 | $config = [ 30 | "project" => $this->client->getProject(), 31 | "recursion" => $recursion 32 | ]; 33 | $snapshots = $this->get($this->getEndpoint() . $name . '/snapshots/', $config); 34 | if ($recursion == 0) { 35 | foreach ($snapshots as &$snapshot) { 36 | $snapshot = str_replace( 37 | '/' . $this->client->getApiVersion() . $this->getEndpoint() . $name . '/snapshots/', 38 | '', 39 | $snapshot 40 | ); 41 | $snapshot = str_replace("?project=" . $config["project"], "", $snapshot); 42 | } 43 | } 44 | 45 | return $snapshots; 46 | } 47 | 48 | /** 49 | * Show information on a snapshot 50 | * 51 | * @param string $name Name of container 52 | * @param string $snapshots Name of snapshots 53 | * @return object 54 | */ 55 | public function info($name, $snapshot) 56 | { 57 | $config = [ 58 | "project" => $this->client->getProject() 59 | ]; 60 | 61 | return $this->get($this->getEndpoint() . $name . '/snapshots/' . $snapshot, $config); 62 | } 63 | 64 | /** 65 | * Create a snapshot of a container 66 | * 67 | * If stateful is true when creating a snapshot of a 68 | * running container, the container's runtime state will be stored in the 69 | * snapshot. Note that CRIU must be installed on the server to create a 70 | * stateful snapshot, or LXD will return a 500 error. 71 | * 72 | * @param string $name Name of container 73 | * @param string $snapshot Name of snapshot 74 | * @param bool $stateful Whether to save runtime state for a running container 75 | * @param bool $wait Wait for operation to finish 76 | * @return object 77 | */ 78 | public function create($name, $snapshot, $stateful = false, $wait = false) 79 | { 80 | $opts['name'] = $snapshot; 81 | $opts['stateful'] = $stateful; 82 | 83 | $config = [ 84 | "project" => $this->client->getProject() 85 | ]; 86 | 87 | $response = $this->post($this->getEndpoint() . $name . '/snapshots', $opts, $config); 88 | 89 | if ($wait) { 90 | $response = $this->client->operations->wait($response['id']); 91 | } 92 | 93 | return $response; 94 | } 95 | 96 | /** 97 | * Restore a snapshot of a container 98 | * 99 | * @param string $name Name of container 100 | * @param string $snapshot Name of snapshot 101 | * @param bool $wait Wait for operation to finish 102 | * @return object 103 | */ 104 | public function restore($name, $snapshot, $wait = false) 105 | { 106 | $opts['restore'] = $snapshot; 107 | 108 | $response = $this->client->instances->replace($name, $opts, $wait); 109 | 110 | return $response; 111 | } 112 | 113 | /** 114 | * Rename a snapshot 115 | * 116 | * @param string $name Name of container 117 | * @param string $snaphot Name of snapshot 118 | * @param string $newSnapshot Name of new snapshot 119 | * @param bool $wait Wait for operation to finish 120 | * @return object 121 | */ 122 | public function rename($name, $snaphot, $newSnapshot, $wait = false) 123 | { 124 | $opts['name'] = $newSnapshot; 125 | $config = [ 126 | "project" => $this->client->getProject() 127 | ]; 128 | $response = $this->post($this->getEndpoint() . $name . '/snapshots/' . $snaphot, $opts, $config); 129 | 130 | if ($wait) { 131 | $response = $this->client->operations->wait($response['id']); 132 | } 133 | 134 | return $response; 135 | } 136 | 137 | /** 138 | * Delete a container 139 | * 140 | * @param string $name Name of container 141 | * @param string $snaphot Name of snapshot 142 | * @param bool $wait Wait for operation to finish 143 | * @return object 144 | */ 145 | public function remove($name, $snaphot, $wait = false) 146 | { 147 | $config = [ 148 | "project" => $this->client->getProject() 149 | ]; 150 | 151 | $response = $this->delete($this->getEndpoint() . $name . '/snapshots/' . $snaphot, $config); 152 | 153 | if ($wait) { 154 | $response = $this->client->operations->wait($response['id']); 155 | } 156 | 157 | return $response; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Endpoint/Profiles.php: -------------------------------------------------------------------------------- 1 | $this->client->getProject(), 23 | "recursion" => $recursion 24 | ]; 25 | 26 | $profiles = $this->get($this->getEndpoint(), $config); 27 | 28 | if ($recursion == 0) { 29 | foreach ($profiles as &$profile) { 30 | $profile = str_replace('/' . $this->client->getApiVersion() . $this->getEndpoint(), '', $profile); 31 | $profile = str_replace('?project=' . $this->client->getProject(), '', $profile); 32 | } 33 | } 34 | 35 | return $profiles; 36 | } 37 | 38 | /** 39 | * Show information on a profile 40 | * 41 | * @param string $name name of profile 42 | * @return object 43 | */ 44 | public function info($name) 45 | { 46 | $config = [ 47 | "project" => $this->client->getProject() 48 | ]; 49 | 50 | return $this->get($this->getEndpoint() . $name, $config); 51 | } 52 | 53 | /** 54 | * Create a new profile 55 | * 56 | * Example: Create profile 57 | * $lxd->profiles->create( 58 | * 'test-profile', 59 | * 'My test profile', 60 | * ["limits.memory" => "2GB"], 61 | * [ 62 | * "kvm" => [ 63 | * "type" => "unix-char", 64 | * "path" => "/dev/kvm" 65 | * ], 66 | * ] 67 | * ); 68 | * 69 | * @param string $name Name of profile 70 | * @param string $description Description of profile 71 | * @param array $config Configuration of profile 72 | * @param array $devices Devices of profile 73 | * @return object 74 | */ 75 | public function create($name, $description = '', ?array $config = null, ?array $devices = null) 76 | { 77 | $profile = []; 78 | $profile['name'] = $name; 79 | $profile['description'] = $description; 80 | $profile['config'] = $config; 81 | $profile['devices'] = $devices; 82 | 83 | $config = [ 84 | "project" => $this->client->getProject() 85 | ]; 86 | 87 | return $this->post($this->getEndpoint(), $profile, $config); 88 | } 89 | 90 | /** 91 | * Update profile. 92 | * This will only update supplied profile settings and leave the other settings 93 | * 94 | * Example: Update profile 95 | * $lxd->profiles->update( 96 | * 'test-profile', 97 | * 'My test profile', 98 | * ["limits.memory" => "2GB"], 99 | * [ 100 | * "kvm" => [ 101 | * "type" => "unix-char", 102 | * "path" => "/dev/kvm" 103 | * ], 104 | * ] 105 | * ); 106 | * 107 | * @param string $name Name of profile 108 | * @param string $description Description of profile 109 | * @param array $config Configuration of profile 110 | * @param array $devices Devices of profile 111 | * @return object 112 | */ 113 | public function update($name, $description = '', ?array $config = null, ?array $devices = null) 114 | { 115 | $profile = []; 116 | $profile['description'] = $description; 117 | $profile['config'] = $config; 118 | $profile['devices'] = $devices; 119 | 120 | $config = [ 121 | "project" => $this->client->getProject() 122 | ]; 123 | 124 | return $this->patch($this->getEndpoint() . $name, $profile, $config); 125 | } 126 | 127 | /** 128 | * Replace profile. 129 | * This will replace all the profile settings with the supplied settings 130 | * 131 | * Example: Replace profile 132 | * $lxd->profiles->replace( 133 | * 'test-profile', 134 | * 'My test profile', 135 | * ["limits.memory" => "2GB"], 136 | * [ 137 | * "kvm" => [ 138 | * "type" => "unix-char", 139 | * "path" => "/dev/kvm" 140 | * ], 141 | * ] 142 | * ); 143 | * 144 | * @param string $name Name of profile 145 | * @param string $description Description of profile 146 | * @param array $config Configuration of profile 147 | * @param array $devices Devices of profile 148 | * @return object 149 | */ 150 | public function replace($name, $description = '', ?array $config = null, ?array $devices = null) 151 | { 152 | $profile = []; 153 | $profile['description'] = $description; 154 | 155 | if (!empty($config)) { 156 | $profile['config'] = $config; 157 | } 158 | 159 | if (!empty($devices)) { 160 | $profile['devices'] = $devices; 161 | } 162 | 163 | $config = [ 164 | "project" => $this->client->getProject() 165 | ]; 166 | 167 | return $this->put($this->getEndpoint() . $name, $profile, $config); 168 | } 169 | 170 | /** 171 | * Rename profile 172 | * 173 | * @param string $name Name of profile 174 | * @param string $newName Name of new profile 175 | * @return object 176 | */ 177 | public function rename($name, $newName) 178 | { 179 | $profile = []; 180 | $profile['name'] = $newName; 181 | 182 | $config = [ 183 | "project" => $this->client->getProject() 184 | ]; 185 | 186 | return $this->post($this->getEndpoint() . $name, $profile, $config); 187 | } 188 | 189 | /** 190 | * Delete a profile 191 | * 192 | * @param string $name Name of profile 193 | */ 194 | public function remove($name) 195 | { 196 | $config = [ 197 | "project" => $this->client->getProject() 198 | ]; 199 | 200 | return $this->delete($this->getEndpoint() . $name, $config); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /docs/containers.md: -------------------------------------------------------------------------------- 1 | ### Containers 2 | 3 | > NOTE: If you haven't setup your LXD server, read [configuration.md](configuration.md) 4 | 5 | To get information on containers: 6 | 7 | ``` 8 | containers->all(); 12 | 13 | // info for the container called 'test' 14 | $info = $lxd->containers->info('test')); 15 | 16 | // get the current state of the container i.e memory usage etc. 17 | $state = $lxd->containers->state('test'); 18 | 19 | ``` 20 | 21 | #### Create new containers 22 | 23 | From image alias: 24 | 25 | ``` 26 | 'ubuntu/xenial/amd64']; 29 | $lxd->containers->create('from-alias', $options); 30 | ``` 31 | 32 | From image fingerprint: 33 | 34 | ``` 35 | 'xxxxxxxxxxxx']; 38 | $lxd->containers->create('from-fingerprint', $options); 39 | ``` 40 | 41 | From image matching properties: 42 | 43 | ``` 44 | [ 48 | 'os' => 'ubuntu', 49 | 'release' => '14.04', 50 | 'architecture' => 'x86_64', 51 | ], 52 | ]; 53 | $lxd->containers->create('from-properties', $options); 54 | ``` 55 | 56 | From private remote server image: 57 | 58 | ``` 59 | 'https://private.example.com:8443', 63 | 'alias' => 'ubuntu/xenial/amd64', 64 | 'secret' => 'my_secrect' 65 | ]; 66 | $lxd->containers->create('remote-private', $options); 67 | ``` 68 | 69 | From public remote server image: 70 | 71 | ``` 72 | 'https://images.linuxcontainers.org:8443', 76 | 'alias' => 'ubuntu/xenial/amd64' 77 | ]; 78 | $lxd->containers->create('remote-public', $options); 79 | ``` 80 | 81 | Create container with empty rootfs: 82 | 83 | ``` 84 | true]; 87 | $lxd->containers->create('empty-rootfs', $options); 88 | ``` 89 | 90 | With addtional container configuration: 91 | 92 | ``` 93 | 'ubuntu/xenial/amd64', 97 | 'config' => [ 98 | 'volatile.eth0.hwaddr' => 'aa:bb:cc:dd:ee:ff', 99 | ], 100 | 'profiles' => ['default'] 101 | ]; 102 | $lxd->containers->create('with-configuration', $options); 103 | ``` 104 | 105 | Copy a container locally: 106 | 107 | ``` 108 | containers->copy('container-name', 'new-container-name')); 111 | ``` 112 | 113 | Migrate a container to a different LXD server: 114 | 115 | ``` 116 | containers->migrate($lxd2, 'container-name'); 120 | 121 | ``` 122 | 123 | > See [lxd/rest.md](https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1) for more information on creating containers. 124 | 125 | #### Rename container 126 | 127 | ``` 128 | containers->rename('container-name', 'container-rename'); 131 | ``` 132 | 133 | #### Remove container 134 | 135 | ``` 136 | containers->remove('container-name'); 139 | ``` 140 | 141 | #### Update container 142 | 143 | Replace containers configuration.
144 | To avoid lost of configuration first of all current config can be read, changed and then set again. 145 | ``` 146 | containers->show('test'); 149 | $container->ephemeral = true; 150 | $lxd->containers->replace('container-name', $container); 151 | ``` 152 | 153 | Restore a snapshot 154 | ``` 155 | containers->replace('container-name', ['restore' => 'snapshot-name'] ); 157 | ``` 158 | 159 | Update containers configuration.
Example: set limit of cpu cores to 4 and rootfs size to 5GB 160 | 161 | ``` 162 | [ 165 | 'limits.cpu' => 4 166 | ], 167 | 'devices' => [ 168 | 'rootfs' => [ 169 | 'size' => '5GB' 170 | ] 171 | ] 172 | ]; 173 | $lxd->containers->update('container-name', $container); 174 | ``` 175 | 176 | #### Change state 177 | 178 | Start container: 179 | 180 | ``` 181 | containers->start('container-name'); 184 | ``` 185 | 186 | Stop container: 187 | 188 | ``` 189 | containers->stop('container-name'); 192 | ``` 193 | 194 | Restart container: 195 | 196 | ``` 197 | containers->restart('container-name'); 200 | ``` 201 | 202 | Freeze container: 203 | 204 | ``` 205 | containers->freeze('container-name'); 208 | ``` 209 | 210 | Unfreeze container: 211 | 212 | ``` 213 | containers->unfreeze('container-name'); 216 | ``` 217 | 218 | #### Execute a command in a container 219 | 220 | ``` 221 | containers->execute('container-name', 'touch /tmp/test.txt'); 224 | ``` 225 | 226 | #### Logs 227 | 228 | Get all logs: 229 | 230 | ``` 231 | containers->logs->all('container-name'); 234 | ``` 235 | 236 | Read log: 237 | 238 | ``` 239 | containers->logs->read('container-name', 'exec_xxxxxxxx.stdout'); 242 | ``` 243 | 244 | Remove log: 245 | 246 | ``` 247 | containers->logs->remove('container-name', 'exec_xxxxxxxx.stdout'); 250 | ``` 251 | 252 | #### Files 253 | 254 | Write to a file: 255 | 256 | ``` 257 | containers->files->write('container-name', '/tmp/test.txt', 'Hello World'); 260 | ``` 261 | 262 | Read from a file: 263 | 264 | ``` 265 | containers->files->read('container-name', '/tmp/test.txt'); 268 | ``` 269 | 270 | #### Snapshots 271 | 272 | View containers snapshots: 273 | 274 | ``` 275 | containers->snapshots->all('container-name'); 278 | ``` 279 | 280 | Get snapshot information: 281 | 282 | ``` 283 | containers->snapshots->info('container-name', 'snapshot0'); 286 | ``` 287 | 288 | Create snapshot: 289 | 290 | ``` 291 | containers->snapshots->create('container-name', 'snapshot1'); 294 | ``` 295 | 296 | Restore snapshot: 297 | 298 | ``` 299 | containers->snapshots->restore('container-name', 'snapshot1'); 302 | ``` 303 | 304 | Rename snapshot: 305 | 306 | ``` 307 | containers->snapshots->rename('container-name', 'snapshot1', 'snapshot1-rename'); 310 | ``` 311 | 312 | Remove snapshot: 313 | 314 | ``` 315 | containers->snapshots->remove('container-name', 'snapshot0'); 318 | ``` 319 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | httpClient = $httpClient ?: HttpClientDiscovery::find(); 82 | $this->messageFactory = MessageFactoryDiscovery::find(); 83 | $this->apiVersion = $apiVersion ?: '1.0'; 84 | $this->url = $url ?: 'https://127.0.0.1:8443'; 85 | $this->project = $projectName; 86 | 87 | $this->addPlugin(new LxdExceptionThrower()); 88 | 89 | $this->setUrl($this->url); 90 | } 91 | 92 | public function hasVms() 93 | { 94 | if ($this->hasVmsCache === null) { 95 | $this->hasVmsCache = in_array("virtual-machines", $this->host->info()["api_extensions"]); 96 | } 97 | return $this->hasVmsCache; 98 | } 99 | 100 | /** 101 | * @return string 102 | */ 103 | public function getUrl() 104 | { 105 | return $this->url; 106 | } 107 | 108 | /** 109 | * Sets the URL of your LXD instance. 110 | * 111 | * @param string $url URL of the API in the form of https://hostname:port 112 | */ 113 | public function setUrl($url) 114 | { 115 | $this->url = $url; 116 | 117 | $this->removePlugin(Plugin\AddHostPlugin::class); 118 | $this->removePlugin(PathPrepend::class); 119 | $this->removePlugin(PathTrimEnd::class); 120 | 121 | $this->addPlugin(new Plugin\AddHostPlugin(UriFactoryDiscovery::find()->createUri($this->url))); 122 | $this->addPlugin(new PathPrepend(sprintf('/%s', $this->getApiVersion()))); 123 | $this->addPlugin(new PathTrimEnd()); 124 | } 125 | 126 | /** 127 | * Add a new plugin to the end of the plugin chain. 128 | * 129 | * @param Plugin $plugin 130 | */ 131 | public function addPlugin(Plugin $plugin) 132 | { 133 | $this->plugins[] = $plugin; 134 | $this->httpClientModified = true; 135 | } 136 | 137 | /** 138 | * Remove a plugin by its fully qualified class name (FQCN). 139 | * 140 | * @param string $fqcn 141 | */ 142 | public function removePlugin($fqcn) 143 | { 144 | foreach ($this->plugins as $idx => $plugin) { 145 | if ($plugin instanceof $fqcn) { 146 | unset($this->plugins[$idx]); 147 | $this->httpClientModified = true; 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * @return HttpMethodsClient 154 | */ 155 | public function getHttpClient() 156 | { 157 | if ($this->httpClientModified) { 158 | $this->httpClientModified = false; 159 | 160 | $this->pluginClient = new HttpMethodsClient( 161 | new PluginClient($this->httpClient, $this->plugins), 162 | $this->messageFactory 163 | ); 164 | } 165 | return $this->pluginClient; 166 | } 167 | 168 | /** 169 | * @param HttpClient $httpClient 170 | */ 171 | public function setHttpClient(HttpClient $httpClient) 172 | { 173 | $this->httpClientModified = true; 174 | $this->httpClient = $httpClient; 175 | } 176 | 177 | /** 178 | * @return string 179 | */ 180 | public function getApiVersion() 181 | { 182 | return $this->apiVersion; 183 | } 184 | 185 | /** 186 | * Add a cache plugin to cache responses locally. 187 | * 188 | * @param CacheItemPoolInterface $cache 189 | * @param array $config 190 | */ 191 | public function addCache(CacheItemPoolInterface $cachePool, array $config = []) 192 | { 193 | $this->removeCache(); 194 | $this->addPlugin(new Plugin\CachePlugin($cachePool, new \Http\Message\StreamFactory\GuzzleStreamFactory(), $config)); 195 | } 196 | 197 | /** 198 | * Remove the cache plugin 199 | */ 200 | public function removeCache() 201 | { 202 | $this->removePlugin(Plugin\CachePlugin::class); 203 | } 204 | 205 | public function __get($endpoint) 206 | { 207 | $class = __NAMESPACE__.'\\Endpoint\\'.ucfirst($endpoint); 208 | 209 | if (class_exists($class)) { 210 | return new $class($this); 211 | } else { 212 | throw new InvalidEndpointException( 213 | 'Endpoint '.$class.', not implemented.' 214 | ); 215 | } 216 | } 217 | 218 | /** 219 | * Make sure to move the cache plugin to the end of the chain 220 | */ 221 | private function pushBackCachePlugin() 222 | { 223 | $cachePlugin = null; 224 | foreach ($this->plugins as $i => $plugin) { 225 | if ($plugin instanceof Plugin\CachePlugin) { 226 | $cachePlugin = $plugin; 227 | unset($this->plugins[$i]); 228 | $this->plugins[] = $cachePlugin; 229 | return; 230 | } 231 | } 232 | } 233 | /** 234 | * Set the project to use on the server 235 | * @param string $projectName The project name to use 236 | */ 237 | public function setProject(string $projectName) 238 | { 239 | $this->project = $projectName; 240 | } 241 | /** 242 | * Get the project using on the client 243 | * @return string The current project 244 | */ 245 | public function getProject() 246 | { 247 | return $this->project; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/Endpoint/Images.php: -------------------------------------------------------------------------------- 1 | $this->client->getProject(), 28 | "recursion" => $recursion 29 | ]; 30 | 31 | $images = $this->get($this->getEndpoint(), $config); 32 | 33 | if ($recursion == 0) { 34 | foreach ($images as &$image) { 35 | $image = str_replace('/' . $this->client->getApiVersion() . $this->getEndpoint(), '', $image); 36 | } 37 | } 38 | return $images; 39 | } 40 | 41 | /** 42 | * Get information on an image 43 | * 44 | * @param string $fingerprint Fingerprint of image 45 | * @param string $secret Secret to access private image by untrusted client 46 | * @return object 47 | */ 48 | public function info($fingerprint, ?string $secret = null): array 49 | { 50 | $endpoint = $this->getEndpoint() . $fingerprint; 51 | if (!empty($secret)) { 52 | $endpoint .= '?secret=' . $secret; 53 | } 54 | 55 | $config = [ 56 | "project" => $this->client->getProject() 57 | ]; 58 | 59 | return $this->get($endpoint, $config); 60 | } 61 | 62 | /** 63 | * Create and publish a new image 64 | * 65 | * Ways to create an image: 66 | * @todo Standard http file upload 67 | * # Source image (transfers a remote image) 68 | * # Source container (makes an image out of a local container) 69 | * @todo Remote image URL (downloads a remote image) 70 | * 71 | * @param array $options Options to create the image 72 | * @param bool $wait Wait for operation to finish 73 | * @return object 74 | */ 75 | public function create(array $options, $headers = [], $wait = false) 76 | { 77 | $config = [ 78 | "project" => $this->client->getProject() 79 | ]; 80 | 81 | $response = $this->post($this->getEndpoint(), $options, $config, $headers); 82 | 83 | if ($wait) { 84 | $response = $this->client->operations->wait($response['id']); 85 | } 86 | 87 | return $response; 88 | } 89 | 90 | /** 91 | * Import an image from a remote server 92 | * 93 | * Example: Import an image by alias 94 | * $lxd->images->createFromRemote( 95 | * "https://images.linuxcontainers.org:8443", 96 | * [ 97 | * "alias" => "ubuntu/xenial/amd64", 98 | * ] 99 | * ); 100 | * 101 | * Example: Import an image by fingerprint 102 | * $lxd->images->createFromRemote( 103 | * "https://images.linuxcontainers.org:8443", 104 | * [ 105 | * "fingerprint" => "65df07147e458f356db90fa66d6f907a164739b554a40224984317eee729e92a", 106 | * ] 107 | * ); 108 | * 109 | * Example: Import image and automatically update it when it is updated on the remote server 110 | * $lxd->images->createFromRemote( 111 | * "https://images.linuxcontainers.org:8443", 112 | * [ 113 | * "alias" => "ubuntu/xenial/amd64", 114 | * ], 115 | * true 116 | * ); 117 | * 118 | * @param string $server Remote server 119 | * @param array $options Options to create the image 120 | * @param bool $autoUpdate Whether or not the image should be automatically updated from the remote server 121 | * @param bool $wait Wait for operation to finish 122 | * @return object 123 | */ 124 | public function createFromRemote($server, array $options, $autoUpdate = false, $wait = false) 125 | { 126 | $source = $this->getSource($options); 127 | 128 | if (isset($options['protocol']) && !in_array($options['protocol'], ['lxd', 'simplestreams'])) { 129 | throw new \Exception('Invalid protocol. Valid choices: lxd, simplestreams'); 130 | } 131 | 132 | $only = [ 133 | 'secret', 134 | 'protocol', 135 | 'certificate', 136 | ]; 137 | $remoteOptions = array_intersect_key($options, array_flip((array) $only)); 138 | 139 | $opts = $this->getOptions($options); 140 | $opts['auto_update'] = $autoUpdate; 141 | $opts['source'] = array_merge($source, $remoteOptions); 142 | $opts['source']['type'] = 'image'; 143 | $opts['source']['mode'] = 'pull'; 144 | $opts['source']['server'] = $server; 145 | 146 | return $this->create($opts, [], $wait); 147 | } 148 | 149 | /** 150 | * Create an image from a container 151 | * 152 | * Example: Create a private image from container 153 | * $lxd->images->createFromContainer("container_name"); 154 | * 155 | * Example: Create a public image from container 156 | * $lxd->images->createFromContainer( 157 | * "container_name", 158 | * [ 159 | * "public" => true, 160 | * ] 161 | * ); 162 | * 163 | * Example: Store properties with the new image, and override its filename 164 | * $lxd->images->createFromContainer( 165 | * "container_name", 166 | * [ 167 | * "filename" => "ubuntu-trusty.tar.gz", 168 | * "properties" => ["os" => "Ubuntu"], 169 | * ] 170 | * ); 171 | * 172 | * @param string $name The name of the container 173 | * @param array $options Options to create the container 174 | * @param bool $wait Wait for operation to finish 175 | * @return object 176 | */ 177 | public function createFromContainer($name, array $options, $wait = false) 178 | { 179 | $opts = $this->getOptions($options); 180 | $opts['source']['type'] = 'instance'; 181 | $opts['source']['name'] = $name; 182 | 183 | return $this->create($opts, [], $wait); 184 | } 185 | 186 | /** 187 | * Create an image from a snapshot 188 | * 189 | * Example: Create a private image from snapshot 190 | * $lxd->images->createFromSnapshot("container_name", "snapshot_name"); 191 | * 192 | * Example: Create a public image from snapshot 193 | * $lxd->images->createFromContainer( 194 | * "container_name", 195 | * "snapshot_name", 196 | * [ 197 | * "public" => true, 198 | * ] 199 | * ); 200 | * 201 | * Example: Store properties with the new image, and override its filename 202 | * $lxd->images->createFromContainer( 203 | * "container_name", 204 | * "snapshot_name", 205 | * [ 206 | * "filename" => "ubuntu-trusty.tar.gz", 207 | * "properties" => ["os" => "Ubuntu"], 208 | * ] 209 | * ); 210 | * 211 | * @param string $container The name of the container 212 | * @param string $snapshot The name of the snapshot 213 | * @param array $options Options to create the container 214 | * @param bool $wait Wait for operation to finish 215 | * @return object 216 | */ 217 | public function createFromSnapshot($container, $snapshot, array $options, $wait = false) 218 | { 219 | $opts = $this->getOptions($options); 220 | $opts['source']['type'] = 'snapshot'; 221 | $opts['source']['name'] = $container . '/' . $snapshot; 222 | 223 | return $this->create($opts, [], $wait); 224 | } 225 | 226 | /** 227 | * Replace the configuration of a image 228 | * 229 | * Configuration is overwritten, not merged. Accordingly, clients should 230 | * first call the info method to obtain the current configuration of a 231 | * image. The resulting object should be modified and then passed to 232 | * the update method. 233 | * 234 | * Note that LXD does not allow certain attributes to be changed (e.g. 235 | * status, status_code, stateful, 236 | * name, etc.) through this call. 237 | * 238 | * Example: Change image to be public 239 | * $image = $lxd->images->info('65df07147e458f356db90fa66d6f907a164739b554a40224984317eee729e92a'); 240 | * $image['public'] = true; 241 | * $lxd->images->replace('test', $image); 242 | * 243 | * @param string $fingerprint Fingerprint of image 244 | * @param array $options Options to replace 245 | * @param bool $wait Wait for operation to finish 246 | * @return array 247 | */ 248 | public function replace($fingerprint, $options, $wait = false) 249 | { 250 | $config = [ 251 | "project" => $this->client->getProject() 252 | ]; 253 | 254 | $response = $this->put($this->getEndpoint() . $fingerprint, $options, $config); 255 | 256 | if ($wait) { 257 | $response = $this->client->operations->wait($response['id']); 258 | } 259 | 260 | return $response; 261 | } 262 | 263 | /** 264 | * Delete an image 265 | * 266 | * @param string $fingerprint Fingerprint of image 267 | * @param bool $wait Wait for operation to finish 268 | * @return array 269 | */ 270 | public function remove($fingerprint, $wait = false) 271 | { 272 | $config = [ 273 | "project" => $this->client->getProject() 274 | ]; 275 | 276 | $response = $this->delete($this->getEndpoint() . $fingerprint, $config); 277 | 278 | if ($wait) { 279 | $response = $this->client->operations->wait($response['id']); 280 | } 281 | 282 | return $response; 283 | } 284 | 285 | public function __get($endpoint) 286 | { 287 | $class = __NAMESPACE__ . '\\Images\\' . ucfirst($endpoint); 288 | 289 | if (class_exists($class)) { 290 | return new $class($this->client); 291 | } else { 292 | throw new InvalidEndpointException( 293 | 'Endpoint ' . $class . ', not implemented.' 294 | ); 295 | } 296 | } 297 | 298 | /** 299 | * Get image source attribute 300 | * 301 | * @param array $options Options for creating image 302 | * @return array 303 | */ 304 | private function getSource($options) 305 | { 306 | foreach (['alias', 'fingerprint'] as $attr) { 307 | if (!empty($options[$attr])) { 308 | return [$attr => $options[$attr]]; 309 | } 310 | } 311 | 312 | throw new \Exception('Alias or Fingerprint must be set'); 313 | } 314 | 315 | /** 316 | * Get the options for creating image 317 | * 318 | * @param string $name Name of image 319 | * @param array $options Options for creating image 320 | * @return array 321 | */ 322 | private function getOptions($options) 323 | { 324 | $only = [ 325 | 'filename', 326 | 'public', 327 | 'properties', 328 | 'auto_update', 329 | 'aliases' 330 | ]; 331 | $opts = array_intersect_key($options, array_flip((array) $only)); 332 | 333 | return $opts; 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/Endpoint/InstaceBase.php: -------------------------------------------------------------------------------- 1 | $this->client->getProject(), 29 | "recursion" => $recursion 30 | ]; 31 | 32 | $containers = $this->get($this->getEndpoint(), $config); 33 | 34 | if ($recursion == 0) { 35 | foreach ($containers as &$container) { 36 | $container = str_replace('/' . $this->client->getApiVersion() . $this->getEndpoint(), '', $container); 37 | } 38 | } 39 | 40 | return $containers; 41 | } 42 | 43 | /** 44 | * Get information on a container 45 | * 46 | * @param string $name Name of container 47 | * @return object 48 | */ 49 | public function info($name) 50 | { 51 | $config = [ 52 | "project" => $this->client->getProject() 53 | ]; 54 | 55 | return $this->get($this->getEndpoint() . $name, $config); 56 | } 57 | 58 | /** 59 | * Get the current state of the container 60 | * 61 | * @param string $name Name of container 62 | * @return object 63 | */ 64 | public function state($name) 65 | { 66 | $config = [ 67 | "project" => $this->client->getProject() 68 | ]; 69 | 70 | return $this->get($this->getEndpoint() . $name . '/state', $config); 71 | } 72 | 73 | /** 74 | * Change the state of the container 75 | * 76 | * @param string $name Name of container 77 | * @param string $state State change action (stop, start, restart, freeze or unfreeze) 78 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 79 | * @param bool $force Whether to force the operation by killing the container 80 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 81 | * @param bool $wait Wait for operation to finish 82 | * @return object 83 | */ 84 | public function setState($name, $state, $timeout = 30, $force = true, $stateful = false, $wait = false) 85 | { 86 | $opts['action'] = $state; 87 | $opts['timeout'] = $timeout; 88 | $opts['force'] = $force; 89 | $opts['stateful'] = $stateful; 90 | 91 | $config = [ 92 | "project" => $this->client->getProject() 93 | ]; 94 | 95 | $response = $this->put($this->getEndpoint() . $name . '/state', $opts, $config); 96 | 97 | if ($wait) { 98 | $response = $this->client->operations->wait($response['id']); 99 | } 100 | 101 | return $response; 102 | } 103 | 104 | /** 105 | * Start the container 106 | * 107 | * @param string $name Name of container 108 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 109 | * @param bool $force Whether to force the operation by killing the container 110 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 111 | * @param bool $wait Wait for operation to finish 112 | * @return object 113 | */ 114 | public function start($name, $timeout = 30, $force = true, $stateful = false, $wait = false) 115 | { 116 | return $this->setState($name, 'start', $timeout, $force, $stateful, $wait); 117 | } 118 | 119 | /** 120 | * Stop the container 121 | * 122 | * @param string $name Name of container 123 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 124 | * @param bool $force Whether to force the operation by killing the container 125 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 126 | * @param bool $wait Wait for operation to finish 127 | * @return object 128 | */ 129 | public function stop($name, $timeout = 30, $force = true, $stateful = false, $wait = false) 130 | { 131 | return $this->setState($name, 'stop', $timeout, $force, $stateful, $wait); 132 | } 133 | 134 | /** 135 | * Restart the container 136 | * 137 | * @param string $name Name of container 138 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 139 | * @param bool $force Whether to force the operation by killing the container 140 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 141 | * @param bool $wait Wait for operation to finish 142 | * @return object 143 | */ 144 | public function restart($name, $timeout = 30, $force = true, $stateful = false, $wait = false) 145 | { 146 | return $this->setState($name, 'restart', $timeout, $force, $stateful, $wait); 147 | } 148 | 149 | /** 150 | * Freeze the container 151 | * 152 | * @param string $name Name of container 153 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 154 | * @param bool $force Whether to force the operation by killing the container 155 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 156 | * @param bool $wait Wait for operation to finish 157 | * @return object 158 | */ 159 | public function freeze($name, $timeout = 30, $force = true, $stateful = false, $wait = false) 160 | { 161 | return $this->setState($name, 'freeze', $timeout, $force, $stateful, $wait); 162 | } 163 | 164 | /** 165 | * Unfreeze the container 166 | * 167 | * @param string $name Name of container 168 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 169 | * @param bool $force Whether to force the operation by killing the container 170 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 171 | * @param bool $wait Wait for operation to finish 172 | * @return object 173 | */ 174 | public function unfreeze($name, $timeout = 30, $force = true, $stateful = false, $wait = false) 175 | { 176 | return $this->setState($name, 'unfreeze', $timeout, $force, $stateful, $wait); 177 | } 178 | 179 | /** 180 | * Create a container 181 | * 182 | * Create from an image (local or remote). The container will 183 | * be created in the stopped state. 184 | * 185 | * Example: Create container from image specified by alias 186 | * $lxd->containers->create( 187 | * "test", 188 | * [ 189 | * "alias" => "ubuntu/xenial/amd64", 190 | * ] 191 | * ); 192 | * 193 | * Example: Create container from image specified by fingerprint 194 | * $lxd->containers->create( 195 | * "test", 196 | * [ 197 | * "fingerprint" => "097e75d6f7419d3a5e204d8125582f2d7bdd4ee4c35bd324513321c645f0c415", 198 | * ] 199 | * ); 200 | * 201 | * Example: Create container based on most recent match of image properties 202 | * $lxd->containers->create( 203 | * "test", 204 | * [ 205 | * "properties" => [ 206 | * "os" => "ubuntu", 207 | * "release" => "14.04", 208 | * "architecture" => "x86_64", 209 | * ], 210 | * ] 211 | * ); 212 | * 213 | * Example: Create an empty container 214 | * $lxd->containers->create( 215 | * "test", 216 | * [ 217 | * "empty" => true, 218 | * ] 219 | * ); 220 | * 221 | * Example: Create container with custom configuration. 222 | * 223 | * # Set the MAC address of the container's eth0 device 224 | * $lxd->containers->create( 225 | * "test", 226 | * [ 227 | * "alias" => "ubuntu/xenial/amd64", 228 | * "config" => [ 229 | * "volatile.eth0.hwaddr" => "aa:bb:cc:dd:ee:ff", 230 | * ], 231 | * ] 232 | * ); 233 | * 234 | * Example: Create container and apply profiles to it 235 | * $lxd->containers->create( 236 | * "test", 237 | * [ 238 | * "alias" => "ubuntu/xenial/amd64", 239 | * "profiles" => ["migratable", "unconfined"], 240 | * ] 241 | * ); 242 | * 243 | * Example: Create container from a publicly-accessible remote image 244 | * $lxd->containers->create( 245 | * "test", 246 | * [ 247 | * "server" => "https://images.linuxcontainers.org:8443", 248 | * "alias" => "ubuntu/xenial/amd64", 249 | * ] 250 | * ); 251 | * 252 | * Example: Create container from a private remote image (authenticated by a secret) 253 | * $lxd->containers->create( 254 | * "test", 255 | * [ 256 | * "server" => "https://private.example.com:8443", 257 | * "alias" => "ubuntu/xenial/amd64", 258 | * "secret" => "my_secrect", 259 | * ] 260 | * ); 261 | * 262 | * @param string $name The name of the container 263 | * @param array $options Options to create the container 264 | * @param bool $wait Wait for operation to finish 265 | * @return object 266 | */ 267 | public function create($name, array $options, $wait = false, array $requestHeaders = [], string $target = "") 268 | { 269 | $source = $this->getSource($options); 270 | 271 | if (empty($options['empty']) && empty($source)) { 272 | throw new SourceImageException(); 273 | } 274 | 275 | if ($source == "backup") { 276 | $opts = $this->getOptions($name, $options); 277 | $requestHeaders["Content-Type"] = "application/octet-stream"; 278 | } elseif (!empty($options['source'])) { 279 | $opts = $this->getOptions($name, $options); 280 | $opts['source'] = $source; 281 | } elseif (isset($options['empty']) && $options['empty']) { 282 | $opts = $this->getEmptyOptions($name, $options); 283 | } elseif (!empty($options['server'])) { 284 | $opts = $this->getRemoteImageOptions($name, $source, $options); 285 | } else { 286 | $opts = $this->getLocalImageOptions($name, $source, $options); 287 | } 288 | 289 | $config = [ 290 | "project" => $this->client->getProject() 291 | ]; 292 | 293 | if (!empty($target)) { 294 | $config["target"] = $target; 295 | } 296 | 297 | 298 | $response = $this->post($this->getEndpoint(), $opts, $config, $requestHeaders); 299 | 300 | if ($wait) { 301 | $response = $this->client->operations->wait($response['id']); 302 | } 303 | 304 | return $response; 305 | } 306 | 307 | /** 308 | * Create a copy of an existing local container 309 | * 310 | * Example: Copy container 311 | * $lxd->containers->copy('existing', 'new'); 312 | * 313 | * Example: Copy container and apply profiles to it 314 | * $lxd->containers->copy( 315 | * 'existing', 316 | * 'new', 317 | * ['profiles' => ['default', 'public'] 318 | * ); 319 | * 320 | * @param string $name Name of existing container 321 | * @param string $copyName Name of copied container 322 | * @param array $options Options for copied container 323 | * @param bool $wait Wait for operation to finish 324 | * @return object 325 | */ 326 | public function copy( 327 | $name, 328 | $copyName, 329 | array $options = [], 330 | $wait = false, 331 | string $targetProject = "" 332 | ) { 333 | $opts = $this->getOptions($copyName, $options); 334 | 335 | $currentProject = $this->client->getProject(); 336 | 337 | $opts['source']['type'] = 'copy'; 338 | $opts['source']['source'] = $name; 339 | $opts['source']['project'] = $currentProject; 340 | 341 | $config = [ 342 | "project" => !empty($targetProject) ? $targetProject : $currentProject 343 | ]; 344 | 345 | $response = $this->post($this->getEndpoint(), $opts, $config); 346 | 347 | if ($wait) { 348 | $response = $this->client->operations->wait($response['id']); 349 | } 350 | 351 | return $response; 352 | } 353 | 354 | /** 355 | * Migrate a container 356 | * 357 | * If the container is running, it either must be shut down 358 | * first or criu must be installed on the source and destination 359 | * machines. 360 | * 361 | * Example: Migrate container 362 | * $lxd2 = new \Opensaucesystems\Lxd\Client($adapter, '1.0', 'https://lxd2.example.com:8443'); 363 | * $lxd->containers->migrate($lxd2, 'test'); 364 | * 365 | * @param object $destination lxd client Instance to destination lxd server 366 | * @param string $name Name of existing container 367 | * @param bool $wait Wait for operation to finish 368 | * @return object 369 | */ 370 | public function migrate( 371 | \Opensaucesystems\Lxd\Client $destination, 372 | $name, 373 | string $newName = "", 374 | $wait = false 375 | ) { 376 | if (empty($newName)) { 377 | $newName = $name; 378 | } 379 | 380 | return $destination->containers->create($newName, $this->initMigration($name, $newName), $wait); 381 | } 382 | 383 | /** 384 | * Initiate the migration of a container 385 | * 386 | * @param string $name Name of existing container 387 | * @return array 388 | */ 389 | public function initMigration($name, $newName) 390 | { 391 | $containerName = ""; 392 | 393 | if (strpos($name, "/") !== false) { 394 | $parts = explode("/", $name); 395 | $partsLength = count($parts); 396 | if ($partsLength == 0 || $partsLength > 2) { 397 | throw new \Exception("Snapshot name format not correct", 1); 398 | } 399 | $containerName = $parts[0]; 400 | $container = $this->snapshots->info($containerName, $parts[1]); 401 | $containerName = $parts[0] . "/snapshots/" . $parts[1]; 402 | } else { 403 | $containerName = $name; 404 | $container = $this->info($name); 405 | } 406 | 407 | 408 | 409 | $migration = $this->post($this->getEndpoint() . $containerName, [ 410 | 'name' => $newName, 411 | 'migration' => true, 412 | 'stateful' => false 413 | ]); 414 | 415 | $host = $this->client->host->info(); 416 | 417 | $hostAddress = $this->client->getUrl(); 418 | 419 | if ($hostAddress === "http://unix.socket/") { 420 | $hostAddress = "https://" . $host["environment"]["addresses"][0]; 421 | } 422 | 423 | $url = $hostAddress . '/' . $this->client->getApiVersion() . '/operations/' . $migration['id']; 424 | 425 | $settings = [ 426 | 'name' => $name, 427 | 'architecture' => $container['architecture'], 428 | 'config' => $container['config'], 429 | 'epehemeral' => $container['ephemeral'], 430 | 'profiles' => $container['profiles'], 431 | 'source' => [ 432 | 'type' => 'migration', 433 | 'operation' => $url, 434 | 'mode' => 'pull', 435 | 'certificate' => $host['environment']['certificate'], 436 | 'secrets' => $migration['metadata'], 437 | ] 438 | ]; 439 | 440 | if (!empty($container["devices"])) { 441 | $settings["devices"] = $container["devices"]; 442 | } 443 | 444 | return $settings; 445 | } 446 | 447 | /** 448 | * Replace the configuration of a container 449 | * 450 | * Configuration is overwritten, not merged. Accordingly, clients should 451 | * first call the info method to obtain the current configuration of a 452 | * container. The resulting object should be modified and then passed to 453 | * the update method. 454 | * 455 | * Note that LXD does not allow certain attributes to be changed (e.g. 456 | * status, status_code, stateful, 457 | * name, etc.) through this call. 458 | * 459 | * Example: Change container to be ephemeral (i.e. it will be deleted when stopped) 460 | * $container = $lxd->containers->show('test'); 461 | * $container->ephemeral = true; 462 | * $lxd->containers->replace('test', $container); 463 | * 464 | * @param string $name Name of container 465 | * @param object $container Container to update 466 | * @param bool $wait Wait for operation to finish 467 | * @return object 468 | */ 469 | public function replace($name, $container, $wait = false) 470 | { 471 | $config = [ 472 | "project" => $this->client->getProject() 473 | ]; 474 | 475 | $response = $this->put($this->getEndpoint() . $name, $container, $config); 476 | 477 | if ($wait) { 478 | $response = $this->client->operations->wait($response['id']); 479 | } 480 | 481 | return $response; 482 | } 483 | 484 | /** 485 | * Update the configuration of a container 486 | * 487 | * Example: Change containers cpu-limit and rootfs size 488 | * $newconfig = [ 489 | * 'config' => [ 490 | * 'limits.cpu' => 4 491 | * ], 492 | * 'devices' => [ 493 | * 'rootfs' => [ 494 | * 'size' => '5GB' 495 | * ] 496 | * ] 497 | * ]; 498 | * $lxd->containers->update('test', $newconfig); 499 | * 500 | * @param string $name Name of container 501 | * @param array $config Options to create the container 502 | * @param bool $wait Wait for operation to finish 503 | * @return object 504 | */ 505 | public function update($name, $config, $wait = false) 506 | { 507 | $options = [ 508 | "project" => $this->client->getProject() 509 | ]; 510 | 511 | $response = $this->patch($this->getEndpoint() . $name, $config, $options); 512 | 513 | if ($wait) { 514 | $response = $this->client->operations->wait($response['id']); 515 | } 516 | 517 | return $response; 518 | } 519 | 520 | /** 521 | * Rename a container 522 | * 523 | * @param string $name Name of existing container 524 | * @param string $newName Name of new container 525 | * @param bool $wait Wait for operation to finish 526 | * @return array 527 | */ 528 | public function rename($name, $newName, $wait = false) 529 | { 530 | $opts['name'] = $newName; 531 | 532 | $config = [ 533 | "project" => $this->client->getProject() 534 | ]; 535 | 536 | $response = $this->post($this->getEndpoint() . $name, $opts, $config); 537 | 538 | if ($wait) { 539 | $response = $this->client->operations->wait($response['id']); 540 | } 541 | 542 | return $response; 543 | } 544 | 545 | /** 546 | * Delete a container 547 | * 548 | * @param string $name Name of container 549 | * @param bool $wait Wait for operation to finish 550 | * @return array 551 | */ 552 | public function remove($name, $wait = false) 553 | { 554 | $config = [ 555 | "project" => $this->client->getProject() 556 | ]; 557 | 558 | $response = $this->delete($this->getEndpoint() . $name, $config); 559 | 560 | if ($wait) { 561 | $response = $this->client->operations->wait($response['id']); 562 | } 563 | 564 | return $response; 565 | } 566 | 567 | /** 568 | * Execute a command in a container 569 | * 570 | * @param string $name Name of container 571 | * @param array|string $command Command and arguments 572 | * @param bool $record Whether to store stdout and stderr 573 | * @param array $environment An associative array, the key will be the environment variable name 574 | * @param bool $wait Wait for operation to finish 575 | * @return object 576 | */ 577 | public function execute($name, $command, $record = false, array $environment = [], $wait = false) 578 | { 579 | if (is_string($command)) { 580 | $command = $this->split($command); 581 | } 582 | 583 | $opts['command'] = $command; 584 | 585 | if (!empty($environment)) { 586 | $opts['environment'] = $environment; 587 | } 588 | 589 | if ($record === true) { 590 | $opts['record-output'] = true; 591 | } 592 | 593 | $opts['wait-for-websocket'] = false; 594 | $opts['interactive'] = false; 595 | 596 | $config = [ 597 | "project" => $this->client->getProject() 598 | ]; 599 | 600 | $response = $this->post($this->getEndpoint() . $name . '/exec', $opts, $config); 601 | 602 | if ($wait) { 603 | $response = $this->client->operations->wait($response['id']); 604 | $logs = []; 605 | $output = $response['metadata']['output']; 606 | $return = $response['metadata']['return']; 607 | unset($response); 608 | 609 | foreach ($output as $log) { 610 | $response['output'][] = str_replace( 611 | '/' . $this->client->getApiVersion() . $this->getEndpoint() . $name . '/logs/', 612 | '', 613 | $log 614 | ); 615 | } 616 | 617 | $response['return'] = $return; 618 | } 619 | 620 | return $response; 621 | } 622 | 623 | public function __get($endpoint) 624 | { 625 | $class = __NAMESPACE__ . '\\Instance\\' . ucfirst($endpoint); 626 | 627 | if (class_exists($class)) { 628 | $class = new $class($this->client); 629 | $class->setEndpoint($this->getEndpoint()); 630 | return $class; 631 | } else { 632 | throw new InvalidEndpointException( 633 | 'Endpoint ' . $class . ', not implemented.' 634 | ); 635 | } 636 | } 637 | 638 | /** 639 | * Get image source attribute 640 | * 641 | * @param array $options Options for creating container 642 | * @return array 643 | */ 644 | private function getSource($options) 645 | { 646 | if (isset($options['source'])) { 647 | $only = [ 648 | 'type', 649 | 'mode', 650 | 'source', 651 | 'server', 652 | 'operation', 653 | 'protocol', 654 | 'base-image', 655 | 'certificate', 656 | 'secret', 657 | 'secrets', 658 | 'alias', 659 | 'fingerprint', 660 | 'properties', 661 | 'live', 662 | 'backup' 663 | ]; 664 | $opts = array_intersect_key($options, array_flip((array) $only)); 665 | 666 | return $opts['source']; 667 | } 668 | 669 | foreach (['alias', 'fingerprint', 'properties'] as $attr) { 670 | if (!empty($options[$attr])) { 671 | return [$attr => $options[$attr]]; 672 | } 673 | } 674 | 675 | return []; 676 | } 677 | 678 | /** 679 | * Get the options for creating container 680 | * 681 | * @param string $name Name of container 682 | * @param array $options Options for creating container 683 | * @return array 684 | */ 685 | private function getOptions($name, $options) 686 | { 687 | if (isset($options["source"]) && $options["source"] == "backup") { 688 | if (!isset($options["file"])) { 689 | throw new \Exception('source => backup requires file => file_get_contents(BACKUP_PATH) '); 690 | } 691 | return $options["file"]; 692 | } 693 | 694 | $only = [ 695 | 'architecture', 696 | 'profiles', 697 | 'ephemeral', 698 | 'config', 699 | 'devices', 700 | 'instance_type', 701 | 'type' 702 | ]; 703 | $opts = array_intersect_key($options, array_flip((array) $only)); 704 | $opts['name'] = $name; 705 | 706 | return $opts; 707 | } 708 | 709 | /** 710 | * Get options for creating an empty container 711 | * 712 | * @param string $name Name of container 713 | * @param array $options Options for creating container 714 | * @return array 715 | */ 716 | private function getEmptyOptions($name, $options) 717 | { 718 | $attrs = [ 719 | 'alias', 720 | 'fingerprint', 721 | 'properties', 722 | 'server', 723 | 'secret', 724 | 'protocol', 725 | 'certificate', 726 | ]; 727 | 728 | foreach ($attrs as $attr) { 729 | if (!empty($options[$attr])) { 730 | throw new \Exception('empty => true is not compatible with ' . $attr); 731 | } 732 | } 733 | 734 | $opts = $this->getOptions($name, $options); 735 | $opts['source']['type'] = 'none'; 736 | 737 | return $opts; 738 | } 739 | 740 | /** 741 | * Get options for creating a container from remote image 742 | * 743 | * @param string $name Name of container 744 | * @param array $source Source of the image 745 | * @param array $options Options for creating container 746 | * @return array 747 | */ 748 | private function getRemoteImageOptions($name, $source, $options) 749 | { 750 | if (isset($options['protocol']) && !in_array($options['protocol'], ['lxd', 'simplestreams'])) { 751 | throw new \Exception('Invalid protocol. Valid choices: lxd, simplestreams'); 752 | } 753 | 754 | $only = [ 755 | 'server', 756 | 'secret', 757 | 'protocol', 758 | 'certificate', 759 | ]; 760 | $remoteOptions = array_intersect_key($options, array_flip((array) $only)); 761 | 762 | $opts = $this->getOptions($name, $options); 763 | $opts['source'] = array_merge($source, $remoteOptions); 764 | $opts['source']['type'] = 'image'; 765 | $opts['source']['mode'] = 'pull'; 766 | 767 | return $opts; 768 | } 769 | 770 | /** 771 | * Get options for creating a container from local image 772 | * 773 | * @param string $name Name of container 774 | * @param array $source Source of the image 775 | * @param array $options Options for creating container 776 | * @return array 777 | */ 778 | private function getLocalImageOptions($name, $source, $options) 779 | { 780 | $attrs = [ 781 | 'secret', 782 | 'protocol', 783 | 'certificate', 784 | ]; 785 | 786 | foreach ($attrs as $attr) { 787 | if (!empty($options[$attr])) { 788 | throw new \Exception('Only setting remote server is compatible with ' . $attr); 789 | } 790 | } 791 | 792 | $opts = $this->getOptions($name, $options); 793 | $opts['source'] = $source; 794 | $opts['source']['type'] = 'image'; 795 | 796 | return $opts; 797 | } 798 | 799 | /** 800 | * To split a string 801 | * 802 | * @param string $string String to split into array 803 | * @return array 804 | */ 805 | private function split($string) 806 | { 807 | $pattern = '/\s*(?>([^\s\\\'\"]+)|\'([^\']*)\'|"((?:[^\"\\\\]|\\.)*)"|(\\.?)|(\S))(\s|\z)?/'; 808 | preg_match_all($pattern, $string, $matches); 809 | $words = []; 810 | 811 | foreach ($matches[0] as $value) { 812 | if (!empty($value)) { 813 | $words[] = trim(trim($value), '\'"'); 814 | } 815 | } 816 | 817 | return $words; 818 | } 819 | } 820 | --------------------------------------------------------------------------------