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.
@@ -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'