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