Remove locations

This commit is contained in:
Lance Pioch 2024-03-14 02:23:30 -04:00
parent 9fb0c451f5
commit e4cee4d69d
66 changed files with 76 additions and 1628 deletions

View File

@ -1,55 +0,0 @@
<?php
namespace App\Console\Commands\Location;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use App\Services\Locations\LocationDeletionService;
use App\Contracts\Repository\LocationRepositoryInterface;
class DeleteLocationCommand extends Command
{
protected $description = 'Deletes a location from the Panel.';
protected $signature = 'p:location:delete {--short= : The short code of the location to delete.}';
protected Collection $locations;
/**
* DeleteLocationCommand constructor.
*/
public function __construct(
private LocationDeletionService $deletionService,
private LocationRepositoryInterface $repository
) {
parent::__construct();
}
/**
* Respond to the command request.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
* @throws \App\Exceptions\Service\Location\HasActiveNodesException
*/
public function handle()
{
$this->locations = $this->locations ?? $this->repository->all();
$short = $this->option('short') ?? $this->anticipate(
trans('command/messages.location.ask_short'),
$this->locations->pluck('short')->toArray()
);
$location = $this->locations->where('short', $short)->first();
if (is_null($location)) {
$this->error(trans('command/messages.location.no_location_found'));
if ($this->input->isInteractive()) {
$this->handle();
}
return;
}
$this->deletionService->handle($location->id);
$this->line(trans('command/messages.location.deleted'));
}
}

View File

@ -1,40 +0,0 @@
<?php
namespace App\Console\Commands\Location;
use Illuminate\Console\Command;
use App\Services\Locations\LocationCreationService;
class MakeLocationCommand extends Command
{
protected $signature = 'p:location:make
{--short= : The shortcode name of this location (ex. us1).}
{--long= : A longer description of this location.}';
protected $description = 'Creates a new location on the system via the CLI.';
/**
* Create a new command instance.
*/
public function __construct(private LocationCreationService $creationService)
{
parent::__construct();
}
/**
* Handle the command execution process.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function handle()
{
$short = $this->option('short') ?? $this->ask(trans('command/messages.location.ask_short'));
$long = $this->option('long') ?? $this->ask(trans('command/messages.location.ask_long'));
$location = $this->creationService->handle(compact('short', 'long'));
$this->line(trans('command/messages.location.created', [
'name' => $location->short,
'id' => $location->id,
]));
}
}

View File

@ -44,7 +44,6 @@ class MakeNodeCommand extends Command
{
$data['name'] = $this->option('name') ?? $this->ask('Enter a short identifier used to distinguish this node from others');
$data['description'] = $this->option('description') ?? $this->ask('Enter a description to identify the node');
$data['location_id'] = $this->option('locationId') ?? $this->ask('Enter a valid location id');
$data['scheme'] = $this->option('scheme') ?? $this->anticipate(
'Please either enter https for SSL or http for a non-ssl connection',
['https', 'http'],
@ -64,6 +63,6 @@ class MakeNodeCommand extends Command
$data['daemonBase'] = $this->option('daemonBase') ?? $this->ask('Enter the base folder', '/var/lib/panel/volumes');
$node = $this->creationService->handle($data);
$this->line('Successfully created a new node on the location ' . $data['location_id'] . ' with the name ' . $data['name'] . ' and has an id of ' . $node->id . '.');
$this->line('Successfully created a new node with the name ' . $data['name'] . ' and has an id of ' . $node->id . '.');
}
}

View File

@ -16,7 +16,6 @@ class NodeListCommand extends Command
'id' => $node->id,
'uuid' => $node->uuid,
'name' => $node->name,
'location' => $node->location->short,
'host' => $node->getConnectionAddress(),
];
});
@ -24,7 +23,7 @@ class NodeListCommand extends Command
if ($this->option('format') === 'json') {
$this->output->write($nodes->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
} else {
$this->table(['ID', 'UUID', 'Name', 'Location', 'Host'], $nodes->toArray());
$this->table(['ID', 'UUID', 'Name', 'Host'], $nodes->toArray());
}
$this->output->newLine();

View File

@ -1,33 +0,0 @@
<?php
namespace App\Contracts\Repository;
use App\Models\Location;
use Illuminate\Support\Collection;
interface LocationRepositoryInterface extends RepositoryInterface
{
/**
* Return locations with a count of nodes and servers attached to it.
*/
public function getAllWithDetails(): Collection;
/**
* Return all the available locations with the nodes as a relationship.
*/
public function getAllWithNodes(): Collection;
/**
* Return all the nodes and their respective count of servers for a location.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithNodes(int $id): Location;
/**
* Return a location and the count of nodes in that location.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithNodeCount(int $id): Location;
}

View File

@ -23,7 +23,7 @@ interface NodeRepositoryInterface extends RepositoryInterface
/**
* Return a single node with location and server information.
*/
public function loadLocationAndServerCount(Node $node, bool $refresh = false): Node;
public function loadServerCount(Node $node, bool $refresh = false): Node;
/**
* Attach a paginated set of allocations to a node mode including

View File

@ -1,14 +0,0 @@
<?php
namespace App\Exceptions\Service\Location;
use Illuminate\Http\Response;
use App\Exceptions\DisplayException;
class HasActiveNodesException extends DisplayException
{
public function getStatusCode(): int
{
return Response::HTTP_BAD_REQUEST;
}
}

View File

@ -2,7 +2,7 @@
namespace App\Http\Controllers\Admin;
use Exception;
use App\Models\Node;
use Illuminate\View\View;
use App\Models\DatabaseHost;
use Illuminate\Http\RedirectResponse;
@ -14,7 +14,6 @@ use App\Http\Requests\Admin\DatabaseHostFormRequest;
use App\Services\Databases\Hosts\HostCreationService;
use App\Services\Databases\Hosts\HostDeletionService;
use App\Contracts\Repository\DatabaseRepositoryInterface;
use App\Contracts\Repository\LocationRepositoryInterface;
use App\Contracts\Repository\DatabaseHostRepositoryInterface;
class DatabaseController extends Controller
@ -29,7 +28,6 @@ class DatabaseController extends Controller
private HostCreationService $creationService,
private HostDeletionService $deletionService,
private HostUpdateService $updateService,
private LocationRepositoryInterface $locationRepository,
private ViewFactory $view
) {
}
@ -40,7 +38,7 @@ class DatabaseController extends Controller
public function index(): View
{
return $this->view->make('admin.databases.index', [
'locations' => $this->locationRepository->getAllWithNodes(),
'nodes' => Node::all(),
'hosts' => $this->repository->getWithViewDetails(),
]);
}
@ -53,7 +51,6 @@ class DatabaseController extends Controller
public function view(int $host): View
{
return $this->view->make('admin.databases.view', [
'locations' => $this->locationRepository->getAllWithNodes(),
'host' => $this->repository->find($host),
'databases' => $this->databaseRepository->getDatabasesForHost($host),
]);

View File

@ -1,103 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\View\View;
use App\Models\Location;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\LocationFormRequest;
use App\Services\Locations\LocationUpdateService;
use App\Services\Locations\LocationCreationService;
use App\Services\Locations\LocationDeletionService;
use App\Contracts\Repository\LocationRepositoryInterface;
class LocationController extends Controller
{
/**
* LocationController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected LocationCreationService $creationService,
protected LocationDeletionService $deletionService,
protected LocationRepositoryInterface $repository,
protected LocationUpdateService $updateService,
protected ViewFactory $view
) {
}
/**
* Return the location overview page.
*/
public function index(): View
{
return $this->view->make('admin.locations.index', [
'locations' => $this->repository->getAllWithDetails(),
]);
}
/**
* Return the location view page.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function view(int $id): View
{
return $this->view->make('admin.locations.view', [
'location' => $this->repository->getWithNodes($id),
]);
}
/**
* Handle request to create new location.
*
* @throws \Throwable
*/
public function create(LocationFormRequest $request): RedirectResponse
{
$location = $this->creationService->handle($request->normalize());
$this->alert->success('Location was created successfully.')->flash();
return redirect()->route('admin.locations.view', $location->id);
}
/**
* Handle request to update or delete location.
*
* @throws \Throwable
*/
public function update(LocationFormRequest $request, Location $location): RedirectResponse
{
if ($request->input('action') === 'delete') {
return $this->delete($location);
}
$this->updateService->handle($location->id, $request->normalize());
$this->alert->success('Location was updated successfully.')->flash();
return redirect()->route('admin.locations.view', $location->id);
}
/**
* Delete a location from the system.
*
* @throws \Exception
* @throws \App\Exceptions\DisplayException
*/
public function delete(Location $location): RedirectResponse
{
try {
$this->deletionService->handle($location->id);
return redirect()->route('admin.locations');
} catch (DisplayException $ex) {
$this->alert->danger($ex->getMessage())->flash();
}
return redirect()->route('admin.locations.view', $location->id);
}
}

