...

воскресенье, 9 марта 2014 г.

Исполнение SSH-команд на сотнях серверов с помощью Go

О чём статья




В этой статье мы с вами напишем простенькую программу на Go (в 100 строк), которая может исполнять команды через протокол SSH на сотнях серверов, делая это достаточно эффективно. Программа будет реализована с помощью go.crypto/ssh — реализации SSH протокола авторами Go.

Более «продвинутая» версия программы, написанной в этой статье, доступна на гитхабе под названием GoSSHa (Go SSH agent).



Введение




В компании, в которой я работаю, чуть больше, чем 1 сервер, и для эффективной работы с нашим количеством серверов по протоколу SSH была написана библиотека libpssh на основе libssh2. Эта библиотека была написана на C с использованием libevent много лет назад, и до сих пор хорошо справляется со своими обязанностями, но является весьма сложной в поддержке. Также, язык Go от компании Google начал набирать популярность, в том числе внутри нашей компании, поэтому я решил попробовать написать замену libpssh на Go, и исправить некоторые её недостатки, заодно значительно упростив код и сложность поддержки.

Чтобы начать работать, нам потребуется компилятор языка Go (можно скачать по адресу golang.org) и работающая команда hg, чтобы скачать go.crypto/ssh с помощью «go get».


Начало работы




Создадим файл «main.go» в какой-нибудь директории, желательно пустой. Давайте теперь напишем «каркас» нашей программы, а потом реализуем недостающие функции по ходу статьи:

package main

import (
"http://ift.tt/1l3VtZo"
// ...
)

// ...

func main() {
cmd := os.Args[1] // первый аргумент - команда, которую мы исполним на всех серверах
hosts := os.Args[2:] // остальные аргументы (начиная со второго) - список серверов

results := make(chan string, 10) // будем записывать результаты в буферизированный канал строк
timeout := time.After(5 * time.Second) // через 5 секунд в канал timeout придет сообщение

// инициализируем структуру с конфигурацией для пакета ssh. Функцию makeKeyring() напишем позднее
config := &ssh.ClientConfig{
User: os.Getenv("LOGNAME"),
Auth: []ssh.ClientAuth{makeKeyring()},
}

// запустим по одной goroutine (легковесный аналог OS thread) на сервер, функцию executeCmd() напишем позднее
for _, hostname := range hosts {
go func(hostname string) {
results <- executeCmd(cmd, hostname, config)
}(hostname)
}

// соберем результаты со всех серверов, или напишем "Timed out", если общее время исполнения истекло
for i := 0; i < len(hosts); i++ {
select {
case res := <-results:
fmt.Print(res)
case <-timeout:
fmt.Println("Timed out!")
return
}
}
}


Если не считать того, что нам нужно написать функции makeKeyring() и executeCmd(), наша программа готова! Благодаря «магии Go» мы установим соединение ко всем серверам параллельно и выполним на них заданную команду, и в любом случае завершимся через 5 секунд, напечатав на экран результаты со всех серверов, которые успели выполниться. Настолько простой способ реализации общего таймаута для всех параллельно исполняющихся операций возможен благодаря концепции каналов и наличию конструкции select, позволяющей выполнять общение одновременно между несколькими каналами: как только хотя бы одна из конструкций в case может выполниться, будет исполнен соответствующий блок кода.


Инициализация структур данных для go.crypto/ssh




Мы ещё не написали makeKeyring() и executeCmd(), но скорее всего ничего сильно интересного вы здесь не увидите. Будем авторизоваться только с помощью SSH-ключей, и будем предполагать, что ключи располагаются в .ssh/id_rsa или .ssh/id_dsa:

type SignerContainer struct {
signers []ssh.Signer
}

func (t *SignerContainer) Key(i int) (key ssh.PublicKey, err error) {
if i >= len(t.signers) {
return
}
key = t.signers[i].PublicKey()
return
}

func (t *SignerContainer) Sign(i int, rand io.Reader, data []byte) (sig []byte, err error) {
if i >= len(t.signers) {
return
}
sig, err = t.signers[i].Sign(rand, data)
return
}

func makeSigner(keyname string) (signer ssh.Signer, err error) {
fp, err := os.Open(keyname)
if err != nil {
return
}
defer fp.Close()

buf, _ := ioutil.ReadAll(fp)
signer, _ = ssh.ParsePrivateKey(buf)
return
}

