From 63d8c759d209ed7fdd87d81d98a9de939d0d5ce8 Mon Sep 17 00:00:00 2001 From: Konstantin Babushkin Date: Sun, 8 Mar 2026 17:41:20 +0100 Subject: [PATCH 1/2] Add scheduler hint sample --- AGENTS.md | 2 + doc/services/compute/v2/server-groups.rst | 8 ++++ .../v2/server_groups/create_server.php | 27 +++++++++++ src/Compute/v2/Api.php | 35 ++++++++------- src/Compute/v2/Params.php | 10 +++++ tests/sample/Compute/v2/ServerGroupTest.php | 45 +++++++++++++++++++ tests/sample/Compute/v2/TestCase.php | 5 ++- tests/unit/Compute/v2/ServiceTest.php | 27 +++++++++++ 8 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 samples/Compute/v2/server_groups/create_server.php diff --git a/AGENTS.md b/AGENTS.md index 1344e6ec..d4ab897a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/doc/services/compute/v2/server-groups.rst b/doc/services/compute/v2/server-groups.rst index 24c19c62..e386b608 100644 --- a/doc/services/compute/v2/server-groups.rst +++ b/doc/services/compute/v2/server-groups.rst @@ -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 ---- diff --git a/samples/Compute/v2/server_groups/create_server.php b/samples/Compute/v2/server_groups/create_server.php new file mode 100644 index 00000000..02896394 --- /dev/null +++ b/samples/Compute/v2/server_groups/create_server.php @@ -0,0 +1,27 @@ + '{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}', + ], +]); \ No newline at end of file diff --git a/src/Compute/v2/Api.php b/src/Compute/v2/Api.php index 9f925c23..571543a7 100644 --- a/src/Compute/v2/Api.php +++ b/src/Compute/v2/Api.php @@ -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 [ @@ -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(), ], ]; } diff --git a/src/Compute/v2/Params.php b/src/Compute/v2/Params.php index 5d8eec78..1297b6b1 100644 --- a/src/Compute/v2/Params.php +++ b/src/Compute/v2/Params.php @@ -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 [ diff --git a/tests/sample/Compute/v2/ServerGroupTest.php b/tests/sample/Compute/v2/ServerGroupTest.php index be5c085b..d3985dbe 100644 --- a/tests/sample/Compute/v2/ServerGroupTest.php +++ b/tests/sample/Compute/v2/ServerGroupTest.php @@ -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 { @@ -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 */ diff --git a/tests/sample/Compute/v2/TestCase.php b/tests/sample/Compute/v2/TestCase.php index 8f186918..76b1578b 100644 --- a/tests/sample/Compute/v2/TestCase.php +++ b/tests/sample/Compute/v2/TestCase.php @@ -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); } diff --git a/tests/unit/Compute/v2/ServiceTest.php b/tests/unit/Compute/v2/ServiceTest.php index e3b8387c..96b62041 100644 --- a/tests/unit/Compute/v2/ServiceTest.php +++ b/tests/unit/Compute/v2/ServiceTest.php @@ -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'); From 72e9e9abee68442442d85cbdc92b06b9e54e3f45 Mon Sep 17 00:00:00 2001 From: Konstantin Babushkin Date: Sun, 8 Mar 2026 18:53:25 +0100 Subject: [PATCH 2/2] ignore not found networks during integration tests --- tests/sample/Compute/v2/TestCase.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/sample/Compute/v2/TestCase.php b/tests/sample/Compute/v2/TestCase.php index 76b1578b..11734414 100644 --- a/tests/sample/Compute/v2/TestCase.php +++ b/tests/sample/Compute/v2/TestCase.php @@ -87,6 +87,9 @@ protected function deleteServer(Server $server): void foreach ($networks as $networkName) { $network = $this->getNetworkService()->listNetworks(['name' => $networkName])->current(); + if ($network == null){ + continue; + } $this->deleteNetwork($network); } }