Module: TasteTester::Commands

Extended by:
Logging
Defined in:
lib/taste_tester/commands.rb

Overview

Functionality dispatch

Class Method Summary collapse

Methods included from Logging

formatter, formatterproc=, logger, logger, use_log_formatter=, verbosity=

Class Method Details

._find_changeset(repo) ⇒ Object



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/taste_tester/commands.rb', line 248

def self._find_changeset(repo)
  # We want to compare changes in the current directory (working set) with
  # the "most recent" commit in the VCS. For SVN, this will be the latest
  # commit on the checked out repository (i.e. 'trunk'). Git/Hg may have
  # different tags or labels assigned to the master branch, (i.e. 'master',
  # 'stable', etc.) and should be configured if different than the default.
  start_ref = case repo
              when BetweenMeals::Repo::Svn
                repo.latest_revision
              when BetweenMeals::Repo::Git
                TasteTester::Config.vcs_start_ref_git
              when BetweenMeals::Repo::Hg
                TasteTester::Config.vcs_start_ref_hg
              end
  end_ref = TasteTester::Config.vcs_end_ref

  changeset = BetweenMeals::Changeset.new(
    logger,
    repo,
    start_ref,
    end_ref,
    {
      :cookbook_dirs =>
        TasteTester::Config.relative_cookbook_dirs,
      :role_dir =>
        TasteTester::Config.relative_role_dir,
      :databag_dir =>
        TasteTester::Config.relative_databag_dir,
    },
    @track_symlinks,
  )

  return changeset
end

._find_roles(changes) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/taste_tester/commands.rb', line 283

def self._find_roles(changes)
  if TasteTester::Config.relative_cookbook_dirs.length > 1
    logger.error('Knife deps does not support multiple cookbook paths.')
    logger.error('Please flatten the cookbooks into a single directory' +
                 ' or define the find_impact method in a local plugin.')
    exit(1)
  end

  cookbooks = Set.new(changes.cookbooks)
  roles = Set.new(changes.roles)
  databags = Set.new(changes.databags)

  if cookbooks.empty? && roles.empty?
    unless TasteTester::Config.json
      logger.warn('No cookbooks or roles have been modified.')
    end
    return Set.new
  end

  unless cookbooks.empty?
    logger.info('Modified Cookbooks:')
    cookbooks.each { |cb| logger.info("\t#{cb}") }
  end
  unless roles.empty?
    logger.info('Modified Roles:')
    roles.each { |r| logger.info("\t#{r}") }
  end
  unless databags.empty?
    logger.info('Modified Databags:')
    databags.each { |db| logger.info("\t#{db}") }
  end

  # Use Knife to list the dependecies for each role in the roles directory.
  # This creates a recursive tree structure that is then searched for
  # instances of modified cookbooks. This can be slow since it must read
  # every line of the Knife output, then search all roles for dependencies.
  # If you have a custom way to calculate these reverse dependencies, this
  # is the part you would replace.
  logger.info('Finding dependencies (this may take a minute or two)...')
  knife = Mixlib::ShellOut.new(
    "knife deps /#{TasteTester::Config.role_dir}/*.rb" +
    " --config #{TasteTester::Config.knife_config}" +
    " --chef-repo-path #{TasteTester::Config.absolute_base_dir}" +
    ' --tree --recurse',
  )
  knife.run_command
  knife.error!

  # Collapse the output from Knife into a hash structure that maps roles
  # to the set of their dependencies. This will ignore duplicates in the
  # Knife output, but must still process each line.
  logger.info('Processing Dependencies...')
  deps_hash = {}
  curr_role = nil

  knife.stdout.each_line do |line|
    elem = line.rstrip
    if elem.length == elem.lstrip.length
      curr_role = elem
      deps_hash[curr_role] = Set.new
    else
      deps_hash[curr_role].add(File.basename(elem, File.extname(elem)))
    end
  end

  # Now we can search for modified dependencies by iterating over each
  # role and checking the hash created earlier. Roles that have been
  # modified directly are automatically included in the impacted set.
  impacted_roles = Set.new(roles.map(&:name))
  deps_hash.each do |role, deplist|
    cookbooks.each do |cb|
      if deplist.include?(cb.name)
        impacted_roles.add(role)
        logger.info("\tFound dependency: #{role} --> #{cb.name}")
        break
      end
    end
  end

  return impacted_roles
