      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\Compute\Resource;
     19:
     20: use OpenCloud\Common\Resource\NovaResource;
     21: use OpenCloud\DNS\Resource\HasPtrRecordsInterface;
     22: use OpenCloud\Image\Resource\ImageInterface;
     23: use OpenCloud\Volume\Resource\Volume;
     24: use OpenCloud\Common\Exceptions;
     25: use OpenCloud\Common\Http\Message\Formatter;
     26: use OpenCloud\Common\Lang;
     27: use OpenCloud\Compute\Constants\ServerState;
     28: use OpenCloud\Compute\Service;
     29:
     30: /**
     31:  * A virtual machine (VM) instance in the Cloud Servers environment.
     32:  *
     33:  * @note This implementation supports extension attributes OS-DCF:diskConfig,
     34:  * RAX-SERVER:bandwidth, rax-bandwidth:bandwith.
     35:  */
     36: class Server extends NovaResource implements HasPtrRecordsInterface
     37: {
     38:     /**
     39:      * The server status. {@see \OpenCloud\Compute\Constants\ServerState} for supported types.
     40:      *
     41:      * @var string
     42:      */
     43:     public $status;
     44:
     45:     /**
     46:      * @var string The time stamp for the last update.
     47:      */
     48:     public $updated;
     49:
     50:     /**
     51:      * The compute provisioning algorithm has an anti-affinity property that
     52:      * attempts to spread customer VMs across hosts. Under certain situations,
     53:      * VMs from the same customer might be placed on the same host. $hostId
     54:      * represents the host your server runs on and can be used to determine this
     55:      * scenario if it is relevant to your application.
     56:      *
     57:      * @var string
     58:      */
     59:     public $hostId;
     60:
     61:     /**
     62:      * @var type Public and private IP addresses for this server.
     63:      */
     64:     public $addresses;
     65:
     66:     /**
     67:      * @var array Server links.
     68:      */
     69:     public $links;
     70:
     71:     /**
     72:      * The Image for this server.
     73:      *
     74:      * @link http://docs.rackspace.com/servers/api/v2/cs-devguide/content/List_Images-d1e4435.html
     75:      * @var ImageInterface
     76:      */
     77:     public $image;
     78:
     79:     /**
     80:      * The Flavor for this server.
     81:      *
     82:      * @link http://docs.rackspace.com/servers/api/v2/cs-devguide/content/List_Flavors-d1e4188.html
     83:      * @var type
     84:      */
     85:     public $flavor;
     86:
     87:     /**
     88:      * @var type
     89:      */
     90:     public $networks = array();
     91:
     92:     /**
     93:      * @var string The server ID.
     94:      */
     95:     public $id;
     96:
     97:     /**
     98:      * @var string The user ID.
     99:      */
    100:     public $user_id;
    101:
    102:     /**
    103:      * @var string The server name.
    104:      */
    105:     public $name;
    106:
    107:     /**
    108:      * @var string The time stamp for the creation date.
    109:      */
    110:     public $created;
    111:
    112:     /**
    113:      * @var string The tenant ID.
    114:      */
    115:     public $tenant_id;
    116:
    117:     /**
    118:      * @var string The public IP version 4 access address.
    119:      */
    120:     public $accessIPv4;
    121:
    122:     /**
    123:      * @var string The public IP version 6 access address.
    124:      */
    125:     public $accessIPv6;
    126:
    127:     /**
    128:      * The build completion progress, as a percentage. Value is from 0 to 100.
    129:      * @var int
    130:      */
    131:     public $progress;
    132:
    133:     /**
    134:      * @var string The root password (only populated on server creation).
    135:      */
    136:     public $adminPass;
    137:
    138:     /**
    139:      * @var mixed Metadata key and value pairs.
    140:      */
    141:     public $metadata;
    142:
    143:     protected static $json_name = 'server';
    144:     protected static $url_resource = 'servers';
    145:
    146:     /** @var string|object Keypair or string representation of keypair name */
    147:     public $keypair;
    148:
    149:     /**
    150:      * @var array Uploaded file attachments
    151:      */
    152:     private $personality = array();
    153:
    154:     /**
    155:      * @var type Image reference (for create)
    156:      */
    157:     private $imageRef;
    158:
    159:     /**
    160:      * @var type Flavor reference (for create)
    161:      */
    162:     private $flavorRef;
    163:
    164:     /**
    165:      * Cloud-init boot executable code
    166:      * @var string
    167:      */
    168:     public $user_data;
    169:
    170:     /**
    171:      * Creates a new Server object and associates it with a Compute service
    172:      *
    173:      * @param mixed $info
    174:      *      * If NULL, an empty Server object is created
    175:      *      * If an object, then a Server object is created from the data in the
    176:      *      object
    177:      *      * If a string, then it's treated as a Server ID and retrieved from the
    178:      *      service
    179:      *      The normal use case for SDK clients is to treat it as either NULL or an
    180:      *      ID. The object value parameter is a special case used to construct
    181:      *      a Server object from a ServerList element to avoid a secondary
    182:      *      call to the Service.
    183:      * @throws ServerNotFound if a 404 is returned
    184:      * @throws UnknownError if another error status is reported
    185:      */
    186:     public function __construct(Service $service, $info = null)
    187:     {
    188:         // make the service persistent
    189:         parent::__construct($service, $info);
    190:
    191:         // the metadata item is an object, not an array
    192:         $this->metadata = $this->metadata();
    193:     }
    194:
    195:     /**
    196:      * Returns the primary external IP address of the server
    197:      *
    198:      * This function is based upon the accessIPv4 and accessIPv6 values.
    199:      * By default, these are set to the public IP address of the server.
    200:      * However, these values can be modified by the user; this might happen,
    201:      * for example, if the server is behind a firewall and needs to be
    202:      * routed through a NAT device to be reached.
    203:      *
    204:      * @api
    205:      * @param integer $type the type of IP version (4 or 6) to return
    206:      * @return string IP address
    207:      */
    208:     public function ip($type = null)
    209:     {
    210:         switch ($type) {
    211:             default:
    212:             case 4:
    213:                 $value = $this->accessIPv4;
    214:                 break;
    215:             case 6:
    216:                 $value = $this->accessIPv6;
    217:                 break;
    218:         }
    219:
    220:         return $value;
    221:     }
    222:
    223:     /**
    224:      * {@inheritDoc}
    225:      */
    226:     public function create($params = array())
    227:     {
    228:         $this->id = null;
    229:         $this->status = null;
    230:
    231:         if (isset($params['imageId'])) {
    232:             $this->imageRef = $params['imageId'];
    233:         }
    234:
    235:         if (isset($params['flavorId'])) {
    236:             $this->flavorRef = $params['flavorId'];
    237:         }
    238:
    239:         return parent::create($params);
    240:     }
    241:
    242:     /**
    243:      * Rebuilds an existing server
    244:      *
    245:      * @api
    246:      * @param array $params - an associative array of key/value pairs of
    247:      *                      attributes to set on the new server
    248:      */
    249:     public function rebuild($params = array())
    250:     {
    251:         if (!isset($params['adminPass'])) {
    252:             throw new Exceptions\RebuildError(
    253:                 Lang::Translate('adminPass required when rebuilding server')
    254:             );
    255:         }
    256:
    257:         if (!isset($params['image'])) {
    258:             throw new Exceptions\RebuildError(
    259:                 Lang::Translate('image required when rebuilding server')
    260:             );
    261:         }
    262:
    263:         $object = (object) array(
    264:             'rebuild' => (object) array(
    265:                     'imageRef'  => $params['image']->id(),
    266:                     'adminPass' => $params['adminPass'],
    267:                     'name'      => (array_key_exists('name', $params) ? $params['name'] : $this->name)
    268:                 )
    269:         );
    270:
    271:         return $this->action($object);
    272:     }
    273:
    274:     /**
    275:      * Reboots a server
    276:      *
    277:      * A "soft" reboot requests that the operating system reboot itself; a "hard" reboot is the equivalent of pulling
    278:      * the power plug and then turning it back on, with a possibility of data loss.
    279:      *
    280:      * @api
    281:      * @param  string $type A particular reboot State. See Constants\ServerState for string values.
    282:      * @return \Guzzle\Http\Message\Response
    283:      */
    284:     public function reboot($type = null)
    285:     {
    286:         if (!$type) {
    287:             $type = ServerState::REBOOT_STATE_HARD;
    288:         }
    289:
    290:         $object = (object) array('reboot' => (object) array('type' => $type));
    291:
    292:         return $this->action($object);
    293:     }
    294:
    295:     /**
    296:      * Creates a new image from a server
    297:      *
    298:      * @api
    299:      * @param string $name     The name of the new image
    300:      * @param array  $metadata Optional metadata to be stored on the image
    301:      * @return boolean|Image New Image instance on success; FALSE on failure
    302:      * @throws Exceptions\ImageError
    303:      */
    304:     public function createImage($name, $metadata = array())
    305:     {
    306:         if (empty($name)) {
    307:             throw new Exceptions\ImageError(
    308:                 Lang::translate('Image name is required to create an image')
    309:             );
    310:         }
    311:
    312:         // construct a createImage object for jsonization
    313:         $object = (object) array('createImage' => (object) array(
    314:                 'name'     => $name,
    315:                 'metadata' => (object) $metadata
    316:             ));
    317:
    318:         $response = $this->action($object);
    319:
    320:         if (!$response || !($location = $response->getHeader('Location'))) {
    321:             return false;
    322:         }
    323:
    324:         return new Image($this->getService(), basename($location));
    325:     }
    326:
    327:     /**
    328:      * Schedule daily image backups
    329:      *
    330:      * @api
    331:      * @param mixed $retention - false (default) indicates you want to
    332:      *                         retrieve the image schedule. $retention <= 0 indicates you
    333:      *                         want to delete the current schedule. $retention > 0 indicates
    334:      *                         you want to schedule image backups and you would like to
    335:      *                         retain $retention backups.
    336:      * @return mixed an object or FALSE on error
    337:      * @throws Exceptions\ServerImageScheduleError if an error is encountered
    338:      */
    339:     public function imageSchedule($retention = false)
    340:     {
    341:         $url = $this->getUrl('rax-si-image-schedule');
    342:
    343:         if ($retention === false) {
    344:             // Get current retention
    345:             $request = $this->getClient()->get($url);
    346:         } elseif ($retention <= 0) {
    347:             // Delete image schedule
    348:             $request = $this->getClient()->delete($url);
    349:         } else {
    350:             // Set image schedule
    351:             $object = (object) array('image_schedule' =>
    352:                                         (object) array('retention' => $retention)
    353:             );
    354:             $body = json_encode($object);
    355:             $request = $this->getClient()->post($url, self::getJsonHeader(), $body);
    356:         }
    357:
    358:         $body = Formatter::decode($request->send());
    359:
    360:         return (isset($body->image_schedule)) ? $body->image_schedule : (object) array();
    361:     }
    362:
    363:     /**
    364:      * Initiates the resize of a server
    365:      *
    366:      * @api
    367:      * @param  Flavor  $flavorRef a Flavor object indicating the new server size
    368:      * @return boolean TRUE on success; FALSE on failure
    369:      */
    370:     public function resize(Flavor $flavorRef)
    371:     {
    372:         // construct a resize object for jsonization
    373:         $object = (object) array(
    374:             'resize' => (object) array('flavorRef' => $flavorRef->id)
    375:         );
    376:
    377:         return $this->action($object);
    378:     }
    379:
    380:     /**
    381:      * confirms the resize of a server
    382:      *
    383:      * @api
    384:      * @return boolean TRUE on success; FALSE on failure
    385:      */
    386:     public function resizeConfirm()
    387:     {
    388:         $object = (object) array('confirmResize' => null);
    389:         $response = $this->action($object);
    390:         $this->refresh($this->id);
    391:
    392:         return $response;
    393:     }
    394:
    395:     /**
    396:      * reverts the resize of a server
    397:      *
    398:      * @api
    399:      * @return boolean TRUE on success; FALSE on failure
    400:      */
    401:     public function resizeRevert()
    402:     {
    403:         $object = (object) array('revertResize' => null);
    404:
    405:         return $this->action($object);
    406:     }
    407:
    408:     /**
    409:      * Sets the root password on the server
    410:      *
    411:      * @api
    412:      * @param  string  $newPassword The new root password for the server
    413:      * @return boolean TRUE on success; FALSE on failure
    414:      */
    415:     public function setPassword($newPassword)
    416:     {
    417:         $object = (object) array(
    418:             'changePassword' => (object) array('adminPass' => $newPassword)
    419:         );
    420:
    421:         return $this->action($object);
    422:     }
    423:
    424:     /**
    425:      * Puts the server into *rescue* mode
    426:      *
    427:      * @api
    428:      * @link http://docs.rackspace.com/servers/api/v2/cs-devguide/content/rescue_mode.html
    429:      * @return string the root password of the rescue server
    430:      * @throws Exceptions\ServerActionError if the server has no ID (i.e., has not
    431:      *      been created yet)
    432:      */
    433:     public function rescue()
    434:     {
    435:         $this->checkExtension('os-rescue');
    436:
    437:         if (empty($this->id)) {
    438:             throw new Exceptions\ServerActionError(
    439:                 Lang::translate('Server has no ID; cannot Rescue()')
    440:             );
    441:         }
    442:
    443:         $data = (object) array('rescue' => 'none');
    444:
    445:         $response = $this->action($data);
    446:         $body = Formatter::decode($response);
    447:
    448:         return (isset($body->adminPass)) ? $body->adminPass : false;
    449:     }
    450:
    451:     /**
    452:      * Takes the server out of RESCUE mode
    453:      *
    454:      * @api
    455:      * @link http://docs.rackspace.com/servers/api/v2/cs-devguide/content/rescue_mode.html
    456:      * @return HttpResponse
    457:      * @throws Exceptions\ServerActionError if the server has no ID (i.e., has not
    458:      *      been created yet)
    459:      */
    460:     public function unrescue()
    461:     {
    462:         $this->checkExtension('os-rescue');
    463:
    464:         if (!isset($this->id)) {
    465:             throw new Exceptions\ServerActionError(Lang::translate('Server has no ID; cannot Unescue()'));
    466:         }
    467:
    468:         $object = (object) array('unrescue' => null);
    469:
    470:         return $this->action($object);
    471:     }
    472:
    473:     /**
    474:      * Retrieves the metadata associated with a Server.
    475:      *
    476:      * If a metadata item name is supplied, then only the single item is
    477:      * returned. Otherwise, the default is to return all metadata associated
    478:      * with a server.
    479:      *
    480:      * @api
    481:      * @param string $key - the (optional) name of the metadata item to return
    482:      * @return ServerMetadata object
    483:      * @throws Exceptions\MetadataError
    484:      */
    485:     public function metadata($key = null)
    486:     {
    487:         return new ServerMetadata($this, $key);
    488:     }
    489:
    490:     /**
    491:      * Returns the IP address block for the Server or for a specific network.
    492:      *
    493:      * @api
    494:      * @param string $network - if supplied, then only the IP(s) for the
    495:      *                        specified network are returned. Otherwise, all IPs are returned.
    496:      * @return object
    497:      * @throws Exceptions\ServerIpsError
    498:      */
    499:     public function ips($network = null)
    500:     {
    501:         $url = Lang::noslash($this->Url('ips/' . $network));
    502:
    503:         $response = $this->getClient()->get($url)->send();
    504:         $body = Formatter::decode($response);
    505:
    506:         return (isset($body->addresses)) ? $body->addresses :
    507:             ((isset($body->network)) ? $body->network : (object) array());
    508:     }
    509:
    510:     /**
    511:      * Attaches a volume to a server
    512:      *
    513:      * Requires the os-volumes extension. This is a synonym for
    514:      * `VolumeAttachment::create()`
    515:      *
    516:      * @api
    517:      * @param OpenCloud\Volume\Resource\Volume $volume The volume to attach. If
    518:      *                                                 "auto" is specified (the default), then the first available
    519:      *                                                 device is used to mount the volume (for example, if the primary
    520:      *                                                 disk is on `/dev/xvhda`, then the new volume would be attached
    521:      *                                                 to `/dev/xvhdb`).
    522:      * @param string                           $device the device to which to attach it
    523:      */
    524:     public function attachVolume(Volume $volume, $device = 'auto')
    525:     {
    526:         $this->checkExtension('os-volumes');
    527:
    528:         return $this->volumeAttachment()->create(array(
    529:             'volumeId' => $volume->id,
    530:             'device'   => ($device == 'auto' ? null : $device)
    531:         ));
    532:     }
    533:
    534:     /**
    535:      * Removes a volume attachment from a server
    536:      *
    537:      * Requires the os-volumes extension. This is a synonym for
    538:      * `VolumeAttachment::delete()`
    539:      * @param OpenCloud\Volume\Resource\Volume $volume The volume to remove
    540:      */
    541:     public function detachVolume(Volume $volume)
    542:     {
    543:         $this->checkExtension('os-volumes');
    544:
    545:         return $this->volumeAttachment($volume->id)->delete();
    546:     }
    547:
    548:     /**
    549:      * Returns a VolumeAttachment object
    550:      *
    551:      */
    552:     public function volumeAttachment($id = null)
    553:     {
    554:         $resource = new VolumeAttachment($this->getService());
    555:         $resource->setParent($this)->populate($id);
    556:
    557:         return $resource;
    558:     }
    559:
    560:     /**
    561:      * Returns a Collection of VolumeAttachment objects
    562:      * @return Collection
    563:      */
    564:     public function volumeAttachmentList()
    565:     {
    566:         return $this->getService()->collection(
    567:             'OpenCloud\Compute\Resource\VolumeAttachment', null, $this
    568:         );
    569:     }
    570:
    571:     /**
    572:      * Adds a "personality" file to be uploaded during create() or rebuild()
    573:      *
    574:      * @api
    575:      * @param string $path The path where the file will be stored on the
    576:      *                     target server (up to 255 characters)
    577:      * @param string $data the file contents (max size set by provider)
    578:      * @return void
    579:      */
    580:     public function addFile($path, $data)
    581:     {
    582:         $this->personality[$path] = base64_encode($data);
    583:     }
    584:
    585:     /**
    586:      * Returns a console connection
    587:      * Note: Where is this documented?
    588:      *
    589:      * @codeCoverageIgnore
    590:      */
    591:     public function console($type = 'novnc')
    592:     {
    593:         $action = (strpos('spice', $type) !== false) ? 'os-getSPICEConsole' : 'os-getVNCConsole';
    594:         $object = (object) array($action => (object) array('type' => $type));
    595:
    596:         $response = $this->action($object);
    597:         $body = Formatter::decode($response);
    598:
    599:         return (isset($body->console)) ? $body->console : false;
    600:     }
    601:
    602:     protected function createJson()
    603:     {
    604:         // Convert some values
    605:         $this->metadata->sdk = $this->getService()->getClient()->getUserAgent();
    606:
    607:         if ($this->image instanceof ImageInterface) {
    608:             $this->imageRef = $this->image->getId();
    609:         }
    610:         if ($this->flavor instanceof Flavor) {
    611:             $this->flavorRef = $this->flavor->id;
    612:         }
    613:
    614:         // Base object
    615:         $server = (object) array(
    616:             'name'      => $this->name,
    617:             'imageRef'  => $this->imageRef,
    618:             'flavorRef' => $this->flavorRef
    619:         );
    620:
    621:         if ($this->metadata->count()) {
    622:             $server->metadata = $this->metadata->toArray();
    623:         }
    624:
    625:         // Networks
    626:         if (is_array($this->networks) && count($this->networks)) {
    627:
    628:             $server->networks = array();
    629:
    630:             foreach ($this->networks as $network) {
    631:                 if (!$network instanceof Network) {
    632:                     throw new Exceptions\InvalidParameterError(sprintf(
    633:                         'When creating a server, the "networks" key must be an ' .
    634:                         'array of OpenCloud\Compute\Network objects with valid ' .
    635:                         'IDs; variable passed in was a [%s]',
    636:                         gettype($network)
    637:                     ));
    638:                 }
    639:                 if (empty($network->id)) {
    640:                     $this->getLogger()->warning('When creating a server, the '
    641:                         . 'network objects passed in must have an ID'
    642:                     );
    643:                     continue;
    644:                 }
    645:                 // Stock networks array
    646:                 $server->networks[] = (object) array('uuid' => $network->id);
    647:             }
    648:         }
    649:
    650:         // Personality files
    651:         if (!empty($this->personality)) {
    652:             $server->personality = array();
    653:             foreach ($this->personality as $path => $data) {
    654:                 // Stock personality array
    655:                 $server->personality[] = (object) array(
    656:                     'path'     => $path,
    657:                     'contents' => $data
    658:                 );
    659:             }
    660:         }
    661:
    662:         // Keypairs
    663:         if (!empty($this->keypair)) {
    664:             if (is_string($this->keypair)) {
    665:                 $server->key_name = $this->keypair;
    666:             } elseif (isset($this->keypair['name']) && is_string($this->keypair['name'])) {
    667:                 $server->key_name = $this->keypair['name'];
    668:             } elseif ($this->keypair instanceof Keypair && $this->keypair->getName()) {
    669:                 $server->key_name = $this->keypair->getName();
    670:             }
    671:         }
    672:
    673:         // Cloud-init executable
    674:         if (!empty($this->user_data)) {
    675:             $server->user_data = $this->user_data;
    676:         }
    677:
    678:         return (object) array('server' => $server);
    679:     }
    680:
    681:     protected function updateJson($params = array())
    682:     {
    683:         return (object) array('server' => (object) $params);
    684:     }
    685: }
    686: