Project

General

Profile

Get Started with the Calamari REST API and PHP » History » Version 3

Jessica Mack, 07/03/2015 11:15 PM

1 1 Jessica Mack
h1. Get Started with the Calamari REST API and PHP
2
3
{{toc}}
4
5
h3. Introduction
6
7 2 Jessica Mack
So you've spent some time getting your "Ceph":http://ceph.com/ cluster configured just the way you like it, everything is balances, your data is being read and written safely and securely and things are humming along nicely. But then, when you least expect it, an SSD goes down, you've lost a journal and an OSD. Ceph is self-healing, so you're not too worried about data loss...but you still need to know which nodes are affected and, once you swap in a new SSD, you need to be able to monitor things for a few hours or days to make sure it was an isolated incident and not the first segment of a larger problem.
8
It's precisely to address these sorts of monitoring and control needs that "Calamari":http://calamari.readthedocs.org/ was created. Calamari is an API for Ceph, allowing Ceph administrators to get a birds-eye view of Ceph cluster status and providing an all-in-one management and monitoring service for cluster elements. Like Ceph itself, it's open source and freely downloadable from Github.
9 1 Jessica Mack
In this article, I'll introduce you to Calamari, show you how to install it and connect it to your Ceph cluster, and then run you through a few quick examples of how to use it. Once you understand how it works, you can easily integrate Ceph with your own user interface or with third-party applications using the Calamari API. Keep reading, you're going to enjoy this!
10
11
h3. Understanding Calamari
12
13 2 Jessica Mack
"Calamari":https://github.com/ceph/calamari is a management and monitoring service for Ceph exposing a high-level REST API. One of the primary goals for the REST API is to make it easy to integrate Ceph with your own user interface. A reference implementation for this user interface already exists as "Romana":https://github.com/ceph/romana.
14
The Calamari REST API is different from "the REST API included with Ceph itself":http://ceph.com/docs/master/man/8/ceph-rest-api/. The Ceph REST API functions more as a wrapper around equivalent Ceph CLI commands, while the Calamari REST API operates at a higher level and allows API consumers to manipulate Ceph objects using GET/POST/PUT/DELETE commands.
15 1 Jessica Mack
This tutorial will focus specifically on using the Calamari REST API to perform common operations on your Ceph cluster. My weapon of choice for interacting with the REST API will be PHP, although you should be able to easily migrate the code listings to other programming languages as well.
16
17
h3. Assumptions and Requirements
18
19
To follow the steps in this tutorial, you'll need an Ubuntu or CentOS server on which to install Calamari and your PHP scripts. This server should be network-accessible by your Ceph cluster. Although I'll be using a VirtualBox server running CentOS 7 for this tutorial, the steps below have also been tested to work on Amazon Web Services (AWS) cloud servers running both Ubuntu and CentOS.
20
Before we get started, here are a few key assumptions that I'll be making:
21 2 Jessica Mack
* You have a working knowledge of "CentOS":https://www.centos.org/, "VirtualBox":https://www.virtualbox.org/ and VirtualBox networking.
22
* You have downloaded and installed the latest version of VirtualBox.
23
* You're familiar with installing software using yum, the CentOS package manager.
24
* You're familiar with common git operations.
25
* You have a working understanding of REST APIs and common REST operations.
26
* You have some familiarity with PHP scripting.
27
* You have administrative access to a working Ceph cluster with at least 3 nodes.
28
29 1 Jessica Mack
In case you’re not familiar with the above topics, look in the “Read More” section at the end of this tutorial, which has links to relevant guides.
30
Did you check all the boxes above? Let's get started!
31
32
h3. Step 1: Install Calamari
33
34
To begin, initialize a new VirtualBox server with CentOS 7, configure networking to make sure it's connect to the Internet and is also accessible by your Ceph cluster, and then run the following commands as root to install some basic necessities:
35 2 Jessica Mack
@shell> yum update
36 1 Jessica Mack
shell> yum install git
37
shell> yum install sudo
38 2 Jessica Mack
shell> yum install wget@
39
Next, create a _calamari_ user account and add it to the _sudoers_ list:
40
@shell> useradd calamari
41 1 Jessica Mack
shell> passwd calamari
42 2 Jessica Mack
shell> gpasswd -a calamari wheel@
43
You should now log in as the _calamari_ user, clone the Calamari repository and install Calamari, as below:
44
@shell> git clone https://github.com/ceph/calamari.git calamari_source
45 1 Jessica Mack
shell> cd calamari_source
46 2 Jessica Mack
shell> ./vps_bootstrap.sh@
47
The _vps_bootstrap.sh_ script downloads all the necessary packages for Calamari and sets up the development environment, including the PostgreSQL database. Here's an example of what you will see during the process:
48
49
!image1.png!
50
51
The installation process should have created a _~/calamari_ directory. Change to this directory and activate the created Python virtual environment in _~/calamari/env:_
52
@shell> cd ~/calamari
53
shell> source env/bin/activate@
54 1 Jessica Mack
You should now be able to start up the various server processes by running the command below:
55 2 Jessica Mack
@shell> cd ~/calamari
56
shell> supervisord -n -c dev/supervisord.conf@
57 1 Jessica Mack
Here's an example of what you should see:
58 2 Jessica Mack
59
!image2.png!
60 1 Jessica Mack
 
