Overview

Namespaces

  • None
  • OpenCloud
    • Autoscale
      • Resource
    • CloudMonitoring
      • Exception
      • Resource
    • Common
      • Exceptions
      • Log
      • Request
        • Response
    • Compute
    • Database
    • DNS
    • LoadBalancer
      • Resources
    • ObjectStore
      • Resource
    • Orchestration
    • Volume
  • PHP

Classes

  • Base
  • Collection
  • Lang
  • Metadata
  • Nova
  • PersistentObject
  • Service
  • ServiceCatalogItem
  • Overview
  • Namespace
  • Class
  • Tree
  • Download
  1: <?php
  2: /**
  3:  * PHP OpenCloud library.
  4:  * 
  5:  * @copyright Copyright 2013 Rackspace US, Inc. See COPYING for licensing information.
  6:  * @license   https://www.apache.org/licenses/LICENSE-2.0 Apache 2.0
  7:  * @version   1.6.0
  8:  * @author    Jamie Hannaford <jamie.hannaford@rackspace.com>
  9:  */
 10: 
 11: namespace OpenCloud\Common;
 12: 
 13: use OpenCloud\Common\Base;
 14: use OpenCloud\Common\Lang;
 15: use OpenCloud\OpenStack;
 16: use OpenCloud\Common\Exceptions;
 17: 
 18: /**
 19:  * This class defines a cloud service; a relationship between a specific OpenStack
 20:  * and a provided service, represented by a URL in the service catalog.
 21:  *
 22:  * Because Service is an abstract class, it cannot be called directly. Provider
 23:  * services such as Rackspace Cloud Servers or OpenStack Swift are each
 24:  * subclassed from Service.
 25:  *
 26:  * @author Glen Campbell <glen.campbell@rackspace.com>
 27:  */
 28: 
 29: abstract class Service extends Base
 30: {
 31: 
 32:     protected $conn;
 33:     private $service_type;
 34:     private $service_name;
 35:     private $service_region;
 36:     private $service_url;
 37: 
 38:     protected $_namespaces = array();
 39: 
 40:     /**
 41:      * Creates a service on the specified connection
 42:      *
 43:      * Usage: `$x = new Service($conn, $type, $name, $region, $urltype);`
 44:      * The service's URL is defined in the OpenStack's serviceCatalog; it
 45:      * uses the $type, $name, $region, and $urltype to find the proper URL
 46:      * and set it. If it cannot find a URL in the service catalog that matches
 47:      * the criteria, then an exception is thrown.
 48:      *
 49:      * @param OpenStack $conn - a Connection object
 50:      * @param string $type - the service type (e.g., "compute")
 51:      * @param string $name - the service name (e.g., "cloudServersOpenStack")
 52:      * @param string $region - the region (e.g., "ORD")
 53:      * @param string $urltype - the specified URL from the catalog
 54:      *      (e.g., "publicURL")
 55:      */
 56:     public function __construct(
 57:         OpenStack $conn,
 58:         $type,
 59:         $name,
 60:         $region,
 61:         $urltype = RAXSDK_URL_PUBLIC,
 62:         $customServiceUrl = null
 63:     ) {
 64:         $this->setConnection($conn);
 65:         $this->service_type = $type;
 66:         $this->service_name = $name;
 67:         $this->service_region = $region;
 68:         $this->service_url = $customServiceUrl ?: $this->getEndpoint($type, $name, $region, $urltype);
 69:     }
 70:     
 71:     /**
 72:      * Set this service's connection.
 73:      * 
 74:      * @param type $connection
 75:      */
 76:     public function setConnection($connection)
 77:     {
 78:         $this->conn = $connection;
 79:     }
 80:     
 81:     /**
 82:      * Get this service's connection.
 83:      * 
 84:      * @return type
 85:      */
 86:     public function getConnection()
 87:     {
 88:         return $this->conn;
 89:     }
 90:     
 91:     /**
 92:      * Returns the URL for the Service
 93:      *
 94:      * @param string $resource optional sub-resource
 95:      * @param array $query optional k/v pairs for query strings
 96:      * @return string
 97:      */
 98:     public function url($resource = '', array $param = array())
 99:     {
100:         $baseurl = $this->service_url;
101: 
102:         // use strlen instead of boolean test because '0' is a valid name
103:         if (strlen($resource) > 0) {
104:             $baseurl = Lang::noslash($baseurl).'/'.$resource;
105:         }
106: 
107:         if (!empty($param)) {
108:             $baseurl .= '?'.$this->MakeQueryString($param);
109:         }
110: 
111:         return $baseurl;
112:     }
113: 
114:     /**
115:      * Returns the /extensions for the service
116:      *
117:      * @api
118:      * @return array of objects
119:      */
120:     public function extensions()
121:     {
122:         $ext = $this->getMetaUrl('extensions');
123:         return (is_object($ext) && isset($ext->extensions)) ? $ext->extensions : array();
124:     }
125: 
126:     /**
127:      * Returns the /limits for the service
128:      *
129:      * @api
130:      * @return array of limits
131:      */
132:     public function limits()
133:     {
134:         $limits = $this->getMetaUrl('limits');
135:         return (is_object($limits)) ? $limits->limits : array();
136:     }
137: 
138:     /**
139:      * Performs an authenticated request
140:      *
141:      * This method handles the addition of authentication headers to each
142:      * request. It always adds the X-Auth-Token: header and will add the
143:      * X-Auth-Project-Id: header if there is a tenant defined on the
144:      * connection.
145:      *
146:      * @param string $url The URL of the request
147:      * @param string $method The HTTP method (defaults to "GET")
148:      * @param array $headers An associative array of headers
149:      * @param string $body An optional body for POST/PUT requests
150:      * @return \OpenCloud\HttpResult
151:      */
152:     public function request(
153:         $url,
154:         $method = 'GET',
155:         array $headers = array(),
156:         $body = null
157:     ) {
158: 
159:         $headers['X-Auth-Token'] = $this->conn->Token();
160: 
161:         if ($tenant = $this->conn->Tenant()) {
162:             $headers['X-Auth-Project-Id'] = $tenant;
163:         }
164:         
165:         return $this->conn->request($url, $method, $headers, $body);
166:     }
167: 
168:     /**
169:      * returns a collection of objects
170:      *
171:      * @param string $class the class of objects to fetch
172:      * @param string $url (optional) the URL to retrieve
173:      * @param mixed $parent (optional) the parent service/object
174:      * @return OpenCloud\Common\Collection
175:      */
176:     public function collection($class, $url = null, $parent = null)
177:     {
178:         // Set the element names
179:         $collectionName = $class::JsonCollectionName();
180:         $elementName    = $class::JsonCollectionElement();
181: 
182:         // Set the parent if empty
183:         if (!$parent) {
184:             $parent = $this;
185:         }
186: 
187:         // Set the URL if empty
188:         if (!$url) {
189:             $url = $parent->url($class::ResourceName());
190:         }
191: 
192:         // Save debug info
193:         $this->getLogger()->info(
194:             '{class}:Collection({url}, {collectionClass}, {collectionName})',
195:             array(
196:                 'class' => get_class($this),
197:                 'url'   => $url,
198:                 'collectionClass' => $class,
199:                 'collectionName'  => $collectionName
200:             )
201:         );
202: 
203:         // Fetch the list
204:         $response = $this->request($url);
205:         
206:         $this->getLogger()->info('Response {status} [{body}]', array(
207:             'status' => $response->httpStatus(),
208:             'body'   => $response->httpBody()
209:         ));
210:         
211:         // Check return code
212:         if ($response->httpStatus() > 204) {
213:             throw new Exceptions\CollectionError(sprintf(
214:                 Lang::translate('Unable to retrieve [%s] list from [%s], status [%d] response [%s]'),
215:                 $class,
216:                 $url,
217:                 $response->httpStatus(),
218:                 $response->httpBody()
219:             ));
220:         }
221:         
222:         // Handle empty response
223:         if (strlen($response->httpBody()) == 0) {
224:             return new Collection($parent, $class, array());
225:         }
226: 
227:         // Parse the return
228:         $object = json_decode($response->httpBody());
229:         $this->checkJsonError();
230:         
231:         // See if there's a "next" link
232:         // Note: not sure if the current API offers links as top-level structures;
233:         //       might have to refactor to allow $nextPageUrl as method argument
234:         // @codeCoverageIgnoreStart
235:         if (isset($object->links) && is_array($object->links)) {
236:             foreach($object->links as $link) {
237:                 if (isset($link->rel) && $link->rel == 'next') {
238:                     if (isset($link->href)) {
239:                         $nextPageUrl = $link->href;
240:                     } else {
241:                         $this->getLogger()->warning(
242:                             'Unexpected [links] found with no [href]'
243:                         );
244:                     }
245:                 }
246:             }
247:         }
248:         // @codeCoverageIgnoreEnd
249:         
250:         // How should we populate the collection?
251:         $data = array();
252: 
253:         if (!$collectionName) {
254:             // No element name, just a plain object
255:             // @codeCoverageIgnoreStart
256:             $data = $object;
257:             // @codeCoverageIgnoreEnd
258:         } elseif (isset($object->$collectionName)) {
259:             if (!$elementName) {
260:                 // The object has a top-level collection name only
261:                 $data = $object->$collectionName;
262:             } else {
263:                 // The object has element levels which need to be iterated over
264:                 $data = array();
265:                 foreach($object->$collectionName as $item) {
266:                     $subValues = $item->$elementName;
267:                     unset($item->$elementName);
268:                     $data[] = array_merge((array)$item, (array)$subValues);
269:                 }
270:             }
271:         }
272:         
273:         $collectionObject = new Collection($parent, $class, $data);
274:         
275:         // if there's a $nextPageUrl, then we need to establish a callback
276:         // @codeCoverageIgnoreStart
277:         if (!empty($nextPageUrl)) {
278:             $collectionObject->setNextPageCallback(array($this, 'Collection'), $nextPageUrl);
279:         }
280:         // @codeCoverageIgnoreEnd
281: 
282:         return $collectionObject;
283:     }
284: 
285:     /**
286:      * returns the Region associated with the service
287:      *
288:      * @api
289:      * @return string
290:      */
291:     public function region()
292:     {
293:         return $this->service_region;
294:     }
295: 
296:     /**
297:      * returns the serviceName associated with the service
298:      *
299:      * This is used by DNS for PTR record lookups
300:      *
301:      * @api
302:      * @return string
303:      */
304:     public function name()
305:     {
306:         return $this->service_name;
307:     }
308: 
309:     /**
310:      * Returns a list of supported namespaces
311:      *
312:      * @return array
313:      */
314:     public function namespaces()
315:     {
316:         return (isset($this->_namespaces) && is_array($this->_namespaces)) ? $this->_namespaces : array();
317:     }
318: 
319:     /**
320:      * Given a service type, name, and region, return the url
321:      *
322:      * This function ensures that services are represented by an entry in the
323:      * service catalog, and NOT by an arbitrarily-constructed URL.
324:      *
325:      * Note that it will always return the first match found in the
326:      * service catalog (there *should* be only one, but you never know...)
327:      *
328:      * @param string $type The OpenStack service type ("compute" or
329:      *      "object-store", for example
330:      * @param string $name The name of the service in the service catlog
331:      * @param string $region The region of the service
332:      * @param string $urltype The URL type; defaults to "publicURL"
333:      * @return string The URL of the service
334:      */
335:     private function getEndpoint($type, $name, $region, $urltype = 'publicURL')
336:     {
337:         $catalog = $this->getConnection()->serviceCatalog();
338: 
339:         // Search each service to find The One
340:         foreach ($catalog as $service) {
341:             // Find the service by comparing the type ("compute") and name ("openstack")
342:             if (!strcasecmp($service->type, $type) && !strcasecmp($service->name, $name)) {
343:                 foreach($service->endpoints as $endpoint) {
344:                     // Only set the URL if:
345:                     // a. It is a regionless service (i.e. no region key set)
346:                     // b. The region matches the one we want
347:                     if (isset($endpoint->$urltype) && 
348:                         (!isset($endpoint->region) || !strcasecmp($endpoint->region, $region))
349:                     ) {
350:                         $url = $endpoint->$urltype;
351:                     }
352:                 }
353:             }
354:         }
355: 
356:         // error if not found
357:         if (empty($url)) {
358:             throw new Exceptions\EndpointError(sprintf(
359:                 'No endpoints for service type [%s], name [%s], region [%s] and urlType [%s]',
360:                 $type,
361:                 $name,
362:                 $region,
363:                 $urltype
364:             ));
365:         }
366:         
367:         return $url;
368:     }
369: 
370:     /**
371:      * Constructs a specified URL from the subresource
372:      *
373:      * Given a subresource (e.g., "extensions"), this constructs the proper
374:      * URL and retrieves the resource.
375:      *
376:      * @param string $resource The resource requested; should NOT have slashes
377:      *      at the beginning or end
378:      * @return \stdClass object
379:      */
380:     private function getMetaUrl($resource)
381:     {
382:         $urlBase = $this->getEndpoint(
383:             $this->service_type,
384:             $this->service_name,
385:             $this->service_region,
386:             RAXSDK_URL_PUBLIC
387:         );
388: 
389:         $url = Lang::noslash($urlBase) . '/' . $resource;
390: 
391:         $response = $this->request($url);
392: 
393:         // check for NOT FOUND response
394:         if ($response->httpStatus() == 404) {
395:             return array();
396:         }
397: 
398:         // @codeCoverageIgnoreStart
399:         if ($response->httpStatus() >= 300) {
400:             throw new Exceptions\HttpError(sprintf(
401:                 Lang::translate('Error accessing [%s] - status [%d], response [%s]'),
402:                 $urlBase,
403:                 $response->httpStatus(),
404:                 $response->httpBody()
405:             ));
406:         }
407:         // @codeCoverageIgnoreEnd
408: 
409:         // we're good; proceed
410:         $object = json_decode($response->httpBody());
411: 
412:         $this->checkJsonError();
413: 
414:         return $object;
415:     }
416:     
417:     /**
418:      * Get all associated resources for this service.
419:      * 
420:      * @access public
421:      * @return void
422:      */
423:     public function getResources()
424:     {
425:         return $this->resources;
426:     }
427: 
428:     /**
429:      * Internal method for accessing child namespace from parent scope.
430:      * 
431:      * @return type
432:      */
433:     protected function getCurrentNamespace()
434:     {
435:         $namespace = get_class($this);
436:         return substr($namespace, 0, strrpos($namespace, '\\'));
437:     }
438:     
439:     /**
440:      * Resolves fully-qualified classname for associated local resource.
441:      * 
442:      * @param  string $resourceName
443:      * @return string
444:      */
445:     protected function resolveResourceClass($resourceName)
446:     {
447:         $className = substr_count($resourceName, '\\') 
448:             ? $resourceName 
449:             : $this->getCurrentNamespace() . '\\Resource\\' . ucfirst($resourceName);
450:         
451:         if (!class_exists($className)) {
452:             throw new Exceptions\UnrecognizedServiceError(sprintf(
453:                 '%s resource does not exist, please try one of the following: %s', 
454:                 $resourceName, 
455:                 implode(', ', $this->getResources())
456:             ));
457:         }
458:         
459:         return $className;
460:     }
461:     
462:     /**
463:      * Factory method for instantiating resource objects.
464:      * 
465:      * @access public
466:      * @param  string $resourceName
467:      * @param  mixed $info (default: null)
468:      * @return object
469:      */
470:     public function resource($resourceName, $info = null)
471:     {
472:         $className = $this->resolveResourceClass($resourceName);
473:         return new $className($this, $info);
474:     }
475:     
476:     /**
477:      * Factory method for instantiate a resource collection.
478:      * 
479:      * @param  string $resourceName
480:      * @param  string|null $url
481:      * @return Collection
482:      */
483:     public function resourceList($resourceName, $url = null, $service = null)
484:     {
485:         $className = $this->resolveResourceClass($resourceName);
486:         return $this->collection($className, $url, $service);
487:     }
488: 
489: }
490: 
PHP OpenCloud API API documentation generated by ApiGen 2.8.0