diff --git a/public_html/deployment/classes/deploy-foreman.class.php b/public_html/deployment/classes/deploy-foreman.class.php new file mode 100644 index 0000000000000000000000000000000000000000..b99127d115155e3d06941b29762f9786ca57f49d --- /dev/null +++ b/public_html/deployment/classes/deploy-foreman.class.php @@ -0,0 +1,490 @@ +<?php +/** + * deployForeman + * + * foreman access to API + * + * @example + * in project class + * $oForeman=new deployForeman($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 deployForeman { + + 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(), + ), + ); + + + protected $_aRequest=array(); + + + // ---------------------------------------------------------------------- + // constructor + // ---------------------------------------------------------------------- + + + public function __construct($aCfg) { + if(!is_array($aCfg) || !count($aCfg) || !array_key_exists('api', $aCfg)){ + 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(!array_key_exists('request', $this->_aRequest)){ + die('ERROR: missing key [request]'); + } + $sReturn=$this->_aCfg['api']; + + $sPrefix=$this->_guessPrefixUrl(); + $sReturn.=$sPrefix; + + foreach($this->_aRequest['request'] as $aReqItem){ + if (!array_key_exists($aReqItem[0], $this->_aAllowedUrls[$sPrefix])){ + 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 (!array_key_exists('filter', $this->_aRequest)){ + return ''; + } + $sReturn='?'; + + // TODO: "sort by" and "sort order" ... need to be added here ... somehow + foreach(array('page', 'per_page', 'search') as $sParam){ + if (array_key_exists($sParam, $this->_aRequest['filter'])){ + $sReturn.='&'.$sParam.'='.urlencode($this->_aRequest['filter'][$sParam]); + } + } + 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 = 5) { + if ($aRequest){ + $this->_aRequest=$aRequest; + } + $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=array_key_exists('user', $this->_aCfg) ? $this->_aCfg['user'] : false; + $sApiPassword=array_key_exists('password', $this->_aCfg) ? $this->_aCfg['password'] : false; + + $sFullUrl=$sApiUrl.$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); + 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' && !array_key_exists('per_page', $this->_aRequest['filter'])){ + $this->_aRequest['filter']['per_page']=1000; + } + // TODO check postdata + if ($this->_aRequest['method']==='POST' && (!array_key_exists('postdata',$this->_aRequest) || !count($this->_aRequest['postdata']))){ + die("ERROR in ".__CLASS__."::".__FUNCTION__.": missing data to make a POST request"); + } + + $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'; + } + 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 (array_key_exists('total', $aJson) && $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>'); + + 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(!array_key_exists('response', $this->_aRequest)){ + return $aData; + } + $aTmp=array_key_exists('results', $aData['_json']) ? $aData['_json']['results'] : array($aData['_json']); + if(!count($aTmp)){ + return array(); + } + $aReturn=array(); + foreach($aTmp as $aItem){ + $aReturn[] = array_filter($aItem, function($key) { + return array_search($key, $this->_aRequest['response']) !==false; + }, ARRAY_FILTER_USE_KEY + ); + } + 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){ + } + /** + * GETTER + * $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') + * + * @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){ + + } + + /** + * TODO + * @param type $aOptions + */ + public function delete($aOptions){ + + } + +}