61 2 Jessica Mack
NOTE: In case you see a number of errors related to _salt-master_ respawning and exiting very quickly, run the following commands, which will correct some Salt directory permissions, then try running the previous command again.
62
63
@shell> cd ~/calamari
64 1 Jessica Mack
shell> sudo salt-master -d
65 2 Jessica Mack
shell> killall salt-master@
66
67 1 Jessica Mack
You should now be able to browse to your server on port 8000 and see the Calamari REST API interface. For example, if your server IP address is 192.168.56.106, browse to http://192.168.56.106:8000/api/v2/ and you should get an HTTP 503 Forbidden response that looks like this:
68 2 Jessica Mack
69
!image3.png!
70
71 1 Jessica Mack
If you're not able to access the Calamari API interface on port 8000, check that your firewall is set up to allow connections on that port. For example, you could run the following command to allow connections on port 8000:
72 2 Jessica Mack
@shell> sudo iptables -A INPUT -p tcp  -m tcp --dport 8000 -j ACCEPT@
73
By default, the _vps_bootstrap.sh_ script creates an administrative user for Calamari named _admin_ with password _admin_. Click the "Log In" menu item and enter these credentials, and you should now see an HTTP 200 OK response and the output of the _/api/v2/_ API call, as shown below:
74 1 Jessica Mack
image4.png
75
At this point, the Calamari server is running and you can proceed to connect it with your Ceph cluster.
76
77
h3. Step 2: Connect Calamari with Your Ceph Cluster
78
79
Ceph cluster nodes communicate with Calamari using Salt minions. To connect a Ceph node to Calamari, follow these steps:
80 2 Jessica Mack
* Log in to the Ceph node as the _ceph_ user.
81
* Run the commands below to install and start the Salt minion on the node. Remember to replace the master's IP address in the fourth command with the IP address or host name for your Calamari server.
82
@shell> wget https://raw.github.com/saltstack/sal...tstrap-salt.sh
83 1 Jessica Mack
shell> chmod +x bootstrap-salt.sh
84
shell> sudo bootstrap-salt.sh
85
shell> sudo sh -c "echo 'master: 192.168.56.106' >> /etc/salt/minion"
86 2 Jessica Mack
shell> sudo service salt-minion restart@
87
88 1 Jessica Mack
Here's a sample of what you will see as the Salt minion starts and connects to the Salt master.
89 2 Jessica Mack
90
!image5.png!
91
92 1 Jessica Mack
Repeat the steps above for each Ceph node in the cluster.
93
Once all the Salt minions are installed and running, you must accept their keys using the Salt master running on the Calamari server. To do this:
94 2 Jessica Mack
* Log in to the Calamari node as the _calamari_ user.
95
* Run the following commands to list and accept all pending keys:
96
@shell> cd ~/calamari
97 1 Jessica Mack
shell> source env/bin/activate
98
shell> salt-key -c dev/etc/salt -L
99 2 Jessica Mack
shell> salt-key -c dev/etc/salt -A@
100
101
!image6.png!
102
103
Now, browse to your Calamari server at port 8000 and access the URL endpoint at _/api/v2/_cluster after logging in - for example, at http://192.168.56.106:8000/api/v2/cluster. You should now see your cluster listed, as shown below:
104
105
!image7.png!
106
107
To get a quick overview of the various nodes and their roles in the cluster, try browsing to the _/api/v2/_server endpoint, and you'll see a listing like the one below:
108
109
!image8.png!
110
111 1 Jessica Mack
Once you're satisfied that you've got the API working and providing "real" information, flip over to the next page and I'll show you how to perform some common tasks with it from a custom application!
112
113
h3. Step 3: Install PHP and Guzzle
114
115
I'll be using PHP to build a few simple scripts that interact with the Calamari API. Obviously, to do this, you need a server with PHP installed. This can be either the Calamari server itself, or another server or development environment.
116
Begin by installing PHP and Apache (if you don't already have them). On CentOS-based systems, use the following command:
117 2 Jessica Mack
@shell> sudo yum install php curl@
118 1 Jessica Mack
If you're using a Debian or Ubuntu-based system, use the following command instead:
119 2 Jessica Mack
@shell> sudo apt-get install php5 php5-curl curl@
120 1 Jessica Mack
Next, create a working directory for your PHP files under your Web server's document root. Download Composer, the PHP dependency manager, into this directory.
121 2 Jessica Mack
@shell> cd /var/www/html
122 1 Jessica Mack
shell> mkdir calamari-php
123
shell> cd calamari-php
124 2 Jessica Mack
shell> curl -sS https://getcomposer.org/installer | php@
125
Create a _composer.json_ file in the working directory and fill it with the following content:
126
<pre>
127 1 Jessica Mack
{
128
    "require": {
129
            "guzzlehttp/guzzle": "~5.0"
130
    }
131
}
132 2 Jessica Mack
</pre>
133
You should now be able to run _php composer.phar_ install and have Composer download the Guzzle HTTP client for PHP and any related dependencies.
134
@shell> cd /var/www/html/calamari-php
135
shell> php composer.phar install@
136 1 Jessica Mack
137
h3. Step 4: Handle API Authentication
138
139
Before you can begin using the API, you need to understand how to authenticate against it. Here's a quick overview of the steps involved:
140 2 Jessica Mack
# Begin by sending a GET request to _/api/v2/auth/login_.
141
# You should receive an HTTP 200 OK response containing a cookie named XSRF-TOKEN. Retrieve the value of this cookie.
142
# Follow up with a POST request to _/api/v2/auth/login_ containing:
143
** A header named X-XSRF-TOKEN with the token value.
144
** A JSON-encoded body containing the username and password - for example @{username: "admin", password: "admin"}@.
145
# You should receive an HTTP 200 OK response containing a session cookie.
146
# You can now proceed to use the API as usual, always remembering to include the session cookie in your requests. Once done, destroy your session by sending an empty POST request _/api/v2/auth/logout_.
147
148 1 Jessica Mack
In all the code examples that follow, remember to replace the host name, user name and password shown with correct values for your environment.
149 2 Jessica Mack
150 1 Jessica Mack
Translating the steps above into PHP code, here's what you'll end up with:
151 2 Jessica Mack
<pre><code class="php">
152 1 Jessica Mack
<?php
153
// load classes
154
include 'vendor/autoload.php';
155
use GuzzleHttp\Client as Client;
156
use GuzzleHttp\Cookie\CookieJar as CookieJar;
157
 
158
// define host and credentials
159
$config = new stdClass;
160
$config->user = 'admin';
161
$config->password = 'admin';
162
$config->host = '192.168.56.106';
163
 
164
// initialize client and cookie jar
165
$client = new Client(['base_url' => 'http://' . $config->host . ':8000']);
166
$jar = new GuzzleHttp\Cookie\CookieJar();
167
 
168
// initial request
169
// get the token
170
$response = $client->get('/api/v2/auth/login',  ['cookies' => $jar]);
171
$cookies = $jar->toArray();
172
foreach ($cookies as $c) {
173
  if ($c['Name'] == 'XSRF-TOKEN') {
174
    $token = $c['Value'];
175
  }
176
}
177
 
178
// authentication request
179
// include the token from the previous step
180
$response = $client->post('/api/v2/auth/login', [
181
  'cookies' => $jar,
182
  'headers' =>  ['X-XSRF-TOKEN' => $token, 'Content-type' => 'application/json; charset=UTF-8'],
183
  'body' => json_encode(array('username' => $config->user, 'password' => $config->password))
184
]);
185
 
186
// authenticated request for /api/v2/cluster
187
$response = $client->get('/api/v2/cluster', [
188
  'cookies' => $jar,
189
  'headers' =>  ['X-XSRF-TOKEN' => $token, 'Content-type' => 'application/json; charset=UTF-8'],
190
]);
191
echo $response->getBody();
192
 
193
// logout request
194
$client->post('/api/v2/auth/logout');
195 2 Jessica Mack
</code></pre>
196
197 1 Jessica Mack
As discussed previously, the code above demonstrates an initial GET request, followed by a POST request containing authentication credentials. A common cookie jar is used throughout to store the session cookie and once authenticated, this session cookie accompanies all subsequent requests.
198
Here's an example of the output of the previous code:
199 2 Jessica Mack
200
!image9.png!
201
202 1 Jessica Mack
Since these steps will need to be performed every time you use the Calamari API, it makes sense to encapsulate them into a class. Here's an example of a custom CalamariClient class, which extends the base Guzzle HTTP Client class with some additional methods:
203 2 Jessica Mack
204
<pre><code class="php">
205 1 Jessica Mack
<?php
206
// load classes
207
include 'vendor/autoload.php';
208
use GuzzleHttp\Client as Client;
209
use GuzzleHttp\Cookie\CookieJar as CookieJar;
210
 
211
class CalamariClient extends Client {
212
  // cookie jar
213
  private $jar;
214
 
215
  // initialize client with cookie jar and base URL
216
  public function __construct(array $config = [], $host) {
217
    $this->jar = new CookieJar();
218
    $config['base_url'] = 'http://' . $host . ':8000';
219
    $config['defaults']['cookies'] = $this->jar;
220
    return parent::__construct($config);
221
  }
222
  // add token to all requests
223
  public function createRequest($method, $url = null, array $options = []) {
224
    if ($token = $this->getToken($this->jar)) {
225
      $options['headers'] = array('X-XSRF-TOKEN' => $token, 'Content-type' => 'application/json; charset=UTF-8');
226
    }
227
    return parent::createRequest($method, $url, $options);
228
  }
229
 
230
  // perform login
231
  public function login($username, $password) {
232
    $request = $this->get('/api/v2/auth/login');
233
    $request = $this->post('/api/v2/auth/login', [
234
          'headers' =>  ['X-XSRF-TOKEN' => $this->getToken($this->jar), 'Content-type' => 'application/json; charset=UTF-8'],
235
          'body' => json_encode(array('username' => $username, 'password' => $password))
236
    ]);
237
    return $this;
238
  }
239
 
240
  // perform logout
241
  public function logout() {
242
    $request = $this->post('/api/v2/auth/logout');
243
    return $this;
244
  }
245
 
246
  public function getToken($jar) {
247
    $cookies = $jar->toArray();
248
    foreach ($cookies as $c) {
249
      if ($c['Name'] == 'XSRF-TOKEN') {
250
        return $c['Value'];
251
      }
252
    }
253
    return false;
254
  }
255
 
256
}
257 2 Jessica Mack
</code></pre>
258
259 1 Jessica Mack
And here's how you'd use it:
260 2 Jessica Mack
261
<pre><code class="php">
262 1 Jessica Mack
<?php
263
use CalamariClient;
264
try {
265
  $client = new CalamariClient(array(), '192.168.56.106');
266
  if ($client->login('admin', 'admin')) {
267
    $response = $client->get('/api/v2/cluster');
268
    echo $response->getBody();
269
    $client->logout();
270
  }
271
} catch (Exception $e) {
272
  die('ERROR: ' . $e->getMessage());
273
}
274 2 Jessica Mack
</code></pre>
275 1 Jessica Mack
276
h3. Step 5: Start Using the API
277
278
Now that you've got a working, authenticated client, let's look at performing a few common tasks using the Calamari API. First up, checking cluster health and status.
279 2 Jessica Mack
Although v2 of the Calamari API doesn't include a specific method to report cluster health, this is available in v1, as the /_api/v1/cluster/{fsid}/health_ method. To see how it works, consider the following script, which requests this endpoint and processes the response:
280
<pre><code class="php">
281 1 Jessica Mack
<?php
282
use CalamariClient;
283
try {
284
  $client = new CalamariClient(array(), '192.168.56.106');
285
  if ($client->login('admin', 'admin')) {
286
    $response = $client->get('/api/v2/cluster');
287
    $data = json_decode($response->getBody());
288
    $fsid = $data[0]->id;
289
    $response = $client->get('/api/v1/cluster/' . $fsid . '/health');
290
    $data = json_decode($response->getBody());
291
    echo 'Current cluster status: ' . $data->report->overall_status;
292
    $client->logout();
293
  }
294
} catch (Exception $e) {
295
  die('ERROR: ' . $e->getMessage());
296
}
297 2 Jessica Mack
</code></pre>
298
299
Here, the authenticated client makes two requests to the API. The first retrieves the list of available cluster and selects the ID of the first one. The second sends a GET request to the _/api/v1/cluster/{fsid}/health_ endpoint. The JSON-encoded response includes an _overall_status_ field which includes the current cluster health. Here's what it looks like:
300
301
!image10.png!
302
303 1 Jessica Mack
You can also use the Calamari API to map services to hosts, by requesting the /api/v2/cluster/{fsid}/server endpoint. This will return a list of all the nodes that are part of the cluster, together with information on the services running on each. Here's an example of the code:
304 2 Jessica Mack
305 3 Jessica Mack
<pre><code class="php">
306 1 Jessica Mack
<?php
307
use CalamariClient;
308
try {
309
  $client = new CalamariClient(array(), '192.168.56.106');
310
  if ($client->login('admin', 'admin')) {
311
    $response = $client->get('/api/v2/cluster');
312
    $data = json_decode($response->getBody());
313
    $fsid = $data[0]->id;
314
    $response = $client->get('/api/v2/cluster/' . $fsid . '/server');
315
    $nodes = json_decode($response->getBody());
316
    if (count($nodes)) {
317
      echo '<ul>';
318
      foreach ($nodes as $n) {
319
        echo '<li>' . $n->fqdn . '</li>';
320
        if (count($n->services)) {
321
          echo '<ul>';
322
          foreach ($n->services as $s) {
323
            echo '<li>' . $s->type . ' (id: ' . $s->fsid . ')</li>';
324
          }
325
          echo '</ul>';
326
        }
327
      }
328
      echo '</ul>';    
329
    }
330
    $client->logout();
331
  }
332
} catch (Exception $e) {
333
  die('ERROR: ' . $e->getMessage());
334
}
335 3 Jessica Mack
</code></pre>
336
337
The output of the _/api/v2/cluster/{fsid}/server_ endpoint is a JSON-encoded collection of nodes and services ("see an example":http://calamari.readthedocs.org/en/latest/calamari_rest/resources/api_example_api_v2_cluster__fsid__server.html). It's quite easy to convert this collection into a PHP array with the _json_decode()_ function and then iterate over the array, printing each node and its services as a nested HTML list. That's what the code above does, and here's an example of what the output looks like:
338
339
!image11.png!
340 1 Jessica Mack
 
341 3 Jessica Mack
There are a couple of important points of difference between the _/api/v2/server_ and the _/api/v2/cluster/{fsid}/server_ endpoints. The former lists all nodes and services that Calamari knows about, while the latter only lists nodes and services that are part of the specified cluster. If you have an object gateway configured on one of your nodes, it will appear in the output of the former, but not in the output of the latter, since at the time of writing, Calamari does not yet include management integration with the Ceph Object Gateway.
342
343
Another common use case involves creating a new pool using the API. This is accomplished by sending a POSt request to the _/api/v2/cluster/{fsid}/pool_ endpoint and including, in the body of the request, a JSON-encoded packet containing the name of the pool and the number of placement groups ("see an example":http://calamari.readthedocs.org/en/latest/calamari_rest/resources/api_example_api_v2_cluster__fsid__pool.html). Here's an example of the code:
344
345
<pre><code class="php">
346 1 Jessica Mack
<?php
347
use CalamariClient;
348
try {
349
  $client = new CalamariClient(array(), '192.168.56.106');
350
  if ($client->login('admin', 'admin')) {
351
    $response = $client->get('/api/v2/cluster');
352
    $data = json_decode($response->getBody());
353
    $fsid = $data[0]->id;
354
    $response = $client->post('/api/v2/cluster/' . $fsid . '/pool', [
355
      'body' => json_encode(array('name' => 'work', 'pg_num' => 100))
356
    ]);
357
    $client->logout();
358
  }
359
} catch (Exception $e) {
360
  die('ERROR: ' . $e->getMessage());
361
}
362 3 Jessica Mack
</code></pre>
363
364 1 Jessica Mack
After you run this code, switch over to your cluster and you should be able to see the new pool, as shown below:
365
 
366 3 Jessica Mack
!image12.png!
367 1 Jessica Mack
 
368 3 Jessica Mack
Although the API includes 50+ methods, there's always the possibility that it might be missing one you need. For those situations, you have the _/api/v2/cluster/{fsid}/cli_ method. This method works as an interface to the Ceph CLI, executing the provided command and returning the results as a JSON-encoded response.
369
To see how it works, consider the following script, which executes the _ceph status_ command using the REST API:
370
371
<pre><code class="php">
372 1 Jessica Mack
<?php
373
use CalamariClient;
374
try {
375
  $client = new CalamariClient(array(), '192.168.56.106');
376
  if ($client->login('admin', 'admin')) {
377
    $response = $client->get('/api/v2/cluster');
378
    $data = json_decode($response->getBody());
379
    $fsid = $data[0]->id;
380
    $response = $client->post('/api/v2/cluster/' . $fsid . '/cli', [
381
      'body' => json_encode(array('command' => 'status'))
382
    ]);
383
    $data = json_decode($response->getBody());
384
    echo 'Output of ceph status: <pre>' . $data->out . '</pre>';
385
    $client->logout();
386
  }
387
} catch (Exception $e) {
388
  die('ERROR: ' . $e->getMessage());
389
}
390 3 Jessica Mack
</code></pre>
391
392
Here, the authenticated client makes two requests to the API. The first retrieves the list of available cluster and selects the ID of the first one. The second sends a POST request with the status command in the body to the _/api/v2/cluster/{fsid}/cli_ endpoint. The JSON-encoded response includes an _out_ field which includes the output of the command. Here's what it looks like:
393
394
!image13.png!
395
396 1 Jessica Mack
If you wanted to, you could easily revise the previous listing to build an interactive tool that accepts commands through a Web browser and returns the result. Here's how:
397 3 Jessica Mack
<pre>
398 1 Jessica Mack
  <form method="post">
399
    Ceph CLI command: ceph <input name="command" type="text" />
400
    <input  type="submit" name="submit" value="Run" />
401
    <br/>
402
    Example: To run 'ceph osd dump', enter 'osd dump' in the field above.    
403
  </form>
404 3 Jessica Mack
</pre>
405 1 Jessica Mack
 
406 3 Jessica Mack
<pre><code class="php">
407 1 Jessica Mack
<?php
408
  use CalamariClient;
409
  if (isset($_POST['submit'])) {
410
    try {
411
      $client = new CalamariClient(array(), '192.168.56.106');
412
      if ($client->login('admin', 'admin')) {
413
        $response = $client->get('/api/v2/cluster');
414
        $data = json_decode($response->getBody());
415
        $fsid = $data[0]->id;
416
        $response = $client->post('/api/v2/cluster/' . $fsid . '/cli', [
417
          'body' => json_encode(array('command' => $_POST['command']))
418
        ]);
419
        $data = json_decode($response->getBody());
420
        echo 'Output of \'ceph ' . $_POST['command'] . '\': <pre>' . $data->out . '</pre>';
421
        $client->logout();
422
      }
423
    } catch (Exception $e) {
424
      die('ERROR: ' . $e->getMessage());
425
    }
426
 }
427 3 Jessica Mack
</code></pre>
428
429 1 Jessica Mack
In this version, the user's form input is transferred into the POST request as a command and the output is displayed as a Web page. Here's an example of it in action:
430 3 Jessica Mack
431
!image14.png!
432 1 Jessica Mack
433
h3. Conclusion
434
435
As you might imagine, there's a lot more you can do with Calamari. This tutorial has illustrated a few common use cases, but the Calamari API also lets you work with CRUSH maps, monitor nodes, OSDs, pools, user accounts and more. With Calamari, you have everything you need to integrate Ceph cluster operations with your enterprise workflows or build your own management and monitoring solution for Ceph.
436
437
h3. Read More
438
439
* "Introduction to Ceph":http://ceph.com/docs/master/start/intro/
440
* "Introduction to Calamari":http://calamari.readthedocs.org/
441
* "Calamari API Documentation":http://calamari.readthedocs.org/en/latest/calamari_rest/resources/resources.html
442
* "Guzzle Quickstart":http://guzzle.readthedocs.org/en/latest/quickstart.html
443
* "VirtualBox Documentation":https://www.virtualbox.org/wiki/Documentation
444
* "PHP Manual":http://php.net/manual/