##// END OF EJS Templates
setup_rhodecode: fix --force-no - force_ask is tri-state...
Mads Kiilerich -
r3671:658ccf97 beta
parent child Browse files
Show More
@@ -1,722 +1,721 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.db_manage
3 rhodecode.lib.db_manage
4 ~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Database creation, and setup module for RhodeCode. Used for creation
6 Database creation, and setup module for RhodeCode. Used for creation
7 of database as well as for migration operations
7 of database as well as for migration operations
8
8
9 :created_on: Apr 10, 2010
9 :created_on: Apr 10, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import sys
28 import sys
29 import uuid
29 import uuid
30 import logging
30 import logging
31 from os.path import dirname as dn, join as jn
31 from os.path import dirname as dn, join as jn
32
32
33 from rhodecode import __dbversion__, __py_version__
33 from rhodecode import __dbversion__, __py_version__
34
34
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.lib.utils import ask_ok
36 from rhodecode.lib.utils import ask_ok
37 from rhodecode.model import init_model
37 from rhodecode.model import init_model
38 from rhodecode.model.db import User, Permission, RhodeCodeUi, \
38 from rhodecode.model.db import User, Permission, RhodeCodeUi, \
39 RhodeCodeSetting, UserToPerm, DbMigrateVersion, RepoGroup, \
39 RhodeCodeSetting, UserToPerm, DbMigrateVersion, RepoGroup, \
40 UserRepoGroupToPerm
40 UserRepoGroupToPerm
41
41
42 from sqlalchemy.engine import create_engine
42 from sqlalchemy.engine import create_engine
43 from rhodecode.model.repos_group import ReposGroupModel
43 from rhodecode.model.repos_group import ReposGroupModel
44 #from rhodecode.model import meta
44 #from rhodecode.model import meta
45 from rhodecode.model.meta import Session, Base
45 from rhodecode.model.meta import Session, Base
46 from rhodecode.model.repo import RepoModel
46 from rhodecode.model.repo import RepoModel
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 def notify(msg):
52 def notify(msg):
53 """
53 """
54 Notification for migrations messages
54 Notification for migrations messages
55 """
55 """
56 ml = len(msg) + (4 * 2)
56 ml = len(msg) + (4 * 2)
57 print >> sys.stdout, ('*** %s ***\n%s' % (msg, '*' * ml)).upper()
57 print >> sys.stdout, ('*** %s ***\n%s' % (msg, '*' * ml)).upper()
58
58
59
59
60 class DbManage(object):
60 class DbManage(object):
61 def __init__(self, log_sql, dbconf, root, tests=False, cli_args={}):
61 def __init__(self, log_sql, dbconf, root, tests=False, cli_args={}):
62 self.dbname = dbconf.split('/')[-1]
62 self.dbname = dbconf.split('/')[-1]
63 self.tests = tests
63 self.tests = tests
64 self.root = root
64 self.root = root
65 self.dburi = dbconf
65 self.dburi = dbconf
66 self.log_sql = log_sql
66 self.log_sql = log_sql
67 self.db_exists = False
67 self.db_exists = False
68 self.cli_args = cli_args
68 self.cli_args = cli_args
69 self.init_db()
69 self.init_db()
70 global ask_ok
71
70
72 if self.cli_args.get('force_ask'):
71 force_ask = self.cli_args.get('force_ask')
73 ask_ok = lambda *args, **kwargs: True
72 if force_ask is not None:
74 elif not self.cli_args.get('force_ask'):
73 global ask_ok
75 ask_ok = lambda *args, **kwargs: False
74 ask_ok = lambda *args, **kwargs: force_ask
76
75
77 def init_db(self):
76 def init_db(self):
78 engine = create_engine(self.dburi, echo=self.log_sql)
77 engine = create_engine(self.dburi, echo=self.log_sql)
79 init_model(engine)
78 init_model(engine)
80 self.sa = Session()
79 self.sa = Session()
81
80
82 def create_tables(self, override=False):
81 def create_tables(self, override=False):
83 """
82 """
84 Create a auth database
83 Create a auth database
85 """
84 """
86
85
87 log.info("Any existing database is going to be destroyed")
86 log.info("Any existing database is going to be destroyed")
88 if self.tests:
87 if self.tests:
89 destroy = True
88 destroy = True
90 else:
89 else:
91 destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
90 destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
92 if not destroy:
91 if not destroy:
93 sys.exit('Nothing done')
92 sys.exit('Nothing done')
94 if destroy:
93 if destroy:
95 Base.metadata.drop_all()
94 Base.metadata.drop_all()
96
95
97 checkfirst = not override
96 checkfirst = not override
98 Base.metadata.create_all(checkfirst=checkfirst)
97 Base.metadata.create_all(checkfirst=checkfirst)
99 log.info('Created tables for %s' % self.dbname)
98 log.info('Created tables for %s' % self.dbname)
100
99
101 def set_db_version(self):
100 def set_db_version(self):
102 ver = DbMigrateVersion()
101 ver = DbMigrateVersion()
103 ver.version = __dbversion__
102 ver.version = __dbversion__
104 ver.repository_id = 'rhodecode_db_migrations'
103 ver.repository_id = 'rhodecode_db_migrations'
105 ver.repository_path = 'versions'
104 ver.repository_path = 'versions'
106 self.sa.add(ver)
105 self.sa.add(ver)
107 log.info('db version set to: %s' % __dbversion__)
106 log.info('db version set to: %s' % __dbversion__)
108
107
109 def upgrade(self):
108 def upgrade(self):
110 """
109 """
111 Upgrades given database schema to given revision following
110 Upgrades given database schema to given revision following
112 all needed steps, to perform the upgrade
111 all needed steps, to perform the upgrade
113
112
114 """
113 """
115
114
116 from rhodecode.lib.dbmigrate.migrate.versioning import api
115 from rhodecode.lib.dbmigrate.migrate.versioning import api
117 from rhodecode.lib.dbmigrate.migrate.exceptions import \
116 from rhodecode.lib.dbmigrate.migrate.exceptions import \
118 DatabaseNotControlledError
117 DatabaseNotControlledError
119
118
120 if 'sqlite' in self.dburi:
119 if 'sqlite' in self.dburi:
121 print (
120 print (
122 '********************** WARNING **********************\n'
121 '********************** WARNING **********************\n'
123 'Make sure your version of sqlite is at least 3.7.X. \n'
122 'Make sure your version of sqlite is at least 3.7.X. \n'
124 'Earlier versions are known to fail on some migrations\n'
123 'Earlier versions are known to fail on some migrations\n'
125 '*****************************************************\n'
124 '*****************************************************\n'
126 )
125 )
127 upgrade = ask_ok('You are about to perform database upgrade, make '
126 upgrade = ask_ok('You are about to perform database upgrade, make '
128 'sure You backed up your database before. '
127 'sure You backed up your database before. '
129 'Continue ? [y/n]')
128 'Continue ? [y/n]')
130 if not upgrade:
129 if not upgrade:
131 sys.exit('Nothing done')
130 sys.exit('Nothing done')
132
131
133 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
132 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
134 'rhodecode/lib/dbmigrate')
133 'rhodecode/lib/dbmigrate')
135 db_uri = self.dburi
134 db_uri = self.dburi
136
135
137 try:
136 try:
138 curr_version = api.db_version(db_uri, repository_path)
137 curr_version = api.db_version(db_uri, repository_path)
139 msg = ('Found current database under version'
138 msg = ('Found current database under version'
140 ' control with version %s' % curr_version)
139 ' control with version %s' % curr_version)
141
140
142 except (RuntimeError, DatabaseNotControlledError):
141 except (RuntimeError, DatabaseNotControlledError):
143 curr_version = 1
142 curr_version = 1
144 msg = ('Current database is not under version control. Setting'
143 msg = ('Current database is not under version control. Setting'
145 ' as version %s' % curr_version)
144 ' as version %s' % curr_version)
146 api.version_control(db_uri, repository_path, curr_version)
145 api.version_control(db_uri, repository_path, curr_version)
147
146
148 notify(msg)
147 notify(msg)
149
148
150 if curr_version == __dbversion__:
149 if curr_version == __dbversion__:
151 sys.exit('This database is already at the newest version')
150 sys.exit('This database is already at the newest version')
152
151
153 #======================================================================
152 #======================================================================
154 # UPGRADE STEPS
153 # UPGRADE STEPS
155 #======================================================================
154 #======================================================================
156
155
157 class UpgradeSteps(object):
156 class UpgradeSteps(object):
158 """
157 """
159 Those steps follow schema versions so for example schema
158 Those steps follow schema versions so for example schema
160 for example schema with seq 002 == step_2 and so on.
159 for example schema with seq 002 == step_2 and so on.
161 """
160 """
162
161
163 def __init__(self, klass):
162 def __init__(self, klass):
164 self.klass = klass
163 self.klass = klass
165
164
166 def step_0(self):
165 def step_0(self):
167 # step 0 is the schema upgrade, and than follow proper upgrades
166 # step 0 is the schema upgrade, and than follow proper upgrades
168 notify('attempting to do database upgrade from '
167 notify('attempting to do database upgrade from '
169 'version %s to version %s' %(curr_version, __dbversion__))
168 'version %s to version %s' %(curr_version, __dbversion__))
170 api.upgrade(db_uri, repository_path, __dbversion__)
169 api.upgrade(db_uri, repository_path, __dbversion__)
171 notify('Schema upgrade completed')
170 notify('Schema upgrade completed')
172
171
173 def step_1(self):
172 def step_1(self):
174 pass
173 pass
175
174
176 def step_2(self):
175 def step_2(self):
177 notify('Patching repo paths for newer version of RhodeCode')
176 notify('Patching repo paths for newer version of RhodeCode')
178 self.klass.fix_repo_paths()
177 self.klass.fix_repo_paths()
179
178
180 notify('Patching default user of RhodeCode')
179 notify('Patching default user of RhodeCode')
181 self.klass.fix_default_user()
180 self.klass.fix_default_user()
182
181
183 log.info('Changing ui settings')
182 log.info('Changing ui settings')
184 self.klass.create_ui_settings()
183 self.klass.create_ui_settings()
185
184
186 def step_3(self):
185 def step_3(self):
187 notify('Adding additional settings into RhodeCode db')
186 notify('Adding additional settings into RhodeCode db')
188 self.klass.fix_settings()
187 self.klass.fix_settings()
189 notify('Adding ldap defaults')
188 notify('Adding ldap defaults')
190 self.klass.create_ldap_options(skip_existing=True)
189 self.klass.create_ldap_options(skip_existing=True)
191
190
192 def step_4(self):
191 def step_4(self):
193 notify('create permissions and fix groups')
192 notify('create permissions and fix groups')
194 self.klass.create_permissions()
193 self.klass.create_permissions()
195 self.klass.fixup_groups()
194 self.klass.fixup_groups()
196
195
197 def step_5(self):
196 def step_5(self):
198 pass
197 pass
199
198
200 def step_6(self):
199 def step_6(self):
201
200
202 notify('re-checking permissions')
201 notify('re-checking permissions')
203 self.klass.create_permissions()
202 self.klass.create_permissions()
204
203
205 notify('installing new UI options')
204 notify('installing new UI options')
206 sett4 = RhodeCodeSetting('show_public_icon', True)
205 sett4 = RhodeCodeSetting('show_public_icon', True)
207 Session().add(sett4)
206 Session().add(sett4)
208 sett5 = RhodeCodeSetting('show_private_icon', True)
207 sett5 = RhodeCodeSetting('show_private_icon', True)
209 Session().add(sett5)
208 Session().add(sett5)
210 sett6 = RhodeCodeSetting('stylify_metatags', False)
209 sett6 = RhodeCodeSetting('stylify_metatags', False)
211 Session().add(sett6)
210 Session().add(sett6)
212
211
213 notify('fixing old PULL hook')
212 notify('fixing old PULL hook')
214 _pull = RhodeCodeUi.get_by_key('preoutgoing.pull_logger')
213 _pull = RhodeCodeUi.get_by_key('preoutgoing.pull_logger')
215 if _pull:
214 if _pull:
216 _pull.ui_key = RhodeCodeUi.HOOK_PULL
215 _pull.ui_key = RhodeCodeUi.HOOK_PULL
217 Session().add(_pull)
216 Session().add(_pull)
218
217
219 notify('fixing old PUSH hook')
218 notify('fixing old PUSH hook')
220 _push = RhodeCodeUi.get_by_key('pretxnchangegroup.push_logger')
219 _push = RhodeCodeUi.get_by_key('pretxnchangegroup.push_logger')
221 if _push:
220 if _push:
222 _push.ui_key = RhodeCodeUi.HOOK_PUSH
221 _push.ui_key = RhodeCodeUi.HOOK_PUSH
223 Session().add(_push)
222 Session().add(_push)
224
223
225 notify('installing new pre-push hook')
224 notify('installing new pre-push hook')
226 hooks4 = RhodeCodeUi()
225 hooks4 = RhodeCodeUi()
227 hooks4.ui_section = 'hooks'
226 hooks4.ui_section = 'hooks'
228 hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
227 hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
229 hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
228 hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
230 Session().add(hooks4)
229 Session().add(hooks4)
231
230
232 notify('installing new pre-pull hook')
231 notify('installing new pre-pull hook')
233 hooks6 = RhodeCodeUi()
232 hooks6 = RhodeCodeUi()
234 hooks6.ui_section = 'hooks'
233 hooks6.ui_section = 'hooks'
235 hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
234 hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
236 hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
235 hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
237 Session().add(hooks6)
236 Session().add(hooks6)
238
237
239 notify('installing hgsubversion option')
238 notify('installing hgsubversion option')
240 # enable hgsubversion disabled by default
239 # enable hgsubversion disabled by default
241 hgsubversion = RhodeCodeUi()
240 hgsubversion = RhodeCodeUi()
242 hgsubversion.ui_section = 'extensions'
241 hgsubversion.ui_section = 'extensions'
243 hgsubversion.ui_key = 'hgsubversion'
242 hgsubversion.ui_key = 'hgsubversion'
244 hgsubversion.ui_value = ''
243 hgsubversion.ui_value = ''
245 hgsubversion.ui_active = False
244 hgsubversion.ui_active = False
246 Session().add(hgsubversion)
245 Session().add(hgsubversion)
247
246
248 notify('installing hg git option')
247 notify('installing hg git option')
249 # enable hggit disabled by default
248 # enable hggit disabled by default
250 hggit = RhodeCodeUi()
249 hggit = RhodeCodeUi()
251 hggit.ui_section = 'extensions'
250 hggit.ui_section = 'extensions'
252 hggit.ui_key = 'hggit'
251 hggit.ui_key = 'hggit'
253 hggit.ui_value = ''
252 hggit.ui_value = ''
254 hggit.ui_active = False
253 hggit.ui_active = False
255 Session().add(hggit)
254 Session().add(hggit)
256
255
257 notify('re-check default permissions')
256 notify('re-check default permissions')
258 default_user = User.get_by_username(User.DEFAULT_USER)
257 default_user = User.get_by_username(User.DEFAULT_USER)
259 perm = Permission.get_by_key('hg.fork.repository')
258 perm = Permission.get_by_key('hg.fork.repository')
260 reg_perm = UserToPerm()
259 reg_perm = UserToPerm()
261 reg_perm.user = default_user
260 reg_perm.user = default_user
262 reg_perm.permission = perm
261 reg_perm.permission = perm
263 Session().add(reg_perm)
262 Session().add(reg_perm)
264
263
265 def step_7(self):
264 def step_7(self):
266 perm_fixes = self.klass.reset_permissions(User.DEFAULT_USER)
265 perm_fixes = self.klass.reset_permissions(User.DEFAULT_USER)
267 Session().commit()
266 Session().commit()
268 if perm_fixes:
267 if perm_fixes:
269 notify('There was an inconsistent state of permissions '
268 notify('There was an inconsistent state of permissions '
270 'detected for default user. Permissions are now '
269 'detected for default user. Permissions are now '
271 'reset to the default value for default user. '
270 'reset to the default value for default user. '
272 'Please validate and check default permissions '
271 'Please validate and check default permissions '
273 'in admin panel')
272 'in admin panel')
274
273
275 def step_8(self):
274 def step_8(self):
276 self.klass.populate_default_permissions()
275 self.klass.populate_default_permissions()
277 self.klass.create_default_options(skip_existing=True)
276 self.klass.create_default_options(skip_existing=True)
278 Session().commit()
277 Session().commit()
279
278
280 def step_9(self):
279 def step_9(self):
281 perm_fixes = self.klass.reset_permissions(User.DEFAULT_USER)
280 perm_fixes = self.klass.reset_permissions(User.DEFAULT_USER)
282 Session().commit()
281 Session().commit()
283 if perm_fixes:
282 if perm_fixes:
284 notify('There was an inconsistent state of permissions '
283 notify('There was an inconsistent state of permissions '
285 'detected for default user. Permissions are now '
284 'detected for default user. Permissions are now '
286 'reset to the default value for default user. '
285 'reset to the default value for default user. '
287 'Please validate and check default permissions '
286 'Please validate and check default permissions '
288 'in admin panel')
287 'in admin panel')
289
288
290 def step_10(self):
289 def step_10(self):
291 pass
290 pass
292
291
293 def step_11(self):
292 def step_11(self):
294 self.klass.update_repo_info()
293 self.klass.update_repo_info()
295
294
296 upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
295 upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
297
296
298 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
297 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
299 _step = None
298 _step = None
300 for step in upgrade_steps:
299 for step in upgrade_steps:
301 notify('performing upgrade step %s' % step)
300 notify('performing upgrade step %s' % step)
302 getattr(UpgradeSteps(self), 'step_%s' % step)()
301 getattr(UpgradeSteps(self), 'step_%s' % step)()
303 self.sa.commit()
302 self.sa.commit()
304 _step = step
303 _step = step
305
304
306 notify('upgrade to version %s successful' % _step)
305 notify('upgrade to version %s successful' % _step)
307
306
308 def fix_repo_paths(self):
307 def fix_repo_paths(self):
309 """
308 """
310 Fixes a old rhodecode version path into new one without a '*'
309 Fixes a old rhodecode version path into new one without a '*'
311 """
310 """
312
311
313 paths = self.sa.query(RhodeCodeUi)\
312 paths = self.sa.query(RhodeCodeUi)\
314 .filter(RhodeCodeUi.ui_key == '/')\
313 .filter(RhodeCodeUi.ui_key == '/')\
315 .scalar()
314 .scalar()
316
315
317 paths.ui_value = paths.ui_value.replace('*', '')
316 paths.ui_value = paths.ui_value.replace('*', '')
318
317
319 try:
318 try:
320 self.sa.add(paths)
319 self.sa.add(paths)
321 self.sa.commit()
320 self.sa.commit()
322 except Exception:
321 except Exception:
323 self.sa.rollback()
322 self.sa.rollback()
324 raise
323 raise
325
324
326 def fix_default_user(self):
325 def fix_default_user(self):
327 """
326 """
328 Fixes a old default user with some 'nicer' default values,
327 Fixes a old default user with some 'nicer' default values,
329 used mostly for anonymous access
328 used mostly for anonymous access
330 """
329 """
331 def_user = self.sa.query(User)\
330 def_user = self.sa.query(User)\
332 .filter(User.username == 'default')\
331 .filter(User.username == 'default')\
333 .one()
332 .one()
334
333
335 def_user.name = 'Anonymous'
334 def_user.name = 'Anonymous'
336 def_user.lastname = 'User'
335 def_user.lastname = 'User'
337 def_user.email = 'anonymous@rhodecode.org'
336 def_user.email = 'anonymous@rhodecode.org'
338
337
339 try:
338 try:
340 self.sa.add(def_user)
339 self.sa.add(def_user)
341 self.sa.commit()
340 self.sa.commit()
342 except Exception:
341 except Exception:
343 self.sa.rollback()
342 self.sa.rollback()
344 raise
343 raise
345
344
346 def fix_settings(self):
345 def fix_settings(self):
347 """
346 """
348 Fixes rhodecode settings adds ga_code key for google analytics
347 Fixes rhodecode settings adds ga_code key for google analytics
349 """
348 """
350
349
351 hgsettings3 = RhodeCodeSetting('ga_code', '')
350 hgsettings3 = RhodeCodeSetting('ga_code', '')
352
351
353 try:
352 try:
354 self.sa.add(hgsettings3)
353 self.sa.add(hgsettings3)
355 self.sa.commit()
354 self.sa.commit()
356 except Exception:
355 except Exception:
357 self.sa.rollback()
356 self.sa.rollback()
358 raise
357 raise
359
358
360 def admin_prompt(self, second=False):
359 def admin_prompt(self, second=False):
361 if not self.tests:
360 if not self.tests:
362 import getpass
361 import getpass
363
362
364 # defaults
363 # defaults
365 defaults = self.cli_args
364 defaults = self.cli_args
366 username = defaults.get('username')
365 username = defaults.get('username')
367 password = defaults.get('password')
366 password = defaults.get('password')
368 email = defaults.get('email')
367 email = defaults.get('email')
369
368
370 def get_password():
369 def get_password():
371 password = getpass.getpass('Specify admin password '
370 password = getpass.getpass('Specify admin password '
372 '(min 6 chars):')
371 '(min 6 chars):')
373 confirm = getpass.getpass('Confirm password:')
372 confirm = getpass.getpass('Confirm password:')
374
373
375 if password != confirm:
374 if password != confirm:
376 log.error('passwords mismatch')
375 log.error('passwords mismatch')
377 return False
376 return False
378 if len(password) < 6:
377 if len(password) < 6:
379 log.error('password is to short use at least 6 characters')
378 log.error('password is to short use at least 6 characters')
380 return False
379 return False
381
380
382 return password
381 return password
383 if username is None:
382 if username is None:
384 username = raw_input('Specify admin username:')
383 username = raw_input('Specify admin username:')
385 if password is None:
384 if password is None:
386 password = get_password()
385 password = get_password()
387 if not password:
386 if not password:
388 #second try
387 #second try
389 password = get_password()
388 password = get_password()
390 if not password:
389 if not password:
391 sys.exit()
390 sys.exit()
392 if email is None:
391 if email is None:
393 email = raw_input('Specify admin email:')
392 email = raw_input('Specify admin email:')
394 self.create_user(username, password, email, True)
393 self.create_user(username, password, email, True)
395 else:
394 else:
396 log.info('creating admin and regular test users')
395 log.info('creating admin and regular test users')
397 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
396 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
398 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
397 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
399 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
398 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
400 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
399 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
401 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
400 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
402
401
403 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
402 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
404 TEST_USER_ADMIN_EMAIL, True)
403 TEST_USER_ADMIN_EMAIL, True)
405
404
406 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
405 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
407 TEST_USER_REGULAR_EMAIL, False)
406 TEST_USER_REGULAR_EMAIL, False)
408
407
409 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
408 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
410 TEST_USER_REGULAR2_EMAIL, False)
409 TEST_USER_REGULAR2_EMAIL, False)
411
410
412 def create_ui_settings(self):
411 def create_ui_settings(self):
413 """
412 """
414 Creates ui settings, fills out hooks
413 Creates ui settings, fills out hooks
415 and disables dotencode
414 and disables dotencode
416 """
415 """
417
416
418 #HOOKS
417 #HOOKS
419 hooks1_key = RhodeCodeUi.HOOK_UPDATE
418 hooks1_key = RhodeCodeUi.HOOK_UPDATE
420 hooks1_ = self.sa.query(RhodeCodeUi)\
419 hooks1_ = self.sa.query(RhodeCodeUi)\
421 .filter(RhodeCodeUi.ui_key == hooks1_key).scalar()
420 .filter(RhodeCodeUi.ui_key == hooks1_key).scalar()
422
421
423 hooks1 = RhodeCodeUi() if hooks1_ is None else hooks1_
422 hooks1 = RhodeCodeUi() if hooks1_ is None else hooks1_
424 hooks1.ui_section = 'hooks'
423 hooks1.ui_section = 'hooks'
425 hooks1.ui_key = hooks1_key
424 hooks1.ui_key = hooks1_key
426 hooks1.ui_value = 'hg update >&2'
425 hooks1.ui_value = 'hg update >&2'
427 hooks1.ui_active = False
426 hooks1.ui_active = False
428 self.sa.add(hooks1)
427 self.sa.add(hooks1)
429
428
430 hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE
429 hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE
431 hooks2_ = self.sa.query(RhodeCodeUi)\
430 hooks2_ = self.sa.query(RhodeCodeUi)\
432 .filter(RhodeCodeUi.ui_key == hooks2_key).scalar()
431 .filter(RhodeCodeUi.ui_key == hooks2_key).scalar()
433 hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_
432 hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_
434 hooks2.ui_section = 'hooks'
433 hooks2.ui_section = 'hooks'
435 hooks2.ui_key = hooks2_key
434 hooks2.ui_key = hooks2_key
436 hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
435 hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
437 self.sa.add(hooks2)
436 self.sa.add(hooks2)
438
437
439 hooks3 = RhodeCodeUi()
438 hooks3 = RhodeCodeUi()
440 hooks3.ui_section = 'hooks'
439 hooks3.ui_section = 'hooks'
441 hooks3.ui_key = RhodeCodeUi.HOOK_PUSH
440 hooks3.ui_key = RhodeCodeUi.HOOK_PUSH
442 hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action'
441 hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action'
443 self.sa.add(hooks3)
442 self.sa.add(hooks3)
444
443
445 hooks4 = RhodeCodeUi()
444 hooks4 = RhodeCodeUi()
446 hooks4.ui_section = 'hooks'
445 hooks4.ui_section = 'hooks'
447 hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
446 hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
448 hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
447 hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
449 self.sa.add(hooks4)
448 self.sa.add(hooks4)
450
449
451 hooks5 = RhodeCodeUi()
450 hooks5 = RhodeCodeUi()
452 hooks5.ui_section = 'hooks'
451 hooks5.ui_section = 'hooks'
453 hooks5.ui_key = RhodeCodeUi.HOOK_PULL
452 hooks5.ui_key = RhodeCodeUi.HOOK_PULL
454 hooks5.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
453 hooks5.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
455 self.sa.add(hooks5)
454 self.sa.add(hooks5)
456
455
457 hooks6 = RhodeCodeUi()
456 hooks6 = RhodeCodeUi()
458 hooks6.ui_section = 'hooks'
457 hooks6.ui_section = 'hooks'
459 hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
458 hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
460 hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
459 hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
461 self.sa.add(hooks6)
460 self.sa.add(hooks6)
462
461
463 # enable largefiles
462 # enable largefiles
464 largefiles = RhodeCodeUi()
463 largefiles = RhodeCodeUi()
465 largefiles.ui_section = 'extensions'
464 largefiles.ui_section = 'extensions'
466 largefiles.ui_key = 'largefiles'
465 largefiles.ui_key = 'largefiles'
467 largefiles.ui_value = ''
466 largefiles.ui_value = ''
468 self.sa.add(largefiles)
467 self.sa.add(largefiles)
469
468
470 # enable hgsubversion disabled by default
469 # enable hgsubversion disabled by default
471 hgsubversion = RhodeCodeUi()
470 hgsubversion = RhodeCodeUi()
472 hgsubversion.ui_section = 'extensions'
471 hgsubversion.ui_section = 'extensions'
473 hgsubversion.ui_key = 'hgsubversion'
472 hgsubversion.ui_key = 'hgsubversion'
474 hgsubversion.ui_value = ''
473 hgsubversion.ui_value = ''
475 hgsubversion.ui_active = False
474 hgsubversion.ui_active = False
476 self.sa.add(hgsubversion)
475 self.sa.add(hgsubversion)
477
476
478 # enable hggit disabled by default
477 # enable hggit disabled by default
479 hggit = RhodeCodeUi()
478 hggit = RhodeCodeUi()
480 hggit.ui_section = 'extensions'
479 hggit.ui_section = 'extensions'
481 hggit.ui_key = 'hggit'
480 hggit.ui_key = 'hggit'
482 hggit.ui_value = ''
481 hggit.ui_value = ''
483 hggit.ui_active = False
482 hggit.ui_active = False
484 self.sa.add(hggit)
483 self.sa.add(hggit)
485
484
486 def create_ldap_options(self, skip_existing=False):
485 def create_ldap_options(self, skip_existing=False):
487 """Creates ldap settings"""
486 """Creates ldap settings"""
488
487
489 for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
488 for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
490 ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
489 ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
491 ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
490 ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
492 ('ldap_dn_pass', ''), ('ldap_base_dn', ''),
491 ('ldap_dn_pass', ''), ('ldap_base_dn', ''),
493 ('ldap_filter', ''), ('ldap_search_scope', ''),
492 ('ldap_filter', ''), ('ldap_search_scope', ''),
494 ('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
493 ('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
495 ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
494 ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
496
495
497 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
496 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
498 log.debug('Skipping option %s' % k)
497 log.debug('Skipping option %s' % k)
499 continue
498 continue
500 setting = RhodeCodeSetting(k, v)
499 setting = RhodeCodeSetting(k, v)
501 self.sa.add(setting)
500 self.sa.add(setting)
502
501
503 def create_default_options(self, skip_existing=False):
502 def create_default_options(self, skip_existing=False):
504 """Creates default settings"""
503 """Creates default settings"""
505
504
506 for k, v in [
505 for k, v in [
507 ('default_repo_enable_locking', False),
506 ('default_repo_enable_locking', False),
508 ('default_repo_enable_downloads', False),
507 ('default_repo_enable_downloads', False),
509 ('default_repo_enable_statistics', False),
508 ('default_repo_enable_statistics', False),
510 ('default_repo_private', False),
509 ('default_repo_private', False),
511 ('default_repo_type', 'hg')]:
510 ('default_repo_type', 'hg')]:
512
511
513 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
512 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
514 log.debug('Skipping option %s' % k)
513 log.debug('Skipping option %s' % k)
515 continue
514 continue
516 setting = RhodeCodeSetting(k, v)
515 setting = RhodeCodeSetting(k, v)
517 self.sa.add(setting)
516 self.sa.add(setting)
518
517
519 def fixup_groups(self):
518 def fixup_groups(self):
520 def_usr = User.get_by_username('default')
519 def_usr = User.get_by_username('default')
521 for g in RepoGroup.query().all():
520 for g in RepoGroup.query().all():
522 g.group_name = g.get_new_name(g.name)
521 g.group_name = g.get_new_name(g.name)
523 self.sa.add(g)
522 self.sa.add(g)
524 # get default perm
523 # get default perm
525 default = UserRepoGroupToPerm.query()\
524 default = UserRepoGroupToPerm.query()\
526 .filter(UserRepoGroupToPerm.group == g)\
525 .filter(UserRepoGroupToPerm.group == g)\
527 .filter(UserRepoGroupToPerm.user == def_usr)\
526 .filter(UserRepoGroupToPerm.user == def_usr)\
528 .scalar()
527 .scalar()
529
528
530 if default is None:
529 if default is None:
531 log.debug('missing default permission for group %s adding' % g)
530 log.debug('missing default permission for group %s adding' % g)
532 ReposGroupModel()._create_default_perms(g)
531 ReposGroupModel()._create_default_perms(g)
533
532
534 def reset_permissions(self, username):
533 def reset_permissions(self, username):
535 """
534 """
536 Resets permissions to default state, usefull when old systems had
535 Resets permissions to default state, usefull when old systems had
537 bad permissions, we must clean them up
536 bad permissions, we must clean them up
538
537
539 :param username:
538 :param username:
540 :type username:
539 :type username:
541 """
540 """
542 default_user = User.get_by_username(username)
541 default_user = User.get_by_username(username)
543 if not default_user:
542 if not default_user:
544 return
543 return
545
544
546 u2p = UserToPerm.query()\
545 u2p = UserToPerm.query()\
547 .filter(UserToPerm.user == default_user).all()
546 .filter(UserToPerm.user == default_user).all()
548 fixed = False
547 fixed = False
549 if len(u2p) != len(User.DEFAULT_PERMISSIONS):
548 if len(u2p) != len(User.DEFAULT_PERMISSIONS):
550 for p in u2p:
549 for p in u2p:
551 Session().delete(p)
550 Session().delete(p)
552 fixed = True
551 fixed = True
553 self.populate_default_permissions()
552 self.populate_default_permissions()
554 return fixed
553 return fixed
555
554
556 def update_repo_info(self):
555 def update_repo_info(self):
557 RepoModel.update_repoinfo()
556 RepoModel.update_repoinfo()
558
557
559 def config_prompt(self, test_repo_path='', retries=3):
558 def config_prompt(self, test_repo_path='', retries=3):
560 defaults = self.cli_args
559 defaults = self.cli_args
561 _path = defaults.get('repos_location')
560 _path = defaults.get('repos_location')
562 if retries == 3:
561 if retries == 3:
563 log.info('Setting up repositories config')
562 log.info('Setting up repositories config')
564
563
565 if _path is not None:
564 if _path is not None:
566 path = _path
565 path = _path
567 elif not self.tests and not test_repo_path:
566 elif not self.tests and not test_repo_path:
568 path = raw_input(
567 path = raw_input(
569 'Enter a valid absolute path to store repositories. '
568 'Enter a valid absolute path to store repositories. '
570 'All repositories in that path will be added automatically:'
569 'All repositories in that path will be added automatically:'
571 )
570 )
572 else:
571 else:
573 path = test_repo_path
572 path = test_repo_path
574 path_ok = True
573 path_ok = True
575
574
576 # check proper dir
575 # check proper dir
577 if not os.path.isdir(path):
576 if not os.path.isdir(path):
578 path_ok = False
577 path_ok = False
579 log.error('Given path %s is not a valid directory' % path)
578 log.error('Given path %s is not a valid directory' % path)
580
579
581 elif not os.path.isabs(path):
580 elif not os.path.isabs(path):
582 path_ok = False
581 path_ok = False
583 log.error('Given path %s is not an absolute path' % path)
582 log.error('Given path %s is not an absolute path' % path)
584
583
585 # check write access
584 # check write access
586 elif not os.access(path, os.W_OK) and path_ok:
585 elif not os.access(path, os.W_OK) and path_ok:
587 path_ok = False
586 path_ok = False
588 log.error('No write permission to given path %s' % path)
587 log.error('No write permission to given path %s' % path)
589
588
590 if retries == 0:
589 if retries == 0:
591 sys.exit('max retries reached')
590 sys.exit('max retries reached')
592 if not path_ok:
591 if not path_ok:
593 retries -= 1
592 retries -= 1
594 return self.config_prompt(test_repo_path, retries)
593 return self.config_prompt(test_repo_path, retries)
595
594
596 real_path = os.path.normpath(os.path.realpath(path))
595 real_path = os.path.normpath(os.path.realpath(path))
597
596
598 if real_path != os.path.normpath(path):
597 if real_path != os.path.normpath(path):
599 if not ask_ok(('Path looks like a symlink, Rhodecode will store '
598 if not ask_ok(('Path looks like a symlink, Rhodecode will store '
600 'given path as %s ? [y/n]') % (real_path)):
599 'given path as %s ? [y/n]') % (real_path)):
601 log.error('Canceled by user')
600 log.error('Canceled by user')
602 sys.exit(-1)
601 sys.exit(-1)
603
602
604 return real_path
603 return real_path
605
604
606 def create_settings(self, path):
605 def create_settings(self, path):
607
606
608 self.create_ui_settings()
607 self.create_ui_settings()
609
608
610 #HG UI OPTIONS
609 #HG UI OPTIONS
611 web1 = RhodeCodeUi()
610 web1 = RhodeCodeUi()
612 web1.ui_section = 'web'
611 web1.ui_section = 'web'
613 web1.ui_key = 'push_ssl'
612 web1.ui_key = 'push_ssl'
614 web1.ui_value = 'false'
613 web1.ui_value = 'false'
615
614
616 web2 = RhodeCodeUi()
615 web2 = RhodeCodeUi()
617 web2.ui_section = 'web'
616 web2.ui_section = 'web'
618 web2.ui_key = 'allow_archive'
617 web2.ui_key = 'allow_archive'
619 web2.ui_value = 'gz zip bz2'
618 web2.ui_value = 'gz zip bz2'
620
619
621 web3 = RhodeCodeUi()
620 web3 = RhodeCodeUi()
622 web3.ui_section = 'web'
621 web3.ui_section = 'web'
623 web3.ui_key = 'allow_push'
622 web3.ui_key = 'allow_push'
624 web3.ui_value = '*'
623 web3.ui_value = '*'
625
624
626 web4 = RhodeCodeUi()
625 web4 = RhodeCodeUi()
627 web4.ui_section = 'web'
626 web4.ui_section = 'web'
628 web4.ui_key = 'baseurl'
627 web4.ui_key = 'baseurl'
629 web4.ui_value = '/'
628 web4.ui_value = '/'
630
629
631 paths = RhodeCodeUi()
630 paths = RhodeCodeUi()
632 paths.ui_section = 'paths'
631 paths.ui_section = 'paths'
633 paths.ui_key = '/'
632 paths.ui_key = '/'
634 paths.ui_value = path
633 paths.ui_value = path
635
634
636 phases = RhodeCodeUi()
635 phases = RhodeCodeUi()
637 phases.ui_section = 'phases'
636 phases.ui_section = 'phases'
638 phases.ui_key = 'publish'
637 phases.ui_key = 'publish'
639 phases.ui_value = False
638 phases.ui_value = False
640
639
641 sett1 = RhodeCodeSetting('realm', 'RhodeCode authentication')
640 sett1 = RhodeCodeSetting('realm', 'RhodeCode authentication')
642 sett2 = RhodeCodeSetting('title', 'RhodeCode')
641 sett2 = RhodeCodeSetting('title', 'RhodeCode')
643 sett3 = RhodeCodeSetting('ga_code', '')
642 sett3 = RhodeCodeSetting('ga_code', '')
644
643
645 sett4 = RhodeCodeSetting('show_public_icon', True)
644 sett4 = RhodeCodeSetting('show_public_icon', True)
646 sett5 = RhodeCodeSetting('show_private_icon', True)
645 sett5 = RhodeCodeSetting('show_private_icon', True)
647 sett6 = RhodeCodeSetting('stylify_metatags', False)
646 sett6 = RhodeCodeSetting('stylify_metatags', False)
648
647
649 self.sa.add(web1)
648 self.sa.add(web1)
650 self.sa.add(web2)
649 self.sa.add(web2)
651 self.sa.add(web3)
650 self.sa.add(web3)
652 self.sa.add(web4)
651 self.sa.add(web4)
653 self.sa.add(paths)
652 self.sa.add(paths)
654 self.sa.add(sett1)
653 self.sa.add(sett1)
655 self.sa.add(sett2)
654 self.sa.add(sett2)
656 self.sa.add(sett3)
655 self.sa.add(sett3)
657 self.sa.add(sett4)
656 self.sa.add(sett4)
658 self.sa.add(sett5)
657 self.sa.add(sett5)
659 self.sa.add(sett6)
658 self.sa.add(sett6)
660
659
661 self.create_ldap_options()
660 self.create_ldap_options()
662 self.create_default_options()
661 self.create_default_options()
663
662
664 log.info('created ui config')
663 log.info('created ui config')
665
664
666 def create_user(self, username, password, email='', admin=False):
665 def create_user(self, username, password, email='', admin=False):
667 log.info('creating user %s' % username)
666 log.info('creating user %s' % username)
668 UserModel().create_or_update(username, password, email,
667 UserModel().create_or_update(username, password, email,
669 firstname='RhodeCode', lastname='Admin',
668 firstname='RhodeCode', lastname='Admin',
670 active=True, admin=admin)
669 active=True, admin=admin)
671
670
672 def create_default_user(self):
671 def create_default_user(self):
673 log.info('creating default user')
672 log.info('creating default user')
674 # create default user for handling default permissions.
673 # create default user for handling default permissions.
675 UserModel().create_or_update(username='default',
674 UserModel().create_or_update(username='default',
676 password=str(uuid.uuid1())[:8],
675 password=str(uuid.uuid1())[:8],
677 email='anonymous@rhodecode.org',
676 email='anonymous@rhodecode.org',
678 firstname='Anonymous', lastname='User')
677 firstname='Anonymous', lastname='User')
679
678
680 def create_permissions(self):
679 def create_permissions(self):
681 # module.(access|create|change|delete)_[name]
680 # module.(access|create|change|delete)_[name]
682 # module.(none|read|write|admin)
681 # module.(none|read|write|admin)
683
682
684 for p in Permission.PERMS:
683 for p in Permission.PERMS:
685 if not Permission.get_by_key(p[0]):
684 if not Permission.get_by_key(p[0]):
686 new_perm = Permission()
685 new_perm = Permission()
687 new_perm.permission_name = p[0]
686 new_perm.permission_name = p[0]
688 new_perm.permission_longname = p[0]
687 new_perm.permission_longname = p[0]
689 self.sa.add(new_perm)
688 self.sa.add(new_perm)
690
689
691 def populate_default_permissions(self):
690 def populate_default_permissions(self):
692 log.info('creating default user permissions')
691 log.info('creating default user permissions')
693
692
694 default_user = User.get_by_username('default')
693 default_user = User.get_by_username('default')
695
694
696 for def_perm in User.DEFAULT_PERMISSIONS:
695 for def_perm in User.DEFAULT_PERMISSIONS:
697
696
698 perm = self.sa.query(Permission)\
697 perm = self.sa.query(Permission)\
699 .filter(Permission.permission_name == def_perm)\
698 .filter(Permission.permission_name == def_perm)\
700 .scalar()
699 .scalar()
701 if not perm:
700 if not perm:
702 raise Exception(
701 raise Exception(
703 'CRITICAL: permission %s not found inside database !!'
702 'CRITICAL: permission %s not found inside database !!'
704 % def_perm
703 % def_perm
705 )
704 )
706 if not UserToPerm.query()\
705 if not UserToPerm.query()\
707 .filter(UserToPerm.permission == perm)\
706 .filter(UserToPerm.permission == perm)\
708 .filter(UserToPerm.user == default_user).scalar():
707 .filter(UserToPerm.user == default_user).scalar():
709 reg_perm = UserToPerm()
708 reg_perm = UserToPerm()
710 reg_perm.user = default_user
709 reg_perm.user = default_user
711 reg_perm.permission = perm
710 reg_perm.permission = perm
712 self.sa.add(reg_perm)
711 self.sa.add(reg_perm)
713
712
714 @staticmethod
713 @staticmethod
715 def check_waitress():
714 def check_waitress():
716 """
715 """
717 Function executed at the end of setup
716 Function executed at the end of setup
718 """
717 """
719 if not __py_version__ >= (2, 6):
718 if not __py_version__ >= (2, 6):
720 notify('Python2.5 detected, please switch '
719 notify('Python2.5 detected, please switch '
721 'egg:waitress#main -> egg:Paste#http '
720 'egg:waitress#main -> egg:Paste#http '
722 'in your .ini file')
721 'in your .ini file')
General Comments 0
You need to be logged in to leave comments. Login now