#!/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: ocata
"""


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
        },
        'neutron-dynamic-routing': {
            'branches': {
                'expand': {
                    'revision': None,
                    'head': None
                },
                'contract': {
                    'revision': None,
                    'head': None
                }
            },
            'installed': False
        }
    },
    'run_contract': True,
    'run_expand': True
}

RELEASES = ['ocata', '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()
