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

# Because Python works

import sys
reload(sys)
sys.setdefaultencoding('utf-8')


###################################################################################################
### This section is default values for the Monitoring Client
###
### You may change these values to suit your needs.  Note that this is Python, thus requires
###  standard Python syntax when setting values:
###  -Strings should be enclose in quotes
###  -Integers should not be in quotes
###  -Boolean should be True or False - they case sensitive
###
###################################################################################################
### Reporting

# Send reports using SSL?
Report_SSL = True

# True by default, some proxy servers require http only
# Can be changed in System Preferences

# Group
#  This will replace the Group currently reported by the machine if set
#  Leaving this as a blank string will have no effect
ClientGroup = 'Individuals'

###################################################################################################
### Running and Updating

# How often the client should run, in hours
RunClient_Interval_Default = 1

# Should we auto-update?
Update_Enabled = True


# Should we wait before checking for updates, or check every time?
Update_Interval_Enabled = False

# How many days between update checks?
Update_Interval = 1


# Do we ask for actions from the server?
CheckSettings_Enabled = True


###################################################################################################
### Branding

# Custom Software name
Branding_PrefPane_Bundle_Name = 'Monitoring Client'


# Custon PrefPane Greeting
Branding_PrefPane_Greeting = 'For support, contact:'


# Do we include a PrefPane?
Branding_PrefPane_Included = True


# Support address to display on the Preference Pane
Support_Address = 'http://www.graphiteas.com/support'


# Display the Contact Menu?
Branding_ContactMenu_Enabled_Default = True


# Menu Title
Branding_ContactMenu_Title = 'Contact Graphite Support'


# Menu URL
Branding_ContactMenu_URL = 'mailto:support@graphiteas.com'


# Template Image?
Branding_ContactMenu_Template_Ready = True


# Subscriber default ContactMenu Items
Branding_ContactMenu_Items = {}

###################################################################################################
### Plugin Options

### Root Capacity
# How full (in %) before we warn about the root volume?
root_capacity_warning_level = 90


### Panic Count
# How many kernel panics do we all before warning?
check_panic_count_qty = 3


### Time Machine
# How many days before we warn about not backing up?
check_time_machine_days = 30


# Should we trigger a manual run after those days have expired?
# (0 = disabled)
check_time_machine_manual_run = 0

# At what hour should we trigger the manual run?
# (0 - 23, only applicable if manual_run is enabled)
check_time_machine_manual_run_hour = 2


### CrashPlan Client
# How many days before we report on CrashPlan destinations which haven't completed?
check_crashplan_days = 7


### Backblaze
# How many days behind schedule before warn about not completing?
check_backblaze_days = 7

### FileVault
# Should we warn if FileVault becomes disabled?
check_filevault_warn_if_disabled = False

###################################################################################################
### Reporting
### Server Address
Server_Address = 'graphite.monitoringclient.com'


# Is automatic logging enabled?
Logging_Enabled = True

# How many days should we hold onto logs?
Logs_To_Keep = 7

# Will this be a diagnostic installation?
Diagnostic_Client = False

###################################################################################################
### All right, on with the Script!
###################################################################################################

import os, shutil
import plistlib, hashlib
import random, string
import time, datetime
from subprocess import Popen, PIPE

# Possible existing MonitoringClient.plist location
Library_ROOT         = os.path.join( sys.argv[2], 'Library' )
MonitoringClient_DIR = os.path.join( Library_ROOT, 'MonitoringClient' )
Modules_DIR          = os.path.join( MonitoringClient_DIR, 'Modules' )
old_Modules_DIR      = os.path.join( MonitoringClient_DIR, 'Utilities', 'Modules' )
if os.path.exists(old_Modules_DIR): shutil.rmtree(old_Modules_DIR)

sys.path.append(Modules_DIR)
from PluginToolkit import *
from UpdaterToolkit import *
Log.parent = 'INSTALL'
Log.enabled = True
Log.write('Postinstallation script starting ...')

ClientData_DIR    = os.path.join( MonitoringClient_DIR, 'ClientData' )
LocalInfo_DIR     = os.path.join( ClientData_DIR, 'LocalInfo' )
LaunchD_DIR       = os.path.join( Library_ROOT, 'LaunchDaemons' )

