Как стать автором
Обновить

Vultr как альтернатива Digital Ocean и работа с его API на PHP

Все знают, что если вам нужны VPS с Linux с почасовой оплатой, то вам прямая дорога в Digital Ocean (далее DO). О нём написано уже много, и знает о нём почти каждый. Но мало кто знает, что у него есть хорошая альтернатива, которая, на мой взгляд, абсолютно незаслуженно была обделена вниманием. На момент написания этой статьи на Хабре всего в трёх статьях упоминается Vultr, и более-менее подробного его обзора нет ни в одной из них.

И, как мне кажется, зря. IMHO, Vultr — это «убийца» Digital Ocean, и дальше я объясню почему.

Vultr во многом похож на DO. Он буквально кричит: «У меня всё то же самое! Нравится DO? Попробуй меня!». Действительно, сами по себе услуги, тарифы, возможности и сервисы — чуть ли не полностью копировали DO. Я не очень люблю плагиаторов, но при детальном рассмотрении я пришёл к выводу, то Vultr практически во всём превосходит DO.

Посудите сами, вот неполный список преимуществ Vultr:

  • В минимальном тарифе ($5/мес) включено 768 Mb RAM против 512 Mb у DO
  • 14 дата центров против 7 у DO (включая такое редкое расположение как Япония и Австралия)
  • Наличие тарифных планов под дешевое «хранилище» (SATA вместо SSD)
  • Есть возможность арендовать не VPS, а Dedicated Server
  • Есть возможность установить любой дистрибутив со своего ISO-образа
  • Есть возможность установить Windows Server (+$16/мес к стоимости любого тарифа)
  • Наличие Startup Scripts (подробнее ниже)
  • Полностью бесплатное хранение снэпшотов (даже их создание — бесплатно)
  • Намного проще регистрация (не требуется идентификация нового пользователя с помощью фото с паспортом)

На некоторых пунктах хочется остановиться поподробнее. Vultr имеет 3 линейки тарифных планов: SSD, Storage и Dedicated (из них у DO есть только первая). Т.е. если Ваш проект подразумевает хранение большого объёма данных (например, более 100 Гб), то в случае с DO вам придётся взять тариф за $160/мес. В то время как у Vultr вы можете взять Storage-тариф, в котором вместо SSD установлен SATA-массив и тариф, в который включено 125 Gb жёсткого диска обойдётся Вам всего в $5/мес. Да, в этом тарифе всего 512 Mb RAM, но можно посмотреть тарифы подороже. Скажем, 1 Gb RAM будет стоить $10/мес, только там ещё и жёсткий диск увеличат до 250 Gb. А такой объём данных в DO будет стоить уже $320/мес. В общем, тарифные планы в Vultr значительно более гибкие. Вы могли бы предположить, что за такой щедростью скрывается более дешевая система виртуализации, но нет. Vultr использует для всех своих машин виртуализацию KVM. Как и DO.

В купе с предыдущим плюсом отмечу важный их плюс — Vultr не берёт деньги ни за создание снэпшотов (в отличие от DO), ни за их хранение, ни за разворачивание. Совсем! И так уже несколько лет и менять пока не планируют (хотя пользовательское соглашение это допускает). Получается, что вы за $5/мес (или 0.7 цента в час) можете создать себе VPS с диском 125 Гб, скриптом ещё на этапе создания (об этом позже) поднять там FTP, подключиться со своей локальной машины туда, передать весь своих архив фото/музыки (у кого не влезет — есть тарифы до 1 Тб за $40/мес или 6 центов в час), после этого снять снэпшот с этой машины и удалить машину. Всё. У вас бесплатно хранится до терабайта вашей информации «на чёрный день». Если она Вам понадобится — заново создаете сервер из этого снэпшота, подключаетесь по FTP, забираете что нужно и удаляете машину. А для удобства, вместо одного огромного сервера можете создать несколько поменьше (по тематике хранения информации), тогда снэпшоты будут быстрее сохраняться/разворачиваться, да и за работу с ними будут брать меньше денег.

