#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2013, John Dewey <john@dewey.ws>
#
# This module is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this software.  If not, see <http://www.gnu.org/licenses/>.

import locale
import os
import six

try:
    from novaclient.openstack.common import uuidutils
    from novaclient.openstack.common import strutils
    from novaclient.v1_1 import client
    from novaclient.v1_1 import security_groups
    from novaclient.v1_1 import security_group_rules
    from novaclient import exceptions
except ImportError:
    print("failed=True msg='novaclient is required for this module to work'")

DOCUMENTATION = '''
---
module: security_group
version_added: "1.6"
short_description: Maintain nova security groups.
description:
  - Manage nova security groups using the python-novaclient library.
options:

  login_username:
    description:
        - Login username to authenticate to keystone.  If not set then the value of the OS_USERNAME environment variable is used.
    required: false
    default: None
  login_password:
    description:
      - Password of login user.  If not set then the value of the OS_PASSWORD environment variable is used.
    required: false
    default: None
  login_tenant_name:
    description:
      - The tenant name of the login user.  If not set then the value of the OS_TENANT_NAME environment variable is used.
    required: false
    default: None
  auth_url:
    description:
      - The keystone url for authentication.  If not set then the value of the OS_AUTH_URL environment variable is used.
    required: false
    default: None
  region_name:
    description:
      - Name of the region.
    required: false
    default: None
  name:
    description:
      - Name of the security group.
    required: true
  description:
    description:
      - Description of the security group.
    required: true
  rules:
    description:
      - List of firewall rules to enforce in this group (see example).
        Must specify either an IPv4 'cidr' address or 'group' UUID.
    required: true
  state:
    description:
      - Indicate desired state of the resource.
    choices: ['present', 'absent']
    required: false
    default: 'present'

requirements: ["novaclient"]
'''

EXAMPLES = '''
- name: create example group and rules
  local_action:
    module: security_group
    name: example
    description: an example nova group
    rules:
      - ip_protocol: tcp
        from_port: 80
        to_port: 80
        cidr: 0.0.0.0/0
      - ip_protocol: tcp
        from_port: 3306
        to_port: 3306
        group: "{{ group_uuid }}"
      - ip_protocol: icmp
        from_port: -1
        to_port: -1
        cidr: 0.0.0.0/0

- name: delete rule from example group
  local_action:
    module: security_group
    name: example
    description: an example nova group
    rules:
      - ip_protocol: tcp
        from_port: 80
        to_port: 80
        cidr: 0.0.0.0/0
      - ip_protocol: icmp
        from_port: -1
        to_port: -1
        cidr: 0.0.0.0/0
        state: absent
'''

class NovaGroup(object):
    def __init__(self, client):
        self._sg = security_groups.SecurityGroupManager(client)

    # Taken from novaclient/v1_1/shell.py.
    def _get_secgroup(self, secgroup):
        # Check secgroup is an UUID
        if uuidutils.is_uuid_like(strutils.safe_encode(secgroup)):
            try:
                sg = self._sg.get(secgroup)
                return sg
            except exceptions.NotFound:
                return False

        # Check secgroup as a name
        for s in self._sg.list():
            encoding = (locale.getpreferredencoding() or
                sys.stdin.encoding or
                'UTF-8')
            if not six.PY3:
                s.name = s.name.encode(encoding)
            if secgroup == s.name:
                return s
        return False


class SecurityGroup(NovaGroup):
    def __init__(self, client, module):
        super(SecurityGroup, self).__init__(client)
        self._module = module
        self._name = module.params.get('name')
        self._description = module.params.get('description')

    def get(self):
        return self._get_secgroup(self._name)

    def create(self):
        return self._sg.create(self._name, self._description)

    def delete(self):
        return self._sg.delete(self._name)


class SecurityGroupRule(NovaGroup):
    def __init__(self, client, module):
        super(SecurityGroupRule, self).__init__(client)
        self._module = module
        self._name = module.params.get('name')
        self._rules = module.params.get('rules')
        self._validate_rules()
        self._sgr = security_group_rules.SecurityGroupRuleManager(client)
        self._secgroup = self._get_secgroup(self._name)
        self._current_rules = self._lookup_dict(self._secgroup.rules)

    def _concat_security_group_rule(self, rule):
        """
        Normalize the given rule into a string in the format of:
          protocol-from_port-to_port-group
        The `group` needs a bit of massaging.
          1. If an empty dict -- return None.
          2. If a dict -- lookup group UUID (novaclient only returns the name).
          3. Return `group` from rules dict.

        :param rule: A novaclient SecurityGroupRule object.
        """
        group = rule.get('group')
        # Oddly novaclient occasionaly returns None as {}.
        if group is not None and not any(group):
            group = None
        elif type(group) == dict:
            g = group.get('name')
            group = self._get_secgroup(g)
        r = "%s-%s-%s-%s" % (rule.get('ip_protocol'),
                             rule.get('from_port'),
                             rule.get('to_port'),
                             group)
        return r

    def _lookup_dict(self, rules):
        """
        Populate a dict with current rules.

        :param rule: A novaclient SecurityGroupRule object.
        """
        return {self._concat_security_group_rule(rule): rule for rule in rules}

    def _get_rule(self, rule):
        """
        Return rule when found and False when not.

        :param rule: A novaclient SecurityGroupRule object.
        """
        r = self._concat_security_group_rule(rule)
        if r in self._current_rules:
            return self._current_rules[r]

    def _validate_rules(self):
        for rule in self._rules:
            if 'group' in rule and 'cidr' in rule:
                self._module.fail_json(msg="Specify group OR cidr")

    def create(self):
        changed = False
        filtered = [rule for rule in self._rules
            if rule.get('state') != 'absent']
        for rule in filtered:
            if not self._get_rule(rule):
                if 'cidr' in rule:
                    self._sgr.create(self._secgroup.id,
                                     rule.get('ip_protocol'),
                                     rule.get('from_port'),
                                     rule.get('to_port'),
                                     cidr=rule.get('cidr'))
                    changed = True
                if 'group' in rule:
                    self._sgr.create(self._secgroup.id,
                                     rule.get('ip_protocol'),
                                     rule.get('from_port'),
                                     rule.get('to_port'),
                                     group_id=rule.get('group'))
                    changed = True
        return changed

    def delete(self):
        changed = False
        filtered = [rule for rule in self._rules
            if rule.get('state') == 'absent']
        for rule in filtered:
            r = self._get_rule(rule)
            if r:
                self._sgr.delete(r.get('id'))
                changed = True
        return changed

    def update(self):
        changed = False
        if self.create():
            changed = True
        if self.delete():
            changed = True
        return changed


def main():
    module = AnsibleModule(
        argument_spec=dict(
            name=dict(required=True),
            description=dict(required=True),
            rules=dict(),
            login_username=dict(),
            login_password=dict(no_log=True),
            login_tenant_name=dict(),
            auth_url= dict(),
            region_name=dict(default=None),
            state = dict(default='present', choices=['present', 'absent']),
        ),
        supports_check_mode=False,
    )
    login_username = module.params.get('login_username')
    login_password = module.params.get('login_password')
    login_tenant_name = module.params.get('login_tenant_name')
    auth_url = module.params.get('auth_url')

    # allow stackrc environment variables to be used if ansible vars aren't set
    if not login_username and 'OS_USERNAME' in os.environ:
        login_username = os.environ['OS_USERNAME']

    if not login_password and 'OS_PASSWORD' in os.environ:
        login_password = os.environ['OS_PASSWORD']

    if not login_tenant_name and 'OS_TENANT_NAME' in os.environ:
        login_tenant_name = os.environ['OS_TENANT_NAME']

    if not auth_url and 'OS_AUTH_URL' in os.environ:
        auth_url = os.environ['OS_AUTH_URL']

    nova = client.Client(login_username,
                         login_password,
                         login_tenant_name,
                         auth_url,
                         service_type='compute')
    try:
        nova.authenticate()
    except exceptions.Unauthorized as e:
        module.fail_json(msg="Invalid OpenStack Nova credentials.: %s" % e.message)
    except exceptions.AuthorizationFailure as e:
        module.fail_json(msg="Unable to authorize user: %s" % e.message)

    rules = module.params.get('rules')
    state = module.params.get('state')
    security_group = SecurityGroup(nova, module)

    changed = False
    group_id = None
    group = security_group.get()
    if group:
        group_id = group.id
        if state == 'absent':
            security_group.delete()
            changed = True
    elif state == 'present':
        group = security_group.create()
        changed = True
        group_id = group.id

    if rules is not None:
        security_group_rules = SecurityGroupRule(nova, module)
        if security_group_rules.update():
            changed = True

    module.exit_json(changed=changed, group_id=group_id)


# import module snippets
from ansible.module_utils.basic import *

main()