run_client        = os.path.join( MonitoringClient_DIR, 'RunClient' )
plist_FILE        = os.path.join( MonitoringClient_DIR, 'ClientSettings.plist' )
plist_FILE_backup = os.path.join(ClientData_DIR, 'ClientSettingsBackup.plist')
old_status        = os.path.join( ClientData_DIR, 'Status.plist' )
volume_info       = os.path.join( MonitoringClient_DIR, 'PluginSupport', 'check_volumes_settings.plist' )

client_launchd    = os.path.join( LaunchD_DIR, 'com.monitoringclient.client.plist' )
updater_launchd   = os.path.join( LaunchD_DIR, 'com.monitoringclient.updater.plist' )
journal_launchd   = os.path.join( LaunchD_DIR, 'com.monitoringclient.diskjournald.plist' )
beacon_launchd    = os.path.join( LaunchD_DIR, 'com.monitoringclient.beacon.plist' )

menu_enabled_file = os.path.join( ClientData_DIR, 'LocalInfo', 'cmhelperd.enabled' )
menu_info_file    = os.path.join( MonitoringClient_DIR, 'Utilities', 'ContactMenu.app', 'Contents', 'Info.plist' )
menu_icon_file    = os.path.join( MonitoringClient_DIR, 'Utilities', 'ContactMenu.icns' )

Temp_DIR          = os.path.join( LocalInfo_DIR, 'tmp' )
Logs_DIR          = os.path.join( ClientData_DIR, 'Logs' )
install_log_old   = os.path.join( Logs_DIR, 'Installation.log' )
install_log_file  = os.path.join( Logs_DIR, '%s_installation.log' % time.strftime('%Y-%m-%d', time.localtime()) )

status_plist      = os.path.join( ClientData_DIR, 'Status.plist' )
status_log_plist  = os.path.join( LocalInfo_DIR, 'Status_Log.plist' )
status_conf       = os.path.join( ClientData_DIR, 'Status.json' )
status_log_conf   = os.path.join( LocalInfo_DIR, 'Status_Log.json' )

if not os.path.exists(ClientData_DIR): os.mkdir(ClientData_DIR)
if not os.path.exists(Temp_DIR): os.mkdir(Temp_DIR)
if not os.path.exists(Logs_DIR): os.mkdir(Logs_DIR)
if os.path.exists( install_log_old ): os.remove( install_log_old )

# Function to generate a WatchmanID
def gen_key(settings):
	Log.write('Generating Client ID ...')
	ID_chars = "%s%s" % (string.uppercase, string.digits)
	id_a = ''
	id_b = ''
	for i in range(4): id_a += random.choice(ID_chars)
	for i in range(6): id_b += random.choice(ID_chars)

	# We only want to generate a DIAG-* key if:
	#	Diagnostic_Client does not exist in the settings, and is true above
	# or:
	#	Diagnostic_Client is True in the settings
	if (
			(
				( settings.get('Diagnostic_Client') == None )
				and Diagnostic_Client
			)
			or settings.get('Diagnostic_Client')
	):
		date = time.strftime('%m%d', time.localtime())
		key = 'DIAG%s-%s-%s' % (str(date), id_a, id_b)

	else:
		date = time.strftime('%Y%m%d', time.localtime())
		key = '%s-%s-%s' % (str(date), id_a, id_b)

	return key

def datetimeToEpoch( value ):
	if isinstance( value, datetime.datetime ):
		value = time.mktime( value.timetuple() )
	return value

def plistToJson( plist_file, conf_file ):
	if os.path.exists( plist_file ):
		Log.write('Converting %s to json ...' % plist_file)
		data = readPlist( plist_file, warn=False )
		if data:
			Log.write('Plist read successfully.')

			# The Status.plist is a dict
			if isinstance( data, dict ):
				settings = {}
				for key in data:
					Log.write('Trying to convert: %s - %s' % ( key, data[key] ))
					settings[key] = datetimeToEpoch( data[key] )

			# The Status_Log.plist is a list of dicts
			elif isinstance( data, list ):
				settings = []
				for i, v in enumerate(data):
					settings.insert( i, {} )
					for key in data[i]:
						Log.write('Trying to convert: %s - %s' % ( key, data[i][key] ))
						settings[i][key] = datetimeToEpoch( data[i][key] )

			if write_status( settings, conf_file ):
				return settings
			else:
				Log.write('Could not save converted settings.')
	return False

