zzdeploy 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,5 @@
1
+ class Info
2
+ def self.version
3
+ "0.0.5"
4
+ end
5
+ end
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
@@ -0,0 +1,5 @@
1
+ #require 'spec'
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ require 'zz_deploy'