Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ All code snippets used in the docs must live in `samples/` rather than being mai

Sample tests typically create a temporary PHP file from a template and `require_once` it, so keep samples self-contained and readable.

When adding sample tests, prefer reusing resources created earlier in the same test file instead of provisioning duplicate ones. In practice, `testCreate` should return the created resource, dependent tests should consume it via `@depends`, and cleanup should happen in the final `testDelete`.

## Documentation

User docs live in `doc/` and use Sphinx plus reStructuredText. If a change affects public behavior, examples, or supported options, update docs as needed.
Expand Down
8 changes: 8 additions & 0 deletions doc/services/compute/v2/server-groups.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ optional ``rules`` object instead:
When Nova responds with the newer singular ``policy`` field, the SDK also exposes that value as the first item in
``policies`` for compatibility with the older response shape.

Create A Server In A Group
--------------------------

To place a server into an existing server group, pass the server group UUID through ``schedulerHints.group`` when you
create the server:

.. sample:: Compute/v2/server_groups/create_server.php

Read
----

Expand Down
27 changes: 27 additions & 0 deletions samples/Compute/v2/server_groups/create_server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

require 'vendor/autoload.php';

$openstack = new OpenStack\OpenStack([
'authUrl' => '{authUrl}',
'region' => '{region}',
'user' => [
'id' => '{userId}',
'password' => '{password}',
],
'scope' => ['project' => ['id' => '{projectId}']],
]);

$compute = $openstack->computeV2(['region' => '{region}']);

$server = $compute->createServer([
'name' => '{serverName}',
'imageId' => '{imageId}',
'flavorId' => '{flavorId}',
'networks' => [
['uuid' => '{networkId}'],
],
'schedulerHints' => [
'group' => '{serverGroupId}',
],
]);
35 changes: 20 additions & 15 deletions src/Compute/v2/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public function __construct()
$this->params = new Params();
}

private function serverParam(array $param): array
{
return array_merge($param, ['path' => 'server']);
}

public function getLimits(): array
{
return [
Expand Down Expand Up @@ -189,21 +194,21 @@ public function deleteImageMetadataKey(): array
public function postServer(): array
{
return [
'path' => 'servers',
'method' => 'POST',
'jsonKey' => 'server',
'params' => [
'imageId' => $this->notRequired($this->params->imageId()),
'flavorId' => $this->params->flavorId(),
'personality' => $this->params->personality(),
'metadata' => $this->notRequired($this->params->metadata()),
'name' => $this->isRequired($this->params->name('server')),
'securityGroups' => $this->params->securityGroups(),
'userData' => $this->params->userData(),
'availabilityZone' => $this->params->availabilityZone(),
'networks' => $this->params->networks(),
'blockDeviceMapping' => $this->params->blockDeviceMapping(),
'keyName' => $this->params->keyName(),
'path' => 'servers',
'method' => 'POST',
'params' => [
'imageId' => $this->serverParam($this->notRequired($this->params->imageId())),
'flavorId' => $this->serverParam($this->params->flavorId()),
'personality' => $this->serverParam($this->params->personality()),
'metadata' => $this->serverParam($this->notRequired($this->params->metadata())),
'name' => $this->serverParam($this->isRequired($this->params->name('server'))),
'securityGroups' => $this->serverParam($this->params->securityGroups()),
'userData' => $this->serverParam($this->params->userData()),
'availabilityZone' => $this->serverParam($this->params->availabilityZone()),
'networks' => $this->serverParam($this->params->networks()),
'blockDeviceMapping' => $this->serverParam($this->params->blockDeviceMapping()),
'keyName' => $this->serverParam($this->params->keyName()),
'schedulerHints' => $this->params->schedulerHints(),
],
];
}
Expand Down
10 changes: 10 additions & 0 deletions src/Compute/v2/Params.php
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,16 @@ public function blockDeviceMapping(): array
];
}

public function schedulerHints(): array
{
return [
'type' => self::OBJECT_TYPE,
'location' => self::JSON,
'sentAs' => 'os:scheduler_hints',
'description' => 'Scheduler hints to pass alongside the server create request, for example ["group" => "{serverGroupId}"].',
];
}

public function filterHost(): array
{
return [
Expand Down
45 changes: 45 additions & 0 deletions tests/sample/Compute/v2/ServerGroupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace OpenStack\Sample\Compute\v2;

use OpenStack\Common\Error\BadResponseError;
use OpenStack\Compute\v2\Models\Server;
use OpenStack\Compute\v2\Models\ServerGroup;
use RuntimeException;

class ServerGroupTest extends TestCase
{
Expand Down Expand Up @@ -47,6 +49,49 @@ public function testCreate(): ServerGroup
return $serverGroup;
}

/**
* @depends testCreate
*/
public function testCreateServerInGroup(ServerGroup $createdServerGroup)
{
$flavorId = getenv('OS_FLAVOR');

if (!$flavorId) {
throw new RuntimeException('OS_FLAVOR env var must be set');
}

$network = $this->getNetworkService()->createNetwork(['name' => $this->randomStr()]);
$this->getNetworkService()->createSubnet(
[
'name' => $this->randomStr(),
'networkId' => $network->id,
'ipVersion' => 4,
'cidr' => '10.20.30.0/24',
]
);

/** @var Server $server */
require_once $this->sampleFile(
'server_groups/create_server.php',
[
'{serverName}' => $this->randomStr(),
'{imageId}' => $this->searchImageId(),
'{flavorId}' => $flavorId,
'{networkId}' => $network->id,
'{serverGroupId}' => $createdServerGroup->id,
]
);

$this->assertInstanceOf(Server::class, $server);

$server->waitUntilActive(300);
$createdServerGroup->retrieve();

$this->assertContains($server->id, $createdServerGroup->members);

$this->deleteServer($server);
}

/**
* @depends testCreate
*/
Expand Down
5 changes: 4 additions & 1 deletion tests/sample/Compute/v2/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,13 @@ protected function createServer(): Server
*/
protected function deleteServer(Server $server): void
{
$server->retrieve();
$networks = array_keys($server->addresses);

$server->delete();
$server->waitUntilDeleted();

foreach (array_keys($server->addresses) as $networkName) {
foreach ($networks as $networkName) {
$network = $this->getNetworkService()->listNetworks(['name' => $networkName])->current();
$this->deleteNetwork($network);
}
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/Compute/v2/ServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,33 @@ public function test_it_creates_servers()
self::assertInstanceOf(Server::class, $this->service->createServer($opts));
}

public function test_it_creates_servers_with_scheduler_hints()
{
$opts = [
'name' => 'foo',
'imageId' => '',
'flavorId' => '',
'schedulerHints' => [
'group' => 'server-group-id',
],
];

$expectedJson = [
'server' => [
'name' => $opts['name'],
'imageRef' => $opts['imageId'],
'flavorRef' => $opts['flavorId'],
],
'os:scheduler_hints' => [
'group' => 'server-group-id',
],
];

$this->mockRequest('POST', 'servers', 'server-post', $expectedJson, []);

self::assertInstanceOf(Server::class, $this->service->createServer($opts));
}

public function test_it_lists_servers()
{
$this->mockRequest('GET', ['path' => 'servers', 'query' => ['limit' => 5]], 'servers-get');
Expand Down
Loading