#!/usr/bin/python

# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# 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/>.

DEFAULT_ANSIBLE_SETUP     = "/etc/ansible/setup"

import array
import fcntl
import glob
import sys
import os
import platform
import re
import shlex
import socket
import struct
import subprocess
import traceback

try:
    import json
except ImportError:
    import simplejson as json

_I386RE = re.compile(r'i[3456]86')
SIOCGIFCONF   = 0x8912
SIOCGIFHWADDR = 0x8927
MEMORY_FACTS = ['MemTotal', 'SwapTotal', 'MemFree', 'SwapFree']
# DMI bits
DMI_DICT = { 'form_factor':  '/sys/devices/virtual/dmi/id/chassis_type',
             'product_name': '/sys/devices/virtual/dmi/id/product_name',
             'product_serial': '/sys/devices/virtual/dmi/id/product_serial',
             'product_uuid': '/sys/devices/virtual/dmi/id/product_uuid',
             'product_version': '/sys/devices/virtual/dmi/id/product_version',
             'system_vendor': '/sys/devices/virtual/dmi/id/sys_vendor' }
# From smolt and DMI spec
FORM_FACTOR = [ "Unknown", "Other", "Unknown", "Desktop",
                "Low Profile Desktop", "Pizza Box", "Mini Tower", "Tower",
                "Portable", "Laptop", "Notebook", "Hand Held", "Docking Station",
                "All In One", "Sub Notebook", "Space-saving", "Lunch Box",
                "Main Server Chassis", "Expansion Chassis", "Sub Chassis",
                "Bus Expansion Chassis", "Peripheral Chassis", "RAID Chassis",
                "Rack Mount Chassis", "Sealed-case PC", "Multi-system",
                "CompactPCI", "AdvancedTCA" ]
# For the most part, we assume that platform.dist() will tell the truth.
# This is the fallback to handle unknowns or exceptions
OSDIST_DICT = { '/etc/redhat-release': 'RedHat',
                '/etc/vmware-release': 'VMwareESX' }

def get_file_content(path):
    if os.path.exists(path) and os.access(path, os.R_OK):
        data = open(path).read().strip()
        if len(data) == 0:
            data = None
    else:
        data = None
    return data

# platform.dist() is deprecated in 2.6
# in 2.6 and newer, you should use platform.linux_distribution()
def get_distribution_facts(facts):
    dist = platform.dist()
    facts['distribution'] = dist[0].capitalize() or 'NA'
    facts['distribution_version'] = dist[1] or 'NA'
    facts['distribution_release'] = dist[2] or 'NA'
    # Try to handle the exceptions now ...
    for (path, name) in OSDIST_DICT.items():
        if os.path.exists(path):
            if facts['distribution'] == 'Fedora':
                pass
            elif name == 'RedHat':
                data = get_file_content(path)
                if 'Red Hat' in data:
                    facts['distribution'] = name
                else:
                    facts['distribution'] = data.split()[0]
            else:
                facts['distribution'] = name

# Platform
# patform.system() can be Linux, Darwin, Java, or Windows
def get_platform_facts(facts):
    facts['system'] = platform.system()
    facts['kernel'] = platform.release()
    facts['machine'] = platform.machine()
    facts['python_version'] = platform.python_version()
    if facts['machine'] == 'x86_64':
        facts['architecture'] = facts['machine']
    elif _I386RE.search(facts['machine']):
        facts['architecture'] = 'i386'
    else:
        facts['archtecture'] = facts['machine']
    if facts['system'] == 'Linux':
        get_distribution_facts(facts)

def get_memory_facts(facts):
    if not os.access("/proc/meminfo", os.R_OK):
        return facts
    for line in open("/proc/meminfo").readlines():
        data = line.split(":", 1)
        key = data[0]
        if key in MEMORY_FACTS:
            val = data[1].strip().split(' ')[0]
            facts["%s_mb" % key.lower()] = long(val) / 1024

