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