23 сентября 2009, 15:59
Темы: ruby, automation, rails
Задача
Допустим, есть приложение, написанное на руби. У него есть основной процесс и есть тесты, которые так или иначе загружают объекты приложения.
Хочется получить доступ в среду приложения в виде irb-консоли, чтобы вручную взаимодействовать с объектами и изменять данные. По типу script/console в rails.
Решение
Как большинство подобных решений, необходимость возникает, когда замечаешь себя за повторением одного и того же набора действий множество раз:
irb
require ...
require ...
...
Раз уж мы захотели как в рельсах, то следует предположить, что окружение нашего приложения загружается одним файлом. Например, config/environment.rb. Это будет первым упрощением многократно повторённого процесса.
Теперь сам файл script/console:
#!/usr/bin/env ruby
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), ".."))
libs = " -r irb/completion"
libs << " -r pp" # специально для консоли автоподставнока и pretty print
libs << %( -r "#{APP_ROOT}/config/environment.rb")
ENV["APP_ENV"] = "console" # пример того, как сообщить приложению, что оно в консоли
exec "irb #{libs} --simple=prompt"
Здесь стоит сделать два акцента:- Для сообщения приложению, что оно запущено в консоли, мы использовали волшебный хэш ENV. К нему потом можно обратиться внутри config/environment.rb и сделать что-то по-другому.
- Для запуска консоли мы использовали Kernel.exec, который не просто выполняет системную команду, но и передаёт ей управление, заменяя текущий процесс.
Теперь, если сделать наш файл запускаемым, будет как в сказке:
chmod +x script/console
Я хочу, чтоб эта песня, эта песня не кончааалась
Если есть приятные библиотеки, которые хочется подключать при каждом запуске irb. А так же, если хочется сохранять историю команд консоли при выходе. То следует воспользоваться мощью ~/.irbrc. Создайте этот файл («~» это $HOME, на всякий случай) и напишите в него:
require 'pp' # pretty print
require 'irb/completion' # автоподстановка
require 'irb/ext/save-history' # сохранение истории
ARGV.concat [ "--readline", # не пробовал без readline
"--prompt-mode", "simple" ] # --simple-prompt
IRB.conf[:SAVE_HISTORY] = 25 # сколько сохранять
IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb-history" # куда сохранять
Материалы для самостоятельного изучения
- Подробная документация irb;
- Занятный способ получить irb-консоль прямо в запущенном работающем приложении.
16 марта 2009, 22:24
Темы: ruby, deploy, automation
Когда я впервые прочитал про, например, Capistrano, мне, конечно же, сразу захотелось тоже начать применять эту клёвую штуку. Но я, конечно же, не преодолел барьер входа. На тот момент у меня было полтора приложения на ruby on rails, которые я довольно редко обновлял. Позже, когда я начал регулярно обновлять несколько приложений, использовать средства автоматизации оказалось очень просто и очень естественно. Для этого достаточно было вручную обновить приложение раз двадцать. :)
Задача
Допустим, речь идёт не о веб-приложении, а о библиотеке, которая используется на сервере несколькими веб-приложениями и другими программами, которые так же исполняются на сервере. Вполне логичным представляется сделать её в виде rubygem. И тогда встает вопрос обновления этой библиотеки на сервере.
Если делать это вручную достаточно долго, то со временем, после упрощений и оптимизаций, становится понятно, что для обновления нужно зайти на сервер по ssh и выполнить простую комманду:
cd somedir && do_some_stuff && sudo do_some_sudo_stuff
Использовать для этого любую готовую библиотеку публикации веб-приложений кажется слишком громостким. Так почему бы не написать задачу для rake, которая бы делала именно то, что нужно.
Ресурсы
Нам понадобится две библиотеки: одна для использования ssh, и другая для защищенного от заглядывания через плечо ввода sudo-пароля. (Оказалось, что сделать на руби такой ввод не так просто, поэтому я просто взял готовую библиотеку, которую и так использует, например, Capistrano и ряд других приложений).
sudo gem i net-ssh highline
Решение
Первым делом я, конечно, попробовал:
Net::SSH.start("myserver", "sudouser") do |ssh|
result = ssh.exec!("cd somedir && do_some_stuff && sudo do_some_sudo_stuff")
puts result
end
Но никакого вывода просто не дождался. Потому что дойдя до sudo-команды, процесс просто оставался в вечном ожидании.
Чтобы сделать ввод пароля, нужно создавать канал. А так же неплохо было бы проверить возможность интерактивного взаимодействия:
Net::SSH.start("myserver", "sudouser") do |ssh|
channel = ssh.open_channel do |ch|
ch.request_pty do |c, success|
raise "Cannot obtain pty" unless success
end
...
end
end
Теперь нужно отправить пароль в нужный момент. Чтобы узнать, когда наступил нужный момент, нужно использовать ключ -p (prompt) при вызове sudo, чтобы сказать ему, каким запросом спрашивать у нас пароль.
sudo -p 'sudo password: ' do_some_sudo_stuff
Когда нужно будет запросить пароль, воспользуемся библиотекой highline:
pwd = HighLine.new.ask("Input remote host sudo password: ") { |q| q.echo = false }
Это позволит нам получить пароль, не светя его на экране. Как это обычно и делает sudo.
Теперь посмотрим на всё решение целиком. В папке библиотеки создаем файл Rakefile. Записываем в него нашу задачу. В моём случае команда для сервера состояла примерно из следующего набора: «Перейти в папку, обновить исходники из scm, собрать джем, sudo установить джем, sudo удалить установленные старые версии джема».
Rakefile
require 'rubygems'
require 'rake'
require "net/ssh"
require 'highline'
...
desc "Update gem on the server by current version on remote origin"
task :deploy do
Net::SSH.start("myserver", "sudouser") do |ssh|
channel = ssh.open_channel do |ch|
ch.request_pty do |c, success|
# Если pseudo-tty недоступен, то невозможно никакого интерактива
raise "Cannot obtain pty" unless success
end
ch.exec("cd somedir && do_some_stuff && sudo -p 'sudo password: ' do_some_sudo_stuff") do |c, success|
abort "Could not execute command" unless success
c.on_data do |c, data|
if data =~ /sudo password: /
pwd = HighLine.new.ask("Input remote host sudo password: ") { |q| q.echo = false }
c.send_data "#{pwd}\n"
else
c[:result] ||= ""
c[:result] << data # Можно, конечно, и в процессе выводить
end
end
c.on_extended_data do |c, type, data|
puts "STDERR : #{data}"
end
end
end
ssh.loop # Ожидаем, пока закончится сеанс
puts channel[:result] # Выводим результат сеанса (можно было и в процессе)
end
end
...
Теперь вместо всей той последовательности действий достаточно написать запустить rake deploy и ввести пароль.
Материалы для изучения
Пособия по использованию Capistrano
Документация Net::SSH