##################################### Handle Old Clients #####################################

# Unload the old launchd's
if os.path.exists(client_launchd):
	Log.write('Unloading %s ...' % client_launchd)
	Popen('/bin/launchctl unload %s' % client_launchd, shell=True).wait()

if os.path.exists(updater_launchd):
	Log.write('Unloading %s ...' % updater_launchd)
	Popen('/bin/launchctl unload %s' % updater_launchd, shell=True).wait()

if os.path.exists(beacon_launchd):
	Log.write('Unloading %s ...' % beacon_launchd)
	Popen('/bin/launchctl unload %s' % beacon_launchd, shell=True).wait()

# If there's a Client process running, we need to kill it
Log.write('Checking for running clients ...')
proc = Popen(
	'ps ax | grep "monitoring_client_error_logger" | grep -v "grep"',
	shell=True,
	stdout=PIPE,
	stderr=PIPE
)
out, err = proc.communicate()
e = proc.wait()

# Parse the process ID from the output
lines = filter(None, out.split('\n'))
for l in lines:
	pid = filter(None, l.split(' '))[0]
	Log.write('Killing pid %s ...' % pid)
	Popen('kill %s' % pid, shell=True).wait()

# Check for invalid old Status.plist files
if os.path.exists(old_status):
	Log.write('Checking old statuses ...')
	try: o_stat = plistlib.readPlist(old_status)
	except: Popen('rm %s' % old_status, shell=True).wait()


##################################### New Client Installation #####################################

# See if a ClientSettings.plist exists, and if so, can we read it
if not os.path.exists(plist_FILE):
	Log.write('Creating new ClientSettings.plist ...')
	plist_dict = {}
else:
	try:
		Log.write('Reading old ClientSettings.plist ...')
		plist_dict = plistlib.readPlist(plist_FILE)
	except:
		Log.write('Could not read old settings, trying plutil ...')
		Popen('plutil -convert xml1 %s' % plist_FILE, shell=True).wait()
		try:
			Log.write('Trying to read plutil\'ed settings ...')
			plist_dict = plistlib.readPlist(plist_FILE)
		except:
			try:
				Log.write('Still could not read old settings, trying backup ...')
				plist_dict = plistlib.readPlist(plist_FILE_backup)
			except:
				Log.write('Could not read backup settings, creating new ...')
				plist_dict = {}

# If there is no key, OR the Server_Address has changed, set it
subdomain = 'graphite'

alt_subdomain = ''


Log.write('Handling settings information ...')
if 'WatchmanID' not in plist_dict:
	plist_dict["WatchmanID"] = gen_key(plist_dict)
elif (
		( "Subdomain" in plist_dict )
		and ( plist_dict['Subdomain'] != subdomain )
		and ( plist_dict['Subdomain'] != alt_subdomain )
):
	plist_dict["WatchmanID"] = gen_key(plist_dict)
	plist_dict["SerialNumber_History"] = {}
	plist_dict["Payload_UUID"] = ''

# Set the ClientGroup
# If this machine doesn't have a Group, set a blank one
if ( 'ClientGroup' not in plist_dict ): plist_dict['ClientGroup'] = ''

# If the admin defined a ClientGroup in the options above, use it
if ( ClientGroup != '' ): plist_dict['ClientGroup'] = ClientGroup.decode('utf-8')

# If a ClientGroup-poposal.txt was written during installation, use that if we're still blank
cid_FILE = os.path.join(
	sys.argv[2],
	"private",
	"tmp",
	"MonitoringClientPlugin",
	"ClientGroup-proposal.txt"
)

