diff --git a/samples/compute/v2/servers/resume_server.php b/samples/compute/v2/servers/resume_server.php new file mode 100644 index 00000000..e155f86a --- /dev/null +++ b/samples/compute/v2/servers/resume_server.php @@ -0,0 +1,21 @@ + '{authUrl}', + 'region' => '{region}', + 'user' => [ + 'id' => '{userId}', + 'password' => '{password}' + ], + 'scope' => ['project' => ['id' => '{projectId}']] +]); + +$compute = $openstack->computeV2(['region' => '{region}']); + +$server = $compute->getServer([ + 'id' => '{serverId}', +]); + +$server->resume(); diff --git a/samples/compute/v2/servers/suspend_server.php b/samples/compute/v2/servers/suspend_server.php new file mode 100644 index 00000000..42b80016 --- /dev/null +++ b/samples/compute/v2/servers/suspend_server.php @@ -0,0 +1,21 @@ + '{authUrl}', + 'region' => '{region}', + 'user' => [ + 'id' => '{userId}', + 'password' => '{password}' + ], + 'scope' => ['project' => ['id' => '{projectId}']] +]); + +$compute = $openstack->computeV2(['region' => '{region}']); + +$server = $compute->getServer([ + 'id' => '{serverId}', +]); + +$server->suspend(); diff --git a/src/BlockStorage/Enum.php b/src/BlockStorage/Enum.php new file mode 100644 index 00000000..31e4c709 --- /dev/null +++ b/src/BlockStorage/Enum.php @@ -0,0 +1,33 @@ + 'POST', + 'path' => 'volumes/{id}/action', + 'jsonKey' => 'os-extend', + 'params' => [ + 'id' => $this->params->idPath(), + 'new_size' => $this->params->size(), + ], + ]; + } + + public function resetVolumeStatus(): array + { + return [ + 'method' => 'POST', + 'path' => 'volumes/{id}/action', + 'jsonKey' => 'os-reset_status', + 'params' => [ + 'id' => $this->params->idPath(), + 'status' => $this->params->status() + ], + ]; + } } diff --git a/src/BlockStorage/v2/Models/Volume.php b/src/BlockStorage/v2/Models/Volume.php index 01e8a1df..de0f4fe3 100644 --- a/src/BlockStorage/v2/Models/Volume.php +++ b/src/BlockStorage/v2/Models/Volume.php @@ -160,6 +160,15 @@ public function parseMetadata(ResponseInterface $response): array return isset($json['metadata']) ? $json['metadata'] : []; } + public function extend(int $size_in_gb) + { + $response = $this->execute($this->api->extendVolume(), [ + 'id' => $this->id, + 'new_size' => $size_in_gb + ]); + $this->populateFromResponse($response); + } + /** * Update the bootable status for a volume, mark it as a bootable volume. * diff --git a/src/BlockStorage/v2/Params.php b/src/BlockStorage/v2/Params.php index 694864fe..23139048 100644 --- a/src/BlockStorage/v2/Params.php +++ b/src/BlockStorage/v2/Params.php @@ -268,6 +268,25 @@ public function quotaSetVolumesIscsi(): array return $this->quotaSetLimit('volumes_iscsi', 'The number of allowed volumes iscsi'); } + public function nullAction(): array + { + return [ + 'type' => self::NULL_TYPE, + 'location' => self::JSON, + 'required' => true + ]; + } + + public function status(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'required' => true, + 'description' => 'The new status of the volume', + ]; + } + public function projectId(): array { return [ diff --git a/src/Compute/v2/Api.php b/src/Compute/v2/Api.php index 2a8dde1a..f5bc8a0b 100644 --- a/src/Compute/v2/Api.php +++ b/src/Compute/v2/Api.php @@ -332,6 +332,90 @@ public function stopServer(): array ]; } + public function resumeServer() : array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'resume' => $this->params->nullAction() + ], + ]; + } + + public function suspendServer() : array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'suspend' => $this->params->nullAction() + ], + ]; + } + + public function shelveServer() : array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'shelve' => $this->params->nullAction(), + ], + ]; + } + + public function shelveOffloadServer() : array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'shelveOffload' => $this->params->nullAction() + ], + ]; + } + + public function unshelveServer() : array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'unshelve' => $this->params->nullAction() + ], + ]; + } + + public function lockServer() : array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'lock' => $this->params->nullAction() + ], + ]; + } + + public function unlockServer() : array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'unlock' => $this->params->nullAction() + ], + ]; + } + public function rebuildServer(): array { return [ @@ -505,6 +589,19 @@ public function getRDPConsole(): array ]; } + public function getConsoleLog(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'os-getConsoleOutput', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'length' => $this->params->consoleLogLength() + ] + ]; + } + public function getAddresses(): array { return [ @@ -889,4 +986,50 @@ public function putQuotaSet(): array ], ]; } + + public function getInstanceActions(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}/os-instance-actions', + 'params' => [ + 'id' => $this->params->urlId('server') + ] + ]; + } + + public function getInstanceAction(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}/os-instance-actions/{requestId}', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'requestId' => $this->params->urlId('request') + ] + ]; + } + + public function getAggregates(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-aggregates', + 'jsonKey' => 'aggregates', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker() + ], + ]; + } + + public function getAggregate(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-aggregates/{id}', + 'params' => ['id' => $this->params->urlId('id')] + ]; + } + } diff --git a/src/Compute/v2/Models/Aggregate.php b/src/Compute/v2/Models/Aggregate.php new file mode 100644 index 00000000..223b1696 --- /dev/null +++ b/src/Compute/v2/Models/Aggregate.php @@ -0,0 +1,64 @@ + 'availabilityZone', + 'created_at' => 'createdAt', + 'deleted_at' => 'deletedAt', + 'updated_at' => 'updatedAt' + ]; + + /** + * {@inheritDoc} + */ + public function retrieve() + { + $response = $this->execute($this->api->getAggregate(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } +} diff --git a/src/Compute/v2/Models/InstanceAction.php b/src/Compute/v2/Models/InstanceAction.php new file mode 100644 index 00000000..52e889ca --- /dev/null +++ b/src/Compute/v2/Models/InstanceAction.php @@ -0,0 +1,44 @@ + 'instanceUuid', + 'project_id' => 'projectId', + 'request_id' => 'requestId', + 'start_time' => 'startTime', + 'user_id' => 'userId' + ]; + +} diff --git a/src/Compute/v2/Models/Server.php b/src/Compute/v2/Models/Server.php index 9e9b4b0d..b83eb8ce 100644 --- a/src/Compute/v2/Models/Server.php +++ b/src/Compute/v2/Models/Server.php @@ -231,6 +231,83 @@ public function stop() ]); } + /** + * Shelves server + */ + public function shelve() + { + $this->execute($this->api->shelveServer(), [ + 'id' => $this->id, + 'shelve' => null + ]); + } + + /* + * Suspend server + */ + public function suspend() + { + $this->execute($this->api->suspendServer(), [ + 'id' => $this->id, + 'suspend' => null + ]); + } + + /** + * Shelf-offloads server + */ + public function shelveOffload() + { + $this->execute($this->api->shelveOffloadServer(), [ + 'id' => $this->id, + 'shelveOffload' => null + ]); + } + + /** + * Unshelves server + */ + public function unshelve() + { + $this->execute($this->api->unshelveServer(), [ + 'id' => $this->id, + 'unshelve' => null + ]); + } + + /* + * Resume server + */ + public function resume() + { + $this->execute($this->api->resumeServer(), [ + 'id' => $this->id, + 'resume' => null + ]); + } + + /** + * Locks server + */ + public function lock() + { + $this->execute($this->api->lockServer(), [ + 'id' => $this->id, + 'lock' => null + ]); + } + + /** + * Unlocks server + */ + public function unlock() + { + $this->execute($this->api->unlockServer(), [ + 'id' => $this->id, + 'unlock' => null + ]); + } + /** * Rebuilds the server. * @@ -376,6 +453,19 @@ public function getSerialConsole($type = Enum::CONSOLE_SERIAL): array return Utils::jsonDecode($response)['console']; } + /** + * Get the console log. + * + * @param int $length Number of lines of console log to grab. + * + * @return string - the console log output + */ + public function getConsoleLog(int $length = 50): string + { + $response = $this->execute($this->api->getConsoleLog(), ['id' => $this->id, 'length' => $length]); + return Utils::jsonDecode($response)['output']; + } + /** * Creates an image for the current server. * @@ -606,4 +696,27 @@ public function detachVolume(string $attachmentId) { $this->execute($this->api->deleteVolumeAttachments(), ['id' => $this->id, 'attachmentId' => $attachmentId]); } + + /** + * Get a Generator for the instance actions + * + * @return \Generator + */ + public function listInstanceActions(): \Generator + { + return $this->model(InstanceAction::class)->enumerate($this->api->getInstanceActions(), ['id' => $this->id]); + } + + /** + * Get a specific instance action + * + * @string The request ID of the instance action + * @return InstanceAction + */ + public function getInstanceAction(string $requestId): InstanceAction + { + $response = $this->execute($this->api->getInstanceAction(), ['id' => $this->id, 'requestId' => $requestId]); + + return $this->model(InstanceAction::class)->populateFromResponse($response); + } } diff --git a/src/Compute/v2/Params.php b/src/Compute/v2/Params.php index e61ae3ad..975c02cb 100644 --- a/src/Compute/v2/Params.php +++ b/src/Compute/v2/Params.php @@ -657,4 +657,5 @@ public function quotaSetLimitServerGroupMembers(): array { return $this->quotaSetLimit('server_group_members', 'The number of allowed members for each server group.'); } + } diff --git a/src/Compute/v2/Service.php b/src/Compute/v2/Service.php index b2c8c9b5..76214233 100644 --- a/src/Compute/v2/Service.php +++ b/src/Compute/v2/Service.php @@ -15,6 +15,7 @@ use OpenStack\Compute\v2\Models\Hypervisor; use OpenStack\Compute\v2\Models\AvailabilityZone; use OpenStack\Compute\v2\Models\QuotaSet; +use OpenStack\Compute\v2\Models\Aggregate; /** * Compute v2 service for OpenStack. @@ -308,4 +309,30 @@ public function getQuotaSet(string $tenantId, bool $detailed = false): QuotaSet return $quotaSet; } + + /** + * List host aggregates. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::getAggregates} + * @param callable $mapFn A callable function that will be invoked on every iteration of the list. + * + * @return \Generator + */ + public function listAggregates(array $options = [], callable $mapFn = null): \Generator + { + return $this->model(Aggregate::class)->enumerate($this->api->getAggregates(), $options, $mapFn); + } + + /** + * Shows details for a given host aggregate. + * + * @param array $options + * + * @return Aggregate + */ + public function getAggregate(array $options = []): Aggregate + { + $aggregate = $this->model(Aggregate::class); + return $aggregate->populateFromArray($options); + } } diff --git a/src/Networking/v2/Api.php b/src/Networking/v2/Api.php index d4cc0e39..bb2a8db9 100644 --- a/src/Networking/v2/Api.php +++ b/src/Networking/v2/Api.php @@ -703,4 +703,24 @@ public function deleteLoadBalancerHealthMonitor(): array ], ]; } + + public function getNetworkIpAvailability() : array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix . '/network-ip-availabilities/{id}', + 'params' => ['id' => $this->params->urlId('network')], + ]; + } + + public function getNetworkIpAvailabilities(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix . '/network-ip-availabilities', + 'params' => [ + 'tenantId' => $this->params->queryTenantId() + ] + ]; + } } diff --git a/src/Networking/v2/Models/NetworkIpAvailability.php b/src/Networking/v2/Models/NetworkIpAvailability.php new file mode 100644 index 00000000..3fd20be2 --- /dev/null +++ b/src/Networking/v2/Models/NetworkIpAvailability.php @@ -0,0 +1,61 @@ + 'id', + 'network_name' => 'networkName', + 'tenant_id' => 'tenantId', + 'project_id' => 'projectId', + 'total_ips' => 'totalIps', + 'used_ips' => 'usedIps', + 'subnet_ip_availability' => 'subnetIpAvailability' + ]; + + protected $resourceKey = 'network_ip_availability'; + protected $resourcesKey = 'network_ip_availabilities'; + + /** + * {@inheritDoc} + */ + public function retrieve() + { + $response = $this->execute($this->api->getNetworkIpAvailability(), ['id' => (string)$this->id]); + $this->populateFromResponse($response); + } +} diff --git a/src/Networking/v2/Service.php b/src/Networking/v2/Service.php index 6a1e0f93..f7b74cdf 100644 --- a/src/Networking/v2/Service.php +++ b/src/Networking/v2/Service.php @@ -11,6 +11,7 @@ use OpenStack\Networking\v2\Models\LoadBalancerMember; use OpenStack\Networking\v2\Models\LoadBalancerPool; use OpenStack\Networking\v2\Models\Network; +use OpenStack\Networking\v2\Models\NetworkIpAvailability; use OpenStack\Networking\v2\Models\Pool; use OpenStack\Networking\v2\Models\Port; use OpenStack\Networking\v2\Models\Quota; @@ -386,4 +387,30 @@ public function createLoadBalancerHealthMonitor(array $options): LoadBalancerHea { return $this->model(LoadBalancerHealthMonitor::class)->create($options); } + + /** + * Retrieve a network IP availability object without calling the remote API. Any values provided in the array will populate the + * empty object, allowing you greater control without the expense of network transactions. To call the remote API + * and have the response populate the object, call {@see NetworkIpAvailabilities::retrieve}. + * + * @param string $id + * + * @return NetworkIpAvailability + */ + public function getNetworkIpAvailability(string $id): NetworkIpAvailability + { + return $this->model(NetworkIpAvailability::class, ['id' => $id]); + } + + /** + * List network IP availability(es) + * + * @param array $options {@see \OpenStack\Networking\v2\Api::getNetworkIpAvailabilities() + * + * @return \Generator + */ + public function listNetworkIpAvailabilities(array $options = []): \Generator + { + return $this->model(NetworkIpAvailability::class)->enumerate($this->api->getNetworkIpAvailabilities(), $options); + } } diff --git a/tests/integration/Compute/v2/CoreTest.php b/tests/integration/Compute/v2/CoreTest.php index eb829b73..26bd89e3 100644 --- a/tests/integration/Compute/v2/CoreTest.php +++ b/tests/integration/Compute/v2/CoreTest.php @@ -145,6 +145,8 @@ public function runTests() //$this->changeServerPassword(); $this->stopServer(); $this->startServer(); + $this->suspendServer(); + $this->resumeServer(); $this->resizeServer(); $this->confirmServerResize(); $this->rebuildServer(); @@ -423,6 +425,30 @@ private function startServer() $this->logStep('Started server {serverId}', $replacements); } + private function suspendServer() + { + $replacements = ['{serverId}' => $this->serverId]; + + /** @var $server \OpenStack\Compute\v2\Models\Server */ + require_once $this->sampleFile($replacements, 'servers/suspend_server.php'); + + $server->waitUntilActive(false); + + $this->logStep('Suspended server {serverId}', $replacements); + } + + private function resumeServer() + { + $replacements = ['{serverId}' => $this->serverId]; + + /** @var $server \OpenStack\Compute\v2\Models\Server */ + require_once $this->sampleFile($replacements, 'servers/resume_server.php'); + + $server->waitUntilActive(false); + + $this->logStep('Resumed server {serverId}', $replacements); + } + private function createFlavor() { $replacements = [ diff --git a/tests/unit/Compute/v2/Models/ServerTest.php b/tests/unit/Compute/v2/Models/ServerTest.php index 44b6b605..cbb794e9 100644 --- a/tests/unit/Compute/v2/Models/ServerTest.php +++ b/tests/unit/Compute/v2/Models/ServerTest.php @@ -215,6 +215,24 @@ public function test_it_stops() $this->assertNull($this->server->stop()); } + public function test_it_suspends() + { + $expectedJson = ['suspend' => null]; + + $this->setupMock('POST', 'servers/serverId/action', $expectedJson, [], new Response(202)); + + $this->assertNull($this->server->suspend()); + } + + public function test_it_resumes() + { + $expectedJson = ['resume' => null]; + + $this->setupMock('POST', 'servers/serverId/action', $expectedJson, [], new Response(202)); + + $this->assertNull($this->server->resume()); + } + public function test_it_resizes() { $expectedJson = ['resize' => ['flavorRef' => 'flavorId']];