API && SSH: создайте и настройте сервер с Python и Digital Ocean
Как разработчики, нам часто приходится иметь дело с управлением сервером, созданием экземпляров и так далее. Очевидно, что многие инструменты позволяют управлять этим процессом, но не было бы забавно написать собственный инструмент управления сервером? В этой статье мы напишем простой инструмент для создания сервера в DigitalOcean, готовый к настройке и наполнению вашими веб-приложениями!
Итак, приступим к работе!
Чему вы научитесь:
- Программное взаимодействие с ssh через библиотеку Paramiko
- Создание интерактивного инструмента командной строки с помощью Inquirer
- Создание сервера (или Droplet) в Digital Ocean через API
- Чтение YAML в Python
- Запуск терминальных команд из python
Сначала немного дизайна и настройки
Что нам понадобится для начала? Чтобы подключиться к DigitalOcean API, нам сначала нужен токен. Вы можете сгенерировать его здесь или:
- На панели управления Digital Ocean щелкните ссылку API.
- Нажмите на "Generate New Token"
- Выберите имя для токена
- Выберите возможности "Read" и "Write"
- Нажмите на "Generate Token"
- Немедленно скопируйте сгенерированный токен
- Вставьте его в папку вашего проекта в новый файл env.yaml с произвольным именем, здесь мы используем
doAuthToken
Следующее, что вам нужно, это sshKey. Если у вас уже есть что-то, загруженное на DigitalOcean, вы можете использовать его; скопируйте цифровой отпечаток устройства в файл env.yaml в список sshKeys
.
В противном случае создайте новый sshKey и добавьте его в Digital Ocean. Если вы не знаете, как сгенерировать sshKey или как загрузить его в вашу команду Digital Ocean, вы можете найти всю эту информацию здесь.
Нам также потребуется установить некоторые пакеты, поэтому выполните следующую команду:
pip install requests paramiko inquirer yaml
Теперь у нас есть то, что нам нужно, давайте начнем кодить!
Первые запросы
Чтобы начать работу с API-интерфейсами Digital Ocean, мы можем вызвать несколько простых конечных точек, которые нам понадобятся позже на этапе создания дроплета: конечные точки распределения и размера.
Мы должны добавить ранее созданный токен в наши заголовки запросов, чтобы конечные точки DO заработали.
Поскольку мы будем часто использовать этот заголовок, лучше написать функцию для его обработки:
def buildBasicHeaders():
configsFile = yaml.safe_load(open('./env.yaml'))
token = configsFile['configs']['doAuthToken']
headers = {'Content-Type':'application/json','Authorization':'Bearer '+token}
return headers
После завершения создания заголовков, мы можем запросить список дистрибутивов:
def getDistributions(distribution=""):
url = "https://api.digitalocean.com/v2/images?type=distribution"
headers = buildBasicHeaders()
response = requests.get(url,headers=headers)
images = response.json()['images']
images = list(filter(lambda i: i['status'] == 'available', images))
return images
С помощью этой функции нам требуются все возможные дистрибутивы для нашего сервера с конечной точки DO. Несмотря на это, они не всегда все доступны, поэтому мы фильтруем результат, чтобы получить список только доступных дистрибутивов. Теперь мы можем запросить доступные размеры для дроплета.
def getSizes():
url = "https://api.digitalocean.com/v2/sizes"
headers = buildBasicHeaders()
response = requests.get(url,headers=headers)
return response.json()['sizes']
Он похож на предыдущий, но более простой, поэтому мы можем двигаться дальше. Мы создали эти функции первыми, потому что они являются необходимыми параметрами для создания Droplet, но вы можете установить дополнительные конфигурации на этапе сборки. Вы можете проверить возможные параметры здесь.
Первые строки скрипта создания
Давайте теперь создадим файл createServer.py
, который предоставит основной процесс нашей программе.
Так как мы собираемся задать пользователю кучу вопросов, воспользуемся библиотекой inquirer
.
Начнем с простого:
import utils
import inquirer
questions = [
inquirer.Text('machineName', message="Pick a name for your machine")
]
answers = inquirer.prompt(questions)
machineName = answers['machineName']
Сначала мы просим пользователя назвать только что созданный дроплет. Переменная Questions
будет массивом всех наших вопросов к пользователю. Со строкой:
answers = inquirer.prompt(questions)
Мы указываем Inquirer задавать все вопросы в списке пользователю и сохранять результаты внутри answers
, которые будут списком, имеющим в качестве ключа значения, указанное в качестве первого аргумента каждой подсказки (в данном случае, machineName
).
Теперь, когда мы поняли это, мы можем получить наши размеры и дистрибутивы. Это немного сложнее, но я объясню это шаг за шагом.
#....
sizes = utils.getSizes()
sizeChoices = []
for i,size in enumerate(sizes, start=1):
choice = f"[{i}] RAM: {size['memory']}MB, CPUs: {size['vcpus']}, disk: {size['disk']}GB"
sizeChoices.append(choice)
images = utils.getDistributions()
imageChoices = []
for i,image in enumerate(images, start=1):
choice = f"[{i}] {image['description']}"
imageChoices.append(choice)
questions = [
inquirer.Text('machineName', message="Pick a name for your machine"),
inquirer.List('dropletSize', message="What size do you need?", choices=sizeChoices ),
inquirer.List('dropletImage', message="What OS do you prefer?", choices=imageChoices)
]
answers = inquirer.prompt(questions)
machineName = answers['machineName']
index = sizeChoices.index(answers['dropletSize'])
dropletSize = sizes[index]['slug']
index = imageChoices.index(answers['dropletImage'])
dropletImage = images[index]['id']
Давайте сделаем шаг назад, чтобы понять, что происходит, не так ли?
Первое: создание списка опций:
sizes = utils.getSizes()
sizeChoices = []
for i,size in enumerate(sizes, start=1):
choice = f"[{i}] RAM: {size['memory']}MB, CPUs: {size['vcpus']}, disk: {size['disk']}GB"
sizeChoices.append(choice)
images = utils.getDistributions()
imageChoices = []
for i,image in enumerate(images, start=1):
choice = f"[{i}] {image['description']}"
imageChoices.append(choice)
Как для размеров, так и для изображений нам нужно сначала перечислить их, чтобы мы могли просмотреть изображения в цикле и иметь ссылочный индекс, на который можно ссылаться позже. После того, как у нас есть набор вариантов, мы можем добавить другие в Questions
для пользователя.
questions = [
inquirer.Text('machineName', message="Pick a name for your machine"),
inquirer.List('dropletSize', message="What size do you need?", choices=sizeChoices ),
inquirer.List('dropletImage', message="What OS do you prefer?", choices=imageChoices)
]
answers = inquirer.prompt(questions)
Как и в предыдущем вопросе об имени компьютера, для типа вопроса inquirer.List
требуется ключ (например, dropletSize
или dropletImage
) и вопрос, отображаемый пользователю. Кроме того, мы должны предоставить список вариантов, списки, которые мы подготовили ранее.
На этом этапе, если мы выполним команду, у нас должно получиться что-то вроде этого:
Это хорошее начало; Что вы думаете?
Давайте быстро объясним последнюю часть приведенного выше кода:
#....
index = sizeChoices.index(answers['dropletSize'])
dropletSize = sizes[index]['slug']
index = imageChoices.index(answers['dropletImage'])
dropletImage = images[index]['id']
Здесь мы немного поковыряемся. Поскольку Inquirer возвращает только текст выбранного ответа, мы находим его индекс в списке вариантов, чтобы получить его в исходных списках. После этого мы извлекаем часть, которая нам нужна для создания дроплета, то есть слаг размера и идентификатор изображения.
Теперь самое интересное!
Создание дроплета
Наконец-то пришло время создать наш дроплет!
def createDroplet(name, size, image):
headers = buildBasicHeaders()
get_droplets_url = "https://api.digitalocean.com/v2/droplets"
configsFile = yaml.safe_load(open('./env.yaml'))
keys = configsFile['configs']['sshKeys']
keys = getConfig('sshKeys')
data = {
'name':name,
'size':size,
'image':int(image),
'ssh_keys': keys
}
response = requests.post(get_droplets_url, headers=headers,json=data)
return response.json()['droplet']
Все довольно просто, поэтому я просто расскажу пару моментов о sshKeys:
- Параметр sshKeys в данных может принимать список значений: это ключи, которые у нас есть в DigitalOcean, которые мы хотим поместить в наш новый дроплет, чтобы подключаться через ssh, используя их вместо аутентификации user:password.
- В нашем YAML параметр sshKeys будет списком цифровых отпечатков устройства, которые можно взять с панели DigitalOcean sshKeys.
При этом, прежде чем вернуться к createServer.py
, мы вероятно хотим, чтобы наш дроплет проверял свой статус, поэтому давайте также напишем функцию для этого.
def getDroplet(dropletId):
headers = buildBasicHeaders()
get_droplets_url = f"https://api.digitalocean.com/v2/droplets/{dropletId}"
response = requests.get(get_droplets_url, headers=headers)
return response.json()['droplet']
Хорошо, давайте воспользуемся нашими новыми функциями для создания нашего дроплета!
newDroplet = utils.createDroplet(machineName,dropletSize,dropletImage)
newDroplet = utils.getDroplet(newDroplet['id'])
print('[*] Creating the droplet... ', end='', flush=True)
while newDroplet['status'] != 'active' :
newDroplet = utils.getDroplet(newDroplet['id'])
time.sleep(1)
print('OK')
print('[*] Powering the new droplet on...', end='', flush=True)
time.sleep(60)
print('Droplet ready')
Итак, что здесь происходит?
- Мы создаем Droplet с нашей функцией createDroplet
- Как только мы создаем новый дроплет и получаем его
id
, мы делаем запрос на проверку статуса (который наверняка еще не будетactive
) - Мы сообщаем пользователю, что создаем дроплет, затем итерируем запросы к серверу, пока он не скажет, что дроплет активен; мы можем дать ответ нашему пользователю о том, что дроплет был создан
- Теперь нам нужно подождать, пока дроплет не включится, прежде чем мы сможем управлять им. Поздравляем! Теперь у вас есть новый дроплет для работы!
Подключение по SSH и установка пакетов
Наконец, мы можем подключиться через ssh, чтобы автоматически установить некоторые пакеты: пришло время представить paramiko!
ssh = paramiko.SSHClient()
ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ip = newDroplet['networks']['v4'][0]['ip_address']
configsFile = yaml.safe_load(open('./env.yaml'))
path = configsFile['configs']['localKeyFile']
ssh.connect(ip, username='root',key_filename=path)
print('CONNECTED')
commands = [
"apt-get update",
"apt-get install -y apache2",
# add all the commands you'd like to exec
]
for command in commands:
ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command(command)
print(ssh_stdout.read().decode())
ssh_stdin.close()
Хорошо, еще раз, что здесь происходит? Давайте посмотрим на это глубже!
ssh = paramiko.SSHClient()
ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
Прежде всего, мы создаем экземпляр sshClient
благодаря paramiko. Это центр всех последующих операций.
После этого мы автоматизируем поиск ключей ssh в нашей системе. В этих двух строках paramiko загружает ключи из мест по умолчанию на нашем компьютере и устанавливает резервный ключ по умолчанию, если мы не предоставим какой-либо точный ключ (что мы все равно сделаем при подключении).
ip = newDroplet['networks']['v4'][0]['ip_address']
configsFile = yaml.safe_load(open('./env.yaml'))
path = configsFile['configs']['localKeyFile']
ssh.connect(ip, username='root',key_filename=path)
print('CONNECTED')
Здесь мы получаем IP-адрес нашего дроплета и путь к желаемому закрытому ключу, который мы используем для подключения; после этого мы можем подключиться через ssh с помощью ssh.connect()
. Теперь мы можем выполнить нужные нам операции:
commands = [
"apt-get update",
"apt-get install -y apache2",
# add all the commands you'd like to exec
]
for command in commands:
ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command(command)
print(ssh_stdout.read().decode())
ssh_stdin.close()
Мы составляем список команд, которые мы хотели бы выполнить: после этого мы выполняем цикл по ним; метод ssh.exec_command(command)
позволяет нам выполнить команду и получить выходные данные внутри переменной ssh_stdout, которые мы напечатаем на экране, чтобы следить за процессом.
Когда все команды выполнены, мы можем закрыть соединение.
Получение контроля в конце
Теперь, когда дроплет готов и пакеты установлены, мы хотим войти в оболочку и проверить правильность установки apache. Итак, давайте закончим, добавив эти последние строки:
print(f"New machine is at IP: {ip}")
webbrowser.open(f'http://{ip}')
os.system(f"ssh -o StrictHostKeyChecking=no root@{ip}")
Мы записываем IP-адрес нашего дроплета, чтобы мы знали его и могли принять к сведению, в случае необходимости (вы всегда можете найти эту информацию на своей панели управления Digital Ocean); затем мы открываем новое окно браузера с этим IP-адресом и затем входим непосредственно по ssh, на терминале, используемом для создания дроплета!
И вот, у вас есть работающий сервер, и вы уже находитесь внутри ssh-терминала, чтобы продолжить выполнение следующих ручных задач.
Заключение
Вот и все! Это было довольно напряженно, но в конце, я надеюсь, достаточно ясно и захватывающе. Что вы думаете?