# # class that hold utilities that I use to test openstack # module Puppetlabs module OsTester require 'yaml' require 'github_api' require 'open3' class TestException < Exception end def cmd_system (cmd, print=true) puts "Running cmd: #{Array(cmd).join(' ')}" if print output = `#{cmd}`.split("\n") puts output.join("\n") if print raise(StandardError, "Cmd #{cmd} failed") unless $?.success? #Open3.popen3(*cmd) do |i, o, e, t| # output = o.read.split("\n") # raise StandardError, e.read unless (t ? t.value : $?).success? #end output end def git_cmd(cmd, print=true) cmd_system('git ' + cmd, print) end def vagrant_command(cmd, box='') require 'vagrant' env = Vagrant::Environment.new(:ui_class => Vagrant::UI::Colored) env.cli(cmd, box) end def on_box (box, cmd) require 'vagrant' env = Vagrant::Environment.new(:ui_class => Vagrant::UI::Colored) raise("Invalid VM: #{box}") unless vm = env.vms[box.to_sym] raise("VM: #{box} was not already created") unless vm.created? ssh_data = '' #vm.channel.sudo(cmd) do |type, data| vm.channel.execute(cmd) do |type, data| ssh_data = data env.ui.info(ssh_data.chomp, :prefix => false) end ssh_data end # destroy all vagrant instances def destroy_all_vms puts "About to destroy all vms..." vagrant_command('destroy -f') puts "Destroyed all vms" end # adds the specified remote name as a read/write remote def dev_setup(remote_name) each_repo do |module_name| # need to handle more failure cases remotes = git_cmd('remote') if remotes.include?(remote_name) puts "Did not have to add remote #{remote_name} to #{module_name}" elsif ! remotes.include?('origin') raise(TestException, "Repo #{module_name} has no remote called origin, failing") else remote_url = git_cmd('remote show origin').detect {|x| x =~ /\s+Push\s+URL: / } if remote_url =~ /(git|https?):\/\/(.+)\/(.+)?\/(.+)/ url = "git@#{$2}:#{remote_name}/#{$4}" else puts "remote_url #{remote_url} did not have the expected format. weird..." end puts "Adding remote #{remote_name} as #{url}" git_cmd("remote add #{remote_name} #{url}") end end end def pull_all each_repo do |module_name| puts "Pulling repo: #{module_name}" puts ' ' + git_cmd('pull').join("\n ") end end def status_all each_repo do |module_name| status = git_cmd('status') if status.include?('nothing to commit (working directory clean)') puts "Module #{module_name} has not changed" if verbose else puts "Uncommitted changes for: #{module_name}" puts " #{status.join("\n ")}" end end end def check_tags(project_name=nil) each_repo do |module_name| require 'puppet' if ! project_name || project_name == module_name modulefile = File.join(Dir.getwd, 'Modulefile') if File.exists?(modulefile) print module_name metadata = ::Puppet::ModuleTool::Metadata.new ::Puppet::ModuleTool::ModulefileReader.evaluate(metadata, modulefile) print ':' + metadata.version branch_output = git_cmd('branch') if branch_output.first =~ /\* (.+)/ puts ":#{$1}" puts ' ' + git_cmd("log #{metadata.version}..HEAD --oneline").join("\n ") puts '' else puts ' ' + branch_output.join("\n ") end else puts "#{module_name} does not have a Modulefile" end end end end # given a pull request, return true if we should test it. # this means that is can be merged, and has a comment where one of the admin users def deploy_two_node vagrant_command('up', 'openstack_controller') vagrant_command('up', 'compute1') end def refresh_modules ['modules', '.librarian', 'Puppetfile.lock', '.tmp', checkedoutfile_name].each do |dir| if File.exists?(File.join(base_dir, dir )) FileUtils.rm_rf(File.join(base_dir, dir)) end end FileUtils.rm(checkedout_file) if File.exists?(checkedout_file) cmd_system('librarian-puppet install') end def each_repo(&block) require 'librarian/puppet' require 'librarian/puppet/source/git' # create a manifest # TODO replace this to use librarian puppet env = Librarian::Puppet::Environment.new() # this is the lock file, so it assumes that install has been run env.lock.manifests.each do |manifest| # I only care about git sources if manifest.source.is_a? Librarian::Puppet::Source::Git module_name = manifest.name.split('/', 2)[1] module_path = File.join(env.install_path,module_name) if File.directory?(module_path) Dir.chdir(module_path) do yield module_name end else puts "Module directory #{module_path} does not exist... How strange." end else puts "Found a non-git manifest: #{manifest.class}, ignoring" end end end def contributor_hash( repos_i_care_about = ['nova', 'glance', 'openstack', 'keystone', 'swift', 'horizon', 'cinder'] ) contributors = {} each_repo do |module_name| if repos_i_care_about.include?(module_name) logs = git_cmd('log --format=short', print=false) user_lines = logs.select {|x| x =~ /^Author:\s+(.*)$/ } user_lines.collect do |x| if x =~ /^Author:\s+(.*)?\s+<((\S+)@(\S+))>$/ unless ['root', 'vagrant', 'Dan'].include?($1) if contributors[$1] contributors[$1][:repos] = contributors[$1][:repos] | [module_name] else contributors[$1] = {:email => $2, :repos => [module_name] } end else # trimming out extra users end else puts "Skipping unexpected line #{x}" end end end end contributors end # has specified the expected body. def testable_pull_request?( pr, admin_users, expected_body = 'test_it', options = {} ) if ! pr['merged'] if pr['mergeable'] if pr['comments'] > 0 comments = Github.new(options).issues.comments.all( pr['base']['user']['login'], pr['base']['repo']['name'], pr['number'] ) puts 'going through comments' comments.each do |comment| if admin_users.include?(comment['user']['login']) if comment['body'] == expected_body return true end end end else puts "PR: #{pr['number']} from #{pr['base']['repo']['name']} has no issue commments.\ I will not test it. We only test things approved. " end else puts "PR: #{pr['number']} from #{pr['base']['repo']['name']} cannot be merged, will not test" end else puts "PR: #{pr['number']} from #{pr['base']['repo']['name']} was already merged, will not test" end puts "Did not find comment matching #{expected_body}" return false end def checkedoutfile_name '.current_testing' end def checkedout_file File.join(base_dir, checkedoutfile_name) end def checkedout_branch return @checkout_branch_results if @checkout_branch_results_results co_file = checkedout_file if File.exists?(co_file) @checkout_branch_results = YAML.load_file(co_file) else @checkout_branch_results = {} end end def write_checkedout_file(project_name, number) File.open(checkedout_file, 'w') do |fh| fh.write({ :project => project_name, :number => number }.to_yaml) end end def checkout_pr(project_name, number, admin_users, expected_body, options) # but I should write some kind of repo select # depends on https://github.com/peter-murach/github require 'github_api' each_repo do |repo_name| if repo_name == project_name pr = Github.new(options).pull_requests.get('puppetlabs', "puppetlabs-#{project_name}", number) # need to be able to override this? if checkedout_branch[:project] if checkedout_branch[:project] == project_name and checkedout_branch[:number] == number puts "#{project_name}/#{number} already checkout out, not doing it again" return else raise(TestException, "Wanted to checkout: #{project_name}/#{number}, but #{checkedout_branch[:project]}/#{checkedout_branch[:number]} was already checked out") end end if testable_pull_request?(pr, admin_users, expected_body, options) clone_url = pr['head']['repo']['clone_url'] remote_name = pr['head']['user']['login'] sha = pr['head']['sha'] base_ref = pr['base']['ref'] if base_ref != 'master' raise(TestException, "At the moment, I do not support non-master base refs") end unless (diffs = git_cmd("diff origin/master")) == [] raise(TestException, "There are differences between the current checked out branch and master, you need to clean up these branhces before running any tests\n#{diffs.join("\n")}") end write_checkedout_file(project_name, number) puts 'found one that we should test' # TODO I am not sure how reliable all of this is going # to be remotes = git_cmd('remote') unless remotes.include?(remote_name) git_cmd("remote add #{remote_name} #{clone_url}") end git_cmd("fetch #{remote_name}") # TODO does that work if master has been updated? git_cmd("merge #{sha}") else raise("pull request #{project_name}/#{number} is not testable") end end end end # publish a string as a gist. # publish a link to that gist as a issue comment. def publish_results(project_name, number, outcome, body, options) require 'github_api' github = Github.new(options) gist_response = github.gists.create( 'description' => "#{project_name}/#{number}@#{Time.now.strftime("%Y%m%dT%H%M%S%z")}", 'public' => true, 'files' => { 'file1' => {'content' => body} } ) comments = github.issues.comments.create( 'puppetlabs', "puppetlabs-#{project_name}", number, 'body' => "Test #{outcome}. Results can be found here: #{gist_response.html_url}" ) end def test_two_node(oses = []) require 'yaml' #Rake::Task['openstack:setup'.to_sym].invoke oses.each do |os| update_vagrant_os(os) cmd_system('vagrant destroy -f') deploy_two_node # I should check this to see if the last line is cirros on_box('openstack_controller', 'sudo bash /tmp/test_nova.sh;exit $?') end end def update_vagrant_os(os) cfg = File.join(base_dir, 'config.yaml') yml = YAML.load_file(cfg).merge({'operatingsystem' => os}) File.open(cfg, 'w') {|f| f.write(yml.to_yaml) } end # iterate through each testable pull request def each_testable_pull_request(&block) end end end