taste_tester 0.0.1 → 0.0.2

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