if (
		os.path.exists( cid_FILE )
		and ( ClientGroup == '' )
		and ( plist_dict['ClientGroup'] == '' )
):
	FILE = open( cid_FILE, 'rU' )
	plist_dict["ClientGroup"] = FILE.readlines()[0].rstrip().decode('utf8')
	FILE.close()

##################################################################

#####################################   ClientSettings.plist info    ##################################

# Versioning
plist_dict["Client_Version"] = "6.6.7.115"
plist_dict["Build_Number"] = "6.6.7.115-43B86"

# Reporting and Support
plist_dict["Subdomain"] = subdomain
plist_dict["Server_Address"] = Server_Address
plist_dict["Report_SSL"] = Report_SSL
plist_dict["Support_Address"] = Support_Address
plist_dict["Logging_Enabled"] = Logging_Enabled
plist_dict["Logs_To_Keep"] = Logs_To_Keep

# Running and Updating
plist_dict["RunClient_Interval_Default"] = RunClient_Interval_Default
plist_dict["Update_Enabled"] = Update_Enabled
plist_dict["Update_Interval"] = Update_Interval
plist_dict["Update_Interval_Enabled"] = Update_Interval_Enabled
plist_dict["CheckSettings_Enabled"] = CheckSettings_Enabled

# Branding
plist_dict["Branding_PrefPane_Bundle_Name"] = Branding_PrefPane_Bundle_Name
plist_dict["Branding_PrefPane_Greeting"] = Branding_PrefPane_Greeting
plist_dict["Branding_PrefPane_Included"] = Branding_PrefPane_Included

# Plugins
plist_dict["root_capacity_warning_level"] = root_capacity_warning_level
plist_dict["check_time_machine_days"] = check_time_machine_days
plist_dict["check_time_machine_manual_run"] = check_time_machine_manual_run
plist_dict["check_time_machine_manual_run_hour"] = check_time_machine_manual_run_hour
plist_dict["check_crashplan_days"] = check_crashplan_days
plist_dict["check_panic_count_qty"] = check_panic_count_qty
plist_dict["check_backblaze_days"] = check_backblaze_days
plist_dict["check_filevault_warn_if_disabled"] = check_filevault_warn_if_disabled


# Defaults for the SupportMenu
# We only want to write these defaults if they don't already exist
plist_dict["Branding_ContactMenu_Title"] = Branding_ContactMenu_Title
plist_dict["Branding_ContactMenu_URL"] = Branding_ContactMenu_URL
plist_dict["Branding_ContactMenu_Template_Ready"] = Branding_ContactMenu_Template_Ready
if ( 'Branding_ContactMenu_Enabled' not in plist_dict ): plist_dict["Branding_ContactMenu_Enabled"] = Branding_ContactMenu_Enabled_Default
if ( 'Branding_ContactMenu_Items' not in plist_dict ): plist_dict["Branding_ContactMenu_Items"] = Branding_ContactMenu_Items
if ( 'Branding_ContactMenu_Visibility' not in plist_dict ): plist_dict["Branding_ContactMenu_Visibility"] = True
if ( 'ContactMenu_Version' not in plist_dict ): plist_dict["ContactMenu_Version"] = '0'

# Later on we'll want to kill/restart the ContactMenu if the version has changed
# We to check this now so that we're not changing anything after the client is launched
try: menu_version = plistlib.readPlist( menu_info_file )['CFBundleVersion']
except: menu_version = plist_dict['ContactMenu_Version']

if (
		( plist_dict['ContactMenu_Version'] == 0 )
		or ( plist_dict['ContactMenu_Version'] != menu_version )
):
	restart_menu = True
else: restart_menu = False
plist_dict['ContactMenu_Version'] = menu_version

# See if the ContactMenu icon was updated
icon_FILE = open( menu_icon_file, 'r' )
icon_DATA = icon_FILE.read()
icon_FILE.close()

hasher = hashlib.sha512()
hasher.update( icon_DATA )
icon_HASH = hasher.hexdigest()

if ( 'Branding_ContactMenu_Icon' not in plist_dict ): plist_dict['Branding_ContactMenu_Icon'] = icon_HASH
elif ( plist_dict['Branding_ContactMenu_Icon'] != icon_HASH ):
	restart_menu = True
	plist_dict['Branding_ContactMenu_Icon'] = icon_HASH

