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