root/management/bs-admin/trunk/beaversource.py

Revision 404, 18.1 kB (checked in by gallardj, 5 months ago)

beaversource.py changes for migration. refs #4227

Line 
1 # vim:set ts=4 sw=4 sts=4 expandtab:
2
3 import os, shutil, stat, sys, tempfile
4 import psycopg2, ConfigParser
5 from datetime import datetime
6 import trac.db_default as tracdefaultdata
7 import trac.db.postgres_backend as tracpg
8
9 class BeaverSourceModule(object):
10     def run_command(self, cmd):
11         #print self.__str__() + ": " + cmd
12         pipe = os.popen(cmd)
13         data = pipe.read()
14         pipe.close()
15         return data
16
17     def backup_filename(self, filename):
18         suffix = datetime.now().strftime('%Y%m%d-%H%M%S')
19         return filename + '.' + suffix
20
21
22 class Project(BeaverSourceModule):
23     basepath = '/data'
24     repository_base = basepath + os.path.sep + 'svn'
25
26     def __init__(self, name, description='', access='public', creation_date=None ):
27         self.name = name
28         self.description = description
29         self.access = access
30         self.creation_date = creation_date
31
32         self.repository = Repository(self.repository_base + os.path.sep + name)
33
34     def __str__(self):
35         return self.name
36
37     def save(self):
38         pass
39
40     def exists(self):
41         if self.repository.exists():
42             return True
43         #if self.trac.exists():
44         #    return True
45         return False
46
47 class Repository(BeaverSourceModule):
48
49     # variables and maps for hook management:
50     hook_arg_map = {
51             'post-commit' : {
52                 'hook_args' : { 'REPOS' : '$1', 'REV' : '$2' },
53                 'script_args' : '--repository "$REPOS" --revision "$REV"',
54                 },
55             'post-lock' : {
56                 'hook_args' : { 'REPOS' : '$1', 'USER' : '$2' },
57                 'script_args' : '--repository "$REPOS" --user "$USER"',
58                 },
59             'post-revprop-change' : {
60                 'hook_args' : { 'REPOS' : '$1', 'REV': '$2', 'USER' : '$3', 'PROPNAME' : '$4', 'ACTION' : '$5' },
61                 'script_args' : '--repository "$REPOS" --revision "$REV" --user "$USER" --propname "$PROPNAME" --action "$ACTION"',
62                 },
63             'post-unlock' : {
64                 'hook_args' : { 'REPOS' : '$1', 'USER' : '$2' },
65                 'script_args' : '--repository "$REPOS" --user "$USER"',
66                 },
67             'pre-commit' : {
68                 'hook_args' : { 'REPOS' : '$1', 'TXN' : '$2' },
69                 'script_args' : '--repository "$REPOS" --transaction "$TXN"',
70                 },
71             'pre-lock' : {
72                 'hook_args' : { 'REPOS' : '$1', 'PATH' : '$2', 'USER' : '$3' },
73                 'script_args' : '--repository "$REPOS" --path "$PATH" --user "$USER"',
74                 },
75             'pre-revprop-change' : {
76                 'hook_args' : { 'REPOS' : '$1', 'REV' : '$2', 'USER' : '$3', 'PROPNAME' : '$4', 'ACTION' : '$4' },
77                 'script_args' : '--repository "$REPOS" --revision "$REV" --user "$USER" --propname "$PROPNAME" --action "$ACTION"',
78                 },
79             'pre-unlock' : {
80                 'hook_args' : { 'REPOS' : '$1', 'PATH' : '$2', 'USER' : '$3' },
81                 'script_args' : '--repository "$REPOS" --path "$PATH" --user "$USER"',
82                 },
83             'start-commit' : {
84                 'hook_args' : { 'REPOS' : '$1', 'USER' : '$2' },
85                 'script_args' : '--repository "$REPOS" --user "$USER"',
86                 },
87             }
88
89     def __init__(self, path):
90         self.path = path
91
92     def exists(self):
93         return os.path.exists(self.path)
94
95     def create(self, debug=False):
96         opts = ' --fs-type fsfs' # leading space!
97         self.run_command("/usr/bin/svnadmin create " + self.path + opts)
98         self.run_command('/bin/setfacl -R -m d:u:apache:rwX,u:apache:rwX,d:g:beaversource:rwX,g:beaversource:rwX %s' % self.path)
99
100     def populate(self, datafile):
101         if not self.exists():
102             return False
103
104         self.run_command("/usr/bin/svnadmin load %s < %s" % (self.path, datafile))
105
106     def setup_hook(self, hook_name):
107         # hooks dir must also exist
108         hooks_dir = self.path + os.sep + 'hooks'
109         if not os.path.exists(hooks_dir):
110             return False
111
112         hook_script = hooks_dir + os.sep + hook_name
113
114         if not os.path.exists(hook_script):
115             #shutil.copy(hook_script + '.tmpl', hook_script)
116             script = [ '#!/bin/sh\n' ]
117             for (key,val) in self.hook_arg_map[hook_name]['hook_args'].items():
118                 script.append("%s=%s\n" % (key, val))
119
120             hook_script_file = open(hook_script, 'w')
121             hook_script_file.writelines(script)
122             hook_script_file.close()
123
124         # check permissions on the hook script:
125         if not os.access(hook_script, os.X_OK):
126             os.chmod(hook_script, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
127
128         return True
129
130     def rename(self, newpath):
131         if not self.exists():
132             return False
133
134         if os.path.exists(newpath):
135             return False
136         os.rename(self.path, newpath)
137         if os.path.exists(newpath):
138             self.path = newpath
139             return True
140         else:
141             return False
142
143     def add_hook(self, hook_name, script, extra_opts = None):
144         hooks_dir = self.path + os.sep + 'hooks'
145         hook_filename = hooks_dir + os.sep + hook_name
146
147         if not self.exists():
148             return False
149
150         self.setup_hook(hook_name)
151         # now append our new command to the hook script:
152
153         if extra_opts:
154             options = '%s %s' % (self.hook_arg_map[hook_name]['script_args'], extra_opts)
155         else:
156             options = self.hook_arg_map[hook_name]['script_args']
157
158         hook_file = open(hook_filename, 'a')
159         hook_file.writelines( '/usr/bin/python %s %s || exit 1\n' % ( script, options ) )
160         hook_file.close()
161
162 class TracDB(BeaverSourceModule):
163
164     privs = ['SELECT', 'UPDATE', 'INSERT', 'DELETE']
165     sequences = [ 'report_id_seq', 'ticket_id_seq' ]
166     views = ['attachment', 'component', 'enum', 'milestone', 'node_change',
167             'permission', 'report', 'revision', 'session', 'session_attribute',
168             'system', 'ticket', 'ticket_change', 'ticket_custom', 'version',
169             'wiki']
170     tables = [ 'master_' + view for view in views ]
171     # it's really a table, but everything must be difficult:
172     views.append('auth_cookie')
173
174     def __init__(self, host, db, admin, admin_pass, project_user, project_pass):
175         self.host = host
176         self.db = db
177         self.admin = admin
178         self.admin_pw = admin_pass
179         self.project_user = project_user
180         self.project_pass = project_pass
181         self.connection = None
182
183     def connect(self):
184         if self.connection and not self.connection.closed:
185             return self.connection
186         self.connection = psycopg2.connect("host='%s' dbname='%s' user='%s' password='%s'" \
187                 % ( self.host, self.db, self.admin, self.admin_pw ) )
188         return self.connection
189
190     def exists(self):
191         # @todo some postgres trickery here
192         cur = self.connect().cursor()
193         cur.execute("SELECT rolname FROM pg_roles WHERE rolname = '%s'" % self.project_user)
194         if cur.rowcount:
195             return True
196         else:
197             return False
198
199     def create(self):
200         if self.exists():
201             return False
202
203         # cannot create users inside a transaction,
204         # so set isolation to 0 and then restore it when done
205         conn = self.connect()
206         old_isolation_level = conn.isolation_level
207         conn.set_isolation_level(0)
208         cur = conn.cursor()
209         create_role_stmt = "CREATE ROLE %s IN ROLE bsc_users LOGIN PASSWORD '%s'" % ( self.project_user, self.project_pass)
210         cur.execute(create_role_stmt)
211
212         #grants
213         priv_str = ', '.join(self.privs)
214         for view in self.views:
215             view_grant_stmt = "GRANT %s ON %s to %s" % (priv_str, view, self.project_user)
216             cur.execute(view_grant_stmt)
217         for seq in self.sequences:
218             seq_grant_stmt = "GRANT %s ON %s to %s" % (priv_str, seq, self.project_user)
219             cur.execute(seq_grant_stmt)
220
221         conn.set_isolation_level(old_isolation_level)
222         return self.exists()
223
224     def destroy(self):
225         if not self.exists():
226             return False
227
228         conn = self.connect()
229         old_isolation_level = conn.isolation_level
230         conn.set_isolation_level(0)
231         cur = conn.cursor()
232
233         # clean up data:
234         for table in self.tables:
235             data_delete_stmt = "DELETE FROM %s WHERE dbuser = '%s'" % (table, self.project_user)
236             #print >> sys.stdder, data_delete_stmt + "\n"; sys.stderr.flush()
237             cur.execute(data_delete_stmt)
238
239         revoke_all_stmt = "REVOKE ALL ON DATABASE %s FROM %s CASCADE" % (self.db, self.project_user)
240         cur.execute(revoke_all_stmt)
241
242         for view in self.views:
243             view_revoke_stmt = "REVOKE ALL ON TABLE %s FROM %s" % (view, self.project_user)
244             cur.execute(view_revoke_stmt)
245
246         for seq in self.sequences:
247             seq_revoke_stmt = "REVOKE ALL ON TABLE %s FROM %s" % (seq, self.project_user)
248             cur.execute(seq_revoke_stmt)
249
250         drop_role_stmt = "DROP ROLE %s" % self.project_user
251         cur.execute(drop_role_stmt)
252         conn.set_isolation_level(old_isolation_level)
253         return not self.exists()
254
255     def rename(self, newname, newpass=None):
256         if not self.exists():
257             return 0
258         # how to rename:
259         # have to juggle self.project_user a bit
260         olduser = self.project_user
261         oldpass = self.project_pass
262         if newpass:
263             oldpass = self.project_pass
264         else:
265             newpass = self.project_pass
266
267         conn = self.connect()
268         old_isolation_level = conn.isolation_level
269         conn.set_isolation_level(0)
270         cur = conn.cursor()
271
272         # first create a role
273         self.project_user = newname
274         self.project_pass = newpass
275         self.create()
276
277         # then update all the tables
278         for table in self.tables:
279             data_update_stmt = "UPDATE %s SET dbuser = '%s' WHERE dbuser = '%s'" % (table, self.project_user, olduser)
280             cur.execute(data_update_stmt)
281
282         # then remove the old
283         self.project_user = olduser
284         self.project_pass = oldpass
285         self.destroy()
286
287         # and juggle back to new info:
288         self.project_user = newname
289         self.project_pass = newpass
290
291 class TracInstall(BeaverSourceModule):
292     def __init__(self, name, path, repository, visibility='A', license='Other', licensefile=''):
293         self.name = name
294         self.path = path
295         self.repository = repository
296         self.visibility=visibility
297         self.license = license
298         self.licensefile = licensefile
299
300         self.config_filename = self.path + os.sep + 'conf' + os.sep + 'trac.ini'
301
302         if self.exists():
303             self.read_config()
304
305     def exists(self):
306         return os.path.exists(self.path)
307
308     def create(self):
309         if self.exists():
310             return False
311
312         self.run_command("/usr/bin/trac-admin %s initenv '%s' %s %s %s %s"
313                 % ( self.path, self.name, 'sqlite:db/trac.db', 'svn', self.repository.path, '/usr/share/trac/templates') )
314         if self.exists():
315             self.run_command('/bin/setfacl -R -m d:u:apache:rwX,u:apache:rwX,d:g:beaversource:rwX,g:beaversource:rwX %s' % self.path)
316             self.read_config()
317             return True
318
319     def write_config(self, backup=True):
320         if not self.exists():
321             return False
322
323         if not self.config:
324             return False
325
326         if backup:
327             shutil.copy(self.config_filename, self.backup_filename(self.config_filename))
328
329         conf = open(self.config_filename, 'w')
330         self.config.write( conf )
331         conf.close()
332
333     def read_config(self):
334         if not self.exists():
335             return False
336
337
338         if not os.path.exists(self.config_filename):
339             return False
340
341         self.config = ConfigParser.ConfigParser()
342         self.config.read(self.config_filename)
343
344     # ripped from Trac's env.py file:
345     def init_db(self, database, db_user, db_pass, db_host):
346
347         # create a new connection using trac's libraries
348         # so we can 'seamlessly' populate the initial data
349         db = tracpg.PostgreSQLConnection(database, db_user, db_pass, db_host)
350         cursor = db.cnx.cursor()
351         for table, cols, vals in tracdefaultdata.get_data(db):
352             cursor.executemany("INSERT INTO %s (%s) VALUES (%s)" %
353                     (table, ','.join(cols), ','.join(['%s' for c in cols])), vals)
354             db.cnx.commit()
355
356     def populate_wiki(self, wikidir):
357         if not self.exists():
358             return False
359
360         return self.run_command('/usr/bin/trac-admin %s wiki load %s' % (self.path, wikidir) )
361
362     def add_plugin(self, pluginfile):
363         if not self.exists():
364             return False
365
366         if not os.path.exists(pluginfile):
367             return False
368
369         shutil.copy(pluginfile, self.path + os.sep + 'plugins')
370
371     def add_user(self, username, permission = 'TRAC_ADMIN'):
372         if not self.exists():
373             return False
374
375         return self.run_command('/usr/bin/trac-admin %s permission add %s %s' % (self.path, username, permission))
376
377     def rename(self, newname, options):
378         newpath = options.trac_base_path + os.sep + newname
379         if os.path.exists(newpath):
380             return False
381
382         os.rename(self.path, newpath)
383         if os.path.exists(newpath):
384             self.path = newpath
385             self.config_filename = self.path + os.sep + 'conf' + os.sep + 'trac.ini'
386             return True
387         else:
388             return False
389
390
391     def postprocess(self):
392         """Postprocess makes modifications to the newly created trac instance to put it in line with the visibility and license options selected on the request form.
393         It basically just makes calls to trac-admin, doing what an admin could easily also do on the command line.  Three things are taken care of in this method:
394         1) Permissions are adjusted to match visibility.
395         2) The text of the selected license are added to the wiki under the page name 'License'.
396         3) A custom copy of the WikiStart (main) page is created that adds the project name, description, svn info, and link to the 'License' page.
397         Note: This doesn't do any of the visibility or license stuff related to SVN, that is handled through the cron job (visibility via an authz file) and through use of the --svn_initial_data parameter which will add the license to the svn as its initial data.
398         """
399         view_permission_list = ('BROWSER_VIEW', 'CHANGESET_VIEW', 'CONFIG_VIEW', 'FILE_VIEW', 'LOG_VIEW', 'MILESTONE_VIEW', 'REPORT_SQL_VIEW', 'REPORT_VIEW', 'ROADMAP_VIEW', 'SEARCH_VIEW', 'TICKET_APPEND', 'TICKET_CREATE', 'TICKET_VIEW', 'TICKET_MODIFY', 'TIMELINE_VIEW', 'WIKI_CREATE', 'WIKI_MODIFY', 'WIKI_VIEW')
400         edit_permission_list = ('TICKET_APPEND', 'TICKET_CREATE', 'TICKET_MODIFY', 'WIKI_CREATE', 'WIKI_MODIFY')
401         #other_permissions = ('MILESTONE_ADMIN', 'MILESTONE_CREATE', 'MILESTONE_DELETE', 'MILESTONE_MODIFY', 'REPORT_ADMIN', 'REPORT_CREATE', 'REPORT_DELETE', 'REPORT_MODIFY',  'ROADMAP_ADMIN', 'TICKET_ADMIN', 'TICKET_CHGPROP', 'TRAC_ADMIN', 'WIKI_ADMIN', 'WIKI_DELETE')
402
403         ### set trac visibility permissions using trac-admin, svn permissions will be autogenerated at the next cron
404         #Publicly visible projects start with all view and edit permissions, we want to restrict that to prevent spam, removing all the view permissions
405         if(self.visibility=='A'):
406             for permission in edit_permission_list:
407                 self.run_command('/usr/bin/trac-admin %s permission remove anonymous %s' % (self.path, permission))
408                 #since authenticated inherits anonymous,we have to give it the permissions we take from anonymous
409                 self.run_command('/usr/bin/trac-admin %s permission add authenticated %s' % (self.path, permission))
410
411         #OSU/VPN visible projects require a person to login to see or edit anything
412         if(self.visibility=='O'):
413             for permission in view_permission_list:
414                 self.run_command('/usr/bin/trac-admin %s permission remove anonymous %s' % (self.path, permission))
415                 #since authenticated inherits anonymous,we have to give it the permissions we take from anonymous
416                 self.run_command('/usr/bin/trac-admin %s permission add authenticated %s' % (self.path, permission))
417
418         #Members-only remove all permissions and only the TRAC_ADMINs will be able to see or edit the project
419         if(self.visibility=='M'):
420             for permission in view_permission_list:
421                 self.run_command('/usr/bin/trac-admin %s permission remove anonymous %s' % (self.path, permission))
422
423
424         #add license to wiki using trac-admin wiki import License [file]
425         license_file = "/data/master-trac/initial-data/" + self.licensefile
426         self.run_command('/usr/bin/trac-admin %s wiki import License %s ' % (self.path, license_file + ".wiki"))
427
428         ###overwrite main wikipage
429         #produce the new wikipage
430         fd, filename = tempfile.mkstemp(suffix='.wiki')
431         f = os.fdopen(fd, 'wb')
432
433         f.write("""
434 = Welcome to """ + self.config.get('project', 'name') + """ =
435
436 """ + self.config.get('project', 'descr') + """
437
438 This project is licensed under the [wiki:License """ + self.license + """]
439
440
441 To checkout the SVN tree for this project, use the following command:
442
443 {{{
444 svn co --username <onid username> https://code.oregonstate.edu/svn/"""+ self.name +"""
445 }}}
446 SVN will them prompt you for your ONID password.  other useful SVN commands are:
447
448 {{{
449 svn up
450 }}}
451 to update your local copy
452
453 {{{
454 svn ci
455 }}}
456 to checkin new changes
457
458
459 Enjoy! [[BR]]
460 ''The """ + self.name + """ Team''
461
462 == Starting Points ==
463
464  * TracGuide --  Built-in Documentation
465  * [http://trac.edgewall.org/ The Trac project] -- Trac Open Source Project
466  * [http://trac.edgewall.org/wiki/TracFaq Trac FAQ] -- Frequently Asked Questions
467  * TracSupport --  Trac Support
468
469 For a complete list of local wiki pages, see TitleIndex.
470
471         """)
472         f.close()
473         #call trac-admin to replace the old wikipage
474         self.run_command('/usr/bin/trac-admin %s wiki remove WikiStart' % (self.path))
475         self.run_command('/usr/bin/trac-admin %s wiki import WikiStart %s ' % (self.path, filename))
476         os.remove(filename)
Note: See TracBrowser for help on using the browser.