Ещё отмечу важную killer-фичу Vultr в сравнении с DO — это «Startup Scripts». Вы можете в своём личном кабинете создать несколько готовых скриптов, вбивая туда команды bash. И этот код исполнится с root-правами сразу после создания виртуальной машины. Внимательный читатель может сейчас воскликнуть: «Ба, так у Digital Ocean вообще-то тоже есть подобная фича и называет User Data или Cloud Script», но будет не совсем прав. Во-первых, у DO это не совсем команды bash. Скрипт нужно писать на YAML. Да, в него можно упаковать нативные команды bash, но перед этим вам придётся почитать мануалы как всё это оформить правильно, чтобы скрипт сработал. Во-вторых (и это более важно, имхо), DO не позволяет хранить эти скрипты в личном кабинете. Т.е. если Вы постоянно разворачиваете какие-то сервера и хотите постоянно применять к ним типовые скрипты, то Вам придётся где-то отдельно хранить эти скрипты в блокнотике и каждый раз «копипастить» в окно создания дроплета. Не очень здорово для облачного хостинга хранить свои скрипты где-то отдельно. А если учесть, что в типовые дистрибутивы CentOS на DO не входит даже wget, то лично я скриптами пользуюсь при каждом создании сервера.

В случае же с Vultr, я заранее создал несколько типовых скриптов и при создании VPS просто выбираю нужный вот так:



И в данном примере после создания сервера у меня сразу развернётся VPN-сервер и я сразу могу к нему подключиться вообще не открывая SSH-терминал.

Но этого мне показалось мало и я понял, что хочу работать с Vultr по API, не заходя постоянно в личный кабинет. Я перерыл весь интернет и нигде не нашел каких-то готовых инструментов. Что в этом случае делают Хабраюзеры? Правильно, берут и пишут сами. Писать решил на PHP. Спасибо Vultr за хорошие мануалы (опять же, на мой взгляд, намного удобнее, чем описание API у DO), за пару дней накидал полноценный PHP Wrapper для API Vultr.

Что он из себя представляет: при первичном открытии страницы я запрашиваю ключ API:



И тут же перезагружаю ту же самую страницу, но уже с помощью GET-запроса передаю в неё ключ API. Соответственно, при открытии страницы просто стоит проверка был ли GET-запрос. Если не было — запрос на ввод ключа. Если был — открывается тело страницы. В теле страницы происходит несколько GET-запросов на API VULTR получаются данные и структурированные отображаются в таблице. Как-то так:



Большую часть занимает форма создания нового сервера. По API также подтягиваются все ваши снэпшоты и Startup Scripts.

Обратите внимание, что здесь же показываются данные биллинга (остаток на счёте). К примеру, API v2 Digital Ocean вообще не поддерживает работу с биллингом и для проверки баланса вам всё равно придётся заходить в личный кабинет. В случае с Vultr в этом нет необходимости.

Да, в примере выше у меня нет созданных серверов. Когда они есть, то их отображение происходит так:



Соответственно, все сервера сведены в одну таблицу. Тут же есть кнопка «удалить». Если сервер в соответствующем статусе, то есть кнопка «терминал», которая в браузере в новом окне открывает терминальный доступ к этому серверу по SSH.

Предоставляю полный листинг исходников:

vultr.php (основой скрипт)
<HTML>
<HEAD>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<TITLE>Vultr API</TITLE>
  <style>
   table, tr{
    border: 0px solid black;   
    border-collapse: collapse;
   } 
   tr {
   valign: center;
   }
   td { 
    border: 1px solid black;
    padding-top: 1px;
    padding-bottom: 2px;
    padding-right: 5px;
    padding-left: 5px;
    border-collapse: collapse;
   }
   .hide {
   border-left: 0px;
   border-right: 0px;
   border-top: 0px;
   border-bottom: 0px;
   padding: 0px;
   padding-left: 5px;
   }
   a {
   text-decoration: none;
   }
  </style>
