Overview

Namespaces

  • OpenCloud
    • Autoscale
      • Resource
    • CloudMonitoring
      • Exception
      • Resource
    • Common
      • Collection
      • Constants
      • Exceptions
      • Http
        • Message
      • Identity
      • Log
      • Service
    • Compute
      • Constants
      • Exception
      • Resource
    • Database
      • Resource
    • DNS
      • Resource
    • LoadBalancer
      • Resource
    • ObjectStore
      • Constants
      • Exception
      • Resource
      • Upload
    • Orchestration
    • Queues
      • Exception
      • Resource
    • Volume
      • Resource
  • PHP

Classes

  • Base
  • Lang
  • Metadata
  • PersistentObject
  • Overview
  • Namespace
  • Class
  • Tree
  • Download
  1: <?php
  2: /**
  3:  * PHP OpenCloud library
  4:  * 
  5:  * @copyright 2013 Rackspace Hosting, Inc. See LICENSE for information.
  6:  * @license   https://www.apache.org/licenses/LICENSE-2.0
  7:  * @author    Glen Campbell <glen.campbell@rackspace.com>
  8:  * @author    Jamie Hannaford <jamie.hannaford@rackspace.com>
  9:  */
 10: 
 11: namespace OpenCloud\Common;
 12: 
 13: use Guzzle\Http\Exception\BadResponseException;
 14: use Guzzle\Http\Message\Response;
 15: use Guzzle\Http\Url;
 16: use OpenCloud\Common\Constants\State as StateConst;
 17: use OpenCloud\Common\Service\AbstractService;
 18: use OpenCloud\Common\Exceptions\RuntimeException;
 19: use OpenCloud\Common\Http\Message\Formatter;
 20: 
 21: /**
 22:  * Represents an object that can be retrieved, created, updated and deleted.
 23:  *
 24:  * This class abstracts much of the common functionality between: 
 25:  *  
 26:  *  * Nova servers;
 27:  *  * Swift containers and objects;
 28:  *  * DBAAS instances;
 29:  *  * Cinder volumes;
 30:  *  * and various other objects that:
 31:  *    * have a URL;
 32:  *    * can be created, updated, deleted, or retrieved;
 33:  *    * use a standard JSON format with a top-level element followed by 
 34:  *      a child object with attributes.
 35:  *
 36:  * In general, you can create a persistent object class by subclassing this
 37:  * class and defining some protected, static variables:
 38:  * 
 39:  *  * $url_resource - the sub-resource value in the URL of the parent. For
 40:  *    example, if the parent URL is `http://something/parent`, then setting this
 41:  *    value to "another" would result in a URL for the persistent object of 
 42:  *    `http://something/parent/another`.
 43:  *
 44:  *  * $json_name - the top-level JSON object name. For example, if the
 45:  *    persistent object is represented by `{"foo": {"attr":value, ...}}`, then
 46:  *    set $json_name to "foo".
 47:  *
 48:  *  * $json_collection_name - optional; this value is the name of a collection
 49:  *    of the persistent objects. If not provided, it defaults to `json_name`
 50:  *    with an appended "s" (e.g., if `json_name` is "foo", then
 51:  *    `json_collection_name` would be "foos"). Set this value if the collection 
 52:  *    name doesn't follow this pattern.
 53:  *
 54:  *  * $json_collection_element - the common pattern for a collection is:
 55:  *    `{"collection": [{"attr":"value",...}, {"attr":"value",...}, ...]}`
 56:  *    That is, each element of the array is a \stdClass object containing the
 57:  *    object's attributes. In rare instances, the objects in the array
 58:  *    are named, and `json_collection_element` contains the name of the
 59:  *    collection objects. For example, in this JSON response:
 60:  *    `{"allowedDomain":[{"allowedDomain":{"name":"foo"}}]}`,
 61:  *    `json_collection_element` would be set to "allowedDomain".
 62:  *
 63:  * The PersistentObject class supports the standard CRUD methods; if these are 
 64:  * not needed (i.e. not supported by  the service), the subclass should redefine 
 65:  * these to call the `noCreate`, `noUpdate`, or `noDelete` methods, which will 
 66:  * trigger an appropriate exception. For example, if an object cannot be created:
 67:  *
 68:  *    function create($params = array()) 
 69:  *    { 
 70:  *       $this->noCreate(); 
 71:  *    }
 72:  */
 73: abstract class PersistentObject extends Base
 74: {
 75:       
 76:     private $service;
 77:     private $parent;
 78:     protected $metadata;
 79: 
 80:     /**
 81:      * Retrieves the instance from persistent storage
 82:      *
 83:      * @param mixed $service The service object for this resource
 84:      * @param mixed $info    The ID or array/object of data
 85:      */
 86:     public function __construct($service = null, $info = null)
 87:     {
 88:         if ($service instanceof AbstractService) {
 89:             $this->setService($service);
 90:         }
 91:         
 92:         $this->metadata = new Metadata;
 93: 
 94:         $this->populate($info);
 95:     }
 96:             
 97:     /**
 98:      * Sets the service associated with this resource object.
 99:      * 
100:      * @param OpenCloud\Common\Service\AbstractService $service
101:      */
102:     public function setService(AbstractService $service)
103:     {
104:         $this->service = $service;
105:         return $this;
106:     }
107:     
108:     /**
109:      * Returns the service object for this resource; required for making
110:      * requests, etc. because it has direct access to the Connection.
111:      * 
112:      * @return OpenCloud\Common\Service\AbstractService
113:      */
114:     public function getService()
115:     {
116:         if (null === $this->service) {
117:             throw new Exceptions\ServiceException(
118:                 'No service defined'
119:             );
120:         }
121:         return $this->service;
122:     }
123:     
124:     /**
125:      * Set the parent object for this resource.
126:      * 
127:      * @param \OpenCloud\Common\PersistentObject $parent
128:      */
129:     public function setParent(PersistentObject $parent)
130:     {
131:         $this->parent = $parent;
132:         return $this;
133:     }
134:     
135:     /**
136:      * Returns the parent.
137:      * 
138:      * @return \OpenCloud\Common\PersistentObject
139:      */
140:     public function getParent()
141:     {
142:         if (null === $this->parent) {
143:             $this->parent = $this->getService();
144:         }
145:         return $this->parent;
146:     }
147:         
148:     public function getClient()
149:     {
150:         return $this->getService()->getClient();
151:     }
152:     
153:     public function setMetadata($metadata)
154:     {
155:         $this->metadata = $metadata;
156:         
157:         return $this;
158:     }
159:     
160:     public function getMetadata()
161:     {
162:         return $this->metadata;
163:     }
164: 
165:     /**
166:      * Creates a new object
167:      *
168:      * @param array $params array of values to set when creating the object
169:      * @return HttpResponse
170:      * @throws VolumeCreateError if HTTP status is not Success
171:      */
172:     public function create($params = array())
173:     {
174:         // set parameters
175:         if (!empty($params)) {
176:             $this->populate($params, false);
177:         }
178: 
179:         // construct the JSON
180:         $json = json_encode($this->createJson());
181:         $this->checkJsonError();
182: 
183:         $createUrl = $this->createUrl();
184: 
185:         $response = $this->getClient()->post($createUrl, array(), $json)->send();
186: 
187:         // We have to try to parse the response body first because it should have precedence over a Location refresh.
188:         // I'd like to reverse the order, but Nova instances return ephemeral properties on creation which are not
189:         // available when you follow the Location link...
190:         if (null !== ($decoded = $this->parseResponse($response))) {
191:             $this->populate($decoded);
192:         } elseif ($location = $response->getHeader('Location')) {
193:             $this->refreshFromLocationUrl($location);
194:         }
195: 
196:         return $response;
197:     }
198: 
199:     public function refreshFromLocationUrl($url)
200:     {
201:         $fullUrl = Url::factory($url);
202: 
203:         $response = $this->getClient()->get($fullUrl)->send();
204: 
205:         if (null !== ($decoded = $this->parseResponse($response))) {
206:             $this->populate($decoded);
207:         }
208:     }
209: 
210:     /**
211:      * Updates an existing object
212:      *
213:      * @api
214:      * @param array $params array of values to set when updating the object
215:      * @return HttpResponse
216:      * @throws VolumeCreateError if HTTP status is not Success
217:      */
218:     public function update($params = array())
219:     {
220:         // set parameters
221:         if (!empty($params)) {
222:             $this->populate($params);
223:         }
224: 
225:         // debug
226:         $this->getLogger()->info('{class}::Update({name})', array(
227:             'class' => get_class($this),
228:             'name'  => $this->getProperty($this->primaryKeyField())
229:         ));
230: 
231:         // construct the JSON
232:         $json = json_encode($this->updateJson($params));
233:         $this->checkJsonError();
234: 
235:         // send the request
236:         return $this->getClient()->put($this->getUrl(), array(), $json)->send();
237:     }
238: 
239:     /**
240:      * Refreshes the object from the origin (useful when the server is
241:      * changing states)
242:      *
243:      * @return void
244:      * @throws IdRequiredError
245:      */
246:     public function refresh($id = null, $url = null)
247:     {
248:         $primaryKey = $this->primaryKeyField();
249:         $primaryKeyVal = $this->getProperty($primaryKey);
250:         
251:         if (!$url) {
252:             
253:             if (!$id = $id ?: $primaryKeyVal) {
254:                 throw new Exceptions\IdRequiredError(sprintf(
255:                     Lang::translate("%s has no %s; cannot be refreshed"),
256:                     get_class($this),
257:                     $primaryKey
258:                 ));
259:             }
260:             
261:             if ($primaryKeyVal != $id) {
262:                 $this->setProperty($primaryKey, $id);
263:             }
264:             
265:             $url = $this->getUrl();
266:         }
267:         
268:         // reset status, if available
269:         if ($this->getProperty('status')) {
270:             $this->setProperty('status', null);
271:         }
272: 
273:         $response = $this->getClient()->get($url)->send();
274:   
275:         if (null !== ($decoded = $this->parseResponse($response))) {
276:             $this->populate($decoded);
277:         }
278:         
279:         return $response;
280:     }
281:     
282:     /**
283:      * Deletes an object
284:      *
285:      * @api
286:      * @return HttpResponse
287:      * @throws DeleteError if HTTP status is not Success
288:      */
289:     public function delete()
290:     {
291:         $this->getLogger()->info('{class}::Delete()', array('class' => get_class($this)));
292: 
293:         // send the request
294:         return $this->getClient()->delete($this->getUrl())->send();
295:     }
296:     
297:     /**
298:      * Returns the default URL of the object
299:      *
300:      * This may have to be overridden in subclasses.
301:      *
302:      * @param string $subresource optional sub-resource string
303:      * @param array $qstr optional k/v pairs for query strings
304:      * @return string
305:      */
306:     public function url($path = null, array $query = array())
307:     {
308:         return $this->getUrl($path, $query);
309:     }
310:     
311:     public function getUrl($path = null, array $query = array())
312:     {
313:         if (!$url = $this->findLink('self')) {
314:             
315:             // ...otherwise construct a URL from parent and this resource's
316:             // "URL name". If no name is set, resourceName() throws an error.
317:             $url = $this->getParent()->getUrl($this->resourceName());
318:             
319:             // Does it have a primary key?
320:             if (null !== ($primaryKey = $this->getProperty($this->primaryKeyField()))) {
321:                 $url->addPath($primaryKey);
322:             }
323:         }
324: 
325:         if (!$url instanceof Url) {
326:             $url = Url::factory($url);
327:         }
328:         
329:         return $url->addPath($path)->setQuery($query);
330:     }
331: 
332:     /**
333:      * Waits for the server/instance status to change
334:      *
335:      * This function repeatedly polls the system for a change in server
336:      * status. Once the status reaches the `$terminal` value (or 'ERROR'),
337:      * then the function returns.
338:      *
339:      * @api
340:      * @param string $terminal the terminal state to wait for
341:      * @param integer $timeout the max time (in seconds) to wait
342:      * @param callable $callback a callback function that is invoked with
343:      *      each repetition of the polling sequence. This can be used, for
344:      *      example, to update a status display or to permit other operations
345:      *      to continue
346:      * @return void
347:      * @codeCoverageIgnore
348:      */
349:     public function waitFor($state = null, $timeout = null, $callback = null, $interval = null)
350:     {
351:         $state    = $state ?: StateConst::ACTIVE;
352:         $timeout  = $timeout ?: StateConst::DEFAULT_TIMEOUT;
353:         $interval = $interval ?: StateConst::DEFAULT_INTERVAL;
354: 
355:         // save stats
356:         $startTime = time();
357:         
358:         $states = array('ERROR', $state);
359:         
360:         while (true) {
361:             
362:             $this->refresh($this->getProperty($this->primaryKeyField()));
363:             
364:             if ($callback) {
365:                 call_user_func($callback, $this);
366:             }
367:             
368:             if (in_array($this->status(), $states) || (time() - $startTime) > $timeout) {
369:                 return;
370:             }
371:             
372:             sleep($interval);
373:         }
374:     }
375:     
376:     /**
377:      * Sends the json string to the /action resource
378:      *
379:      * This is used for many purposes, such as rebooting the server,
380:      * setting the root password, creating images, etc.
381:      * Since it can only be used on a live server, it checks for a valid ID.
382:      *
383:      * @param $object - this will be encoded as json, and we handle all the JSON
384:      *     error-checking in one place
385:      * @throws ServerIdError if server ID is not defined
386:      * @throws ServerActionError on other errors
387:      * @returns boolean; TRUE if successful, FALSE otherwise
388:      */
389:     protected function action($object)
390:     {
391:         if (!$this->getProperty($this->primaryKeyField())) {
392:             throw new Exceptions\IdRequiredError(sprintf(
393:                 Lang::translate('%s is not defined'),
394:                 get_class($this),
395:                 $this->primaryKeyField()
396:             ));
397:         }
398: 
399:         if (!is_object($object)) {
400:             throw new Exceptions\ServerActionError(sprintf(
401:                 Lang::translate('%s::Action() requires an object as its parameter'),
402:                 get_class($this)
403:             ));
404:         }
405: 
406:         // convert the object to json
407:         $json = json_encode($object);
408:         $this->checkJsonError();
409: 
410:         // debug - save the request
411:         $this->getLogger()->info(Lang::translate('{class}::action [{json}]'), array(
412:             'class' => get_class($this), 
413:             'json'  => $json
414:         ));
415: 
416:         // get the URL for the POST message
417:         $url = $this->url('action');
418: 
419:         // POST the message
420:         return $this->getClient()->post($url, array(), $json)->send();
421:     }
422: 
423:      /**
424:      * Returns an object for the Create() method JSON
425:      * Must be overridden in a child class.
426:      *
427:      * @throws CreateError if not overridden
428:      */
429:     protected function createJson()
430:     {
431:         if (!isset($this->createKeys)) {
432:             throw new RuntimeException(sprintf(
433:                 'This resource object [%s] must have a visible createKeys array',
434:                 get_class($this)
435:             ));
436:         }
437:         
438:         $element = (object) array();
439: 
440:         foreach ($this->createKeys as $key) {
441:             if (null !== ($property = $this->getProperty($key))) {
442:                 $element->$key = $property;
443:             }
444:         }
445: 
446:         if (!empty($this->metadata)) {
447:             $element->metadata = (object) $this->metadata->toArray();
448:         }
449: 
450:         return (object) array($this->jsonName() => (object) $element);
451:     }
452: 
453:     /**
454:      * Returns an object for the Update() method JSON
455:      * Must be overridden in a child class.
456:      *
457:      * @throws UpdateError if not overridden
458:      */
459:     protected function updateJson($params = array())
460:     {
461:         throw new Exceptions\UpdateError(sprintf(
462:             Lang::translate('[%s] UpdateJson() must be overridden'),
463:             get_class($this)
464:         ));
465:     }
466: 
467:     /**
468:      * throws a CreateError for subclasses that don't support Create
469:      *
470:      * @throws CreateError
471:      */
472:     protected function noCreate()
473:     {
474:         throw new Exceptions\CreateError(sprintf(
475:             Lang::translate('[%s] does not support Create()'),
476:             get_class($this)
477:         ));
478:     }
479: 
480:     /**
481:      * throws a DeleteError for subclasses that don't support Delete
482:      *
483:      * @throws DeleteError
484:      */
485:     protected function noDelete()
486:     {
487:         throw new Exceptions\DeleteError(sprintf(
488:             Lang::translate('[%s] does not support Delete()'),
489:             get_class($this)
490:         ));
491:     }
492: 
493:     /**
494:      * throws a UpdateError for subclasses that don't support Update
495:      *
496:      * @throws UpdateError
497:      */
498:     protected function noUpdate()
499:     {
500:         throw new Exceptions\UpdateError(sprintf(
501:             Lang::translate('[%s] does not support Update()'),
502:             get_class($this)
503:         ));
504:     }
505:         
506:     /**
507:      * Returns the displayable name of the object
508:      *
509:      * Can be overridden by child objects; *must* be overridden by child
510:      * objects if the object does not have a `name` attribute defined.
511:      *
512:      * @api
513:      * @return string
514:      * @throws NameError if attribute 'name' is not defined
515:      */
516:     public function name()
517:     {
518:         if (null !== ($name = $this->getProperty('name'))) {
519:             return $name;
520:         } else {
521:             throw new Exceptions\NameError(sprintf(
522:                 Lang::translate('Name attribute does not exist for [%s]'),
523:                 get_class($this)
524:             ));
525:         }
526:     }
527: 
528:     /**
529:      * returns the object's status or `N/A` if not available
530:      *
531:      * @api
532:      * @return string
533:      */
534:     public function status()
535:     {
536:         return (isset($this->status)) ? $this->status : 'N/A';
537:     }
538: 
539:     /**
540:      * returns the object's identifier
541:      *
542:      * Can be overridden by a child class if the identifier is not in the
543:      * `$id` property. Use of this function permits the `$id` attribute to
544:      * be protected or private to prevent unauthorized overwriting for
545:      * security.
546:      *
547:      * @api
548:      * @return string
549:      */
550:     public function id()
551:     {
552:         return $this->id;
553:     }
554: 
555:     /**
556:      * checks for `$alias` in extensions and throws an error if not present
557:      *
558:      * @throws UnsupportedExtensionError
559:      */
560:     public function checkExtension($alias)
561:     {
562:         if (!in_array($alias, $this->getService()->namespaces())) {
563:             throw new Exceptions\UnsupportedExtensionError(sprintf(
564:                 Lang::translate('Extension [%s] is not installed'),
565:                 $alias
566:             ));
567:         }
568:         
569:         return true;
570:     }
571: 
572:     /**
573:      * returns the region associated with the object
574:      *
575:      * navigates to the parent service to determine the region.
576:      *
577:      * @api
578:      */
579:     public function region()
580:     {
581:         return $this->getService()->Region();
582:     }
583:     
584:     /**
585:      * Since each server can have multiple links, this returns the desired one
586:      *
587:      * @param string $type - 'self' is most common; use 'bookmark' for
588:      *      the version-independent one
589:      * @return string the URL from the links block
590:      */
591:     public function findLink($type = 'self')
592:     {
593:         if (empty($this->links)) {
594:             return false;
595:         }
596: 
597:         foreach ($this->links as $link) {
598:             if ($link->rel == $type) {
599:                 return $link->href;
600:             }
601:         }
602: 
603:         return false;
604:     }
605: 
606:     /**
607:      * returns the URL used for Create
608:      *
609:      * @return string
610:      */
611:     public function createUrl()
612:     {
613:         return $this->getParent()->getUrl($this->resourceName());
614:     }
615: 
616:     /**
617:      * Returns the primary key field for the object
618:      *
619:      * The primary key is usually 'id', but this function is provided so that
620:      * (in rare cases where it is not 'id'), it can be overridden.
621:      *
622:      * @return string
623:      */
624:     protected function primaryKeyField()
625:     {
626:         return 'id';
627:     }
628: 
629:     /**
630:      * Returns the top-level document identifier for the returned response
631:      * JSON document; must be overridden in child classes
632:      *
633:      * For example, a server document is (JSON) `{"server": ...}` and an
634:      * Instance document is `{"instance": ...}` - this function must return
635:      * the top level document name (either "server" or "instance", in
636:      * these examples).
637:      *
638:      * @throws DocumentError if not overridden
639:      */
640:     public static function jsonName()
641:     {
642:         if (isset(static::$json_name)) {
643:             return static::$json_name;
644:         }
645: 
646:         throw new Exceptions\DocumentError(sprintf(
647:             Lang::translate('No JSON object defined for class [%s] in JsonName()'),
648:             get_class()
649:         ));
650:     }
651: 
652:     /**
653:      * returns the collection JSON element name
654:      *
655:      * When an object is returned in a collection, it usually has a top-level
656:      * object that is an array holding child objects of the object types.
657:      * This static function returns the name of the top-level element. Usually,
658:      * that top-level element is simply the JSON name of the resource.'s';
659:      * however, it can be overridden by specifying the $json_collection_name
660:      * attribute.
661:      *
662:      * @return string
663:      */
664:     public static function jsonCollectionName()
665:     {
666:         if (isset(static::$json_collection_name)) {
667:             return static::$json_collection_name;
668:         } else {
669:             return static::$json_name . 's';
670:         }
671:     }
672: 
673:     /**
674:      * returns the JSON name for each element in a collection
675:      *
676:      * Usually, elements in a collection are anonymous; this function, however,
677:      * provides for an element level name:
678:      *
679:      *  `{ "collection" : [ { "element" : ... } ] }`
680:      *
681:      * @return string
682:      */
683:     public static function jsonCollectionElement()
684:     {
685:         if (isset(static::$json_collection_element)) {
686:             return static::$json_collection_element;
687:         }
688:     }
689: 
690:     /**
691:      * Returns the resource name for the URL of the object; must be overridden
692:      * in child classes
693:      *
694:      * For example, a server is `/servers/`, a database instance is
695:      * `/instances/`. Must be overridden in child classes.
696:      *
697:      * @throws UrlError
698:      */
699:     public static function resourceName()
700:     {
701:         if (isset(static::$url_resource)) {
702:             return static::$url_resource;
703:         }
704: 
705:         throw new Exceptions\UrlError(sprintf(
706:             Lang::translate('No URL resource defined for class [%s] in ResourceName()'),
707:             get_class()
708:         ));
709:     }
710:     
711:     public function parseResponse(Response $response)
712:     {
713:         $body = Formatter::decode($response);
714:         
715:         $top = $this->jsonName();
716:             
717:         if ($top && isset($body->$top)) {
718:             $content = $body->$top;
719:         } else {
720:             $content = $body;
721:         }
722:         
723:         return $content;
724:     }
725: 
726: }
727: 
PHP OpenCloud API API documentation generated by ApiGen 2.8.0