#!/usr/bin/env python # Copyright 2015, Rackspace US, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import re import subprocess from ansible.module_utils.basic import * DOCUMENTATION = """ --- module: neutron_migrations_facts short_description: - A module for gathering neutron migrations facts. description: - This module creates a fact called 'neutron_migrations', which is a dict containing keys that represent each alembic migration branch. The value for each key is another dict, containing a key value for the current neutron revision for that branch (revision), and whether or not the branch is currently at the latest revision (head). The 'neutron_migrations' fact can then be used to determine if migrations for either branch are required, and allows us to only apply migrations if necessary. options: release: description: - This is the OpenStack release you're running, used when searching for migration revisions in the neutron code. default: liberty library_path: description: - Local path to the location where the neutron python package is installed. default: /usr/local/lib/python2.7/dist-packages/neutron bin_path: description: - Local path to the where the neutron binaries are. default: /usr/local/bin author: Rcbops """ EXAMPLES = """ - name: Gather neutron migration facts neutron_migrations_facts: release: newton """ MIGRATIONS = { 'projects': { 'neutron': { 'branches': { 'expand': { 'revision': None, 'head': None }, 'contract': { 'revision': None, 'head': None } }, 'installed': False }, 'neutron-fwaas': { 'branches': { 'expand': { 'revision': None, 'head': None }, 'contract': { 'revision': None, 'head': None } }, 'installed': False }, 'neutron-lbaas': { 'branches': { 'expand': { 'revision': None, 'head': None }, 'contract': { 'revision': None, 'head': None } }, 'installed': False }, 'neutron-vpnaas': { 'branches': { 'expand': { 'revision': None, 'head': None }, 'contract': { 'revision': None, 'head': None } }, 'installed': False } }, 'run_contract': True, 'run_expand': True } RELEASES = ['newton', 'mitaka', 'liberty'] def get_branch(release, revision, library_path, project): # Here we drop any releases that are newer than the specified release. # We have to check previous releases for migrations as at time of writing, # neutron-lbaas doesn't have mitaka migrations and therefore the plugin # will fail unless we also check the previous release's migrations. for release in RELEASES[RELEASES.index(release):]: migrations_dir = ( '%s/%s/db/migration/alembic_migrations/versions/%s/' % ( library_path, project.replace('-', '_'), release, ) ) if os.path.isdir(migrations_dir): for branch in MIGRATIONS['projects'][project]['branches'].keys(): migration_dir = os.path.join( get_abs_path(migrations_dir), branch ) # If a release has no migrations for a given branch, the branch # directory will not exist. if os.path.isdir(migration_dir): for file in os.listdir(migration_dir): if (file.endswith('.py') and file.split('_')[0] == revision): return branch def get_abs_path(path): return os.path.abspath( os.path.expanduser( path ) ) def update_run_keys(): projects = MIGRATIONS['projects'] # If a probject or subproject is marked as installed but has no revision, # we can assume the project has just been installed. In this case we # just return to main, leaving run_contract/run_expand as True so that # migrations will run. # TODO(mattt): We will want to try to figure out if migrations in this # situation are expand, contract, or both, so that we avoid # unnecessary neutron-server downtime. for p in projects: if (projects[p]['installed'] is True and projects[p]['branches']['expand']['revision'] is None and projects[p]['branches']['contract']['revision'] is None): return # Here we will determine if migrations should be skipped -- this will # happen when all the projects or subprojects that have a revision # are also at head. for b in ('contract', 'expand'): migrations_with_a_rev = [ v for v in projects.values() if v['branches'][b]['revision'] is not None ] if (len(migrations_with_a_rev) > 0 and all(m['branches'][b]['head'] is True for m in migrations_with_a_rev)): MIGRATIONS['run_%s' % b] = False def main(): module = AnsibleModule( argument_spec=dict( release=dict( type='str', default='liberty' ), library_path=dict( type='str', default='/usr/local/lib/python2.7/dist-packages/neutron' ), bin_path=dict( type='str', default='/usr/local/bin' ) ), supports_check_mode=False ) if module.params['release'] not in RELEASES: module.fail_json( msg='neutron fact collection failed: release %s not found in' ' %s' % (module.params['release'], RELEASES) ) state_change = False project = None command = [ '%s/neutron-db-manage' % get_abs_path(module.params['bin_path']), 'current' ] try: current = subprocess.check_output(command) except subprocess.CalledProcessError as e: module.fail_json(msg='neutron fact collection failed: "%s".' % e) for line in current.splitlines(): head = False project_match = re.search( "^\s*Running current for (neutron(-[a-z]+)?) ...$", line ) migration_match = re.search("^([0-9a-z]{4,12})(\s\(head\))?$", line) if project_match: project = project_match.group(1) MIGRATIONS['projects'][project]['installed'] = True if migration_match: revision = migration_match.group(1) if project is None: module.fail_json( msg='neutron fact collection failed: unable to detect' ' project for revision %s' % revision ) if migration_match.group(2): head = True branch = get_branch( release=module.params['release'], revision=revision, library_path=get_abs_path(module.params['library_path']), project=project ) if branch is None: module.fail_json( msg='neutron fact collection failed: unable to find' ' migration with revision %s' % revision ) proj_migrations = MIGRATIONS['projects'][project]['branches'] proj_migrations[branch]['revision'] = revision proj_migrations[branch]['head'] = head update_run_keys() module.exit_json( changed=state_change, ansible_facts={'neutron_migrations': MIGRATIONS} ) if __name__ == '__main__': main()