def get_cpu_facts(facts):
    i = 0
    physid = 0
    sockets = {}
    if not os.access("/proc/cpuinfo", os.R_OK):
        return facts
    for line in open("/proc/cpuinfo").readlines():
        data = line.split(":", 1)
        key = data[0].strip()
        if key == 'model name':
            if 'processor' not in facts:
                facts['processor'] = []
            facts['processor'].append(data[1].strip())
            i += 1
        elif key == 'physical id':
            physid = data[1].strip()
            if physid not in sockets:
                sockets[physid] = 1
        elif key == 'cpu cores':
            sockets[physid] = int(data[1].strip())
    if len(sockets) > 0:
        facts['processor_count'] = len(sockets)
        facts['processor_cores'] = reduce(lambda x, y: x + y, sockets.values())
    else:
        facts['processor_count'] = i
        facts['processor_cores'] = 'NA'

def get_hardware_facts(facts):
    get_memory_facts(facts)
    get_cpu_facts(facts)
    for (key,path) in DMI_DICT.items():
        data = get_file_content(path)
        if data is not None:
            if key == 'form_factor':
                facts['form_factor'] = FORM_FACTOR[int(data)]
            else:
                facts[key] = data
        else:
            facts[key] = 'NA'

def get_linux_virtual_facts(facts):
    if os.path.exists("/proc/xen"):
        facts['virtualization_type'] = 'xen'
        facts['virtualization_role'] = 'guest'
        if os.path.exists("/proc/xen/capabilities"):
            facts['virtualization_role'] = 'host'
    if os.path.exists("/proc/modules"):
        modules = []
        for line in open("/proc/modules").readlines():
            data = line.split(" ", 1)
            modules.append(data[0])
        if 'kvm' in modules:
            facts['virtualization_type'] = 'kvm'
            facts['virtualization_role'] = 'host'
        elif 'vboxdrv' in modules:
            facts['virtualization_type'] = 'virtualbox'
            facts['virtualization_role'] = 'host'
        elif 'vboxguest' in modules:
            facts['virtualization_type'] = 'virtualbox'
            facts['virtualization_role'] = 'guest'
    if 'QEMU' in facts['processor'][0]:
        facts['virtualization_type'] = 'kvm'
        facts['virtualization_role'] = 'guest'
    if facts['distribution'] == 'VMwareESX':
        facts['virtualization_type'] = 'VMware'
        facts['virtualization_role'] = 'host'
    # You can spawn a dmidecode process and parse that or infer from devices
    for dev_model in glob.glob('/proc/ide/hd*/model'):
        info = open(dev_model).read()
        if 'VMware' in info:
            facts['virtualization_type'] = 'VMware'
            facts['virtualization_role'] = 'guest'
        elif 'Virtual HD' in info or 'Virtual CD' in info:
            facts['virtualization_type'] = 'VirtualPC'
            facts['virtualization_role'] = 'guest'

def get_virtual_facts(facts):
    facts['virtualization_type'] = 'None'
    facts['virtualization_role'] = 'None'
    if facts['system'] == 'Linux':
        facts = get_linux_virtual_facts(facts)

# get list of interfaces that are up
def get_interfaces():
    length = 4096
    offset = 32
    step = 32
    if platform.architecture()[0] == '64bit':
        offset = 16
        step = 40
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    names = array.array('B', '\0' * length)
    bytelen = struct.unpack('iL', fcntl.ioctl(
        s.fileno(), SIOCGIFCONF, struct.pack(
            'iL', length, names.buffer_info()[0])
        ))[0]
    return [names.tostring()[i:i+offset].split('\0', 1)[0]
            for i in range(0, bytelen, step)]

def get_iface_hwaddr(iface):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    info = fcntl.ioctl(s.fileno(), SIOCGIFHWADDR,
                       struct.pack('256s', iface[:15]))
    return ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1]

