mofa 0.0.1

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,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