├── .editorconfig ├── .styleci.yml ├── CHANGELOG.md ├── CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── docs ├── certificates.md ├── configuration.md ├── containers.md ├── host.md ├── images.md ├── networks.md ├── operations.md └── profiles.md ├── phpunit.xml └── src ├── Client.php ├── Endpoint ├── AbstractEndpoint.php ├── Certificates.php ├── Cluster.php ├── Cluster │ └── Members.php ├── Containers.php ├── Host.php ├── Images.php ├── Images │ └── Aliases.php ├── InstaceBase.php ├── Instance │ ├── Backups.php │ ├── Files.php │ ├── Logs.php │ └── Snapshots.php ├── Instances.php ├── Networks.php ├── Operations.php ├── Profiles.php ├── Projects.php ├── Resources.php ├── Storage.php ├── Storage │ ├── Resources.php │ └── Volumes.php ├── VirtualMachines.php ├── Warnings.php └── Warnings │ └── Status.php ├── Exception ├── AuthenticationFailedException.php ├── BadRequestException.php ├── ClientConnectionException.php ├── ConflictException.php ├── InvalidEndpointException.php ├── NotFoundException.php ├── OperationException.php ├── ServerException.php └── SourceImageException.php └── HttpClient ├── Message └── ResponseMediator.php └── Plugin ├── LxdExceptionThower.php ├── PathPrepend.php └── PathTrimEnd.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | -------------------------------------------------------------------------------- /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 | # [0.25.0] 8 | 9 | ## Added 10 | Token paramater when adding new certificate to LXD 11 | 12 | # [0.24.0] 13 | 14 | ## Fixed 15 | Cant restore snapshot from VM 16 | 17 | ## Dev 18 | Add some dynamic properties and return types to satisfy phpstan 19 | 20 | # [0.23.0] 21 | 22 | ## Changed 23 | - Handle 400 responses from LXD (the generic error response) this may impact 24 | exisiting code. 25 | 26 | # [0.22.1] 27 | 28 | ## Changed 29 | Don't set "config" and/or "device" properties when replacing profile 30 | 31 | 32 | # [0.22.0] 33 | 34 | ## Added 35 | - Recursion parameter for all project method 36 | 37 | ## Fixed 38 | - If source host is using the socket when migrating an instance, use host 39 | environment address instead of client url 40 | 41 | # [0.21.0] 42 | 43 | ## Added 44 | - Recursion parameter for getting snapshots 45 | - Recursion parameter for getting networks 46 | 47 | ## Fixed 48 | - Cant create storage pool when sending empty config 49 | 50 | # [0.20.0] 51 | ## Added 52 | - Create storage pool volume 53 | - Delete storage pool volume 54 | 55 | # [0.19.1] 56 | 57 | ## Added 58 | - Optional target project parameter to copy instance 59 | 60 | # [0.19.0] 61 | 62 | ## Added 63 | - Support for the warnings API 64 | 65 | # [0.18.2] 66 | 67 | ## Change 68 | - Support recursion param on get all pools 69 | 70 | # [0.18.1] 71 | 72 | ## Added 73 | - Get volume info 74 | 75 | # [0.18.0] 76 | 77 | ## Added 78 | - Recursion parameter for getting images (#17) 79 | 80 | ## Changed 81 | - Make networks project aware (#16) 82 | 83 | 84 | # [0.17.0] 85 | 86 | ## Added 87 | - Instance files are now project aware 88 | - Add the timeout parameter correctly when waiting for an operation (@dhzavann) 89 | 90 | ## Changed 91 | - Delay check if vms are supported until needed (@TonyBogdanov) 92 | 93 | # [0.16.4] 94 | 95 | ## Added 96 | - Recursion parameter for getting profiles 97 | 98 | # [0.16.3] 99 | 100 | ## Added 101 | - Recursion parameter for getting cluster members 102 | 103 | # [0.16.2] 104 | 105 | ## Fixed 106 | - Cant load project info because path is wrong 107 | 108 | # [0.16.1] 109 | 110 | ## Added 111 | - Support passing alias param when creating image 112 | - Use "instance" instead of "container" when creating image from "container" 113 | 114 | # [0.16.0] 115 | 116 | ## Added 117 | - Recursion parameter to get all instances / containers / vms 118 | 119 | # [0.15.2] 120 | 121 | ## Added 122 | - Added optional target parameter for creating instance 123 | 124 | # [0.15.1] 125 | 126 | ## Fixed 127 | - Files method using the wrong param for headers 128 | 129 | # [0.15.0] 130 | 131 | ## Added 132 | - Added an instance class that you should use instead of containers, it will 133 | fall back to the `/containers` endpoint if your host doesn't support `/instances` 134 | which is the agnostic way of dealing with both containers and virtual machines 135 | 136 | 137 | # [0.14.0] 138 | 139 | ## Added 140 | - Provide backup as source type & file to create container from backup file 141 | 142 | ## [0.13.2] 143 | 144 | ## Fixed 145 | - not being able to cache responses due to bad namespaces 146 | 147 | 148 | ## [0.13.1] 149 | 150 | ## Added 151 | - Delete container file (#11 @TonyBogdanov) 152 | 153 | ## [0.13.0] 154 | 155 | ## Added 156 | - Some of the cluster endpoints 157 | 158 | ## [0.12.3] 159 | 160 | ## Fixed 161 | - Migrating snapshot of runnin container (#9) 162 | 163 | ## [0.12.2] 164 | 165 | ## Added 166 | - Support for using container snapshot as source for migration 167 | 168 | ## [0.12.1] 169 | 170 | ### Added 171 | - Backup export method 172 | 173 | ## [0.12.0] 174 | 175 | ### Added 176 | - Backup endpoints 177 | 178 | ## [0.6.0] - 2017-01-23 179 | 180 | ### Added 181 | - Documentation 182 | - container migration 183 | 184 | ### Deprecated 185 | - Nothing 186 | 187 | ### Fixed 188 | - Nothing 189 | 190 | ### Removed 191 | - Nothing 192 | 193 | ### Security 194 | - Nothing 195 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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": "~5.5|~7.0", 29 | "psr/http-message": "^1.0", 30 | "php-http/httplug": "^1.0", 31 | "php-http/discovery": "^1.0", 32 | "php-http/client-implementation": "^1.0", 33 | "php-http/client-common": "^1.1", 34 | "php-http/cache-plugin": "^1.6" 35 | }, 36 | "require-dev": { 37 | "phpunit/phpunit": "4.*", 38 | "mockery/mockery": "^0.9.5", 39 | "php-http/guzzle6-adapter": "^1.0", 40 | "guzzlehttp/psr7": "^1.2", 41 | "php-http/mock-client": "^0.3", 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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->projectName = $projectName; 86 | 87 | $this->addPlugin(new LxdExceptionThower()); 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/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/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, $password = null, $name = null, $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/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 | -------------------------------------------------------------------------------- /src/Endpoint/Cluster/Members.php: -------------------------------------------------------------------------------- 1 | 0) { 26 | $config["recursion"] = $recursion; 27 | } 28 | 29 | foreach ($this->get($this->getEndpoint(), $config) as $member) { 30 | $members[] = str_replace('/'.$this->client->getApiVersion().$this->getEndpoint(), '', $member); 31 | } 32 | 33 | return $members; 34 | } 35 | 36 | public function info(string $name) 37 | { 38 | return $this->get($this->getEndpoint()."$name"); 39 | } 40 | 41 | public function rename(string $name, string $newName) 42 | { 43 | return $this->post($this->getEndpoint()."$name", ["server_name"=>$newName]); 44 | } 45 | 46 | public function remove(string $name) 47 | { 48 | return $this->delete($this->getEndpoint()."$name"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Endpoint/Containers.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/Images.php: -------------------------------------------------------------------------------- 1 | $this->client->getProject() 30 | ]; 31 | 32 | if ($recursion > 0) { 33 | $config["recursion"] = $recursion; 34 | } 35 | 36 | foreach ($this->get($this->getEndpoint(), $config) as $image) { 37 | $images[] = str_replace('/'.$this->client->getApiVersion().$this->getEndpoint(), '', $image); 38 | } 39 | 40 | return $images; 41 | } 42 | 43 | /** 44 | * Get information on an image 45 | * 46 | * @param string $fingerprint Fingerprint of image 47 | * @param string $secret Secret to access private image by untrusted client 48 | * @return object 49 | */ 50 | public function info($fingerprint, $secret = null) :array 51 | { 52 | $endpoint = $this->getEndpoint().$fingerprint; 53 | if (!empty($secret)) { 54 | $endpoint .= '?secret='.$secret; 55 | } 56 | 57 | $config = [ 58 | "project"=>$this->client->getProject() 59 | ]; 60 | 61 | return $this->get($endpoint, $config); 62 | } 63 | 64 | /** 65 | * Create and publish a new image 66 | * 67 | * Ways to create an image: 68 | * @todo Standard http file upload 69 | * # Source image (transfers a remote image) 70 | * # Source container (makes an image out of a local container) 71 | * @todo Remote image URL (downloads a remote image) 72 | * 73 | * @param array $options Options to create the image 74 | * @param bool $wait Wait for operation to finish 75 | * @return object 76 | */ 77 | public function create(array $options, $headers = [], $wait = false) 78 | { 79 | $config = [ 80 | "project"=>$this->client->getProject() 81 | ]; 82 | 83 | $response = $this->post($this->getEndpoint(), $options, $config, $headers); 84 | 85 | if ($wait) { 86 | $response = $this->client->operations->wait($response['id']); 87 | } 88 | 89 | return $response; 90 | } 91 | 92 | /** 93 | * Import an image from a remote server 94 | * 95 | * Example: Import an image by alias 96 | * $lxd->images->createFromRemote( 97 | * "https://images.linuxcontainers.org:8443", 98 | * [ 99 | * "alias" => "ubuntu/xenial/amd64", 100 | * ] 101 | * ); 102 | * 103 | * Example: Import an image by fingerprint 104 | * $lxd->images->createFromRemote( 105 | * "https://images.linuxcontainers.org:8443", 106 | * [ 107 | * "fingerprint" => "65df07147e458f356db90fa66d6f907a164739b554a40224984317eee729e92a", 108 | * ] 109 | * ); 110 | * 111 | * Example: Import image and automatically update it when it is updated on the remote server 112 | * $lxd->images->createFromRemote( 113 | * "https://images.linuxcontainers.org:8443", 114 | * [ 115 | * "alias" => "ubuntu/xenial/amd64", 116 | * ], 117 | * true 118 | * ); 119 | * 120 | * @param string $server Remote server 121 | * @param array $options Options to create the image 122 | * @param bool $autoUpdate Whether or not the image should be automatically updated from the remote server 123 | * @param bool $wait Wait for operation to finish 124 | * @return object 125 | */ 126 | public function createFromRemote($server, array $options, $autoUpdate = false, $wait = false) 127 | { 128 | $source = $this->getSource($options); 129 | 130 | if (isset($options['protocol']) && !in_array($options['protocol'], ['lxd', 'simplestreams'])) { 131 | throw new \Exception('Invalid protocol. Valid choices: lxd, simplestreams'); 132 | } 133 | 134 | $only = [ 135 | 'secret', 136 | 'protocol', 137 | 'certificate', 138 | ]; 139 | $remoteOptions = array_intersect_key($options, array_flip((array) $only)); 140 | 141 | $opts = $this->getOptions($options); 142 | $opts['auto_update'] = $autoUpdate; 143 | $opts['source'] = array_merge($source, $remoteOptions); 144 | $opts['source']['type'] = 'image'; 145 | $opts['source']['mode'] = 'pull'; 146 | $opts['source']['server'] = $server; 147 | 148 | return $this->create($opts, [], $wait); 149 | } 150 | 151 | /** 152 | * Create an image from a container 153 | * 154 | * Example: Create a private image from container 155 | * $lxd->images->createFromContainer("container_name"); 156 | * 157 | * Example: Create a public image from container 158 | * $lxd->images->createFromContainer( 159 | * "container_name", 160 | * [ 161 | * "public" => true, 162 | * ] 163 | * ); 164 | * 165 | * Example: Store properties with the new image, and override its filename 166 | * $lxd->images->createFromContainer( 167 | * "container_name", 168 | * [ 169 | * "filename" => "ubuntu-trusty.tar.gz", 170 | * "properties" => ["os" => "Ubuntu"], 171 | * ] 172 | * ); 173 | * 174 | * @param string $name The name of the container 175 | * @param array $options Options to create the container 176 | * @param bool $wait Wait for operation to finish 177 | * @return object 178 | */ 179 | public function createFromContainer($name, array $options, $wait = false) 180 | { 181 | $opts = $this->getOptions($options); 182 | $opts['source']['type'] = 'instance'; 183 | $opts['source']['name'] = $name; 184 | 185 | return $this->create($opts, [], $wait); 186 | } 187 | 188 | /** 189 | * Create an image from a snapshot 190 | * 191 | * Example: Create a private image from snapshot 192 | * $lxd->images->createFromSnapshot("container_name", "snapshot_name"); 193 | * 194 | * Example: Create a public image from snapshot 195 | * $lxd->images->createFromContainer( 196 | * "container_name", 197 | * "snapshot_name", 198 | * [ 199 | * "public" => true, 200 | * ] 201 | * ); 202 | * 203 | * Example: Store properties with the new image, and override its filename 204 | * $lxd->images->createFromContainer( 205 | * "container_name", 206 | * "snapshot_name", 207 | * [ 208 | * "filename" => "ubuntu-trusty.tar.gz", 209 | * "properties" => ["os" => "Ubuntu"], 210 | * ] 211 | * ); 212 | * 213 | * @param string $container The name of the container 214 | * @param string $snapshot The name of the snapshot 215 | * @param array $options Options to create the container 216 | * @param bool $wait Wait for operation to finish 217 | * @return object 218 | */ 219 | public function createFromSnapshot($container, $snapshot, array $options, $wait = false) 220 | { 221 | $opts = $this->getOptions($options); 222 | $opts['source']['type'] = 'snapshot'; 223 | $opts['source']['name'] = $container.'/'.$snapshot; 224 | 225 | return $this->create($opts, [], $wait); 226 | } 227 | 228 | /** 229 | * Replace the configuration of a image 230 | * 231 | * Configuration is overwritten, not merged. Accordingly, clients should 232 | * first call the info method to obtain the current configuration of a 233 | * image. The resulting object should be modified and then passed to 234 | * the update method. 235 | * 236 | * Note that LXD does not allow certain attributes to be changed (e.g. 237 | * status, status_code, stateful, 238 | * name, etc.) through this call. 239 | * 240 | * Example: Change image to be public 241 | * $image = $lxd->images->info('65df07147e458f356db90fa66d6f907a164739b554a40224984317eee729e92a'); 242 | * $image['public'] = true; 243 | * $lxd->images->replace('test', $image); 244 | * 245 | * @param string $fingerprint Fingerprint of image 246 | * @param array $options Options to replace 247 | * @param bool $wait Wait for operation to finish 248 | * @return array 249 | */ 250 | public function replace($fingerprint, $options, $wait = false) 251 | { 252 | $config = [ 253 | "project"=>$this->client->getProject() 254 | ]; 255 | 256 | $response = $this->put($this->getEndpoint().$fingerprint, $options, $config); 257 | 258 | if ($wait) { 259 | $response = $this->client->operations->wait($response['id']); 260 | } 261 | 262 | return $response; 263 | } 264 | 265 | /** 266 | * Delete an image 267 | * 268 | * @param string $fingerprint Fingerprint of image 269 | * @param bool $wait Wait for operation to finish 270 | * @return array 271 | */ 272 | public function remove($fingerprint, $wait = false) 273 | { 274 | $config = [ 275 | "project"=>$this->client->getProject() 276 | ]; 277 | 278 | $response = $this->delete($this->getEndpoint().$fingerprint, $config); 279 | 280 | if ($wait) { 281 | $response = $this->client->operations->wait($response['id']); 282 | } 283 | 284 | return $response; 285 | } 286 | 287 | public function __get($endpoint) 288 | { 289 | $class = __NAMESPACE__.'\\Images\\'.ucfirst($endpoint); 290 | 291 | if (class_exists($class)) { 292 | return new $class($this->client); 293 | } else { 294 | throw new InvalidEndpointException( 295 | 'Endpoint '.$class.', not implemented.' 296 | ); 297 | } 298 | } 299 | 300 | /** 301 | * Get image source attribute 302 | * 303 | * @param array $options Options for creating image 304 | * @return array 305 | */ 306 | private function getSource($options) 307 | { 308 | foreach (['alias', 'fingerprint'] as $attr) { 309 | if (!empty($options[$attr])) { 310 | return [$attr => $options[$attr]]; 311 | } 312 | } 313 | 314 | throw new \Exception('Alias or Fingerprint must be set'); 315 | } 316 | 317 | /** 318 | * Get the options for creating image 319 | * 320 | * @param string $name Name of image 321 | * @param array $options Options for creating image 322 | * @return array 323 | */ 324 | private function getOptions($options) 325 | { 326 | $only = [ 327 | 'filename', 328 | 'public', 329 | 'properties', 330 | 'auto_update', 331 | 'aliases' 332 | ]; 333 | $opts = array_intersect_key($options, array_flip((array) $only)); 334 | 335 | return $opts; 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Endpoint/InstaceBase.php: -------------------------------------------------------------------------------- 1 | $this->client->getProject() 31 | ]; 32 | 33 | if ($recursion > 0) { 34 | $config["recursion"] = $recursion; 35 | } 36 | 37 | foreach ($this->get($this->getEndpoint(), $config) as $container) { 38 | $containers[] = str_replace('/'.$this->client->getApiVersion().$this->getEndpoint(), '', $container); 39 | } 40 | 41 | return $containers; 42 | } 43 | 44 | /** 45 | * Get information on a container 46 | * 47 | * @param string $name Name of container 48 | * @return object 49 | */ 50 | public function info($name) 51 | { 52 | $config = [ 53 | "project"=>$this->client->getProject() 54 | ]; 55 | 56 | return $this->get($this->getEndpoint().$name, $config); 57 | } 58 | 59 | /** 60 | * Get the current state of the container 61 | * 62 | * @param string $name Name of container 63 | * @return object 64 | */ 65 | public function state($name) 66 | { 67 | $config = [ 68 | "project"=>$this->client->getProject() 69 | ]; 70 | 71 | return $this->get($this->getEndpoint().$name.'/state', $config); 72 | } 73 | 74 | /** 75 | * Change the state of the container 76 | * 77 | * @param string $name Name of container 78 | * @param string $state State change action (stop, start, restart, freeze or unfreeze) 79 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 80 | * @param bool $force Whether to force the operation by killing the container 81 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 82 | * @param bool $wait Wait for operation to finish 83 | * @return object 84 | */ 85 | public function setState($name, $state, $timeout = 30, $force = true, $stateful = false, $wait = false) 86 | { 87 | $opts['action'] = $state; 88 | $opts['timeout'] = $timeout; 89 | $opts['force'] = $force; 90 | $opts['stateful'] = $stateful; 91 | 92 | $config = [ 93 | "project"=>$this->client->getProject() 94 | ]; 95 | 96 | $response = $this->put($this->getEndpoint().$name.'/state', $opts, $config); 97 | 98 | if ($wait) { 99 | $response = $this->client->operations->wait($response['id']); 100 | } 101 | 102 | return $response; 103 | } 104 | 105 | /** 106 | * Start the container 107 | * 108 | * @param string $name Name of container 109 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 110 | * @param bool $force Whether to force the operation by killing the container 111 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 112 | * @param bool $wait Wait for operation to finish 113 | * @return object 114 | */ 115 | public function start($name, $timeout = 30, $force = true, $stateful = false, $wait = false) 116 | { 117 | return $this->setState($name, 'start', $timeout, $force, $stateful, $wait); 118 | } 119 | 120 | /** 121 | * Stop the container 122 | * 123 | * @param string $name Name of container 124 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 125 | * @param bool $force Whether to force the operation by killing the container 126 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 127 | * @param bool $wait Wait for operation to finish 128 | * @return object 129 | */ 130 | public function stop($name, $timeout = 30, $force = true, $stateful = false, $wait = false) 131 | { 132 | return $this->setState($name, 'stop', $timeout, $force, $stateful, $wait); 133 | } 134 | 135 | /** 136 | * Restart the container 137 | * 138 | * @param string $name Name of container 139 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 140 | * @param bool $force Whether to force the operation by killing the container 141 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 142 | * @param bool $wait Wait for operation to finish 143 | * @return object 144 | */ 145 | public function restart($name, $timeout = 30, $force = true, $stateful = false, $wait = false) 146 | { 147 | return $this->setState($name, 'restart', $timeout, $force, $stateful, $wait); 148 | } 149 | 150 | /** 151 | * Freeze the container 152 | * 153 | * @param string $name Name of container 154 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 155 | * @param bool $force Whether to force the operation by killing the container 156 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 157 | * @param bool $wait Wait for operation to finish 158 | * @return object 159 | */ 160 | public function freeze($name, $timeout = 30, $force = true, $stateful = false, $wait = false) 161 | { 162 | return $this->setState($name, 'freeze', $timeout, $force, $stateful, $wait); 163 | } 164 | 165 | /** 166 | * Unfreeze the container 167 | * 168 | * @param string $name Name of container 169 | * @param int $timeout Time after which the operation is considered to have failed (default: no timeout) 170 | * @param bool $force Whether to force the operation by killing the container 171 | * @param bool $stateful Whether to store/restore runtime state (only valid for stop and start, default: false) 172 | * @param bool $wait Wait for operation to finish 173 | * @return object 174 | */ 175 | public function unfreeze($name, $timeout = 30, $force = true, $stateful = false, $wait = false) 176 | { 177 | return $this->setState($name, 'unfreeze', $timeout, $force, $stateful, $wait); 178 | } 179 | 180 | /** 181 | * Create a container 182 | * 183 | * Create from an image (local or remote). The container will 184 | * be created in the stopped state. 185 | * 186 | * Example: Create container from image specified by alias 187 | * $lxd->containers->create( 188 | * "test", 189 | * [ 190 | * "alias" => "ubuntu/xenial/amd64", 191 | * ] 192 | * ); 193 | * 194 | * Example: Create container from image specified by fingerprint 195 | * $lxd->containers->create( 196 | * "test", 197 | * [ 198 | * "fingerprint" => "097e75d6f7419d3a5e204d8125582f2d7bdd4ee4c35bd324513321c645f0c415", 199 | * ] 200 | * ); 201 | * 202 | * Example: Create container based on most recent match of image properties 203 | * $lxd->containers->create( 204 | * "test", 205 | * [ 206 | * "properties" => [ 207 | * "os" => "ubuntu", 208 | * "release" => "14.04", 209 | * "architecture" => "x86_64", 210 | * ], 211 | * ] 212 | * ); 213 | * 214 | * Example: Create an empty container 215 | * $lxd->containers->create( 216 | * "test", 217 | * [ 218 | * "empty" => true, 219 | * ] 220 | * ); 221 | * 222 | * Example: Create container with custom configuration. 223 | * 224 | * # Set the MAC address of the container's eth0 device 225 | * $lxd->containers->create( 226 | * "test", 227 | * [ 228 | * "alias" => "ubuntu/xenial/amd64", 229 | * "config" => [ 230 | * "volatile.eth0.hwaddr" => "aa:bb:cc:dd:ee:ff", 231 | * ], 232 | * ] 233 | * ); 234 | * 235 | * Example: Create container and apply profiles to it 236 | * $lxd->containers->create( 237 | * "test", 238 | * [ 239 | * "alias" => "ubuntu/xenial/amd64", 240 | * "profiles" => ["migratable", "unconfined"], 241 | * ] 242 | * ); 243 | * 244 | * Example: Create container from a publicly-accessible remote image 245 | * $lxd->containers->create( 246 | * "test", 247 | * [ 248 | * "server" => "https://images.linuxcontainers.org:8443", 249 | * "alias" => "ubuntu/xenial/amd64", 250 | * ] 251 | * ); 252 | * 253 | * Example: Create container from a private remote image (authenticated by a secret) 254 | * $lxd->containers->create( 255 | * "test", 256 | * [ 257 | * "server" => "https://private.example.com:8443", 258 | * "alias" => "ubuntu/xenial/amd64", 259 | * "secret" => "my_secrect", 260 | * ] 261 | * ); 262 | * 263 | * @param string $name The name of the container 264 | * @param array $options Options to create the container 265 | * @param bool $wait Wait for operation to finish 266 | * @return object 267 | */ 268 | public function create($name, array $options, $wait = false, array $requestHeaders = [], string $target = "") 269 | { 270 | $source = $this->getSource($options); 271 | 272 | if (empty($options['empty']) && empty($source)) { 273 | throw new SourceImageException(); 274 | } 275 | 276 | if ($source == "backup") { 277 | $opts = $this->getOptions($name, $options); 278 | $requestHeaders["Content-Type"] = "application/octet-stream"; 279 | } elseif (!empty($options['source'])) { 280 | $opts = $this->getOptions($name, $options); 281 | $opts['source'] = $source; 282 | } elseif (isset($options['empty']) && $options['empty']) { 283 | $opts = $this->getEmptyOptions($name, $options); 284 | } elseif (!empty($options['server'])) { 285 | $opts = $this->getRemoteImageOptions($name, $source, $options); 286 | } else { 287 | $opts = $this->getLocalImageOptions($name, $source, $options); 288 | } 289 | 290 | $config = [ 291 | "project"=>$this->client->getProject() 292 | ]; 293 | 294 | if (!empty($target)) { 295 | $config["target"] = $target; 296 | } 297 | 298 | 299 | $response = $this->post($this->getEndpoint(), $opts, $config, $requestHeaders); 300 | 301 | if ($wait) { 302 | $response = $this->client->operations->wait($response['id']); 303 | } 304 | 305 | return $response; 306 | } 307 | 308 | /** 309 | * Create a copy of an existing local container 310 | * 311 | * Example: Copy container 312 | * $lxd->containers->copy('existing', 'new'); 313 | * 314 | * Example: Copy container and apply profiles to it 315 | * $lxd->containers->copy( 316 | * 'existing', 317 | * 'new', 318 | * ['profiles' => ['default', 'public'] 319 | * ); 320 | * 321 | * @param string $name Name of existing container 322 | * @param string $copyName Name of copied container 323 | * @param array $options Options for copied container 324 | * @param bool $wait Wait for operation to finish 325 | * @return object 326 | */ 327 | public function copy( 328 | $name, 329 | $copyName, 330 | array $options = [], 331 | $wait = false, 332 | string $targetProject = "" 333 | ) { 334 | $opts = $this->getOptions($copyName, $options); 335 | 336 | $currentProject = $this->client->getProject(); 337 | 338 | $opts['source']['type'] = 'copy'; 339 | $opts['source']['source'] = $name; 340 | $opts['source']['project'] = $currentProject; 341 | 342 | $config = [ 343 | "project"=>!empty($targetProject) ? $targetProject : $currentProject 344 | ]; 345 | 346 | $response = $this->post($this->getEndpoint(), $opts, $config); 347 | 348 | if ($wait) { 349 | $response = $this->client->operations->wait($response['id']); 350 | } 351 | 352 | return $response; 353 | } 354 | 355 | /** 356 | * Migrate a container 357 | * 358 | * If the container is running, it either must be shut down 359 | * first or criu must be installed on the source and destination 360 | * machines. 361 | * 362 | * Example: Migrate container 363 | * $lxd2 = new \Opensaucesystems\Lxd\Client($adapter, '1.0', 'https://lxd2.example.com:8443'); 364 | * $lxd->containers->migrate($lxd2, 'test'); 365 | * 366 | * @param object $destination lxd client Instance to destination lxd server 367 | * @param string $name Name of existing container 368 | * @param bool $wait Wait for operation to finish 369 | * @return object 370 | */ 371 | public function migrate( 372 | \Opensaucesystems\Lxd\Client $destination, 373 | $name, 374 | string $newName = "", 375 | $wait = false 376 | ) { 377 | if (empty($newName)) { 378 | $newName = $name; 379 | } 380 | 381 | return $destination->containers->create($newName, $this->initMigration($name, $newName), $wait); 382 | } 383 | 384 | /** 385 | * Initiate the migration of a container 386 | * 387 | * @param string $name Name of existing container 388 | * @return array 389 | */ 390 | public function initMigration($name, $newName) 391 | { 392 | $containerName = ""; 393 | 394 | if (strpos($name, "/") !== false) { 395 | $parts = explode("/", $name); 396 | $partsLength = count($parts); 397 | if ($partsLength == 0 || $partsLength > 2) { 398 | throw new \Exception("Snapshot name format not correct", 1); 399 | } 400 | $containerName = $parts[0]; 401 | $container = $this->snapshots->info($containerName, $parts[1]); 402 | $containerName = $parts[0] . "/snapshots/". $parts[1]; 403 | } else { 404 | $containerName = $name; 405 | $container = $this->info($name); 406 | } 407 | 408 | 409 | 410 | $migration = $this->post($this->getEndpoint().$containerName, [ 411 | 'name'=>$newName, 412 | 'migration' => true, 413 | 'stateful'=>false 414 | ]); 415 | 416 | $host = $this->client->host->info(); 417 | 418 | $hostAddress = $this->client->getUrl(); 419 | 420 | if ($hostAddress === "http://unix.socket/") { 421 | $hostAddress = "https://" . $host["environment"]["addresses"][0]; 422 | } 423 | 424 | $url = $hostAddress.'/'.$this->client->getApiVersion().'/operations/'.$migration['id']; 425 | 426 | $settings = [ 427 | 'name' => $name, 428 | 'architecture' => $container['architecture'], 429 | 'config' => $container['config'], 430 | 'epehemeral' => $container['ephemeral'], 431 | 'profiles' => $container['profiles'], 432 | 'source' => [ 433 | 'type' => 'migration', 434 | 'operation' => $url, 435 | 'mode' => 'pull', 436 | 'certificate' => $host['environment']['certificate'], 437 | 'secrets' => $migration['metadata'], 438 | ] 439 | ]; 440 | 441 | if (!empty($container["devices"])) { 442 | $settings["devices"] = $container["devices"]; 443 | } 444 | 445 | return $settings; 446 | } 447 | 448 | /** 449 | * Replace the configuration of a container 450 | * 451 | * Configuration is overwritten, not merged. Accordingly, clients should 452 | * first call the info method to obtain the current configuration of a 453 | * container. The resulting object should be modified and then passed to 454 | * the update method. 455 | * 456 | * Note that LXD does not allow certain attributes to be changed (e.g. 457 | * status, status_code, stateful, 458 | * name, etc.) through this call. 459 | * 460 | * Example: Change container to be ephemeral (i.e. it will be deleted when stopped) 461 | * $container = $lxd->containers->show('test'); 462 | * $container->ephemeral = true; 463 | * $lxd->containers->replace('test', $container); 464 | * 465 | * @param string $name Name of container 466 | * @param object $container Container to update 467 | * @param bool $wait Wait for operation to finish 468 | * @return object 469 | */ 470 | public function replace($name, $container, $wait = false) 471 | { 472 | $config = [ 473 | "project"=>$this->client->getProject() 474 | ]; 475 | 476 | $response = $this->put($this->getEndpoint().$name, $container, $config); 477 | 478 | if ($wait) { 479 | $response = $this->client->operations->wait($response['id']); 480 | } 481 | 482 | return $response; 483 | } 484 | 485 | /** 486 | * Update the configuration of a container 487 | * 488 | * Example: Change containers cpu-limit and rootfs size 489 | * $newconfig = [ 490 | * 'config' => [ 491 | * 'limits.cpu' => 4 492 | * ], 493 | * 'devices' => [ 494 | * 'rootfs' => [ 495 | * 'size' => '5GB' 496 | * ] 497 | * ] 498 | * ]; 499 | * $lxd->containers->update('test', $newconfig); 500 | * 501 | * @param string $name Name of container 502 | * @param array $config Options to create the container 503 | * @param bool $wait Wait for operation to finish 504 | * @return object 505 | */ 506 | public function update($name, $config, $wait = false) 507 | { 508 | $options = [ 509 | "project"=>$this->client->getProject() 510 | ]; 511 | 512 | $response = $this->patch($this->getEndpoint().$name, $config, $options); 513 | 514 | if ($wait) { 515 | $response = $this->client->operations->wait($response['id']); 516 | } 517 | 518 | return $response; 519 | } 520 | 521 | /** 522 | * Rename a container 523 | * 524 | * @param string $name Name of existing container 525 | * @param string $newName Name of new container 526 | * @param bool $wait Wait for operation to finish 527 | * @return array 528 | */ 529 | public function rename($name, $newName, $wait = false) 530 | { 531 | $opts['name'] = $newName; 532 | 533 | $config = [ 534 | "project"=>$this->client->getProject() 535 | ]; 536 | 537 | $response = $this->post($this->getEndpoint().$name, $opts, $config); 538 | 539 | if ($wait) { 540 | $response = $this->client->operations->wait($response['id']); 541 | } 542 | 543 | return $response; 544 | } 545 | 546 | /** 547 | * Delete a container 548 | * 549 | * @param string $name Name of container 550 | * @param bool $wait Wait for operation to finish 551 | * @return array 552 | */ 553 | public function remove($name, $wait = false) 554 | { 555 | $config = [ 556 | "project"=>$this->client->getProject() 557 | ]; 558 | 559 | $response = $this->delete($this->getEndpoint().$name, $config); 560 | 561 | if ($wait) { 562 | $response = $this->client->operations->wait($response['id']); 563 | } 564 | 565 | return $response; 566 | } 567 | 568 | /** 569 | * Execute a command in a container 570 | * 571 | * @param string $name Name of container 572 | * @param array|string $command Command and arguments 573 | * @param bool $record Whether to store stdout and stderr 574 | * @param array $environment An associative array, the key will be the environment variable name 575 | * @param bool $wait Wait for operation to finish 576 | * @return object 577 | */ 578 | public function execute($name, $command, $record = false, array $environment = [], $wait = false) 579 | { 580 | if (is_string($command)) { 581 | $command = $this->split($command); 582 | } 583 | 584 | $opts['command'] = $command; 585 | 586 | if (!empty($environment)) { 587 | $opts['environment'] = $environment; 588 | } 589 | 590 | if ($record === true) { 591 | $opts['record-output'] = true; 592 | } 593 | 594 | $opts['wait-for-websocket'] = false; 595 | $opts['interactive'] = false; 596 | 597 | $config = [ 598 | "project"=>$this->client->getProject() 599 | ]; 600 | 601 | $response = $this->post($this->getEndpoint().$name.'/exec', $opts, $config); 602 | 603 | if ($wait) { 604 | $response = $this->client->operations->wait($response['id']); 605 | $logs = []; 606 | $output = $response['metadata']['output']; 607 | $return = $response['metadata']['return']; 608 | unset($response); 609 | 610 | foreach ($output as $log) { 611 | $response['output'][] = str_replace( 612 | '/'.$this->client->getApiVersion().$this->getEndpoint().$name.'/logs/', 613 | '', 614 | $log 615 | ); 616 | } 617 | 618 | $response['return'] = $return; 619 | } 620 | 621 | return $response; 622 | } 623 | 624 | public function __get($endpoint) 625 | { 626 | $class = __NAMESPACE__.'\\Instance\\'.ucfirst($endpoint); 627 | 628 | if (class_exists($class)) { 629 | $class = new $class($this->client); 630 | $class->setEndpoint($this->getEndpoint()); 631 | return $class; 632 | } else { 633 | throw new InvalidEndpointException( 634 | 'Endpoint '.$class.', not implemented.' 635 | ); 636 | } 637 | } 638 | 639 | /** 640 | * Get image source attribute 641 | * 642 | * @param array $options Options for creating container 643 | * @return array 644 | */ 645 | private function getSource($options) 646 | { 647 | if (isset($options['source'])) { 648 | $only = [ 649 | 'type', 650 | 'mode', 651 | 'source', 652 | 'server', 653 | 'operation', 654 | 'protocol', 655 | 'base-image', 656 | 'certificate', 657 | 'secret', 658 | 'secrets', 659 | 'alias', 660 | 'fingerprint', 661 | 'properties', 662 | 'live', 663 | 'backup' 664 | ]; 665 | $opts = array_intersect_key($options, array_flip((array) $only)); 666 | 667 | return $opts['source']; 668 | } 669 | 670 | foreach (['alias', 'fingerprint', 'properties'] as $attr) { 671 | if (!empty($options[$attr])) { 672 | return [$attr => $options[$attr]]; 673 | } 674 | } 675 | 676 | return []; 677 | } 678 | 679 | /** 680 | * Get the options for creating container 681 | * 682 | * @param string $name Name of container 683 | * @param array $options Options for creating container 684 | * @return array 685 | */ 686 | private function getOptions($name, $options) 687 | { 688 | if (isset($options["source"]) && $options["source"] == "backup") { 689 | if (!isset($options["file"])) { 690 | throw new \Exception('source => backup requires file => file_get_contents(BACKUP_PATH) '); 691 | } 692 | return $options["file"]; 693 | } 694 | 695 | $only = [ 696 | 'architecture', 697 | 'profiles', 698 | 'ephemeral', 699 | 'config', 700 | 'devices', 701 | 'instance_type', 702 | 'type' 703 | ]; 704 | $opts = array_intersect_key($options, array_flip((array) $only)); 705 | $opts['name'] = $name; 706 | 707 | return $opts; 708 | } 709 | 710 | /** 711 | * Get options for creating an empty container 712 | * 713 | * @param string $name Name of container 714 | * @param array $options Options for creating container 715 | * @return array 716 | */ 717 | private function getEmptyOptions($name, $options) 718 | { 719 | $attrs = [ 720 | 'alias', 721 | 'fingerprint', 722 | 'properties', 723 | 'server', 724 | 'secret', 725 | 'protocol', 726 | 'certificate', 727 | ]; 728 | 729 | foreach ($attrs as $attr) { 730 | if (!empty($options[$attr])) { 731 | throw new \Exception('empty => true is not compatible with '.$attr); 732 | } 733 | } 734 | 735 | $opts = $this->getOptions($name, $options); 736 | $opts['source']['type'] = 'none'; 737 | 738 | return $opts; 739 | } 740 | 741 | /** 742 | * Get options for creating a container from remote image 743 | * 744 | * @param string $name Name of container 745 | * @param array $source Source of the image 746 | * @param array $options Options for creating container 747 | * @return array 748 | */ 749 | private function getRemoteImageOptions($name, $source, $options) 750 | { 751 | if (isset($options['protocol']) && !in_array($options['protocol'], ['lxd', 'simplestreams'])) { 752 | throw new \Exception('Invalid protocol. Valid choices: lxd, simplestreams'); 753 | } 754 | 755 | $only = [ 756 | 'server', 757 | 'secret', 758 | 'protocol', 759 | 'certificate', 760 | ]; 761 | $remoteOptions = array_intersect_key($options, array_flip((array) $only)); 762 | 763 | $opts = $this->getOptions($name, $options); 764 | $opts['source'] = array_merge($source, $remoteOptions); 765 | $opts['source']['type'] = 'image'; 766 | $opts['source']['mode'] = 'pull'; 767 | 768 | return $opts; 769 | } 770 | 771 | /** 772 | * Get options for creating a container from local image 773 | * 774 | * @param string $name Name of container 775 | * @param array $source Source of the image 776 | * @param array $options Options for creating container 777 | * @return array 778 | */ 779 | private function getLocalImageOptions($name, $source, $options) 780 | { 781 | $attrs = [ 782 | 'secret', 783 | 'protocol', 784 | 'certificate', 785 | ]; 786 | 787 | foreach ($attrs as $attr) { 788 | if (!empty($options[$attr])) { 789 | throw new \Exception('Only setting remote server is compatible with '.$attr); 790 | } 791 | } 792 | 793 | $opts = $this->getOptions($name, $options); 794 | $opts['source'] = $source; 795 | $opts['source']['type'] = 'image'; 796 | 797 | return $opts; 798 | } 799 | 800 | /** 801 | * To split a string 802 | * 803 | * @param string $string String to split into array 804 | * @return array 805 | */ 806 | private function split($string) 807 | { 808 | $pattern = '/\s*(?>([^\s\\\'\"]+)|\'([^\']*)\'|"((?:[^\"\\\\]|\\.)*)"|(\\.?)|(\S))(\s|\z)?/'; 809 | preg_match_all($pattern, $string, $matches); 810 | $words = []; 811 | 812 | foreach ($matches[0] as $value) { 813 | if (!empty($value)) { 814 | $words[] = trim(trim($value), '\'"'); 815 | } 816 | } 817 | 818 | return $words; 819 | } 820 | } 821 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | $snapshots = []; 29 | 30 | $config = [ 31 | "project"=>$this->client->getProject(), 32 | "recursion"=>$recursion 33 | ]; 34 | 35 | foreach ($this->get($this->getEndpoint().$name.'/snapshots/', $config) 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 | $snapshots[] = $snapshot; 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/Instances.php: -------------------------------------------------------------------------------- 1 | client->hasVms() ? "/instances/" : "/containers/"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Endpoint/Networks.php: -------------------------------------------------------------------------------- 1 | $this->client->getProject(), 25 | "recursion"=>$recursion 26 | ]; 27 | 28 | foreach ($this->get($this->getEndpoint(), $config) as $network) { 29 | $networks[] = str_replace('/'.$this->client->getApiVersion().$this->getEndpoint(), '', $network); 30 | } 31 | 32 | return $networks; 33 | } 34 | 35 | /** 36 | * Show information on a network 37 | * 38 | * @param string $name name of network 39 | * @return object 40 | */ 41 | public function info($name) 42 | { 43 | $config = [ 44 | "project"=>$this->client->getProject() 45 | ]; 46 | 47 | return $this->get($this->getEndpoint().$name, $config); 48 | } 49 | 50 | /** 51 | * Create a network 52 | * 53 | * @param string $name name of network 54 | * @param array $config configuration of the network (Optional) 55 | * @return object 56 | */ 57 | public function create(string $name, string $description = "", array $config = [], $type = "") 58 | { 59 | $data = []; 60 | 61 | $data["name"] = $name; 62 | $data["description"] = $description; 63 | if (!empty($config)) { 64 | $data["config"] = $config; 65 | } 66 | $data["type"] = $type; 67 | 68 | $config = [ 69 | "project"=>$this->client->getProject() 70 | ]; 71 | 72 | return $this->post($this->getEndpoint(), $data, $config); 73 | } 74 | 75 | /** 76 | * Delete network 77 | * @param string $name name of network 78 | * @return object 79 | */ 80 | public function remove($name) 81 | { 82 | $config = [ 83 | "project"=>$this->client->getProject() 84 | ]; 85 | 86 | return $this->delete($this->getEndpoint().$name, $config); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /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, $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/Profiles.php: -------------------------------------------------------------------------------- 1 | $this->client->getProject() 25 | ]; 26 | 27 | if ($recursion > 0) { 28 | $config["recursion"] = $recursion; 29 | } 30 | 31 | 32 | foreach ($this->get($this->getEndpoint(), $config) as $profile) { 33 | $x = str_replace('/'.$this->client->getApiVersion().$this->getEndpoint(), '', $profile); 34 | $x = str_replace('?project=' . $this->client->getProject(), '', $x); 35 | 36 | $profiles[] = $x; 37 | } 38 | 39 | return $profiles; 40 | } 41 | 42 | /** 43 | * Show information on a profile 44 | * 45 | * @param string $name name of profile 46 | * @return object 47 | */ 48 | public function info($name) 49 | { 50 | $config = [ 51 | "project"=>$this->client->getProject() 52 | ]; 53 | 54 | return $this->get($this->getEndpoint().$name, $config); 55 | } 56 | 57 | /** 58 | * Create a new profile 59 | * 60 | * Example: Create profile 61 | * $lxd->profiles->create( 62 | * 'test-profile', 63 | * 'My test profile', 64 | * ["limits.memory" => "2GB"], 65 | * [ 66 | * "kvm" => [ 67 | * "type" => "unix-char", 68 | * "path" => "/dev/kvm" 69 | * ], 70 | * ] 71 | * ); 72 | * 73 | * @param string $name Name of profile 74 | * @param string $description Description of profile 75 | * @param array $config Configuration of profile 76 | * @param array $devices Devices of profile 77 | * @return object 78 | */ 79 | public function create($name, $description = '', array $config = null, array $devices = null) 80 | { 81 | $profile = []; 82 | $profile['name'] = $name; 83 | $profile['description'] = $description; 84 | $profile['config'] = $config; 85 | $profile['devices'] = $devices; 86 | 87 | $config = [ 88 | "project"=>$this->client->getProject() 89 | ]; 90 | 91 | return $this->post($this->getEndpoint(), $profile, $config); 92 | } 93 | 94 | /** 95 | * Update profile. 96 | * This will only update supplied profile settings and leave the other settings 97 | * 98 | * Example: Update profile 99 | * $lxd->profiles->update( 100 | * 'test-profile', 101 | * 'My test profile', 102 | * ["limits.memory" => "2GB"], 103 | * [ 104 | * "kvm" => [ 105 | * "type" => "unix-char", 106 | * "path" => "/dev/kvm" 107 | * ], 108 | * ] 109 | * ); 110 | * 111 | * @param string $name Name of profile 112 | * @param string $description Description of profile 113 | * @param array $config Configuration of profile 114 | * @param array $devices Devices of profile 115 | * @return object 116 | */ 117 | public function update($name, $description = '', array $config = null, array $devices = null) 118 | { 119 | $profile = []; 120 | $profile['description'] = $description; 121 | $profile['config'] = $config; 122 | $profile['devices'] = $devices; 123 | 124 | $config = [ 125 | "project"=>$this->client->getProject() 126 | ]; 127 | 128 | return $this->patch($this->getEndpoint().$name, $profile, $config); 129 | } 130 | 131 | /** 132 | * Replace profile. 133 | * This will replace all the profile settings with the supplied settings 134 | * 135 | * Example: Replace profile 136 | * $lxd->profiles->replace( 137 | * 'test-profile', 138 | * 'My test profile', 139 | * ["limits.memory" => "2GB"], 140 | * [ 141 | * "kvm" => [ 142 | * "type" => "unix-char", 143 | * "path" => "/dev/kvm" 144 | * ], 145 | * ] 146 | * ); 147 | * 148 | * @param string $name Name of profile 149 | * @param string $description Description of profile 150 | * @param array $config Configuration of profile 151 | * @param array $devices Devices of profile 152 | * @return object 153 | */ 154 | public function replace($name, $description = '', array $config = null, array $devices = null) 155 | { 156 | $profile = []; 157 | $profile['description'] = $description; 158 | 159 | if (!empty($config)) { 160 | $profile['config'] = $config; 161 | } 162 | 163 | if (!empty($devices)) { 164 | $profile['devices'] = $devices; 165 | } 166 | 167 | $config = [ 168 | "project"=>$this->client->getProject() 169 | ]; 170 | 171 | return $this->put($this->getEndpoint().$name, $profile, $config); 172 | } 173 | 174 | /** 175 | * Rename profile 176 | * 177 | * @param string $name Name of profile 178 | * @param string $newName Name of new profile 179 | * @return object 180 | */ 181 | public function rename($name, $newName) 182 | { 183 | $profile = []; 184 | $profile['name'] = $newName; 185 | 186 | $config = [ 187 | "project"=>$this->client->getProject() 188 | ]; 189 | 190 | return $this->post($this->getEndpoint().$name, $profile, $config); 191 | } 192 | 193 | /** 194 | * Delete a profile 195 | * 196 | * @param string $name Name of profile 197 | */ 198 | public function remove($name) 199 | { 200 | $config = [ 201 | "project"=>$this->client->getProject() 202 | ]; 203 | 204 | return $this->delete($this->getEndpoint().$name, $config); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Endpoint/Projects.php: -------------------------------------------------------------------------------- 1 | 0) { 23 | $config["recursion"] = $recursion; 24 | } 25 | 26 | foreach ($this->get($this->getEndpoint(), $config) as $project) { 27 | $x = str_replace('/'.$this->client->getApiVersion().$this->getEndpoint(), '', $project); 28 | $x = str_replace('?project=' . $this->client->getProject(), '', $x); 29 | 30 | $projects[] = $x; 31 | } 32 | 33 | return $projects; 34 | } 35 | 36 | public function create(string $name, string $description = "", array $config = []) 37 | { 38 | $project = []; 39 | $project["name"] = $name; 40 | $project["description"] = $description; 41 | $project["config"] = empty($config) ? $this->defaultProjectConfig() : $config; 42 | 43 | return $this->post($this->getEndpoint(), $project); 44 | } 45 | 46 | public function info(string $name) 47 | { 48 | return $this->get($this->getEndpoint() . $name); 49 | } 50 | 51 | public function replace(string $name, string $description = "", array $config = []) 52 | { 53 | $project = []; 54 | $project["description"] = $description; 55 | $project["config"] = empty($config) ? $this->defaultProjectConfig() : $config; 56 | 57 | return $this->put($this->getEndpoint() . $name, $project); 58 | } 59 | 60 | public function update(string $name, string $description = "", array $config = []) 61 | { 62 | $project = []; 63 | if (!empty($description)) { 64 | $project["description"] = $description; 65 | } 66 | $project["config"] = empty($config) ? $this->defaultProjectConfig() : $config; 67 | return $this->patch($this->getEndpoint() . $name, $project); 68 | } 69 | 70 | public function rename(string $name, string $newName) 71 | { 72 | $config = ["name"=>$newName]; 73 | return $this->post($this->getEndpoint() . $name, $config); 74 | } 75 | 76 | public function remove(string $name) 77 | { 78 | return $this->delete($this->getEndpoint() . $name); 79 | } 80 | 81 | private function defaultProjectConfig() 82 | { 83 | return [ 84 | "features.images"=>"true", 85 | "features.profiles"=>"true", 86 | ]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Endpoint/Resources.php: -------------------------------------------------------------------------------- 1 | get($this->getEndpoint()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Endpoint/Storage.php: -------------------------------------------------------------------------------- 1 | 0) { 17 | $config["recursion"] = $recursion; 18 | } 19 | 20 | $storagePools = []; 21 | foreach ($this->get($this->getEndpoint(), $config) as $pool) { 22 | $storagePools[] = str_replace('/'.$this->client->getApiVersion().$this->getEndpoint(), '', $pool); 23 | } 24 | return $storagePools; 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/Endpoint/Storage/Resources.php: -------------------------------------------------------------------------------- 1 | get($this->getEndpoint().$name.'/resources'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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 remove(string $pool, string $name) 44 | { 45 | $httpConfig = [ 46 | "project"=>$this->client->getProject() 47 | ]; 48 | 49 | return $this->delete($this->getEndpoint().$pool . '/volumes/' . $name, [], $httpConfig); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Endpoint/VirtualMachines.php: -------------------------------------------------------------------------------- 1 | 1 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Exception/AuthenticationFailedException.php: -------------------------------------------------------------------------------- 1 | message, $request, $response, $previous); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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/Exception/ClientConnectionException.php: -------------------------------------------------------------------------------- 1 | message, $request, $response, $previous); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exception/InvalidEndpointException.php: -------------------------------------------------------------------------------- 1 | message, $request, $response, $previous); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exception/OperationException.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/HttpClient/Plugin/LxdExceptionThower.php: -------------------------------------------------------------------------------- 1 | then(function (ResponseInterface $response) use ($request) { 29 | return $response; 30 | }, function (\Exception $e) use ($request) { 31 | if (get_class($e) === HttpException::class) { 32 | $response = $e->getResponse(); 33 | 34 | if (400 === $response->getStatusCode()) { 35 | throw new BadRequestException($request, $response, $e); 36 | } 37 | 38 | if (401 === $response->getStatusCode()) { 39 | throw new OperationException($request, $response, $e); 40 | } 41 | 42 | if (403 === $response->getStatusCode()) { 43 | throw new AuthenticationFailedException($request, $response, $e); 44 | } 45 | 46 | if (404 === $response->getStatusCode()) { 47 | throw new NotFoundException($request, $response, $e); 48 | } 49 | 50 | if (409 === $response->getStatusCode()) { 51 | throw new ConflictException($request, $response, $e); 52 | } 53 | } 54 | 55 | throw $e; 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/HttpClient/Plugin/PathPrepend.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PathPrepend implements Plugin 14 | { 15 | private $path; 16 | 17 | /** 18 | * @param string $path 19 | */ 20 | public function __construct($path) 21 | { 22 | $this->path = $path; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function handleRequest(RequestInterface $request, callable $next, callable $first) 29 | { 30 | $currentPath = $request->getUri()->getPath(); 31 | $uri = $request->getUri()->withPath($this->path.$currentPath); 32 | $request = $request->withUri($uri); 33 | 34 | return $next($request); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/HttpClient/Plugin/PathTrimEnd.php: -------------------------------------------------------------------------------- 1 | trim = $trim; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function handleRequest(RequestInterface $request, callable $next, callable $first) 27 | { 28 | $trimPath = rtrim($request->getUri()->getPath(), $this->trim); 29 | $uri = $request->getUri()->withPath($trimPath); 30 | $request = $request->withUri($uri); 31 | 32 | return $next($request); 33 | } 34 | } 35 | --------------------------------------------------------------------------------