</HEAD>
<BODY>
<?php
// Автор: Brizovsky

//Функция преобразования Class Object в массив
 function objectToArray($d) {
 if (is_object($d)) {
 // Gets the properties of the given object
 // with get_object_vars function
 $d = get_object_vars($d);
 }
 
 if (is_array($d)) {
 /*
 * Return array converted to object
 * Using __FUNCTION__ (Magic constant)
 * for recursive call
 */
 return array_map(__FUNCTION__, $d);
 }
 else {
 // Return array
 return $d;
 }
 }

if (!empty($_GET)){ //Идёт проверка введен ли ключ API
 
 //Список используемых запросов к API
$url_regions = "https://api.vultr.com/v1/regions/list";
$url_plans = "https://api.vultr.com/v1/plans/list";
$url_oslist = "https://api.vultr.com/v1/os/list";
$url_scripts = "https://api.vultr.com/v1/startupscript/list";
$url_snapshots = "https://api.vultr.com/v1/snapshot/list";
$url_balance = "https://api.vultr.com/v1/account/info";
$url_servers = "https://api.vultr.com/v1/server/list";
$apikey = $_GET['apikey'];
if ($apikey == "myapi") { //alias для ключа API, который можно запомнить
$apikey = "сюда_вставляете_ваш_ключ_api"; //наш ключ API, который нам лень запоминать
} // эти строчки можно повторить для нескольких алиасов

if (file_get_contents("$url_balance?api_key=$apikey", true) != false){ //Проверка правильный ли ключ API

//Делаем запросы к API, записывем ответы
$regions_raw = file_get_contents("$url_regions", true);
$regions = objectToArray(json_decode($regions_raw)); //преобразовали сначала в class Object, потом в обычный массив
$plans_raw = file_get_contents("$url_plans", true);
$plans = objectToArray(json_decode($plans_raw)); //преобразовали сначала в class Object, потом в обычный массив
$oslist_raw = file_get_contents("$url_oslist", true);
$oslist = objectToArray(json_decode($oslist_raw)); //преобразовали сначала в class Object, потом в обычный массив
$scripts_raw = file_get_contents("$url_scripts?api_key=$apikey", true);
$scripts = objectToArray(json_decode($scripts_raw)); //преобразовали сначала в class Object, потом в обычный массив
$snapshots_raw = file_get_contents("$url_snapshots?api_key=$apikey", true);
$snapshots = objectToArray(json_decode($snapshots_raw)); //преобразовали сначала в class Object, потом в обычный массив
$balance_raw = file_get_contents("$url_balance?api_key=$apikey", true);
$balance = objectToArray(json_decode($balance_raw)); //преобразовали сначала в class Object, потом в обычный массив
$servers_raw = file_get_contents("$url_servers?api_key=$apikey", true);
$servers = objectToArray(json_decode($servers_raw)); //преобразовали сначала в class Object, потом в обычный массив


//Вывод баланса
$bal = abs($balance[balance]);//берём баланс по модулю, т.к. он почему-то идёт с отрицательным знаком
$charge = abs($balance[pending_charges]); //тоже берем по модулю
$bal = $bal - $charge; //Вычитаем из баланса текущие расходы, т.к. по API он показывает остаток на начала месяца, а не текущий, хотя в личном кабинете показывает именно текущий остаток.
echo "Ваш баланс: $$bal
"; echo "\n";
echo "Расход в этом месяце: $$charge

"; echo "\n";

//вывод серверов
if (!empty($servers)){ //проверяем есть ли сервера
echo "Ваши сервера:
\n";
echo "<table cellspacing=0>\n";
echo "<tr align=center><td>Номер</td><td>Название</td><td>ОС</td><td>Ядер</td><td>RAM</td><td>Диск</td><td>IP</td><td>Город</td><td>Пароль</td><td>Статус</td><td>Дата создания</td><td class=hide colspan=2></td></tr>\n";
foreach($servers as $obj): //перебираем все сервера, если есть
echo "<tr align=center><td>$obj[SUBID]</td><td>$obj[label]</td><td>$obj[os]</td><td>$obj[vcpu_count]</td><td>$obj[ram]</td><td>$obj[disk]</td><td>$obj[main_ip]</td><td>$obj[location]</td><td>$obj[default_password]</td><td>$obj[status], $obj[power_status], $obj[server_state]</td><td>$obj[date_created]</td>";
echo '<td class="hide"><form name="form2" method="post" action="vultr_del.php" style="margin: 0px;" '; ?>onsubmit='return confirm("Вы действительно хотите удалить сервер?")'<?php
echo '><input type="hidden" style="margin: 0px;" name="server_id" value="';
echo $obj[SUBID]; //прячем в форме параметр сервера
echo '">'; echo "\n";
echo '<input type="hidden" style="margin: 0px;" name="apikey" value="';
echo $apikey; //добавляем в форму ключ API для передачи в другие скрипты
echo '">'; echo "\n";
echo '<input type="submit" value="удалить" /></form></td>';
echo "<td class=hide>";
if (!empty($obj[kvm_url])){ //проверяем есть ли ссылка на терминал и выводим её только если есть
echo "<a href=$obj[kvm_url] target=_blank><input type=button value=Терминал></a>";
}
echo "</td>";
echo "</tr>\n";
endforeach;
echo "</table>
\n";
 }else{ //если серверов нет
echo "У вас нет созданных серверов.

\n";
 }


//начало формы на создание сервера
echo "Выберите параметры нового сервера:<br>\n";
echo '<form style="width: 450px; border: 1px groove #000000;" name="form1" method="post" action="vultr_create.php"><div style="margin: 10;">'; echo "\n";
echo '<input type="hidden" style="margin: 0px;" name="apikey" value="';
echo $apikey; //добавляем в форму ключ API для передачи в другие скрипты
echo '">'; echo "\n";
echo "Название сервера:
\n";
echo '<input type="text" name="label" width=15>
<font size=1> </font>
';
echo "\n";

//вывод дата-центров
echo "Дата-центр:<br>\n";
echo "<select id='dc' name='dc'>\n";
foreach($regions as $obj): //перебираем все дата-центры
echo "<option value='$obj[DCID]'";
if ($obj[DCID] == "7") echo " selected"; //проверка: Если это амстердам - выбираем по умолчанию. Если у вас другие предпочтения - меняйте ID (посмотреть можно в исходном html-коде после открытия этой страницы)
echo ">$obj[continent]: $obj[country], $obj[name]</option>\n";
endforeach;
echo "</select>
<font size=1> </font>
\n";

//вывод тарифного плана
echo "Тарифный план:<br>\n";
echo "<select id='plan' name='plan'>\n";
foreach($plans as $obj): //перебираем все планы
$arr = array(); //обнулили временный массив, в котором будем хранить список тарифных планов
echo "<option value='$obj[VPSPLANID]'>$$obj[price_per_month]: $obj[vcpu_count] CPU, $obj[ram] Mb RAM, $obj[disk] Gb $obj[plan_type] (";
foreach($obj[available_locations] as $obj2):
array_push($arr, $regions[$obj2]["country"]); //записали во временный массив список стран
endforeach;
$arr = array_unique($arr); //убрали повторяющиеся значения
foreach($arr as $obj3):
if ($arr[0] != $obj3) echo " "; //проверка: если это не первое значение в массиве, то ставим пробел перед его записью
echo "$obj3";
endforeach;
echo ")</option>\n";
endforeach;
echo "</select>
<font size=1> </font>
\n";

//вывод списка ОС
echo "ОС:<br>\n";
echo "<select id='os' name='os'>\n";
foreach($oslist as $obj):
echo "<option value='$obj[OSID]'>$obj[name]</option>\n";
endforeach;
echo "</select>
<font size=1> </font>
\n";

//вывод списка снапшотов
echo "Снапшот:<br>\n";
echo "<select id='snapshot' name='snapshot'>\n";
echo "<option value='' selected>не использовать</option>\n";
foreach($snapshots as $obj):
echo "<option value='$obj[SNAPSHOTID]'>$obj[description] ($obj[date_created])</option>\n";
endforeach;
echo "</select>
<font size=1> </font>
\n";


//вывод списка Стартовых скриптов
echo "Startup Script:<br>\n";
echo "<select id='script' name='script'>\n";
echo "<option value='' selected>не использовать</option>\n";
foreach($scripts as $obj):
echo "<option value='$obj[SCRIPTID]'>$obj[name]</option>\n";
endforeach;
echo "</select>
<font size=1> </font>
\n";

//конец формы
echo '<input type="submit" value="Создать сервер" />'; echo "\n";
echo "</div></form>\n";
} //Конец места где API ключ есть и он правильный
else { //местл, где API ключ есть, но он неправильный
echo "Ключ API неправильный!

\n";
echo '<a href="vultr.php">Попробовать снова</a>';
}
}
//Ниже идёт первичный вход (не указан ключ API)
else {
echo "Введите ключ API:
";
echo '<form name="form0" method="GET" action="vultr.php">'; echo "\n";
echo '<input type="text" name="apikey">';
echo '<input type="submit" value="Готово" />'; echo "\n";
echo "</form>\n";
}
?>
</BODY>
</HTML>


