zzdeploy 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|