      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;
     19:
     20: use Guzzle\Http\Url;
     21: use OpenCloud\Common\Exceptions;
     22: use OpenCloud\Common\Http\Client;
     23: use OpenCloud\Common\Http\Message\Formatter;
     24: use OpenCloud\Common\Http\Message\RequestSubscriber;
     25: use OpenCloud\Common\Lang;
     26: use OpenCloud\Common\Service\Catalog;
     27: use OpenCloud\Common\Service\ServiceBuilder;
     28: use OpenCloud\Identity\Resource\Tenant;
     29: use OpenCloud\Identity\Resource\Token;
     30: use OpenCloud\Identity\Resource\User;
     31: use OpenCloud\Identity\Service as IdentityService;
     32:
     33: define('RACKSPACE_US', 'https://identity.api.rackspacecloud.com/v2.0/');
     34: define('RACKSPACE_UK', 'https://lon.identity.api.rackspacecloud.com/v2.0/');
     35:
     36: /**
     37:  * The main client of the library. This object is the central point of negotiation between your application and the
     38:  * API because it handles all of the HTTP transactions required to perform operations. It also manages the services
     39:  * for your application through convenient factory methods.
     40:  */
     41: class OpenStack extends Client
     42: {
     43:     /**
     44:      * @var array Credentials passed in by the user
     45:      */
     46:     private $secret = array();
     47:
     48:     /**
     49:      * @var string The token produced by the API
     50:      */
     51:     private $token;
     52:
     53:     /**
     54:      * @var string The unique identifier for who's accessing the API
     55:      */
     56:     private $tenant;
     57:
     58:     /**
     59:      * @var \OpenCloud\Common\Service\Catalog The catalog of services which are provided by the API
     60:      */
     61:     private $catalog;
     62:
     63:     /**
     64:      * @var \OpenCloud\Common\Log\LoggerInterface The object responsible for logging output
     65:      */
     66:     private $logger;
     67:
     68:     /**
     69:      * @var string The endpoint URL used for authentication
     70:      */
     71:     private $authUrl;
     72:
     73:     /**
     74:      * @var \OpenCloud\Identity\Resource\User
     75:      */
     76:     private $user;
     77:
     78:     public function __construct($url, array $secret, array $options = array())
     79:     {
     80:         $this->setSecret($secret);
     81:         $this->setAuthUrl($url);
     82:
     83:         parent::__construct($url, $options);
     84:
     85:         $this->addSubscriber(RequestSubscriber::getInstance());
     86:         $this->setDefaultOption('headers/Accept', 'application/json');
     87:     }
     88:
     89:     /**
     90:      * Set the credentials for the client
     91:      *
     92:      * @param array $secret
     93:      * @return $this
     94:      */
     95:     public function setSecret(array $secret = array())
     96:     {
     97:         $this->secret = $secret;
     98:
     99:         return $this;
    100:     }
    101:
    102:     /**
    103:      * Get the secret.
    104:      *
    105:      * @return array
    106:      */
    107:     public function getSecret()
    108:     {
    109:         return $this->secret;
    110:     }
    111:
    112:     /**
    113:      * Set the token. If a string is passed in, the SDK assumes you want to set the ID of the full Token object
    114:      * and sets this property accordingly. For any other data type, it assumes you want to populate the Token object.
    115:      * This ambiguity arises due to backwards compatibility.
    116:      *
    117:      * @param  string $token
    118:      * @return $this
    119:      */
    120:     public function setToken($token)
    121:     {
    122:         $identity = IdentityService::factory($this);
    123:
    124:         if (is_string($token)) {
    125:
    126:             if (!$this->token) {
    127:                 $this->setTokenObject($identity->resource('Token'));
    128:             }
    129:             $this->token->setId($token);
    130:         } else {
    131:             $this->setTokenObject($identity->resource('Token', $token));
    132:         }
    133:
    134:         return $this;
    135:     }
    136:
    137:     /**
    138:      * Get the token ID for this client.
    139:      *
    140:      * @return string
    141:      */
    142:     public function getToken()
    143:     {
    144:         return ($this->getTokenObject()) ? $this->getTokenObject()->getId() : null;
    145:     }
    146:
    147:     /**
    148:      * Set the full toke object
    149:      */
    150:     public function setTokenObject(Token $token)
    151:     {
    152:         $this->token = $token;
    153:     }
    154:
    155:     /**
    156:      * Get the full token object.
    157:      */
    158:     public function getTokenObject()
    159:     {
    160:         return $this->token;
    161:     }
    162:
    163:     /**
    164:      * @deprecated
    165:      */
    166:     public function setExpiration($expiration)
    167:     {
    168:         $this->getLogger()->deprecated(__METHOD__, '::getTokenObject()->setExpires()');
    169:         if ($this->getTokenObject()) {
    170:             $this->getTokenObject()->setExpires($expiration);
    171:         }
    172:
    173:         return $this;
    174:     }
    175:
    176:     /**
    177:      * @deprecated
    178:      */
    179:     public function getExpiration()
    180:     {
    181:         $this->getLogger()->deprecated(__METHOD__, '::getTokenObject()->getExpires()');
    182:         if ($this->getTokenObject()) {
    183:             return $this->getTokenObject()->getExpires();
    184:         }
    185:     }
    186:
    187:     /**
    188:      * Set the tenant. If an integer is passed in, the SDK assumes you want to set the ID of the full Tenant object
    189:      * and sets this property accordingly. For any other data type, it assumes you want to populate the Tenant object.
    190:      * This ambiguity arises due to backwards compatibility.
    191:      *
    192:      * @param  string $tenant
    193:      * @return $this
    194:      */
    195:     public function setTenant($tenant)
    196:     {
    197:         $identity = IdentityService::factory($this);
    198:
    199:         if (is_numeric($tenant)) {
    200:
    201:             if (!$this->tenant) {
    202:                 $this->setTenantObject($identity->resource('Tenant'));
    203:             }
    204:             $this->tenant->setId($tenant);
    205:         } else {
    206:             $this->setTenantObject($identity->resource('Tenant', $tenant));
    207:         }
    208:
    209:         return $this;
    210:     }
    211:
    212:     /**
    213:      * Returns the tenant ID only (backwards compatibility).
    214:      *
    215:      * @return string
    216:      */
    217:     public function getTenant()
    218:     {
    219:         return ($this->getTenantObject()) ? $this->getTenantObject()->getId() : null;
    220:     }
    221:
    222:     /**
    223:      * Set the full Tenant object for this client.
    224:      *
    225:      * @param OpenCloud\Identity\Resource\Tenant $tenant
    226:      */
    227:     public function setTenantObject(Tenant $tenant)
    228:     {
    229:         $this->tenant = $tenant;
    230:     }
    231:
    232:     /**
    233:      * Get the full Tenant object for this client.
    234:      *
    235:      * @return OpenCloud\Identity\Resource\Tenant
    236:      */
    237:     public function getTenantObject()
    238:     {
    239:         return $this->tenant;
    240:     }
    241:
    242:     /**
    243:      * Set the service catalog.
    244:      *
    245:      * @param  mixed $catalog
    246:      * @return $this
    247:      */
    248:     public function setCatalog($catalog)
    249:     {
    250:         $this->catalog = Catalog::factory($catalog);
    251:
    252:         return $this;
    253:     }
    254:
    255:     /**
    256:      * Get the service catalog.
    257:      *
    258:      * @return array
    259:      */
    260:     public function getCatalog()
    261:     {
    262:         return $this->catalog;
    263:     }
    264:
    265:     /**
    266:      * @param Common\Log\LoggerInterface $logger
    267:      * @return $this
    268:      */
    269:     public function setLogger(Common\Log\LoggerInterface $logger)
    270:     {
    271:         $this->logger = $logger;
    272:
    273:         return $this;
    274:     }
    275:
    276:     /**
    277:      * @return Common\Log\LoggerInterface
    278:      */
    279:     public function getLogger()
    280:     {
    281:         if (null === $this->logger) {
    282:             $this->setLogger(new Common\Log\Logger);
    283:         }
    284:
    285:         return $this->logger;
    286:     }
    287:
    288:     /**
    289:      * @deprecated
    290:      */
    291:     public function hasExpired()
    292:     {
    293:         $this->getLogger()->deprecated(__METHOD__, 'getTokenObject()->hasExpired()');
    294:
    295:         return $this->getTokenObject() && $this->getTokenObject()->hasExpired();
    296:     }
    297:
    298:     /**
    299:      * Formats the credentials array (as a string) for authentication
    300:      *
    301:      * @return string
    302:      * @throws Common\Exceptions\CredentialError
    303:      */
    304:     public function getCredentials()
    305:     {
    306:         if (!empty($this->secret['username']) && !empty($this->secret['password'])) {
    307:
    308:             $credentials = array('auth' => array(
    309:                 'passwordCredentials' => array(
    310:                     'username' => $this->secret['username'],
    311:                     'password' => $this->secret['password']
    312:                 )
    313:             ));
    314:
    315:             if (!empty($this->secret['tenantName'])) {
    316:                 $credentials['auth']['tenantName'] = $this->secret['tenantName'];
    317:             } elseif (!empty($this->secret['tenantId'])) {
    318:                 $credentials['auth']['tenantId'] = $this->secret['tenantId'];
    319:             }
    320:
    321:             return json_encode($credentials);
    322:         } else {
    323:             throw new Exceptions\CredentialError(
    324:                 Lang::translate('Unrecognized credential secret')
    325:             );
    326:         }
    327:     }
    328:
    329:     /**
    330:      * @param $url
    331:      * @return $this
    332:      */
    333:     public function setAuthUrl($url)
    334:     {
    335:         $this->authUrl = Url::factory($url);
    336:
    337:         return $this;
    338:     }
    339:
    340:     /**
    341:      * @return Url
    342:      */
    343:     public function getAuthUrl()
    344:     {
    345:         return $this->authUrl;
    346:     }
    347:
    348:     /**
    349:      * Sets the current user based on the generated token.
    350:      *
    351:      * @param $data Object of user data
    352:      */
    353:     public function setUser(User $user)
    354:     {
    355:         $this->user = $user;
    356:     }
    357:
    358:     /**
    359:      * @return \OpenCloud\Identity\Resource\User
    360:      */
    361:     public function getUser()
    362:     {
    363:         return $this->user;
    364:     }
    365:
    366:     /**
    367:      * Authenticate the tenant using the supplied credentials
    368:      *
    369:      * @return void
    370:      * @throws AuthenticationError
    371:      */
    372:     public function authenticate()
    373:     {
    374:         $identity = IdentityService::factory($this);
    375:         $response = $identity->generateToken($this->getCredentials());
    376:
    377:         $body = Formatter::decode($response);
    378:
    379:         $this->setCatalog($body->access->serviceCatalog);
    380:         $this->setTokenObject($identity->resource('Token', $body->access->token));
    381:         $this->setUser($identity->resource('User', $body->access->user));
    382:
    383:         if (isset($body->access->token->tenant)) {
    384:             $this->setTenantObject($identity->resource('Tenant', $body->access->token->tenant));
    385:         }
    386:
    387:         // Set X-Auth-Token HTTP request header
    388:         $this->updateTokenHeader();
    389:     }
    390:
    391:     /**
    392:      * @deprecated
    393:      */
    394:     public function getUrl()
    395:     {
    396:         return $this->getBaseUrl();
    397:     }
    398:
    399:     /**
    400:      * Convenience method for exporting current credentials. Useful for local caching.
    401:      * @return array
    402:      */
    403:     public function exportCredentials()
    404:     {
    405:         if ($this->hasExpired()) {
    406:             $this->authenticate();
    407:         }
    408:
    409:         return array(
    410:             'token'      => $this->getToken(),
    411:             'expiration' => $this->getExpiration(),
    412:             'tenant'     => $this->getTenant(),
    413:             'catalog'    => $this->getCatalog()
    414:         );
    415:     }
    416:
    417:     /**
    418:      * Convenience method for importing credentials. Useful for local caching because it reduces HTTP traffic.
    419:      *
    420:      * @param array $values
    421:      */
    422:     public function importCredentials(array $values)
    423:     {
    424:         if (!empty($values['token'])) {
    425:             $this->setToken($values['token']);
    426:             $this->updateTokenHeader();
    427:         }
    428:         if (!empty($values['expiration'])) {
    429:             $this->setExpiration($values['expiration']);
    430:         }
    431:         if (!empty($values['tenant'])) {
    432:             $this->setTenant($values['tenant']);
    433:         }
    434:         if (!empty($values['catalog'])) {
    435:             $this->setCatalog($values['catalog']);
    436:         }
    437:     }
    438:
    439:     /**
    440:      * Sets the X-Auth-Token header. If no value is explicitly passed in, the current token is used.
    441:      *
    442:      * @param  string $token Optional value of token.
    443:      * @return void
    444:      */
    445:     private function updateTokenHeader($token = null)
    446:     {
    447:         $token = $token ? : $this->getToken();
    448:         $this->setDefaultOption('headers/X-Auth-Token', (string)$token);
    449:     }
    450:
    451:     /**
    452:      * Creates a new ObjectStore object (Swift/Cloud Files)
    453:      *
    454:      * @param string $name    The name of the service as it appears in the Catalog
    455:      * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
    456:      * @param string $urltype The URL type ("publicURL" or "internalURL")
    457:      * @return \OpenCloud\ObjectStore\Service
    458:      */
    459:     public function objectStoreService($name = null, $region = null, $urltype = null)
    460:     {
    461:         return ServiceBuilder::factory($this, 'OpenCloud\ObjectStore\Service', array(
    462:             'name'    => $name,
    463:             'region'  => $region,
    464:             'urlType' => $urltype
    465:         ));
    466:     }
    467:
    468:     /**
    469:      * Creates a new Compute object (Nova/Cloud Servers)
    470:      *
    471:      * @param string $name    The name of the service as it appears in the Catalog
    472:      * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
    473:      * @param string $urltype The URL type ("publicURL" or "internalURL")
    474:      * @return \OpenCloud\Compute\Service
    475:      */
    476:     public function computeService($name = null, $region = null, $urltype = null)
    477:     {
    478:         return ServiceBuilder::factory($this, 'OpenCloud\Compute\Service', array(
    479:             'name'    => $name,
    480:             'region'  => $region,
    481:             'urlType' => $urltype
    482:         ));
    483:     }
    484:
    485:     /**
    486:      * Creates a new Orchestration (Heat) service object
    487:      *
    488:      * @param string $name    The name of the service as it appears in the Catalog
    489:      * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
    490:      * @param string $urltype The URL type ("publicURL" or "internalURL")
    491:      * @return \OpenCloud\Orchestration\Service
    492:      * @codeCoverageIgnore
    493:      */
    494:     public function orchestrationService($name = null, $region = null, $urltype = null)
    495:     {
    496:         return ServiceBuilder::factory($this, 'OpenCloud\Orchestration\Service', array(
    497:             'name'    => $name,
    498:             'region'  => $region,
    499:             'urlType' => $urltype
    500:         ));
    501:     }
    502:
    503:     /**
    504:      * Creates a new Volume (Cinder) service object
    505:      *
    506:      * @param string $name    The name of the service as it appears in the Catalog
    507:      * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
    508:      * @param string $urltype The URL type ("publicURL" or "internalURL")
    509:      * @return \OpenCloud\Volume\Service
    510:      */
    511:     public function volumeService($name = null, $region = null, $urltype = null)
    512:     {
    513:         return ServiceBuilder::factory($this, 'OpenCloud\Volume\Service', array(
    514:             'name'    => $name,
    515:             'region'  => $region,
    516:             'urlType' => $urltype
    517:         ));
    518:     }
    519:
    520:     /**
    521:      * Creates a new Rackspace "Cloud Identity" service.
    522:      *
    523:      * @return \OpenCloud\Identity\Service
    524:      */
    525:     public function identityService()
    526:     {
    527:         $service = IdentityService::factory($this);
    528:         $this->authenticate();
    529:
    530:         return $service;
    531:     }
    532:
    533:     /**
    534:      * Creates a new Glance service
    535:      *
    536:      * @param string $name    The name of the service as it appears in the Catalog
    537:      * @param string $region  The region (DFW, IAD, ORD, LON, SYD)
    538:      * @param string $urltype The URL type ("publicURL" or "internalURL")
    539:      * @return Common\Service\ServiceInterface
    540:      */
    541:     public function imageService($name = null, $region = null, $urltype = null)
    542:     {
    543:         return ServiceBuilder::factory($this, 'OpenCloud\Image\Service', array(
    544:             'name'    => $name,
    545:             'region'  => $region,
    546:             'urlType' => $urltype
    547:         ));
    548:     }
    549: }
    550: