OpenStack Networking (Neutron)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

cli.py 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. # Copyright 2012 New Dream Network, LLC (DreamHost)
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. # not use this file except in compliance with the License. You may obtain
  5. # a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. # License for the specific language governing permissions and limitations
  13. # under the License.
  14. import copy
  15. from logging import config as logging_config
  16. import os
  17. from alembic import command as alembic_command
  18. from alembic import config as alembic_config
  19. from alembic import environment
  20. from alembic import migration as alembic_migration
  21. from alembic import script as alembic_script
  22. from alembic import util as alembic_util
  23. from oslo_config import cfg
  24. from oslo_utils import fileutils
  25. from oslo_utils import importutils
  26. import six
  27. from neutron._i18n import _
  28. from neutron.conf.db import migration_cli
  29. from neutron.db import migration
  30. from neutron.db.migration.connection import DBConnection
  31. HEAD_FILENAME = 'HEAD'
  32. HEADS_FILENAME = 'HEADS'
  33. CONTRACT_HEAD_FILENAME = 'CONTRACT_HEAD'
  34. EXPAND_HEAD_FILENAME = 'EXPAND_HEAD'
  35. CURRENT_RELEASE = migration.TRAIN
  36. RELEASES = (
  37. migration.LIBERTY,
  38. migration.MITAKA,
  39. migration.NEWTON,
  40. migration.OCATA,
  41. migration.PIKE,
  42. migration.QUEENS,
  43. migration.ROCKY,
  44. migration.STEIN,
  45. migration.TRAIN,
  46. )
  47. EXPAND_BRANCH = 'expand'
  48. CONTRACT_BRANCH = 'contract'
  49. MIGRATION_BRANCHES = (EXPAND_BRANCH, CONTRACT_BRANCH)
  50. neutron_alembic_ini = os.path.join(os.path.dirname(__file__), 'alembic.ini')
  51. CONF = cfg.ConfigOpts()
  52. migration_cli.register_db_cli_opts(CONF)
  53. log_error = alembic_util.err
  54. log_warning = alembic_util.warn
  55. log_info = alembic_util.msg
  56. def do_alembic_command(config, cmd, revision=None, desc=None, **kwargs):
  57. args = []
  58. if revision:
  59. args.append(revision)
  60. project = config.get_main_option('neutron_project')
  61. if desc:
  62. log_info(_('Running %(cmd)s (%(desc)s) for %(project)s ...') %
  63. {'cmd': cmd, 'desc': desc, 'project': project})
  64. else:
  65. log_info(_('Running %(cmd)s for %(project)s ...') %
  66. {'cmd': cmd, 'project': project})
  67. try:
  68. getattr(alembic_command, cmd)(config, *args, **kwargs)
  69. except alembic_util.CommandError as e:
  70. log_error(six.text_type(e))
  71. log_info(_('OK'))
  72. def _get_alembic_entrypoint(project):
  73. if project not in migration_cli.migration_entrypoints:
  74. log_error(_('Sub-project %s not installed.') % project)
  75. return migration_cli.migration_entrypoints[project]
  76. def do_generic_show(config, cmd):
  77. kwargs = {'verbose': CONF.command.verbose}
  78. do_alembic_command(config, cmd, **kwargs)
  79. def do_check_migration(config, cmd):
  80. do_alembic_command(config, 'branches')
  81. validate_revisions(config)
  82. validate_head_files(config)
  83. def add_alembic_subparser(sub, cmd):
  84. return sub.add_parser(cmd, help=getattr(alembic_command, cmd).__doc__)
  85. def add_branch_options(parser):
  86. group = parser.add_mutually_exclusive_group()
  87. group.add_argument('--expand', action='store_true')
  88. group.add_argument('--contract', action='store_true')
  89. return group
  90. def _find_milestone_revisions(config, milestone, branch=None):
  91. """Return the revision(s) for a given milestone."""
  92. script = alembic_script.ScriptDirectory.from_config(config)
  93. return [
  94. (m.revision, label)
  95. for m in _get_revisions(script)
  96. for label in (m.branch_labels or [None])
  97. if milestone in getattr(m.module, 'neutron_milestone', []) and
  98. (branch is None or branch in m.branch_labels)
  99. ]
  100. def do_upgrade(config, cmd):
  101. branch = None
  102. if ((CONF.command.revision or CONF.command.delta) and
  103. (CONF.command.expand or CONF.command.contract)):
  104. raise SystemExit(_(
  105. 'Phase upgrade options do not accept revision specification'))
  106. if CONF.command.expand:
  107. branch = EXPAND_BRANCH
  108. revision = _get_branch_head(EXPAND_BRANCH)
  109. elif CONF.command.contract:
  110. branch = CONTRACT_BRANCH
  111. revision = _get_branch_head(CONTRACT_BRANCH)
  112. elif not CONF.command.revision and not CONF.command.delta:
  113. raise SystemExit(_('You must provide a revision or relative delta'))
  114. else:
  115. revision = CONF.command.revision or ''
  116. if '-' in revision:
  117. raise SystemExit(_('Negative relative revision (downgrade) not '
  118. 'supported'))
  119. delta = CONF.command.delta
  120. if delta:
  121. if '+' in revision:
  122. raise SystemExit(_('Use either --delta or relative revision, '
  123. 'not both'))
  124. if delta < 0:
  125. raise SystemExit(_('Negative delta (downgrade) not supported'))
  126. revision = '%s+%d' % (revision, delta)
  127. # leave branchless 'head' revision request backward compatible by
  128. # applying all heads in all available branches.
  129. if revision == 'head':
  130. revision = 'heads'
  131. if revision in migration.NEUTRON_MILESTONES:
  132. expand_revisions = _find_milestone_revisions(config, revision,
  133. EXPAND_BRANCH)
  134. contract_revisions = _find_milestone_revisions(config, revision,
  135. CONTRACT_BRANCH)
  136. # Expand revisions must be run before contract revisions
  137. revisions = expand_revisions + contract_revisions
  138. else:
  139. revisions = [(revision, branch)]
  140. for revision, branch in revisions:
  141. if not CONF.command.sql:
  142. run_sanity_checks(config, revision)
  143. do_alembic_command(config, cmd, revision=revision,
  144. desc=branch, sql=CONF.command.sql)
  145. def no_downgrade(config, cmd):
  146. raise SystemExit(_("Downgrade no longer supported"))
  147. def do_stamp(config, cmd):
  148. do_alembic_command(config, cmd,
  149. revision=CONF.command.revision,
  150. sql=CONF.command.sql)
  151. def _get_branch_head(branch):
  152. '''Get the latest @head specification for a branch.'''
  153. return '%s@head' % branch
  154. def _check_bootstrap_new_branch(branch, version_path, addn_kwargs):
  155. addn_kwargs['version_path'] = version_path
  156. addn_kwargs['head'] = _get_branch_head(branch)
  157. if not os.path.exists(version_path):
  158. # Bootstrap initial directory structure
  159. fileutils.ensure_tree(version_path, mode=0o755)
  160. def do_revision(config, cmd):
  161. kwargs = {
  162. 'message': CONF.command.message,
  163. 'autogenerate': CONF.command.autogenerate,
  164. 'sql': CONF.command.sql,
  165. }
  166. branches = []
  167. if CONF.command.expand:
  168. kwargs['head'] = 'expand@head'
  169. branches.append(EXPAND_BRANCH)
  170. elif CONF.command.contract:
  171. kwargs['head'] = 'contract@head'
  172. branches.append(CONTRACT_BRANCH)
  173. else:
  174. branches = MIGRATION_BRANCHES
  175. if not CONF.command.autogenerate:
  176. for branch in branches:
  177. args = copy.copy(kwargs)
  178. version_path = _get_version_branch_path(
  179. config, release=CURRENT_RELEASE, branch=branch)
  180. _check_bootstrap_new_branch(branch, version_path, args)
  181. do_alembic_command(config, cmd, **args)
  182. else:
  183. # autogeneration code will take care of enforcing proper directories
  184. do_alembic_command(config, cmd, **kwargs)
  185. update_head_files(config)
  186. def _get_release_labels(labels):
  187. result = set()
  188. for label in labels:
  189. # release labels were introduced Liberty for a short time and dropped
  190. # in that same release cycle
  191. result.add('%s_%s' % (migration.LIBERTY, label))
  192. return result
  193. def _compare_labels(revision, expected_labels):
  194. # validate that the script has expected labels only
  195. bad_labels = revision.branch_labels - expected_labels
  196. if bad_labels:
  197. # NOTE(ihrachyshka): this hack is temporary to accommodate those
  198. # projects that already initialized their branches with liberty_*
  199. # labels. Let's notify them about the deprecation for now and drop it
  200. # later.
  201. bad_labels_with_release = (revision.branch_labels -
  202. _get_release_labels(expected_labels))
  203. if not bad_labels_with_release:
  204. log_warning(
  205. _('Release aware branch labels (%s) are deprecated. '
  206. 'Please switch to expand@ and contract@ '
  207. 'labels.') % bad_labels)
  208. return
  209. script_name = os.path.basename(revision.path)
  210. log_error(
  211. _('Unexpected label for script %(script_name)s: %(labels)s') %
  212. {'script_name': script_name,
  213. 'labels': bad_labels}
  214. )
  215. def _validate_single_revision_labels(script_dir, revision, label=None):
  216. expected_labels = set()
  217. if label is not None:
  218. expected_labels.add(label)
  219. _compare_labels(revision, expected_labels)
  220. # if it's not the root element of the branch, expect the parent of the
  221. # script to have the same label
  222. if revision.down_revision is not None:
  223. down_revision = script_dir.get_revision(revision.down_revision)
  224. _compare_labels(down_revision, expected_labels)
  225. def _validate_revision(script_dir, revision):
  226. for branch in MIGRATION_BRANCHES:
  227. if branch in revision.path:
  228. _validate_single_revision_labels(
  229. script_dir, revision, label=branch)
  230. return
  231. # validate script from branchless part of migration rules
  232. _validate_single_revision_labels(script_dir, revision)
  233. def validate_revisions(config):
  234. script_dir = alembic_script.ScriptDirectory.from_config(config)
  235. revisions = _get_revisions(script_dir)
  236. for revision in revisions:
  237. _validate_revision(script_dir, revision)
  238. branchpoints = _get_branch_points(script_dir)
  239. if len(branchpoints) > 1:
  240. branchpoints = ', '.join(p.revision for p in branchpoints)
  241. log_error(
  242. _('Unexpected number of alembic branch points: %(branchpoints)s') %
  243. {'branchpoints': branchpoints}
  244. )
  245. def _get_revisions(script):
  246. return list(script.walk_revisions(base='base', head='heads'))
  247. def _get_branch_points(script):
  248. branchpoints = []
  249. for revision in _get_revisions(script):
  250. if revision.is_branch_point:
  251. branchpoints.append(revision)
  252. return branchpoints
  253. def _get_heads_map(config):
  254. script = alembic_script.ScriptDirectory.from_config(config)
  255. heads = script.get_heads()
  256. head_map = {}
  257. for head in heads:
  258. if CONTRACT_BRANCH in script.get_revision(head).branch_labels:
  259. head_map[CONTRACT_BRANCH] = head
  260. else:
  261. head_map[EXPAND_BRANCH] = head
  262. return head_map
  263. def _check_head(branch_name, head_file, head):
  264. try:
  265. with open(head_file) as file_:
  266. observed_head = file_.read().strip()
  267. except IOError:
  268. pass
  269. else:
  270. if observed_head != head:
  271. log_error(
  272. _('%(branch)s HEAD file does not match migration timeline '
  273. 'head, expected: %(head)s') % {'branch': branch_name.title(),
  274. 'head': head})
  275. def validate_head_files(config):
  276. '''Check that HEAD files contain the latest head for the branch.'''
  277. contract_head = _get_contract_head_file_path(config)
  278. expand_head = _get_expand_head_file_path(config)
  279. if not os.path.exists(contract_head) or not os.path.exists(expand_head):
  280. log_warning(_("Repository does not contain HEAD files for "
  281. "contract and expand branches."))
  282. return
  283. head_map = _get_heads_map(config)
  284. _check_head(CONTRACT_BRANCH, contract_head, head_map[CONTRACT_BRANCH])
  285. _check_head(EXPAND_BRANCH, expand_head, head_map[EXPAND_BRANCH])
  286. def update_head_files(config):
  287. '''Update HEAD files with the latest branch heads.'''
  288. head_map = _get_heads_map(config)
  289. contract_head = _get_contract_head_file_path(config)
  290. expand_head = _get_expand_head_file_path(config)
  291. with open(contract_head, 'w+') as f:
  292. f.write(head_map[CONTRACT_BRANCH] + '\n')
  293. with open(expand_head, 'w+') as f:
  294. f.write(head_map[EXPAND_BRANCH] + '\n')
  295. old_head_file = _get_head_file_path(config)
  296. old_heads_file = _get_heads_file_path(config)
  297. for file_ in (old_head_file, old_heads_file):
  298. fileutils.delete_if_exists(file_)
  299. def _get_current_database_heads(config):
  300. with DBConnection(config.neutron_config.database.connection) as conn:
  301. opts = {
  302. 'version_table': get_alembic_version_table(config)
  303. }
  304. context = alembic_migration.MigrationContext.configure(
  305. conn, opts=opts)
  306. return context.get_current_heads()
  307. def has_offline_migrations(config, cmd):
  308. heads_map = _get_heads_map(config)
  309. if heads_map[CONTRACT_BRANCH] not in _get_current_database_heads(config):
  310. # If there is at least one contract revision not applied to database,
  311. # it means we should shut down all neutron-server instances before
  312. # proceeding with upgrade.
  313. project = config.get_main_option('neutron_project')
  314. log_info(_('Need to apply migrations from %(project)s '
  315. 'contract branch. This will require all Neutron '
  316. 'server instances to be shutdown before '
  317. 'proceeding with the upgrade.') %
  318. {"project": project})
  319. return True
  320. return False
  321. def add_command_parsers(subparsers):
  322. for name in ['current', 'history', 'branches', 'heads']:
  323. parser = add_alembic_subparser(subparsers, name)
  324. parser.set_defaults(func=do_generic_show)
  325. parser.add_argument('--verbose',
  326. action='store_true',
  327. help='Display more verbose output for the '
  328. 'specified command')
  329. help_text = (getattr(alembic_command, 'branches').__doc__ +
  330. ' and validate head file')
  331. parser = subparsers.add_parser('check_migration', help=help_text)
  332. parser.set_defaults(func=do_check_migration)
  333. parser = add_alembic_subparser(subparsers, 'upgrade')
  334. parser.add_argument('--delta', type=int)
  335. parser.add_argument('--sql', action='store_true')
  336. parser.add_argument('revision', nargs='?')
  337. parser.add_argument('--mysql-engine',
  338. default='',
  339. help='Change MySQL storage engine of current '
  340. 'existing tables')
  341. add_branch_options(parser)
  342. parser.set_defaults(func=do_upgrade)
  343. parser = subparsers.add_parser('downgrade', help="(No longer supported)")
  344. parser.add_argument('None', nargs='?', help="Downgrade not supported")
  345. parser.set_defaults(func=no_downgrade)
  346. parser = add_alembic_subparser(subparsers, 'stamp')
  347. parser.add_argument('--sql', action='store_true')
  348. parser.add_argument('revision')
  349. parser.set_defaults(func=do_stamp)
  350. parser = add_alembic_subparser(subparsers, 'revision')
  351. parser.add_argument('-m', '--message')
  352. parser.add_argument('--sql', action='store_true')
  353. group = add_branch_options(parser)
  354. group.add_argument('--autogenerate', action='store_true')
  355. parser.set_defaults(func=do_revision)
  356. parser = subparsers.add_parser(
  357. 'has_offline_migrations',
  358. help='Determine whether there are pending migration scripts that '
  359. 'require full shutdown for all services that directly access '
  360. 'database.')
  361. parser.set_defaults(func=has_offline_migrations)
  362. command_opt = cfg.SubCommandOpt('command',
  363. title='Command',
  364. help=_('Available commands'),
  365. handler=add_command_parsers)
  366. CONF.register_cli_opt(command_opt)
  367. def _get_project_base(config):
  368. '''Return the base python namespace name for a project.'''
  369. script_location = config.get_main_option('script_location')
  370. return script_location.split(':')[0].split('.')[0]
  371. def _get_package_root_dir(config):
  372. root_module = importutils.try_import(_get_project_base(config))
  373. if not root_module:
  374. project = config.get_main_option('neutron_project')
  375. log_error(_("Failed to locate source for %s.") % project)
  376. # The root_module.__file__ property is a path like
  377. # '/opt/stack/networking-foo/networking_foo/__init__.py'
  378. # We return just
  379. # '/opt/stack/networking-foo'
  380. return os.path.dirname(os.path.dirname(root_module.__file__))
  381. def _get_root_versions_dir(config):
  382. '''Return root directory that contains all migration rules.'''
  383. root_dir = _get_package_root_dir(config)
  384. script_location = config.get_main_option('script_location')
  385. # Script location is something like:
  386. # 'project_base.db.migration:alembic_migrations'
  387. # Convert it to:
  388. # 'project_base/db/migration/alembic_migrations/versions'
  389. part1, part2 = script_location.split(':')
  390. parts = part1.split('.') + part2.split('.') + ['versions']
  391. # Return the absolute path to the versions dir
  392. return os.path.join(root_dir, *parts)
  393. def _get_head_file_path(config):
  394. '''Return the path of the file that contains single head.'''
  395. return os.path.join(
  396. _get_root_versions_dir(config),
  397. HEAD_FILENAME)
  398. def _get_heads_file_path(config):
  399. '''Get heads file path
  400. Return the path of the file that was once used to maintain the list of
  401. latest heads.
  402. '''
  403. return os.path.join(
  404. _get_root_versions_dir(config),
  405. HEADS_FILENAME)
  406. def _get_contract_head_file_path(config):
  407. '''Return the path of the file that is used to maintain contract head'''
  408. return os.path.join(
  409. _get_root_versions_dir(config),
  410. CONTRACT_HEAD_FILENAME)
  411. def _get_expand_head_file_path(config):
  412. '''Return the path of the file that is used to maintain expand head'''
  413. return os.path.join(
  414. _get_root_versions_dir(config),
  415. EXPAND_HEAD_FILENAME)
  416. def _get_version_branch_path(config, release=None, branch=None):
  417. version_path = _get_root_versions_dir(config)
  418. if branch and release:
  419. return os.path.join(version_path, release, branch)
  420. return version_path
  421. def _set_version_locations(config):
  422. '''Make alembic see all revisions in all migration branches.'''
  423. split_branches = False
  424. version_paths = [_get_version_branch_path(config)]
  425. for release in RELEASES:
  426. for branch in MIGRATION_BRANCHES:
  427. version_path = _get_version_branch_path(config, release, branch)
  428. if split_branches or os.path.exists(version_path):
  429. split_branches = True
  430. version_paths.append(version_path)
  431. config.set_main_option('version_locations', ' '.join(version_paths))
  432. def _get_installed_entrypoint(subproject):
  433. '''Get the entrypoint for the subproject, which must be installed.'''
  434. if subproject not in migration_cli.migration_entrypoints:
  435. log_error(_('Package %s not installed') % subproject)
  436. return migration_cli.migration_entrypoints[subproject]
  437. def _get_subproject_script_location(subproject):
  438. '''Get the script location for the installed subproject.'''
  439. entrypoint = _get_installed_entrypoint(subproject)
  440. return ':'.join([entrypoint.module_name, entrypoint.attrs[0]])
  441. def _get_subproject_base(subproject):
  442. '''Get the import base name for the installed subproject.'''
  443. entrypoint = _get_installed_entrypoint(subproject)
  444. return entrypoint.module_name.split('.')[0]
  445. def get_alembic_version_table(config):
  446. script_dir = alembic_script.ScriptDirectory.from_config(config)
  447. alembic_version_table = [None]
  448. def alembic_version_table_from_env(rev, context):
  449. alembic_version_table[0] = context.version_table
  450. return []
  451. with environment.EnvironmentContext(config, script_dir,
  452. fn=alembic_version_table_from_env):
  453. script_dir.run_env()
  454. return alembic_version_table[0]
  455. def get_alembic_configs():
  456. '''Return a list of alembic configs, one per project.
  457. '''
  458. # Get the script locations for the specified or installed projects.
  459. # Which projects to get script locations for is determined by the CLI
  460. # options as follows:
  461. # --subproject P # only subproject P (where P can be neutron)
  462. # (none specified) # neutron and all installed subprojects
  463. script_locations = {}
  464. if CONF.subproject:
  465. script_location = _get_subproject_script_location(CONF.subproject)
  466. script_locations[CONF.subproject] = script_location
  467. else:
  468. for subproject in migration_cli.migration_entrypoints:
  469. script_locations[subproject] = _get_subproject_script_location(
  470. subproject)
  471. # Return a list of alembic configs from the projects in the
  472. # script_locations dict. If neutron is in the list it is first.
  473. configs = []
  474. project_seq = sorted(script_locations.keys())
  475. # Core neutron must be the first project if there is more than one
  476. if len(project_seq) > 1 and 'neutron' in project_seq:
  477. project_seq.insert(0, project_seq.pop(project_seq.index('neutron')))
  478. for project in project_seq:
  479. config = alembic_config.Config(neutron_alembic_ini)
  480. config.set_main_option('neutron_project', project)
  481. script_location = script_locations[project]
  482. config.set_main_option('script_location', script_location)
  483. _set_version_locations(config)
  484. config.neutron_config = CONF
  485. configs.append(config)
  486. return configs
  487. def get_neutron_config():
  488. # Neutron's alembic config is always the first one
  489. return get_alembic_configs()[0]
  490. def run_sanity_checks(config, revision):
  491. script_dir = alembic_script.ScriptDirectory.from_config(config)
  492. def check_sanity(rev, context):
  493. # TODO(ihrachyshka): here we use internal API for alembic; we may need
  494. # alembic to expose implicit_base= argument into public
  495. # iterate_revisions() call
  496. for script in script_dir.revision_map.iterate_revisions(
  497. revision, rev, implicit_base=True):
  498. if hasattr(script.module, 'check_sanity'):
  499. script.module.check_sanity(context.connection)
  500. return []
  501. with environment.EnvironmentContext(config, script_dir,
  502. fn=check_sanity,
  503. starting_rev=None,
  504. destination_rev=revision):
  505. script_dir.run_env()
  506. def get_engine_config():
  507. return [obj for obj in migration_cli.DB_OPTS if obj.name == 'engine']
  508. def main():
  509. # Interpret the config file for Python logging.
  510. # This line sets up loggers basically.
  511. logging_config.fileConfig(neutron_alembic_ini)
  512. CONF(project='neutron')
  513. return_val = False
  514. for config in get_alembic_configs():
  515. # TODO(gongysh) enable logging
  516. return_val |= bool(CONF.command.func(config, CONF.command.name))
  517. if CONF.command.name == 'has_offline_migrations' and not return_val:
  518. log_info(_('No offline migrations pending.'))
  519. return return_val