HPCloud-PHP  1.2.0
PHP bindings for HPCloud and OpenStack services.
 All Classes Namespaces Files Functions Variables Pages
ObjectStorage.php
Go to the documentation of this file.
1 <?php
2 /* ============================================================================
3 (c) Copyright 2012 Hewlett-Packard Development Company, L.P.
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights to
7 use, copy, modify, merge,publish, distribute, sublicense, and/or sell copies of
8 the Software, and to permit persons to whom the Software is furnished to do so,
9 subject to the following conditions:
10 
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13 
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 ============================================================================ */
22 /**
23  * @file
24  *
25  * This file provides the ObjectStorage class, which is the primary
26  * representation of the ObjectStorage system.
27  *
28  * ObjectStorage (aka Swift) is the OpenStack service for providing
29  * storage of complete and discrete pieces of data (e.g. an image file,
30  * a text document, a binary).
31  */
32 
33 namespace HPCloud\Storage;
34 
35 use HPCloud\Storage\ObjectStorage\Container;
36 use HPCloud\Storage\ObjectStorage\ACL;
37 
38 /**
39  * Access to ObjectStorage (Swift).
40  *
41  * This is the primary piece of the Object Oriented representation of
42  * the Object Storage service. Developers wishing to work at a low level
43  * should use this API.
44  *
45  * There is also a stream wrapper interface that exposes ObjectStorage
46  * to PHP's streams system. For common use of an object store, you may
47  * prefer to use that system. (See HPCloud::Bootstrap).
48  *
49  * When constructing a new ObjectStorage object, you will need to know
50  * what kind of authentication you are going to perform. Older
51  * implementations of OpenStack provide a separate authentication
52  * mechanism for Swift. You can use ObjectStorage::newFromSwiftAuth() to
53  * perform this type of authentication.
54  *
55  * Newer versions use the IdentityServices authentication mechanism (see
56  * HPCloud::Services::IdentityServices). That method is the preferred
57  * method.
58  *
59  * Common Tasks
60  *
61  * - Create a new container with createContainer().
62  * - List containers with containers().
63  * - Remove a container with deleteContainer().
64  *
65  * @todo ObjectStorage is not yet constrained to a particular version
66  * of the API. It attempts to use whatever version is passed in to the
67  * URL. This is different than IdentityServices and CDN, which use a
68  * fixed version.
69  */
71 
72  /**
73  * The name of this service type in HPCloud.
74  *
75  * This is used with IdentityService::serviceCatalog().
76  */
77  const SERVICE_TYPE = 'object-store';
78 
79  const API_VERSION = '1';
80 
81  const DEFAULT_REGION = 'region-a.geo-1';
82 
83  /**
84  * The authorization token.
85  */
86  protected $token = NULL;
87  /**
88  * The URL to the Swift endpoint.
89  */
90  protected $url = NULL;
91 
92  /**
93  * CDN containers.
94  *
95  * This is an associative array of container names to URLs.
96  */
97  protected $cdnContainers;
98 
99 
100  /**
101  * Create a new instance after getting an authenitcation token.
102  *
103  * THIS METHOD IS DEPRECATED. OpenStack now uses Keyston to authenticate.
104  * You should use HPCloud::Services::IdentityServices to authenticate.
105  * Then use this class's constructor to create an object.
106  *
107  * This uses the legacy Swift authentication facility to authenticate
108  * to swift, get a new token, and then create a new ObjectStorage
109  * instance with that token.
110  *
111  * To use the legacy Object Storage authentication mechanism, you will
112  * need the follwing pieces of information:
113  *
114  * - Account ID: Your account username or ID. For HP Cloud customers,
115  * this is typically a long string of numbers and letters.
116  * - Key: Your secret key. For HP Customers, this is a string of
117  * random letters and numbers.
118  * - Endpoint URL: The URL given to you by your service provider.
119  *
120  * HP Cloud users can find all of this information on your Object
121  * Storage account dashboard.
122  *
123  * @param string $account
124  * Your account name.
125  * @param string $key
126  * Your secret key.
127  * @param string $url
128  * The URL to the object storage endpoint.
129  *
130  * @throws HPCloud::Transport::AuthorizationException if the
131  * authentication failed.
132  * @throws HPCloud::Transport::FileNotFoundException if the URL is
133  * wrong.
134  * @throws HPCloud::Exception if some other exception occurs.
135  *
136  * @deprecated Newer versions of OpenStack use Keystone auth instead
137  * of Swift auth.
138  */
139  public static function newFromSwiftAuth($account, $key, $url) {
140  $headers = array(
141  'X-Auth-User' => $account,
142  'X-Auth-Key' => $key,
143  );
144 
145  $client = \HPCloud\Transport::instance();
146 
147  // This will throw an exception if it cannot connect or
148  // authenticate.
149  $res = $client->doRequest($url, 'GET', $headers);
150 
151 
152  // Headers that come back:
153  // X-Storage-Url: https://region-a.geo-1.objects.hpcloudsvc.com:443/v1/AUTH_d8e28d35-3324-44d7-a625-4e6450dc1683
154  // X-Storage-Token: AUTH_tkd2ffb4dac4534c43afbe532ca41bcdba
155  // X-Auth-Token: AUTH_tkd2ffb4dac4534c43afbe532ca41bcdba
156  // X-Trans-Id: tx33f1257e09f64bc58f28e66e0577268a
157 
158 
159  $token = $res->header('X-Auth-Token');
160  $newUrl = $res->header('X-Storage-Url');
161 
162 
163  $store = new ObjectStorage($token, $newUrl);
164 
165  return $store;
166  }
167 
168  /**
169  * Given an IdentityServices instance, create an ObjectStorage instance.
170  *
171  * This constructs a new ObjectStorage from an authenticated instance
172  * of an HPCloud::Services::IdentityServices object.
173  *
174  * @param HPCloud::Services::IdentityServices $identity
175  * An identity services object that already has a valid token and a
176  * service catalog.
177  * @retval HPCloud::Storage::ObjectStorage
178  * @return \HPCloud\Storage\ObjectStorage
179  * A new ObjectStorage instance.
180  */
181  public static function newFromIdentity($identity, $region = ObjectStorage::DEFAULT_REGION) {
182  $cat = $identity->serviceCatalog();
183  $tok = $identity->token();
184  return self::newFromServiceCatalog($cat, $tok, $region);
185  }
186 
187  /**
188  * Given a service catalog and an token, create an ObjectStorage instance.
189  *
190  * The IdentityServices object contains a service catalog listing all of the
191  * services to which the present account has access.
192  *
193  * This builder can scan the catalog and generate a new ObjectStorage
194  * instance pointed to the first object storage endpoint in the catalog.
195  *
196  * @param array $catalog
197  * The serice catalog from IdentityServices::serviceCatalog(). This
198  * can be either the entire catalog or a catalog filtered to
199  * just ObjectStorage::SERVICE_TYPE.
200  * @param string $authToken
201  * The auth token returned by IdentityServices.
202  * @retval HPCloud::Storage::ObjectStorage
203  * @return \HPCloud\Storage\ObjectStorage
204  * A new ObjectStorage instance.
205  */
206  public static function newFromServiceCatalog($catalog, $authToken, $region = ObjectStorage::DEFAULT_REGION) {
207  $c = count($catalog);
208  for ($i = 0; $i < $c; ++$i) {
209  if ($catalog[$i]['type'] == self::SERVICE_TYPE) {
210  foreach ($catalog[$i]['endpoints'] as $endpoint) {
211  if (isset($endpoint['publicURL']) && $endpoint['region'] == $region) {
212  $os= new ObjectStorage($authToken, $endpoint['publicURL']);
213  //$cdn->url = $endpoint['publicURL'];
214 
215  return $os;
216  }
217  }
218  }
219  }
220  return FALSE;
221 
222  }
223 
224  /**
225  * Construct a new ObjectStorage object.
226  *
227  * Use this if newFromServiceCatalog() does not meet your needs.
228  *
229  * @param string $authToken
230  * A token that will be included in subsequent requests to validate
231  * that this client has authenticated correctly.
232  * @param string $url
233  * The URL to the endpoint. This typically is returned after
234  * authentication.
235  */
236  public function __construct($authToken, $url) {
237  $this->token = $authToken;
238  $this->url = $url;
239  }
240 
241  /**
242  * Indicate that this ObjectStorage instance should use the given CDN service.
243  *
244  * This will cause this ObjectStorage instance to use CDN as often as it can.
245  * Any containers (and subsequently container objects) that can leverage
246  * CDN services will act accordingly.
247  *
248  * CDN is used for *read* operations. Because CDN is a time-based cache,
249  * objects in CDN can be older than objects in Swift itself. For that
250  * reason, CDN should not be used when a combination of read and write
251  * operations occur.
252  *
253  * @retval HPCloud::Storage::ObjectStorage
254  * @return \HPCloud\Storage\ObjectStorage
255  * $this for current object so the method can be used in chaining.
256  */
257  public function useCDN($cdn) {
258 
259  // This should not happen, but has happened when service
260  // catalog was bad.
261  if (empty($cdn)) {
262  throw new \HPCloud\Exception('Cannot use CDN: No CDN provided.');
263  }
264 
265  $containers = $cdn->containers(TRUE);
266  $buffer = array();
267 
268  foreach ($containers as $item) {
269  // This is needed b/c of a bug in SOS that sometimes
270  // returns disabled containers (DEVEX-1733).
271  if ($item['cdn_enabled'] == 1) {
272  $buffer[$item['name']] = array(
273  'url' => $item['x-cdn-uri'],
274  'sslUrl' => $item['x-cdn-ssl-uri'],
275  );
276  }
277  }
278  $this->cdnContainers = $buffer;
279 
280  return $this;
281  }
282 
283  public function hasCDN() {
284  return !empty($this->cdnContainers);
285  }
286 
287  /**
288  * Return the CDN URL for a particular container.
289  *
290  * If CDN is enabled, this will attempt to get the URL
291  * to the CDN endpoint for the given container.
292  *
293  * @param string $containerName
294  * The name of the container.
295  * @param boolean $ssl
296  * If this is TRUE (default), get the URL to the SSL CDN;
297  * otherwise get the URL to the plain HTTP CDN.
298  * @retval string
299  * @return string
300  * The URL to the CDN container, or NULL if no such
301  * URL is found.
302  */
303  public function cdnUrl($containerName, $ssl = TRUE) {
304  if (!empty($this->cdnContainers[$containerName])) {
305  $key = $ssl ? 'sslUrl' : 'url';
306  return $this->cdnContainers[$containerName][$key];
307  }
308  }
309 
310  /**
311  * Get the authentication token.
312  *
313  * @retval string
314  * @return string
315  * The authentication token.
316  */
317  public function token() {
318  return $this->token;
319  }
320 
321  /**
322  * Get the URL endpoint.
323  *
324  * @retval string
325  * @return string
326  * The URL that is the endpoint for this service.
327  */
328  public function url() {
329  return $this->url;
330  }
331 
332  /**
333  * Fetch a list of containers for this account.
334  *
335  * By default, this fetches the entire list of containers for the
336  * given account. If you have more than 10,000 containers (who
337  * wouldn't?), you will need to use $marker for paging.
338  *
339  * If you want more controlled paging, you can use $limit to indicate
340  * the number of containers returned per page, and $marker to indicate
341  * the last container retrieved.
342  *
343  * Containers are ordered. That is, they will always come back in the
344  * same order. For that reason, the pager takes $marker (the name of
345  * the last container) as a paging parameter, rather than an offset
346  * number.
347  *
348  * @todo For some reason, ACL information does not seem to be returned
349  * in the JSON data. Need to determine how to get that. As a
350  * stop-gap, when a container object returned from here has its ACL
351  * requested, it makes an additional round-trip to the server to
352  * fetch that data.
353  *
354  * @param int $limit
355  * The maximum number to return at a time. The default is -- brace
356  * yourself -- 10,000 (as determined by OpenStack. Implementations
357  * may vary).
358  * @param string $marker
359  * The name of the last object seen. Used when paging.
360  *
361  * @retval array
362  * @return array
363  * An associative array of containers, where the key is the
364  * container's name and the value is an
365  * HPCloud::Storage::ObjectStorage::Container object. Results are
366  * ordered in server order (the order that the remote host puts them
367  * in).
368  */
369  public function containers($limit = 0, $marker = NULL) {
370 
371  $url = $this->url() . '?format=json';
372 
373  if ($limit > 0) {
374  $url .= sprintf('&limit=%d', $limit);
375  }
376  if (!empty($marker)) {
377  $url .= sprintf('&marker=%d', $marker);
378  }
379 
380  $containers = $this->get($url);
381 
382  $containerList = array();
383  foreach ($containers as $container) {
384  $cname = $container['name'];
385  $containerList[$cname] = Container::newFromJSON($container, $this->token(), $this->url());
386 
387  if (!empty($this->cdnContainers[$cname])) {
388  $cdnList = $this->cdnContainers[$cname];
389  $containerList[$cname]->useCDN($cdnList['url'], $cdnList['sslUrl']);
390  }
391  }
392 
393  return $containerList;
394  }
395 
396  /**
397  * Get a single specific container.
398  *
399  * This loads only the named container from the remote server.
400  *
401  * @param string $name
402  * The name of the container to load.
403  * @retval HPCloud::Storage::ObjectStorage::Container
404  * @return \HPCloud\Storage\ObjectStorage\Container
405  * A container.
406  * @throws HPCloud::Transport::FileNotFoundException
407  * if the named container is not found on the remote server.
408  */
409  public function container($name) {
410 
411  $url = $this->url() . '/' . rawurlencode($name);
412  $data = $this->req($url, 'HEAD', FALSE);
413 
414  $status = $data->status();
415  if ($status == 204) {
416  $container = Container::newFromResponse($name, $data, $this->token(), $this->url());
417 
418  if (isset($this->cdnContainers[$name])) {
419  $cdnList = $this->cdnContainers[$name];
420  $container->useCDN($cdnList['url'], $cdnList['sslUrl']);
421  }
422 
423  return $container;
424  }
425 
426  // If we get here, it's not a 404 and it's not a 204.
427  throw new \HPCloud\Exception("Unknown status: $status");
428  }
429 
430  /**
431  * Check to see if this container name exists.
432  *
433  * This method directly checks the remote server. Calling container()
434  * or containers() might be more efficient if you plan to work with
435  * the resulting container.
436  *
437  * @param string $name
438  * The name of the container to test.
439  * @retval boolean
440  * @return boolean
441  * TRUE if the container exists, FALSE if it does not.
442  * @throws HPCloud::Exception
443  * If an unexpected network error occurs.
444  */
445  public function hasContainer($name) {
446  try {
447  $container = $this->container($name);
448  }
449  catch (\HPCloud\Transport\FileNotFoundException $fnfe) {
450  return FALSE;
451  }
452  return TRUE;
453  }
454 
455  /**
456  * Create a container with the given name.
457  *
458  * This creates a new container on the ObjectStorage
459  * server with the name provided in $name.
460  *
461  * A boolean is returned when the operation did not generate an error
462  * condition.
463  *
464  * - TRUE means that the container was created.
465  * - FALSE means that the container was not created because it already
466  * exists.
467  *
468  * Any actual error will cause an exception to be thrown. These will
469  * be the HTTP-level exceptions.
470  *
471  * ACLs
472  *
473  * Swift supports an ACL stream that allows for specifying (with
474  * certain caveats) various levels of read and write access. However,
475  * there are two standard settings that cover the vast majority of
476  * cases.
477  *
478  * - Make the resource private: This grants read and write access to
479  * ONLY the creating account. This is the default; it can also be
480  * specified with ACL::makeNonPublic().
481  * - Make the resource public: This grants READ permission to any
482  * requesting host, yet only allows the creator to WRITE to the
483  * object. This level can be granted by ACL::makePublic().
484  *
485  * Note that ACLs operate at a container level. Thus, marking a
486  * container public will allow access to ALL objects inside of the
487  * container.
488  *
489  * To find out whether an existing container is public, you can
490  * write something like this:
491  *
492  * @code
493  * <?php
494  * // Get the container.
495  * $container = $objectStorage->container('my_container');
496  *
497  * //Check the permission on the ACL:
498  * $boolean = $container->acl()->isPublic();
499  * ?>
500  * @endcode
501  *
502  * For details on ACLs, see HPCloud::Storage::ObjectStorage::ACL.
503  *
504  * @param string $name
505  * The name of the container.
506  * @param object $acl HPCloud::Storage::ObjectStorage::ACL
507  * An access control list object. By default, a container is
508  * non-public (private). To change this behavior, you can add a
509  * custom ACL. To make the container publically readable, you can
510  * use this: HPCloud::Storage::ObjectStorage::ACL::makePublic().
511  * @param array $metadata
512  * An associative array of metadata to attach to the container.
513  * @retval boolean
514  * @return boolean
515  * TRUE if the container was created, FALSE if the container was not
516  * created because it already exists.
517  */
518  public function createContainer($name, ACL $acl = NULL, $metadata = array()) {
519  $url = $this->url() . '/' . rawurlencode($name);
520  $headers = array(
521  'X-Auth-Token' => $this->token(),
522  );
523 
524  if (!empty($metadata)) {
526  $headers += Container::generateMetadataHeaders($metadata, $prefix);
527  }
528 
529  $client = \HPCloud\Transport::instance();
530  // Add ACLs to header.
531  if (!empty($acl)) {
532  $headers += $acl->headers();
533  }
534 
535  $data = $client->doRequest($url, 'PUT', $headers);
536  //syslog(LOG_WARNING, print_r($data, TRUE));
537 
538  $status = $data->status();
539 
540  if ($status == 201) {
541  return TRUE;
542  } elseif ($status == 202) {
543  return FALSE;
544  }
545  // According to the OpenStack docs, there are no other return codes.
546  else {
547  throw new \HPCloud\Exception('Server returned unexpected code: ' . $status);
548  }
549  }
550 
551  /**
552  * Alias of createContainer().
553  *
554  * At present, there is no distinction in the Swift REST API between
555  * creating an updating a container. In the future this may change, so
556  * you are encouraged to use this alias in cases where you clearly intend
557  * to update an existing container.
558  */
559  public function updateContainer($name, ACL $acl = NULL, $metadata = array()) {
560  return $this->createContainer($name, $acl, $metadata);
561  }
562 
563  /**
564  * Change the container's ACL.
565  *
566  * This will attempt to change the ACL on a container. If the
567  * container does not already exist, it will be created first, and
568  * then the ACL will be set. (This is a relic of the OpenStack Swift
569  * implementation, which uses the same HTTP verb to create a container
570  * and to set the ACL.)
571  *
572  * @param string $name
573  * The name of the container.
574  * @param object $acl HPCloud::Storage::ObjectStorage::ACL
575  * An ACL. To make the container publically readable, use
576  * ACL::makePublic().
577  * @retval boolean
578  * @return boolean
579  * TRUE if the cointainer was created, FALSE otherwise.
580  */
581  public function changeContainerACL($name, ACL $acl) {
582  // Oddly, the way to change an ACL is to issue the
583  // same request as is used to create a container.
584  return $this->createContainer($name, $acl);
585  }
586 
587  /**
588  * Delete an empty container.
589  *
590  * Given a container name, this attempts to delete the container in
591  * the object storage.
592  *
593  * The container MUST be empty before it can be deleted. If it is not,
594  * an HPCloud::Storage::ObjectStorage::ContainerNotEmptyException will
595  * be thrown.
596  *
597  * @param string $name
598  * The name of the container.
599  * @retval boolean
600  * @return boolean
601  * TRUE if the container was deleted, FALSE if the container was not
602  * found (and hence, was not deleted).
603  * @throws HPCloud::Storage::ObjectStorage::ContainerNotEmptyException
604  * if the container is not empty.
605  * @throws HPCloud::Exception if an unexpected response code is returned.
606  * While this should never happen on HPCloud servers, forks of
607  * OpenStack may choose to extend object storage in a way that
608  * results in a non-standard code.
609  */
610  public function deleteContainer($name) {
611  $url = $this->url() . '/' . rawurlencode($name);
612 
613  try {
614  $data = $this->req($url, 'DELETE', FALSE);
615  }
616  catch (\HPCloud\Transport\FileNotFoundException $e) {
617  return FALSE;
618  }
619  // XXX: I'm not terribly sure about this. Why not just throw the
620  // ConflictException?
621  catch (\HPCloud\Transport\ConflictException $e) {
622  throw new ObjectStorage\ContainerNotEmptyException("Non-empty container cannot be deleted.");
623  }
624 
625  $status = $data->status();
626 
627  // 204 indicates that the container has been deleted.
628  if ($status == 204) {
629  return TRUE;
630  }
631  // OpenStacks documentation doesn't suggest any other return
632  // codes.
633  else {
634  throw new \HPCloud\Exception('Server returned unexpected code: ' . $status);
635  }
636  }
637 
638  /**
639  * Retrieve account info.
640  *
641  * This returns information about:
642  *
643  * - The total bytes used by this Object Storage instance (`bytes`).
644  * - The number of containers (`count`).
645  *
646  * @retval array
647  * @return array
648  * An associative array of account info. Typical keys are:
649  * - bytes: Bytes consumed by existing content.
650  * - containers: Number of containers.
651  * - objects: Number of objects.
652  * @throws HPCloud::Transport::AuthorizationException
653  * if the user credentials are invalid or have expired.
654  */
655  public function accountInfo() {
656  $url = $this->url();
657  $data = $this->req($url, 'HEAD', FALSE);
658 
659  $results = array(
660  'bytes' => $data->header('X-Account-Bytes-Used', 0),
661  'containers' => $data->header('X-Account-Container-Count', 0),
662  'objects' => $data->header('X-Account-Container-Count', 0),
663  );
664 
665  return $results;
666  }
667 
668  /**
669  * Do a GET on Swift.
670  *
671  * This is a convenience method that handles the
672  * most common case of Swift requests.
673  */
674  protected function get($url, $jsonDecode = TRUE) {
675  return $this->req($url, 'GET', $jsonDecode);
676  }
677 
678  /**
679  * Internal request issuing command.
680  */
681  protected function req($url, $method = 'GET', $jsonDecode = TRUE, $body = '') {
682  $client = \HPCloud\Transport::instance();
683  $headers = array(
684  'X-Auth-Token' => $this->token(),
685  );
686 
687  $raw = $client->doRequest($url, $method, $headers, $body);
688  if (!$jsonDecode) {
689  return $raw;
690  }
691  return json_decode($raw->content(), TRUE);
692 
693  }
694 }