mofa 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,87 @@
1
+ require 'pathname'
2
+ require 'thor/base'
3
+ require 'thor/actions'
4
+
5
+ class Cookbook
6
+ include Thor::Base
7
+ include Thor::Actions
8
+ include FileUtils
9
+
10
+ attr_accessor :name
11
+ attr_accessor :version
12
+ attr_accessor :type
13
+ attr_accessor :pkg_uri
14
+ attr_accessor :source_uri
15
+ attr_accessor :cookbooks_url
16
+ attr_accessor :token
17
+
18
+ def self.create(cookbook_name_or_path='.', token=nil)
19
+ cb = nil
20
+ begin
21
+ case
22
+ when cookbook_name_or_path.match(/:/)
23
+ fail "Did not find released Cookbook #{cookbook_name_or_path}!" unless ReleasedCookbook.exists?(cookbook_name_or_path)
24
+ fail "Did not find Version #{cookbook_version} of released Cookbook #{cookbook_name_or_path}!" unless ReleasedCookbook.exists?(cookbook_name_or_path, cookbook_version)
25
+
26
+ cb = ReleasedCookbook.new(cookbook_name_or_path)
27
+
28
+ else
29
+ cb = SourceCookbook.new(cookbook_name_or_path)
30
+ end
31
+ rescue RuntimeError => e
32
+ error e.message
33
+ raise "Cookbook not found/detected!"
34
+ end
35
+ cb.token = token
36
+ cb.autodetect_type
37
+
38
+ cb
39
+
40
+ end
41
+
42
+ def autodetect_type
43
+ env_indicator = Mofa::Config.config['cookbook_type_indicator']['env']
44
+ wrapper_indicator = Mofa::Config.config['cookbook_type_indicator']['wrapper']
45
+ base_indicator = Mofa::Config.config['cookbook_type_indicator']['base']
46
+
47
+ say "Autodetecting Cookbook Architectural Type... "
48
+ case
49
+ when @name.match(env_indicator)
50
+ @type = 'env'
51
+ when @name.match(wrapper_indicator)
52
+ @type = 'wrapper'
53
+ when @name.match(base_indicator)
54
+ @type = 'base'
55
+ else
56
+ @type = 'application'
57
+ end
58
+ say "#{type.capitalize} Cookbook"
59
+ end
60
+
61
+ def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)$/))
62
+ color ||= :green
63
+ super
64
+ end
65
+
66
+ def ok(detail=nil)
67
+ text = detail ? "OK, #{detail}." : "OK."
68
+ say text, :green
69
+ end
70
+
71
+ def error(detail)
72
+ say detail, :red
73
+ end
74
+
75
+ # Enforce silent system calls, unless the --verbose option is passed.
76
+ # One may either pass -v, --verbose or --[v|verbose]=[true|t|yes|y|1].
77
+ #
78
+ def run(cmd, *args)
79
+ args = args.empty? ? {} : args.pop
80
+ verbose = (Mofa::CLI::option_debug) ? true : false
81
+ #verbose = !!(options[:verbose] && options[:verbose].to_s.match(/(verbose|true|t|yes|y|1)$/i))
82
+ exit_code = super(cmd, args.merge(:verbose => verbose))
83
+ fail "Failed to run #{cmd.inspect}!" unless exit_code == true
84
+ end
85
+
86
+
87
+ end
@@ -0,0 +1,61 @@
1
+ require 'rest-client'
2
+ require 'net/ping'
3
+
4
+ class Hostlist
5
+ attr_accessor :list
6
+ attr_accessor :filter
7
+ attr_accessor :service_host
8
+ attr_accessor :service_url
9
+ attr_accessor :filter
10
+ attr_accessor :api_key
11
+
12
+ def self.create(filter = nil)
13
+ hl = Hostlist.new
14
+ filter ||= Mofa::Config.config['service_hostlist_default_filter']
15
+ hl.filter = filter
16
+ hl.service_host = Mofa::Config.config['service_hostlist_url'].gsub(/^http:\/\//, '').gsub(/\/.*$/, '').gsub(/:.*$/, '')
17
+ hl.service_url = Mofa::Config.config['service_hostlist_url']
18
+ hl.api_key = Mofa::Config.config['service_hostlist_api_key']
19
+ hl
20
+ end
21
+
22
+ def retrieve
23
+ fail "Hostlist Service not reachable! (cannot ping #{service_host})" unless up?
24
+ response = RestClient.get(@service_url, { :params => {:key => api_key}})
25
+ hosts_list_json = JSON.parse response.body
26
+ @list = hosts_list_json['data'].collect { |i| i['cname'] }
27
+
28
+ apply_filter
29
+ sort_by_domainname
30
+ end
31
+
32
+
33
+ def up?
34
+ p = Net::Ping::TCP.new(@service_host, 'http')
35
+ p.ping?
36
+ end
37
+
38
+ def apply_filter
39
+ # building matcher
40
+ regex = @filter.gsub(/\*/, '__ASTERISK__')
41
+ regex = Regexp.escape(regex).gsub(/__ASTERISK__/, '.*')
42
+ regex = '^' + regex + '$'
43
+
44
+ puts "regex=#{regex}"
45
+
46
+ @list.select! {|hostname| hostname.match(regex) }
47
+ end
48
+
49
+ def sort_by_domainname
50
+ sortable = {}
51
+ @list.each do |hostname|
52
+ sortable.store(hostname.split(/\./).reverse.join('.'), hostname)
53
+ end
54
+ @list = sortable.keys.sort.collect { |s| sortable[s] }
55
+ end
56
+
57
+ def filter_by_runlist_map(runlist_map)
58
+ @list.select! { |hostname| runlist_map.mp.key?(hostname)}
59
+ end
60
+
61
+ end
@@ -0,0 +1,246 @@
1
+ require 'net/ssh'
2
+ require 'net/sftp'
3
+
4
+ class MofaCmd
5
+ attr_accessor :token
6
+ attr_accessor :cookbook
7
+ attr_accessor :hostlist
8
+ attr_accessor :runlist_map
9
+
10
+ def self.generate_token
11
+ Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
12
+ end
13
+
14
+ def self.create(cookbook, hostlist, runlist_map, token)
15
+ mofa_cmd = MofaCmd.new
16
+ mofa_cmd.token = token
17
+ mofa_cmd.cookbook = cookbook
18
+ mofa_cmd.hostlist = hostlist
19
+ mofa_cmd.runlist_map = runlist_map
20
+ mofa_cmd
21
+ end
22
+
23
+ def prepare
24
+ cookbook.prepare
25
+ fail "Hostlist Service not reachable! (cannot ping #{hostlist.service_url})" unless hostlist.up?
26
+ end
27
+
28
+ def execute
29
+ cookbook.execute
30
+ hostlist.retrieve
31
+ runlist_map.generate
32
+
33
+ puts "Runlist Map: #{runlist_map.mp.inspect}"
34
+ puts "Hostlist before runlist filtering: #{hostlist.list.inspect}"
35
+
36
+ hostlist.filter_by_runlist_map(runlist_map)
37
+
38
+ puts "Hostlist after runlist filtering: #{hostlist.list.inspect}"
39
+
40
+ exit_code = run_chef_solo_on_hosts
41
+
42
+ exit_code
43
+ end
44
+
45
+ def cleanup
46
+ cookbook.cleanup
47
+ end
48
+
49
+ # FIXME
50
+ # This Code is Copy'n'Pasted from the old mofa tooling. Only to make the MVP work in time!!
51
+ # This needs to be refactored ASAP.
52
+
53
+ def run_chef_solo_on_hosts
54
+ time = Time.new
55
+ puts 'Chef-Solo Run started at ' + time.strftime('%Y-%m-%d %H:%M:%S')
56
+ puts "Will use ssh_user #{Mofa::Config.config['ssh_user']} and ssh_key_file #{Mofa::Config.config['ssh_keyfile']}"
57
+ at_least_one_chef_solo_run_failed = false
58
+ chef_solo_runs = {}
59
+ host_index = 0
60
+ hostlist.list.each do |hostname|
61
+ host_index = host_index + 1
62
+ puts
63
+ puts "----------------------------------------------------------------------"
64
+ puts "Chef-Solo on Host #{hostname} (#{host_index}/#{hostlist.list.length.to_s})"
65
+ puts "----------------------------------------------------------------------"
66
+ chef_solo_runs.store(hostname, {})
67
+
68
+ # do only one for faster dev-cycle...
69
+ #next unless hostname.match(/^dash/)
70
+
71
+ puts "Pinging host #{hostname}..."
72
+ exit_status = system("ping -q -c 1 #{hostname} >/dev/null 2>&1")
73
+ unless exit_status then
74
+ puts " --> Host #{hostname} is unavailable!"
75
+ chef_solo_runs[hostname].store('status', 'UNAVAIL')
76
+ chef_solo_runs[hostname].store('status_msg', "Host #{hostname} unreachable.")
77
+ else
78
+ puts " --> Host #{hostname} is available."
79
+ prerequesits_met = true
80
+ # Create a temp working dir on the target host
81
+ solo_dir = '/var/tmp/' + time.strftime('%Y-%m-%d_%H%M%S')
82
+ Net::SSH.start(hostname, Mofa::Config.config['ssh_user'], :keys => [Mofa::Config.config['ssh_keyfile']], :verbose => :error) do |ssh|
83
+ puts "Remotely creating solo_dir \"#{solo_dir}\" on host #{hostname}"
84
+ # remotely create the temp folder
85
+ out = ssh_exec!(ssh, "[ -d #{solo_dir} ] || mkdir #{solo_dir}")
86
+ puts "ERROR (#{out[0]}): #{out[2]}" if out[0] != 0
87
+
88
+ # remotely create a data_bags folder structure on the target host
89
+ if File.directory?("#{cookbook.source_dir}/data_bags")
90
+ Dir.entries("#{cookbook.source_dir}/data_bags").select{|f| !f.match(/^\.\.?$/)}.each do |data_bag|
91
+ puts "Remotely creating data_bags dir \"#{solo_dir}/data_bags/#{data_bag}\""
92
+ out = ssh_exec!(ssh, "[ -d #{solo_dir}/data_bags/#{data_bag} ] || mkdir -p #{solo_dir}/data_bags/#{data_bag}")
93
+ puts "ERROR (#{out[0]}): #{out[2]}" if out[0] != 0
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ # skip the rest if prerequesits are not met
100
+ next unless prerequesits_met
101
+
102
+
103
+ Net::SFTP.start(hostname, Mofa::Config.config['ssh_user'], :keys => [Mofa::Config.config['ssh_keyfile']], :verbose => :error) do |sftp|
104
+
105
+ # remotely creating solo.rb
106
+ puts "Remotely creating \"#{solo_dir}/solo.rb\""
107
+ sftp.file.open("#{solo_dir}/solo.rb", "w") do |file|
108
+ solo_rb = <<-"EOF"
109
+ cookbook_path [ "#{solo_dir}/cookbooks" ]
110
+ data_bag_path "#{solo_dir}/data_bags"
111
+ log_level :info
112
+ log_location "#{solo_dir}/log"
113
+ verify_api_cert true
114
+ EOF
115
+
116
+ file.write(solo_rb)
117
+ end
118
+
119
+ # remotely creating node.json
120
+ puts "Remotely creating \"#{solo_dir}/node.json\""
121
+ node_json = {}
122
+ node_json.store('run_list', runlist_map.mp[hostname])
123
+
124
+ sftp.file.open("#{solo_dir}/node.json", "w") do |file|
125
+ file.write(JSON.pretty_generate(node_json))
126
+ end
127
+
128
+ # remotely create data_bag items
129
+ if File.directory?("#{cookbook.source_dir}/data_bags")
130
+ Dir.entries("#{cookbook.source_dir}/data_bags").select{|f| !f.match(/^\.\.?$/)}.each do |data_bag|
131
+ Dir.entries("#{cookbook.source_dir}/data_bags/#{data_bag}").select{|f| f.match(/\.json$/)}.each do |data_bag_item|
132
+ puts "Uploading data_bag_item #{data_bag_item}... "
133
+ sftp.upload!("#{cookbook.source_dir}/data_bags/#{data_bag}/#{data_bag_item}", "#{solo_dir}/data_bags/#{data_bag}/#{data_bag_item}")
134
+ puts "OK."
135
+ end
136
+ end
137
+ end
138
+
139
+ if cookbook.instance_of?(SourceCookbook)
140
+ puts "Cookbook is a SourceCookbook! Uploading Snapshot Package #{cookbook.pkg_name}... "
141
+ sftp.upload!("#{cookbook.pkg_dir}/#{cookbook.pkg_name}", "#{solo_dir}/#{cookbook.pkg_name}")
142
+ puts "OK."
143
+ end
144
+
145
+ # Do it -> Execute the chef-solo run!
146
+ Net::SSH.start(hostname, Mofa::Config::config['ssh_user'], :keys => [Mofa::Config::config['ssh_keyfile']], :verbose => :error) do |ssh|
147
+
148
+ if cookbook.instance_of?(SourceCookbook)
149
+ puts "Remotely unpacking Snapshot Package #{cookbook.pkg_name}... "
150
+ out = ssh_exec!(ssh, "cd #{solo_dir}; tar xvfz #{cookbook.pkg_name}")
151
+ if out[0] != 0
152
+ puts "ERROR (#{out[0]}): #{out[2]}"
153
+ puts out[1]
154
+ else
155
+ puts "OK."
156
+ end
157
+ end
158
+
159
+ puts "Remotely running chef-solo -c #{solo_dir}/solo.rb -j #{solo_dir}/node.json"
160
+ out = ssh_exec!(ssh, "sudo chef-solo -c #{solo_dir}/solo.rb -j #{solo_dir}/node.json")
161
+ if out[0] != 0
162
+ puts "ERROR (#{out[0]}): #{out[2]}"
163
+ out = ssh_exec!(ssh, "sudo cat #{solo_dir}/log")
164
+ puts "ERROR (#{out[0]}): #{out[2]}" if out[0] != 0
165
+ puts out[1]
166
+ chef_solo_runs[hostname].store('status', 'FAIL')
167
+ chef_solo_runs[hostname].store('status_msg', out[1])
168
+ else
169
+ unless Mofa::CLI::option_debug
170
+ out = ssh_exec!(ssh, "sudo grep 'Chef Run' #{solo_dir}/log")
171
+ puts "ERROR (#{out[0]}): #{out[2]}" if out[0] != 0
172
+ puts "Done."
173
+ else
174
+ out = ssh_exec!(ssh, "sudo cat #{solo_dir}/log")
175
+ puts "ERROR (#{out[0]}): #{out[2]}" if out[0] != 0
176
+ puts out[1]
177
+ end
178
+ chef_solo_runs[hostname].store('status', 'SUCCESS')
179
+ chef_solo_runs[hostname].store('status_msg', '')
180
+ end
181
+ out = ssh_exec!(ssh, "sudo chown -R #{Mofa::Config.config['ssh_user']}.#{Mofa::Config.config['ssh_user']} #{solo_dir}")
182
+ puts "ERROR (#{out[0]}): #{out[2]}" if out[0] != 0
183
+ end
184
+ end
185
+ at_least_one_chef_solo_run_failed = true if chef_solo_runs[hostname]['status'] == 'FAIL'
186
+ end
187
+
188
+ # ------- print out report
189
+ puts
190
+ puts "----------------------------------------------------------------------"
191
+ puts "Chef-Solo Run REPORT"
192
+ puts "----------------------------------------------------------------------"
193
+ puts "Chef-Solo has been run on #{chef_solo_runs.keys.length.to_s} hosts."
194
+
195
+ chef_solo_runs.each do |hostname, content|
196
+ status_msg = ''
197
+ status_msg = "(#{content['status_msg']})" if content['status'] == 'FAIL'
198
+ puts "#{content['status']}: #{hostname} #{status_msg}"
199
+ end
200
+
201
+ exit_code = 0
202
+ if at_least_one_chef_solo_run_failed
203
+ exit_code = 1
204
+ end
205
+
206
+ puts "Exiting with exit code #{exit_code}."
207
+ exit_code
208
+
209
+ end
210
+
211
+ def ssh_exec!(ssh, command)
212
+ stdout_data = ""
213
+ stderr_data = ""
214
+ exit_code = nil
215
+ exit_signal = nil
216
+ ssh.open_channel do |channel|
217
+ channel.exec(command) do |ch, success|
218
+ unless success
219
+ abort "FAILED: couldn't execute command (ssh.channel.exec)"
220
+ end
221
+ channel.on_data do |ch, data|
222
+ stdout_data+=data
223
+ end
224
+
225
+ channel.on_extended_data do |ch, type, data|
226
+ stderr_data+=data
227
+ end
228
+
229
+ channel.on_request("exit-status") do |ch, data|
230
+ exit_code = data.read_long
231
+ end
232
+
233
+ channel.on_request("exit-signal") do |ch, data|
234
+ exit_signal = data.read_long
235
+ end
236
+ end
237
+ end
238
+ ssh.loop
239
+ [exit_code, stdout_data, stderr_data, exit_signal]
240
+ end
241
+
242
+
243
+ private
244
+
245
+ end
246
+
@@ -0,0 +1,47 @@
1
+ class ReleasedCookbook < Cookbook
2
+ attr_accessor :pkg_dir
3
+ attr_accessor :pkg_name
4
+
5
+ def initialize(cookbook_name_or_path)
6
+ super()
7
+ # TODO: this needs proper vaidation!
8
+ @name = cookbook_name_or_path.split(/:/).first
9
+ @version = cookbook_name_or_path.split(/:/).last
10
+ end
11
+
12
+ # ------------- Interface Methods
13
+
14
+ def prepare
15
+ @pkg_name = "#{name}-#{version}.tar.gz"
16
+ @pkg_dir = "#{Mofa::Config.config['tmp_dir']}/.mofa/#{token}"
17
+ end
18
+
19
+ def execute
20
+ # TODO: Download & unpack released cookbook
21
+ # Important for guessing role runlists (when cookbook is an env-cookbook)
22
+ end
23
+
24
+ def cleanup
25
+ say "Removing folder #{pkg_dir}...#{nl}"
26
+ run "rm -r #{pkg_dir}"
27
+ ok
28
+ end
29
+
30
+ # ------------- /Interface Methods
31
+
32
+ def cleanup!
33
+ unless (Dir.entries("#{Mofa::Config.config['tmp_dir']}/.mofa") - %w{ . .. }).empty?
34
+ say "Removing content of folder #{Mofa::Config.config['tmp_dir']}/.mofa"
35
+ run "rm -r #{Mofa::Config.config['tmp_dir']}/.mofa/*"
36
+ else
37
+ say "Folder #{Mofa::Config.config['tmp_dir']}/.mofa is (already) clean."
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def nl
44
+ return (Mofa::CLI::option_verbose) ? '' : ' '
45
+ end
46
+
47
+ end
@@ -0,0 +1,54 @@
1
+ class RunlistMap
2
+ attr_accessor :mp
3
+ attr_accessor :cookbook
4
+ attr_accessor :hostlist
5
+ attr_accessor :token
6
+ attr_accessor :option_runlist
7
+ attr_accessor :default_runlist
8
+
9
+ def self.create(cookbook, hostlist, token, option_runlist = nil)
10
+ rl = RunlistMap.new
11
+ rl.cookbook = cookbook
12
+ rl.hostlist = hostlist
13
+ rl.token = token
14
+ rl.default_runlist = (!option_runlist.nil?) ? option_runlist : nil
15
+ rl
16
+ end
17
+
18
+ def initialize
19
+ @mp = {}
20
+ end
21
+
22
+ def generate
23
+ @default_runlist ||= "recipe[#{cookbook.name}::default]"
24
+
25
+ case cookbook.type
26
+ when 'env'
27
+ guess_runlists_by_hostnames
28
+ else
29
+ set_default_runlist_for_every_host
30
+ end
31
+ end
32
+
33
+ def guess_runlists_by_hostnames
34
+ # recipes/jkmaster.rb --> runlist[<env_cookbook_name>::jkmaster] for all hosts with shortname jkmaster
35
+ # recipes/jkslave.rb --> runlist[<env_cookbook_name>::jkslave] for all hosts with shortname jkslave[0-9]
36
+ # and so on
37
+ hostlist.list.each do |hostname|
38
+ cookbook.recipes.each do |recipe|
39
+ recipe_regex = "^#{recipe}[0-9]*\."
40
+ if hostname.match(recipe_regex)
41
+ @mp.store(hostname, "recipe[#{cookbook.name}::#{recipe}]")
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ def set_default_runlist_for_every_host
49
+ hostlist.list.each do |hostname|
50
+ @mp.store(hostname, @default_runlist)
51
+ end
52
+ end
53
+
54
+ end