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