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 Glen Campbell <glen.campbell@rackspace.com>
9: * @author Jamie Hannaford <jamie.hannaford@rackspace.com>
10: */
11:
12: namespace OpenCloud;
13:
14: require_once __DIR__ . '/Globals.php';
15:
16: use OpenCloud\Common\Base;
17: use OpenCloud\Common\Lang;
18: use OpenCloud\Common\Exceptions;
19: use OpenCloud\Common\ServiceCatalogItem;
20:
21: /**
22: * The OpenStack class represents a relationship (or "connection")
23: * between a user and a service.
24: *
25: * This is the primary entry point into an OpenStack system, and the only one
26: * where the developer is required to know and provide the endpoint URL (in
27: * all other cases, the endpoint is derived from the Service Catalog provided
28: * by the authentication system).
29: *
30: * Since various providers have different mechanisms for authentication, users
31: * will often use a subclass of OpenStack. For example, the Rackspace
32: * class is provided for users of Rackspace's cloud services, and other cloud
33: * providers are welcome to add their own subclasses as well.
34: *
35: * General usage example:
36: * <code>
37: * $username = 'My Username';
38: * $secret = 'My Secret';
39: * $connection = new OpenCloud\OpenStack($username, $secret);
40: * // having established the connection, we can set some defaults
41: * // this sets the default name and region of the Compute service
42: * $connection->SetDefaults('Compute', 'cloudServersOpenStack', 'ORD');
43: * // access a Compute service
44: * $chicago = $connection->Compute();
45: * // if we want to access a different service, we can:
46: * $dallas = $connection->Compute('cloudServersOpenStack', 'DFW');
47: * </code>
48: */
49: class OpenStack extends Base
50: {
51:
52: /**
53: * This holds the HTTP User-Agent: used for all requests to the services. It
54: * is public so that, if necessary, it can be entirely overridden by the
55: * developer. However, it's strongly recomended that you use the
56: * appendUserAgent() method to APPEND your own User Agent identifier to the
57: * end of this string; the user agent information can be very valuable to
58: * service providers to track who is using their service.
59: *
60: * @var string
61: */
62: public $useragent = RAXSDK_USER_AGENT;
63:
64: protected $url;
65: protected $secret = array();
66: protected $token;
67: protected $expiration = 0;
68: protected $tenant;
69: protected $catalog;
70: protected $connectTimeout = RAXSDK_CONNECTTIMEOUT;
71: protected $httpTimeout = RAXSDK_TIMEOUT;
72: protected $overlimitTimeout = RAXSDK_OVERLIMIT_TIMEOUT;
73:
74: /**
75: * This associative array holds default values used to identify each
76: * service (and to select it from the Service Catalog). Use the
77: * Compute::SetDefaults() method to change the default values, or
78: * define the global constants (for example, RAXSDK_COMPUTE_NAME)
79: * BEFORE loading the OpenCloud library:
80: *
81: * <code>
82: * define('RAXSDK_COMPUTE_NAME', 'cloudServersOpenStack');
83: * include('openstack.php');
84: * </code>
85: */
86: protected $defaults = array(
87: 'Compute' => array(
88: 'name' => RAXSDK_COMPUTE_NAME,
89: 'region' => RAXSDK_COMPUTE_REGION,
90: 'urltype' => RAXSDK_COMPUTE_URLTYPE
91: ),
92: 'ObjectStore' => array(
93: 'name' => RAXSDK_OBJSTORE_NAME,
94: 'region' => RAXSDK_OBJSTORE_REGION,
95: 'urltype' => RAXSDK_OBJSTORE_URLTYPE
96: ),
97: 'Database' => array(
98: 'name' => RAXSDK_DATABASE_NAME,
99: 'region' => RAXSDK_DATABASE_REGION,
100: 'urltype' => RAXSDK_DATABASE_URLTYPE
101: ),
102: 'Volume' => array(
103: 'name' => RAXSDK_VOLUME_NAME,
104: 'region' => RAXSDK_VOLUME_REGION,
105: 'urltype' => RAXSDK_VOLUME_URLTYPE
106: ),
107: 'LoadBalancer' => array(
108: 'name' => RAXSDK_LBSERVICE_NAME,
109: 'region' => RAXSDK_LBSERVICE_REGION,
110: 'urltype' => RAXSDK_LBSERVICE_URLTYPE
111: ),
112: 'DNS' => array(
113: 'name' => RAXSDK_DNS_NAME,
114: 'region' => RAXSDK_DNS_REGION,
115: 'urltype' => RAXSDK_DNS_URLTYPE
116: ),
117: 'Orchestration' => array(
118: 'name' => RAXSDK_ORCHESTRATION_NAME,
119: 'region' => RAXSDK_ORCHESTRATION_REGION,
120: 'urltype' => RAXSDK_ORCHESTRATION_URLTYPE
121: ),
122: 'CloudMonitoring' => array(
123: 'name' => RAXSDK_MONITORING_NAME,
124: 'region' => RAXSDK_MONITORING_REGION,
125: 'urltype' => RAXSDK_MONITORING_URLTYPE
126: ),
127: 'Autoscale' => array(
128: 'name' => RAXSDK_AUTOSCALE_NAME,
129: 'region' => RAXSDK_AUTOSCALE_REGION,
130: 'urltype' => RAXSDK_AUTOSCALE_URLTYPE
131: )
132: );
133:
134: private $_user_write_progress_callback_func;
135: private $_user_read_progress_callback_func;
136:
137: /**
138: * Tracks file descriptors used by streaming downloads
139: *
140: * This will permit multiple simultaneous streaming downloads; the
141: * key is the URL of the object, and the value is its file descriptor.
142: *
143: * To prevent memory overflows, each array element is deleted when
144: * the end of the file is reached.
145: */
146: private $fileDescriptors = array();
147:
148: /**
149: * array of options to pass to the CURL request object
150: */
151: private $curlOptions = array();
152:
153: /**
154: * list of attributes to export/import
155: */
156: private $exportItems = array(
157: 'token',
158: 'expiration',
159: 'tenant',
160: 'catalog'
161: );
162:
163: /**
164: * Creates a new OpenStack object
165: *
166: * The OpenStack object needs two bits of information: the URL to
167: * authenticate against, and a "secret", which is an associative array
168: * of name/value pairs. Usually, the secret will be a username and a
169: * password, but other values may be required by different authentication
170: * systems. For example, OpenStack Keystone requires a username and
171: * password, but Rackspace uses a username, tenant ID, and API key.
172: * (See OpenCloud\Rackspace for that.)
173: *
174: * @param string $url - the authentication endpoint URL
175: * @param array $secret - an associative array of auth information:
176: * * username
177: * * password
178: * @param array $options - CURL options to pass to the HttpRequest object
179: */
180: public function __construct($url, array $secret, array $options = array())
181: {
182: // check for supported version
183: // @codeCoverageIgnoreStart
184: $version = phpversion();
185: if ($version < '5.3.1') {
186: throw new Exceptions\UnsupportedVersionError(sprintf(
187: Lang::translate('PHP version [%s] is not supported'),
188: $version
189: ));
190: }
191: // @codeCoverageIgnoreEnd
192:
193: // Start processing
194: $this->getLogger()->info(Lang::translate('Initializing OpenStack client'));
195:
196: // Set properties
197: $this->setUrl($url);
198: $this->setSecret($secret);
199: $this->setCurlOptions($options);
200: }
201:
202: /**
203: * Set user agent.
204: *
205: * @param string $useragent
206: * @return OpenCloud\OpenStack
207: */
208: public function setUserAgent($useragent)
209: {
210: $this->useragent = $useragent;
211:
212: return $this;
213: }
214:
215: /**
216: * Allows the user to append a user agent string
217: *
218: * Programs that are using these bindings are encouraged to add their
219: * user agent to the one supplied by this SDK. This will permit cloud
220: * providers to track users so that they can provide better service.
221: *
222: * @param string $agent an arbitrary user-agent string; e.g. "My Cloud App"
223: * @return OpenCloud\OpenStack
224: */
225: public function appendUserAgent($useragent)
226: {
227: $this->useragent .= ';' . $useragent;
228:
229: return $this;
230: }
231:
232: /**
233: * Get user agent.
234: *
235: * @return string
236: */
237: public function getUserAgent()
238: {
239: return $this->useragent;
240: }
241:
242: /**
243: * Sets the URL which the client will access.
244: *
245: * @param string $url
246: * @return OpenCloud\OpenStack
247: */
248: public function setUrl($url)
249: {
250: $this->url = $url;
251:
252: return $this;
253: }
254:
255: /**
256: * Get the URL.
257: *
258: * @return string
259: */
260: public function getUrl()
261: {
262: return $this->url;
263: }
264:
265: /**
266: * Set the secret for the client.
267: *
268: * @param array $secret
269: * @return OpenCloud\OpenStack
270: */
271: public function setSecret(array $secret = array())
272: {
273: $this->secret = $secret;
274:
275: return $this;
276: }
277:
278: /**
279: * Get the secret.
280: *
281: * @return array
282: */
283: public function getSecret()
284: {
285: return $this->secret;
286: }
287:
288: /**
289: * Set the token for this client.
290: *
291: * @param string $token
292: * @return OpenCloud\OpenStack
293: */
294: public function setToken($token)
295: {
296: $this->token = $token;
297:
298: return $this;
299: }
300:
301: /**
302: * Get the token for this client.
303: *
304: * @return string
305: */
306: public function getToken()
307: {
308: return $this->token;
309: }
310:
311: /**
312: * Set the expiration for this token.
313: *
314: * @param int $expiration
315: * @return OpenCloud\OpenStack
316: */
317: public function setExpiration($expiration)
318: {
319: $this->expiration = $expiration;
320:
321: return $this;
322: }
323:
324: /**
325: * Get the expiration time.
326: *
327: * @return int
328: */
329: public function getExpiration()
330: {
331: return $this->expiration;
332: }
333:
334: /**
335: * Set the tenant for this client.
336: *
337: * @param string $tenant
338: * @return OpenCloud\OpenStack
339: */
340: public function setTenant($tenant)
341: {
342: $this->tenant = $tenant;
343:
344: return $this;
345: }
346:
347: /**
348: * Get the tenant for this client.
349: *
350: * @return string
351: */
352: public function getTenant()
353: {
354: return $this->tenant;
355: }
356:
357: /**
358: * Set the service catalog.
359: *
360: * @param mixed $catalog
361: * @return OpenCloud\OpenStack
362: */
363: public function setCatalog($catalog)
364: {
365: $this->catalog = $catalog;
366:
367: return $this;
368: }
369:
370: /**
371: * Get the service catalog.
372: *
373: * @return array
374: */
375: public function getCatalog()
376: {
377: return $this->catalog;
378: }
379:
380: /**
381: * Set (all) the cURL options.
382: *
383: * @param array $options
384: * @return OpenCloud\OpenStack
385: */
386: public function setCurlOptions(array $options)
387: {
388: $this->curlOptions = $options;
389:
390: return $this;
391: }
392:
393: /**
394: * Get the cURL options.
395: *
396: * @return array
397: */
398: public function getCurlOptions()
399: {
400: return $this->curlOptions;
401: }
402:
403: /**
404: * Set a specific file descriptor (associated with a URL)
405: *
406: * @param string $key
407: * @param resource $value
408: * @return OpenCloud\OpenStack
409: */
410: public function setFileDescriptor($key, $value)
411: {
412: $this->descriptors[$key] = $value;
413:
414: return $this;
415: }
416:
417: /**
418: * Get a specific file descriptor (associated with a URL)
419: *
420: * @param string $key
421: * @return resource|false
422: */
423: public function getFileDescriptor($key)
424: {
425: return (!isset($this->descriptors[$key])) ? false : $this->descriptors[$key];
426: }
427:
428: /**
429: * Get the items to be exported.
430: *
431: * @return array
432: */
433: public function getExportItems()
434: {
435: return $this->exportItems;
436: }
437:
438: /**
439: * Sets the connect timeout.
440: *
441: * @param int $timeout
442: * @return OpenCloud\OpenStack
443: */
444: public function setConnectTimeout($timeout)
445: {
446: $this->connectTimeout = $timeout;
447:
448: return $this;
449: }
450:
451: /**
452: * Get the connect timeout.
453: *
454: * @return int
455: */
456: public function getConnectTimeout()
457: {
458: return $this->connectTimeout;
459: }
460:
461: /**
462: * Set the HTTP timeout.
463: *
464: * @param int $timeout
465: * @return OpenCloud\OpenStack
466: */
467: public function setHttpTimeout($timeout)
468: {
469: $this->httpTimeout = $timeout;
470:
471: return $this;
472: }
473:
474: /**
475: * Get the HTTP timeout.
476: *
477: * @return int
478: */
479: public function getHttpTimeout()
480: {
481: return $this->httpTimeout;
482: }
483:
484: /**
485: * Set the overlimit timeout.
486: *
487: * @param int $timeout
488: * @return OpenCloud\OpenStack
489: */
490: public function setOverlimitTimeout($timeout)
491: {
492: $this->overlimitTimeout = $timeout;
493:
494: return $this;
495: }
496:
497: /**
498: * Get the overlimit timeout.
499: *
500: * @return int
501: */
502: public function getOverlimitTimeout()
503: {
504: return $this->overlimitTimeout;
505: }
506:
507: /**
508: * Sets default values (an array) for a service. Each array must contain a
509: * "name", "region" and "urltype" key.
510: *
511: * @param string $service
512: * @param array $value
513: * @return OpenCloud\OpenStack
514: */
515: public function setDefault($service, array $value = array())
516: {
517: if (isset($value['name']) && isset($value['region']) && isset($value['urltype'])) {
518: $this->defaults[$service] = $value;
519: }
520:
521: return $this;
522: }
523:
524: /**
525: * Get a specific default value for a service. If none exist, return FALSE.
526: *
527: * @param string $service
528: * @return array|false
529: */
530: public function getDefault($service)
531: {
532: return (!isset($this->defaults[$service])) ? false : $this->defaults[$service];
533: }
534:
535: /**
536: * Sets the timeouts for the current connection
537: *
538: * @api
539: * @param integer $t_http the HTTP timeout value (the max period that
540: * the OpenStack object will wait for any HTTP request to complete).
541: * Value is in seconds.
542: * @param integer $t_conn the Connect timeout value (the max period
543: * that the OpenStack object will wait to establish an HTTP
544: * connection). Value is in seconds.
545: * @param integer $t_overlimit the overlimit timeout value (the max period
546: * that the OpenStack object will wait to retry on an overlimit
547: * condition). Value is in seconds.
548: * @return void
549: */
550: public function setTimeouts($httpTimeout, $connectTimeout = null, $overlimitTimeout = null)
551: {
552: $this->setHttpTimeout($httpTimeout);
553:
554: if (isset($connectTimeout)) {
555: $this->setConnectTimeout($connectTimeout);
556: }
557:
558: if (isset($overlimitTimeout)) {
559: $this->setOverlimitTimeout($overlimitTimeout);
560: }
561: }
562:
563: /**
564: * Returns the URL of this object
565: *
566: * @api
567: * @param string $subresource specified subresource
568: * @return string
569: */
570: public function url($subresource='tokens')
571: {
572: return Lang::noslash($this->url) . '/' . $subresource;
573: }
574:
575: /**
576: * Returns the stored secret
577: *
578: * @return array
579: */
580: public function secret()
581: {
582: return $this->getSecret();
583: }
584:
585: /**
586: * Re-authenticates session if expired.
587: */
588: public function checkExpiration()
589: {
590: if ($this->hasExpired()) {
591: $this->authenticate();
592: }
593: }
594:
595: /**
596: * Checks whether token has expired.
597: *
598: * @return bool
599: */
600: public function hasExpired()
601: {
602: return time() > ($this->getExpiration() - RAXSDK_FUDGE);
603: }
604:
605: /**
606: * Returns the cached token; if it has expired, then it re-authenticates
607: *
608: * @api
609: * @return string
610: */
611: public function token()
612: {
613: $this->checkExpiration();
614:
615: return $this->getToken();
616: }
617:
618: /**
619: * Returns the cached expiration time;
620: * if it has expired, then it re-authenticates
621: *
622: * @api
623: * @return string
624: */
625: public function expiration()
626: {
627: $this->checkExpiration();
628:
629: return $this->getExpiration();
630: }
631:
632: /**
633: * Returns the tenant ID, re-authenticating if necessary
634: *
635: * @api
636: * @return string
637: */
638: public function tenant()
639: {
640: $this->checkExpiration();
641:
642: return $this->getTenant();
643: }
644:
645: /**
646: * Returns the service catalog object from the auth service
647: *
648: * @return \stdClass
649: */
650: public function serviceCatalog()
651: {
652: $this->checkExpiration();
653:
654: return $this->getCatalog();
655: }
656:
657: /**
658: * Returns a Collection of objects with information on services
659: *
660: * Note that these are informational (read-only) and are not actually
661: * 'Service'-class objects.
662: */
663: public function serviceList()
664: {
665: return new Common\Collection($this, 'ServiceCatalogItem', $this->serviceCatalog());
666: }
667:
668: /**
669: * Creates and returns the formatted credentials to POST to the auth
670: * service.
671: *
672: * @return string
673: */
674: public function credentials()
675: {
676: if (isset($this->secret['username']) && isset($this->secret['password'])) {
677:
678: $credentials = array(
679: 'auth' => array(
680: 'passwordCredentials' => array(
681: 'username' => $this->secret['username'],
682: 'password' => $this->secret['password']
683: )
684: )
685: );
686:
687: if (isset($this->secret['tenantName'])) {
688: $credentials['auth']['tenantName'] = $this->secret['tenantName'];
689: }
690:
691: return json_encode($credentials);
692:
693: } else {
694: throw new Exceptions\CredentialError(
695: Lang::translate('Unrecognized credential secret')
696: );
697: }
698: }
699:
700: /**
701: * Authenticates using the supplied credentials
702: *
703: * @api
704: * @return void
705: * @throws AuthenticationError
706: */
707: public function authenticate()
708: {
709: // try to auth
710: $response = $this->request(
711: $this->url(),
712: 'POST',
713: array('Content-Type'=>'application/json'),
714: $this->credentials()
715: );
716:
717: $json = $response->httpBody();
718:
719: // check for errors
720: if ($response->HttpStatus() >= 400) {
721: throw new Exceptions\AuthenticationError(sprintf(
722: Lang::translate('Authentication failure, status [%d], response [%s]'),
723: $response->httpStatus(),
724: $json
725: ));
726: }
727:
728: // Decode and check
729: $object = json_decode($json);
730: $this->checkJsonError();
731:
732: // Save the token information as well as the ServiceCatalog
733: $this->setToken($object->access->token->id);
734: $this->setExpiration(strtotime($object->access->token->expires));
735: $this->setCatalog($object->access->serviceCatalog);
736:
737: /**
738: * In some cases, the tenant name/id is not returned
739: * as part of the auth token, so we check for it before
740: * we set it. This occurs with pure Keystone, but not
741: * with the Rackspace auth.
742: */
743: if (isset($object->access->token->tenant)) {
744: $this->setTenant($object->access->token->tenant->id);
745: }
746: }
747:
748: /**
749: * Performs a single HTTP request
750: *
751: * The request() method is one of the most frequently-used in the entire
752: * library. It performs an HTTP request using the specified URL, method,
753: * and with the supplied headers and body. It handles error and
754: * exceptions for the request.
755: *
756: * @api
757: * @param string url - the URL of the request
758: * @param string method - the HTTP method (defaults to GET)
759: * @param array headers - an associative array of headers
760: * @param string data - either a string or a resource (file pointer) to
761: * use as the request body (for PUT or POST)
762: * @return HttpResponse object
763: * @throws HttpOverLimitError, HttpUnauthorizedError, HttpForbiddenError
764: */
765: public function request($url, $method = 'GET', $headers = array(), $data = null)
766: {
767: $this->getLogger()->info('Resource [{url}] method [{method}] body [{body}]', array(
768: 'url' => $url,
769: 'method' => $method,
770: 'data' => $data
771: ));
772:
773: // get the request object
774: $http = $this->getHttpRequestObject($url, $method, $this->getCurlOptions());
775:
776: // set various options
777: $this->getLogger()->info('Headers: [{headers}]', array(
778: 'headers' => print_r($headers, true)
779: ));
780:
781: $http->setheaders($headers);
782: $http->setHttpTimeout($this->getHttpTimeout());
783: $http->setConnectTimeout($this->getConnectTimeout());
784: $http->setOption(CURLOPT_USERAGENT, $this->getUserAgent());
785:
786: // data can be either a resource or a string
787: if (is_resource($data)) {
788: // loading from or writing to a file
789: // set the appropriate callback functions
790: switch($method) {
791: // @codeCoverageIgnoreStart
792: case 'GET':
793: // need to save the file descriptor
794: $this->setFileDescriptor($url, $data);
795: // set the CURL options
796: $http->setOption(CURLOPT_FILE, $data);
797: $http->setOption(CURLOPT_WRITEFUNCTION, array($this, '_write_cb'));
798: break;
799: // @codeCoverageIgnoreEnd
800: case 'PUT':
801: case 'POST':
802: // need to save the file descriptor
803: $this->setFileDescriptor($url, $data);
804: if (!isset($headers['Content-Length'])) {
805: throw new Exceptions\HttpError(
806: Lang::translate('The Content-Length: header must be specified for file uploads')
807: );
808: }
809: $http->setOption(CURLOPT_UPLOAD, TRUE);
810: $http->setOption(CURLOPT_INFILE, $data);
811: $http->setOption(CURLOPT_INFILESIZE, $headers['Content-Length']);
812: $http->setOption(CURLOPT_READFUNCTION, array($this, '_read_cb'));
813: break;
814: default:
815: // do nothing
816: break;
817: }
818: } elseif (is_string($data)) {
819: $http->setOption(CURLOPT_POSTFIELDS, $data);
820: } elseif (isset($data)) {
821: throw new Exceptions\HttpError(
822: Lang::translate('Unrecognized data type for PUT/POST body, must be string or resource')
823: );
824: }
825:
826: // perform the HTTP request; returns an HttpResult object
827: $response = $http->execute();
828:
829: // handle and retry on overlimit errors
830: if ($response->httpStatus() == 413) {
831:
832: $object = json_decode($response->httpBody());
833: $this->checkJsonError();
834:
835: // @codeCoverageIgnoreStart
836: if (isset($object->overLimit)) {
837: /**
838: * @TODO(glen) - The documentation says "retryAt", but
839: * the field returned is "retryAfter". If the doc changes,
840: * then there's no problem, but we'll need to fix this if
841: * they change the code to match the docs.
842: */
843: $retryAfter = $object->overLimit->retryAfter;
844: $sleepInterval = strtotime($retryAfter) - time();
845:
846: if ($sleepInterval && $sleepInterval <= $this->getOverlimitTimeout()) {
847: sleep($sleepInterval);
848: $response = $http->Execute();
849: } else {
850: throw new Exceptions\HttpOverLimitError(sprintf(
851: Lang::translate('Over limit; next available request [%s][%s] is not for [%d] seconds at [%s]'),
852: $method,
853: $url,
854: $sleepInterval,
855: $retryAfter
856: ));
857: }
858: }
859: // @codeCoverageIgnoreEnd
860: }
861:
862: // do some common error checking
863: switch ($response->httpStatus()) {
864: case 401:
865: throw new Exceptions\HttpUnauthorizedError(sprintf(
866: Lang::translate('401 Unauthorized for [%s] [%s]'),
867: $url,
868: $response->HttpBody()
869: ));
870: break;
871: case 403:
872: throw new Exceptions\HttpForbiddenError(sprintf(
873: Lang::translate('403 Forbidden for [%s] [%s]'),
874: $url,
875: $response->HttpBody()
876: ));
877: break;
878: case 413: // limit
879: throw new Exceptions\HttpOverLimitError(sprintf(
880: Lang::translate('413 Over limit for [%s] [%s]'),
881: $url,
882: $response->HttpBody()
883: ));
884: break;
885: default:
886: // everything is fine here, we're fine, how are you?
887: break;
888: }
889:
890: // free the handle
891: $http->close();
892:
893: // return the HttpResponse object
894: $this->getLogger()->info('HTTP STATUS [{code}]', array(
895: 'code' => $response->httpStatus()
896: ));
897:
898: return $response;
899: }
900:
901: /**
902: * Sets default values for name, region, URL type for a service
903: *
904: * Once these are set (and they can also be set by defining global
905: * constants), then you do not need to specify these values when
906: * creating new service objects.
907: *
908: * @api
909: * @param string $service the name of a supported service; e.g. 'Compute'
910: * @param string $name the service name; e.g., 'cloudServersOpenStack'
911: * @param string $region the region name; e.g., 'LON'
912: * @param string $urltype the type of URL to use; e.g., 'internalURL'
913: * @return void
914: * @throws UnrecognizedServiceError
915: */
916: public function setDefaults(
917: $service,
918: $name = null,
919: $region = null,
920: $urltype = null
921: ) {
922:
923: if (!isset($this->defaults[$service])) {
924: throw new Exceptions\UnrecognizedServiceError(sprintf(
925: Lang::translate('Service [%s] is not recognized'), $service
926: ));
927: }
928:
929: if (isset($name)) {
930: $this->defaults[$service]['name'] = $name;
931: }
932:
933: if (isset($region)) {
934: $this->defaults[$service]['region'] = $region;
935: }
936:
937: if (isset($urltype)) {
938: $this->defaults[$service]['urltype'] = $urltype;
939: }
940: }
941:
942: /**
943: * Allows the user to define a function for tracking uploads
944: *
945: * This can be used to implement a progress bar or similar function. The
946: * callback function is called with a single parameter, the length of the
947: * data that is being uploaded on this call.
948: *
949: * @param callable $callback the name of a global callback function, or an
950: * array($object, $functionname)
951: * @return void
952: */
953: public function setUploadProgressCallback($callback)
954: {
955: $this->_user_write_progress_callback_func = $callback;
956: }
957:
958: /**
959: * Allows the user to define a function for tracking downloads
960: *
961: * This can be used to implement a progress bar or similar function. The
962: * callback function is called with a single parameter, the length of the
963: * data that is being downloaded on this call.
964: *
965: * @param callable $callback the name of a global callback function, or an
966: * array($object, $functionname)
967: * @return void
968: */
969: public function setDownloadProgressCallback($callback)
970: {
971: $this->_user_read_progress_callback_func = $callback;
972: }
973:
974: /**
975: * Callback function to handle reads for file uploads
976: *
977: * Internal function for handling file uploads. Note that, although this
978: * function's visibility is public, this is only because it must be called
979: * from the HttpRequest interface. This should NOT be called by users
980: * directly.
981: *
982: * @param resource $ch a CURL handle
983: * @param resource $fd a file descriptor
984: * @param integer $length the amount of data to read
985: * @return string the data read
986: * @codeCoverageIgnore
987: */
988: public function _read_cb($ch, $fd, $length)
989: {
990: $data = fread($fd, $length);
991: $len = strlen($data);
992: if (isset($this->_user_write_progress_callback_func)) {
993: call_user_func($this->_user_write_progress_callback_func, $len);
994: }
995: return $data;
996: }
997:
998: /**
999: * Callback function to handle writes for file downloads
1000: *
1001: * Internal function for handling file downloads. Note that, although this
1002: * function's visibility is public, this is only because it must be called
1003: * via the HttpRequest interface. This should NOT be called by users
1004: * directly.
1005: *
1006: * @param resource $ch a CURL handle
1007: * @param string $data the data to be written to a file
1008: * @return integer the number of bytes written
1009: * @codeCoverageIgnore
1010: */
1011: public function _write_cb($ch, $data)
1012: {
1013: $url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
1014:
1015: if (false === ($fp = $this->getFileDescriptor($url))) {
1016: throw new Exceptions\HttpUrlError(sprintf(
1017: Lang::translate('Cannot find file descriptor for URL [%s]'), $url)
1018: );
1019: }
1020:
1021: $dlen = strlen($data);
1022: fwrite($fp, $data, $dlen);
1023:
1024: // call used callback function
1025: if (isset($this->_user_read_progress_callback_func)) {
1026: call_user_func($this->_user_read_progress_callback_func, $dlen);
1027: }
1028:
1029: // MUST return the length to CURL
1030: return $dlen;
1031: }
1032:
1033: /**
1034: * exports saved token, expiration, tenant, and service catalog as an array
1035: *
1036: * This could be stored in a cache (APC or disk file) and reloaded using
1037: * ImportCredentials()
1038: *
1039: * @return array
1040: */
1041: public function exportCredentials()
1042: {
1043: $this->authenticate();
1044:
1045: $array = array();
1046:
1047: foreach ($this->getExportItems() as $key) {
1048: $array[$key] = $this->$key;
1049: }
1050:
1051: return $array;
1052: }
1053:
1054: /**
1055: * imports credentials from an array
1056: *
1057: * Takes the same values as ExportCredentials() and reuses them.
1058: *
1059: * @return void
1060: */
1061: public function importCredentials(array $values)
1062: {
1063: foreach ($this->getExportItems() as $item) {
1064: $this->$item = $values[$item];
1065: }
1066: }
1067:
1068: /********** FACTORY METHODS **********
1069: *
1070: * These methods are provided to permit easy creation of services
1071: * (for example, Nova or Swift) from a connection object. As new
1072: * services are supported, factory methods should be provided here.
1073: */
1074:
1075: /**
1076: * Creates a new ObjectStore object (Swift/Cloud Files)
1077: *
1078: * @api
1079: * @param string $name the name of the Object Storage service to attach to
1080: * @param string $region the name of the region to use
1081: * @param string $urltype the URL type (normally "publicURL")
1082: * @return ObjectStore
1083: */
1084: public function objectStore($name = null, $region = null, $urltype = null)
1085: {
1086: return $this->service('ObjectStore', $name, $region, $urltype);
1087: }
1088:
1089: /**
1090: * Creates a new Compute object (Nova/Cloud Servers)
1091: *
1092: * @api
1093: * @param string $name the name of the Compute service to attach to
1094: * @param string $region the name of the region to use
1095: * @param string $urltype the URL type (normally "publicURL")
1096: * @return Compute
1097: */
1098: public function compute($name = null, $region = null, $urltype = null)
1099: {
1100: return $this->service('Compute', $name, $region, $urltype);
1101: }
1102:
1103: /**
1104: * Creates a new Orchestration (heat) service object
1105: *
1106: * @api
1107: * @param string $name the name of the Compute service to attach to
1108: * @param string $region the name of the region to use
1109: * @param string $urltype the URL type (normally "publicURL")
1110: * @return Orchestration\Service
1111: * @codeCoverageIgnore
1112: */
1113: public function orchestration($name = null, $region = null, $urltype = null)
1114: {
1115: return $this->service('Orchestration', $name, $region, $urltype);
1116: }
1117:
1118: /**
1119: * Creates a new VolumeService (cinder) service object
1120: *
1121: * This is a factory method that is Rackspace-only (NOT part of OpenStack).
1122: *
1123: * @param string $name the name of the service (e.g., 'cloudBlockStorage')
1124: * @param string $region the region (e.g., 'DFW')
1125: * @param string $urltype the type of URL (e.g., 'publicURL');
1126: */
1127: public function volumeService($name = null, $region = null, $urltype = null)
1128: {
1129: return $this->service('Volume', $name, $region, $urltype);
1130: }
1131:
1132: /**
1133: * Generic Service factory method
1134: *
1135: * Contains code reused by the other service factory methods.
1136: *
1137: * @param string $class the name of the Service class to produce
1138: * @param string $name the name of the Compute service to attach to
1139: * @param string $region the name of the region to use
1140: * @param string $urltype the URL type (normally "publicURL")
1141: * @return Service (or subclass such as Compute, ObjectStore)
1142: * @throws ServiceValueError
1143: */
1144: public function service($class, $name = null, $region = null, $urltype = null)
1145: {
1146: // debug message
1147: $this->getLogger()->info('Factory for class [{class}] [{name}/{region}/{urlType}]', array(
1148: 'class' => $class,
1149: 'name' => $name,
1150: 'region' => $region,
1151: 'urlType' => $urltype
1152: ));
1153:
1154: // Strips off base namespace
1155: $class = preg_replace('#\\\?OpenCloud\\\#', '', $class);
1156:
1157: // check for defaults
1158: $default = $this->getDefault($class);
1159:
1160: // report errors
1161: if (!$name = $name ?: $default['name']) {
1162: throw new Exceptions\ServiceValueError(sprintf(
1163: Lang::translate('No value for %s name'),
1164: $class
1165: ));
1166: }
1167:
1168: if (!$region = $region ?: $default['region']) {
1169: throw new Exceptions\ServiceValueError(sprintf(
1170: Lang::translate('No value for %s region'),
1171: $class
1172: ));
1173: }
1174:
1175: if (!$urltype = $urltype ?: $default['urltype']) {
1176: throw new Exceptions\ServiceValueError(sprintf(
1177: Lang::translate('No value for %s URL type'),
1178: $class
1179: ));
1180: }
1181:
1182: // return the object
1183: $fullclass = 'OpenCloud\\' . $class . '\\Service';
1184:
1185: return new $fullclass($this, $name, $region, $urltype);
1186: }
1187:
1188: /**
1189: * returns a service catalog item
1190: *
1191: * This is a helper function used to list service catalog items easily
1192: */
1193: public function serviceCatalogItem($info = array())
1194: {
1195: return new ServiceCatalogItem($info);
1196: }
1197:
1198: }
1199: