zzdeploy 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -0
- data/README.rdoc +10 -0
- data/Rakefile +25 -0
- data/bin/zz +14 -0
- data/lib/commands/add_instance.rb +187 -0
- data/lib/commands/build_deploy_config.rb +143 -0
- data/lib/commands/chef_bake.rb +61 -0
- data/lib/commands/chef_upload.rb +62 -0
- data/lib/commands/config_amazon.rb +64 -0
- data/lib/commands/delete_instances.rb +114 -0
- data/lib/commands/deploy_group_create.rb +128 -0
- data/lib/commands/deploy_group_delete.rb +42 -0
- data/lib/commands/deploy_group_list.rb +41 -0
- data/lib/commands/deploy_group_modify.rb +65 -0
- data/lib/commands/deploy_instances.rb +108 -0
- data/lib/commands/list_instances.rb +56 -0
- data/lib/commands/maint_instances.rb +63 -0
- data/lib/commands/meta_options.rb +26 -0
- data/lib/commands/multi_ssh_instance.rb +87 -0
- data/lib/commands/ssh_instance.rb +98 -0
- data/lib/commands.rb +23 -0
- data/lib/info.rb +5 -0
- data/lib/multi_ssh.rb +155 -0
- data/lib/printer.rb +54 -0
- data/lib/zz_deploy.rb +177 -0
- data/spec/spec_helper.rb +5 -0
- metadata +219 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
require "set"
|
2
|
+
require 'readline'
|
3
|
+
|
4
|
+
module Commands
|
5
|
+
class SSHInstance
|
6
|
+
|
7
|
+
# holds the options that were passed
|
8
|
+
# you can set any initial defaults here
|
9
|
+
def options
|
10
|
+
@options ||= {
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
# required options
|
15
|
+
def required_options
|
16
|
+
@required_options ||= Set.new [
|
17
|
+
:group
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
def register(opts, global_options)
|
22
|
+
opts.banner = "Usage: ssh [options]"
|
23
|
+
opts.description = "SSH into a server"
|
24
|
+
|
25
|
+
opts.on('-i', "--instance instance", "The instance to connect to..") do |v|
|
26
|
+
options[:instance] = v
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on('-r', "--role role", MetaOptions.roles, "Role to look for.") do |v|
|
30
|
+
options[:role] = v
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on('-g', "--group deploy_group", "Required: Group to look for.") do |v|
|
34
|
+
options[:group] = v
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def run(global_options, amazon)
|
40
|
+
ec2 = amazon.ec2
|
41
|
+
|
42
|
+
group_name = options[:group]
|
43
|
+
deploy_group = amazon.find_deploy_group(group_name)
|
44
|
+
group_config = deploy_group.config
|
45
|
+
|
46
|
+
pick = nil
|
47
|
+
instance_id = options[:instance]
|
48
|
+
if instance_id.nil?
|
49
|
+
instances = amazon.find_and_sort_named_instances(options[:group], options[:role])
|
50
|
+
else
|
51
|
+
instances = amazon.find_and_sort_named_instances()
|
52
|
+
instances.each do |instance|
|
53
|
+
resource_id = instance[:resource_id]
|
54
|
+
if resource_id == instance_id
|
55
|
+
pick = instance
|
56
|
+
break
|
57
|
+
end
|
58
|
+
end
|
59
|
+
if pick.nil?
|
60
|
+
raise "The instance you specified was not a valid ZangZing deployed instance."
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if pick.nil?
|
65
|
+
if instances.length == 0
|
66
|
+
raise "No instance matched your search criteria."
|
67
|
+
end
|
68
|
+
|
69
|
+
pick = instances[0]
|
70
|
+
if instances.length > 1
|
71
|
+
# more than one
|
72
|
+
puts "More than one instance matched, pick from list below the instance you want."
|
73
|
+
i = 1
|
74
|
+
instances.each do |instance|
|
75
|
+
name = instance[:Name]
|
76
|
+
resource_id = instance[:resource_id]
|
77
|
+
puts "#{i}) #{name} => #{resource_id}"
|
78
|
+
i += 1
|
79
|
+
end
|
80
|
+
print "Type the one you want to use: "
|
81
|
+
r = Readline.readline()
|
82
|
+
pick_num = r.to_i
|
83
|
+
if pick_num < 1 || pick_num > instances.length
|
84
|
+
raise "Your pick was not in range."
|
85
|
+
end
|
86
|
+
pick = instances[pick_num - 1]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# ok, we have a pick lets ssh to it
|
91
|
+
ec2_instance = ec2.describe_instances(pick[:resource_id])[0]
|
92
|
+
dns_name = ec2_instance[:dns_name]
|
93
|
+
puts "Running SSH for #{pick[:Name]}"
|
94
|
+
ssh_cmd = "ssh -i ~/.ssh/#{group_config[:amazon_security_key]}.pem ec2-user@#{dns_name}"
|
95
|
+
ZZSharedLib::CL.do_cmd ssh_cmd
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/commands.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
require "multi_ssh"
|
4
|
+
require "printer"
|
5
|
+
|
6
|
+
require 'commands/build_deploy_config'
|
7
|
+
|
8
|
+
require 'commands/deploy_group_create'
|
9
|
+
require 'commands/deploy_group_delete'
|
10
|
+
require 'commands/deploy_group_list'
|
11
|
+
require 'commands/deploy_group_modify'
|
12
|
+
|
13
|
+
require 'commands/add_instance'
|
14
|
+
require 'commands/delete_instances'
|
15
|
+
require 'commands/list_instances'
|
16
|
+
require 'commands/deploy_instances'
|
17
|
+
require 'commands/maint_instances'
|
18
|
+
require 'commands/ssh_instance'
|
19
|
+
require 'commands/multi_ssh_instance'
|
20
|
+
require 'commands/meta_options'
|
21
|
+
require 'commands/chef_upload'
|
22
|
+
require 'commands/chef_bake'
|
23
|
+
require 'commands/config_amazon'
|
data/lib/info.rb
ADDED
data/lib/multi_ssh.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
require 'net/ssh/multi'
|
3
|
+
|
4
|
+
class MultiSSH
|
5
|
+
attr_reader :amazon, :group_name, :deploy_group, :group_config
|
6
|
+
attr_accessor :instances, :longest, :ui, :concurrent_max
|
7
|
+
|
8
|
+
def initialize(amazon, group_name, deploy_group, concurrent_max = 1000)
|
9
|
+
@amazon = amazon
|
10
|
+
@group_name = group_name
|
11
|
+
@deploy_group = deploy_group
|
12
|
+
@group_config = deploy_group.config
|
13
|
+
@concurrent_max = concurrent_max
|
14
|
+
@session = nil
|
15
|
+
@ui = Printer.new(STDOUT, STDERR, STDIN)
|
16
|
+
end
|
17
|
+
|
18
|
+
def run(remote_cmd)
|
19
|
+
@instances ||= amazon.find_and_sort_named_instances(group_name)
|
20
|
+
run_instances(@instances, remote_cmd)
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_instances(instances, remote_cmd)
|
24
|
+
clear_session
|
25
|
+
@longest = 0
|
26
|
+
@instances = instances
|
27
|
+
@instances.each do |instance|
|
28
|
+
session_opts = {}
|
29
|
+
host = instance[:public_hostname]
|
30
|
+
session_opts[:keys] = File.expand_path("~/.ssh/#{group_config[:amazon_security_key]}.pem")
|
31
|
+
session_opts[:paranoid] = false
|
32
|
+
session_opts[:user_known_hosts_file] = "/dev/null"
|
33
|
+
session_opts[:timeout] = 30
|
34
|
+
hostspec = "ec2-user@#{host}"
|
35
|
+
session.use(hostspec, session_opts)
|
36
|
+
|
37
|
+
@longest = host.length if host.length > @longest
|
38
|
+
end
|
39
|
+
|
40
|
+
ssh_command(remote_cmd, session)
|
41
|
+
|
42
|
+
result = 0
|
43
|
+
failed_count = 0
|
44
|
+
@instances.each do |instance|
|
45
|
+
ssh_result = instance[:ssh_exit_status]
|
46
|
+
if ssh_result != 0
|
47
|
+
failed_count += 1
|
48
|
+
result = ssh_result if result == 0
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if failed_count > 0
|
53
|
+
raise "Failed SSH command. Out of #{@instances.count} servers, #{failed_count} failed. First error was: #{result}"
|
54
|
+
else
|
55
|
+
ui.msg(ui.color("All #{@instances.count} ssh requests completed without error.", :green, :bold))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# find a matching instance for the specified host
|
61
|
+
def find_instance_by_host(host)
|
62
|
+
self.instances.each do |instance|
|
63
|
+
if instance[:public_hostname] == host
|
64
|
+
return instance
|
65
|
+
end
|
66
|
+
end
|
67
|
+
return nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def clear_session
|
71
|
+
@session = nil
|
72
|
+
# a map that tracks all output data in an array
|
73
|
+
# keyed by the host name
|
74
|
+
@output_tracker = {}
|
75
|
+
end
|
76
|
+
|
77
|
+
def output_tracker
|
78
|
+
@output_tracker
|
79
|
+
end
|
80
|
+
|
81
|
+
# dump the result data into a file for each host
|
82
|
+
def output_tracked_data_to_files(prefix, result_path)
|
83
|
+
return if result_path.nil?
|
84
|
+
output_tracker.each do |key, data|
|
85
|
+
file_path = File.expand_path('.', "#{result_path}/results_#{prefix}_#{key}.txt")
|
86
|
+
File.open(file_path, 'w') do |f|
|
87
|
+
data.each do |line|
|
88
|
+
f.puts(line)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_output(host, msg)
|
95
|
+
# see if we already have an entry for this host
|
96
|
+
data_array = output_tracker[host]
|
97
|
+
if data_array.nil?
|
98
|
+
data_array = []
|
99
|
+
output_tracker[host] = data_array
|
100
|
+
end
|
101
|
+
# now add the msg to the array
|
102
|
+
data_array << msg
|
103
|
+
end
|
104
|
+
|
105
|
+
def session
|
106
|
+
return @session unless @session.nil?
|
107
|
+
|
108
|
+
ssh_error_handler = Proc.new do |server|
|
109
|
+
host = server.host
|
110
|
+
msg = "Failed to connect to #{host} -- #{$!.class.name}: #{$!.message}"
|
111
|
+
print_data(host, msg, :red)
|
112
|
+
instance = find_instance_by_host(host)
|
113
|
+
instance[:ssh_exit_status] = 99
|
114
|
+
end
|
115
|
+
|
116
|
+
@session ||= Net::SSH::Multi.start(:concurrent_connections => concurrent_max, :on_error => ssh_error_handler)
|
117
|
+
end
|
118
|
+
|
119
|
+
# print and group data by host
|
120
|
+
def print_data(host, data, color = :cyan)
|
121
|
+
if data =~ /\n/
|
122
|
+
data.split(/\n/).each { |d| print_data(host, d) }
|
123
|
+
else
|
124
|
+
add_output(host, data)
|
125
|
+
padding = self.longest - host.length
|
126
|
+
str = ui.color(host, color) + (" " * (padding + 1)) + data
|
127
|
+
ui.msg(str)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def ssh_command(command, subsession)
|
132
|
+
subsession.open_channel do |ch|
|
133
|
+
ch.request_pty
|
134
|
+
ch.exec command do |ch, success|
|
135
|
+
raise ArgumentError, "Cannot execute #{command}" unless success
|
136
|
+
ch.on_data do |ichannel, data|
|
137
|
+
print_data(ichannel[:host], data)
|
138
|
+
end
|
139
|
+
ch.on_extended_data do |ichannel, type, data|
|
140
|
+
print_data(ichannel[:host], data, :red)
|
141
|
+
end
|
142
|
+
ch.on_request("exit-status") do |ichannel, data|
|
143
|
+
status = data.read_long
|
144
|
+
ichannel[:exit_status] = status
|
145
|
+
host = ichannel[:host]
|
146
|
+
instance = find_instance_by_host(host)
|
147
|
+
instance[:ssh_exit_status] = status
|
148
|
+
print_data(host, "Exit status is: #{status}", status == 0 ? :blue : :red)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
session.loop
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
data/lib/printer.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
class Printer
|
2
|
+
attr_reader :stdout
|
3
|
+
attr_reader :stderr
|
4
|
+
attr_reader :stdin
|
5
|
+
|
6
|
+
def initialize(stdout=STDOUT, stderr=STDERR, stdin=STDIN)
|
7
|
+
@stdout, @stderr, @stdin = stdout, stderr, stdin
|
8
|
+
end
|
9
|
+
|
10
|
+
def highline
|
11
|
+
@highline ||= begin
|
12
|
+
require 'highline'
|
13
|
+
HighLine.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Prints a message to stdout. Aliased as +info+ for compatibility with
|
18
|
+
# the logger API.
|
19
|
+
def msg(message)
|
20
|
+
stdout.puts message
|
21
|
+
stdout.flush
|
22
|
+
end
|
23
|
+
|
24
|
+
alias :info :msg
|
25
|
+
|
26
|
+
# Print a warning message
|
27
|
+
def warn(message)
|
28
|
+
msg("#{color('WARNING:', :yellow, :bold)} #{message}")
|
29
|
+
end
|
30
|
+
|
31
|
+
# Print an error message
|
32
|
+
def error(message)
|
33
|
+
msg("#{color('ERROR:', :red, :bold)} #{message}")
|
34
|
+
end
|
35
|
+
|
36
|
+
# Print a message describing a fatal error.
|
37
|
+
def fatal(message)
|
38
|
+
msg("#{color('FATAL:', :red, :bold)} #{message}")
|
39
|
+
end
|
40
|
+
|
41
|
+
def color(string, *colors)
|
42
|
+
if color?
|
43
|
+
highline.color(string, *colors)
|
44
|
+
else
|
45
|
+
string
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Should colored output be used? Only on TTY
|
50
|
+
def color?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/lib/zz_deploy.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
models = File.expand_path('../../models', __FILE__)
|
3
|
+
$:.unshift(models)
|
4
|
+
require 'subcommand'
|
5
|
+
require 'commands'
|
6
|
+
require 'right_aws'
|
7
|
+
require 'sdb/active_sdb'
|
8
|
+
require 'json'
|
9
|
+
require 'zzsharedlib'
|
10
|
+
|
11
|
+
class ZZDeploy
|
12
|
+
include Subcommands
|
13
|
+
|
14
|
+
VERSION = "0.0.4"
|
15
|
+
CMD = "zzdeploy"
|
16
|
+
|
17
|
+
RECIPES_DIR = "/var/chef/cookbooks/zz-chef-repo"
|
18
|
+
RECIPES_BUNDLE_DIR = "/var/chef/cookbooks/zz-chef-repo_bundle"
|
19
|
+
|
20
|
+
# required options
|
21
|
+
def required_options
|
22
|
+
@required_options ||= Set.new [
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
# the options that were set
|
27
|
+
def options
|
28
|
+
@options ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def sub_commands
|
32
|
+
@sub_commands ||= {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def printer
|
36
|
+
@printer ||= Printer.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# define sub commands here
|
40
|
+
def define_sub_commands
|
41
|
+
sub_commands[:deploy_group_create] = Commands::DeployGroupCreate.new
|
42
|
+
sub_commands[:deploy_group_delete] = Commands::DeployGroupDelete.new
|
43
|
+
sub_commands[:deploy_group_list] = Commands::DeployGroupList.new
|
44
|
+
sub_commands[:deploy_group_modify] = Commands::DeployGroupModify.new
|
45
|
+
sub_commands[:add] = Commands::AddInstance.new
|
46
|
+
sub_commands[:delete] = Commands::DeleteInstances.new
|
47
|
+
sub_commands[:list] = Commands::ListInstances.new
|
48
|
+
sub_commands[:deploy] = Commands::DeployInstances.new
|
49
|
+
sub_commands[:maint] = Commands::MaintInstances.new
|
50
|
+
sub_commands[:ssh] = Commands::SSHInstance.new
|
51
|
+
sub_commands[:multi_ssh] = Commands::MultiSSHInstance.new
|
52
|
+
sub_commands[:chef_upload] = Commands::ChefUpload.new
|
53
|
+
sub_commands[:chef_apply] = Commands::ChefBake.new
|
54
|
+
sub_commands[:config_amazon] = Commands::ConfigAmazon.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup
|
58
|
+
options.clear
|
59
|
+
set_amazon_options
|
60
|
+
# global options
|
61
|
+
global_options do |opts|
|
62
|
+
opts.banner = "Version: #{VERSION} - Usage: #{CMD} [options] [subcommand [options]]"
|
63
|
+
opts.description = "ZangZing configuration and deploy tool. You must specify a valid sub command."
|
64
|
+
opts.separator ""
|
65
|
+
opts.separator "Global options are:"
|
66
|
+
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
67
|
+
options[:verbose] = v
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on("--akey AmazonAccessKey", "Amazon access key, or environment AWS_ACCESS_KEY_ID.") do |v|
|
71
|
+
options[:access_key] = v
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on("--skey AmazonSecretKey", "Amazon secret key or environment AWS_SECRET_ACCESS_KEY") do |v|
|
75
|
+
options[:secret_key] = v
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
add_help_option
|
80
|
+
|
81
|
+
define_sub_commands
|
82
|
+
|
83
|
+
sub_commands.each_pair do |command_name, sub_cmd|
|
84
|
+
command command_name do |opts|
|
85
|
+
sub_cmd.send("register", opts, options)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
def force_fail(cmd = nil)
|
92
|
+
# force failure and show options
|
93
|
+
ARGV.clear
|
94
|
+
ARGV << "help"
|
95
|
+
ARGV << cmd unless cmd.nil?
|
96
|
+
opt_parse
|
97
|
+
end
|
98
|
+
|
99
|
+
# validate the options passed
|
100
|
+
def validate(cmd, sub_cmd)
|
101
|
+
required_options.each do |r|
|
102
|
+
if options.has_key?(r) == false
|
103
|
+
puts "Missing options"
|
104
|
+
force_fail
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
sub_cmd.required_options.each do |r|
|
109
|
+
if sub_cmd.options.has_key?(r) == false
|
110
|
+
puts "Missing options"
|
111
|
+
force_fail(cmd)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_amazon_options
|
117
|
+
json = File.open("/var/chef/amazon.json", 'r') {|f| f.read }
|
118
|
+
ak = JSON.parse(json)
|
119
|
+
options[:access_key] = ak["aws_access_key_id"]
|
120
|
+
options[:secret_key] = ak["aws_secret_access_key"]
|
121
|
+
rescue
|
122
|
+
# do nothing
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse
|
126
|
+
if ARGV.empty?
|
127
|
+
ARGV << "help"
|
128
|
+
end
|
129
|
+
|
130
|
+
cmd = opt_parse()
|
131
|
+
if cmd.nil?
|
132
|
+
force_fail
|
133
|
+
end
|
134
|
+
|
135
|
+
# ok, we have a valid command so dispatch it
|
136
|
+
# puts "cmd: #{cmd}"
|
137
|
+
# puts "options ......"
|
138
|
+
# p options
|
139
|
+
# puts "ARGV:"
|
140
|
+
# p ARGV
|
141
|
+
|
142
|
+
sub_cmd = sub_commands[cmd.to_sym]
|
143
|
+
validate(cmd, sub_cmd)
|
144
|
+
|
145
|
+
# track both types of options
|
146
|
+
ZZSharedLib::Options.global_options = options
|
147
|
+
ZZSharedLib::Options.cmd_options = sub_cmd.options
|
148
|
+
|
149
|
+
# dispatch the command
|
150
|
+
amazon = ZZSharedLib::Amazon.new
|
151
|
+
|
152
|
+
sub_cmd.send("run", options, amazon)
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
|
157
|
+
def run(argv = ARGV)
|
158
|
+
exit_code = true
|
159
|
+
begin
|
160
|
+
setup
|
161
|
+
parse
|
162
|
+
rescue SystemExit => ex
|
163
|
+
# ignore direct calls to exit
|
164
|
+
rescue Exception => ex
|
165
|
+
printer.error printer.color(ex.message, :red)
|
166
|
+
# puts ex.backtrace
|
167
|
+
exit_code = false
|
168
|
+
end
|
169
|
+
|
170
|
+
# make sure buffer is flushed
|
171
|
+
# debugger doesn't seem to do this always
|
172
|
+
STDOUT.flush
|
173
|
+
|
174
|
+
return exit_code
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|