vultr_create.php (создание сервера)
<HTML>
<HEAD>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<TITLE>Vultr API</TITLE>
<BODY>
<?php
// Функция преобразования class Object в массив
 function objectToArray($d) {
 if (is_object($d)) {
 // Gets the properties of the given object
 // with get_object_vars function
 $d = get_object_vars($d);
 }
 
 if (is_array($d)) {
 /*
 * Return array converted to object
 * Using __FUNCTION__ (Magic constant)
 * for recursive call
 */
 return array_map(__FUNCTION__, $d);
 }
 else {
 // Return array
 return $d;
 }
 }


if (!empty($_POST)){ //проверяем был ли post-запрос или это прямое открывание файла
$url_create = "https://api.vultr.com/v1/server/create";
$dc = $_POST['dc'];
$apikey = $_POST['apikey'];
$plan = $_POST['plan'];
$os   = $_POST['os'];
$snapshot = $_POST['snapshot'];
$script   = $_POST['script'];
$label = $_POST['label'];

if (!empty($snapshot)){ //Если создаем сервер с помощью snapshot, то игнорируем выбранную ОС и отключаем Startup Script
$os = "164";
$postdata = http_build_query(
    array(
        'label' => $label,
        'DCID' => $dc,
        'VPSPLANID' => $plan,
        'OSID' => $os,
        'SNAPSHOTID' => $snapshot
    )
);
}
else { //Это вариант, когда Snapshot не используется
$postdata = http_build_query(
    array(
        'label' => $label,
        'DCID' => $dc,
        'VPSPLANID' => $plan,
        'OSID' => $os,
        'SNAPSHOTID' => $snapshot,
        'SCRIPTID' => $script
    )
);
}
$opts = array('http' => //так мы генерируем правильный контекст для отправки POST-запроса с помощью file_get_contents
    array(
        'method'  => 'POST',
        'header'  => 'Content-type: application/x-www-form-urlencoded',
        'content' => $postdata
    )
);
$uri_create="$url_create?api_key=$apikey"; //генерируем правильную ссылку, с учётом ключа API

$context  = stream_context_create($opts); //генерируем правильный контекст для отправки POST-запроса с помощью file_get_contents

$create_raw = file_get_contents("$uri_create", false, $context); //делаем POST-запрос на API
$create = objectToArray(json_decode($create_raw)); //полученные данные сначла преобразовываем из JSON в Class Object, потом в массив для удобства работы
  if ($create[SUBID] != "") { //Проверяем был ли создан сервер через возрат ID сервера от API
//Сервер был создан
echo "Был создан сервер $create[SUBID]

\n";
echo '<a href="vultr.php?apikey='; //генерируем ссылку "назад" с нашим ключом API, чтобы не запрашивать повторно.
echo $apikey;
echo '">Вернуться назад</a>';
  }
  else // Здесь оказываемся, если сервер не был создан
  {
  echo "Oops. Что-то пошло не так. Сервер не был создан. Возможно, указанный тарифный план не допустим в указанном регионе.

\n";
  echo '<a href="vultr.php?apikey='; //генерируем ссылку "назад" с нашим ключом API, чтобы не запрашивать повторно.
  echo $apikey;
  echo '">Попробовать снова</a>';
  }
}
else { //Сюда попадаем, когда файл открылся без POST-запроса
echo "В скрипт не переданы данные из формы. Прямое открытие файла?

\n";
echo '<a href="vultr.php">Попробовать снова</a>';
}

