      1: <?php
      2: /**
      3:  * Copyright 2012-2014 Rackspace US, Inc.
      4:  *
      5:  * Licensed under the Apache License, Version 2.0 (the "License");
      6:  * you may not use this file except in compliance with the License.
      7:  * You may obtain a copy of the License at
      8:  *
      9:  * http://www.apache.org/licenses/LICENSE-2.0
     10:  *
     11:  * Unless required by applicable law or agreed to in writing, software
     12:  * distributed under the License is distributed on an "AS IS" BASIS,
     13:  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14:  * See the License for the specific language governing permissions and
     15:  * limitations under the License.
     16:  */
     17:
     18: namespace OpenCloud\Common\Service;
     19:
     20: use Guzzle\Http\ClientInterface;
     21: use Guzzle\Http\Exception\BadResponseException;
     22: use Guzzle\Http\Url;
     23: use OpenCloud\Common\Exceptions;
     24: use OpenCloud\Common\Http\Message\Formatter;
     25: use Symfony\Component\EventDispatcher\EventSubscriberInterface;
     26:
     27: abstract class CatalogService extends AbstractService
     28: {
     29:     const DEFAULT_URL_TYPE = 'publicURL';
     30:
     31:     /**
     32:      * @var string The type of this service, as set in Catalog.
     33:      */
     34:     private $type;
     35:
     36:     /**
     37:      * @var string The name of this service, as set in Catalog.
     38:      */
     39:     private $name;
     40:
     41:     /**
     42:      * @var string The chosen region(s) for this service.
     43:      */
     44:     private $region;
     45:
     46:     /**
     47:      * @var string Either 'publicURL' or 'privateURL'.
     48:      */
     49:     private $urlType;
     50:
     51:     /**
     52:      * @var bool Indicates whether a service is "regionless" or not. Defaults to FALSE because nearly all services
     53:      *           are region-specific.
     54:      */
     55:     protected $regionless = false;
     56:
     57:     /**
     58:      * Creates a service object, based off the specified client.
     59:      *
     60:      * The service's URL is defined in the client's serviceCatalog; it
     61:      * uses the $type, $name, $region, and $urlType to find the proper endpoint
     62:      * and set it. If it cannot find a URL in the service catalog that matches
     63:      * the criteria, then an exception is thrown.
     64:      *
     65:      * @param Client $client  Client object
     66:      * @param string $type    Service type (e.g. 'compute')
     67:      * @param string $name    Service name (e.g. 'cloudServersOpenStack')
     68:      * @param string $region  Service region (e.g. 'DFW', 'ORD', 'IAD', 'LON', 'SYD' or 'HKG')
     69:      * @param string $urlType Either 'publicURL' or 'privateURL'
     70:      */
     71:     public function __construct(ClientInterface $client, $type = null, $name = null, $region = null, $urlType = null)
     72:     {
     73:         $this->setClient($client);
     74:
     75:         $this->name = $name ? : static::DEFAULT_NAME;
     76:         $this->region = $region;
     77:
     78:         $this->region = $region;
     79:         if ($this->regionless !== true && !$this->region) {
     80:             throw new Exceptions\ServiceException(sprintf(
     81:                 'The %s service must have a region set. You can either pass in a region string as an argument param, or'
     82:                 . ' set a default region for your user account by executing User::setDefaultRegion and ::update().',
     83:                 $this->name
     84:             ));
     85:         }
     86:
     87:         $this->type = $type ? : static::DEFAULT_TYPE;
     88:         $this->urlType = $urlType ? : static::DEFAULT_URL_TYPE;
     89:         $this->setEndpoint($this->findEndpoint());
     90:
     91:         $this->client->setBaseUrl($this->getBaseUrl());
     92:
     93:         if ($this instanceof EventSubscriberInterface) {
     94:             $this->client->getEventDispatcher()->addSubscriber($this);
     95:         }
     96:     }
     97:
     98:     /**
     99:      * @return string
    100:      */
    101:     public function getType()
    102:     {
    103:         return $this->type;
    104:     }
    105:
    106:     /**
    107:      * @return string
    108:      */
    109:     public function getRegion()
    110:     {
    111:         return $this->region;
    112:     }
    113:
    114:     /**
    115:      * @return string
    116:      */
    117:     public function getName()
    118:     {
    119:         return $this->name;
    120:     }
    121:
    122:     /**
    123:      * @return string
    124:      */
    125:     public function getUrlType()
    126:     {
    127:         return $this->urlType;
    128:     }
    129:
    130:     /**
    131:      * @deprecated
    132:      */
    133:     public function region()
    134:     {
    135:         return $this->getRegion();
    136:     }
    137:
    138:     /**
    139:      * @deprecated
    140:      */
    141:     public function name()
    142:     {
    143:         return $this->name;
    144:     }
    145:
    146:     /**
    147:      * Returns the URL for the Service
    148:      *
    149:      * @param  string $path  URL path segment
    150:      * @param  array  $query Array of query pairs
    151:      * @return Guzzle\Http\Url
    152:      */
    153:     public function getUrl($path = null, array $query = array())
    154:     {
    155:         return Url::factory($this->getBaseUrl())
    156:             ->addPath($path)
    157:             ->setQuery($query);
    158:     }
    159:
    160:     /**
    161:      * @deprecated
    162:      */
    163:     public function url($path = null, array $query = array())
    164:     {
    165:         return $this->getUrl($path, $query);
    166:     }
    167:
    168:     /**
    169:      * Returns the /extensions for the service
    170:      *
    171:      * @api
    172:      * @return array of objects
    173:      */
    174:     public function getExtensions()
    175:     {
    176:         $ext = $this->getMetaUrl('extensions');
    177:
    178:         return (is_object($ext) && isset($ext->extensions)) ? $ext->extensions : array();
    179:     }
    180:
    181:     /**
    182:      * Returns the limits for the service
    183:      *
    184:      * @return array of limits
    185:      */
    186:     public function limits()
    187:     {
    188:         $limits = $this->getMetaUrl('limits');
    189:
    190:         return (is_object($limits)) ? $limits->limits : array();
    191:     }
    192:
    193:     /**
    194:      * Extracts the appropriate endpoint from the service catalog based on the
    195:      * name and type of a service, and sets for further use.
    196:      *
    197:      * @return \OpenCloud\Common\Service\Endpoint
    198:      * @throws \OpenCloud\Common\Exceptions\EndpointError
    199:      */
    200:     private function findEndpoint()
    201:     {
    202:         if (!$this->getClient()->getCatalog()) {
    203:             $this->getClient()->authenticate();
    204:         }
    205:
    206:         $catalog = $this->getClient()->getCatalog();
    207:
    208:         // Search each service to find The One
    209:         foreach ($catalog->getItems() as $service) {
    210:             if ($service->hasType($this->type) && $service->hasName($this->name)) {
    211:                 return Endpoint::factory($service->getEndpointFromRegion($this->region));
    212:             }
    213:         }
    214:
    215:         throw new Exceptions\EndpointError(sprintf(
    216:             'No endpoints for service type [%s], name [%s], region [%s] and urlType [%s]',
    217:             $this->type,
    218:             $this->name,
    219:             $this->region,
    220:             $this->urlType
    221:         ));
    222:     }
    223:
    224:     /**
    225:      * Constructs a specified URL from the subresource
    226:      *
    227:      * Given a subresource (e.g., "extensions"), this constructs the proper
    228:      * URL and retrieves the resource.
    229:      *
    230:      * @param string $resource The resource requested; should NOT have slashes
    231:      *                         at the beginning or end
    232:      * @return \stdClass object
    233:      */
    234:     private function getMetaUrl($resource)
    235:     {
    236:         $url = clone $this->getBaseUrl();
    237:         $url->addPath($resource);
    238:         try {
    239:             $response = $this->getClient()->get($url)->send();
    240:
    241:             return Formatter::decode($response);
    242:         } catch (BadResponseException $e) {
    243:             // @codeCoverageIgnoreStart
    244:             return array();
    245:             // @codeCoverageIgnoreEnd
    246:         }
    247:     }
    248:
    249:     /**
    250:      * Get the base URL for this service, based on the set URL type.
    251:      * @return \Guzzle\Http\Url
    252:      * @throws \OpenCloud\Common\Exceptions\ServiceException
    253:      */
    254:     public function getBaseUrl()
    255:     {
    256:         $url = ($this->urlType == 'publicURL')
    257:             ? $this->endpoint->getPublicUrl()
    258:             : $this->endpoint->getPrivateUrl();
    259:
    260:         if ($url === null) {
    261:             throw new Exceptions\ServiceException(sprintf(
    262:                 'The base %s could not be found. Perhaps the service '
    263:                 . 'you are using requires a different URL type, or does '
    264:                 . 'not support this region.',
    265:                 $this->urlType
    266:             ));
    267:         }
    268:
    269:         return $url;
    270:     }
    271: }
    272: