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; } }