def get_network_facts(facts):
    facts['fqdn'] = socket.getfqdn()
    facts['hostname'] = facts['fqdn'].split('.')[0]
    facts['interfaces'] = get_interfaces()
    for iface in facts['interfaces']:
        facts[iface] = { 'macaddress': get_iface_hwaddr(iface) }
        # This is lame, but there doesn't appear to be a good way
        # to get all addresses for both IPv4 and IPv6.
        cmd = subprocess.Popen("/sbin/ifconfig %s" % iface, shell=True,
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out, err = cmd.communicate()
        for line in out.split('\n'):
            data = line.split()
            if 'inet addr' in line:
                if 'ipv4' not in facts[iface]:
                    facts[iface]['ipv4'] = {}
                facts[iface]['ipv4'] = { 'address': data[1].split(':')[1],
                                         'netmask': data[-1].split(':')[1] }
            if 'inet6 addr' in line:
                (ip, prefix) = data[2].split('/')
                scope = data[3].split(':')[1].lower()
                if 'ipv6' not in facts[iface]:
                    facts[iface]['ipv6'] = []
                facts[iface]['ipv6'].append( { 'address': ip,
                                               'prefix': prefix,
                                               'scope': scope } )
    return facts

def get_public_ssh_host_keys(facts):
    dsa = get_file_content('/etc/ssh/ssh_host_dsa_key.pub')
    rsa = get_file_content('/etc/ssh/ssh_host_rsa_key.pub')
    if dsa is None:
        dsa = 'NA'
    else:
        facts['ssh_host_key_dsa_public'] = dsa.split()[1]
    if rsa is None:
        rsa = 'NA'
    else:
        facts['ssh_host_key_rsa_public'] = rsa.split()[1]

def get_service_facts(facts):
    get_public_ssh_host_keys(facts)

def ansible_facts():
    facts = {}
    get_platform_facts(facts)
    get_hardware_facts(facts)
    get_virtual_facts(facts)
    get_network_facts(facts)
    get_service_facts(facts)
    return facts

# load config & template variables

if len(sys.argv) == 1:
    sys.exit(1)


argfile = sys.argv[1]
if not os.path.exists(argfile):
    sys.exit(1)

setup_options = open(argfile).read().strip()
try:
   setup_options = json.loads(setup_options)
except:
   list_options = shlex.split(setup_options)
   setup_options = {}
   for opt in list_options:
       (k,v) = opt.split("=")
       setup_options[k]=v   

ansible_file = os.path.expandvars(setup_options.get('metadata', DEFAULT_ANSIBLE_SETUP))
ansible_dir = os.path.dirname(ansible_file)

# create the config dir if it doesn't exist

if not os.path.exists(ansible_dir):
    os.makedirs(ansible_dir)

changed = False
md5sum = None
if not os.path.exists(ansible_file):
    changed = True
else:
    md5sum = os.popen("md5sum %s" % ansible_file).read().split()[0]

# Get some basic facts in case facter or ohai are not installed
for (k, v) in ansible_facts().items():
    setup_options["ansible_%s" % k] = v

# if facter is installed, and we can use --json because
# ruby-json is ALSO installed, include facter data in the JSON

if os.path.exists("/usr/bin/facter"):
   cmd = subprocess.Popen("/usr/bin/facter --json", shell=True,
       stdout=subprocess.PIPE, stderr=subprocess.PIPE)
   out, err = cmd.communicate()
   facter = True
   try:
       facter_ds = json.loads(out)
   except:
       facter = False
   if facter:
       for (k,v) in facter_ds.items():
           setup_options["facter_%s" % k] = v

# ditto for ohai, but just top level string keys
# because it contains a lot of nested stuff we can't use for
# templating w/o making a nicer key for it (TODO)

if os.path.exists("/usr/bin/ohai"):
   cmd = subprocess.Popen("/usr/bin/ohai", shell=True,
       stdout=subprocess.PIPE, stderr=subprocess.PIPE)
   out, err = cmd.communicate()
   ohai = True
   try:
       ohai_ds = json.loads(out)
   except:
       ohai = False
   if ohai:
       for (k,v) in ohai_ds.items():
           if type(v) == str or type(v) == unicode:
               k2 = "ohai_%s" % k
               setup_options[k2] = v

# write the template/settings file using
# instructions from server

f = open(ansible_file, "w+")
reformat = json.dumps(setup_options, sort_keys=True, indent=4)
f.write(reformat)
f.close()

md5sum2 = os.popen("md5sum %s" % ansible_file).read().split()[0]

if md5sum != md5sum2:
   changed = True

setup_result = {}
setup_result['written'] = ansible_file
setup_result['changed'] = changed
setup_result['md5sum']  = md5sum2 
setup_result['ansible_facts'] = setup_options

print json.dumps(setup_result)

