Merge branch 'master' into 8079-api-client-auth-uuid
[arvados.git] / services / api / script / migrate-gitolite-to-uuid-storage.rb
1 #!/usr/bin/env ruby
2 #
3 # Prior to April 2015, Arvados Gitolite integration stored repositories by
4 # name.  To improve user repository management, we switched to storing
5 # repositories by UUID, and aliasing them to names.  This makes it easy to
6 # have rich name hierarchies, and allow users to rename repositories.
7 #
8 # This script will migrate a name-based Gitolite configuration to a UUID-based
9 # one.  To use it:
10 #
11 # 1. Change the value of REPOS_DIR below, if needed.
12 # 2. Install this script in the same directory as `update-gitolite.rb`.
13 # 3. Ensure that no *other* users can access Gitolite: edit gitolite's
14 #    authorized_keys file so it only contains the arvados_git_user key,
15 #    and disable the update-gitolite cron job.
16 # 4. Run this script: `ruby migrate-gitolite-to-uuid-storage.rb production`.
17 # 5. Undo step 3.
18
19 require 'rubygems'
20 require 'pp'
21 require 'arvados'
22 require 'tempfile'
23 require 'yaml'
24
25 REPOS_DIR = "/var/lib/gitolite/repositories"
26
27 # Default is development
28 production = ARGV[0] == "production"
29
30 ENV["RAILS_ENV"] = "development"
31 ENV["RAILS_ENV"] = "production" if production
32
33 DEBUG = 1
34
35 # load and merge in the environment-specific application config info
36 # if present, overriding base config parameters as specified
37 path = File.dirname(__FILE__) + '/config/arvados-clients.yml'
38 if File.exists?(path) then
39   cp_config = YAML.load_file(path)[ENV['RAILS_ENV']]
40 else
41   puts "Please create a\n " + File.dirname(__FILE__) + "/config/arvados-clients.yml\n file"
42   exit 1
43 end
44
45 gitolite_url = cp_config['gitolite_url']
46 gitolite_arvados_git_user_key = cp_config['gitolite_arvados_git_user_key']
47
48 gitolite_tmpdir = File.join(File.absolute_path(File.dirname(__FILE__)),
49                             cp_config['gitolite_tmp'])
50 gitolite_admin = File.join(gitolite_tmpdir, 'gitolite-admin')
51 gitolite_keydir = File.join(gitolite_admin, 'keydir', 'arvados')
52
53 ENV['ARVADOS_API_HOST'] = cp_config['arvados_api_host']
54 ENV['ARVADOS_API_TOKEN'] = cp_config['arvados_api_token']
55 if cp_config['arvados_api_host_insecure']
56   ENV['ARVADOS_API_HOST_INSECURE'] = 'true'
57 else
58   ENV.delete('ARVADOS_API_HOST_INSECURE')
59 end
60
61 def ensure_directory(path, mode)
62   begin
63     Dir.mkdir(path, mode)
64   rescue Errno::EEXIST
65   end
66 end
67
68 def replace_file(path, contents)
69   unlink_now = true
70   dirname, basename = File.split(path)
71   new_file = Tempfile.new([basename, ".tmp"], dirname)
72   begin
73     new_file.write(contents)
74     new_file.flush
75     File.rename(new_file, path)
76     unlink_now = false
77   ensure
78     new_file.close(unlink_now)
79   end
80 end
81
82 def file_has_contents?(path, contents)
83   begin
84     IO.read(path) == contents
85   rescue Errno::ENOENT
86     false
87   end
88 end
89
90 module TrackCommitState
91   module ClassMethods
92     # Note that all classes that include TrackCommitState will have
93     # @@need_commit = true if any of them set it.  Since this flag reports
94     # a boolean state of the underlying git repository, that's OK in the
95     # current implementation.
96     @@need_commit = false
97
98     def changed?
99       @@need_commit
100     end
101
102     def ensure_in_git(path, contents)
103       unless file_has_contents?(path, contents)
104         replace_file(path, contents)
105         system("git", "add", path)
106         @@need_commit = true
107       end
108     end
109   end
110
111   def ensure_in_git(path, contents)
112     self.class.ensure_in_git(path, contents)
113   end
114
115   def self.included(base)
116     base.extend(ClassMethods)
117   end
118 end
119
120 class Repository
121   include TrackCommitState
122
123   @@aliases = {}
124
125   def initialize(arv_repo)
126     @arv_repo = arv_repo
127   end
128
129   def self.ensure_system_config(conf_root)
130     ensure_in_git(File.join(conf_root, "arvadosaliases.pl"), alias_config)
131   end
132
133   def self.rename_repos(repos_root)
134     @@aliases.each_pair do |uuid, name|
135       begin
136         File.rename(File.join(repos_root, "#{name}.git/"),
137                     File.join(repos_root, "#{uuid}.git"))
138       rescue Errno::ENOENT
139       end
140       if name == "arvados"
141         Dir.chdir(repos_root) { File.symlink("#{uuid}.git/", "arvados.git") }
142       end
143     end
144   end
145
146   def ensure_config(conf_root)
147     return if name.nil?
148     @@aliases[uuid] = name
149     name_conf_path = auto_conf_path(conf_root, name)
150     return unless File.exist?(name_conf_path)
151     conf_file = IO.read(name_conf_path)
152     conf_file.gsub!(/^repo #{Regexp.escape(name)}$/m, "repo #{uuid}")
153     ensure_in_git(auto_conf_path(conf_root, uuid), conf_file)
154     File.unlink(name_conf_path)
155     system("git", "rm", "--quiet", name_conf_path)
156   end
157
158   private
159
160   def auto_conf_path(conf_root, basename)
161     File.join(conf_root, "conf", "auto", "#{basename}.conf")
162   end
163
164   def uuid
165     @arv_repo[:uuid]
166   end
167
168   def name
169     if @arv_repo[:name].nil?
170       nil
171     else
172       @clean_name ||=
173         @arv_repo[:name].sub(/^[^A-Za-z]+/, "").gsub(/[^\w\.\/]/, "")
174     end
175   end
176
177   def self.alias_config
178     conf_s = "{\n"
179     @@aliases.sort.each do |(repo_name, repo_uuid)|
180       conf_s += "\t'#{repo_name}' \t=> '#{repo_uuid}',\n"
181     end
182     conf_s += "};\n"
183     conf_s
184   end
185 end
186
187 begin
188   # Get our local gitolite-admin repo up to snuff
189   if not File.exists?(gitolite_admin) then
190     ensure_directory(gitolite_tmpdir, 0700)
191     Dir.chdir(gitolite_tmpdir)
192     `git clone #{gitolite_url}`
193     Dir.chdir(gitolite_admin)
194   else
195     Dir.chdir(gitolite_admin)
196     `git pull`
197   end
198
199   arv = Arvados.new
200   permissions = arv.repository.get_all_permissions
201
202   permissions[:repositories].each do |repo_record|
203     repo = Repository.new(repo_record)
204     repo.ensure_config(gitolite_admin)
205   end
206   Repository.ensure_system_config(gitolite_admin)
207
208   message = "#{Time.now().to_s}: migrate to storing repositories by UUID"
209   Dir.chdir(gitolite_admin)
210   `git add --all`
211   `git commit -m '#{message}'`
212   Repository.rename_repos(REPOS_DIR)
213   `git push`
214
215 rescue => bang
216   puts "Error: " + bang.to_s
217   puts bang.backtrace.join("\n")
218   exit 1
219 end
220