func makeKeyring() ssh.ClientAuth {
signers := []ssh.Signer{}
keys := []string{os.Getenv("HOME") + "/.ssh/id_rsa", os.Getenv("HOME") + "/.ssh/id_dsa"}

for _, keyname := range keys {
signer, err := makeSigner(keyname)
if err == nil {
signers = append(signers, signer)
}
}

return ssh.ClientAuthKeyring(&SignerContainer{signers})
}


Как можно видеть, мы возвращаем интерфейс ssh.ClientAuth, который обладает нужными методами для авторизации на сервере. Для краткости обработка ошибок почти полностью отсутствует, в production-режиме объем кода будет раза в 1,5 больше.


Чтобы исполнить команду на сервере, код тоже весьма тривиален (обработка ошибок выкинута для краткости):



func executeCmd(cmd, hostname string, config *ssh.ClientConfig) string {
conn, _ := ssh.Dial("tcp", hostname+":22", config)
session, _ := conn.NewSession()
defer session.Close()

var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
session.Run(cmd)

return hostname + ": " + stdoutBuf.String()
}


Для простоты и краткости мы всегда используем текущее имя пользователя для авторизации на серверах, а также 22 порт по умолчанию.


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


Скрытый текст


package main

import (
"bytes"
"http://ift.tt/1l3VtZo"
"fmt"
"io"
"io/ioutil"
"os"
"time"
)

type SignerContainer struct {
signers []ssh.Signer
}

func (t *SignerContainer) Key(i int) (key ssh.PublicKey, err error) {
if i >= len(t.signers) {
return
}
key = t.signers[i].PublicKey()
return
}

func (t *SignerContainer) Sign(i int, rand io.Reader, data []byte) (sig []byte, err error) {
if i >= len(t.signers) {
return
}
sig, err = t.signers[i].Sign(rand, data)
return
}

func makeSigner(keyname string) (signer ssh.Signer, err error) {
fp, err := os.Open(keyname)
if err != nil {
return
}
defer fp.Close()

buf, _ := ioutil.ReadAll(fp)
signer, _ = ssh.ParsePrivateKey(buf)
return
}

func makeKeyring() ssh.ClientAuth {
signers := []ssh.Signer{}
keys := []string{os.Getenv("HOME") + "/.ssh/id_rsa", os.Getenv("HOME") + "/.ssh/id_dsa"}

for _, keyname := range keys {
signer, err := makeSigner(keyname)
if err == nil {
signers = append(signers, signer)
}
}

return ssh.ClientAuthKeyring(&SignerContainer{signers})
}

func executeCmd(cmd, hostname string, config *ssh.ClientConfig) string {
conn, _ := ssh.Dial("tcp", hostname+":22", config)
session, _ := conn.NewSession()
defer session.Close()

var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
session.Run(cmd)

return hostname + ": " + stdoutBuf.String()
}

func main() {
cmd := os.Args[1]
hosts := os.Args[2:]

results := make(chan string, 10)
timeout := time.After(5 * time.Second)
config := &ssh.ClientConfig{
User: os.Getenv("LOGNAME"),
Auth: []ssh.ClientAuth{makeKeyring()},
}

for _, hostname := range hosts {
go func(hostname string) {
results <- executeCmd(cmd, hostname, config)
}(hostname)
}

for i := 0; i < len(hosts); i++ {
select {
case res := <-results:
fmt.Print(res)
case <-timeout:
fmt.Println("Timed out!")
return
}
}
}







Запустим наше приложение:

$ vim main.go # напишем программу :)
$ go get # скачаем все зависимости
$ time go run main.go 'hostname -f; sleep 4.7' localhost srv1 srv2
localhost: localhost
srv1: srv1
Timed out!

real 0m5.543s


Работает! У серверов localhost, srv1 и srv2 было всего 0,3 секунды, чтобы исполнить все команды, и медленный srv2 не успел. Вместе с компиляцией программы «на лету» из исходных текстов, исполнение программы заняло 5,5 секунд, из которых 5 секунд — это наш таймаут по умолчанию на выполнение команды.


Заключение




Статья получилась короткой, но при этом мы написали весьма полезное приложение, которое можно спокойно использовать в production. Более продвинутую версию этого приложения мы проверили в production-окружении и она показала отличные результаты.

Ссылки:




1. Язык Go: golang.org/

2. Библиотека go.crypto: http://ift.tt/1oAUeyp

3. GoSSHa (SSH-proxy с общением с внешним миром через JSON): http://ift.tt/1oAUeyr

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий