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: