#!/usr/bin/python
# This file is part of Ansible
#
# Ansible 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.
#
# Ansible 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 Ansible.  If not, see <http://www.gnu.org/licenses/>.

ANSIBLE_METADATA = {'metadata_version': '1.0',
                    'status': ['preview'],
                    'supported_by': 'community'}


DOCUMENTATION = '''
---
module: proxmox
short_description: management of instances in Proxmox VE cluster
description:
  - allows you to create/delete/stop instances in Proxmox VE cluster
  - Starting in Ansible 2.1, it automatically detects containerization type (lxc for PVE 4, openvz for older)
version_added: "2.0"
options:
  api_host:
    description:
      - the host of the Proxmox VE cluster
    required: true
  api_user:
    description:
      - the user to authenticate with
    required: true
  api_password:
    description:
      - the password to authenticate with
      - you can use PROXMOX_PASSWORD environment variable
    default: null
    required: false
  vmid:
    description:
      - the instance id
      - if not set, the next available VM ID will be fetched from ProxmoxAPI.
      - if not set, will be fetched from PromoxAPI based on the hostname
    default: null
    required: false
  validate_certs:
    description:
      - enable / disable https certificate verification
    default: false
    required: false
    type: bool
  node:
    description:
      - Proxmox VE node, when new VM will be created
      - required only for C(state=present)
      - for another states will be autodiscovered
    default: null
    required: false
  pool:
    description:
      - Proxmox VE resource pool
    default: null
    required: false
    version_added: "2.3"
  password:
    description:
      - the instance root password
      - required only for C(state=present)
    default: null
    required: false
  hostname:
    description:
      - the instance hostname
      - required only for C(state=present)
      - must be unique if vmid is not passed
    default: null
    required: false
  ostemplate:
    description:
      - the template for VM creating
      - required only for C(state=present)
    default: null
    required: false
  disk:
    description:
      - hard disk size in GB for instance
    default: 3
    required: false
  cpus:
    description:
      - numbers of allocated cpus for instance
    default: 1
    required: false
  memory:
    description:
      - memory size in MB for instance
    default: 512
    required: false
  swap:
    description:
      - swap memory size in MB for instance
    default: 0
    required: false
  netif:
    description:
      - specifies network interfaces for the container. As a hash/dictionary defining interfaces.
    default: null
    required: false
  mounts:
    description:
      - specifies additional mounts (separate disks) for the container. As a hash/dictionary defining mount points
    default: null
    required: false
    version_added: "2.2"
  ip_address:
    description:
      - specifies the address the container will be assigned
    default: null
    required: false
  onboot:
    description:
      - specifies whether a VM will be started during system bootup
    default: false
    required: false
  storage:
    description:
      - target storage
    default: 'local'
    required: false
  cpuunits:
    description:
      - CPU weight for a VM
    default: 1000
    required: false
  nameserver:
    description:
      - sets DNS server IP address for a container
    default: null
    required: false
  searchdomain:
    description:
      - sets DNS search domain for a container
    default: null
    required: false
  timeout:
    description:
      - timeout for operations
    default: 30
    required: false
  force:
    description:
      - forcing operations
      - can be used only with states C(present), C(stopped), C(restarted)
      - with C(state=present) force option allow to overwrite existing container
      - with states C(stopped) , C(restarted) allow to force stop instance
    default: false
    required: false
    type: bool
  state:
    description:
     - Indicate desired state of the instance
    choices: ['present', 'started', 'absent', 'stopped', 'restarted']
    default: present
  pubkey:
    description:
      - Public key to add to /root/.ssh/authorized_keys. This was added on Proxmox 4.2, it is ignored for earlier versions
    version_added: "2.3"
    default: null
  unprivileged:
    version_added: "2.3"
    description:
      - Indicate if the container should be unprivileged
    default: false
    required: false

notes:
  - Requires proxmoxer and requests modules on host. This modules can be installed with pip.
requirements: [ "proxmoxer", "python >= 2.7", "requests" ]
author: "Sergei Antipov @UnderGreen"
'''

EXAMPLES = '''
# Create new container with minimal options
- proxmox:
    vmid: 100
    node: uk-mc02
    api_user: root@pam
    api_password: 1q2w3e
    api_host: node1
    password: 123456
    hostname: example.org
    ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz'

# Create new container automatically selecting the next available vmid.
- proxmox: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz'

# Create new container with minimal options with force(it will rewrite existing container)
- proxmox:
    vmid: 100
    node: uk-mc02
    api_user: root@pam
    api_password: 1q2w3e
    api_host: node1
    password: 123456
    hostname: example.org
    ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz'
    force: yes

# Create new container with minimal options use environment PROXMOX_PASSWORD variable(you should export it before)
- proxmox:
    vmid: 100
    node: uk-mc02
    api_user: root@pam
    api_host: node1
    password: 123456
    hostname: example.org
    ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz'

# Create new container with minimal options defining network interface with dhcp
- proxmox:
    vmid: 100
    node: uk-mc02
    api_user: root@pam
    api_password: 1q2w3e
    api_host: node1
    password: 123456
    hostname: example.org
    ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz'
    netif: '{"net0":"name=eth0,ip=dhcp,ip6=dhcp,bridge=vmbr0"}'

# Create new container with minimal options defining network interface with static ip
- proxmox:
    vmid: 100
    node: uk-mc02
    api_user: root@pam
    api_password: 1q2w3e
    api_host: node1
    password: 123456
    hostname: example.org
    ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz'
    netif: '{"net0":"name=eth0,gw=192.168.0.1,ip=192.168.0.2/24,bridge=vmbr0"}'

# Create new container with minimal options defining a mount
- proxmox:
    vmid: 100
    node: uk-mc02
    api_user: root@pam
    api_password: 1q2w3e
    api_host: node1
    password: 123456
    hostname: example.org
    ostemplate: local:vztmpl/ubuntu-14.04-x86_64.tar.gz'
    mounts: '{"mp0":"local:8,mp=/mnt/test/"}'

# Start container
- proxmox:
    vmid: 100
    api_user: root@pam
    api_password: 1q2w3e
    api_host: node1
    state: started

# Stop container
- proxmox:
    vmid: 100
    api_user: root@pam
    api_password: 1q2w3e
    api_host: node1
    state: stopped

# Stop container with force
- proxmox:
    vmid: 100
    api_user: root@pam
    api_password: 1q2w3e
    api_host: node1
    force: yes
    state: stopped

# Restart container(stopped or mounted container you can't restart)
- proxmox:
    vmid: 100
    api_user: root@pam
    api_password: 1q2w3e
    api_host: node1
    state: stopped

# Remove container
- proxmox:
    vmid: 100
    api_user: root@pam
    api_password: 1q2w3e
    api_host: node1
    state: absent
'''

import os
import time

try:
  from proxmoxer import ProxmoxAPI
  HAS_PROXMOXER = True
except ImportError:
  HAS_PROXMOXER = False

VZ_TYPE=None

def get_nextvmid(proxmox):
  try:
    vmid = proxmox.cluster.nextid.get()
    return vmid
  except Exception as e:
    module.fail_json(msg="Unable to get next vmid. Failed with exception: %s")

def get_vmid(proxmox, hostname):
    return [ vm['vmid'] for vm in proxmox.cluster.resources.get(type='vm') if vm['name'] == hostname ]

def get_instance(proxmox, vmid):
  return [ vm for vm in proxmox.cluster.resources.get(type='vm') if vm['vmid'] == int(vmid) ]

def content_check(proxmox, node, ostemplate, template_store):
  return [ True for cnt in proxmox.nodes(node).storage(template_store).content.get() if cnt['volid'] == ostemplate ]

def node_check(proxmox, node):
  return [ True for nd in proxmox.nodes.get() if nd['node'] == node ]

def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, **kwargs):
  proxmox_node = proxmox.nodes(node)
  kwargs = dict((k,v) for k, v in kwargs.items() if v is not None)

  if VZ_TYPE =='lxc':
      kwargs['cpulimit']=cpus
      kwargs['rootfs']=disk
      if 'netif' in kwargs:
        kwargs.update(kwargs['netif'])
        del kwargs['netif']
      if 'mounts' in kwargs:
        kwargs.update(kwargs['mounts'])
        del kwargs['mounts']
      if 'pubkey' in kwargs:
        if float(proxmox.version.get()['version']) >= 4.2:
          kwargs['ssh-public-keys'] = kwargs['pubkey']
        del kwargs['pubkey']
  else:
      kwargs['cpus']=cpus
      kwargs['disk']=disk

  taskid = getattr(proxmox_node, VZ_TYPE).create(vmid=vmid, storage=storage, memory=memory, swap=swap, **kwargs)

  while timeout:
    if ( proxmox_node.tasks(taskid).status.get()['status'] == 'stopped'
        and proxmox_node.tasks(taskid).status.get()['exitstatus'] == 'OK' ):
      return True
    timeout = timeout - 1
    if timeout == 0:
      module.fail_json(msg='Reached timeout while waiting for creating VM. Last line in task before timeout: %s'
                       % proxmox_node.tasks(taskid).log.get()[:1])

    time.sleep(1)
  return False

