taste_tester 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/bin/taste-tester ADDED
@@ -0,0 +1,354 @@
1
+ #!/opt/chef/embedded/bin/ruby
2
+ # vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
3
+ # rubocop:disable UnusedBlockArgument, AlignParameters
4
+
5
+ if ENV['MY_RUBY_HOME'] && ENV['MY_RUBY_HOME'].include?('rvm')
6
+ puts 'Please disable RVM before running taste-tester'
7
+ exit(1)
8
+ end
9
+
10
+ require 'rubygems'
11
+ require 'time'
12
+ require 'optparse'
13
+ require 'colorize'
14
+
15
+ require 'taste_tester/logging'
16
+ require 'taste_tester/config'
17
+ require 'taste_tester/commands'
18
+ require 'taste_tester/hooks'
19
+
20
+ include TasteTester::Logging
21
+
22
+ if ENV['USER'] == 'root'
23
+ logger.warn('You should not be running as root')
24
+ exit(1)
25
+ end
26
+
27
+ # Command line parsing and param descriptions
28
+ module TasteTester
29
+ verify = 'Verify your changes were actually applied as intended!'.red
30
+ cmd = TasteTester::Config.chef_client_command
31
+ description = <<-EOF
32
+ Welcome to taste-tester!
33
+
34
+ Usage: taste-tester <mode> [<options>]
35
+
36
+ TLDR; Most common usage is:
37
+ vi cookbooks/... # Make your changes and commit locally
38
+ taste-tester test -s [host] # Put host in test mode
39
+ ssh root@[host] # Log on host
40
+ #{format('%-28s', " #{cmd}")} # Run chef and watch it break
41
+ vi cookbooks/... # Fix your cookbooks
42
+ taste-tester upload # Upload the diff
43
+ ssh root@[host] # Log on host
44
+ #{format('%-28s', " #{cmd}")} # Run chef and watch it succeed
45
+ <#{verify}>
46
+ taste-tester untest [host] # Put host back in production
47
+ # (optional - will revert itself after 1 hour)
48
+
49
+ And you're done! See the above wiki page for more details.
50
+
51
+ MODES:
52
+ test
53
+ Sync your local repo to your virtual Chef server (same as 'upload'), and
54
+ point some production server specified by -s to your virtual chef server for
55
+ testing. If you have you a plugin that uses the hookpoint, it'll may amend
56
+ your commit message to denote the server you tested.
57
+
58
+ upload
59
+ Sync your local repo to your virtual Chef server (i.e. just the first step
60
+ of 'test'). By defailt, it intelligently uploads whatever has changed since
61
+ the last time you ran upload (or test), but tracking git changes (even
62
+ across branch changes). You may specify -f to force a full upload of all
63
+ cookbooks and roles. It also does a fair amount of sanity checking on
64
+ your repo and you may specify --skip-repo-checks to bypass this.
65
+
66
+ keeptesting
67
+ Extend the testing time on server specified by -s by 1 hour unless
68
+ otherwise specified by -t.
69
+
70
+ untest
71
+ Return the server specified in -s to production.
72
+
73
+ status
74
+ Print out the state of the world.
75
+
76
+ run
77
+ Run #{cmd} on the machine specified by '-s' over SSH and print the output.
78
+ NOTE!! This is #{'NOT'.red} a sufficient test, you must log onto the remote
79
+ machine and verify the changes you are trying to make are actually present.
80
+
81
+ stop
82
+ You probably don't want this. It will shutdown the chef-zero server on
83
+ your localhost.
84
+
85
+ start
86
+ You probably don't want this. It will start up the chef-zero server on
87
+ your localhost. taste-tester dynamically starts this if it's down, so there
88
+ should be no need to do this manually.
89
+
90
+ restart
91
+ You probably don't want this. It will restart up the chef-zero server on
92
+ your localhost. taste-tester dynamically starts this if it's down, so there
93
+ should be no need to do this manually.
94
+ EOF
95
+
96
+ mode = ARGV.shift unless ARGV.size > 0 && ARGV[0].start_with?('-')
97
+
98
+ unless mode
99
+ mode = 'help'
100
+ puts "ERROR: No mode specified\n\n"
101
+ end
102
+
103
+ options = { :config_file => TasteTester::Config.config_file }
104
+ parser = OptionParser.new do |opts|
105
+ opts.banner = description
106
+
107
+ opts.separator ''
108
+ opts.separator 'Global options:'.upcase
109
+
110
+ opts.on('-c', '--config FILE', 'Config file') do |file|
111
+ unless File.exists?(File.expand_path(file))
112
+ logger.error("Sorry, cannot find #{file}")
113
+ exit(1)
114
+ end
115
+ options[:config_file] = file
116
+ end
117
+
118
+ opts.on('-v', '--verbose', 'Verbosity, provide twice for all debug') do
119
+ # If -vv is supplied this block is executed twice
120
+ if options[:verbosity]
121
+ options[:verbosity] = Logger::DEBUG
122
+ else
123
+ options[:verbosity] = Logger::INFO
124
+ end
125
+ end
126
+
127
+ opts.on('-p', '--plugin-path FILE', String, 'Plugin file') do |file|
128
+ unless File.exists?(File.expand_path(file))
129
+ logger.error("Sorry, cannot find #{file}")
130
+ exit(1)
131
+ end
132
+ options[:plugin_path] = file
133
+ end
134
+
135
+ opts.on('-h', '--help', 'Print help message.') do
136
+ print opts
137
+ exit
138
+ end
139
+
140
+ opts.on('-T', '--timestamp', 'Time-stamped log style output') do
141
+ options[:timestamp] = true
142
+ end
143
+
144
+ opts.separator ''
145
+ opts.separator 'Sub-command options:'.upcase
146
+
147
+ opts.on(
148
+ '-C', '--cookbooks COOKBOOK[,COOKBOOK]', Array,
149
+ 'Specific cookbooks to upload. Intended mostly for debugging,' +
150
+ ' not recommended. Works on upload and test. Not yet implemented.'
151
+ ) do |cbs|
152
+ options[:cookbooks] = cbs
153
+ end
154
+
155
+ opts.on(
156
+ '-D', '--databag DATABAG/ITEM[,DATABAG/ITEM]', Array,
157
+ 'Specific cookbooks to upload. Intended mostly for debugging,' +
158
+ ' not recommended. Works on upload and test. Not yet implemented.'
159
+ ) do |cbs|
160
+ options[:databags] = cbs
161
+ end
162
+
163
+ opts.on(
164
+ '-f', '--force-upload',
165
+ 'Force upload everything. Works on upload and test.'
166
+ ) do
167
+ options[:force_upload] = true
168
+ end
169
+
170
+ opts.on(
171
+ '--chef-port-range PORT1,PORT2', Array,
172
+ 'Port range for chef-zero'
173
+ ) do |ports|
174
+ unless ports.count == 2
175
+ logger.error("Invalid port range: #{ports}")
176
+ exit 1
177
+ end
178
+ options[:chef_port_range] = ports
179
+ end
180
+
181
+ opts.on(
182
+ '--tunnel-port PORT', 'Port for ssh tunnel'
183
+ ) do |port|
184
+ options[:user_tunnel_port] = port
185
+ end
186
+
187
+ opts.on(
188
+ '-l', '--linkonly', 'Only setup the remote server, skip uploading.'
189
+ ) do
190
+ options[:linkonly] = true
191
+ end
192
+
193
+ opts.on(
194
+ '-t', '--testing-timestamp TIME',
195
+ 'Until when should the host remain in testing.' +
196
+ ' Anything parsable is ok, such as "5/18 4:35" or "16/9/13".'
197
+ ) do |time|
198
+ begin
199
+ options[:testing_until] = Time.parse(time)
200
+ rescue
201
+ logger.error("Invalid date: #{time}")
202
+ exit 1
203
+ end
204
+ end
205
+
206
+ opts.on(
207
+ '-t', '--testing-time TIME',
208
+ 'How long should the host remain in testing.' +
209
+ ' Takes a simple relative time string, such as "45m", "4h" or "2d".'
210
+ ) do |time|
211
+ m = time.match(/^(\d+)([d|h|m]+)$/)
212
+ if m
213
+ exp = {
214
+ :d => 60 * 60 * 24,
215
+ :h => 60 * 60,
216
+ :m => 60,
217
+ }[m[2].to_sym]
218
+ delta = m[1].to_i * exp
219
+ options[:testing_until] = Time.now + delta.to_i
220
+ else
221
+ logger.error("Invalid testing-time: #{time}")
222
+ exit 1
223
+ end
224
+ end
225
+
226
+ opts.on(
227
+ '-r', '--repo DIR',
228
+ "Custom repo location, current deafult is #{TasteTester::Config.repo}." +
229
+ ' Works on upload and test.'
230
+ ) do |dir|
231
+ options[:repo] = dir
232
+ end
233
+
234
+ opts.on(
235
+ '-R', '--roles ROLE[,ROLE]', Array,
236
+ 'Specific roles to upload. Intended mostly for debugging,' +
237
+ ' not recommended. Works on upload and test. Not yet implemented.'
238
+ ) do |roles|
239
+ options[:roles] = roles
240
+ end
241
+
242
+ opts.on('--really', 'Really do link-only. DANGEROUS!') do |r|
243
+ options[:really] = r
244
+ end
245
+
246
+ opts.on(
247
+ '-s', '--servers SERVER[,SERVER]', Array,
248
+ 'Server to test/untest/keeptesting.'
249
+ ) do |s|
250
+ options[:servers] = s
251
+ end
252
+
253
+ opts.on(
254
+ '--user USER', 'Custom username for SSH, defaults to "root".' +
255
+ ' If custom user is specified, we will use sudo for all commands.'
256
+ ) do |user|
257
+ options[:user] = user
258
+ end
259
+
260
+ opts.on(
261
+ '-S', '--[no-]use-ssh-tunnels', 'Protect Chef traffic with SSH tunnels'
262
+ ) do |s|
263
+ options[:use_ssh_tunnels] = s
264
+ end
265
+
266
+ opts.on('--skip-repo-checks', 'Skip repository sanity checks') do
267
+ options[:skip_repo_checks] = true
268
+ end
269
+
270
+ opts.on('-y', '--yes', 'Do not prompt before testing.') do
271
+ options[:yes] = true
272
+ end
273
+
274
+ opts.separator ''
275
+ opts.separator 'Control local hook behavior with these options:'
276
+
277
+ opts.on(
278
+ '--skip-pre-upload-hook', 'Skip pre-upload hook. Works on upload, test.'
279
+ ) do
280
+ options[:skip_pre_upload_hook] = true
281
+ end
282
+
283
+ opts.on(
284
+ '--skip-post-upload-hook', 'Skip post-upload hook. Works on upload, test.'
285
+ ) do
286
+ options[:skip_post_upload_hook] = true
287
+ end
288
+
289
+ opts.on(
290
+ '--skip-pre-test-hook', 'Skip pre-test hook. Works on test.'
291
+ ) do
292
+ options[:skip_pre_test_hook] = true
293
+ end
294
+
295
+ opts.on(
296
+ '--skip-post-test-hook', 'Skip post-test hook. Works on test.'
297
+ ) do
298
+ options[:skip_post_test_hook] = true
299
+ end
300
+
301
+ opts.on(
302
+ '--skip-repo-checks-hook', 'Skip repo-checks hook. Works on upload, test.'
303
+ ) do
304
+ options[:skip_post_test_hook] = true
305
+ end
306
+ end
307
+
308
+ if mode == 'help'
309
+ puts parser
310
+ exit
311
+ end
312
+
313
+ parser.parse!
314
+
315
+ if File.exists?(File.expand_path(options[:config_file]))
316
+ TasteTester::Config.from_file(File.expand_path(options[:config_file]))
317
+ end
318
+ TasteTester::Config.merge!(options)
319
+ TasteTester::Logging.verbosity = TasteTester::Config.verbosity
320
+ TasteTester::Logging.use_log_formatter = TasteTester::Config.timestamp
321
+
322
+ if File.exists?(File.expand_path(TasteTester::Config.plugin_path))
323
+ TasteTester::Hooks.get(File.expand_path(TasteTester::Config[:plugin_path]))
324
+ end
325
+
326
+ case mode.to_sym
327
+ when :start
328
+ TasteTester::Commands.start
329
+ when :stop
330
+ TasteTester::Commands.stop
331
+ when :restart
332
+ TasteTester::Commands.restart
333
+ when :keeptesting
334
+ TasteTester::Commands.keeptesting
335
+ when :status
336
+ TasteTester::Commands.status
337
+ when :test
338
+ TasteTester::Commands.test
339
+ when :untest
340
+ TasteTester::Commands.untest
341
+ when :run
342
+ TasteTester::Commands.runchef
343
+ when :upload
344
+ TasteTester::Commands.upload
345
+ else
346
+ logger.error("Invalid mode: #{mode}")
347
+ puts parser
348
+ exit(1)
349
+ end
350
+ end
351
+
352
+ if __FILE__ == $PROGRAM_NAME
353
+ include TasteTester
354
+ end
@@ -0,0 +1,169 @@
1
+ # vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
2
+
3
+ require 'taste_tester/logging'
4
+ require 'between_meals/repo'
5
+ require 'between_meals/knife'
6
+ require 'between_meals/changeset'
7
+
8
+ module TasteTester
9
+ # Client side upload functionality
10
+ # Ties together Repo/Changeset diff logic
11
+ # and Server/Knife uploads
12
+ class Client
13
+ include TasteTester::Logging
14
+ include BetweenMeals::Util
15
+
16
+ attr_accessor :force, :skip_checks
17
+
18
+ def initialize(server)
19
+ @server = server
20
+ @knife = BetweenMeals::Knife.new(
21
+ :logger => logger,
22
+ :user => @server.user,
23
+ :host => @server.host,
24
+ :port => @server.port,
25
+ :role_dir => TasteTester::Config.roles,
26
+ :cookbook_dirs => TasteTester::Config.cookbooks,
27
+ :databag_dir => TasteTester::Config.databags,
28
+ :checksum_dir => TasteTester::Config.checksum_dir,
29
+ )
30
+ @knife.write_user_config
31
+ @repo = BetweenMeals::Repo.get(TasteTester::Config.repo_type,
32
+ TasteTester::Config.repo, logger)
33
+ fail 'Could not read repo' unless @repo
34
+ end
35
+
36
+ def checks
37
+ unless @skip_checks
38
+ TasteTester::Hooks.repo_checks(TasteTester::Config.dryrun, @repo)
39
+ end
40
+ end
41
+
42
+ def upload
43
+ checks unless @skip_checks
44
+
45
+ logger.warn("Using #{TasteTester::Config.repo}")
46
+ logger.info("Last commit: #{@repo.head_rev} " +
47
+ "'#{@repo.last_msg.split("\n").first}'" +
48
+ " by #{@repo.last_author[:email]}")
49
+
50
+ if @force || !@server.latest_uploaded_ref
51
+ logger.info('Full upload forced') if @force
52
+ unless TasteTester::Config.skip_pre_upload_hook
53
+ TasteTester::Hooks.pre_upload(TasteTester::Config.dryrun,
54
+ @repo,
55
+ nil,
56
+ @repo.head_rev)
57
+ end
58
+ time(logger) { full }
59
+ unless TasteTester::Config.skip_post_upload_hook
60
+ TasteTester::Hooks.post_upload(TasteTester::Config.dryrun,
61
+ @repo,
62
+ nil,
63
+ @repo.head_rev)
64
+ end
65
+ else
66
+ # Since we also upload the index, we always need to run the
67
+ # diff even if the version we're on is the same as the last
68
+ # revision
69
+ unless TasteTester::Config.skip_pre_upload_hook
70
+ TasteTester::Hooks.pre_upload(TasteTester::Config.dryrun,
71
+ @repo,
72
+ @server.latest_uploaded_ref,
73
+ @repo.head_rev)
74
+ end
75
+ begin
76
+ time(logger) { partial }
77
+ rescue BetweenMeals::Changeset::ReferenceError
78
+ logger.warn('Something changed with your repo, doing full upload')
79
+ time(logger) { full }
80
+ end
81
+ unless TasteTester::Config.skip_post_upload_hook
82
+ TasteTester::Hooks.post_upload(TasteTester::Config.dryrun,
83
+ @repo,
84
+ @server.latest_uploaded_ref,
85
+ @repo.head_rev)
86
+ end
87
+ end
88
+
89
+ @server.latest_uploaded_ref = @repo.head_rev
90
+ end
91
+
92
+ private
93
+
94
+ def full
95
+ logger.warn('Doing full upload')
96
+ @knife.cookbook_upload_all
97
+ @knife.role_upload_all
98
+ @knife.databag_upload_all
99
+ end
100
+
101
+ def partial
102
+ logger.info('Doing differential upload from ' +
103
+ @server.latest_uploaded_ref)
104
+ changeset = BetweenMeals::Changeset.new(
105
+ logger,
106
+ @repo,
107
+ @server.latest_uploaded_ref,
108
+ nil,
109
+ {
110
+ :cookbook_dirs =>
111
+ TasteTester::Config.relative_cookbook_dirs,
112
+ :role_dir =>
113
+ TasteTester::Config.relative_role_dir,
114
+ :databag_dir =>
115
+ TasteTester::Config.relative_databag_dir,
116
+ },
117
+ )
118
+
119
+ cbs = changeset.cookbooks
120
+ deleted_cookbooks = cbs.select { |x| x.status == :deleted }
121
+ modified_cookbooks = cbs.select { |x| x.status == :modified }
122
+ roles = changeset.roles
123
+ deleted_roles = roles.select { |x| x.status == :deleted }
124
+ modified_roles = roles.select { |x| x.status == :modified }
125
+ databags = changeset.databags
126
+ deleted_databags = databags.select { |x| x.status == :deleted }
127
+ modified_databags = databags.select { |x| x.status == :modified }
128
+
129
+ didsomething = false
130
+ unless deleted_cookbooks.empty?
131
+ didsomething = true
132
+ logger.warn("Deleting cookbooks: [#{deleted_cookbooks.join(' ')}]")
133
+ @knife.cookbook_delete(deleted_cookbooks)
134
+ end
135
+
136
+ unless modified_cookbooks.empty?
137
+ didsomething = true
138
+ logger.warn("Uploading cookbooks: [#{modified_cookbooks.join(' ')}]")
139
+ @knife.cookbook_upload(modified_cookbooks)
140
+ end
141
+
142
+ unless deleted_roles.empty?
143
+ didsomething = true
144
+ logger.warn("Deleting roles: [#{deleted_roles.join(' ')}]")
145
+ @knife.role_delete(deleted_roles)
146
+ end
147
+
148
+ unless modified_roles.empty?
149
+ didsomething = true
150
+ logger.warn("Uploading roles: [#{modified_roles.join(' ')}]")
151
+ @knife.role_upload(modified_roles)
152
+ end
153
+
154
+ unless deleted_databags.empty?
155
+ didsomething = true
156
+ logger.warn("Deleting databags: [#{deleted_databags.join(' ')}]")
157
+ @knife.databag_delete(deleted_databags)
158
+ end
159
+
160
+ unless modified_databags.empty?
161
+ didsomething = true
162
+ logger.warn("Uploading databags: [#{modified_databags.join(' ')}]")
163
+ @knife.databag_upload(modified_databags)
164
+ end
165
+
166
+ logger.warn('Nothing to upload!') unless didsomething
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,144 @@
1
+ # vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
2
+ # rubocop:disable UnusedBlockArgument, UnusedMethodArgument
3
+
4
+ require 'taste_tester/server'
5
+ require 'taste_tester/host'
6
+ require 'taste_tester/config'
7
+ require 'taste_tester/client'
8
+ require 'taste_tester/logging'
9
+
10
+ module TasteTester
11
+ # Functionality dispatch
12
+ module Commands
13
+ extend TasteTester::Logging
14
+
15
+ def self.start
16
+ server = TasteTester::Server.new
17
+ return if TasteTester::Server.running?
18
+ server.start
19
+ end
20
+
21
+ def self.restart
22
+ server = TasteTester::Server.new
23
+ server.stop if TasteTester::Server.running?
24
+ server.start
25
+ end
26
+
27
+ def self.stop
28
+ server = TasteTester::Server.new
29
+ server.stop
30
+ end
31
+
32
+ def self.status
33
+ server = TasteTester::Server.new
34
+ if TasteTester::Server.running?
35
+ logger.warn("Local taste-tester server running on port #{server.port}")
36
+ if server.latest_uploaded_ref
37
+ logger.warn('Latest uploaded revision is ' +
38
+ server.latest_uploaded_ref)
39
+ else
40
+ logger.warn('No cookbooks/roles uploads found')
41
+ end
42
+ else
43
+ logger.warn('Local taste-tester server not running')
44
+ end
45
+ end
46
+
47
+ def self.test
48
+ hosts = TasteTester::Config.servers
49
+ unless hosts
50
+ logger.warn('You must provide a hostname')
51
+ exit(1)
52
+ end
53
+ unless TasteTester::Config.yes
54
+ printf("Set #{TasteTester::Config.servers} to test mode? [y/N] ")
55
+ ans = STDIN.gets.chomp
56
+ exit(1) unless ans =~ /^[yY](es)?$/
57
+ end
58
+ if TasteTester::Config.linkonly && TasteTester::Config.really
59
+ logger.warn('Skipping upload at user request... potentially dangerous!')
60
+ else
61
+ if TasteTester::Config.linkonly
62
+ logger.warn('Ignoring --linkonly because --really not set')
63
+ end
64
+ upload
65
+ end
66
+ server = TasteTester::Server.new
67
+ repo = BetweenMeals::Repo.get(TasteTester::Config.repo_type,
68
+ TasteTester::Config.repo, logger)
69
+ unless TasteTester::Config.skip_pre_test_hook
70
+ TasteTester::Hooks.pre_test(TasteTester::Config.dryrun, repo, hosts)
71
+ end
72
+ tested_hosts = []
73
+ hosts.each do |hostname|
74
+ host = TasteTester::Host.new(hostname, server)
75
+ if host.in_test?
76
+ username = host.who_is_testing
77
+ logger.error("User #{username} is already testing on #{hostname}")
78
+ else
79
+ host.test
80
+ tested_hosts << hostname
81
+ end
82
+ end
83
+ unless TasteTester::Config.skip_post_test_hook
84
+ TasteTester::Hooks.post_test(TasteTester::Config.dryrun, repo,
85
+ tested_hosts)
86
+ end
87
+ end
88
+
89
+ def self.untest
90
+ hosts = TasteTester::Config.servers
91
+ unless hosts
92
+ logger.warn('You must provide a hostname')
93
+ exit(1)
94
+ end
95
+ server = TasteTester::Server.new
96
+ hosts.each do |hostname|
97
+ host = TasteTester::Host.new(hostname, server)
98
+ host.untest
99
+ end
100
+ end
101
+
102
+ def self.runchef
103
+ hosts = TasteTester::Config.servers
104
+ unless hosts
105
+ logger.warn('You must provide a hostname')
106
+ exit(1)
107
+ end
108
+ server = TasteTester::Server.new
109
+ hosts.each do |hostname|
110
+ host = TasteTester::Host.new(hostname, server)
111
+ host.run
112
+ end
113
+ end
114
+
115
+ def self.keeptesting
116
+ hosts = TasteTester::Config.servers
117
+ unless hosts
118
+ logger.warn('You must provide a hostname')
119
+ exit(1)
120
+ end
121
+ server = TasteTester::Server.new
122
+ hosts.each do |hostname|
123
+ host = TasteTester::Host.new(hostname, server)
124
+ host.keeptesting
125
+ end
126
+ end
127
+
128
+ def self.upload
129
+ server = TasteTester::Server.new
130
+ # On a fore-upload rather than try to clean up whatever's on the server
131
+ # we'll restart chef-zero which will clear everything and do a full
132
+ # upload
133
+ if TasteTester::Config.force_upload
134
+ server.restart
135
+ else
136
+ server.start unless TasteTester::Server.running?
137
+ end
138
+ client = TasteTester::Client.new(server)
139
+ client.skip_checks = true if TasteTester::Config.skip_checks
140
+ client.force = true if TasteTester::Config.force_upload
141
+ client.upload
142
+ end
143
+ end
144
+ end