mirror of
https://github.com/Icinga/icingaweb2-module-director.git
synced 2025-08-15 06:48:11 +02:00
289 lines
9.6 KiB
PHP
289 lines
9.6 KiB
PHP
<?php
|
|
|
|
namespace Icinga\Module\Director\RestApi;
|
|
|
|
use Exception;
|
|
use Icinga\Exception\IcingaException;
|
|
use Icinga\Exception\NotFoundError;
|
|
use Icinga\Exception\ProgrammingError;
|
|
use Icinga\Module\Director\Core\CoreApi;
|
|
use Icinga\Module\Director\Data\Exporter;
|
|
use Icinga\Module\Director\DirectorObject\Lookup\ServiceFinder;
|
|
use Icinga\Module\Director\Exception\DuplicateKeyException;
|
|
use Icinga\Module\Director\Objects\IcingaHost;
|
|
use Icinga\Module\Director\Objects\IcingaObject;
|
|
use Icinga\Module\Director\Resolver\OverrideHelper;
|
|
use InvalidArgumentException;
|
|
use PDO;
|
|
use RuntimeException;
|
|
|
|
class IcingaObjectHandler extends RequestHandler
|
|
{
|
|
/** @var IcingaObject */
|
|
protected $object;
|
|
|
|
/** @var CoreApi */
|
|
protected $api;
|
|
|
|
public function setObject(IcingaObject $object)
|
|
{
|
|
$this->object = $object;
|
|
return $this;
|
|
}
|
|
|
|
public function setApi(CoreApi $api)
|
|
{
|
|
$this->api = $api;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return IcingaObject
|
|
* @throws ProgrammingError
|
|
*/
|
|
protected function requireObject()
|
|
{
|
|
if ($this->object === null) {
|
|
throw new ProgrammingError('Object is required');
|
|
}
|
|
|
|
return $this->object;
|
|
}
|
|
|
|
/**
|
|
* @return IcingaObject
|
|
*/
|
|
protected function loadOptionalObject()
|
|
{
|
|
return $this->object;
|
|
}
|
|
|
|
protected function requireJsonBody()
|
|
{
|
|
$data = json_decode($this->request->getRawBody());
|
|
|
|
if ($data === null) {
|
|
$this->response->setHttpResponseCode(400);
|
|
throw new IcingaException(
|
|
'Invalid JSON: %s',
|
|
$this->getLastJsonError()
|
|
);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function getType()
|
|
{
|
|
return $this->request->getControllerName();
|
|
}
|
|
|
|
protected function processApiRequest()
|
|
{
|
|
try {
|
|
$this->handleApiRequest();
|
|
} catch (NotFoundError $e) {
|
|
$this->sendJsonError($e, 404);
|
|
return;
|
|
} catch (DuplicateKeyException $e) {
|
|
$this->sendJsonError($e, 422);
|
|
return;
|
|
} catch (Exception $e) {
|
|
$this->sendJsonError($e);
|
|
}
|
|
|
|
if ($this->request->getActionName() !== 'index' && $this->request->getActionName() !== 'variables') {
|
|
throw new NotFoundError('Not found');
|
|
}
|
|
}
|
|
|
|
public function getCustomProperties(IcingaObject $object): array
|
|
{
|
|
if ($object->get('uuid') === null) {
|
|
return [];
|
|
}
|
|
|
|
$type = $object->getShortTableName();
|
|
$db = $object->getConnection();
|
|
$uuids = $object->listAncestorUuIds();
|
|
$query = $db->getDbAdapter()
|
|
->select()
|
|
->from(
|
|
['dp' => 'director_property'],
|
|
[
|
|
'key_name' => 'dp.key_name',
|
|
'uuid' => 'dp.uuid',
|
|
'value_type' => 'dp.value_type',
|
|
'label' => 'dp.label',
|
|
'instantiable' => 'dp.instantiable',
|
|
'required' => 'iop.required',
|
|
'children' => 'COUNT(cdp.uuid)'
|
|
]
|
|
)
|
|
->join(['iop' => "icinga_$type" . '_property'], 'dp.uuid = iop.property_uuid', [])
|
|
->joinLeft(['cdp' => 'director_property'], 'cdp.parent_uuid = dp.uuid', [])
|
|
->where('iop.' . $type . '_uuid IN (?)', $uuids)
|
|
->group(['dp.uuid', 'dp.key_name', 'dp.value_type', 'dp.label', 'dp.instantiable', 'iop.required'])
|
|
->order('children')
|
|
->order('instantiable')
|
|
->order('key_name');
|
|
|
|
$result = [];
|
|
foreach ($db->getDbAdapter()->fetchAll($query, fetchMode: PDO::FETCH_ASSOC) as $row) {
|
|
$result[$row['key_name']] = $row;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function handleApiRequest()
|
|
{
|
|
$request = $this->request;
|
|
$db = $this->db;
|
|
|
|
// TODO: I hate doing this:
|
|
if ($this->request->getActionName() === 'ticket') {
|
|
$host = $this->requireObject();
|
|
|
|
if ($host->getResolvedProperty('has_agent') !== 'y') {
|
|
throw new NotFoundError('The host "%s" is not an agent', $host->getObjectName());
|
|
}
|
|
|
|
$this->sendJson($this->api->getTicket($host->getObjectName()));
|
|
|
|
// TODO: find a better way to shut down. Currently, this avoids
|
|
// "not found" errors:
|
|
exit;
|
|
}
|
|
|
|
switch ($request->getMethod()) {
|
|
case 'DELETE':
|
|
$object = $this->requireObject();
|
|
$object->delete();
|
|
$this->sendJson($object->toPlainObject(false, true));
|
|
|
|
break;
|
|
case 'POST':
|
|
case 'PUT':
|
|
$data = (array) $this->requireJsonBody();
|
|
$params = $this->request->getUrl()->getParams();
|
|
$allowsOverrides = $params->get('allowOverrides');
|
|
$type = $this->getType();
|
|
$object = $this->loadOptionalObject();
|
|
$actionName = $this->request->getActionName();
|
|
|
|
$overRiddenCustomVars = [];
|
|
if ($actionName === 'variables') {
|
|
$overRiddenCustomVars = ['vars' => $data];
|
|
} else {
|
|
if ($type === 'host') {
|
|
$overRiddenCustomVars = $this->getCustomVarsFromData($data);
|
|
}
|
|
|
|
if ($object) {
|
|
if ($request->getMethod() === 'POST') {
|
|
$object->setProperties($data);
|
|
} else {
|
|
$data = array_merge([
|
|
'object_type' => $object->get('object_type'),
|
|
'object_name' => $object->getObjectName()
|
|
], $data);
|
|
$object->replaceWith(IcingaObject::createByType($type, $data, $db));
|
|
}
|
|
|
|
$this->persistChanges($object);
|
|
} elseif ($allowsOverrides && $type === 'service') {
|
|
if ($request->getMethod() === 'PUT') {
|
|
throw new InvalidArgumentException('Overrides are not (yet) available for HTTP PUT');
|
|
}
|
|
|
|
$this->setServiceProperties($params->getRequired('host'), $params->getRequired('name'), $data);
|
|
} else {
|
|
$object = IcingaObject::createByType($type, $data, $db);
|
|
$this->persistChanges($object);
|
|
}
|
|
}
|
|
|
|
if ($type !== 'service' && $overRiddenCustomVars) {
|
|
$customProperties = $this->getCustomProperties($object);
|
|
if (! empty($overRiddenCustomVars)) {
|
|
$diff = array_diff(array_keys($overRiddenCustomVars['vars']), array_keys($customProperties));
|
|
if (! empty($diff)) {
|
|
throw new NotFoundError(sprintf(
|
|
"The custom properties (%s) are not supported by this object",
|
|
implode(", ", $diff)
|
|
));
|
|
}
|
|
}
|
|
|
|
$object->setProperties($overRiddenCustomVars);
|
|
$this->persistChanges($object);
|
|
}
|
|
|
|
$this->sendJson($object->toPlainObject(false, true));
|
|
|
|
break;
|
|
|
|
case 'GET':
|
|
$object = $this->requireObject();
|
|
$exporter = new Exporter($this->db);
|
|
RestApiParams::applyParamsToExporter($exporter, $this->request, $object->getShortTableName());
|
|
$this->sendJson($exporter->export($object));
|
|
break;
|
|
|
|
default:
|
|
$request->getResponse()->setHttpResponseCode(400);
|
|
throw new IcingaException('Unsupported method ' . $request->getMethod());
|
|
}
|
|
}
|
|
|
|
protected function persistChanges(IcingaObject $object)
|
|
{
|
|
if ($object->hasBeenModified()) {
|
|
$status = $object->hasBeenLoadedFromDb() ? 200 : 201;
|
|
$object->store();
|
|
$this->response->setHttpResponseCode($status);
|
|
} else {
|
|
$this->response->setHttpResponseCode(304);
|
|
}
|
|
}
|
|
|
|
protected function setServiceProperties($hostname, $serviceName, $properties)
|
|
{
|
|
$host = IcingaHost::load($hostname, $this->db);
|
|
$service = ServiceFinder::find($host, $serviceName);
|
|
if ($service === false) {
|
|
throw new NotFoundError('Not found');
|
|
}
|
|
if ($service->requiresOverrides()) {
|
|
unset($properties['host']);
|
|
OverrideHelper::applyOverriddenVars($host, $serviceName, $properties);
|
|
$this->persistChanges($host);
|
|
$this->sendJson($host->toPlainObject(false, true));
|
|
} else {
|
|
throw new RuntimeException('Found a single service, which should have been found (and dealt with) before');
|
|
}
|
|
}
|
|
|
|
private function getCustomVarsFromData(array &$data): array
|
|
{
|
|
$customVars = [];
|
|
|
|
foreach ($data as $key => $value) {
|
|
if ($key === 'vars') {
|
|
$customVars = ['vars' => (array) $value];
|
|
|
|
unset($data['vars']);
|
|
}
|
|
|
|
if (substr($key, 0, 5) === 'vars.') {
|
|
$customVars['vars'][substr($key, 5)] = $value;
|
|
|
|
unset($data[$key]);
|
|
}
|
|
}
|
|
|
|
return $customVars;
|
|
}
|
|
}
|