def start_instance(module, proxmox, vm, vmid, timeout):
  taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.start.post()
  while timeout:
    if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped'
        and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ):
      return True
    timeout = timeout - 1
    if timeout == 0:
      module.fail_json(msg='Reached timeout while waiting for starting VM. Last line in task before timeout: %s'
                       % proxmox.nodes(vm[0]['node']).tasks(taskid).log.get()[:1])

    time.sleep(1)
  return False

def stop_instance(module, proxmox, vm, vmid, timeout, force):
  if force:
    taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.shutdown.post(forceStop=1)
  else:
    taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.shutdown.post()
  while timeout:
    if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped'
        and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ):
      return True
    timeout = timeout - 1
    if timeout == 0:
      module.fail_json(msg='Reached timeout while waiting for stopping VM. Last line in task before timeout: %s'
                       % proxmox_node.tasks(taskid).log.get()[:1])

    time.sleep(1)
  return False

def umount_instance(module, proxmox, vm, vmid, timeout):
  taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.umount.post()
  while timeout:
    if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped'
        and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ):
      return True
    timeout = timeout - 1
    if timeout == 0:
      module.fail_json(msg='Reached timeout while waiting for unmounting VM. Last line in task before timeout: %s'
                       % proxmox_node.tasks(taskid).log.get()[:1])

    time.sleep(1)
  return False

