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