?>
</BODY>
</HTML>


vultr_del.php (удаление сервера)
<HTML>
<HEAD>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<TITLE>Vultr API</TITLE>
<BODY>
<?php
if (!empty($_POST)){  //проверяем был ли post-запрос или это прямое открывание файла

//собираем данные из POST-запроса
$url_del = "https://api.vultr.com/v1/server/destroy";
$server_id = $_POST['server_id'];
$apikey = $_POST['apikey'];

$postdata = http_build_query(
    array(
        'SUBID' => $server_id
    )
);

$opts = array('http' => //так мы генерируем правильный контекст для отправки POST-запроса с помощью file_get_contents
    array(
        'method'  => 'POST',
        'header'  => 'Content-type: application/x-www-form-urlencoded',
        'content' => $postdata
    )
);
$uri_del="$url_del?api_key=$apikey"; //генерируем правильную ссылку, с учётом ключа API

$context  = stream_context_create($opts); //генерируем правильный контекст для отправки POST-запроса с помощью file_get_contents

$del_raw = file_get_contents("$uri_del", false, $context);  //Делаем POST-запрос к API

echo "Был удален сервер $server_id 

\n";
echo '<a href="vultr.php?apikey='; //генерируем ссылку "назад" с нашим ключом API, чтобы не запрашивать повторно.
echo $apikey;
echo '">Вернуться назад</a>';
}
else { //Сюда попадаем, когда файл открылся без POST-запроса
echo "В скрипт не переданы данные из формы. Прямое открытие файла?

\n";
echo '<a href="vultr.php">Попробовать снова</a>';
}
?>
</BODY>
</HTML>


Все три файла должны лежать в одном месте и называться так, как в заголовке спойлера.

Кстати, когда мне надоело постоянно вводить свой ключ API, поступил следующим образом: сделал легко-запоминаемый алиас. Т.е. я вместо своего ключа API ввожу что-нибудь типа «hello api», а внутри php идёт замена этого значения на мой реальный ключ. Т.е. достаточно запомнить только кодовую фразу. Можете себе так же сделать — править в основном файле в строке 72.

Весь свой код старался хорошо прокомментировать, чтобы несложно было разобраться. Скрипт делал для себя, поэтому особо сложных проверок и «защит от дурака» не ставил, но, тем не менее: сделал подтверждение перед удалением сервера, сделал проверку на правильность ключа API. А, также, предусмотрел наличие взаимоисключающих параметров (к примеру, указание ОС сервера и указание своего Снэпшота).
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.