#!/usr/local/bin/ruby require 'fileutils' require 'rubygems' gem 'activerecord' require 'activerecord' # TODO use native driver! gem 'postgres-pr' require 'postgres-pr/connection' gem 'actionpack' require 'pp' require '/home/tom/api/app/models/gem_download.rb' require '/home/tom/api/app/models/forum_group.rb' require '/home/tom/api/app/models/group.rb' require '/home/tom/api/app/models/group_history.rb' require '/home/tom/api/app/models/user_group.rb' require '/home/tom/api/app/models/forum.rb' require '/home/tom/api/app/models/user.rb' require '/home/tom/api/app/models/plugin.rb' require '/home/tom/api/app/models/frs_file.rb' require '/home/tom/api/app/models/mailing_list.rb' ActiveRecord::Base.establish_connection({:adapter => "postgresql", :database => "gforge", :username => "gforge" }) GITOSIS_UPDATE_LOCK_FILENAME = "/tmp/gitosis_update.lck" WWW_DIR_PREFIX="/var/www/gforge-projects/" APACHECTL="/sbin/service httpd " GIT_PATH="/usr/local/bin/git" CVSROOT="/var/cvs/" SVNROOT="/var/svn/" GITROOT="/var/git/" KEYDIR = "/home/tom/gitosis-admin/keydir/" GLOBAL_CONN = PostgresPR::Connection.new("gforge", "gforge") class GitosisConf def initialize(path) # TODO if a parsing exception happens here, the entire file # can be erased. This is bad, because it also erases the configuration # for the gitosis-admin group, which means it can't be fixed until that's # manually restored to /var/git/.gitosis.conf. See build_out_scripts/rubyforge/git_plugin.txt # for the exact config settings. Anyhow, should clean that up. @path = path @projects = File.read(@path).split(/\[group/)[1..-1].collect do |group| lines = group.split("\n") { :name => lines[0].split("\]")[0].strip, :keys => begin lines[2].split("=")[1].split(" ") ; rescue [] ; end } end end def add_or_update(group_name, keys) @projects.reject! {|p| p[:name] == group_name} @projects << { :name => group_name, :keys => keys } end def write File.open(@path, "w") do |f| f.syswrite(header) @projects.sort {|a,b| a[:name] <=> b[:name]}.each do |p| f.syswrite("[group #{p[:name]}]\n") f.syswrite("writable = #{p[:name]}\n") f.syswrite("members = #{p[:keys].join(" ")}\n") f.syswrite("\n") end end end def test_add pp @projects add "foobar", %w{tom@bux bash@goo} pp @projects write end private def header "[gitosis]\n\n" end end class GForge def approve_pending_projects group_was_provisioned = false Group.pending.each do |group| cmd = "/usr/local/bin/php -f /home/tom/support/trunk/support/approve_project.php #{group.unix_group_name}" puts "Approving project with: #{cmd}" puts `#{cmd}` group.provision group_was_provisioned = true end if group_was_provisioned puts "#{Time.now}: Updating unix accounts" update_unix_accounts puts "#{Time.now}: Restarting Apache" restart_apache end end def restart_apache system "/sbin/service httpd restart" end def clear_old_newgems still_old = [] File.read("/usr/local/apache2/conf/newgems.txt").each do |line| filename = line.split(" ")[0] frs = FrsFile.find_by_filename(filename) if frs && frs.hours_old < 48 puts "Retaining #{filename} since it's #{frs.hours_old} hours old" still_old << filename else puts "Discarding #{filename} since it's #{frs.nil? ? 'nil' : "#{frs.hours_old} hours old"}" end end File.open("/usr/local/apache2/conf/newgems.txt", "w") do |file| still_old.each do |name| file.syswrite("#{name} 1\n") end end FileUtils.chown "webuser", "webgroup", "/usr/local/apache2/conf/newgems.txt" end def etc_passwd_contents @etc_passwd_contents ||= File.read("/etc/passwd") end def etc_shadow_contents @etc_shadow_contents ||= File.read("/etc/shadow") end def ensure_user_account_set_up(user) if etc_passwd_contents !~ /:\/home\/#{user.user_name}:/ add_unix_user_account(user.user_name) @etc_passwd_contents = nil end if etc_shadow_contents !~ /#{user.user_name}:#{Regexp.escape(user.unix_pw)}/ set_unix_password(user.user_name, user.unix_pw) @etc_shadow_contents = nil end set_dot_forward_file_for(user) if !File.exists?(user.unix_forward_file_path) || File.read(user.unix_forward_file_path).strip != user.email end def update_unix_accounts puts "#{Time.now}: Starting update_unix_accounts" puts "#{Time.now}: Updating unix status" ActiveRecord::Base.connection.execute("update users set unix_status = 'A' where user_id in (select distinct u.user_id from users u, user_group ug WHERE u.user_id=ug.user_id and ug.cvs_flags='1' and u.status='A' and u.unix_status != 'A')") puts "#{Time.now}: Adding accounts, setting passwords, setting .forward files" User.active.with_scm_write_to_at_least_one_project.find_each do |user| ensure_user_account_set_up user end puts "#{Time.now}: Updating authorized_keys" User.active.with_uploaded_keys.find_each do |user| next if !File.exists?(user.home_directory) next if File.exists?(user.unix_authorized_keys_file_path) && File.read(user.unix_authorized_keys_file_path) == user.authorized_keys_with_newlines set_unix_authorized_keys_data_for(user) end puts "#{Time.now}: Updating user groups" cached_user_group_map = {} File.read("/root/gforge/user_group_map.dat").split("\n").each do |x| user,groups = x.split(":") cached_user_group_map[user] = groups.split(",") end cached_user_group_map.each {|k,v| cached_user_group_map[k] = v.sort } need_to_rebuild_cache = false database_user_group_map = User.user_with_sorted_groups_hash (cached_user_group_map.keys + database_user_group_map.keys).uniq.each do |user| next if database_user_group_map[user] && cached_user_group_map[user] && database_user_group_map[user].join == cached_user_group_map[user].join need_to_rebuild_cache = true puts "#{Time.now}: Need to update list of groups for #{user}; cache has #{user} in #{cached_user_group_map[user].join(',') rescue ''}; database has #{user} in #{database_user_group_map[user].join(',') rescue ''}" set_list_of_groups_for_user(user, database_user_group_map[user]) end if need_to_rebuild_cache puts "#{Time.now}: Rebuilding user/group cache" File.open("/root/gforge/user_group_map.dat", "w") do |f| database_user_group_map.each do |user,groups| f.syswrite("#{user}:#{groups.join(",")}\n") end end puts "#{Time.now}: Done update_unix_accounts" end end def set_unix_authorized_keys_data_for(user) puts "#{Time.now}: Updating keys for #{user.user_name}" if !File.exists?(user.dot_ssh_directory) FileUtils.mkdir(user.dot_ssh_directory, :mode => 0755) end File.open(user.unix_authorized_keys_file_path, "w") {|f| f.syswrite(user.authorized_keys_with_newlines)} FileUtils.chmod(0644, user.unix_authorized_keys_file_path) FileUtils.chown_R(user.user_name, "users", user.dot_ssh_directory) end def set_dot_forward_file_for(user) puts "#{Time.now}: Adding .forward file for #{user.user_name}" File.open(user.unix_forward_file_path, "w") {|f| f.syswrite(user.email + "\n")} FileUtils.chmod(0644, user.unix_forward_file_path) FileUtils.chown(user.user_name, "users", user.unix_forward_file_path) end def set_list_of_groups_for_user(user, groups) cmd = "/usr/sbin/usermod -G #{groups.join(",") rescue 'users'} #{user}" puts "#{Time.now}: Running command: #{cmd}" system cmd end def set_unix_password(username, password) cmd = "/usr/sbin/usermod -p '#{password}' #{username}"; puts "#{Time.now}: Updating encoded password for #{username} to #{password}" system cmd end def add_unix_user_account(username) cmd = "/usr/sbin/useradd -g users -s /bin/cvssh #{username}" puts "#{Time.now}: Adding user #{username}" system cmd end def gem_stats_recorder(filename) conn = PostgresPR::Connection.new("gforge", "gforge") count = 0 File.read(filename).each_line do |line| useful_data = line.split(" ").delete_if {|x| x =~ /^-$|HTTP|GET|HEAD|^\"-\"$|-[\d]{4}/}.slice(0..-1) if useful_data[0] == 'gems.rubyforge.org' useful_data = useful_data.slice!(1..-1) end next unless useful_data[3] == '302' next unless useful_data[5] =~ /^\"RubyGems/ ip, time, gemfile, client_signature = useful_data[0], useful_data[1].slice(1..-1), useful_data[2].split("/gems/")[1], useful_data[5].gsub(/\"/, "") sql = "INSERT INTO gem_downloads (ip, downloaded_at, gem_name, client_signature) VALUES ('#{ip}', '#{time}', '#{gemfile}', '#{client_signature}')" begin conn.query(sql) rescue RuntimeError => e puts "FAILED: #{sql}" puts e.backtrace end puts "Inserted record #{count}" if count % 1000 == 0 count += 1 end puts "Total inserts: #{count}" conn.close end def scrub_old_messages Dir.glob("/var/mailman/data/*.pck").each do |file| FileUtils.rm_f(file) if (Time.now - 60*60*24*14) > File.atime(file) end end def update_gitosis # TODO wow, this really wants a block puts "#{Time.now}: Starting" if File.exists?(GITOSIS_UPDATE_LOCK_FILENAME) puts "Lock file exists, skipping gitosis update" return end FileUtils.touch GITOSIS_UPDATE_LOCK_FILENAME puts "#{Time.now}: Updating each project" gc = GitosisConf.new("/home/tom/gitosis-admin/gitosis.conf") Group.active.uses_git.each do |g| public_keys = g.find_member_public_keys g.add_keys_to_keydir(public_keys) gc.add_or_update(g.unix_group_name, public_keys.collect { |k,v| k } ) end gc.write puts "#{Time.now}: Pushing" GForge.new.commit_and_push_new_keys GForge.new.commit_and_push_gitosis_conf FileUtils.rm_f GITOSIS_UPDATE_LOCK_FILENAME puts "#{Time.now}: Done" end def commit_and_push_gitosis_conf cmd = "cd /home/tom/gitosis-admin/ && #{GIT_PATH} commit -a -m 'added more groups to gitosis.conf' && chown -R tom:tom /home/tom/gitosis-admin/ && su tom -c \"#{GIT_PATH} push\"" `#{cmd}` end def commit_and_push_new_keys # TODO how to check if there are actually any new keys? # I started with doing a "git add *.pub" and then a "git diff", but diff doesn't seem to pick up on added files before they're committed. # Maybe possible to do a git diff without even doing an add? cmd = "cd #{KEYDIR} && #{GIT_PATH} add *.pub && #{GIT_PATH} commit -a -m 'key updates' && chown -R tom:tom /home/tom/gitosis-admin/ && su tom -c \"#{GIT_PATH} push\"" `#{cmd}` end end if __FILE__ == $0 action = ARGV[0] if action == "update_overall_gem_stats" GemDownload.update_overall_gem_stats elsif action == "gem_stats_recorder" puts "Starting at #{Time.now}" puts "Backing up log file" `cp /var/log/httpd/gems-access_log /tmp/` File.truncate("/var/log/httpd/gems-access_log", 0) puts "Restarting Apache at #{Time.now}" puts `/usr/local/apache2/bin/apachectl restart` puts "Restarted Apache at #{Time.now}" filename = "/tmp/gems-access_log" puts "Processing #{filename}" GForge.new.gem_stats_recorder(filename) `mv /tmp/gems-access_log /tmp/gems-access_log_#{Time.now.to_s.gsub(/ /, "")}` puts "Done at #{Time.now}" elsif action == "update_gitosis" puts "Updating gitosis" GForge.new.update_gitosis elsif action == "scrub_old_messages" GForge.new.scrub_old_messages elsif action == "cleanwikis" puts "Cleaning up old wikis: #{Time.now}" PostgresPR::Connection.new("gforge", "gforge").query("select unix_group_name from groups where groups.status = 'A' and unix_group_name != 'rpa-base' and unix_group_name not in (select g.unix_group_name from groups g, group_plugin gp, plugins p where g.group_id = gp.group_id and gp.plugin_id = p.plugin_id and p.plugin_name = 'usemodwiki')").rows.each do |row| project = row[0] if File.exists?("/var/www/gforge-projects/#{project}/wiki") puts "Cleaning up unused wiki for #{project}: #{Time.now}" puts "Deleting files" `tar -zcf /tmp/wikicleanup_#{project}.tar.gz /var/www/gforge-projects/#{project}/wiki` FileUtils.rm_rf("/var/www/gforge-projects/#{project}/wiki") puts "Removing wiki directives from vhost" skipping = false out = "" group = Group.find_by_unix_group_name(project) File.read(group.vhost_conf).each do |line| if skipping skipping = false if line =~ /Directory/ next end if line =~ %r{Directory "/var/www/gforge-projects/#{group.unix_group_name}/wiki} skipping = true next end out << line end File.open(group.vhost_conf, "w") {|f| f.write(out) } `#{APACHECTL} graceful` end end puts "Adding any new wikis: #{Time.now}" PostgresPR::Connection.new("gforge", "gforge").query("select unix_group_name from groups where groups.status = 'A' and unix_group_name != 'rpa-base' and unix_group_name in (select g.unix_group_name from groups g, group_plugin gp, plugins p where g.group_id = gp.group_id and gp.plugin_id = p.plugin_id and p.plugin_name = 'usemodwiki')").rows.each do |row| project = row[0] if !File.exists?("/var/www/gforge-projects/#{project}/wiki") puts "Need to add wiki for #{project}" out = "" group = Group.find_by_unix_group_name(project) File.read(group.vhost_conf).each do |line| out << line if line =~ %r{DocumentRoot /var/www/gforge-projects/#{group.unix_group_name}} out << File.read("/home/tom/support/trunk/support/httpd.conf.usemod.template").gsub(/projectname/, group.unix_group_name) end end File.open(group.vhost_conf, "w") {|f| f.write(out) } group.wikify `#{APACHECTL} graceful` end end elsif action == "update_gems_features_box" GemDownload.update_gems_features_box elsif action == "reset_vhost_permissions" Group.needs_vhost_permissions_reset.each do |group| group.reset_vhost_permissions group.update_attribute(:needs_vhost_permissions_reset, false) end elsif action == "delete" raise "Pass in a --name parameter to delete a project" if !ARGV.include?("--name") name = ARGV[ARGV.index("--name")+1] raise "Group #{name} doesn't exist" if !Group.exists?(:unix_group_name => name) Group.find_by_unix_group_name(name).show_steps_to_delete elsif action == "clear_old_newgems" GForge.new.clear_old_newgems elsif action == "resetsvn" raise "Pass in a --name parameter to reset a group's Subversion repo" if !ARGV.include?("--name") name = ARGV[ARGV.index("--name")+1] raise "Group name was blank" if name.nil? raise "No existing repository for #{name}" if !File.exists?("/var/svn/#{name}") Group.find_by_unix_group_name(name).show_steps_to_resetsvn elsif action == "cvs2svn" raise "Pass in a --name parameter to convert a group from CVS to SVN" if !ARGV.include?("--name") name = ARGV[ARGV.index("--name")+1] raise "Group name was blank" if name.nil? raise "No CVS repository for #{name}" if !File.exists?("/var/cvs/#{name}") Group.find_by_unix_group_name(name).show_steps_to_cvs2svn(ARGV.include?("--clear")) elsif action == "svn2git" raise "Pass in --name and parameters to convert a group from SVN to Git" if !ARGV.include?("--name") name = ARGV[ARGV.index("--name")+1] raise "Group name was blank" if name.nil? raise "No Svn repository for #{name}" if !File.exists?("#{SVNROOT}/#{name}") Group.find_by_unix_group_name(name).show_steps_to_svn2git(ARGV.include?("--clear")) elsif action == "wiki-setup-files" g = GForge.new g.wiki_setup_files elsif action == "update_unix_accounts" GForge.new.update_unix_accounts elsif action == "approve_projects_and_update_unix_accounts" g = GForge.new g.approve_pending_projects g.update_unix_accounts else puts "Unknown action: #{action}" end # Note: To delete a news item 'forum', do this: # delete from forum_group_list where group_forum_id = 28061; # # Note: convert to new svn 1.5 # sudo svnadmin upgrade /var/svn/xxx # sudo /usr/local/src/subversion-1.5.4/tools/server-side/svn-populate-node-origins-index /var/svn/xxx/ # chown -R admin:xxx /var/svn/xxx # chmod -R g+w /var/svn/xxx end