##// END OF EJS Templates
db: ensure migrations are executed and steps are tested
marcink -
r2653:1c433ba9 default
parent child Browse files
Show More
@@ -1,620 +1,620 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database creation, and setup module for RhodeCode Enterprise. Used for creation
23 23 of database as well as for migration operations
24 24 """
25 25
26 26 import os
27 27 import sys
28 28 import time
29 29 import uuid
30 30 import logging
31 31 import getpass
32 32 from os.path import dirname as dn, join as jn
33 33
34 34 from sqlalchemy.engine import create_engine
35 35
36 36 from rhodecode import __dbversion__
37 37 from rhodecode.model import init_model
38 38 from rhodecode.model.user import UserModel
39 39 from rhodecode.model.db import (
40 40 User, Permission, RhodeCodeUi, RhodeCodeSetting, UserToPerm,
41 41 DbMigrateVersion, RepoGroup, UserRepoGroupToPerm, CacheKey, Repository)
42 42 from rhodecode.model.meta import Session, Base
43 43 from rhodecode.model.permission import PermissionModel
44 44 from rhodecode.model.repo import RepoModel
45 45 from rhodecode.model.repo_group import RepoGroupModel
46 46 from rhodecode.model.settings import SettingsModel
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 def notify(msg):
53 53 """
54 54 Notification for migrations messages
55 55 """
56 56 ml = len(msg) + (4 * 2)
57 57 print(('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper())
58 58
59 59
60 60 class DbManage(object):
61 61
62 62 def __init__(self, log_sql, dbconf, root, tests=False,
63 63 SESSION=None, cli_args=None):
64 64 self.dbname = dbconf.split('/')[-1]
65 65 self.tests = tests
66 66 self.root = root
67 67 self.dburi = dbconf
68 68 self.log_sql = log_sql
69 69 self.db_exists = False
70 70 self.cli_args = cli_args or {}
71 71 self.init_db(SESSION=SESSION)
72 72 self.ask_ok = self.get_ask_ok_func(self.cli_args.get('force_ask'))
73 73
74 74 def get_ask_ok_func(self, param):
75 75 if param not in [None]:
76 76 # return a function lambda that has a default set to param
77 77 return lambda *args, **kwargs: param
78 78 else:
79 79 from rhodecode.lib.utils import ask_ok
80 80 return ask_ok
81 81
82 82 def init_db(self, SESSION=None):
83 83 if SESSION:
84 84 self.sa = SESSION
85 85 else:
86 86 # init new sessions
87 87 engine = create_engine(self.dburi, echo=self.log_sql)
88 88 init_model(engine)
89 89 self.sa = Session()
90 90
91 91 def create_tables(self, override=False):
92 92 """
93 93 Create a auth database
94 94 """
95 95
96 96 log.info("Existing database with the same name is going to be destroyed.")
97 97 log.info("Setup command will run DROP ALL command on that database.")
98 98 if self.tests:
99 99 destroy = True
100 100 else:
101 101 destroy = self.ask_ok('Are you sure that you want to destroy the old database? [y/n]')
102 102 if not destroy:
103 103 log.info('Nothing done.')
104 104 sys.exit(0)
105 105 if destroy:
106 106 Base.metadata.drop_all()
107 107
108 108 checkfirst = not override
109 109 Base.metadata.create_all(checkfirst=checkfirst)
110 110 log.info('Created tables for %s' % self.dbname)
111 111
112 112 def set_db_version(self):
113 113 ver = DbMigrateVersion()
114 114 ver.version = __dbversion__
115 115 ver.repository_id = 'rhodecode_db_migrations'
116 116 ver.repository_path = 'versions'
117 117 self.sa.add(ver)
118 118 log.info('db version set to: %s' % __dbversion__)
119 119
120 120 def run_pre_migration_tasks(self):
121 121 """
122 122 Run various tasks before actually doing migrations
123 123 """
124 124 # delete cache keys on each upgrade
125 125 total = CacheKey.query().count()
126 126 log.info("Deleting (%s) cache keys now...", total)
127 127 CacheKey.delete_all_cache()
128 128
129 def upgrade(self):
129 def upgrade(self, version=None):
130 130 """
131 131 Upgrades given database schema to given revision following
132 132 all needed steps, to perform the upgrade
133 133
134 134 """
135 135
136 136 from rhodecode.lib.dbmigrate.migrate.versioning import api
137 137 from rhodecode.lib.dbmigrate.migrate.exceptions import \
138 138 DatabaseNotControlledError
139 139
140 140 if 'sqlite' in self.dburi:
141 141 print (
142 142 '********************** WARNING **********************\n'
143 143 'Make sure your version of sqlite is at least 3.7.X. \n'
144 144 'Earlier versions are known to fail on some migrations\n'
145 145 '*****************************************************\n')
146 146
147 147 upgrade = self.ask_ok(
148 148 'You are about to perform a database upgrade. Make '
149 149 'sure you have backed up your database. '
150 150 'Continue ? [y/n]')
151 151 if not upgrade:
152 152 log.info('No upgrade performed')
153 153 sys.exit(0)
154 154
155 155 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
156 156 'rhodecode/lib/dbmigrate')
157 157 db_uri = self.dburi
158 158
159 159 try:
160 curr_version = api.db_version(db_uri, repository_path)
161 msg = ('Found current database under version '
162 'control with version %s' % curr_version)
160 curr_version = version or api.db_version(db_uri, repository_path)
161 msg = ('Found current database db_uri under version '
162 'control with version {}'.format(curr_version))
163 163
164 164 except (RuntimeError, DatabaseNotControlledError):
165 165 curr_version = 1
166 166 msg = ('Current database is not under version control. Setting '
167 167 'as version %s' % curr_version)
168 168 api.version_control(db_uri, repository_path, curr_version)
169 169
170 170 notify(msg)
171 171
172 172 self.run_pre_migration_tasks()
173 173
174 174 if curr_version == __dbversion__:
175 175 log.info('This database is already at the newest version')
176 176 sys.exit(0)
177 177
178 178 upgrade_steps = range(curr_version + 1, __dbversion__ + 1)
179 179 notify('attempting to upgrade database from '
180 180 'version %s to version %s' % (curr_version, __dbversion__))
181 181
182 182 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
183 183 _step = None
184 184 for step in upgrade_steps:
185 185 notify('performing upgrade step %s' % step)
186 186 time.sleep(0.5)
187 187
188 188 api.upgrade(db_uri, repository_path, step)
189 189 self.sa.rollback()
190 190 notify('schema upgrade for step %s completed' % (step,))
191 191
192 192 _step = step
193 193
194 194 notify('upgrade to version %s successful' % _step)
195 195
196 196 def fix_repo_paths(self):
197 197 """
198 198 Fixes an old RhodeCode version path into new one without a '*'
199 199 """
200 200
201 201 paths = self.sa.query(RhodeCodeUi)\
202 202 .filter(RhodeCodeUi.ui_key == '/')\
203 203 .scalar()
204 204
205 205 paths.ui_value = paths.ui_value.replace('*', '')
206 206
207 207 try:
208 208 self.sa.add(paths)
209 209 self.sa.commit()
210 210 except Exception:
211 211 self.sa.rollback()
212 212 raise
213 213
214 214 def fix_default_user(self):
215 215 """
216 216 Fixes an old default user with some 'nicer' default values,
217 217 used mostly for anonymous access
218 218 """
219 219 def_user = self.sa.query(User)\
220 220 .filter(User.username == User.DEFAULT_USER)\
221 221 .one()
222 222
223 223 def_user.name = 'Anonymous'
224 224 def_user.lastname = 'User'
225 225 def_user.email = User.DEFAULT_USER_EMAIL
226 226
227 227 try:
228 228 self.sa.add(def_user)
229 229 self.sa.commit()
230 230 except Exception:
231 231 self.sa.rollback()
232 232 raise
233 233
234 234 def fix_settings(self):
235 235 """
236 236 Fixes rhodecode settings and adds ga_code key for google analytics
237 237 """
238 238
239 239 hgsettings3 = RhodeCodeSetting('ga_code', '')
240 240
241 241 try:
242 242 self.sa.add(hgsettings3)
243 243 self.sa.commit()
244 244 except Exception:
245 245 self.sa.rollback()
246 246 raise
247 247
248 248 def create_admin_and_prompt(self):
249 249
250 250 # defaults
251 251 defaults = self.cli_args
252 252 username = defaults.get('username')
253 253 password = defaults.get('password')
254 254 email = defaults.get('email')
255 255
256 256 if username is None:
257 257 username = raw_input('Specify admin username:')
258 258 if password is None:
259 259 password = self._get_admin_password()
260 260 if not password:
261 261 # second try
262 262 password = self._get_admin_password()
263 263 if not password:
264 264 sys.exit()
265 265 if email is None:
266 266 email = raw_input('Specify admin email:')
267 267 api_key = self.cli_args.get('api_key')
268 268 self.create_user(username, password, email, True,
269 269 strict_creation_check=False,
270 270 api_key=api_key)
271 271
272 272 def _get_admin_password(self):
273 273 password = getpass.getpass('Specify admin password '
274 274 '(min 6 chars):')
275 275 confirm = getpass.getpass('Confirm password:')
276 276
277 277 if password != confirm:
278 278 log.error('passwords mismatch')
279 279 return False
280 280 if len(password) < 6:
281 281 log.error('password is too short - use at least 6 characters')
282 282 return False
283 283
284 284 return password
285 285
286 286 def create_test_admin_and_users(self):
287 287 log.info('creating admin and regular test users')
288 288 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
289 289 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
290 290 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
291 291 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
292 292 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
293 293
294 294 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
295 295 TEST_USER_ADMIN_EMAIL, True, api_key=True)
296 296
297 297 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
298 298 TEST_USER_REGULAR_EMAIL, False, api_key=True)
299 299
300 300 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
301 301 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
302 302
303 303 def create_ui_settings(self, repo_store_path):
304 304 """
305 305 Creates ui settings, fills out hooks
306 306 and disables dotencode
307 307 """
308 308 settings_model = SettingsModel(sa=self.sa)
309 309 from rhodecode.lib.vcs.backends.hg import largefiles_store
310 310 from rhodecode.lib.vcs.backends.git import lfs_store
311 311
312 312 # Build HOOKS
313 313 hooks = [
314 314 (RhodeCodeUi.HOOK_REPO_SIZE, 'python:vcsserver.hooks.repo_size'),
315 315
316 316 # HG
317 317 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
318 318 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
319 319 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
320 320 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
321 321 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
322 322 (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'),
323 323
324 324 ]
325 325
326 326 for key, value in hooks:
327 327 hook_obj = settings_model.get_ui_by_key(key)
328 328 hooks2 = hook_obj if hook_obj else RhodeCodeUi()
329 329 hooks2.ui_section = 'hooks'
330 330 hooks2.ui_key = key
331 331 hooks2.ui_value = value
332 332 self.sa.add(hooks2)
333 333
334 334 # enable largefiles
335 335 largefiles = RhodeCodeUi()
336 336 largefiles.ui_section = 'extensions'
337 337 largefiles.ui_key = 'largefiles'
338 338 largefiles.ui_value = ''
339 339 self.sa.add(largefiles)
340 340
341 341 # set default largefiles cache dir, defaults to
342 342 # /repo_store_location/.cache/largefiles
343 343 largefiles = RhodeCodeUi()
344 344 largefiles.ui_section = 'largefiles'
345 345 largefiles.ui_key = 'usercache'
346 346 largefiles.ui_value = largefiles_store(repo_store_path)
347 347
348 348 self.sa.add(largefiles)
349 349
350 350 # set default lfs cache dir, defaults to
351 351 # /repo_store_location/.cache/lfs_store
352 352 lfsstore = RhodeCodeUi()
353 353 lfsstore.ui_section = 'vcs_git_lfs'
354 354 lfsstore.ui_key = 'store_location'
355 355 lfsstore.ui_value = lfs_store(repo_store_path)
356 356
357 357 self.sa.add(lfsstore)
358 358
359 359 # enable hgsubversion disabled by default
360 360 hgsubversion = RhodeCodeUi()
361 361 hgsubversion.ui_section = 'extensions'
362 362 hgsubversion.ui_key = 'hgsubversion'
363 363 hgsubversion.ui_value = ''
364 364 hgsubversion.ui_active = False
365 365 self.sa.add(hgsubversion)
366 366
367 367 # enable hgevolve disabled by default
368 368 hgevolve = RhodeCodeUi()
369 369 hgevolve.ui_section = 'extensions'
370 370 hgevolve.ui_key = 'evolve'
371 371 hgevolve.ui_value = ''
372 372 hgevolve.ui_active = False
373 373 self.sa.add(hgevolve)
374 374
375 375 # enable hggit disabled by default
376 376 hggit = RhodeCodeUi()
377 377 hggit.ui_section = 'extensions'
378 378 hggit.ui_key = 'hggit'
379 379 hggit.ui_value = ''
380 380 hggit.ui_active = False
381 381 self.sa.add(hggit)
382 382
383 383 # set svn branch defaults
384 384 branches = ["/branches/*", "/trunk"]
385 385 tags = ["/tags/*"]
386 386
387 387 for branch in branches:
388 388 settings_model.create_ui_section_value(
389 389 RhodeCodeUi.SVN_BRANCH_ID, branch)
390 390
391 391 for tag in tags:
392 392 settings_model.create_ui_section_value(RhodeCodeUi.SVN_TAG_ID, tag)
393 393
394 394 def create_auth_plugin_options(self, skip_existing=False):
395 395 """
396 396 Create default auth plugin settings, and make it active
397 397
398 398 :param skip_existing:
399 399 """
400 400
401 401 for k, v, t in [('auth_plugins', 'egg:rhodecode-enterprise-ce#rhodecode', 'list'),
402 402 ('auth_rhodecode_enabled', 'True', 'bool')]:
403 403 if (skip_existing and
404 404 SettingsModel().get_setting_by_name(k) is not None):
405 405 log.debug('Skipping option %s' % k)
406 406 continue
407 407 setting = RhodeCodeSetting(k, v, t)
408 408 self.sa.add(setting)
409 409
410 410 def create_default_options(self, skip_existing=False):
411 411 """Creates default settings"""
412 412
413 413 for k, v, t in [
414 414 ('default_repo_enable_locking', False, 'bool'),
415 415 ('default_repo_enable_downloads', False, 'bool'),
416 416 ('default_repo_enable_statistics', False, 'bool'),
417 417 ('default_repo_private', False, 'bool'),
418 418 ('default_repo_type', 'hg', 'unicode')]:
419 419
420 420 if (skip_existing and
421 421 SettingsModel().get_setting_by_name(k) is not None):
422 422 log.debug('Skipping option %s' % k)
423 423 continue
424 424 setting = RhodeCodeSetting(k, v, t)
425 425 self.sa.add(setting)
426 426
427 427 def fixup_groups(self):
428 428 def_usr = User.get_default_user()
429 429 for g in RepoGroup.query().all():
430 430 g.group_name = g.get_new_name(g.name)
431 431 self.sa.add(g)
432 432 # get default perm
433 433 default = UserRepoGroupToPerm.query()\
434 434 .filter(UserRepoGroupToPerm.group == g)\
435 435 .filter(UserRepoGroupToPerm.user == def_usr)\
436 436 .scalar()
437 437
438 438 if default is None:
439 439 log.debug('missing default permission for group %s adding' % g)
440 440 perm_obj = RepoGroupModel()._create_default_perms(g)
441 441 self.sa.add(perm_obj)
442 442
443 443 def reset_permissions(self, username):
444 444 """
445 445 Resets permissions to default state, useful when old systems had
446 446 bad permissions, we must clean them up
447 447
448 448 :param username:
449 449 """
450 450 default_user = User.get_by_username(username)
451 451 if not default_user:
452 452 return
453 453
454 454 u2p = UserToPerm.query()\
455 455 .filter(UserToPerm.user == default_user).all()
456 456 fixed = False
457 457 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
458 458 for p in u2p:
459 459 Session().delete(p)
460 460 fixed = True
461 461 self.populate_default_permissions()
462 462 return fixed
463 463
464 464 def update_repo_info(self):
465 465 RepoModel.update_repoinfo()
466 466
467 467 def config_prompt(self, test_repo_path='', retries=3):
468 468 defaults = self.cli_args
469 469 _path = defaults.get('repos_location')
470 470 if retries == 3:
471 471 log.info('Setting up repositories config')
472 472
473 473 if _path is not None:
474 474 path = _path
475 475 elif not self.tests and not test_repo_path:
476 476 path = raw_input(
477 477 'Enter a valid absolute path to store repositories. '
478 478 'All repositories in that path will be added automatically:'
479 479 )
480 480 else:
481 481 path = test_repo_path
482 482 path_ok = True
483 483
484 484 # check proper dir
485 485 if not os.path.isdir(path):
486 486 path_ok = False
487 487 log.error('Given path %s is not a valid directory' % (path,))
488 488
489 489 elif not os.path.isabs(path):
490 490 path_ok = False
491 491 log.error('Given path %s is not an absolute path' % (path,))
492 492
493 493 # check if path is at least readable.
494 494 if not os.access(path, os.R_OK):
495 495 path_ok = False
496 496 log.error('Given path %s is not readable' % (path,))
497 497
498 498 # check write access, warn user about non writeable paths
499 499 elif not os.access(path, os.W_OK) and path_ok:
500 500 log.warning('No write permission to given path %s' % (path,))
501 501
502 502 q = ('Given path %s is not writeable, do you want to '
503 503 'continue with read only mode ? [y/n]' % (path,))
504 504 if not self.ask_ok(q):
505 505 log.error('Canceled by user')
506 506 sys.exit(-1)
507 507
508 508 if retries == 0:
509 509 sys.exit('max retries reached')
510 510 if not path_ok:
511 511 retries -= 1
512 512 return self.config_prompt(test_repo_path, retries)
513 513
514 514 real_path = os.path.normpath(os.path.realpath(path))
515 515
516 516 if real_path != os.path.normpath(path):
517 517 q = ('Path looks like a symlink, RhodeCode Enterprise will store '
518 518 'given path as %s ? [y/n]') % (real_path,)
519 519 if not self.ask_ok(q):
520 520 log.error('Canceled by user')
521 521 sys.exit(-1)
522 522
523 523 return real_path
524 524
525 525 def create_settings(self, path):
526 526
527 527 self.create_ui_settings(path)
528 528
529 529 ui_config = [
530 530 ('web', 'push_ssl', 'False'),
531 531 ('web', 'allow_archive', 'gz zip bz2'),
532 532 ('web', 'allow_push', '*'),
533 533 ('web', 'baseurl', '/'),
534 534 ('paths', '/', path),
535 535 ('phases', 'publish', 'True')
536 536 ]
537 537 for section, key, value in ui_config:
538 538 ui_conf = RhodeCodeUi()
539 539 setattr(ui_conf, 'ui_section', section)
540 540 setattr(ui_conf, 'ui_key', key)
541 541 setattr(ui_conf, 'ui_value', value)
542 542 self.sa.add(ui_conf)
543 543
544 544 # rhodecode app settings
545 545 settings = [
546 546 ('realm', 'RhodeCode', 'unicode'),
547 547 ('title', '', 'unicode'),
548 548 ('pre_code', '', 'unicode'),
549 549 ('post_code', '', 'unicode'),
550 550 ('show_public_icon', True, 'bool'),
551 551 ('show_private_icon', True, 'bool'),
552 552 ('stylify_metatags', False, 'bool'),
553 553 ('dashboard_items', 100, 'int'),
554 554 ('admin_grid_items', 25, 'int'),
555 555 ('show_version', True, 'bool'),
556 556 ('use_gravatar', False, 'bool'),
557 557 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
558 558 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
559 559 ('support_url', '', 'unicode'),
560 560 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
561 561 ('show_revision_number', True, 'bool'),
562 562 ('show_sha_length', 12, 'int'),
563 563 ]
564 564
565 565 for key, val, type_ in settings:
566 566 sett = RhodeCodeSetting(key, val, type_)
567 567 self.sa.add(sett)
568 568
569 569 self.create_auth_plugin_options()
570 570 self.create_default_options()
571 571
572 572 log.info('created ui config')
573 573
574 574 def create_user(self, username, password, email='', admin=False,
575 575 strict_creation_check=True, api_key=None):
576 576 log.info('creating user %s' % username)
577 577 user = UserModel().create_or_update(
578 578 username, password, email, firstname=u'RhodeCode', lastname=u'Admin',
579 579 active=True, admin=admin, extern_type="rhodecode",
580 580 strict_creation_check=strict_creation_check)
581 581
582 582 if api_key:
583 583 log.info('setting a provided api key for the user %s', username)
584 584 from rhodecode.model.auth_token import AuthTokenModel
585 585 AuthTokenModel().create(
586 586 user=user, description=u'BUILTIN TOKEN')
587 587
588 588 def create_default_user(self):
589 589 log.info('creating default user')
590 590 # create default user for handling default permissions.
591 591 user = UserModel().create_or_update(username=User.DEFAULT_USER,
592 592 password=str(uuid.uuid1())[:20],
593 593 email=User.DEFAULT_USER_EMAIL,
594 594 firstname=u'Anonymous',
595 595 lastname=u'User',
596 596 strict_creation_check=False)
597 597 # based on configuration options activate/deactive this user which
598 598 # controlls anonymous access
599 599 if self.cli_args.get('public_access') is False:
600 600 log.info('Public access disabled')
601 601 user.active = False
602 602 Session().add(user)
603 603 Session().commit()
604 604
605 605 def create_permissions(self):
606 606 """
607 607 Creates all permissions defined in the system
608 608 """
609 609 # module.(access|create|change|delete)_[name]
610 610 # module.(none|read|write|admin)
611 611 log.info('creating permissions')
612 612 PermissionModel(self.sa).create_permissions()
613 613
614 614 def populate_default_permissions(self):
615 615 """
616 616 Populate default permissions. It will create only the default
617 617 permissions that are missing, and not alter already defined ones
618 618 """
619 619 log.info('creating default user permissions')
620 620 PermissionModel(self.sa).create_default_user_permissions(user=User.DEFAULT_USER)
@@ -1,288 +1,292 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from subprocess32 import Popen, PIPE
22 22 import os
23 23 import shutil
24 24 import sys
25 25 import tempfile
26 26
27 27 import pytest
28 28 from sqlalchemy.engine import url
29 29
30 30 from rhodecode.tests.fixture import TestINI
31 31
32 32
33 33 def _get_dbs_from_metafunc(metafunc):
34 34 if hasattr(metafunc.function, 'dbs'):
35 35 # Supported backends by this test function, created from
36 36 # pytest.mark.dbs
37 37 backends = metafunc.function.dbs.args
38 38 else:
39 39 backends = metafunc.config.getoption('--dbs')
40 40 return backends
41 41
42 42
43 43 def pytest_generate_tests(metafunc):
44 44 # Support test generation based on --dbs parameter
45 45 if 'db_backend' in metafunc.fixturenames:
46 46 requested_backends = set(metafunc.config.getoption('--dbs'))
47 47 backends = _get_dbs_from_metafunc(metafunc)
48 48 backends = requested_backends.intersection(backends)
49 49 # TODO: johbo: Disabling a backend did not work out with
50 50 # parametrization, find better way to achieve this.
51 51 if not backends:
52 52 metafunc.function._skip = True
53 53 metafunc.parametrize('db_backend_name', backends)
54 54
55 55
56 56 def pytest_collection_modifyitems(session, config, items):
57 57 remaining = [
58 58 i for i in items if not getattr(i.obj, '_skip', False)]
59 59 items[:] = remaining
60 60
61 61
62 62 @pytest.fixture
63 63 def db_backend(
64 64 request, db_backend_name, ini_config, tmpdir_factory):
65 65 basetemp = tmpdir_factory.getbasetemp().strpath
66 66 klass = _get_backend(db_backend_name)
67 67
68 68 option_name = '--{}-connection-string'.format(db_backend_name)
69 69 connection_string = request.config.getoption(option_name) or None
70 70
71 71 return klass(
72 72 config_file=ini_config, basetemp=basetemp,
73 73 connection_string=connection_string)
74 74
75 75
76 76 def _get_backend(backend_type):
77 77 return {
78 78 'sqlite': SQLiteDBBackend,
79 79 'postgres': PostgresDBBackend,
80 80 'mysql': MySQLDBBackend,
81 81 '': EmptyDBBackend
82 82 }[backend_type]
83 83
84 84
85 85 class DBBackend(object):
86 86 _store = os.path.dirname(os.path.abspath(__file__))
87 87 _type = None
88 88 _base_ini_config = [{'app:main': {'vcs.start_server': 'false',
89 'startup.import_repos': 'false'}}]
89 'startup.import_repos': 'false',
90 'is_test': 'False'}}]
90 91 _db_url = [{'app:main': {'sqlalchemy.db1.url': ''}}]
91 92 _base_db_name = 'rhodecode_test_db_backend'
92 93
93 94 def __init__(
94 95 self, config_file, db_name=None, basetemp=None,
95 96 connection_string=None):
96 97
97 98 from rhodecode.lib.vcs.backends.hg import largefiles_store
98 99 from rhodecode.lib.vcs.backends.git import lfs_store
99 100
100 101 self.fixture_store = os.path.join(self._store, self._type)
101 102 self.db_name = db_name or self._base_db_name
102 103 self._base_ini_file = config_file
103 104 self.stderr = ''
104 105 self.stdout = ''
105 106 self._basetemp = basetemp or tempfile.gettempdir()
106 107 self._repos_location = os.path.join(self._basetemp, 'rc_test_repos')
107 108 self._repos_hg_largefiles_store = largefiles_store(self._basetemp)
108 109 self._repos_git_lfs_store = lfs_store(self._basetemp)
109 110 self.connection_string = connection_string
110 111
111 112 @property
112 113 def connection_string(self):
113 114 return self._connection_string
114 115
115 116 @connection_string.setter
116 117 def connection_string(self, new_connection_string):
117 118 if not new_connection_string:
118 119 new_connection_string = self.get_default_connection_string()
119 120 else:
120 121 new_connection_string = new_connection_string.format(
121 122 db_name=self.db_name)
122 123 url_parts = url.make_url(new_connection_string)
123 124 self._connection_string = new_connection_string
124 125 self.user = url_parts.username
125 126 self.password = url_parts.password
126 127 self.host = url_parts.host
127 128
128 129 def get_default_connection_string(self):
129 130 raise NotImplementedError('default connection_string is required.')
130 131
131 132 def execute(self, cmd, env=None, *args):
132 133 """
133 134 Runs command on the system with given ``args``.
134 135 """
135 136
136 137 command = cmd + ' ' + ' '.join(args)
137 138 sys.stdout.write(command)
138 139
139 140 # Tell Python to use UTF-8 encoding out stdout
140 141 _env = os.environ.copy()
141 142 _env['PYTHONIOENCODING'] = 'UTF-8'
142 143 if env:
143 144 _env.update(env)
144 145 self.p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, env=_env)
145 146 self.stdout, self.stderr = self.p.communicate()
146 147 sys.stdout.write('COMMAND:'+command+'\n')
147 148 sys.stdout.write(self.stdout)
148 149 return self.stdout, self.stderr
149 150
150 151 def assert_returncode_success(self):
151 152 if not self.p.returncode == 0:
152 153 print(self.stderr)
153 154 raise AssertionError('non 0 retcode:{}'.format(self.p.returncode))
154 155
156 def assert_correct_output(self, stdout, version):
157 assert 'UPGRADE FOR STEP {} COMPLETED'.format(version) in stdout
158
155 159 def setup_rhodecode_db(self, ini_params=None, env=None):
156 160 if not ini_params:
157 161 ini_params = self._base_ini_config
158 162
159 163 ini_params.extend(self._db_url)
160 164 with TestINI(self._base_ini_file, ini_params,
161 165 self._type, destroy=True) as _ini_file:
162 166
163 167 if not os.path.isdir(self._repos_location):
164 168 os.makedirs(self._repos_location)
165 169 if not os.path.isdir(self._repos_hg_largefiles_store):
166 170 os.makedirs(self._repos_hg_largefiles_store)
167 171 if not os.path.isdir(self._repos_git_lfs_store):
168 172 os.makedirs(self._repos_git_lfs_store)
169 173
170 self.execute(
174 return self.execute(
171 175 "rc-setup-app {0} --user=marcink "
172 176 "--email=marcin@rhodeocode.com --password={1} "
173 177 "--repos={2} --force-yes".format(
174 178 _ini_file, 'qweqwe', self._repos_location), env=env)
175 179
176 180 def upgrade_database(self, ini_params=None):
177 181 if not ini_params:
178 182 ini_params = self._base_ini_config
179 183 ini_params.extend(self._db_url)
180 184
181 185 test_ini = TestINI(
182 186 self._base_ini_file, ini_params, self._type, destroy=True)
183 187 with test_ini as ini_file:
184 188 if not os.path.isdir(self._repos_location):
185 189 os.makedirs(self._repos_location)
186 self.execute(
190
191 return self.execute(
187 192 "rc-upgrade-db {0} --force-yes".format(ini_file))
188 193
189 194 def setup_db(self):
190 195 raise NotImplementedError
191 196
192 197 def teardown_db(self):
193 198 raise NotImplementedError
194 199
195 200 def import_dump(self, dumpname):
196 201 raise NotImplementedError
197 202
198 203
199 204 class EmptyDBBackend(DBBackend):
200 205 _type = ''
201 206
202 207 def setup_db(self):
203 208 pass
204 209
205 210 def teardown_db(self):
206 211 pass
207 212
208 213 def import_dump(self, dumpname):
209 214 pass
210 215
211 216 def assert_returncode_success(self):
212 217 assert True
213 218
214 219
215 220 class SQLiteDBBackend(DBBackend):
216 221 _type = 'sqlite'
217 222
218 223 def get_default_connection_string(self):
219 224 return 'sqlite:///{}/{}.sqlite'.format(self._basetemp, self.db_name)
220 225
221 226 def setup_db(self):
222 227 # dump schema for tests
223 228 # cp -v $TEST_DB_NAME
224 229 self._db_url = [{'app:main': {
225 230 'sqlalchemy.db1.url': self.connection_string}}]
226 231
227 232 def import_dump(self, dumpname):
228 233 dump = os.path.join(self.fixture_store, dumpname)
229 shutil.copy(
230 dump,
231 os.path.join(self._basetemp, '{0.db_name}.sqlite'.format(self)))
234 target = os.path.join(self._basetemp, '{0.db_name}.sqlite'.format(self))
235 return self.execute('cp -v {} {}'.format(dump, target))
232 236
233 237 def teardown_db(self):
234 self.execute("rm -rf {}.sqlite".format(
238 return self.execute("rm -rf {}.sqlite".format(
235 239 os.path.join(self._basetemp, self.db_name)))
236 240
237 241
238 242 class MySQLDBBackend(DBBackend):
239 243 _type = 'mysql'
240 244
241 245 def get_default_connection_string(self):
242 246 return 'mysql://root:qweqwe@127.0.0.1/{}'.format(self.db_name)
243 247
244 248 def setup_db(self):
245 249 # dump schema for tests
246 250 # mysqldump -uroot -pqweqwe $TEST_DB_NAME
247 251 self._db_url = [{'app:main': {
248 252 'sqlalchemy.db1.url': self.connection_string}}]
249 self.execute("mysql -v -u{} -p{} -e 'create database '{}';'".format(
253 return self.execute("mysql -v -u{} -p{} -e 'create database '{}';'".format(
250 254 self.user, self.password, self.db_name))
251 255
252 256 def import_dump(self, dumpname):
253 257 dump = os.path.join(self.fixture_store, dumpname)
254 self.execute("mysql -u{} -p{} {} < {}".format(
258 return self.execute("mysql -u{} -p{} {} < {}".format(
255 259 self.user, self.password, self.db_name, dump))
256 260
257 261 def teardown_db(self):
258 self.execute("mysql -v -u{} -p{} -e 'drop database '{}';'".format(
262 return self.execute("mysql -v -u{} -p{} -e 'drop database '{}';'".format(
259 263 self.user, self.password, self.db_name))
260 264
261 265
262 266 class PostgresDBBackend(DBBackend):
263 267 _type = 'postgres'
264 268
265 269 def get_default_connection_string(self):
266 270 return 'postgresql://postgres:qweqwe@localhost/{}'.format(self.db_name)
267 271
268 272 def setup_db(self):
269 273 # dump schema for tests
270 274 # pg_dump -U postgres -h localhost $TEST_DB_NAME
271 275 self._db_url = [{'app:main': {
272 276 'sqlalchemy.db1.url':
273 277 self.connection_string}}]
274 self.execute("PGPASSWORD={} psql -U {} -h localhost "
278 return self.execute("PGPASSWORD={} psql -U {} -h localhost "
275 279 "-c 'create database '{}';'".format(
276 280 self.password, self.user, self.db_name))
277 281
278 282 def teardown_db(self):
279 self.execute("PGPASSWORD={} psql -U {} -h localhost "
283 return self.execute("PGPASSWORD={} psql -U {} -h localhost "
280 284 "-c 'drop database if exists '{}';'".format(
281 285 self.password, self.user, self.db_name))
282 286
283 287 def import_dump(self, dumpname):
284 288 dump = os.path.join(self.fixture_store, dumpname)
285 self.execute(
289 return self.execute(
286 290 "PGPASSWORD={} psql -U {} -h localhost -d {} -1 "
287 291 "-f {}".format(
288 292 self.password, self.user, self.db_name, dump))
@@ -1,63 +1,65 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23
24 24 @pytest.mark.dbs("postgres")
25 25 @pytest.mark.parametrize("dumpname", [
26 26 '1.4.4.sql',
27 27 '1.5.0.sql',
28 28 '1.6.0.sql',
29 29 '1.6.0_no_repo_name_index.sql',
30 30 ])
31 31 def test_migrate_postgres_db(db_backend, dumpname):
32 32 _run_migration_test(db_backend, dumpname)
33 33
34 34
35 35 @pytest.mark.dbs("sqlite")
36 36 @pytest.mark.parametrize("dumpname", [
37 37 'rhodecode.1.4.4.sqlite',
38 38 'rhodecode.1.4.4_with_groups.sqlite',
39 39 'rhodecode.1.4.4_with_ldap_active.sqlite',
40 40 ])
41 41 def test_migrate_sqlite_db(db_backend, dumpname):
42 42 _run_migration_test(db_backend, dumpname)
43 43
44 44
45 45 @pytest.mark.dbs("mysql")
46 46 @pytest.mark.parametrize("dumpname", [
47 47 '1.4.4.sql',
48 48 '1.5.0.sql',
49 49 '1.6.0.sql',
50 50 '1.6.0_no_repo_name_index.sql',
51 51 ])
52 52 def test_migrate_mysql_db(db_backend, dumpname):
53 53 _run_migration_test(db_backend, dumpname)
54 54
55 55
56 56 def _run_migration_test(db_backend, dumpname):
57 57 db_backend.teardown_db()
58 58 db_backend.setup_db()
59 59 db_backend.assert_returncode_success()
60 60
61 61 db_backend.import_dump(dumpname)
62 db_backend.upgrade_database()
62 stdout, stderr = db_backend.upgrade_database()
63
64 db_backend.assert_correct_output(stdout+stderr, version='16')
63 65 db_backend.assert_returncode_success()
General Comments 0
You need to be logged in to leave comments. Login now