<?php /** * ForemanApi * * foreman access to API * * @example * in project class * $oForeman=new ForemanApi($this->_aConfig['foreman']); * * // enable debugging * $oForeman->setDebug(1); * * // self check * $oForeman->selfcheck(); die(__FUNCTION__); * * // read operating systems and get id and title only * $aForemanHostgroups=$oForeman->read(array( * 'request'=>array( * array('operatingsystems'), * ), * 'response'=>array( * 'id','title' * ), * )); * * // read details for operating systems #4 * $aForemanHostgroups=$oForeman->read(array( * 'request'=>array( * array('operatingsystems', 4), * ), * )); * * * $aOptions ... can contain optional subkeys * - request * [] list of array(keyword [,id]) * - filter (array) * - search (string) * - page (string) * - per_page (string) * - response (array) * - list of keys, i.e. array('id', 'title') * @author hahn */ class ForemanApi { protected $_aCfg=array(); protected $_bDebug = false; protected $_aAllowedUrls=array( 'api'=>array( ''=>array(), 'architectures'=>array(), 'audits'=>array('methods'=>array('GET')), 'auth_source_ldaps'=>array(), 'bookmarks'=>array(), 'common_parameters'=>array(), 'compliance'=>array(), 'compute_attributes'=>array(), 'compute_profiles'=>array(), 'compute_resources'=>array(), 'config_groups'=>array(), 'config_reports'=>array(), 'config_templates'=>array(), 'dashboard'=>array('methods'=>array('GET')), 'domains'=>array(), 'environments'=>array(), 'fact_values'=>array(), 'filters'=>array(), 'hosts'=>array(), 'hostgroups'=>array(), 'job_invocations'=>array(), 'job_templates'=>array(), 'locations'=>array(), 'mail_notifications'=>array(), 'media'=>array(), 'models'=>array(), 'operatingsystems'=>array('methods'=>array('GET')), 'orchestration'=>array(), 'organizations'=>array(), 'permissions'=>array(), 'plugins'=>array(), 'provisioning_templates'=>array(), 'ptables'=>array(), 'puppetclasses'=>array(), 'realms'=>array(), 'remote_execution_features'=>array(), 'reports'=>array(), 'roles'=>array(), 'settings'=>array(), 'smart_class_parameters'=>array(), 'smart_proxies'=>array(), 'smart_variables'=>array(), 'statistics'=>array('methods'=>array('GET')), 'status'=>array('methods'=>array('GET')), 'subnets'=>array(), 'template_combinations'=>array(), 'template_kinds'=>array('methods'=>array('GET')), 'templates'=>array(), 'usergroups'=>array(), 'users'=>array(), // ... ), 'api/v2'=>array( 'discovered_hosts'=>array(), 'discovery_rules'=>array(), ), 'foreman_tasks/api'=>array( 'recurring_logics'=>array(), 'tasks'=>array(), ), ); /** * last request * @var type */ protected $_aRequest=array(); /** * last response * @var type */ protected $_aResponse=array(); // ---------------------------------------------------------------------- // constructor // ---------------------------------------------------------------------- public function __construct($aCfg) { if(!isset($aCfg['api'])){ die("ERROR: class ".__CLASS__." must be initialized with an array containing api config for foreman."); return false; } $this->_aCfg=$aCfg; return true; } // ---------------------------------------------------------------------- // private functions // ---------------------------------------------------------------------- /** * add a log messsage * @global object $oLog * @param string $sMessage messeage text * @param string $sLevel warnlevel of the given message * @return bool */ protected function log($sMessage, $sLevel = "info") { global $oCLog; return $oCLog->add(basename(__FILE__) . " class " . __CLASS__ . " - " . $sMessage, $sLevel); } /** * search url prefix in $this->_aAllowedUrls by given key * @param type $sFunction * @return type */ protected function _guessPrefixUrl($sFunction=false){ $sReturn=''; /* if (!$sFunction){ $sFunction=$this->_aRequest['request'][0][0]; } * */ foreach($this->_aAllowedUrls as $sPrefix=>$aUrls){ foreach(array_keys($aUrls) as $sKeyword){ if ($sFunction==$sKeyword){ $sReturn=$sPrefix; break; } } } return $sReturn; } /** * generate an url for foreman API request based on option keys * @return string */ protected function _generateUrl(){ if(!isset($this->_aRequest['request'])){ die('ERROR: missing key [request]'); } $sReturn=$this->_aCfg['api']; $sPrefix=$this->_guessPrefixUrl(); $sReturn.=$sPrefix.'/'; foreach($this->_aRequest['request'] as $aReqItem){ if (!isset($this->_aAllowedUrls[$sPrefix][$aReqItem[0]])){ echo 'WARNING: wrong item: [' . $aReqItem[0]."]<br>\n"; } $sReturn.=$aReqItem[0] ? $aReqItem[0].'/' : ''; if(count($aReqItem)>1){ $sReturn.=(int)$aReqItem[1].'/'; } } return $sReturn; } /** * add parameter for search and paging in an foreman API URL * * @return string */ protected function _generateParams(){ if (!isset($this->_aRequest['filter'])){ return ''; } $sReturn='?'; foreach ($this->_aRequest['filter'] as $sKey=>$value){ $sReturn.='&'.$sKey.'='.urlencode($value); } return $sReturn; } /** * make an http get request and return the response body * it is called by _makeRequest * $aRequest contains subkeys * - url * - method; one of GET|POST|PUT|DELETE * - postdata; for POST only * * @param array $aRequest arrayurl for Foreman API * @return string */ protected function _httpCall($aRequest=false, $iTimeout = 15) { if ($aRequest){ $this->_aRequest=$aRequest; } $this->_aResponse=array(); $this->log(__FUNCTION__ . " start <pre>".print_r($this->_aRequest,1)."</pre>"); if (!function_exists("curl_init")) { die("ERROR: PHP CURL module is not installed."); } $sApiUser=isset($this->_aCfg['user']) ? $this->_aCfg['user'] : false; $sApiPassword=isset($this->_aCfg['password']) ? $this->_aCfg['password'] : false; // $sFullUrl=$sApiUrl.$this->_aRequest['url']; $sFullUrl=$this->_aRequest['url']; $this->log(__FUNCTION__ . " ".$this->_aRequest['method']." " . $this->_aRequest['url']); $ch = curl_init($this->_aRequest['url']); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->_aRequest['method']); if ($this->_aRequest['method']==='POST'){ curl_setopt($ch, CURLOPT_POSTFIELDS, $this->_aRequest['postdata']); } curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); if (array_key_exists('ignore-ssl-error', $this->_aCfg) && $this->_aCfg['ignore-ssl-error']){ $this->log(__FUNCTION__ . " WARNING: SSL errors will be ignored by config.", 'warning'); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); } curl_setopt($ch, CURLOPT_TIMEOUT, $iTimeout); curl_setopt($ch, CURLOPT_USERAGENT, 'IML Deployment GUI :: ' . __CLASS__); if ($sApiUser){ curl_setopt($ch, CURLOPT_USERPWD, $sApiUser.":".$sApiPassword); } $res = curl_exec($ch); $aReturn=array('info'=>curl_getinfo($ch), 'error'=>curl_error($ch)); curl_close($ch); $this->log(__FUNCTION__ . " status ".$aReturn['info']['http_code'].' '.$this->_aRequest['method']." $sFullUrl"); $sHeader=substr($res, 0, $aReturn['info']['header_size']); $aReturn['header']=explode("\n", $sHeader); $aReturn['body']=str_replace($sHeader, "", $res); return $aReturn; } /** * write debug infos if enabled * @param string $sMessage * @return boolean */ protected function _writeDebug($sMessage){ if ($this->_bDebug){ echo "DEBUG :: ".__CLASS__." :: $sMessage<br>\n"; } return true; } // ---------------------------------------------------------------------- // public functions :: low level // ---------------------------------------------------------------------- /** * make an http(s) request to foreman and scan result for http code and * content in response json; method returns an array with subkeys * - info: curl info array * - error: curl error message * - header: http response headers * - body: http response body * - _json: parsed json data from response body * - _OK: flag if result is OK and complete * - _status: info * * * $aRequest contains subkeys * - function --> to extract method and generate url * - method; one of GET|POST|PUT|DELETE * - postdata; for POST only * @param array $aRequest arrayurl for Foreman API * @return array */ public function makeRequest($aRequest=false) { if ($aRequest){ $this->_aRequest=$aRequest; } $sStatus='unknown'; $bOk=false; // prevent missing data because of paging if ($this->_aRequest['method']==='GET' && !isset($this->_aRequest['filter']['per_page'])){ $this->_aRequest['filter']['per_page']=1000; } // TODO check postdata if ($this->_aRequest['method']==='POST' && ( !isset($this->_aRequest['postdata']) || !is_array($this->_aRequest['postdata']) || !count($this->_aRequest['postdata']) ) ){ die("ERROR in ".__CLASS__."::".__FUNCTION__.": missing data to make a POST request"); } $this->_aRequest['url']=isset($this->_aRequest['url']) ? $this->_aRequest['url'] : $this->_generateUrl().$this->_generateParams() ; // ----- request $this->_writeDebug(__FUNCTION__ . ' start request <pre>'.print_r($this->_aRequest,1).'</pre>'); $aReturn=$this->_httpCall(); // ----- check result // check status $iStatuscode=$aReturn['info']['http_code']; if ($iStatuscode===0){ $sStatus='wrong host or no connect'; } if ($iStatuscode>=200 && $iStatuscode<400){ $sStatus='OK'; $bOk=true; } if ($iStatuscode>=400 && $iStatuscode<500){ $sStatus='error'; } if ($iStatuscode===404){ $sStatus='wrong url'; } // check result json if($bOk){ $aJson=json_decode($aReturn['body'], 1); if (is_array($aJson)){ if (isset($aJson['total']) && $aJson['total'] > $aJson['per_page']){ $bOk=false; $sStatus='Http OK, but incomplete results (paging)'; } $aReturn['_json']=$aJson; } else { $bOk=false; $sStatus='Http OK, but wrong response'; } } $aReturn['_OK']=$bOk; $aReturn['_status']=$sStatus; $this->_writeDebug(__FUNCTION__ . ' result of request <pre>'.print_r($aReturn,1).'</pre>'); $this->_aResponse=$aReturn; return $aReturn; } /** * filter output for the response based on values $this->_aRequest['response'] * @param array $aData response of $this->makeRequest(); * @return array */ protected function _filterOutput($aData){ if(!isset($this->_aRequest['response'])){ return $aData; } $aTmp=isset($aData['_json']['results']) ? $aData['_json']['results'] : array($aData['_json']); if(!count($aTmp)){ return array(); } $aReturn=array(); foreach($aTmp as $aItem){ $aReturnitem=array(); foreach($this->_aRequest['response'] as $sKey){ if (array_key_exists($sKey, $aItem)){ $aReturnitem[$sKey]=$aItem[$sKey]; } } $aReturn[] = $aReturnitem; } /* return ($bIsList==1) ? $aReturn : (count($aReturn) ? $aReturn[0] : array()); */ return $aReturn; } // ---------------------------------------------------------------------- // public foreman functions // ---------------------------------------------------------------------- /** * enable/ disable debugging * @param boolean $bNewDebugFlag new value; true|false * @return boolean */ public function setDebug($bNewDebugFlag){ return $this->_bDebug=$bNewDebugFlag; } /** * check for missing config entries * @return type */ public function selfcheck() { $sOut=''; $sWarning=''; $sOut.="<h1>selfcheck</h1>"; $aApi=$this->read(array('request'=>array(array('')))); if($aApi['_OK']){ foreach($aApi['_json']['links'] as $sKey=>$aCalls){ $sOut.="<h2>$sKey</h2><ul>"; foreach ($aCalls as $sLabel=>$sUrl){ $sOut.="<li>$sLabel .. $sUrl "; $aTmp=preg_split('#\/#', $sUrl); $sDir2=count($aTmp)>2 ? $aTmp[2] : '??'; $sDir3=count($aTmp)>3 ? $aTmp[3] : '??'; $sOut.="..... " . ($this->_guessPrefixUrl($sDir2).$this->_guessPrefixUrl($sDir3) ?'<span style="background:#cfc">OK</span>' :'<span style="background:#fcc">miss</span>' ) . ' ' . $sDir2.', '.$sDir3 . "</li>\n"; if (!($this->_guessPrefixUrl($sDir2).$this->_guessPrefixUrl($sDir3))){ $sWarning.="<li>$sKey - $sLabel - $sUrl</li>"; } } $sOut.="</ul>"; } } else { $sOut.='ERROR: unable to connect to foreman or missing permissions.<br>'; } if ($sWarning){ echo 'WARNINGS:<ol>'.$sWarning.'</ol>'; } echo $sOut; return true; } // ---------------------------------------------------------------------- // public foreman API CRUD functions // ---------------------------------------------------------------------- /** * TODO: create * @param array $aOptions */ public function create($aOptions){ /* $this->_aRequest=$aOptions; $this->_aRequest['method']='POST'; return $this->makeRequest(); */ } /** * GETTER * $aOptions ... can contain optional subkeys * - request * [] list of array(keyword [,id]) * - filter (array) * - search (string) * - page (string) * - per_page (string) * - any attribute in the return resultset * - response (array) * - list of keys, i.e. array('id', 'title') * * @param array $aOptions * @return array */ public function read($aOptions){ $this->_aRequest=$aOptions; $this->_aRequest['method']='GET'; $aData=$this->makeRequest(); if (!$aData['_OK']){ return false; } return $this->_filterOutput($aData); } /** * TODO * @param type $aOptions */ public function update($aOptions){ /* $this->_aRequest=$aOptions; $this->_aRequest['method']='PUT'; return $this->makeRequest(); */ } /** * TODO * @param type $aOptions */ public function delete($aOptions){ /* $this->_aRequest=$aOptions; $this->_aRequest['method']='DELETE'; return $this->makeRequest(); */ } // ---------------------------------------------------------------------- // get response infos // ---------------------------------------------------------------------- /** * get curl info data from last response * Array ( [url] => https://foreman/api/hostgroups/?&per_page=1000 [content_type] => application/json; charset=utf-8 [http_code] => 200 [header_size] => 1034 [request_size] => 218 [filetime] => -1 [ssl_verify_result] => 0 [redirect_count] => 0 [total_time] => 1.644417 [namelookup_time] => 0.007198 [connect_time] => 0.009012 [pretransfer_time] => 0.0332 [size_upload] => 0 [size_download] => 119602 [speed_download] => 72732 [speed_upload] => 0 [download_content_length] => -1 [upload_content_length] => 0 [starttransfer_time] => 1.642775 [redirect_time] => 0 [redirect_url] => [primary_ip] => 10.0.2.10 [certinfo] => Array ( ) [primary_port] => 443 [local_ip] => 10.0.2.15 [local_port] => 33906 ) * @param string $sKey get value of given key only * @return any */ public function getResponseInfo($sKey=false){ if($sKey){ return isset($this->_aResponse['info'][$sKey]) ? $this->_aResponse['info'][$sKey] : false ; } return isset($this->_aResponse['info']) ? $this->_aResponse['info'] : false ; /* return isset($sKey) && $sKey ? isset($this->_aResponse['info'][$sKey]) ? $this->_aResponse['info'][$sKey] : false : isset($this->_aResponse['info']) ? $this->_aResponse['info'] : false ; */ } /** * get array of last http response header * @return array */ public function getResponseHeader(){ return isset($this->_aResponse['header']) ? $this->_aResponse['header'] : false ; } /** * get status of last request * @return string */ public function getResponseStatus(){ // print_r($this->_aResponse); die(); return isset($this->_aResponse['_status']) ? $this->_aResponse['_status'] : false ; } }