end

._print_impact(final_impact) ⇒ Object



365
366
367
368
369
370
371
372
373
374
375
# File 'lib/taste_tester/commands.rb', line 365

def self._print_impact(final_impact)
  if TasteTester::Config.json
    puts JSON.pretty_generate(final_impact.to_a)
  elsif final_impact.empty?
    logger.warn('No impacted roles were found.')
  else
    logger.warn('The following roles have modified dependencies.' +
                ' Please test a host in each of these roles.')
    final_impact.each { |r| logger.warn("\t#{r}") }
  end
end

.impactObject



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/taste_tester/commands.rb', line 214

def self.impact
  # Use the repository specified in config.rb to calculate the changes
  # that may affect Chef. These changes will be further analyzed to
  # determine specific roles which may change due to modifed dependencies.
  repo = BetweenMeals::Repo.get(
    TasteTester::Config.repo_type,
    TasteTester::Config.repo,
    logger,
  )
  if repo && !repo.exists?
    fail "Could not open repo from #{TasteTester::Config.repo}"
  end

  changes = _find_changeset(repo)

  # Perform preliminary impact analysis. By default, use Knife to find
  # the roles dependent on modified cookbooks. Custom logic may provide
  # additional information by defining the find_impact plugin method.
  basic_impact = TasteTester::Hooks.find_impact(changes)
  basic_impact ||= _find_roles(changes)

  # Do any post processing required on the list of impacted roles, such
  # as looking up hostnames associated with each role. By default, pass
  # the preliminary results through unmodified.
  final_impact = TasteTester::Hooks.post_impact(basic_impact)
  final_impact ||= basic_impact

  # Print the calculated impact. If a print hook is defined that
  # returns true, then the default print function is skipped.
  unless TasteTester::Hooks.print_impact(final_impact)
    _print_impact(final_impact)
  end
end

.keeptestingObject



165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/taste_tester/commands.rb', line 165

def self.keeptesting
  hosts = TasteTester::Config.servers
  unless hosts
    logger.warn('You must provide a hostname')
    exit(1)
  end
  server = TasteTester::Server.new
  hosts.each do |hostname|
    host = TasteTester::Host.new(hostname, server)
    host.keeptesting
  end
end

.restartObject



36
37
38
39
# File 'lib/taste_tester/commands.rb', line 36

def self.restart
  server = TasteTester::Server.new
  server.restart
end

.runchefObject



152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/taste_tester/commands.rb', line 152

def self.runchef
  hosts = TasteTester::Config.servers
  unless hosts
    logger.warn('You must provide a hostname')
    exit(1)
  end
  server = TasteTester::Server.new
  hosts.each do |hostname|
    host = TasteTester::Host.new(hostname, server)
    host.runchef
  end
end

.startObject



29
30
31
32
33
34
# File 'lib/taste_tester/commands.rb', line 29

def self.start
  server = TasteTester::Server.new
  return if TasteTester::Server.running?

  server.start
end

.statusObject



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/taste_tester/commands.rb', line 46

def self.status
  server = TasteTester::Server.new
  if TasteTester::Server.running?
    logger.warn("Local taste-tester server running on port #{server.port}")
    if TasteTester::Config.no_repo && server.last_upload_time
      logger.warn("Last upload time was #{server.last_upload_time}")
    elsif !TasteTester::Config.no_repo && server.latest_uploaded_ref
      if server.last_upload_time
        logger.warn("Last upload time was #{server.last_upload_time}")
      end
      logger.warn('Latest uploaded revision is ' +
        server.latest_uploaded_ref)
    else
      logger.warn('No cookbooks/roles uploads found')
    end
  else
    logger.warn('Local taste-tester server not running')
  end
end

.stopObject



41
42
43
44
# File 'lib/taste_tester/commands.rb', line 41