# If the Contact Menu is enabled, and this is a GUI install, we have some checking to do
if plist_dict['Branding_ContactMenu_Enabled']:
	Log.write('Contact Menu enabled, checking for %s ...' % menu_enabled_file)

	# Create the cmhelperd.enabled file so launchd will keep it alive
	if not os.path.exists(menu_enabled_file):
		Log.write('Creating %s ...' % menu_enabled_file)
		FILE = open( menu_enabled_file, 'w' )
		FILE.write('')
		FILE.close()

	# Load the user launchagents
	# This has to happen regardless of GUI install
	#if not ('COMMAND_LINE_INSTALL' in os.environ): load_user_agents()
	load_user_agents()

# If it isn't, just remove the enabled file - Contact Menu knows to quit if that file is gone
else:
	Log.write('Removing %s ...' % menu_enabled_file)
	if os.path.exists(menu_enabled_file): os.remove(menu_enabled_file)


##########################################   Do not edit below this line #########################################

# Diagnostics record
if 'Diagnostic_Client' not in plist_dict:
	plist_dict['Diagnostic_Client'] = Diagnostic_Client

# Check for a Disabled Plugins list
if 'PluginsDisabled' not in plist_dict:
	plist_dict['PluginsDisabled'] = []

# Log installation time
if 'Date_Client_Installed' not in plist_dict:
	plist_dict['Date_Client_Installed'] = datetime.datetime.now()
plist_dict['Date_Client_Updated'] = datetime.datetime.now()

plistlib.writePlist(plist_dict, plist_FILE)

# Make a backup copy of the ClientSettings, just in case
shutil.copy(plist_FILE, plist_FILE_backup)

# See if the PrefPane is wanted
if not plist_dict['Branding_PrefPane_Included']:
	Popen('rm -rf /Library/PreferencePanes/MonitoringClient.prefPane', shell=True).wait()

###################################################################################################
# All the postflight will do now is load the launchd - everything else is handled by the preflight
# and diskjournald's own internal awesomeness
###################################################################################################

# The original PrefPane kept the volume path as 'VolumeName'
# An updated version keeps 'VolumePath' and 'VolumeName'
# For immediate reporting, we also need a PresenceThreshold key
if os.path.exists( volume_info ):
	try:
		volumes = plistlib.readPlist( volume_info )
		for v in volumes:
			i = volumes.index(v)
			if ( 'VolumePath' not in v ):
				v['VolumePath'] = v['VolumeName']
				v['VolumeName'] = v['VolumeName'].split('/')[-1]
			if ( 'PresenceThreshold' not in v ):
				v['PresenceThreshold'] = 5
			volumes[i] = v
		plistlib.writePlist( volumes, volume_info )
	except:
		pass

# Unload and ReLoad the DeviceNodeTool launchd
#Log.write('Attempting to unload old diskjournald launchd ...')
#unload_proc = Popen('/bin/launchctl unload com.monitoringclient.diskjournald', shell=True, stdout=PIPE, stderr=PIPE)
#out, err = unload_proc.communicate()
#es = unload_proc.wait()
#Log.write('Status: %s\nResponse: %s\nErrors: %s\n' % ( str(es), out, err ))

Log.write('Attempting to load diskjournald launchd ...')
load_proc = Popen('/bin/launchctl load %s' % journal_launchd, shell=True, stdout=PIPE, stderr=PIPE)
out, err = load_proc.communicate()
es = load_proc.wait()
Log.write('Status: %s\nResponse: %s\nErrors: %s\n' % ( str(es), out, err ))

# If loading threw an error, remove and load again
#if err and ( err != '' ) and ( err != '\n' ):
#	Log.write('Error loading diskjournald launchd, attempting remove ...')
#	remove_proc = Popen('/bin/launchctl remove com.monitoringclient.diskjournald', shell=True, stdout=PIPE, stderr=PIPE)
#	out, err = remove_proc.communicate()
#	es = remove_proc.wait()
#	Log.write('Status: %s\nResponse: %s\nErrors: %s\n' % ( str(es), out, err ))