View File

@ -8,14 +8,12 @@ use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Models\Mount;
use App\Models\Location;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\MountFormRequest;
use App\Repositories\Eloquent\MountRepository;
use App\Contracts\Repository\LocationRepositoryInterface;
class MountController extends Controller
{
@ -24,7 +22,6 @@ class MountController extends Controller
*/
public function __construct(
protected AlertsMessageBag $alert,
protected LocationRepositoryInterface $locationRepository,
protected MountRepository $repository,
protected ViewFactory $view
) {
@ -48,12 +45,10 @@ class MountController extends Controller
public function view(string $id): View
{
$eggs = Egg::all();
$locations = Location::query()->with('nodes')->get();
return $this->view->make('admin.mounts.view', [
'mount' => $this->repository->getWithRelations($id),
'eggs' => $eggs,
'locations' => $locations,
]);
}

View File

@ -24,7 +24,7 @@ class NodeController extends Controller
public function index(Request $request): View
{
$nodes = QueryBuilder::for(
Node::query()->with('location')->withCount('servers')
Node::query()->withCount('servers')
)
->allowedFilters(['uuid', 'name'])
->allowedSorts(['id'])

View File

@ -13,7 +13,6 @@ use App\Repositories\Eloquent\NodeRepository;
use App\Repositories\Eloquent\ServerRepository;
use App\Traits\Controllers\JavascriptInjection;
use App\Services\Helpers\SoftwareVersionService;
use App\Repositories\Eloquent\LocationRepository;
use App\Repositories\Eloquent\AllocationRepository;
class NodeViewController extends Controller
@ -25,7 +24,6 @@ class NodeViewController extends Controller
*/
public function __construct(
private AllocationRepository $allocationRepository,
private LocationRepository $locationRepository,
private NodeRepository $repository,
private ServerRepository $serverRepository,
private SoftwareVersionService $versionService,
@ -38,7 +36,7 @@ class NodeViewController extends Controller
*/
public function index(Request $request, Node $node): View
{
$node = $this->repository->loadLocationAndServerCount($node);
$node = $this->repository->loadServerCount($node);
return $this->view->make('admin.nodes.view.index', [
'node' => $node,
@ -54,7 +52,6 @@ class NodeViewController extends Controller
{
return $this->view->make('admin.nodes.view.settings', [
'node' => $node,
'locations' => $this->locationRepository->all(),
]);
}

View File

@ -22,7 +22,6 @@ use App\Contracts\Repository\NodeRepositoryInterface;
use App\Contracts\Repository\ServerRepositoryInterface;
use App\Http\Requests\Admin\Node\AllocationFormRequest;
use App\Services\Allocations\AllocationDeletionService;
use App\Contracts\Repository\LocationRepositoryInterface;
use App\Contracts\Repository\AllocationRepositoryInterface;
use App\Http\Requests\Admin\Node\AllocationAliasFormRequest;
@ -39,7 +38,6 @@ class NodesController extends Controller
protected CacheRepository $cache,
protected NodeCreationService $creationService,
protected NodeDeletionService $deletionService,
protected LocationRepositoryInterface $locationRepository,
protected NodeRepositoryInterface $repository,
protected ServerRepositoryInterface $serverRepository,
protected NodeUpdateService $updateService,
@ -53,14 +51,7 @@ class NodesController extends Controller
*/
public function create(): View|RedirectResponse
{
$locations = $this->locationRepository->all();
if (count($locations) < 1) {
$this->alert->warning(trans('admin/node.notices.location_required'))->flash();
return redirect()->route('admin.locations');
}
return $this->view->make('admin.nodes.new', ['locations' => $locations]);
return $this->view->make('admin.nodes.new');
}
/**

View File

@ -5,7 +5,6 @@ namespace App\Http\Controllers\Admin\Servers;
use App\Models\Egg;
use Illuminate\View\View;
use App\Models\Node;
use App\Models\Location;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
@ -49,8 +48,8 @@ class CreateServerController extends Controller
]);
return $this->view->make('admin.servers.new', [
'locations' => Location::all(),
'eggs' => $eggs,
'nodes' => Node::all(),
]);
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Admin\Servers;
use App\Models\Egg;
use App\Models\Node;
use App\Repositories\Eloquent\EggRepository;
use Illuminate\View\View;
use Illuminate\Http\Request;
@ -15,7 +16,6 @@ use App\Repositories\Eloquent\NodeRepository;
use App\Repositories\Eloquent\MountRepository;
use App\Repositories\Eloquent\ServerRepository;
use App\Traits\Controllers\JavascriptInjection;
use App\Repositories\Eloquent\LocationRepository;
use App\Repositories\Eloquent\DatabaseHostRepository;
class ServerViewController extends Controller
@ -27,7 +27,6 @@ class ServerViewController extends Controller
*/
public function __construct(
private DatabaseHostRepository $databaseHostRepository,
private LocationRepository $locationRepository,
private MountRepository $mountRepository,
private EggRepository $eggRepository,
private NodeRepository $nodeRepository,
@ -134,8 +133,8 @@ class ServerViewController extends Controller
]);
return $this->view->make('admin.servers.view.manage', [
'nodes' => Node::all(),
'server' => $server,
'locations' => $this->locationRepository->all(),
'canTransfer' => $canTransfer,
]);
}

View File

@ -1,104 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Application\Locations;
use Illuminate\Http\Response;
use App\Models\Location;
use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder;
use App\Services\Locations\LocationUpdateService;
use App\Services\Locations\LocationCreationService;
use App\Services\Locations\LocationDeletionService;
use App\Transformers\Api\Application\LocationTransformer;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\Locations\GetLocationRequest;
use App\Http\Requests\Api\Application\Locations\GetLocationsRequest;
use App\Http\Requests\Api\Application\Locations\StoreLocationRequest;
use App\Http\Requests\Api\Application\Locations\DeleteLocationRequest;
use App\Http\Requests\Api\Application\Locations\UpdateLocationRequest;
class LocationController extends ApplicationApiController
{
/**
* LocationController constructor.
*/
public function __construct(
private LocationCreationService $creationService,
private LocationDeletionService $deletionService,
private LocationUpdateService $updateService
) {
parent::__construct();
}
/**
* Return all the locations currently registered on the Panel.
*/
public function index(GetLocationsRequest $request): array
{
$locations = QueryBuilder::for(Location::query())
->allowedFilters(['short', 'long'])
->allowedSorts(['id'])
->paginate($request->query('per_page') ?? 50);
return $this->fractal->collection($locations)
->transformWith($this->getTransformer(LocationTransformer::class))
->toArray();
}
/**
* Return a single location.
*/
public function view(GetLocationRequest $request, Location $location): array
{
return $this->fractal->item($location)
->transformWith($this->getTransformer(LocationTransformer::class))
->toArray();
}
/**
* Store a new location on the Panel and return an HTTP/201 response code with the
* new location attached.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function store(StoreLocationRequest $request): JsonResponse
{
$location = $this->creationService->handle($request->validated());
return $this->fractal->item($location)
->transformWith($this->getTransformer(LocationTransformer::class))
->addMeta([
'resource' => route('api.application.locations.view', [
'location' => $location->id,
]),
])
->respond(201);
}
/**
* Update a location on the Panel and return the updated record to the user.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function update(UpdateLocationRequest $request, Location $location): array
{
$location = $this->updateService->handle($location, $request->validated());
return $this->fractal->item($location)
->transformWith($this->getTransformer(LocationTransformer::class))
->toArray();
}
/**
* Delete a location from the Panel.
*
* @throws \App\Exceptions\Service\Location\HasActiveNodesException
*/
public function delete(DeleteLocationRequest $request, Location $location): Response
{
$this->deletionService->handle($location);
return response('', 204);
}
}

View File

@ -27,7 +27,7 @@ class NodeDeploymentController extends ApplicationApiController
public function __invoke(GetDeployableNodesRequest $request): array
{
$data = $request->validated();
$nodes = $this->viableNodesService->setLocations($data['location_ids'] ?? [])
$nodes = $this->viableNodesService
->setMemory($data['memory'])
->setDisk($data['disk'])
->handle($request->query('per_page'), $request->query('page'));

View File

@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use App\Models\Location;
class LocationFormRequest extends AdminFormRequest
{
/**
* Set up the validation rules to use for these requests.
*/
public function rules(): array
{
if ($this->method() === 'PATCH') {
return Location::getRulesForUpdate($this->route()->parameter('location')->id);
}
return Location::getRules();
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Locations;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class DeleteLocationRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_LOCATIONS;
protected int $permission = AdminAcl::WRITE;
}

View File

@ -1,7 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Locations;
class GetLocationRequest extends GetLocationsRequest
{
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Locations;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class GetLocationsRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_LOCATIONS;
protected int $permission = AdminAcl::READ;
}

View File

@ -1,36 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Locations;
use App\Models\Location;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreLocationRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_LOCATIONS;
protected int $permission = AdminAcl::WRITE;
/**
* Rules to validate the request against.
*/
public function rules(): array
{
return collect(Location::getRules())->only([
'long',
'short',
])->toArray();
}
/**
* Rename fields to be more clear in error messages.
*/
public function attributes(): array
{
return [
'long' => 'Location Description',
'short' => 'Location Identifier',
];
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Locations;
use App\Models\Location;
class UpdateLocationRequest extends StoreLocationRequest
{
/**
* Rules to validate this request against.
*/
public function rules(): array
{
$locationId = $this->route()->parameter('location')->id;
return collect(Location::getRulesForUpdate($locationId))->only([
'short',
'long',
])->toArray();
}
}

View File

@ -10,8 +10,6 @@ class GetDeployableNodesRequest extends GetNodesRequest
'page' => 'integer',
'memory' => 'required|integer|min:0',
'disk' => 'required|integer|min:0',
'location_ids' => 'array',
'location_ids.*' => 'integer',
];
}
}

View File

@ -20,7 +20,6 @@ class StoreNodeRequest extends ApplicationApiRequest
return collect($rules ?? Node::getRules())->only([
'public',
'name',
'location_id',
'fqdn',
'scheme',
'behind_proxy',
@ -48,7 +47,6 @@ class StoreNodeRequest extends ApplicationApiRequest
return [
'daemon_base' => 'Daemon Base Path',
'upload_size' => 'File Upload Size Limit',
'location_id' => 'Location',
'public' => 'Node Visibility',
];
}

View File

@ -123,10 +123,6 @@ class StoreServerRequest extends ApplicationApiRequest
return !$input->deploy;
});
$validator->sometimes('deploy.locations', 'present', function ($input) {
return $input->deploy;
});
$validator->sometimes('deploy.port_range', 'present', function ($input) {
return $input->deploy;
});
@ -143,7 +139,6 @@ class StoreServerRequest extends ApplicationApiRequest
$object = new DeploymentObject();
$object->setDedicated($this->input('deploy.dedicated_ip', false));
$object->setLocations($this->input('deploy.locations', []));
$object->setPorts($this->input('deploy.port_range', []));
return $object;

View File

@ -25,7 +25,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $r_nodes
* @property int $r_allocations
* @property int $r_users
* @property int $r_locations
* @property int $r_eggs
* @property int $r_database_hosts
* @property int $r_server_databases
@ -46,7 +45,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRAllocations($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRDatabaseHosts($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereREggs($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRLocations($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRNodes($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRServerDatabases($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereRServers($value)
@ -105,8 +103,6 @@ class ApiKey extends Model
'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'int',
'r_' . AdminAcl::RESOURCE_SERVER_DATABASES => 'int',
'r_' . AdminAcl::RESOURCE_EGGS => 'int',
'r_' . AdminAcl::RESOURCE_LOCATIONS => 'int',
'r_' . AdminAcl::RESOURCE_EGGS => 'int',
'r_' . AdminAcl::RESOURCE_NODES => 'int',
'r_' . AdminAcl::RESOURCE_SERVERS => 'int',
];

View File

@ -1,66 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
/**
* @property int $id
* @property string $short
* @property string $long
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \App\Models\Node[] $nodes
* @property \App\Models\Server[] $servers
*/
class Location extends Model
{
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
public const RESOURCE_NAME = 'location';
/**
* The table associated with the model.
*/
protected $table = 'locations';
/**
* Fields that are not mass assignable.
*/
protected $guarded = ['id', 'created_at', 'updated_at'];
/**
* Rules ensuring that the raw data stored in the database meets expectations.
*/
public static array $validationRules = [
'short' => 'required|string|between:1,60|unique:locations,short',
'long' => 'string|nullable|between:1,191',
];
/**
* {@inheritDoc}
*/
public function getRouteKeyName(): string
{
return $this->getKeyName();
}
/**
* Gets the nodes in a specified location.
*/
public function nodes(): HasMany
{
return $this->hasMany(Node::class);
}
/**
* Gets the servers within a given location.
*/
public function servers(): HasManyThrough
{
return $this->hasManyThrough(Server::class, Node::class);
}
}

View File

@ -17,7 +17,6 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
* @property bool $public
* @property string $name
* @property string|null $description
* @property int $location_id
* @property string $fqdn
* @property string $scheme
* @property bool $behind_proxy
@ -34,7 +33,6 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
* @property string $daemonBase
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \App\Models\Location $location
* @property \App\Models\Mount[]|\Illuminate\Database\Eloquent\Collection $mounts
* @property \App\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
* @property \App\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
@ -66,7 +64,6 @@ class Node extends Model
* Cast values to correct type.
*/
protected $casts = [
'location_id' => 'integer',
'memory' => 'integer',
'disk' => 'integer',
'daemonListen' => 'integer',
@ -80,7 +77,7 @@ class Node extends Model
* Fields that are mass assignable.
*/
protected $fillable = [
'public', 'name', 'location_id',
'public', 'name',
'fqdn', 'scheme', 'behind_proxy',
'memory', 'memory_overallocate', 'disk',
'disk_overallocate', 'upload_size', 'daemonBase',
@ -91,7 +88,6 @@ class Node extends Model
public static array $validationRules = [
'name' => 'required|regex:/^([\w .-]{1,100})$/',
'description' => 'string|nullable',
'location_id' => 'required|exists:locations,id',
'public' => 'boolean',
'fqdn' => 'required|string',
'scheme' => 'required',
@ -196,14 +192,6 @@ class Node extends Model
return $this->hasManyThrough(Mount::class, MountNode::class, 'node_id', 'id', 'id', 'mount_id');
}
/**
* Gets the location associated with a node.
*/
public function location(): BelongsTo
{
return $this->belongsTo(Location::class);
}
/**
* Gets the servers associated with a node.
*/

View File

@ -6,8 +6,6 @@ class DeploymentObject
{
private bool $dedicated = false;
private array $locations = [];
private array $ports = [];
public function isDedicated(): bool
@ -22,18 +20,6 @@ class DeploymentObject
return $this;
}
public function getLocations(): array
{
return $this->locations;
}
public function setLocations(array $locations): self
{
$this->locations = $locations;
return $this;
}
public function getPorts(): array
{
return $this->ports;

View File

@ -289,16 +289,6 @@ class Server extends Model
return $this->hasMany(Database::class);
}
/**
* Returns the location that a server belongs to.
*
* @throws \Exception
*/
public function location(): \Znck\Eloquent\Relations\BelongsToThrough
{
return $this->belongsToThrough(Location::class, Node::class);
}
/**
* Returns the associated server transfer.
*/

View File

@ -12,7 +12,6 @@ use App\Repositories\Eloquent\ServerRepository;
use App\Repositories\Eloquent\SessionRepository;
use App\Repositories\Eloquent\SubuserRepository;
use App\Repositories\Eloquent\DatabaseRepository;
use App\Repositories\Eloquent\LocationRepository;
use App\Repositories\Eloquent\ScheduleRepository;
use App\Repositories\Eloquent\SettingsRepository;
use App\Repositories\Eloquent\AllocationRepository;
@ -28,7 +27,6 @@ use App\Repositories\Eloquent\ServerVariableRepository;
use App\Contracts\Repository\SessionRepositoryInterface;
use App\Contracts\Repository\SubuserRepositoryInterface;
use App\Contracts\Repository\DatabaseRepositoryInterface;
use App\Contracts\Repository\LocationRepositoryInterface;
use App\Contracts\Repository\ScheduleRepositoryInterface;
use App\Contracts\Repository\SettingsRepositoryInterface;
use App\Contracts\Repository\AllocationRepositoryInterface;
@ -50,7 +48,6 @@ class RepositoryServiceProvider extends ServiceProvider
$this->app->bind(DatabaseHostRepositoryInterface::class, DatabaseHostRepository::class);
$this->app->bind(EggRepositoryInterface::class, EggRepository::class);
$this->app->bind(EggVariableRepositoryInterface::class, EggVariableRepository::class);
$this->app->bind(LocationRepositoryInterface::class, LocationRepository::class);
$this->app->bind(NodeRepositoryInterface::class, NodeRepository::class);
$this->app->bind(ScheduleRepositoryInterface::class, ScheduleRepository::class);
$this->app->bind(ServerRepositoryInterface::class, ServerRepository::class);

View File

@ -1,64 +0,0 @@
<?php
namespace App\Repositories\Eloquent;
use App\Models\Location;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Exceptions\Repository\RecordNotFoundException;
use App\Contracts\Repository\LocationRepositoryInterface;
class LocationRepository extends EloquentRepository implements LocationRepositoryInterface
{
/**
* Return the model backing this repository.
*/
public function model(): string
{
return Location::class;
}
/**
* Return locations with a count of nodes and servers attached to it.
*/
public function getAllWithDetails(): Collection
{
return $this->getBuilder()->withCount('nodes', 'servers')->get($this->getColumns());
}
/**
* Return all the available locations with the nodes as a relationship.
*/
public function getAllWithNodes(): Collection
{
return $this->getBuilder()->with('nodes')->get($this->getColumns());
}
/**
* Return all the nodes and their respective count of servers for a location.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithNodes(int $id): Location
{
try {
return $this->getBuilder()->with('nodes.servers')->findOrFail($id, $this->getColumns());
} catch (ModelNotFoundException) {
throw new RecordNotFoundException();
}
}
/**
* Return a location and the count of nodes in that location.
*
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function getWithNodeCount(int $id): Location
{
try {
return $this->getBuilder()->withCount('nodes')->findOrFail($id, $this->getColumns());
} catch (ModelNotFoundException) {
throw new RecordNotFoundException();
}
}
}

View File

@ -73,14 +73,10 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
}
/**
* Return a single node with location and server information.
* Return a single node with server information.
*/
public function loadLocationAndServerCount(Node $node, bool $refresh = false): Node
public function loadServerCount(Node $node, bool $refresh = false): Node
{
if (!$node->relationLoaded('location') || $refresh) {
$node->load('location');
}
// This is quite ugly and can probably be improved down the road.
// And by probably, I mean it should.
if (is_null($node->servers_count) || $refresh) {

View File

@ -28,7 +28,6 @@ class AdminAcl
public const RESOURCE_NODES = 'nodes';
public const RESOURCE_ALLOCATIONS = 'allocations';
public const RESOURCE_USERS = 'users';
public const RESOURCE_LOCATIONS = 'locations';
public const RESOURCE_EGGS = 'eggs';
public const RESOURCE_DATABASE_HOSTS = 'database_hosts';
public const RESOURCE_SERVER_DATABASES = 'server_databases';

View File

@ -10,22 +10,9 @@ use App\Exceptions\Service\Deployment\NoViableNodeException;
class FindViableNodesService
{
protected array $locations = [];
protected ?int $disk = null;
protected ?int $memory = null;
/**
* Set the locations that should be searched through to locate available nodes.
*/
public function setLocations(array $locations): self
{
Assert::allIntegerish($locations, 'An array of location IDs should be provided when calling setLocations.');
$this->locations = $locations;
return $this;
}
/**
* Set the amount of disk that will be used by the server being created. Nodes will be
* filtered out if they do not have enough available free disk space for this server
@ -77,10 +64,6 @@ class FindViableNodesService
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.public', 1);
if (!empty($this->locations)) {
$query = $query->whereIn('nodes.location_id', $this->locations);
}
$results = $query->groupBy('nodes.id')
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [$this->memory])
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk]);

View File

@ -1,26 +0,0 @@
<?php
namespace App\Services\Locations;
use App\Models\Location;
use App\Contracts\Repository\LocationRepositoryInterface;
class LocationCreationService
{
/**
* LocationCreationService constructor.
*/
public function __construct(protected LocationRepositoryInterface $repository)
{
}
/**
* Create a new location.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function handle(array $data): Location
{
return $this->repository->create($data);
}
}

View File

@ -1,40 +0,0 @@
<?php
namespace App\Services\Locations;
use Webmozart\Assert\Assert;
use App\Models\Location;
use App\Contracts\Repository\NodeRepositoryInterface;
use App\Contracts\Repository\LocationRepositoryInterface;
use App\Exceptions\Service\Location\HasActiveNodesException;
class LocationDeletionService
{
/**
* LocationDeletionService constructor.
*/
public function __construct(
protected LocationRepositoryInterface $repository,
protected NodeRepositoryInterface $nodeRepository
) {
}
/**
* Delete an existing location.
*
* @throws \App\Exceptions\Service\Location\HasActiveNodesException
*/
public function handle(Location|int $location): ?int
{
$location = ($location instanceof Location) ? $location->id : $location;
Assert::integerish($location, 'First argument passed to handle must be numeric or an instance of ' . Location::class . ', received %s.');
$count = $this->nodeRepository->findCountWhere([['location_id', '=', $location]]);
if ($count > 0) {
throw new HasActiveNodesException(trans('exceptions.locations.has_nodes'));
}
return $this->repository->delete($location);
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Services\Locations;
use App\Models\Location;
use App\Contracts\Repository\LocationRepositoryInterface;
class LocationUpdateService
{
/**
* LocationUpdateService constructor.
*/
public function __construct(protected LocationRepositoryInterface $repository)
{
}
/**
* Update an existing location.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Repository\RecordNotFoundException
*/
public function handle(Location|int $location, array $data): Location
{
$location = ($location instanceof Location) ? $location->id : $location;
return $this->repository->update($location, $data);
}
}

View File

@ -66,7 +66,6 @@ class EnvironmentService
{
return [
'STARTUP' => 'startup',
'P_SERVER_LOCATION' => 'location.short',
'P_SERVER_UUID' => 'uuid',
];
}

View File

@ -109,7 +109,7 @@ class ServerCreationService
private function configureDeployment(array $data, DeploymentObject $deployment): Allocation
{
/** @var \Illuminate\Support\Collection $nodes */
$nodes = $this->findViableNodesService->setLocations($deployment->getLocations())
$nodes = $this->findViableNodesService
->setDisk(Arr::get($data, 'disk'))
->setMemory(Arr::get($data, 'memory'))
->handle();

View File

@ -1,70 +0,0 @@
<?php
namespace App\Transformers\Api\Application;
use App\Models\Location;
use League\Fractal\Resource\Collection;
use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
class LocationTransformer extends BaseTransformer
{
/**
* List of resources that can be included.
*/
protected array $availableIncludes = ['nodes', 'servers'];
/**
* Return the resource name for the JSONAPI output.
*/
public function getResourceName(): string
{
return Location::RESOURCE_NAME;
}
/**
* Return a generic transformed location array.
*/
public function transform(Location $location): array
{
return [
'id' => $location->id,
'short' => $location->short,
'long' => $location->long,
$location->getUpdatedAtColumn() => $this->formatTimestamp($location->updated_at),
$location->getCreatedAtColumn() => $this->formatTimestamp($location->created_at),
];
}
/**
* Return the nodes associated with this location.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeServers(Location $location): Collection|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) {
return $this->null();
}
$location->loadMissing('servers');
return $this->collection($location->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server');
}
/**
* Return the nodes associated with this location.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeNodes(Location $location): Collection|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) {
return $this->null();
}
$location->loadMissing('nodes');
return $this->collection($location->getRelation('nodes'), $this->makeTransformer(NodeTransformer::class), 'node');
}
}

View File

@ -70,26 +70,6 @@ class NodeTransformer extends BaseTransformer
);
}
/**
* Return the nodes associated with this location.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeLocation(Node $node): Item|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_LOCATIONS)) {
return $this->null();
}
$node->loadMissing('location');
return $this->item(
$node->getRelation('location'),
$this->makeTransformer(LocationTransformer::class),
'location'
);
}
/**
* Return the nodes associated with this location.
*

View File

@ -169,22 +169,6 @@ class ServerTransformer extends BaseTransformer
return $this->collection($server->getRelation('variables'), $this->makeTransformer(ServerVariableTransformer::class), 'server_variable');
}
/**
* Return a generic array with location information for this server.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeLocation(Server $server): Item|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_LOCATIONS)) {
return $this->null();
}
$server->loadMissing('location');
return $this->item($server->getRelation('location'), $this->makeTransformer(LocationTransformer::class), 'location');
}
/**
* Return a generic array with node information for this server.
*

View File

@ -1,28 +0,0 @@
<?php
namespace Database\Factories;
use Illuminate\Support\Str;
use App\Models\Location;
use Illuminate\Database\Eloquent\Factories\Factory;
class LocationFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Location::class;
/**
* Define the model's default state.
*/
public function definition(): array
{
return [
'short' => Str::random(8),
'long' => Str::random(32),
];
}
}

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->dropForeign('nodes_location_id_foreign');
});
Schema::drop('locations');
Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn('r_locations');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::create('locations', function (Blueprint $table) {
$table->increments('id');
$table->string('short');
$table->text('long')->nullable();
$table->timestamps();
});
Schema::table('api_keys', function (Blueprint $table) {
$table->unsignedTinyInteger('r_locations')->default(0);
});
}
};

View File

@ -8,7 +8,6 @@ return [
'notices' => [
'allocations_added' => 'Allocations have successfully been added to this node.',
'node_deleted' => 'Node has been successfully removed from the panel.',
'location_required' => 'You must have at least one location configured before you can add a node to this panel.',
'node_created' => 'Successfully created new node. You can automatically configure the daemon on this machine by visiting the \'Configuration\' tab. <strong>Before you can add any servers you must first allocate at least one IP address and port.</strong>',
'node_updated' => 'Node information has been updated. If any daemon settings were changed you will need to reboot it for those changes to take effect.',
'unallocated_deleted' => 'Deleted all un-allocated ports for <code>:ip</code>.',

View File

@ -1,13 +1,6 @@
<?php
return [
'location' => [
'no_location_found' => 'Could not locate a record matching the provided short code.',
'ask_short' => 'Location Short Code',
'ask_long' => 'Location Description',
'created' => 'Successfully created a new location (:name) with an ID of :id.',
'deleted' => 'Successfully deleted the requested location.',
],
'user' => [
'search_users' => 'Enter a Username, User ID, or Email Address',
'select_search_user' => 'ID of user to delete (Enter \'0\' to re-search)',

View File

@ -99,12 +99,8 @@
<label for="pNodeId" class="form-label">Linked Node</label>
<select name="node_id" id="pNodeId" class="form-control">
<option value="">None</option>
@foreach($locations as $location)
<optgroup label="{{ $location->short }}">
@foreach($location->nodes as $node)
<option value="{{ $node->id }}">{{ $node->name }}</option>
@endforeach
</optgroup>
@foreach($nodes as $node)
<option value="{{ $node->id }}">{{ $node->name }}</option>
@endforeach
</select>
<p class="text-muted small">This setting does nothing other than default to this database host when adding a database to a server on the selected node.</p>

View File

@ -1,81 +0,0 @@
@extends('layouts.admin')
@section('title')
Locations
@endsection
@section('content-header')
<h1>Locations<small>All locations that nodes can be assigned to for easier categorization.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Locations</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Location List</h3>
<div class="box-tools">
<button class="btn btn-sm btn-primary" data-toggle="modal" data-target="#newLocationModal">Create New</button>
</div>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tbody>
<tr>
<th>ID</th>
<th>Short Code</th>
<th>Description</th>
<th class="text-center">Nodes</th>
<th class="text-center">Servers</th>
</tr>
@foreach ($locations as $location)
<tr>
<td><code>{{ $location->id }}</code></td>
<td><a href="{{ route('admin.locations.view', $location->id) }}">{{ $location->short }}</a></td>
<td>{{ $location->long }}</td>
<td class="text-center">{{ $location->nodes_count }}</td>
<td class="text-center">{{ $location->servers_count }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="modal fade" id="newLocationModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="{{ route('admin.locations') }}" method="POST">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Create Location</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-12">
<label for="pShortModal" class="form-label">Short Code</label>
<input type="text" name="short" id="pShortModal" class="form-control" />
<p class="text-muted small">A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, <code>us.nyc.lvl3</code>.</p>
</div>
<div class="col-md-12">
<label for="pLongModal" class="form-label">Description</label>
<textarea name="long" id="pLongModal" class="form-control" rows="4"></textarea>
<p class="text-muted small">A longer description of this location. Must be less than 191 characters.</p>
</div>
</div>
</div>
<div class="modal-footer">
{!! csrf_field() !!}
<button type="button" class="btn btn-default btn-sm pull-left" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success btn-sm">Create</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@ -1,69 +0,0 @@
@extends('layouts.admin')
@section('title')
Locations &rarr; View &rarr; {{ $location->short }}
@endsection
@section('content-header')
<h1>{{ $location->short }}<small>{{ str_limit($location->long, 75) }}</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.locations') }}">Locations</a></li>
<li class="active">{{ $location->short }}</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-sm-6">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Location Details</h3>
</div>
<form action="{{ route('admin.locations.view', $location->id) }}" method="POST">
<div class="box-body">
<div class="form-group">
<label for="pShort" class="form-label">Short Code</label>
<input type="text" id="pShort" name="short" class="form-control" value="{{ $location->short }}" />
</div>
<div class="form-group">
<label for="pLong" class="form-label">Description</label>
<textarea id="pLong" name="long" class="form-control" rows="4">{{ $location->long }}</textarea>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
{!! method_field('PATCH') !!}
<button name="action" value="edit" class="btn btn-sm btn-primary pull-right">Save</button>
<button name="action" value="delete" class="btn btn-sm btn-danger pull-left muted muted-hover"><i class="fa fa-trash-o"></i></button>
</div>
</form>
</div>
</div>
<div class="col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Nodes</h3>
</div>
<div class="box-body table-responsive no-padding">
<table class="table table-hover">
<tr>
<th>ID</th>
<th>Name</th>
<th>FQDN</th>
<th>Servers</th>
</tr>
@foreach($location->nodes as $node)
<tr>
<td><code>{{ $node->id }}</code></td>
<td><a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></td>
<td><code>{{ $node->fqdn }}</code></td>
<td>{{ $node->servers->count() }}</td>
</tr>
@endforeach
</table>
</div>
</div>
</div>
</div>
@endsection

View File

@ -41,7 +41,6 @@
<tr>
<th></th>
<th>Name</th>
<th>Location</th>
<th>Memory</th>
<th>Disk</th>
<th class="text-center">Servers</th>
@ -52,7 +51,6 @@
<tr>
<td class="text-center text-muted left-icon" data-action="ping" data-secret="{{ $node->getDecryptedKey() }}" data-location="{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}/api/system"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
<td>{!! $node->maintenance_mode ? '<span class="label label-warning"><i class="fa fa-wrench"></i></span> ' : '' !!}<a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></td>
<td>{{ $node->location->short }}</td>
<td>{{ $node->memory }} MiB</td>
<td>{{ $node->disk }} MiB</td>
<td class="text-center">{{ $node->servers_count }}</td>

View File

@ -31,14 +31,6 @@
<label for="pDescription" class="form-label">Description</label>
<textarea name="description" id="pDescription" rows="4" class="form-control">{{ old('description') }}</textarea>
</div>
<div class="form-group">
<label for="pLocationId" class="form-label">Location</label>
<select name="location_id" id="pLocationId">
@foreach($locations as $location)
<option value="{{ $location->id }}" {{ $location->id != old('location_id') ?: 'selected' }}>{{ $location->short }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<label class="form-label">Node Visibility</label>
<div>
@ -166,10 +158,3 @@
</div>
</form>
@endsection
@section('footer-scripts')
@parent
<script>
$('#pLocationId').select2();
</script>
@endsection

View File

@ -49,16 +49,6 @@
<textarea name="description" id="description" rows="4" class="form-control">{{ $node->description }}</textarea>
</div>
</div>
<div class="form-group col-xs-12">
<label for="name" class="control-label">Location</label>
<div>
<select name="location_id" class="form-control">
@foreach($locations as $location)
<option value="{{ $location->id }}" {{ (old('location_id', $node->location_id) === $location->id) ? 'selected' : '' }}>{{ $location->long }} ({{ $location->short }})</option>
@endforeach
</select>
</div>
</div>
<div class="form-group col-xs-12">
<label for="public" class="control-label">Allow Automatic Allocation <sup><a data-toggle="tooltip" data-placement="top" title="Allow automatic allocation to this Node?">?</a></sup></label>
<div>

View File

@ -68,16 +68,8 @@
<div class="form-group col-sm-4">
<label for="pNodeId">Node</label>
<select name="node_id" id="pNodeId" class="form-control">
@foreach($locations as $location)
<optgroup label="{{ $location->long }} ({{ $location->short }})">
@foreach($location->nodes as $node)
<option value="{{ $node->id }}"
@if($location->id === old('location_id')) selected @endif
>{{ $node->name }}</option>
@endforeach
</optgroup>
@foreach($nodes as $node)
<option value="{{ $node->id }}">{{ $node->name }}</option>
@endforeach
</select>

View File

@ -106,7 +106,7 @@
</div>
<div class="box-footer">
@if($canTransfer)
@if(!$canTransfer)
<button class="btn btn-success" data-toggle="modal" data-target="#transferServerModal">Transfer Server</button>
@else
<button class="btn btn-success disabled">Transfer Server</button>
@ -150,18 +150,10 @@
<div class="form-group col-md-12">
<label for="pNodeId">Node</label>
<select name="node_id" id="pNodeId" class="form-control">
@foreach($locations as $location)
<optgroup label="{{ $location->long }} ({{ $location->short }})">
@foreach($location->nodes as $node)
@if($node->id != $server->node_id)
<option value="{{ $node->id }}"
@if($location->id === old('location_id')) selected @endif
>{{ $node->name }}</option>
@endif
@endforeach
</optgroup>
@foreach($nodes as $node)
@if($node->id != $server->node_id)
<option value="{{ $node->id }}">{{ $node->name }}</option>
@endif
@endforeach
</select>
<p class="small text-muted no-margin">The node which this server will be transferred to.</p>

View File

@ -80,11 +80,6 @@
<i class="fa fa-database"></i> <span>Databases</span>
</a>
</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.locations') ?: 'active' }}">
<a href="{{ route('admin.locations') }}">
<i class="fa fa-globe"></i> <span>Locations</span>
</a>
</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.nodes') ?: 'active' }}">
<a href="{{ route('admin.nodes') }}">
<i class="fa fa-sitemap"></i> <span>Nodes</span>

View File

@ -8,7 +8,7 @@ Route::get('/', [Admin\BaseController::class, 'index'])->name('admin.index');
/*
|--------------------------------------------------------------------------
| Location Controller Routes
| API Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /admin/api
@ -23,22 +23,6 @@ Route::group(['prefix' => 'api'], function () {
Route::delete('/revoke/{identifier}', [Admin\ApiController::class, 'delete'])->name('admin.api.delete');
});
/*
|--------------------------------------------------------------------------
| Location Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /admin/locations
|
*/
Route::group(['prefix' => 'locations'], function () {
Route::get('/', [Admin\LocationController::class, 'index'])->name('admin.locations');
Route::get('/view/{location:id}', [Admin\LocationController::class, 'view'])->name('admin.locations.view');
Route::post('/', [Admin\LocationController::class, 'create']);
Route::patch('/view/{location:id}', [Admin\LocationController::class, 'update']);
});
/*
|--------------------------------------------------------------------------
| Database Controller Routes

View File

@ -48,24 +48,6 @@ Route::group(['prefix' => '/nodes'], function () {
});
});
/*
|--------------------------------------------------------------------------
| Location Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /api/application/locations
|
*/
Route::group(['prefix' => '/locations'], function () {
Route::get('/', [Application\Locations\LocationController::class, 'index'])->name('api.applications.locations');
Route::get('/{location:id}', [Application\Locations\LocationController::class, 'view'])->name('api.application.locations.view');
Route::post('/', [Application\Locations\LocationController::class, 'store']);
Route::patch('/{location:id}', [Application\Locations\LocationController::class, 'update']);
Route::delete('/{location:id}', [Application\Locations\LocationController::class, 'delete']);
});
/*
|--------------------------------------------------------------------------
| Server Controller Routes

View File

@ -84,7 +84,6 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase
'r_nodes' => AdminAcl::READ | AdminAcl::WRITE,
'r_allocations' => AdminAcl::READ | AdminAcl::WRITE,
'r_users' => AdminAcl::READ | AdminAcl::WRITE,
'r_locations' => AdminAcl::READ | AdminAcl::WRITE,
'r_eggs' => AdminAcl::READ | AdminAcl::WRITE,
'r_database_hosts' => AdminAcl::READ | AdminAcl::WRITE,
'r_server_databases' => AdminAcl::READ | AdminAcl::WRITE,

View File

@ -1,268 +0,0 @@
<?php
namespace App\Tests\Integration\Api\Application\Location;
use App\Models\Node;
use Illuminate\Http\Response;
use App\Models\Location;
use App\Transformers\Api\Application\NodeTransformer;
use App\Transformers\Api\Application\ServerTransformer;
use App\Transformers\Api\Application\LocationTransformer;
use App\Tests\Integration\Api\Application\ApplicationApiIntegrationTestCase;
class LocationControllerTest extends ApplicationApiIntegrationTestCase
{
/**
* Test getting all locations through the API.
*/
public function testGetLocations()
{
$locations = Location::factory()->times(2)->create();
$response = $this->getJson('/api/application/locations?per_page=60');
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonCount(2, 'data');
$response->assertJsonStructure([
'object',
'data' => [
['object', 'attributes' => ['id', 'short', 'long', 'created_at', 'updated_at']],
['object', 'attributes' => ['id', 'short', 'long', 'created_at', 'updated_at']],
],
'meta' => ['pagination' => ['total', 'count', 'per_page', 'current_page', 'total_pages']],
]);
$response
->assertJson([
'object' => 'list',
'data' => [[], []],
'meta' => [
'pagination' => [
'total' => 2,
'count' => 2,
'per_page' => 60,
'current_page' => 1,
'total_pages' => 1,
],
],
])
->assertJsonFragment([
'object' => 'location',
'attributes' => [
'id' => $locations[0]->id,
'short' => $locations[0]->short,
'long' => $locations[0]->long,
'created_at' => $this->formatTimestamp($locations[0]->created_at),
'updated_at' => $this->formatTimestamp($locations[0]->updated_at),
],
])->assertJsonFragment([
'object' => 'location',
'attributes' => [
'id' => $locations[1]->id,
'short' => $locations[1]->short,
'long' => $locations[1]->long,
'created_at' => $this->formatTimestamp($locations[1]->created_at),
'updated_at' => $this->formatTimestamp($locations[1]->updated_at),
],
]);
}
/**
* Test getting a single location on the API.
*/
public function testGetSingleLocation()
{
$location = Location::factory()->create();
$response = $this->getJson('/api/application/locations/' . $location->id);
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonCount(2);
$response->assertJsonStructure(['object', 'attributes' => ['id', 'short', 'long', 'created_at', 'updated_at']]);
$response->assertJson([
'object' => 'location',
'attributes' => [
'id' => $location->id,
'short' => $location->short,
'long' => $location->long,
'created_at' => $this->formatTimestamp($location->created_at),
'updated_at' => $this->formatTimestamp($location->updated_at),
],
], true);
}
/**
* Test that a location can be created.
*/
public function testCreateLocation()
{
$response = $this->postJson('/api/application/locations', [
'short' => 'inhouse',
'long' => 'This is my inhouse location',
]);
$response->assertStatus(Response::HTTP_CREATED);
$response->assertJsonCount(3);
$response->assertJsonStructure([
'object',
'attributes' => ['id', 'short', 'long', 'created_at', 'updated_at'],
'meta' => ['resource'],
]);
$this->assertDatabaseHas('locations', ['short' => 'inhouse', 'long' => 'This is my inhouse location']);
$location = Location::where('short', 'inhouse')->first();
$response->assertJson([
'object' => 'location',
'attributes' => $this->getTransformer(LocationTransformer::class)->transform($location),
'meta' => [
'resource' => route('api.application.locations.view', $location->id),
],
], true);
}
/**
* Test that a location can be updated.
*/
public function testUpdateLocation()
{
$location = Location::factory()->create();
$response = $this->patchJson('/api/application/locations/' . $location->id, [
'short' => 'new inhouse',
'long' => 'This is my new inhouse location',
]);
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonCount(2);
$response->assertJsonStructure([
'object',
'attributes' => ['id', 'short', 'long', 'created_at', 'updated_at'],
]);
$this->assertDatabaseHas('locations', ['short' => 'new inhouse', 'long' => 'This is my new inhouse location']);
$location = $location->fresh();
$response->assertJson([
'object' => 'location',
'attributes' => $this->getTransformer(LocationTransformer::class)->transform($location),
]);
}
/**
* Test that a location can be deleted from the database.
*/
public function testDeleteLocation()
{
$location = Location::factory()->create();
$this->assertDatabaseHas('locations', ['id' => $location->id]);
$response = $this->delete('/api/application/locations/' . $location->id);
$response->assertStatus(Response::HTTP_NO_CONTENT);
$this->assertDatabaseMissing('locations', ['id' => $location->id]);
}
/**
* Test that all the defined relationships for a location can be loaded successfully.
*/
public function testRelationshipsCanBeLoaded()
{
$location = Location::factory()->create();
$server = $this->createServerModel(['user_id' => $this->getApiUser()->id, 'location_id' => $location->id]);
$response = $this->getJson('/api/application/locations/' . $location->id . '?include=servers,nodes');
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonCount(2)->assertJsonCount(2, 'attributes.relationships');
$response->assertJsonStructure([
'attributes' => [
'relationships' => [
'nodes' => ['object', 'data' => [['attributes' => ['id']]]],
'servers' => ['object', 'data' => [['attributes' => ['id']]]],
],
],
]);
// Just assert that we see the expected relationship IDs in the response.
$response->assertJson([
'attributes' => [
'relationships' => [
'nodes' => [
'object' => 'list',
'data' => [
[
'object' => 'node',
'attributes' => $this->getTransformer(NodeTransformer::class)->transform($server->getRelation('node')),
],
],
],
'servers' => [
'object' => 'list',
'data' => [
[
'object' => 'server',
'attributes' => $this->getTransformer(ServerTransformer::class)->transform($server),
],
],
],
],
],
]);
}
/**
* Test that a relationship that an API key does not have permission to access
* cannot be loaded onto the model.
*/
public function testKeyWithoutPermissionCannotLoadRelationship()
{
$this->createNewDefaultApiKey($this->getApiUser(), ['r_nodes' => 0]);
$location = Location::factory()->create();
Node::factory()->create(['location_id' => $location->id]);
$response = $this->getJson('/api/application/locations/' . $location->id . '?include=nodes');
$response->assertStatus(Response::HTTP_OK);
$response->assertJsonCount(2)->assertJsonCount(1, 'attributes.relationships');
$response->assertJsonStructure([
'attributes' => [
'relationships' => [
'nodes' => ['object', 'attributes'],
],
],
]);
// Just assert that we see the expected relationship IDs in the response.
$response->assertJson([
'attributes' => [
'relationships' => [
'nodes' => [
'object' => 'null_resource',
'attributes' => null,
],
],
],
]);
}
/**
* Test that a missing location returns a 404 error.
*
* GET /api/application/locations/:id
*/
public function testGetMissingLocation()
{
$response = $this->getJson('/api/application/locations/nil');
$this->assertNotFoundJson($response);
}
/**
* Test that an authentication error occurs if a key does not have permission
* to access a resource.
*/
public function testErrorReturnedIfNoPermission()
{
$location = Location::factory()->create();
$this->createNewDefaultApiKey($this->getApiUser(), ['r_locations' => 0]);
$response = $this->getJson('/api/application/locations/' . $location->id);
$this->assertAccessDeniedJson($response);
}
}

View File

@ -9,7 +9,6 @@ use App\Models\Model;
use App\Models\Backup;
use App\Models\Server;
use App\Models\Database;
use App\Models\Location;
use App\Models\Schedule;
use Illuminate\Support\Collection;
use App\Models\Allocation;
@ -31,7 +30,6 @@ abstract class ClientApiIntegrationTestCase extends IntegrationTestCase
Backup::query()->forceDelete();
Server::query()->forceDelete();
Node::query()->forceDelete();
Location::query()->forceDelete();
User::query()->forceDelete();
parent::tearDown();

View File

@ -61,7 +61,7 @@ class DeployServerDatabaseServiceTest extends IntegrationTestCase
{
$server = $this->createServerModel();
$node = Node::factory()->create(['location_id' => $server->location->id]);
$node = Node::factory()->create();
DatabaseHost::factory()->create(['node_id' => $node->id]);
config()->set('panel.client_features.databases.allow_random', false);
@ -96,7 +96,7 @@ class DeployServerDatabaseServiceTest extends IntegrationTestCase
{
$server = $this->createServerModel();
$node = Node::factory()->create(['location_id' => $server->location->id]);
$node = Node::factory()->create();
DatabaseHost::factory()->create(['node_id' => $node->id]);
$host = DatabaseHost::factory()->create(['node_id' => $server->node_id]);
@ -123,7 +123,7 @@ class DeployServerDatabaseServiceTest extends IntegrationTestCase
{
$server = $this->createServerModel();
$node = Node::factory()->create(['location_id' => $server->location->id]);
$node = Node::factory()->create();
$host = DatabaseHost::factory()->create(['node_id' => $node->id]);
$this->managementService->expects('create')->with($server, [

View File

@ -5,11 +5,8 @@ namespace App\Tests\Integration\Services\Deployment;
use App\Models\Node;
use App\Models\Server;
use App\Models\Database;
use App\Models\Location;
use Illuminate\Support\Collection;
use App\Tests\Integration\IntegrationTestCase;
use App\Services\Deployment\FindViableNodesService;
use App\Exceptions\Service\Deployment\NoViableNodeException;
class FindViableNodesServiceTest extends IntegrationTestCase
{
@ -38,146 +35,6 @@ class FindViableNodesServiceTest extends IntegrationTestCase
$this->getService()->setDisk(10)->handle();
}
/**
* Ensure that errors are not thrown back when passing in expected values.
*/
public function testNoExceptionIsThrownIfStringifiedIntegersArePassedForLocations()
{
$this->getService()->setLocations([1, 2, 3]);
$this->getService()->setLocations(['1', '2', '3']);
$this->getService()->setLocations(['1', 2, 3]);
try {
$this->getService()->setLocations(['a']);
$this->fail('This expectation should not be called.');
} catch (\Exception $exception) {
$this->assertInstanceOf(\InvalidArgumentException::class, $exception);
$this->assertSame('An array of location IDs should be provided when calling setLocations.', $exception->getMessage());
}
try {
$this->getService()->setLocations(['1.2', '1', 2]);
$this->fail('This expectation should not be called.');
} catch (\Exception $exception) {
$this->assertInstanceOf(\InvalidArgumentException::class, $exception);
$this->assertSame('An array of location IDs should be provided when calling setLocations.', $exception->getMessage());
}
}
public function testExpectedNodeIsReturnedForLocation()
{
/** @var \App\Models\Location[] $locations */
$locations = Location::factory()->times(2)->create();
/** @var \App\Models\Node[] $nodes */
$nodes = [
// This node should never be returned once we've completed the initial test which
// runs without a location filter.
Node::factory()->create([
'location_id' => $locations[0]->id,
'memory' => 2048,
'disk' => 1024 * 100,
]),
Node::factory()->create([
'location_id' => $locations[1]->id,
'memory' => 1024,
'disk' => 10240,
'disk_overallocate' => 10,
]),
Node::factory()->create([
'location_id' => $locations[1]->id,
'memory' => 1024 * 4,
'memory_overallocate' => 50,
'disk' => 102400,
]),
];
// Expect that all the nodes are returned as we're under all of their limits
// and there is no location filter being provided.
$response = $this->getService()->setDisk(512)->setMemory(512)->handle();
$this->assertInstanceOf(Collection::class, $response);
$this->assertCount(3, $response);
$this->assertInstanceOf(Node::class, $response[0]);
// Expect that only the last node is returned because it is the only one with enough
// memory available to this instance.
$response = $this->getService()->setDisk(512)->setMemory(2049)->handle();
$this->assertInstanceOf(Collection::class, $response);
$this->assertCount(1, $response);
$this->assertSame($nodes[2]->id, $response[0]->id);
// Helper, I am lazy.
$base = function () use ($locations) {
return $this->getService()->setLocations([$locations[1]->id])->setDisk(512);
};
// Expect that we can create this server on either node since the disk and memory
// limits are below the allowed amount.
$response = $base()->setMemory(512)->handle();
$this->assertCount(2, $response);
$this->assertSame(2, $response->where('location_id', $locations[1]->id)->count());
// Expect that we can only create this server on the second node since the memory
// allocated is over the amount of memory available to the first node.
$response = $base()->setMemory(2048)->handle();
$this->assertCount(1, $response);
$this->assertSame($nodes[2]->id, $response[0]->id);
// Expect that we can only create this server on the second node since the disk
// allocated is over the limit assigned to the first node (even with the overallocate).
$response = $base()->setDisk(20480)->setMemory(256)->handle();
$this->assertCount(1, $response);
$this->assertSame($nodes[2]->id, $response[0]->id);
// Expect that we could create the server on either node since the disk allocated is
// right at the limit for Node 1 when the overallocate value is included in the calc.
$response = $base()->setDisk(11264)->setMemory(256)->handle();
$this->assertCount(2, $response);
// Create two servers on the first node so that the disk space used is equal to the
// base amount available to the node (without overallocation included).
$servers = Collection::make([
$this->createServerModel(['node_id' => $nodes[1]->id, 'disk' => 5120]),
$this->createServerModel(['node_id' => $nodes[1]->id, 'disk' => 5120]),
]);
// Expect that we cannot create a server with a 1GB disk on the first node since there
// is not enough space (even with the overallocate) available to the node.
$response = $base()->setDisk(1024)->setMemory(256)->handle();
$this->assertCount(1, $response);
$this->assertSame($nodes[2]->id, $response[0]->id);
// Cleanup servers since we need to test some other stuff with memory here.
$servers->each->delete();
// Expect that no viable node can be found when the memory limit for the given instance
// is greater than either node can support, even with the overallocation limits taken
// into account.
$this->expectException(NoViableNodeException::class);
$base()->setMemory(10000)->handle();
// Create four servers so that the memory used for the second node is equal to the total
// limit for that node (pre-overallocate calculation).
Collection::make([
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
$this->createServerModel(['node_id' => $nodes[2]->id, 'memory' => 1024]),
]);
// Expect that either node can support this server when we account for the overallocate
// value of the second node.
$response = $base()->setMemory(500)->handle();
$this->assertCount(2, $response);
$this->assertSame(2, $response->where('location_id', $locations[1]->id)->count());
// Expect that only the first node can support this server when we go over the remaining
// memory for the second nodes overallocate calculation.
$response = $base()->setMemory(640)->handle();
$this->assertCount(1, $response);
$this->assertSame($nodes[1]->id, $response[0]->id);
}
private function getService(): FindViableNodesService
{
return $this->app->make(FindViableNodesService::class);

View File

@ -9,7 +9,6 @@ use App\Models\Node;
use App\Models\User;
use GuzzleHttp\Psr7\Response;
use App\Models\Server;
use App\Models\Location;
use App\Models\Allocation;
use Illuminate\Foundation\Testing\WithFaker;
use GuzzleHttp\Exception\BadResponseException;
@ -57,20 +56,15 @@ class ServerCreationServiceTest extends IntegrationTestCase
/** @var \App\Models\User $user */
$user = User::factory()->create();
/** @var \App\Models\Location $location */
$location = Location::factory()->create();
/** @var \App\Models\Node $node */
$node = Node::factory()->create([
'location_id' => $location->id,
]);
$node = Node::factory()->create();
/** @var \App\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations */
$allocations = Allocation::factory()->times(5)->create([
'node_id' => $node->id,
]);
$deployment = (new DeploymentObject())->setDedicated(true)->setLocations([$node->location_id])->setPorts([
$deployment = (new DeploymentObject())->setDedicated(true)->setPorts([
$allocations[0]->port,
]);
@ -159,13 +153,8 @@ class ServerCreationServiceTest extends IntegrationTestCase
/** @var \App\Models\User $user */
$user = User::factory()->create();
/** @var \App\Models\Location $location */
$location = Location::factory()->create();
/** @var \App\Models\Node $node */
$node = Node::factory()->create([
'location_id' => $location->id,
]);
$node = Node::factory()->create();
/** @var \App\Models\Allocation $allocation */
$allocation = Allocation::factory()->create([

View File

@ -8,7 +8,6 @@ use App\Models\Node;
use App\Models\User;
use App\Models\Server;
use App\Models\Subuser;
use App\Models\Location;
use App\Models\Allocation;
trait CreatesTestModels
@ -33,14 +32,8 @@ trait CreatesTestModels
}
if (!isset($attributes['node_id'])) {
if (!isset($attributes['location_id'])) {
/** @var \App\Models\Location $location */
$location = Location::factory()->create();
$attributes['location_id'] = $location->id;
}
/** @var \App\Models\Node $node */
$node = Node::factory()->create(['location_id' => $attributes['location_id']]);
$node = Node::factory()->create();
$attributes['node_id'] = $node->id;
}
@ -56,7 +49,7 @@ trait CreatesTestModels
$attributes['egg_id'] = $egg->id;
}
unset($attributes['user_id'], $attributes['location_id']);
unset($attributes['user_id']);
/** @var \App\Models\Server $server */
$server = Server::factory()->create($attributes);
@ -64,7 +57,7 @@ trait CreatesTestModels
Allocation::query()->where('id', $server->allocation_id)->update(['server_id' => $server->id]);
return $server->fresh([
'location', 'user', 'node', 'allocation', 'egg',
'user', 'node', 'allocation', 'egg',
]);
}