def main():
  module = AnsibleModule(
      argument_spec = dict(
          api_host = dict(required=True),
          api_user = dict(required=True),
          api_password = dict(no_log=True),
          vmid = dict(required=False),
          validate_certs = dict(type='bool', default='no'),
          node = dict(),
          pool = dict(),
          password = dict(no_log=True),
          hostname = dict(),
          ostemplate = dict(),
          disk = dict(type='str', default='3'),
          cpus = dict(type='int', default=1),
          memory = dict(type='int', default=512),
          swap = dict(type='int', default=0),
          netif = dict(type='dict'),
          mounts = dict(type='dict'),
          ip_address = dict(),
          onboot = dict(type='bool', default='no'),
          storage = dict(default='local'),
          cpuunits = dict(type='int', default=1000),
          nameserver = dict(),
          searchdomain = dict(),
          timeout = dict(type='int', default=30),
          force = dict(type='bool', default='no'),
          state = dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted']),
          pubkey = dict(type='str', default=None),
          unprivileged = dict(type='bool', default='no')
          )
  )

  if not HAS_PROXMOXER:
    module.fail_json(msg='proxmoxer required for this module')

  state = module.params['state']
  api_user = module.params['api_user']
  api_host = module.params['api_host']
  api_password = module.params['api_password']
  vmid = module.params['vmid']
  validate_certs = module.params['validate_certs']
  node = module.params['node']
  disk = module.params['disk']
  cpus = module.params['cpus']
  memory = module.params['memory']
  swap = module.params['swap']
  storage = module.params['storage']
  hostname = module.params['hostname']
  if module.params['ostemplate'] is not None:
    template_store = module.params['ostemplate'].split(":")[0]
  timeout = module.params['timeout']

  # If password not set get it from PROXMOX_PASSWORD env
  if not api_password:
    try:
      api_password = os.environ['PROXMOX_PASSWORD']
    except KeyError as e:
      module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable')

  try:
    proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs)
    global VZ_TYPE
    VZ_TYPE = 'openvz' if float(proxmox.version.get()['version']) < 4.0 else 'lxc'

  except Exception as e:
    module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e)

  # If vmid not set get the Next VM id from ProxmoxAPI
  # If hostname is set get the VM id from ProxmoxAPI
  if not vmid and state == 'present':
    vmid = get_nextvmid(proxmox)
  elif not vmid and hostname:
    vmid = get_vmid(proxmox, hostname)[0]
  elif not vmid:
    module.exit_json(changed=False, msg="Vmid could not be fetched for the following action: %s" % state)

  if state == 'present':
    try:
      if get_instance(proxmox, vmid) and not module.params['force']:
        module.exit_json(changed=False, msg="VM with vmid = %s is already exists" % vmid)
      # If no vmid was passed, there cannot be another VM named 'hostname'
      if not module.params['vmid'] and get_vmid(proxmox, hostname) and not module.params['force']:
        module.exit_json(changed=False, msg="VM with hostname %s already exists and has ID number %s" % (hostname, get_vmid(proxmox, hostname)[0]))
      elif not (node, module.params['hostname'] and module.params['password'] and module.params['ostemplate']):
        module.fail_json(msg='node, hostname, password and ostemplate are mandatory for creating vm')
      elif not node_check(proxmox, node):
        module.fail_json(msg="node '%s' not exists in cluster" % node)
      elif not content_check(proxmox, node, module.params['ostemplate'], template_store):
        module.fail_json(msg="ostemplate '%s' not exists on node %s and storage %s"
                         % (module.params['ostemplate'], node, template_store))

      create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout,
                      pool = module.params['pool'],
                      password = module.params['password'],
                      hostname = module.params['hostname'],
                      ostemplate = module.params['ostemplate'],
                      netif = module.params['netif'],
                      mounts = module.params['mounts'],
                      ip_address = module.params['ip_address'],
                      onboot = int(module.params['onboot']),
                      cpuunits = module.params['cpuunits'],
                      nameserver = module.params['nameserver'],
                      searchdomain = module.params['searchdomain'],
                      force = int(module.params['force']),
                      pubkey = module.params['pubkey'],
                      unprivileged = int(module.params['unprivileged']))

      module.exit_json(changed=True, msg="deployed VM %s from template %s"  % (vmid, module.params['ostemplate']))
    except Exception as e:
      module.fail_json(msg="creation of %s VM %s failed with exception: %s" % ( VZ_TYPE, vmid, e ))

  elif state == 'started':
    try:
      vm = get_instance(proxmox, vmid)
      if not vm:
        module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)
      if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'running':
        module.exit_json(changed=False, msg="VM %s is already running" % vmid)

      if start_instance(module, proxmox, vm, vmid, timeout):
        module.exit_json(changed=True, msg="VM %s started" % vmid)
    except Exception as e:
      module.fail_json(msg="starting of VM %s failed with exception: %s" % ( vmid, e ))

  elif state == 'stopped':
    try:
      vm = get_instance(proxmox, vmid)
      if not vm:
        module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)

      if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted':
        if module.params['force']:
          if umount_instance(module, proxmox, vm, vmid, timeout):
            module.exit_json(changed=True, msg="VM %s is shutting down" % vmid)
        else:
          module.exit_json(changed=False, msg=("VM %s is already shutdown, but mounted. "
                                               "You can use force option to umount it.") % vmid)

      if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'stopped':
        module.exit_json(changed=False, msg="VM %s is already shutdown" % vmid)

      if stop_instance(module, proxmox, vm, vmid, timeout, force = module.params['force']):
        module.exit_json(changed=True, msg="VM %s is shutting down" % vmid)
    except Exception as e:
      module.fail_json(msg="stopping of VM %s failed with exception: %s" % ( vmid, e ))

  elif state == 'restarted':
    try:
      vm = get_instance(proxmox, vmid)
      if not vm:
        module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)
      if ( getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'stopped'
          or getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted' ):
        module.exit_json(changed=False, msg="VM %s is not running" % vmid)

      if ( stop_instance(module, proxmox, vm, vmid, timeout, force = module.params['force']) and
          start_instance(module, proxmox, vm, vmid, timeout) ):
        module.exit_json(changed=True, msg="VM %s is restarted" % vmid)
    except Exception as e:
      module.fail_json(msg="restarting of VM %s failed with exception: %s" % ( vmid, e ))

  elif state == 'absent':
    try:
      vm = get_instance(proxmox, vmid)
      if not vm:
        module.exit_json(changed=False, msg="VM %s does not exist" % vmid)

      if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'running':
        module.exit_json(changed=False, msg="VM %s is running. Stop it before deletion." % vmid)

      if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted':
        module.exit_json(changed=False, msg="VM %s is mounted. Stop it with force option before deletion." % vmid)

      taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE).delete(vmid)
      while timeout:
        if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped'
            and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ):
          module.exit_json(changed=True, msg="VM %s removed" % vmid)
        timeout = timeout - 1
        if timeout == 0:
          module.fail_json(msg='Reached timeout while waiting for removing VM. Last line in task before timeout: %s'
                           % proxmox_node.tasks(taskid).log.get()[:1])

        time.sleep(1)
    except Exception as e:
      module.fail_json(msg="deletion of VM %s failed with exception: %s" % ( vmid, e ))

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

if __name__ == '__main__':
    main()