def self.stop
  server = TasteTester::Server.new
  server.stop
end

.testObject



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/taste_tester/commands.rb', line 66

def self.test
  hosts = TasteTester::Config.servers
  unless hosts
    logger.warn('You must provide a hostname')
    exit(1)
  end
  unless TasteTester::Config.yes
    printf("Set #{TasteTester::Config.servers} to test mode? [y/N] ")
    ans = STDIN.gets.chomp
    exit(1) unless ans =~ /^[yY](es)?$/
  end
  if TasteTester::Config.linkonly && TasteTester::Config.really
    logger.warn('Skipping upload at user request... potentially dangerous!')
  else
    if TasteTester::Config.linkonly
      logger.warn('Ignoring --linkonly because --really not set')
    end
    upload
  end
  server = TasteTester::Server.new
  unless TasteTester::Config.linkonly
    if TasteTester::Config.no_repo
      repo = nil
    else
      repo = BetweenMeals::Repo.get(
        TasteTester::Config.repo_type,
        TasteTester::Config.repo,
        logger,
      )
    end
    if repo && !repo.exists?
      fail "Could not open repo from #{TasteTester::Config.repo}"
    end
  end
  unless TasteTester::Config.skip_pre_test_hook ||
      TasteTester::Config.linkonly
    TasteTester::Hooks.pre_test(TasteTester::Config.dryrun, repo, hosts)
  end
  tested_hosts = []
  hosts.each do |hostname|
    host = TasteTester::Host.new(hostname, server)
    begin
      host.test
      tested_hosts << hostname
    rescue TasteTester::Exceptions::AlreadyTestingError => e
      logger.error("User #{e.username} is already testing on #{hostname}")
    end
  end
  unless TasteTester::Config.skip_post_test_hook ||
      TasteTester::Config.linkonly
    TasteTester::Hooks.post_test(TasteTester::Config.dryrun, repo,
                                 tested_hosts)
  end
  # Strictly: hosts and tested_hosts should be sets to eliminate variance in
  # order or duplicates. The exact comparison works here because we're
  # building tested_hosts from hosts directly.
  if tested_hosts == hosts
    # No exceptions, complete success: every host listed is now configured
    # to use our chef-zero instance.
    exit(0)
  end
  if tested_hosts.empty?
    # All requested hosts are being tested by another user. We didn't change
    # their configuration.
    exit(3)
  end
  # Otherwise, we got a mix of success and failure due to being tested by
  # another user. We'll be pessemistic and return an error because the
  # intent to taste test the complete list was not successful.
  # code.
  exit(2)
end

.untestObject



139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/taste_tester/commands.rb', line 139

def self.untest
  hosts = TasteTester::Config.servers
  unless hosts
    logger.error('You must provide a hostname')
    exit(1)
  end
  server = TasteTester::Server.new
  hosts.each do |hostname|
    host = TasteTester::Host.new(hostname, server)
    host.untest
  end
end

.uploadObject



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/taste_tester/commands.rb', line 178

def self.upload
  server = TasteTester::Server.new
  # On a force-upload rather than try to clean up whatever's on the server
  # we'll restart chef-zero which will clear everything and do a full
  # upload
  if TasteTester::Config.force_upload
    server.restart
  else
    server.start
  end
  client = TasteTester::Client.new(server)
  client.skip_checks = true if TasteTester::Config.skip_repo_checks
  client.force = true if TasteTester::Config.force_upload
  client.upload
rescue StandardError => exception
  # We're trying to recover from common chef-zero errors
  # Most of them happen due to half finished uploads, which leave
  # chef-zero in undefined state
  errors = [
    'Cannot find a cookbook named',
    'Connection reset by peer',
    'Object not found',
  ]
  if errors.any? { |e| exception.to_s.match(/#{e}/im) }
    TasteTester::Config.force_upload = true
    unless @already_retried
      @already_retried = true
      retry
    end
  end
  logger.error('Upload failed')
  logger.error(exception.to_s)
  logger.error(exception.backtrace.join("\n"))
  exit 1
end