diff --git a/Splunk/Collection.php b/Splunk/Collection.php index 82278c3..cb8db1d 100644 --- a/Splunk/Collection.php +++ b/Splunk/Collection.php @@ -22,7 +22,7 @@ */ class Splunk_Collection extends Splunk_Endpoint { - private $entitySubclass; + protected $entitySubclass; /** * @internal @@ -137,7 +137,7 @@ private function loadEntitiesFromResponse($response) * @param SimpleXMLElement $entry an element. * @return Splunk_Entry */ - private function loadEntityFromEntry($entry) + protected function loadEntityFromEntry($entry) { return new $this->entitySubclass( $this->service, @@ -275,7 +275,7 @@ public function delete($name, $args=array()) /** * Returns the absolute path of the child entity with the specified name. */ - private function getEntityPath($name) + protected function getEntityPath($name) { return $this->path . $this->getEntityRelativePath($name); } diff --git a/Splunk/Entity.php b/Splunk/Entity.php index 87e2b6f..16ec2bf 100644 --- a/Splunk/Entity.php +++ b/Splunk/Entity.php @@ -22,9 +22,30 @@ */ class Splunk_Entity extends Splunk_Endpoint implements ArrayAccess { + /** + * Indicates whether the entity has been loaded + * + * @var boolean + */ private $loaded = FALSE; + + /** + * + * @var SimpleXMLElement + */ private $entry; + + /** + * + * @var string + */ private $namespace; + + /** + * Holds the available properties for this entity + * + * @var array + */ private $content; /** @@ -88,9 +109,9 @@ protected function validate($fetchArgs=array()) * * @throws Splunk_IOException */ - private function load($fetchArgs) + private function load() { - $response = $this->fetch($fetchArgs); + $response = $this->fetch(); $xml = new SimpleXMLElement($response->body); $this->entry = $this->extractEntryFromRootXmlElement($xml); @@ -100,9 +121,10 @@ private function load($fetchArgs) /** * Fetches this entity's Atom feed from the Splunk server. * + * @return Splunk_HttpResponse * @throws Splunk_IOException */ - protected function fetch($fetchArgs) + protected function fetch() { return $this->sendGet(''); } @@ -123,14 +145,20 @@ protected function extractEntryFromRootXmlElement($xml) return $xml->entry; } - /** Parses the entry's contents. */ + /** + * Parses the entry's contents. + */ private function parseContentsFromEntry() { $this->content = Splunk_AtomFeed::parseValueInside($this->entry->content); $this->loaded = TRUE; } - /** Returns a value that indicates whether the entity has been loaded. */ + /** + * Returns a value that indicates whether the entity has been loaded. + * + * @return boolean + */ protected function isLoaded() { return $this->loaded; @@ -189,7 +217,11 @@ protected function getTitle() return (string) $this->validate()->entry->title; } - /** Gets the namespace in which this entity resides. */ + /** + * Gets the namespace in which this entity resides. + * + * @return string + */ protected function getSearchNamespace() { return $this->namespace; @@ -222,7 +254,8 @@ public function getNamespace() // === ArrayAccess Methods === /** - * Gets the value of the specified entity property. + * ArrayAccess Interface Method to get the value of the + * specified entity property. * * @param string $key The name of an entity property. * @return string The value of the specified entity property. @@ -231,21 +264,36 @@ public function offsetGet($key) { return $this->validate()->content[$key]; } - - /** @internal */ + + /** + * unsupported ArrayAccess Interface Method to assign a value to an offset + * + * @internal + * @param mixed $key + * @param mixed $value + * @throws Splunk_UnsupportedOperationException + */ public function offsetSet($key, $value) { throw new Splunk_UnsupportedOperationException(); } - /** @internal */ + /** + * unsupported ArrayAccess Interface Method to unset an offset + * + * @internal + * @param mixed $key + * @param mixed $value + * @throws Splunk_UnsupportedOperationException + */ public function offsetUnset($key) { throw new Splunk_UnsupportedOperationException(); } /** - * Gets a value that indicates whether the specified entity property exists. + * ArrayAccess Interface Method to gets a value that indicates whether + * the specified entity property exists. * * @param string $key The name of an entity property. * @return string Whether the specified entity property exists. diff --git a/Splunk/Http.php b/Splunk/Http.php index ff4f9da..5ad780a 100644 --- a/Splunk/Http.php +++ b/Splunk/Http.php @@ -105,9 +105,13 @@ public function request( 'max_redirects' => 0, // [PHP 5.2] don't follow HTTP 3xx automatically 'ignore_errors' => TRUE, // don't throw exceptions on bad status codes ), + 'ssl' => array( + 'verify_peer' => false, // don't verify ssl peer. Since php 5.6 this defaults to true + 'verify_peer_name' => false // allow certificate CN missmtach + ) )); - - // NOTE: PHP does not perform certificate validation for HTTPS URLs. + + // NOTE: PHP does not perform certificate validation for HTTPS URLs prior version 5.6. // NOTE: fopen() magically sets the $http_response_header local variable. $bodyStream = @fopen($url, 'rb', /*use_include_path=*/FALSE, $fopenContext); if ($bodyStream === FALSE) diff --git a/Splunk/Job.php b/Splunk/Job.php index 87f7b56..6ed2483 100644 --- a/Splunk/Job.php +++ b/Splunk/Job.php @@ -22,43 +22,18 @@ */ class Splunk_Job extends Splunk_Entity { + /** + * Indicates if the Job is ready for querying + * @var boolean + */ + private $isReady; + // NOTE: These constants are somewhat arbitrary and could use some tuning const DEFAULT_FETCH_MAX_TRIES = 10; const DEFAULT_FETCH_DELAY_PER_RETRY = 0.1; // secs // === Load === - - /* - * Job requests sometimes yield an HTTP 204 code when they are in the - * process of being created. To hide this from the caller, transparently - * retry requests when an HTTP 204 is received. - */ - protected function fetch($fetchArgs) - { - $fetchArgs = array_merge(array( - 'maxTries' => Splunk_Job::DEFAULT_FETCH_MAX_TRIES, - 'delayPerRetry' => Splunk_Job::DEFAULT_FETCH_DELAY_PER_RETRY, - ), $fetchArgs); - - for ($numTries = 0; $numTries < $fetchArgs['maxTries']; $numTries++) - { - $response = parent::fetch($fetchArgs); - if ($this->isFullResponse($response)) - return $response; - usleep($fetchArgs['delayPerRetry'] * 1000000); - } - - // If actually HTTP 200, convert to a simulated HTTP 204 response - if ($response->status != 204) - { - $response->status = 204; - $response->reason = "Timed out waiting for job to parse."; - } - - // Give up - throw new Splunk_HttpException($response); - } - + protected function extractEntryFromRootXmlElement($xml) { // element is at the root of a job's Atom feed @@ -68,15 +43,50 @@ protected function extractEntryFromRootXmlElement($xml) // === Ready === /** - * Returns a value that indicates whether this job has been loaded. + * Indicates whether the job has been scheduled and is ready to + * return data. * - * @return bool Whether this job has been loaded. + * @return boolean */ public function isReady() { - return $this->isLoaded(); + if (!$this->isReady) + { + $reponse = $this->fetch(); + $this->isReady = $this->isFullResponse($reponse); + } + + return $this->isReady; } + /** + * Refreshes this entity's properties from the Splunk server + * if the search is scheduled. + * + * @return Splunk_Job This Job. + * @throws Splunk_IOException + */ + public function refresh() + { + if ($this->isReady()) + { + return parent::refresh(); + } + + return $this; + } + + /** + * Checks if the job is ready for quering + * + * @throws Splunk_JobNotReadyException + */ + private function checkReady() + { + if (!$this->isReady()) + throw new Splunk_JobNotReadyException("Job is not yet scheduled by the server"); + } + /** * Loads this job, retrying the specified number of times as necessary. * @@ -88,24 +98,32 @@ public function isReady() * @throws Splunk_IOException */ public function makeReady( - $maxTries=Splunk_Job::DEFAULT_FETCH_MAX_TRIES, - $delayPerRetry=Splunk_Job::DEFAULT_FETCH_DELAY_PER_RETRY) + $maxTries = Splunk_Job::DEFAULT_FETCH_MAX_TRIES, + $delayPerRetry = Splunk_Job::DEFAULT_FETCH_DELAY_PER_RETRY) { - return $this->validate(/*fetchArgs=*/array( - 'maxTries' => $maxTries, - 'delayPerRetry' => $delayPerRetry, - )); + for ($numTries = 0; $numTries < $maxTries; $numTries++) + { + $reponse = $this->fetch(); + if ($this->isFullResponse($reponse)) + { + return $this->validate(); + } + usleep($delayPerRetry * 1000000); + } + throw new Splunk_HttpException($reponse); } // === Accessors === - // Overrides superclass to return the correct ID of this job, - // which can be used to lookup this job from the Jobs collection. /** + * Overrides superclass to return the correct ID of this job, + * which can be used to lookup this job from the Jobs collection. + * * @see Splunk_Entity::getName() */ public function getName() { + $this->checkReady(); return $this['sid']; } @@ -116,6 +134,7 @@ public function getName() */ public function getSearch() { + $this->checkReady(); return $this->getTitle(); } @@ -123,8 +142,8 @@ public function getSearch() /** * Returns a value that indicates the percentage of this job's results - * that were computed at the time this job was last loaded or - * refreshed. + * that were computed at the time this job was last loaded or refreshed. + * If the job is not yet sheduled on the server it returns 0. * * @return float Percentage of this job's results that were * computed (0.0-1.0) at the time this job was @@ -133,12 +152,14 @@ public function getSearch() */ public function getProgress() { + if (!$this->isReady()) + return 0; return floatval($this['doneProgress']); } /** * Returns a value that indicates whether this job's results were available - * at the time this job was last loaded or refreshed. + * at the time this job was last loaded or refreshed. * * @return boolean Whether this job's results were available * at the time this job was last loaded or @@ -147,6 +168,8 @@ public function getProgress() */ public function isDone() { + if (!$this->isReady()) + return false; return ($this['isDone'] === '1'); } @@ -211,6 +234,7 @@ public function isDone() */ public function getResults($args=array()) { + $this->checkReady(); return new Splunk_PaginatedResultsReader($this, $args); } @@ -280,6 +304,7 @@ public function getResults($args=array()) */ public function getResultsPage($args=array()) { + $this->checkReady(); $response = $this->fetchPage('results', $args); if ($response->status == 204) throw new Splunk_JobNotDoneException($response); @@ -332,7 +357,14 @@ public function getResultsPreviewPage($args=array()) return $response->bodyStream; } - /** Fetches a page of the specified type. */ + /** + * Fetches a page of the specified type. + * + * @param string $pageType + * @param array $args + * @return Splunk_HttpResponse + * @throws InvalidArgumentException + */ private function fetchPage($pageType, $args) { $args = array_merge(array( @@ -350,13 +382,15 @@ private function fetchPage($pageType, $args) return $response; } - /** Determines whether a response contains full or partial results */ - private function isFullResponse($response) + /** + * Determines whether a response contains full or partial results + * @param Splunk_HttpResponse $response + * @return boolean + */ + private function isFullResponse(Splunk_HttpResponse $response) { if ($response->status == 204) - { $result = FALSE; - } else { $responseBody = new SimpleXMLElement($response->body); diff --git a/Splunk/JobNotReadyException.php b/Splunk/JobNotReadyException.php new file mode 100644 index 0000000..9b056c2 --- /dev/null +++ b/Splunk/JobNotReadyException.php @@ -0,0 +1,26 @@ +getReference($name, $namespace)->makeReady(); } + /** + * Returns an entity from the given entry element. + * This method uses the SID to identify the entity instead of the title property. + * + * @param SimpleXMLElement $entry an element. + * @return Splunk_Entry + */ + protected function loadEntityFromEntry($entry) + { + $content = Splunk_AtomFeed::parseValueInside($entry->content); + + //A Job should always have a SID. + if (!isset($content['sid'])) + throw new \RuntimeException("loadEntityFromEntry expected the entry to contain a SID. no SID found."); + + return new $this->entitySubclass( + $this->service, + $this->getEntityPath($content['sid']), + $entry); + } + /** * Creates a new search job. * diff --git a/Splunk/Receiver.php b/Splunk/Receiver.php index e351d0b..2e76cab 100644 --- a/Splunk/Receiver.php +++ b/Splunk/Receiver.php @@ -96,19 +96,26 @@ public function attach($args=array()) $scheme = $this->service->getScheme(); $host = $this->service->getHost(); $port = $this->service->getPort(); - + $errno = 0; $errstr = ''; + $socketContext = stream_context_create(array( + 'ssl' => array( + 'verify_peer' => false, //don't verify ssl: Since php 5.6 this defaults to true! + 'verify_peer_name' => false //allows certificate CN missmtach + ) + )); + if ($scheme == 'http') - $stream = @fsockopen($host, $port, /*out*/ $errno, /*out*/ $errstr); + $stream = @stream_socket_client($host . ":" . $port, /*out*/ $errno, /*out*/ $errstr, 30, STREAM_CLIENT_CONNECT, $socketContext); else if ($scheme == 'https') - $stream = @fsockopen('ssl://' . $host, $port, /*out*/ $errno, /*out*/ $errstr); + $stream = @stream_socket_client('ssl://' . $host . ":" . $port, /*out*/ $errno, /*out*/ $errstr, 30, STREAM_CLIENT_CONNECT, $socketContext); else throw new Splunk_UnsupportedOperationException( 'Unsupported URL scheme.'); if ($stream === FALSE) - throw new Splunk_ConnectException($errstr, $errno); - + throw new Splunk_ConnectException("error: ".$errstr.$host.":".$port, $errno); + $path = '/services/receivers/stream?' . http_build_query($args); $token = $this->service->getToken(); diff --git a/tests/JobTest.php b/tests/JobTest.php index c4c6f7e..994948a 100644 --- a/tests/JobTest.php +++ b/tests/JobTest.php @@ -24,11 +24,11 @@ public function testGetTimeout() list($service, $http) = $this->loginToMockService(); // Get job - $httpResponse = (object) array( + $httpResponse = new Splunk_HttpResponse(array( 'status' => 204, 'reason' => 'No Content', 'headers' => array(), - 'body' => ''); + 'body' => '')); $http->expects($this->atLeastOnce()) ->method('get') ->will($this->returnValue($httpResponse)); @@ -38,187 +38,35 @@ public function testGetTimeout() try { $this->touch($job); - $this->assertTrue(FALSE, 'Expected Splunk_HttpException to be thrown.'); + $this->assertTrue(FALSE, 'Expected Splunk_JobNotReadyException to be thrown.'); } - catch (Splunk_HttpException $e) + catch (Splunk_JobNotReadyException $e) { - $this->assertEquals(204, $e->getResponse()->status); - } - } - - public function testGetTimeoutSimulated() - { - $bodyForJobInParsingState = -' - - - - search index=_internal latest=-5m | stats count | appendcols [search index=_internal latest=-5m | stats count] - https://localhost:8089/services/search/jobs/1404154730.29 - 2014-06-30T11:58:51.000-07:00 - - 2014-06-30T11:58:50.000-07:00 - - - - - - - - - admin - - - - 36800464769513394 - 2038-01-18T19:14:07.000-08:00 - 604800 - 600 - - 0 - PARSING - 0 - 0 - 1969-12-31T16:00:00.000-08:00 - 0 - 0 - 0 - 1 - 1 - - desc - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - - - 0 - 2359 - 5 - - - 0 - 1 - 0 - 0.001000 - 0 - 1404154730.29 - 0 - 600 - - - - - 0.001000 - 1 - - - - - 0.045000 - 1 - - - - - - - - - - search index=_internal latest=-5m | stats count | appendcols [search index=_internal latest=-5m | stats count] - - - - - 0 - 0 - - - - - - - - - admin - - - - - admin - - - - - admin - 1 - global - search - 1 - 600 - - - - - - - -'; - - list($service, $http) = $this->loginToMockService(); - - // Get job - $httpResponse = (object) array( - 'status' => 200, - 'reason' => 'OK', - 'headers' => array(), - 'body' => $bodyForJobInParsingState); - $http->expects($this->atLeastOnce()) - ->method('get') - ->will($this->returnValue($httpResponse)); - $job = $service->getJobs()->getReference('A_JOB'); - - // Try to touch job when server refuses to return it - try - { - $this->touch($job); - $this->assertTrue(FALSE, 'Expected Splunk_HttpException to be thrown.'); - } - catch (Splunk_HttpException $e) - { - $this->assertEquals(204, $e->getResponse()->status); + // Good } } - + public function testMakeReady() { $maxTries = 7; + $additionalGetCalls = 1; //the new isRead Method makes an http call now $this->assertTrue( $maxTries != Splunk_Job::DEFAULT_FETCH_MAX_TRIES, 'This test is only valid for a non-default number of fetch attempts.'); list($service, $http) = $this->loginToMockService(); - $httpResponse = (object) array( + $httpResponse = new Splunk_HttpResponse(array( 'status' => 204, 'reason' => 'No Content', 'headers' => array(), - 'body' => ''); - $http->expects($this->exactly($maxTries)) + 'body' => '')); + $http->expects($this->exactly($maxTries+$additionalGetCalls)) ->method('get') ->will($this->returnValue($httpResponse)); $job = $service->getJobs()->getReference('A_JOB'); - $this->assertFalse($job->isReady()); + $this->assertFalse($job->isReady()); //calls http->get() an additional time try { $job->makeReady(/*maxTries=*/$maxTries, /*delayPerRetry=*/0.1); @@ -226,7 +74,7 @@ public function testMakeReady() } catch (Splunk_HttpException $e) { - $this->assertEquals(204, $e->getResponse()->status); + // Good } } @@ -234,7 +82,7 @@ public function testMakeReadyReturnsSelf() { list($service, $http) = $this->loginToMockService(); - $httpResponse = (object) array( + $httpResponse = new Splunk_HttpResponse(array( 'status' => 200, 'reason' => 'OK', 'headers' => array(), @@ -243,8 +91,8 @@ public function testMakeReadyReturnsSelf() -'); - $http->expects($this->once()) +')); + $http->expects($this->exactly(2)) // make ready now needs two calls ->method('get') ->will($this->returnValue($httpResponse)); $job = $service->getJobs()->getReference('A_JOB'); @@ -311,7 +159,11 @@ public function testResultsNotDone() try { $job->getResultsPage(); - $this->fail('Expected Splunk_JobNotDoneException.'); + $this->fail('Expected Splunk_JobNotReadyException or Splunk_JobNotDoneException.'); + } + catch (Splunk_JobNotReadyException $e) + { + // Good } catch (Splunk_JobNotDoneException $e) { @@ -379,6 +231,9 @@ public function testPreview() 'latest_time' => 'rt', )); + //wait for the search to become ready + $this->makeReady($rtjob); + $this->assertTrue($rtjob['isRealTimeSearch'] === '1', 'This should be a realtime job.'); @@ -443,6 +298,9 @@ public function testControlActions() 'latest_time' => 'rt', )); + //wait for the search to become ready + $this->makeReady($rtjob); + $this->assertTrue($rtjob['isRealTimeSearch'] === '1', 'This should be a realtime job.'); @@ -489,6 +347,9 @@ public function testGetName() $ss = $service->getSavedSearches()->get('Top five sourcetypes'); $job = $ss->dispatch(); + //wait for the search to become ready + $this->makeReady($job); + // Ensure that we have a fully loaded Job $this->touch($job); @@ -505,14 +366,14 @@ public function testCreateInCustomNamespace() { $namespace = Splunk_Namespace::createUser('USER', 'APP'); - $postResponse = (object) array( + $postResponse = new Splunk_HttpResponse(array( 'status' => 200, 'reason' => 'OK', 'headers' => array(), 'body' => trim(" 1345584253.35 -")); +"))); $postArgs = array( // (The URL should correspond to the namespace) 'https://localhost:8089/servicesNS/USER/APP/search/jobs/', @@ -539,14 +400,14 @@ public function testCreateInServiceNamespace() { $namespace = Splunk_Namespace::createUser('USER', 'APP'); - $postResponse = (object) array( + $postResponse = new Splunk_HttpResponse(array( 'status' => 200, 'reason' => 'OK', 'headers' => array(), 'body' => trim(" 1345584253.35 -")); +"))); $postArgs = array( // (The URL should correspond to the namespace) 'https://localhost:8089/servicesNS/USER/APP/search/jobs/', @@ -656,6 +517,22 @@ public function testResultsPageDocstringSample() //... } } + + public function testWaitingForParsing() + { + $service = $this->loginToRealService(); + + //This query has only one job: to stay in the parsing state for several seconds + $job = $service->getJobs()->create('search index=_internal | join host [search index=_internal] | join host [search index=_internal]'); + + //this should not raise an exeption + while(!$job->isDone()) + { + usleep(0.5 * 1000000); + $job->refresh(); + } + + } // === Utility === @@ -667,7 +544,7 @@ private function pageHasResults($resultsPage) return $pageHasResults; } - private function makeDone($job) + private function makeDone(Splunk_Job $job) { while (!$job->isDone()) { diff --git a/tests/JobsTest.php b/tests/JobsTest.php index 279c44c..3772153 100644 --- a/tests/JobsTest.php +++ b/tests/JobsTest.php @@ -28,6 +28,8 @@ public function testCreateNormal() $job = $service->getJobs()->create(JobsTest::SEARCH_QUERY, array( 'exec_mode' => 'normal', )); + //wait for the search to become ready + $this->makeReady($job); $this->touch($job); return array($service, $job); diff --git a/tests/SavedSearchTest.php b/tests/SavedSearchTest.php index 050684d..1ff43de 100644 --- a/tests/SavedSearchTest.php +++ b/tests/SavedSearchTest.php @@ -34,6 +34,7 @@ public function testGet() public function testDispatch($savedSearch) { $job = $savedSearch->dispatch(); + $this->makeReady($job); $this->assertEquals('1', $job['isSavedSearch']); } diff --git a/tests/SplunkTest.php b/tests/SplunkTest.php index e398fca..602e9be 100644 --- a/tests/SplunkTest.php +++ b/tests/SplunkTest.php @@ -142,4 +142,17 @@ private function createGuid() mt_rand(0, 65535), mt_rand(0, 65535)); } -} \ No newline at end of file + + /** + * Waits for the $job to get scheduled on the server. + * + * @param Splunk_Job $job + */ + protected function makeReady(Splunk_Job $job) + { + while(!$job->isReady()) + { + usleep(0.1 * 1000000); + } + } +}