diff --git a/README.md b/README.md index ae4f1270..385ced72 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Total Downloads](https://poser.pugx.org/lkdevelopment/hetzner-cloud-php-sdk/downloads)](https://packagist.org/packages/lkdevelopment/hetzner-cloud-php-sdk) [![Actions Status](https://github.com/lkdevelopment/hetzner-cloud-php-sdk/workflows/CI/badge.svg)](https://github.com/lkdevelopment/hetzner-cloud-php-sdk/actions) # Hetzner Cloud PHP SDK -A PHP SDK for the Hetzner Cloud API: https://docs.hetzner.cloud/ +A PHP SDK for both [Hetzner Cloud API](https://docs.hetzner.cloud/reference/cloud) and [Hetzner API](https://docs.hetzner.cloud/reference/hetzner) ## Installation You can install the package via composer: diff --git a/examples/storage_boxes/create_a_storage_box.php b/examples/storage_boxes/create_a_storage_box.php new file mode 100644 index 00000000..c2ddf823 --- /dev/null +++ b/examples/storage_boxes/create_a_storage_box.php @@ -0,0 +1,24 @@ +serverTypes()->get(1); +$location = $hetznerClient->locations()->getByName('fsn1'); +$type = $hetznerClient->storageBoxTypes()->get('bx11'); + +$response = $hetznerClient->storageBoxes()->create( + name: 'my-storage-box', + location: $location->name, + storageBoxType: $type->name, + password: '{my s3cr3t p@ssword}', + labels: ['type' => 'untest'], + accessSettings: new StorageBoxAccessSettings(reachable_externally:true), +); + +$response->getResponsePart('action')->waitUntilCompleted(); +$box = $response->getResponsePart('storage_box')->reload(); + +echo "Name: {$box->name}".PHP_EOL; +echo "ID: {$box->id}".PHP_EOL; diff --git a/examples/storage_boxes/create_a_storage_box_subaccount.php b/examples/storage_boxes/create_a_storage_box_subaccount.php new file mode 100644 index 00000000..0513dfb1 --- /dev/null +++ b/examples/storage_boxes/create_a_storage_box_subaccount.php @@ -0,0 +1,27 @@ +storageBoxes()->get('some-existing-box'); + +$response = $box->createSubaccount( + 'my_home_dir', + 'MySecret!1234', + 'some_name', + new StorageBoxAccessSettings( + reachable_externally: true, + ssh_enabled: true, + ), + 'Description', + [ + 'some' => 'label', + ], +); +$response->getResponsePart('action')->waitUntilCompleted(); +$account = $response->getResponsePart('subaccount')->reload(); + +echo "Name: {$account->name}".PHP_EOL; +echo "ID: {$account->id}".PHP_EOL; +echo "HomeDir: {$account->home_directory}".PHP_EOL; diff --git a/examples/storage_boxes/get_all_storage_box_types.php b/examples/storage_boxes/get_all_storage_box_types.php new file mode 100644 index 00000000..0413721a --- /dev/null +++ b/examples/storage_boxes/get_all_storage_box_types.php @@ -0,0 +1,7 @@ +storageBoxTypes()->all() as $type) { + echo "Name: {$type->name} - ID: {$type->id}".PHP_EOL; +} diff --git a/src/HetznerAPIClient.php b/src/HetznerAPIClient.php index 0998f44b..bcc782b6 100644 --- a/src/HetznerAPIClient.php +++ b/src/HetznerAPIClient.php @@ -20,6 +20,9 @@ use LKDev\HetznerCloud\Models\Servers\Servers; use LKDev\HetznerCloud\Models\Servers\Types\ServerTypes; use LKDev\HetznerCloud\Models\SSHKeys\SSHKeys; +use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxActions; +use LKDev\HetznerCloud\Models\StorageBoxes\StorageBoxes; +use LKDev\HetznerCloud\Models\StorageBoxTypes\StorageBoxTypes; use LKDev\HetznerCloud\Models\Volumes\Volumes; use LKDev\HetznerCloud\Models\Zones\Zones; use Psr\Http\Message\ResponseInterface; @@ -63,6 +66,11 @@ class HetznerAPIClient */ protected GuzzleClient $httpClient; + /** + * @var \LKDev\HetznerCloud\Clients\GuzzleClient|null + */ + protected ?GuzzleClient $apiHetznerComClient = null; + /** * @param string $apiToken * @param string $baseUrl @@ -141,6 +149,28 @@ public function setHttpClient(GuzzleClient $client): self return $this; } + /** + * @return GuzzleClient + */ + public function getApiHetznerComClient(): GuzzleClient + { + if ($this->apiHetznerComClient === null) { + $this->apiHetznerComClient = new GuzzleClient($this, ['base_uri' => 'https://api.hetzner.com/v1/']); + } + + return $this->apiHetznerComClient; + } + + /** + * @return $this + */ + public function setApiHetznerComClient(GuzzleClient $client): self + { + $this->apiHetznerComClient = $client; + + return $this; + } + /** * @param \Psr\Http\Message\ResponseInterface $response * @@ -333,6 +363,30 @@ public function zones() return new Zones($this->httpClient); } + /** + * @return StorageBoxes + */ + public function storageBoxes(): StorageBoxes + { + return new StorageBoxes($this->getApiHetznerComClient()); + } + + /** + * @return StorageBoxTypes + */ + public function storageBoxTypes(): StorageBoxTypes + { + return new StorageBoxTypes($this->getApiHetznerComClient()); + } + + /** + * @return StorageBoxActions + */ + public function storageBoxActions(): StorageBoxActions + { + return new StorageBoxActions($this->getApiHetznerComClient()); + } + /** * @return GuzzleClient */ diff --git a/src/Models/StorageBoxTypes/StorageBoxType.php b/src/Models/StorageBoxTypes/StorageBoxType.php new file mode 100644 index 00000000..054c50b1 --- /dev/null +++ b/src/Models/StorageBoxTypes/StorageBoxType.php @@ -0,0 +1,98 @@ +getApiHetznerComClient() : null); + parent::__construct($storageClient); + } + + /** + * @param $data + * @return $this + */ + public function setAdditionalData($data) + { + $this->id = $data->id; + $this->name = $data->name; + $this->description = $data->description; + $this->snapshot_limit = $data->snapshot_limit ?? null; + $this->automatic_snapshot_limit = $data->automatic_snapshot_limit ?? null; + $this->subaccounts_limit = $data->subaccounts_limit; + $this->size = $data->size; + $this->prices = isset($data->prices) ? array_map(function ($price) { + return StorageBoxTypePrice::parse($price); + }, $data->prices) : null; + $this->deprecation = $data->deprecation ?? null; + + return $this; + } + + /** + * @param $input + * @return static|null + */ + public static function parse($input) + { + if ($input == null) { + return; + } + + return (new self())->setAdditionalData($input); + } +} diff --git a/src/Models/StorageBoxTypes/StorageBoxTypePrice.php b/src/Models/StorageBoxTypes/StorageBoxTypePrice.php new file mode 100644 index 00000000..9737ae29 --- /dev/null +++ b/src/Models/StorageBoxTypes/StorageBoxTypePrice.php @@ -0,0 +1,60 @@ +location = $location; + $this->price_hourly = $priceHourly; + $this->price_monthly = $priceMonthly; + $this->setup_fee = $setupFee; + } + + /** + * @param $input + * @return self|null + */ + public static function parse($input): ?self + { + if ($input == null) { + return null; + } + + return new self( + $input->location ?? '', + Price::parse($input->price_hourly ?? null), + Price::parse($input->price_monthly ?? null), + Price::parse($input->setup_fee ?? null) + ); + } +} diff --git a/src/Models/StorageBoxTypes/StorageBoxTypes.php b/src/Models/StorageBoxTypes/StorageBoxTypes.php new file mode 100644 index 00000000..a84d6eae --- /dev/null +++ b/src/Models/StorageBoxTypes/StorageBoxTypes.php @@ -0,0 +1,151 @@ +getApiHetznerComClient() : null); + parent::__construct($storageClient); + } + + /** + * Returns all Storage Box type objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-types/list_storage_box_types + * + * @param RequestOpts|null $requestOpts + * @return array + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function all(?RequestOpts $requestOpts = null): array + { + if ($requestOpts == null) { + $requestOpts = new RequestOpts(); + } + + return $this->_all($requestOpts); + } + + /** + * Returns a page of Storage Box type objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-types/list_storage_box_types + * + * @param RequestOpts|null $requestOpts + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function list(?RequestOpts $requestOpts = null): ?APIResponse + { + if ($requestOpts == null) { + $requestOpts = new RequestOpts(); + } + $response = $this->httpClient->get('storage_box_types'.$requestOpts->buildQuery()); + if (! HetznerAPIClient::hasError($response)) { + $resp = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'meta' => Meta::parse($resp->meta), + $this->_getKeys()['many'] => self::parse($resp->{$this->_getKeys()['many']})->{$this->_getKeys()['many']}, + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific Storage Box type by ID. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-types/get_storage_box_type + * + * @param int $id + * @return StorageBoxType|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getById(int $id): ?StorageBoxType + { + $response = $this->httpClient->get('storage_box_types/'.$id); + if (! HetznerAPIClient::hasError($response)) { + return StorageBoxType::parse(json_decode((string) $response->getBody())->storage_box_type); + } + + return null; + } + + /** + * Returns a specific Storage Box type by name. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-types/list_storage_box_types + * + * @param string $name + * @return StorageBoxType|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getByName(string $name): ?StorageBoxType + { + $types = $this->list(); + + foreach ($types->storage_box_types as $type) { + if ($type->name === $name) { + return $type; + } + } + + return null; + } + + /** + * @param $input + * @return $this + */ + public function setAdditionalData($input) + { + $this->storage_box_types = array_map(function ($type) { + return StorageBoxType::parse($type); + }, $input); + + return $this; + } + + /** + * @param $input + * @return static + */ + public static function parse($input) + { + return (new self())->setAdditionalData($input); + } + + /** + * @return array + */ + public function _getKeys(): array + { + return ['one' => 'storage_box_type', 'many' => 'storage_box_types']; + } +} diff --git a/src/Models/StorageBoxes/StorageBox.php b/src/Models/StorageBoxes/StorageBox.php new file mode 100644 index 00000000..00529277 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBox.php @@ -0,0 +1,793 @@ +id = $id; + $storageClient = $httpClient ?? (HetznerAPIClient::$instance ? HetznerAPIClient::$instance->getApiHetznerComClient() : null); + parent::__construct($storageClient); + } + + /** + * @param $data + * @return $this + */ + public function setAdditionalData($data) + { + $this->id = $data->id; + $this->name = $data->name; + $this->storage_box_type = $data->storage_box_type ? StorageBoxType::parse($data->storage_box_type) : null; + $this->location = $data->location ? Location::parse($data->location) : null; + $this->access_settings = $data->access_settings ? StorageBoxAccessSettings::parse($data->access_settings) : null; + $this->snapshot_plan = $data->snapshot_plan ?? null; + $this->protection = $data->protection ? Protection::parse($data->protection) : null; + $this->labels = isset($data->labels) ? get_object_vars($data->labels) : []; + $this->status = $data->status; + $this->username = $data->username ?? null; + $this->server = $data->server ?? null; + $this->system = $data->system ?? null; + $this->stats = $data->stats ? StorageBoxStats::parse($data->stats) : null; + $this->created = $data->created; + + return $this; + } + + /** + * @param $input + * @return static|null + */ + public static function parse($input) + { + if ($input == null) { + return; + } + + return (new self($input->id))->setAdditionalData($input); + } + + /** + * Reload the data of the Storage Box. + * + * @return static + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function reload() + { + return HetznerAPIClient::$instance->storageBoxes()->getById($this->id); + } + + /** + * Deletes a Storage Box. This immediately removes the Storage Box and all its data. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/delete_storage_box + * + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function delete(): ?APIResponse + { + $response = $this->httpClient->delete('storage_boxes/'.$this->id); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Updates a Storage Box. Currently supports renaming and updating labels. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/update_storage_box + * + * @param array $data Keys: name (string), labels (object) + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function update(array $data): ?APIResponse + { + $response = $this->httpClient->put('storage_boxes/'.$this->id, ['json' => $data]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'storage_box' => self::parse(json_decode((string) $response->getBody())->storage_box), + ], $response->getHeaders()); + } + + return null; + } + + // --- Actions --- + + /** + * Changes the delete protection of the Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/change_storage_box_protection + * + * @param bool $delete Whether to enable delete protection + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function changeProtection(bool $delete): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/change_protection', [ + 'json' => ['delete' => $delete], + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Upgrades or downgrades a Storage Box to a different type. + * Downgrading is only possible if current usage does not exceed the new type's capacity. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/change_storage_box_type + * + * @param string $storageBoxType ID or name of the target Storage Box type + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function changeType(string $storageBoxType): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/change_type', [ + 'json' => ['storage_box_type' => $storageBoxType], + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Resets the password of the Storage Box. + * The password must comply with the password policy (12–128 chars, mixed case, digits, special chars). + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/reset_storage_box_password + * + * @param string $password New password + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function resetPassword( + #[SensitiveParameter] + string $password + ): ?APIResponse { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/reset_password', [ + 'json' => ['password' => $password], + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Updates the access settings of the Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/update_storage_box_access_settings + * + * @param StorageBoxAccessSettings $settings + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function updateAccessSettings(StorageBoxAccessSettings $settings): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/update_access_settings', [ + 'json' => $settings->toArray(), + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Enables automatic snapshots on a schedule. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/enable_storage_box_snapshot_plan + * + * @param StorageBoxSnapshotPlanRequest $schedule + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function enableSnapshotPlan(StorageBoxSnapshotPlanRequest $schedule): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/enable_snapshot_plan', [ + 'json' => $schedule->toArray(), + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Disables the automatic snapshot plan. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/disable_storage_box_snapshot_plan + * + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function disableSnapshotPlan(): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/disable_snapshot_plan'); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Restores the Storage Box to the state of a given snapshot. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/rollback_storage_box_snapshot + * + * @param int $snapshotId ID of the snapshot to restore from + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function rollbackSnapshot(int $snapshotId): ?APIResponse + { + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/actions/rollback_snapshot', [ + 'json' => ['snapshot_id' => $snapshotId], + ]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + // --- Per-box action listing --- + + /** + * Returns all actions for this Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/list_storage_box_actions + * + * @return Action[] + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function listActions(): array + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/actions'); + if (! HetznerAPIClient::hasError($response)) { + return array_map(function ($action) { + return Action::parse($action); + }, json_decode((string) $response->getBody())->actions); + } + + return []; + } + + /** + * Returns a specific action for this Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/get_storage_box_action + * + * @param int $actionId + * @return Action|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getAction(int $actionId): ?Action + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/actions/'.$actionId); + if (! HetznerAPIClient::hasError($response)) { + return Action::parse(json_decode((string) $response->getBody())->action); + } + + return null; + } + + // --- Subaccounts --- + + /** + * Returns all subaccounts of this Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/list_storage_box_subaccounts + * + * @return StorageBoxSubaccount[] + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function listSubaccounts(): array + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/subaccounts'); + if (! HetznerAPIClient::hasError($response)) { + return array_map(function ($subaccount) { + return StorageBoxSubaccount::parse($subaccount); + }, json_decode((string) $response->getBody())->subaccounts); + } + + return []; + } + + /** + * Creates a new subaccount for this Storage Box. + * Subaccounts share the storage space of the parent Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/create_storage_box_subaccount + * + * @param string $homeDirectory Home directory of the subaccount (e.g. "backups/server01") + * @param string $password Password (must meet the password policy) + * @param string|null $name Display name + * @param StorageBoxSubaccountAccessSettings|null $accessSettings Access settings for the subaccount + * @param string|null $description Optional description + * @param array $labels User-defined labels + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function createSubaccount( + string $homeDirectory, + #[SensitiveParameter] + string $password, + ?string $name = null, + ?StorageBoxSubaccountAccessSettings $accessSettings = null, + ?string $description = null, + array $labels = [] + ): ?APIResponse { + $payload = [ + 'home_directory' => $homeDirectory, + 'password' => $password, + ]; + if ($name !== null) { + $payload['name'] = $name; + } + if ($accessSettings !== null) { + $payload['access_settings'] = $accessSettings->toArray(); + } + if ($description !== null) { + $payload['description'] = $description; + } + if (! empty($labels)) { + $payload['labels'] = $labels; + } + + $response = $this->httpClient->post('storage_boxes/'.$this->id.'/subaccounts', ['json' => $payload]); + if (! HetznerAPIClient::hasError($response)) { + $data = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'subaccount' => $data->subaccount, + 'action' => Action::parse($data->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific subaccount by ID. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/get_storage_box_subaccount + * + * @param int $subaccountId + * @return StorageBoxSubaccount|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getSubaccount(int $subaccountId): ?StorageBoxSubaccount + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId); + if (! HetznerAPIClient::hasError($response)) { + return StorageBoxSubaccount::parse(json_decode((string) $response->getBody())->subaccount); + } + + return null; + } + + /** + * Updates a subaccount. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/update_storage_box_subaccount + * + * @param int $subaccountId + * @param string|null $name Display name + * @param string|null $description Optional description + * @param array|null $labels User-defined labels + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function updateSubaccount( + int $subaccountId, + ?string $name = null, + ?string $description = null, + ?array $labels = null + ): ?APIResponse { + $payload = []; + if ($name !== null) { + $payload['name'] = $name; + } + if ($description !== null) { + $payload['description'] = $description; + } + if ($labels !== null) { + $payload['labels'] = $labels; + } + + $response = $this->httpClient->put('storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId, ['json' => $payload]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'subaccount' => StorageBoxSubaccount::parse(json_decode((string) $response->getBody())->subaccount), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Deletes a subaccount. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccounts/delete_storage_box_subaccount + * + * @param int $subaccountId + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function deleteSubaccount(int $subaccountId): ?APIResponse + { + $response = $this->httpClient->delete('storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Resets the password of a subaccount. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccount-actions/reset_storage_box_subaccount_password + * + * @param int $subaccountId + * @param string $password New password (must meet the password policy) + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function resetSubaccountPassword( + int $subaccountId, + #[SensitiveParameter] + string $password + ): ?APIResponse { + $response = $this->httpClient->post( + 'storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId.'/actions/reset_subaccount_password', + ['json' => ['password' => $password]] + ); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Changes the home directory of a subaccount. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccount-actions/change_storage_box_subaccount_home_directory + * + * @param int $subaccountId + * @param string $homeDirectory New home directory path + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function changeSubaccountHomeDirectory(int $subaccountId, string $homeDirectory): ?APIResponse + { + $response = $this->httpClient->post( + 'storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId.'/actions/change_home_directory', + ['json' => ['home_directory' => $homeDirectory]] + ); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Updates the access settings of a subaccount. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-subaccount-actions/update_storage_box_subaccount_access_settings + * + * @param int $subaccountId + * @param StorageBoxSubaccountAccessSettings $settings + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function updateSubaccountAccessSettings(int $subaccountId, StorageBoxSubaccountAccessSettings $settings): ?APIResponse + { + $response = $this->httpClient->post( + 'storage_boxes/'.$this->id.'/subaccounts/'.$subaccountId.'/actions/update_access_settings', + ['json' => $settings->toArray()] + ); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + // --- Snapshots --- + + /** + * Returns all snapshots of this Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/list_storage_box_snapshots + * + * @return StorageBoxSnapshot[] + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function listSnapshots(): array + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/snapshots'); + if (! HetznerAPIClient::hasError($response)) { + return array_map(function ($snapshot) { + return StorageBoxSnapshot::parse($snapshot); + }, json_decode((string) $response->getBody())->snapshots); + } + + return []; + } + + /** + * Creates a new snapshot of this Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/create_storage_box_snapshot + * + * @param string|null $description Optional description + * @param array $labels User-defined labels + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function createSnapshot(?string $description = null, array $labels = []): ?APIResponse + { + $payload = []; + if ($description !== null) { + $payload['description'] = $description; + } + if (! empty($labels)) { + $payload['labels'] = $labels; + } + + $response = $this->httpClient->post( + 'storage_boxes/'.$this->id.'/snapshots', + empty($payload) ? [] : ['json' => $payload] + ); + if (! HetznerAPIClient::hasError($response)) { + $data = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'snapshot' => $data->snapshot, + 'action' => Action::parse($data->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific snapshot by ID. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/get_storage_box_snapshot + * + * @param int $snapshotId + * @return StorageBoxSnapshot|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getSnapshot(int $snapshotId): ?StorageBoxSnapshot + { + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/snapshots/'.$snapshotId); + if (! HetznerAPIClient::hasError($response)) { + return StorageBoxSnapshot::parse(json_decode((string) $response->getBody())->snapshot); + } + + return null; + } + + /** + * Updates a snapshot's description or labels. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/update_storage_box_snapshot + * + * @param int $snapshotId + * @param array $data Keys: description (string), labels (object) + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function updateSnapshot(int $snapshotId, array $data): ?APIResponse + { + $response = $this->httpClient->put('storage_boxes/'.$this->id.'/snapshots/'.$snapshotId, ['json' => $data]); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'snapshot' => StorageBoxSnapshot::parse(json_decode((string) $response->getBody())->snapshot), + ], $response->getHeaders()); + } + + return null; + } + + /** + * Deletes a snapshot. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-snapshots/delete_storage_box_snapshot + * + * @param int $snapshotId + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function deleteSnapshot(int $snapshotId): ?APIResponse + { + $response = $this->httpClient->delete('storage_boxes/'.$this->id.'/snapshots/'.$snapshotId); + if (! HetznerAPIClient::hasError($response)) { + return APIResponse::create([ + 'action' => Action::parse(json_decode((string) $response->getBody())->action), + ], $response->getHeaders()); + } + + return null; + } + + // --- Folders --- + + /** + * Returns the list of folder names at the given path within the Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/list_storage_box_folders + * + * @param string $path Directory path to list (default: ".", i.e. the root) + * @return string[] + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function listFolders(string $path = '.'): array + { + $query = $path !== '.' ? '?path='.urlencode($path) : ''; + $response = $this->httpClient->get('storage_boxes/'.$this->id.'/folders'.$query); + if (! HetznerAPIClient::hasError($response)) { + return json_decode((string) $response->getBody())->folders; + } + + return []; + } +} diff --git a/src/Models/StorageBoxes/StorageBoxAccessSettings.php b/src/Models/StorageBoxes/StorageBoxAccessSettings.php new file mode 100644 index 00000000..3ea9a25e --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxAccessSettings.php @@ -0,0 +1,85 @@ +reachable_externally = $reachable_externally; + $this->samba_enabled = $samba_enabled; + $this->ssh_enabled = $ssh_enabled; + $this->webdav_enabled = $webdav_enabled; + $this->zfs_enabled = $zfs_enabled; + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'reachable_externally' => $this->reachable_externally, + 'samba_enabled' => $this->samba_enabled, + 'ssh_enabled' => $this->ssh_enabled, + 'webdav_enabled' => $this->webdav_enabled, + 'zfs_enabled' => $this->zfs_enabled, + ]; + } + + /** + * @param object $input + * @return self|null + */ + public static function parse(object $input): ?self + { + if ($input == null) { + return null; + } + + return new self( + $input->reachable_externally, + $input->samba_enabled, + $input->ssh_enabled, + $input->webdav_enabled, + $input->zfs_enabled + ); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxActions.php b/src/Models/StorageBoxes/StorageBoxActions.php new file mode 100644 index 00000000..b0e074eb --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxActions.php @@ -0,0 +1,134 @@ +getApiHetznerComClient() : null); + parent::__construct($storageClient); + } + + /** + * Returns all Storage Box action objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/list_storage_boxes_actions + * + * @param RequestOpts|null $requestOpts + * @return array + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function all(?RequestOpts $requestOpts = null): array + { + if ($requestOpts == null) { + $requestOpts = new RequestOpts(); + } + + return $this->_all($requestOpts); + } + + /** + * Returns a page of Storage Box action objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/list_storage_boxes_actions + * + * @param RequestOpts|null $requestOpts + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function list(?RequestOpts $requestOpts = null): ?APIResponse + { + if ($requestOpts == null) { + $requestOpts = new RequestOpts(); + } + $response = $this->httpClient->get('storage_boxes/actions'.$requestOpts->buildQuery()); + if (! HetznerAPIClient::hasError($response)) { + $resp = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'meta' => Meta::parse($resp->meta), + 'actions' => self::parse($resp->actions)->actions, + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific Storage Box action by ID. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-box-actions/get_storage_boxes_action + * + * @param int $actionId + * @return Action|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getById(int $actionId): ?Action + { + $response = $this->httpClient->get('storage_boxes/actions/'.$actionId); + if (! HetznerAPIClient::hasError($response)) { + return Action::parse(json_decode((string) $response->getBody())->action); + } + + return null; + } + + public function getByName(string $name) + { + throw new \BadMethodCallException('getByName is not possible on StorageBoxActions'); + } + + /** + * @param $input + * @return $this + */ + public function setAdditionalData($input) + { + $this->actions = array_map(function ($action) { + return Action::parse($action); + }, $input); + + return $this; + } + + /** + * @param $input + * @return static + */ + public static function parse($input) + { + return (new self())->setAdditionalData($input); + } + + /** + * @return array + */ + public function _getKeys(): array + { + return ['one' => 'action', 'many' => 'actions']; + } +} diff --git a/src/Models/StorageBoxes/StorageBoxRequestOpts.php b/src/Models/StorageBoxes/StorageBoxRequestOpts.php new file mode 100644 index 00000000..b7c67db2 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxRequestOpts.php @@ -0,0 +1,23 @@ +name = $name; + } +} diff --git a/src/Models/StorageBoxes/StorageBoxSnapshot.php b/src/Models/StorageBoxes/StorageBoxSnapshot.php new file mode 100644 index 00000000..b5485144 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxSnapshot.php @@ -0,0 +1,79 @@ +id = $data->id; + $this->storage_box = $data->storage_box; + $this->name = $data->name ?? null; + $this->description = $data->description ?? null; + $this->labels = $data->labels ?? null; + $this->stats = $data->stats ?? null; + $this->is_automatic = $data->is_automatic ?? null; + $this->created = $data->created ?? null; + + return $this; + } + + /** + * @param $input + * @return static|null + */ + public static function parse($input) + { + if ($input == null) { + return; + } + + return (new self())->setAdditionalData($input); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php b/src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php new file mode 100644 index 00000000..d9234f98 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxSnapshotPlanRequest.php @@ -0,0 +1,76 @@ +max_snapshots = $max_snapshots; + $this->minute = $minute; + $this->hour = $hour; + $this->day_of_week = $day_of_week; + $this->day_of_month = $day_of_month; + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'max_snapshots' => $this->max_snapshots, + 'minute' => $this->minute, + 'hour' => $this->hour, + 'day_of_week' => $this->day_of_week, + 'day_of_month' => $this->day_of_month, + ]; + } +} diff --git a/src/Models/StorageBoxes/StorageBoxStats.php b/src/Models/StorageBoxes/StorageBoxStats.php new file mode 100644 index 00000000..7fcb7671 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxStats.php @@ -0,0 +1,50 @@ +size = $size; + $this->size_data = $size_data; + $this->size_snapshots = $size_snapshots; + } + + /** + * @param object $input + * @return self|null + */ + public static function parse(object $input): ?self + { + if ($input == null) { + return null; + } + + return new self( + $input->size, + $input->size_data, + $input->size_snapshots + ); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxSubaccount.php b/src/Models/StorageBoxes/StorageBoxSubaccount.php new file mode 100644 index 00000000..6688442b --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxSubaccount.php @@ -0,0 +1,91 @@ +id = $data->id; + $this->storage_box = $data->storage_box; + $this->name = $data->name; + $this->home_directory = $data->home_directory; + $this->access_settings = isset($data->access_settings) ? StorageBoxSubaccountAccessSettings::parse($data->access_settings) : null; + $this->description = $data->description ?? null; + $this->labels = $data->labels ?? null; + $this->username = $data->username ?? null; + $this->server = $data->server ?? null; + $this->created = $data->created ?? null; + + return $this; + } + + /** + * @param $input + * @return static|null + */ + public static function parse($input) + { + if ($input == null) { + return; + } + + return (new self())->setAdditionalData($input); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxSubaccountAccessSettings.php b/src/Models/StorageBoxes/StorageBoxSubaccountAccessSettings.php new file mode 100644 index 00000000..dd094c2b --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxSubaccountAccessSettings.php @@ -0,0 +1,52 @@ +reachable_externally = $reachable_externally; + $this->readonly = $readonly; + $this->samba_enabled = $samba_enabled; + $this->ssh_enabled = $ssh_enabled; + $this->webdav_enabled = $webdav_enabled; + } + + public function toArray(): array + { + return [ + 'reachable_externally' => $this->reachable_externally, + 'readonly' => $this->readonly, + 'samba_enabled' => $this->samba_enabled, + 'ssh_enabled' => $this->ssh_enabled, + 'webdav_enabled' => $this->webdav_enabled, + ]; + } + + public static function parse(object $input): ?self + { + if ($input == null) { + return null; + } + + return new self( + $input->reachable_externally, + $input->readonly, + $input->samba_enabled, + $input->ssh_enabled, + $input->webdav_enabled, + ); + } +} diff --git a/src/Models/StorageBoxes/StorageBoxes.php b/src/Models/StorageBoxes/StorageBoxes.php new file mode 100644 index 00000000..286621d9 --- /dev/null +++ b/src/Models/StorageBoxes/StorageBoxes.php @@ -0,0 +1,202 @@ +getApiHetznerComClient() : null); + parent::__construct($storageClient); + } + + /** + * Returns all existing Storage Box objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/list_storage_boxes + * + * @param RequestOpts|null $requestOpts + * @return array + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function all(?RequestOpts $requestOpts = null): array + { + if ($requestOpts == null) { + $requestOpts = new RequestOpts(); + } + + return $this->_all($requestOpts); + } + + /** + * Returns a page of Storage Box objects. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/list_storage_boxes + * + * @param RequestOpts|null $requestOpts + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function list(?RequestOpts $requestOpts = null): ?APIResponse + { + if ($requestOpts == null) { + $requestOpts = new StorageBoxRequestOpts(); + } + $response = $this->httpClient->get('storage_boxes'.$requestOpts->buildQuery()); + if (! HetznerAPIClient::hasError($response)) { + $resp = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'meta' => Meta::parse($resp->meta), + $this->_getKeys()['many'] => self::parse($resp->{$this->_getKeys()['many']})->{$this->_getKeys()['many']}, + ], $response->getHeaders()); + } + + return null; + } + + /** + * Returns a specific Storage Box by ID. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/get_storage_box + * + * @param int $id + * @return StorageBox|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getById(int $id): ?StorageBox + { + $response = $this->httpClient->get('storage_boxes/'.$id); + if (! HetznerAPIClient::hasError($response)) { + return StorageBox::parse(json_decode((string) $response->getBody())->storage_box); + } + + return null; + } + + /** + * Returns a specific Storage Box by name. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/list_storage_boxes + * + * @param string $name + * @return StorageBox|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function getByName(string $name): ?StorageBox + { + $boxes = $this->list(new StorageBoxRequestOpts($name)); + + return (count($boxes->storage_boxes) > 0) ? $boxes->storage_boxes[0] : null; + } + + /** + * Creates a new Storage Box. + * + * @see https://docs.hetzner.cloud/reference/hetzner#tag/storage-boxes/create_storage_box + * + * @param string $name Name of the Storage Box + * @param string $location ID or name of the location + * @param string $storageBoxType ID or name of the Storage Box type + * @param string $password Initial password (must meet the password policy) + * @param array $labels User-defined labels + * @param array $sshKeys SSH public keys to authorize + * @param StorageBoxAccessSettings|null $accessSettings Initial access settings + * @return APIResponse|null + * + * @throws \LKDev\HetznerCloud\APIException + */ + public function create( + string $name, + string $location, + string $storageBoxType, + #[SensitiveParameter] + string $password, + array $labels = [], + array $sshKeys = [], + ?StorageBoxAccessSettings $accessSettings = null + ): ?APIResponse { + $payload = [ + 'name' => $name, + 'location' => $location, + 'storage_box_type' => $storageBoxType, + 'password' => $password, + ]; + if (! empty($labels)) { + $payload['labels'] = $labels; + } + if (! empty($sshKeys)) { + $payload['ssh_keys'] = $sshKeys; + } + if ($accessSettings !== null) { + $payload['access_settings'] = $accessSettings->toArray(); + } + + $response = $this->httpClient->post('storage_boxes', ['json' => $payload]); + if (! HetznerAPIClient::hasError($response)) { + $data = json_decode((string) $response->getBody()); + + return APIResponse::create([ + 'storage_box' => StorageBox::parse($data->storage_box), + 'action' => Action::parse($data->action), + ], $response->getHeaders()); + } + + return null; + } + + /** + * @param $input + * @return $this + */ + public function setAdditionalData($input) + { + $this->storage_boxes = array_map(function ($box) { + return StorageBox::parse($box); + }, $input); + + return $this; + } + + /** + * @param $input + * @return static + */ + public static function parse($input) + { + return (new self())->setAdditionalData($input); + } + + /** + * @return array + */ + public function _getKeys(): array + { + return ['one' => 'storage_box', 'many' => 'storage_boxes']; + } +} diff --git a/tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php new file mode 100644 index 00000000..eb4faecb --- /dev/null +++ b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypeTest.php @@ -0,0 +1,44 @@ +hetznerApi->setApiHetznerComClient( + new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) + ); + $tmp = new StorageBoxTypes($this->hetznerApi->getApiHetznerComClient()); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_type.json'))); + $this->storageBoxType = $tmp->getById(1); + } + + public function testFields() + { + $this->assertEquals(1, $this->storageBoxType->id); + $this->assertEquals('bx11', $this->storageBoxType->name); + $this->assertEquals('BX11', $this->storageBoxType->description); + $this->assertEquals(10, $this->storageBoxType->snapshot_limit); + $this->assertEquals(10, $this->storageBoxType->automatic_snapshot_limit); + $this->assertEquals(200, $this->storageBoxType->subaccounts_limit); + $this->assertEquals(1073741824, $this->storageBoxType->size); + $this->assertIsArray($this->storageBoxType->prices); + $this->assertInstanceOf(StorageBoxTypePrice::class, $this->storageBoxType->prices[0]); + $this->assertEquals('fsn1', $this->storageBoxType->prices[0]->location); + $this->assertEquals('0.0051', $this->storageBoxType->prices[0]->price_hourly->net); + $this->assertEquals('3.2000', $this->storageBoxType->prices[0]->price_monthly->net); + $this->assertEquals('0.0000', $this->storageBoxType->prices[0]->setup_fee->net); + $this->assertNull($this->storageBoxType->deprecation); + } +} diff --git a/tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php new file mode 100644 index 00000000..cd9c9c79 --- /dev/null +++ b/tests/Unit/Models/StorageBoxTypes/StorageBoxTypesTest.php @@ -0,0 +1,67 @@ +hetznerApi->setApiHetznerComClient( + new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) + ); + $this->storageBoxTypes = new StorageBoxTypes($this->hetznerApi->getApiHetznerComClient()); + } + + public function testGetById() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_type.json'))); + $type = $this->storageBoxTypes->getById(1); + + $this->assertEquals(1, $type->id); + $this->assertEquals('bx11', $type->name); + $this->assertEquals(1073741824, $type->size); + $this->assertEquals(200, $type->subaccounts_limit); + + $this->assertLastRequestEquals('GET', '/storage_box_types/1'); + } + + public function testGetByName() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_types.json'))); + $type = $this->storageBoxTypes->getByName('bx11'); + + $this->assertEquals(1, $type->id); + $this->assertEquals('bx11', $type->name); + + $this->assertLastRequestEquals('GET', '/storage_box_types'); + } + + public function testList() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_types.json'))); + $resp = $this->storageBoxTypes->list(); + + $this->assertCount(1, $resp->storage_box_types); + $this->assertEquals('bx11', $resp->storage_box_types[0]->name); + $this->assertNotNull($resp->meta); + + $this->assertLastRequestEquals('GET', '/storage_box_types'); + } + + public function testAll() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box_types.json'))); + $types = $this->storageBoxTypes->all(); + + $this->assertCount(1, $types); + $this->assertEquals('bx11', $types[0]->name); + } +} diff --git a/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_type.json b/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_type.json new file mode 100644 index 00000000..c51031d4 --- /dev/null +++ b/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_type.json @@ -0,0 +1,20 @@ +{ + "storage_box_type": { + "id": 1, + "name": "bx11", + "description": "BX11", + "snapshot_limit": 10, + "automatic_snapshot_limit": 10, + "subaccounts_limit": 200, + "size": 1073741824, + "prices": [ + { + "location": "fsn1", + "price_hourly": {"gross": "0.0061", "net": "0.0051"}, + "price_monthly": {"gross": "3.8080", "net": "3.2000"}, + "setup_fee": {"gross": "0.0000", "net": "0.0000"} + } + ], + "deprecation": null + } +} diff --git a/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_types.json b/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_types.json new file mode 100644 index 00000000..f20bc705 --- /dev/null +++ b/tests/Unit/Models/StorageBoxTypes/fixtures/storage_box_types.json @@ -0,0 +1,32 @@ +{ + "storage_box_types": [ + { + "id": 1, + "name": "bx11", + "description": "BX11", + "snapshot_limit": 10, + "automatic_snapshot_limit": 10, + "subaccounts_limit": 200, + "size": 1073741824, + "prices": [ + { + "location": "fsn1", + "price_hourly": {"gross": "0.0061", "net": "0.0051"}, + "price_monthly": {"gross": "3.8080", "net": "3.2000"}, + "setup_fee": {"gross": "0.0000", "net": "0.0000"} + } + ], + "deprecation": null + } + ], + "meta": { + "pagination": { + "page": 1, + "per_page": 25, + "previous_page": null, + "next_page": null, + "last_page": 1, + "total_entries": 1 + } + } +} diff --git a/tests/Unit/Models/StorageBoxes/StorageBoxTest.php b/tests/Unit/Models/StorageBoxes/StorageBoxTest.php new file mode 100644 index 00000000..62e07227 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/StorageBoxTest.php @@ -0,0 +1,334 @@ +hetznerApi->setApiHetznerComClient( + new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) + ); + $tmp = new StorageBoxes($this->hetznerApi->getApiHetznerComClient()); + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box.json'))); + $this->storageBox = $tmp->getById(1); + } + + public function testDelete() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->delete(); + + $this->assertNotNull($resp->action); + $this->assertEquals('change_protection', $resp->action->command); + $this->assertLastRequestEquals('DELETE', '/storage_boxes/1'); + } + + public function testUpdate() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box.json'))); + $resp = $this->storageBox->update(['name' => 'renamed-box']); + + $this->assertNotNull($resp->storage_box); + $this->assertLastRequestEquals('PUT', '/storage_boxes/1'); + $this->assertLastRequestBodyParametersEqual(['name' => 'renamed-box']); + } + + public function testChangeProtection() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->changeProtection(true); + + $this->assertNotNull($resp->action); + $this->assertEquals('change_protection', $resp->action->command); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/change_protection'); + $this->assertLastRequestBodyParametersEqual(['delete' => true]); + } + + public function testChangeType() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->changeType('bx21'); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/change_type'); + $this->assertLastRequestBodyParametersEqual(['storage_box_type' => 'bx21']); + } + + public function testResetPassword() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->resetPassword('NewSecurePass123!'); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/reset_password'); + $this->assertLastRequestBodyParametersEqual(['password' => 'NewSecurePass123!']); + } + + public function testUpdateAccessSettings() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $settings = new StorageBoxAccessSettings(false, false, true, false, false); + $resp = $this->storageBox->updateAccessSettings($settings); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/update_access_settings'); + $this->assertLastRequestBodyParametersEqual([ + 'reachable_externally' => false, + 'samba_enabled' => false, + 'ssh_enabled' => true, + 'webdav_enabled' => false, + 'zfs_enabled' => false, + ]); + } + + public function testEnableSnapshotPlan() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->enableSnapshotPlan( + new StorageBoxSnapshotPlanRequest(5, 0, 2) + ); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/enable_snapshot_plan'); + } + + public function testDisableSnapshotPlan() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->disableSnapshotPlan(); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/disable_snapshot_plan'); + } + + public function testRollbackSnapshot() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->rollbackSnapshot(20); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/actions/rollback_snapshot'); + $this->assertLastRequestBodyParametersEqual(['snapshot_id' => 20]); + } + + public function testListActions() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/actions.json'))); + $actions = $this->storageBox->listActions(); + + $this->assertCount(1, $actions); + $this->assertEquals(101, $actions[0]->id); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/actions'); + } + + public function testGetAction() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $action = $this->storageBox->getAction(101); + + $this->assertEquals(101, $action->id); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/actions/101'); + } + + public function testListSubaccounts() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/subaccounts.json'))); + $subaccounts = $this->storageBox->listSubaccounts(); + + $this->assertCount(1, $subaccounts); + $this->assertEquals(10, $subaccounts[0]->id); + $this->assertEquals('my-name', $subaccounts[0]->name); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/subaccounts'); + } + + public function testCreateSubaccount() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/subaccount_create.json'))); + $resp = $this->storageBox->createSubaccount('backups/server01', 'SubPass123!', 'backup-user'); + + $this->assertNotNull($resp->action); + $this->assertEquals('create_subaccount', $resp->action->command); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/subaccounts'); + $this->assertLastRequestBodyParametersEqual([ + 'home_directory' => 'backups/server01', + 'password' => 'SubPass123!', + 'name' => 'backup-user', + ]); + } + + public function testCreateSubaccountWithAccessSettings() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/subaccount_create.json'))); + $settings = new StorageBoxSubaccountAccessSettings(ssh_enabled: true); + $resp = $this->storageBox->createSubaccount('backups/server01', 'SubPass123!', 'backup-user', $settings); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/subaccounts'); + $this->assertLastRequestBodyParametersEqual([ + 'home_directory' => 'backups/server01', + 'password' => 'SubPass123!', + 'name' => 'backup-user', + 'access_settings' => [ + 'reachable_externally' => false, + 'readonly' => false, + 'samba_enabled' => false, + 'ssh_enabled' => true, + 'webdav_enabled' => false, + ], + ]); + } + + public function testGetSubaccount() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/subaccount.json'))); + $sub = $this->storageBox->getSubaccount(10); + + $this->assertEquals(10, $sub->id); + $this->assertEquals('my-name', $sub->name); + $this->assertEquals('my_backups/host01.my.company', $sub->home_directory); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/subaccounts/10'); + } + + public function testUpdateSubaccount() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/subaccount.json'))); + $resp = $this->storageBox->updateSubaccount(10, name: 'renamed-user'); + + $this->assertNotNull($resp->subaccount); + $this->assertLastRequestEquals('PUT', '/storage_boxes/1/subaccounts/10'); + $this->assertLastRequestBodyParametersEqual(['name' => 'renamed-user']); + } + + public function testDeleteSubaccount() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->deleteSubaccount(10); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('DELETE', '/storage_boxes/1/subaccounts/10'); + } + + public function testResetSubaccountPassword() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->resetSubaccountPassword(10, 'NewSubPass123!'); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/subaccounts/10/actions/reset_subaccount_password'); + $this->assertLastRequestBodyParametersEqual(['password' => 'NewSubPass123!']); + } + + public function testChangeSubaccountHomeDirectory() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->changeSubaccountHomeDirectory(10, 'backups/server02'); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/subaccounts/10/actions/change_home_directory'); + $this->assertLastRequestBodyParametersEqual(['home_directory' => 'backups/server02']); + } + + public function testUpdateSubaccountAccessSettings() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $settings = new StorageBoxSubaccountAccessSettings(ssh_enabled: true, readonly: true); + $resp = $this->storageBox->updateSubaccountAccessSettings(10, $settings); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/subaccounts/10/actions/update_access_settings'); + $this->assertLastRequestBodyParametersEqual([ + 'reachable_externally' => false, + 'readonly' => true, + 'samba_enabled' => false, + 'ssh_enabled' => true, + 'webdav_enabled' => false, + ]); + } + + public function testListSnapshots() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/snapshots.json'))); + $snapshots = $this->storageBox->listSnapshots(); + + $this->assertCount(1, $snapshots); + $this->assertEquals(20, $snapshots[0]->id); + $this->assertEquals('before-migration', $snapshots[0]->description); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/snapshots'); + } + + public function testCreateSnapshot() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/snapshot_create.json'))); + $resp = $this->storageBox->createSnapshot('before-migration'); + + $this->assertNotNull($resp->action); + $this->assertEquals('create_snapshot', $resp->action->command); + $this->assertLastRequestEquals('POST', '/storage_boxes/1/snapshots'); + $this->assertLastRequestBodyParametersEqual(['description' => 'before-migration']); + } + + public function testGetSnapshot() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/snapshot.json'))); + $snap = $this->storageBox->getSnapshot(20); + + $this->assertEquals(20, $snap->id); + $this->assertEquals('before-migration', $snap->description); + $this->assertFalse($snap->is_automatic); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/snapshots/20'); + } + + public function testUpdateSnapshot() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/snapshot.json'))); + $resp = $this->storageBox->updateSnapshot(20, ['description' => 'updated-desc']); + + $this->assertNotNull($resp->snapshot); + $this->assertLastRequestEquals('PUT', '/storage_boxes/1/snapshots/20'); + $this->assertLastRequestBodyParametersEqual(['description' => 'updated-desc']); + } + + public function testDeleteSnapshot() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/action.json'))); + $resp = $this->storageBox->deleteSnapshot(20); + + $this->assertNotNull($resp->action); + $this->assertLastRequestEquals('DELETE', '/storage_boxes/1/snapshots/20'); + } + + public function testListFolders() + { + $foldersJson = json_encode(['folders' => ['documents', 'backups', 'media']]); + $this->mockHandler->append(new Response(200, [], $foldersJson)); + $folders = $this->storageBox->listFolders(); + + $this->assertCount(3, $folders); + $this->assertEquals('documents', $folders[0]); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/folders'); + } + + public function testListFoldersWithPath() + { + $foldersJson = json_encode(['folders' => ['server01', 'server02']]); + $this->mockHandler->append(new Response(200, [], $foldersJson)); + $folders = $this->storageBox->listFolders('./backups'); + + $this->assertCount(2, $folders); + $this->assertLastRequestEquals('GET', '/storage_boxes/1/folders'); + $this->assertLastRequestQueryParametersContains('path', './backups'); + } +} diff --git a/tests/Unit/Models/StorageBoxes/StorageBoxesTest.php b/tests/Unit/Models/StorageBoxes/StorageBoxesTest.php new file mode 100644 index 00000000..b2d7cf67 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/StorageBoxesTest.php @@ -0,0 +1,101 @@ +hetznerApi->setApiHetznerComClient( + new GuzzleClient($this->hetznerApi, ['handler' => $this->mockHandler]) + ); + $this->storageBoxes = new StorageBoxes($this->hetznerApi->getApiHetznerComClient()); + } + + public function testCreate() + { + $this->mockHandler->append(new Response(201, [], file_get_contents(__DIR__.'/fixtures/storage_box_create.json'))); + $resp = $this->storageBoxes->create('new-storage-box', 'fsn1', 'bx11', 'SecurePass123!'); + + $box = $resp->getResponsePart('storage_box'); + $this->assertEquals(2, $box->id); + $this->assertEquals('my-resource', $box->name); + $this->assertEquals('initializing', $box->status); + + $this->assertNotNull($resp->action); + $this->assertEquals('create', $resp->action->command); + + $this->assertLastRequestEquals('POST', '/storage_boxes'); + $this->assertLastRequestBodyParametersEqual([ + 'name' => 'new-storage-box', + 'location' => 'fsn1', + 'storage_box_type' => 'bx11', + 'password' => 'SecurePass123!', + ]); + } + + public function testGetById() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_box.json'))); + $box = $this->storageBoxes->getById(1); + + $this->assertEquals(1, $box->id); + $this->assertEquals('my-storage-box', $box->name); + $this->assertEquals('active', $box->status); + $this->assertEquals('u1337', $box->username); + $this->assertNotNull($box->location); + $this->assertEquals('fsn1', $box->location->name); + $this->assertNotNull($box->storage_box_type); + $this->assertEquals('bx11', $box->storage_box_type->name); + $this->assertInstanceOf(StorageBoxAccessSettings::class, $box->access_settings); + $this->assertTrue($box->access_settings->ssh_enabled); + $this->assertFalse($box->access_settings->samba_enabled); + $this->assertInstanceOf(StorageBoxStats::class, $box->stats); + $this->assertEquals(0, $box->stats->size); + + $this->assertLastRequestEquals('GET', '/storage_boxes/1'); + } + + public function testGetByName() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_boxes.json'))); + $box = $this->storageBoxes->getByName('my-storage-box'); + + $this->assertEquals(1, $box->id); + $this->assertEquals('my-storage-box', $box->name); + + $this->assertLastRequestEquals('GET', '/storage_boxes'); + $this->assertLastRequestQueryParametersContains('name', 'my-storage-box'); + } + + public function testList() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_boxes.json'))); + $resp = $this->storageBoxes->list(); + + $this->assertCount(1, $resp->storage_boxes); + $this->assertEquals(1, $resp->storage_boxes[0]->id); + $this->assertNotNull($resp->meta); + + $this->assertLastRequestEquals('GET', '/storage_boxes'); + } + + public function testAll() + { + $this->mockHandler->append(new Response(200, [], file_get_contents(__DIR__.'/fixtures/storage_boxes.json'))); + $boxes = $this->storageBoxes->all(); + + $this->assertCount(1, $boxes); + $this->assertEquals(1, $boxes[0]->id); + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/action.json b/tests/Unit/Models/StorageBoxes/fixtures/action.json new file mode 100644 index 00000000..c2755036 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/action.json @@ -0,0 +1,14 @@ +{ + "action": { + "id": 101, + "command": "change_protection", + "status": "success", + "progress": 100, + "started": "2016-01-30T23:50:00Z", + "finished": "2016-01-30T23:55:00Z", + "resources": [ + {"id": 1, "type": "storage_box"} + ], + "error": null + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/actions.json b/tests/Unit/Models/StorageBoxes/fixtures/actions.json new file mode 100644 index 00000000..094591e1 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/actions.json @@ -0,0 +1,26 @@ +{ + "actions": [ + { + "id": 101, + "command": "change_protection", + "status": "success", + "progress": 100, + "started": "2016-01-30T23:50:00Z", + "finished": "2016-01-30T23:55:00Z", + "resources": [ + {"id": 1, "type": "storage_box"} + ], + "error": null + } + ], + "meta": { + "pagination": { + "page": 1, + "per_page": 25, + "previous_page": null, + "next_page": null, + "last_page": 1, + "total_entries": 1 + } + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/snapshot.json b/tests/Unit/Models/StorageBoxes/fixtures/snapshot.json new file mode 100644 index 00000000..45f4b979 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/snapshot.json @@ -0,0 +1,17 @@ +{ + "snapshot": { + "id": 20, + "storage_box": 1, + "name": "2025-02-12T11-35-19", + "description": "before-migration", + "labels": { + "environment": "prod" + }, + "stats": { + "size": 2097152, + "size_filesystem": 1048576 + }, + "is_automatic": false, + "created": "2025-02-12T11:35:19Z" + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/snapshot_create.json b/tests/Unit/Models/StorageBoxes/fixtures/snapshot_create.json new file mode 100644 index 00000000..e22c3f23 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/snapshot_create.json @@ -0,0 +1,19 @@ +{ + "snapshot": { + "id": 20, + "storage_box": 1 + }, + "action": { + "id": 13, + "command": "create_snapshot", + "status": "running", + "progress": 0, + "started": "2016-01-30T23:50:00Z", + "finished": null, + "resources": [ + {"id": 1, "type": "storage_box"}, + {"id": 20, "type": "storage_box_snapshot"} + ], + "error": null + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/snapshots.json b/tests/Unit/Models/StorageBoxes/fixtures/snapshots.json new file mode 100644 index 00000000..8fccb463 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/snapshots.json @@ -0,0 +1,19 @@ +{ + "snapshots": [ + { + "id": 20, + "storage_box": 1, + "name": "2025-02-12T11-35-19", + "description": "before-migration", + "labels": { + "environment": "prod" + }, + "stats": { + "size": 2097152, + "size_filesystem": 1048576 + }, + "is_automatic": false, + "created": "2025-02-12T11:35:19Z" + } + ] +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/storage_box.json b/tests/Unit/Models/StorageBoxes/fixtures/storage_box.json new file mode 100644 index 00000000..644dc997 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/storage_box.json @@ -0,0 +1,58 @@ +{ + "storage_box": { + "id": 1, + "name": "my-storage-box", + "storage_box_type": { + "id": 1, + "name": "bx11", + "description": "BX11", + "snapshot_limit": 10, + "automatic_snapshot_limit": 10, + "subaccounts_limit": 200, + "size": 1073741824, + "prices": [ + { + "location": "fsn1", + "price_hourly": {"gross": "0.0061", "net": "0.0051"}, + "price_monthly": {"gross": "3.8080", "net": "3.2000"}, + "setup_fee": {"gross": "0.0000", "net": "0.0000"} + } + ], + "deprecation": null + }, + "location": { + "id": 1, + "name": "fsn1", + "description": "Falkenstein DC Park 1", + "country": "DE", + "city": "Falkenstein", + "latitude": 50.47612, + "longitude": 12.370071, + "network_zone": "eu-central" + }, + "access_settings": { + "reachable_externally": false, + "samba_enabled": false, + "ssh_enabled": true, + "webdav_enabled": false, + "zfs_enabled": false + }, + "snapshot_plan": null, + "protection": { + "delete": false + }, + "labels": { + "environment": "prod" + }, + "status": "active", + "username": "u1337", + "server": "u1337.your-storagebox.de", + "system": "FSN1-BX355", + "stats": { + "size": 0, + "size_data": 0, + "size_snapshots": 0 + }, + "created": "2016-01-30T23:50:00Z" + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/storage_box_create.json b/tests/Unit/Models/StorageBoxes/fixtures/storage_box_create.json new file mode 100644 index 00000000..4225c353 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/storage_box_create.json @@ -0,0 +1,68 @@ +{ + "storage_box": { + "id": 2, + "name": "my-resource", + "storage_box_type": { + "id": 1, + "name": "bx11", + "description": "BX11", + "snapshot_limit": 10, + "automatic_snapshot_limit": 10, + "subaccounts_limit": 200, + "size": 1073741824, + "prices": [ + { + "location": "fsn1", + "price_hourly": {"gross": "0.0061", "net": "0.0051"}, + "price_monthly": {"gross": "3.8080", "net": "3.2000"}, + "setup_fee": {"gross": "0.0000", "net": "0.0000"} + } + ], + "deprecation": null + }, + "location": { + "id": 1, + "name": "fsn1", + "description": "Falkenstein DC Park 1", + "country": "DE", + "city": "Falkenstein", + "latitude": 50.47612, + "longitude": 12.370071, + "network_zone": "eu-central" + }, + "access_settings": { + "reachable_externally": false, + "samba_enabled": false, + "ssh_enabled": false, + "webdav_enabled": false, + "zfs_enabled": false + }, + "snapshot_plan": null, + "protection": { + "delete": false + }, + "labels": {}, + "status": "initializing", + "username": null, + "server": null, + "system": null, + "stats": { + "size": 0, + "size_data": 0, + "size_snapshots": 0 + }, + "created": "2016-01-30T23:50:00Z" + }, + "action": { + "id": 13, + "command": "create", + "status": "running", + "progress": 0, + "started": "2016-01-30T23:50:00Z", + "finished": null, + "resources": [ + {"id": 2, "type": "storage_box"} + ], + "error": null + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/storage_boxes.json b/tests/Unit/Models/StorageBoxes/fixtures/storage_boxes.json new file mode 100644 index 00000000..2d8a54ef --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/storage_boxes.json @@ -0,0 +1,70 @@ +{ + "storage_boxes": [ + { + "id": 1, + "name": "my-storage-box", + "storage_box_type": { + "id": 1, + "name": "bx11", + "description": "BX11", + "snapshot_limit": 10, + "automatic_snapshot_limit": 10, + "subaccounts_limit": 200, + "size": 1073741824, + "prices": [ + { + "location": "fsn1", + "price_hourly": {"gross": "0.0061", "net": "0.0051"}, + "price_monthly": {"gross": "3.8080", "net": "3.2000"}, + "setup_fee": {"gross": "0.0000", "net": "0.0000"} + } + ], + "deprecation": null + }, + "location": { + "id": 1, + "name": "fsn1", + "description": "Falkenstein DC Park 1", + "country": "DE", + "city": "Falkenstein", + "latitude": 50.47612, + "longitude": 12.370071, + "network_zone": "eu-central" + }, + "access_settings": { + "reachable_externally": false, + "samba_enabled": false, + "ssh_enabled": true, + "webdav_enabled": false, + "zfs_enabled": false + }, + "snapshot_plan": null, + "protection": { + "delete": false + }, + "labels": { + "environment": "prod" + }, + "status": "active", + "username": "u1337", + "server": "u1337.your-storagebox.de", + "system": "FSN1-BX355", + "stats": { + "size": 0, + "size_data": 0, + "size_snapshots": 0 + }, + "created": "2016-01-30T23:50:00Z" + } + ], + "meta": { + "pagination": { + "page": 1, + "per_page": 25, + "previous_page": null, + "next_page": null, + "last_page": 1, + "total_entries": 1 + } + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/subaccount.json b/tests/Unit/Models/StorageBoxes/fixtures/subaccount.json new file mode 100644 index 00000000..2fa56d1a --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/subaccount.json @@ -0,0 +1,22 @@ +{ + "subaccount": { + "id": 10, + "storage_box": 1, + "name": "my-name", + "home_directory": "my_backups/host01.my.company", + "access_settings": { + "reachable_externally": true, + "samba_enabled": false, + "ssh_enabled": true, + "webdav_enabled": false, + "readonly": false + }, + "description": "host01 backup", + "labels": { + "environment": "prod" + }, + "username": "u1337-sub1", + "server": "u1337-sub1.your-storagebox.de", + "created": "2025-02-22T00:02:00Z" + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/subaccount_create.json b/tests/Unit/Models/StorageBoxes/fixtures/subaccount_create.json new file mode 100644 index 00000000..85b553c1 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/subaccount_create.json @@ -0,0 +1,19 @@ +{ + "subaccount": { + "id": 10, + "storage_box": 1 + }, + "action": { + "id": 13, + "command": "create_subaccount", + "status": "running", + "progress": 0, + "started": "2016-01-30T23:50:00Z", + "finished": null, + "resources": [ + {"id": 1, "type": "storage_box"}, + {"id": 10, "type": "storage_box_subaccount"} + ], + "error": null + } +} diff --git a/tests/Unit/Models/StorageBoxes/fixtures/subaccounts.json b/tests/Unit/Models/StorageBoxes/fixtures/subaccounts.json new file mode 100644 index 00000000..867fecc7 --- /dev/null +++ b/tests/Unit/Models/StorageBoxes/fixtures/subaccounts.json @@ -0,0 +1,24 @@ +{ + "subaccounts": [ + { + "id": 10, + "storage_box": 1, + "name": "my-name", + "home_directory": "my_backups/host01.my.company", + "access_settings": { + "reachable_externally": true, + "samba_enabled": false, + "ssh_enabled": true, + "webdav_enabled": false, + "readonly": false + }, + "description": "host01 backup", + "labels": { + "environment": "prod" + }, + "username": "u1337-sub1", + "server": "u1337-sub1.your-storagebox.de", + "created": "2025-02-22T00:02:00Z" + } + ] +}