#	Log.write('Attempting to reload diskjournald launchd ...')
#	load_proc = Popen('/bin/launchctl load %s' % journal_launchd, shell=True, stdout=PIPE, stderr=PIPE)
#	out, err = load_proc.communicate()
#	es = load_proc.wait()
#	Log.write('Status: %s\nResponse: %s\nErrors: %s\n' % ( str(es), out, err ))

# Stop the launchd, and let PathState take over (for updates)
#Log.write('Attempting to stop diskjournald launchd ...')
#stop_proc = Popen('/bin/launchctl stop com.monitoringclient.diskjournald', shell=True, stdout=PIPE, stderr=PIPE)
#out, err = stop_proc.communicate()
#es = stop_proc.wait()
#Log.write('Status: %s\nResponse: %s\nErrors: %s\n' % ( str(es), out, err ))
###################################################################################################

# Set the PrefPane Identifier
Log.write('Setting PrefPane CFBundleIdentifier ...')
prefpane_info_PATH = os.path.join( MonitoringClient_DIR, 'Utilities', 'MonitoringClient.prefPane', 'Contents', 'Info.plist' )
if os.path.exists( prefpane_info_PATH ):
	try:
		prefpane_info = plistlib.readPlist( prefpane_info_PATH )
		prefpane_info['CFBundleIdentifier'] = 'com.apple.monitoringclient.prefpane'
		plistlib.writePlist( prefpane_info, prefpane_info_PATH )
	except:
		Log.write('Could not set CFBundleIdentifier.')

# Try to move the plsit stuff to JSON
if os.path.exists( status_plist ):
	Log.write('Old status format found, converting ...')
	if plistToJson( status_plist, status_conf ):
		Log.write('Settings converted.')
	else:
		Log.write('Conversion failed.')
	os.remove( status_plist )

if os.path.exists( status_log_plist ):
	os.remove( status_log_plist )
if os.path.exists( status_log_conf ):
	os.remove( status_log_conf )

# Force-run the client
#Log.write('Force-running the client ...')
#Popen(
#	'%s -F --install --log --no-log-plugins --no-print-log --log-file %s' % ( run_client, install_log_file ), shell=True,
#	cwd='/'
#)

# Prevent duplicate initial reports by waiting a moment
#time.sleep(1)

# Load the client scheduler with LaunchD
Log.write('Loading %s ...' % client_launchd)
Popen('/bin/launchctl load %s' % client_launchd, shell=True).wait()

# Load the ContactMenu scheduler with launchd
#TODO test to see if it's necessary to load before we can unload -w

### This is here for if/when we want to pull ContactMenu items from the server at installation
#client_actions = check_actions()
#if ( 'Branding_ContactMenu_Items' in client_actions ):
#	plist_dict['Branding_ContactMenu_Items'] = client_actions['Branding_ContactMenu_Items']
#	plistlib.writePlist( plist_dict, plist_FILE )

# In all cases, send a killall a stop to restart any running Contact Menus and the diskjournald.
# Commenting this out, trying to use PathState function and launchctl stop above
#Popen('/usr/bin/killall diskjournald', shell=True).wait()

# Only killall the ContactMenu if a new version has been installed
if restart_menu:
	Log.write('Restarting the Contact Menu...')
	Popen('/usr/bin/killall cmhelperd', shell=True).wait()


# Clean up /tmp
Log.write('Cleaning up ...')
if os.path.exists( cid_FILE ): os.remove(cid_FILE)
if os.path.exists("/private/tmp/MonitoringClientPlugin"): shutil.rmtree("/private/tmp/MonitoringClientPlugin")

# Load the beacon launchd
Log.write('Loading %s ...' % beacon_launchd)
Popen('/bin/launchctl load %s' % beacon_launchd, shell=True).wait()

# Now that we're all done running, let's schedule the updates
Log.write('Loading %s ...' % updater_launchd)
Popen('/bin/launchctl load %s' % updater_launchd, shell=True).wait()

# Thanks for reading this far, we love you for it!  Do remember... don't steal code ;-)
