#!/usr/bin/env python
#==============================================================
#
#  Abs:   Script for initializing IOC applications
#
#  Name:  iocIsBorn.py
#
#  Rem:   This file is read when "cram init" is called.
#
#  Usage: To initialize IOCs interactively,
#         type the following at the the unix prompt:
#
#            cram init
#
#         To initialize many IOCs at once,
#         type the following:
#
#            cram init -b <csvfile> -f <facility>
#
#         A template of <csvfile> can be found at
#         $EPICS_IOCS/template/cramInitTemplate.
#
#  Facility: SLAC Controls (Development)
#
#  Auth:  15-Apr-2016, M. Shankar (mshankar)
#--------------------------------------------------------------
#  Mods: (Latest to oldest)
#     22-Mar-2024, A. Le (anle)
#         Support both new and old screeniocs formats
#     30-Aug-2019, A. Le (anle)
#         Do not remake links if --remake-links is set and application version is blank
#         Add ability to force rewrite templates
#         Mention facility in email subject
#         Make sure that auto-restart scripts for SIOCs exist in /etc/init.d
#         Refactor code after review by Matt Gibbs
#     28-Aug-2019, A. Le (anle)
#         Fix port checking message for --verbose
#     27-Aug-2019, A. Le (anle)
#         Add option to skip verifying IOCs exist in iocBoot
#         Add test mode
#     26-Aug-2019, A. Le (anle)
#         Clean up screeniocs output
#     23-Aug-2019, A. Le (anle)
#         Do not modify or commit screeniocs if no IOCs are changed
#         Check current/iocBoot if application version is blank
#     22-Aug-2019, A. Le (anle)
#         Use os.path.join in iocBoot error message
#         Also validate terminal port numbers of CPUs
#         Do not error out when reinitializing HIOC with same terminal server and port
#     21-Aug-2019, A. Le (anle)
#         Make sure IOCs in CSV file exist in iocBoot
#         Remove sendEmail key from IOC dicts
#     16-Aug-2019, A. Le (anle)
#         Add message on CVS commit
#         Write temporary files in home directory instead of current
#         Write name instead of relative path for failed IOCs
#     15-Aug-2019, A. Le (anle)
#         Exclude IOCs in subdirectories if they were already initialized
#         Fix terminal port validation bug
#         Do not send email if no IOCs were initialized or modified
#         Retire Ask_For_DHCP_Etc_Init_d and Initialize_DHCP_Etc_Init_d
#         Write IOC name instead of relative path in DHCP message
#         Make modification indent smaller
#     14-Aug-2019, A. Le (anle)
#         Search subdirectories in iocBoot
#     13-Aug-2019, A. Le (anle)
#         Skip checking out screeniocs and checking if release folder exists after the first IOC
#         Avoid reinitializing CPUs during batch install
#     12-Aug-2019, A. Le (anle)
#         Create function to make multiple directories at once on Prod
#         Record modifications to screeniocs in header
#         Validate terminal port number
#         Add option to create startup.cmd.new instead of startup.cmd
#         Keep track of modified IOCs
#     09-Aug-2019, A. Le (anle)
#         Check if terminal ports conflict
#     08-Aug-2019, A. Le (anle)
#         Spare local screeniocs file if needed
#         Separate different applications in screeniocs
#         Make yes/no choices more descriptive
#         Put application name in screeniocs
#         Update screeniocs on Prod instead of Dev
#         Only sort, commit, and update screeniocs at the very end
#         Create yaml directory in $IOC_DATA
#         Move getScreenIOCSEntry and updateScreenIOCSEntry from multi_facility_process_helper.py to iocIsBorn.py
#         Add space between # and description in screeniocs
#     07-Aug-2019, A. Le (anle)
#         Sort screeniocs
#         Modify local copy of screeniocs instead of remote
#         Make wizard step to commit and update screeniocs
#         Make initial working directory visible on batch install
#         Delete temporary files when iocIsBorn is done or errors out
#         Check out correct version of screeniocs depending on facility
#     05-Aug-2019, A. Le (anle)
#         Change doneSIOCSPart to doneSIOCsPart
#         Check out epics/iocCommon/All/Dev/screeniocs
#     02-Aug-2019, A. Le (anle)
#         Add --remake-links option
#     31-Aug-2019, A. Le (anle)
#         Make screen host required for hard IOCS
#     30-Jul-2019, A. Le (anle)
#         Reword DHCP section again
#     29-Jul-2019, A. Le (anle)
#         Use different email list for SPEAR
#         Ask systems team to create /etc/init.d entries for SIOCS
#         Do not crash if application name and version are not specified
#         Ask whether to use cexp
#     26-Jul-2019, A. Le (anle)
#         Retire --silent option
#         Reword message about dhcpd.conf
#         Validate CSV file
#         Use $EPICS_IOC_TOP to look for application information
#         Add quiet mode
#     25-Jul-2019, A. Le (anle)
#         Check if entries for CPUs exist in $TFTPBOOT/dhcpd.conf
#         Allow user to specify application name and version in csv file
#     24-Jul-2019, A. Le (anle)
#         For each HIOC, screen host should be itself
#         Improve email formatting
#         Use logging module instead of custom logger
#         Keep track of failed IOCs and log errors
#         Do not preset screen host or user
#     23-Jul-2019, A. Le (anle)
#         Use custom logger and attach log to email
#         Validate the facility if provided
#         Separate code for processing --batch and --silent
#         Send email to controls-naming and controls-cd after batch install
#     22-Jul-2019, A. Le (anle)
#         Indicate what IOC is currently being installed
#         Add feature to initialize many IOCs at once from a file
#         Change cram install to cram init in usage message
#     19-Jul-2019, A. Le (anle)
#         Add capability to initialize CPUs without specifying IOCs
#         Only set default startup script for VIOCs if not yet set
#         Use correct facility directories
#         Detect EPICS base version from RELEASE_SITE
#     18-Jul-2019, A. Le (anle)
#         Set startup.cmd.linuxRT as the default startup script for VIOCs
#         Allow user to choose startup.cmd for VIOCs
#     17-Jul-2019, A. Le (anle)
#         Update templates for startup.cmd
#         Change screenrc and startup.cmd links for cpus
#         Change verbose to globals.verbose
#
#==============================================================
#
import os
import sys
import argparse
import subprocess
import fnmatch
import json
import datetime
import string
import types
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import pwd
import csv
import time
import traceback
import logging
import shutil
import re
from sortScreenIOCS import sortScreenIOCs
from operator import itemgetter
from tables import make_table
from getIOCAtPort import getIOCAtPort
from wizardPy import Wizard, TemplateWithValuesWizardStep, WizardStep, WizardContext, CallableWizardStep, TemplateWithValues

import globals
import toolconfig
import pushRelease
import upgradeRelease
import multi_facility_process_helper_client

scriptPath = os.path.expandvars("$TOOLS/script")
sys.path.append(scriptPath)
from parseScreenIOCS import getScreenIOCsFormat, getScreenIOCsEntry

def loadIOCRelPaths(folder):
    '''Load relative paths of iocs from specified folder using *ioc-*-* syntax'''
    iocs = set()
    if not os.path.exists(folder):
        return []
    for root, dirs, files in os.walk(folder):
        path = os.path.normpath(root)
        folders = path.split(os.sep)
        possibleIocRelPath = os.sep.join(folders[1:])
        possibleIocName = os.path.split(root)[1]
        if possibleIocName == 'iocBoot':
            continue
        if fnmatch.fnmatch(possibleIocName, '*ioc-*-*'):
            iocs.add(possibleIocRelPath)
    return sorted(list(iocs))

def createFolderIfItDoesNotExist(facility, folder, mode):
    '''Create the named folder on the other side with mode and optionally make if owned by IOC owner
    Return True if the folder was created.
    '''
    commandsAndArgs = []
    commandsAndArgs.append(('fileinfo', [folder]))
    results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
    folderInfo = results[0]
    if globals.verbose:
        print(folderInfo)
    if not folder in folderInfo:
        if globals.verbose:
            print("Creating the folder", folder)
        commandsAndArgs = []
        options = {'name': folder, 'mode': mode}
        commandsAndArgs.append(('mkdirs', options))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        if globals.verbose:
            print(results)
        return results[0]['justcreated']
    else:
        if globals.verbose:
            print("Folder {0} already exists.".format(folder))
        return False

def createFoldersIfTheyDoNotExist(facility, folders, mode):
    '''Create the named folders on the other side with mode and optionally make if owned by IOC owner
    Return a list indicating wheather each folder was created.
    '''
    if globals.verbose:
        print("Creating the folders", folders)
    commandsAndArgs = []
    for folder in folders:
        options = {'name': folder, 'mode': mode}
        commandsAndArgs.append(('mkdirs', options))
    results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
    if globals.verbose:
        print(results)
    return [result['justcreated'] for result in results]

def linkToFile(facility, link, target):
    '''
    Create a link to the target on facility.
    '''
    commandsAndArgs = []
    commandsAndArgs.append(('link', {
            'link' : link,
            'target' : target
            }))
    results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
    if globals.verbose:
        print("Done creating link {} to target{} with results".format(link, target), results)


def templateReplaceFile(facility, src, dest, templateVars):
    '''Template replace file on the other side'''
    commandsAndArgs = []
    commandsAndArgs.append(('templateReplace', {
        'source': src,
        'dest': dest,
        'templateVars' :  templateVars,
        'force': True
        }))
    results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
    if globals.verbose:
        print(results)
    return results[0]['status']


def makeIOCDataLists(defaultAppName, facility, csvFile):
    # Create list of IOC dictionaries and list of IOC names from CSV file
    cpuList = []
    hiocList = []
    siocList = []
    viocList = []
    iocDataList = []
    iocNameList = []
    with open(csvFile, 'r') as f:
        reader = csv.DictReader(f)
        for row in reader:
            if not row['Name']:
                raise ValueError("IOC name not found. Please verify that all IOCs in your CSV file have names")
            iocNameList.append(row['Name'])
            iocName = row['Name']
            iocType = row['Type']
            iocAppName = row['Application Name']
            iocAppVersion = row['Application Version']
            if iocType == 'CPU':
                cpuList.append(row)
            elif iocType == 'HIOC':
                hiocList.append(row)
            elif iocType == 'SIOC':
                siocList.append(row)
            elif iocType == 'VIOC':
                viocList.append(row)
            else:
                raise ValueError("Invalid IOC type for {}: {}".format(iocName, iocType))

            # Make sure that IOC exists in iocBoot, unless --ignore-iocboot is active
            if iocType != 'CPU' and not globals.ignoreIocBoot:
                assertExistsInIOCBoot(facility, iocName, iocAppName, iocAppVersion, 'HIOC')

    for cpu in cpuList:
        cpuDict = {}
        cpuDict['CPU'] = cpu['Name']
        cpuDict['IOCsToInitialize'] = []
        cpuDict['currIOCBeingInitialized'] = ''
        if cpu['Application Name']:
            cpuDict['appName'] = cpu['Application Name']
        else:
            cpuDict['appName'] = defaultAppName
        if cpu['Application Version']:
            cpuDict['appVersion'] = cpu['Application Version']
        else:
            cpuDict['appVersion'] = ''
        cpuDict['description'] = cpu['Description']
        cpuDict['facility'] = facility
        cpuDict['iocType'] = 'HIOC'
        setPropertyIfNotEmpty(cpuDict, 'screenHost', cpu, 'Screen Host')
        setPropertyIfNotEmpty(cpuDict, 'terminalPort', cpu, 'Port')
        setPropertyIfNotEmpty(cpuDict, 'terminalServer', cpu, 'Terminal Server')
        cpuDict['updateScreenIOCsForCPU'] = True
        cpuDict['useEPICSCPUs'] = False
        iocDataList.append(cpuDict)

    for hioc in hiocList:
        hiocDict = {}
        hiocDict['IOCsToInitialize'] = [hioc['Name']]
        if hioc['Application Name']:
            hiocDict['appName'] = hioc['Application Name']
        else:
            hiocDict['appName'] = defaultAppName
        if hioc['Application Version']:
            hiocDict['appVersion'] = hioc['Application Version']
        else:
            hiocDict['appVersion'] = ''
        hiocDict['description'] = hioc['Description']
        hiocDict['facility'] = facility
        hiocDict['iocType'] = 'HIOC'
        setPropertyIfNotEmpty(hiocDict, 'screenHost', hioc, 'Screen Host')
        setPropertyIfNotEmpty(hiocDict, 'terminalPort', hioc, 'Port')
        setPropertyIfNotEmpty(hiocDict, 'terminalServer', hioc, 'Terminal Server')
        iocDataList.append(hiocDict)

    for sioc in siocList:
        siocDict = {}
        siocDict['IOCsToInitialize'] = [sioc['Name']]
        if sioc['Application Name']:
            siocDict['appName'] = sioc['Application Name']
        else:
            siocDict['appName'] = defaultAppName
        if sioc['Application Version']:
            siocDict['appVersion'] = sioc['Application Version']
        else:
            siocDict['appVersion'] = ''
        siocDict['description'] = sioc['Description']
        setPropertyIfNotEmpty(siocDict, 'executable', sioc, 'Executable')
        siocDict['facility'] = facility
        siocDict['iocType'] = 'SIOC'
        setPropertyIfNotEmpty(siocDict, 'screenHost', sioc, 'Screen Host')
        if sioc['Use cexp?'].lower() in ['y', 'yes']:
            siocDict['useCexp'] = 'Yes'
        elif sioc['Use cexp?'].lower() in ['n', 'no']:
            siocDict['useCexp'] = 'No'
        else:
            raise ValueError("Invalid value for Use cexp? field: {}. Possible values are y, yes, n, no")
        iocDataList.append(siocDict)

    for vioc in viocList:
        viocDict = {}
        setPropertyIfNotEmpty(viocDict, 'CPU', vioc, 'CPU')
        viocDict['IOCsToInitialize'] = [vioc['Name']]
        if vioc['Application Name']:
            viocDict['appName'] = vioc['Application Name']
        else:
            viocDict['appName'] = defaultAppName
        if vioc['Application Version']:
            viocDict['appVersion'] = vioc['Application Version']
        else:
            viocDict['appVersion'] = ''
        viocDict['description'] = vioc['Description']
        setPropertyIfNotEmpty(viocDict, 'executable', vioc, 'Executable')
        viocDict['facility'] = facility
        viocDict['iocType'] = 'VIOC'
        if vioc['Use cexp?'].lower() in ['y', 'yes']:
            viocDict['useCexp'] = 'Yes'
        elif vioc['Use cexp?'].lower() in ['n', 'no']:
            viocDict['useCexp'] = 'No'
        else:
            raise ValueError("Invalid value for Use cexp? field: {}. Possible values are y, yes, n, no")
        setPropertyIfNotEmpty(viocDict, 'user', vioc, 'User')
        iocDataList.append(viocDict)

    return iocDataList, iocNameList


def setPropertyIfNotEmpty(iocDict, dictKey, csvIoc, csvKey):
    if not csvIoc[csvKey]:
        iocName = csvIoc['Name']
        raise ValueError("{} of {} cannot be empty".format(csvKey, iocName))

    iocDict[dictKey] = csvIoc[csvKey]


def sendEmailWithAttachments(facility, subject, message, attachPaths):
    # Read config emailToControls.cfg
    emailConfig = []
    with open(os.path.join(os.path.dirname(__file__), 'email.cfg'), 'r') as f:
        emailConfig = json.load(f)
    userDetails = pwd.getpwuid(os.getuid())
    msg = MIMEMultipart()
    msg['Subject'] = '' + subject
    fromEmailAddr = userDetails.pw_name + "@slac.stanford.edu"
    if globals.testDir:
        # Test mode: send email to requester only
        toEmailAddrList = [fromEmailAddr]
    elif facility == 'SPEAR':
        toEmailAddrList = emailConfig['to_spear']
    else:
        toEmailAddrList = emailConfig['to_other']
    msg['To'] = ','.join(toEmailAddrList)
    msg['From'] = fromEmailAddr
    msg['Cc'] = fromEmailAddr
    msg.attach(MIMEText(message, 'html'))
    # Attach files
    for attachPath in attachPaths:
        with open(attachPath) as f:
            contents = f.read()
            attachment = MIMEText(contents, 'plain')
            attachFilename = os.path.basename(os.path.normpath(attachPath))
            attachment.add_header('Content-Disposition', 'attachment', filename=attachFilename)
            msg.attach(attachment)
    s = smtplib.SMTP(emailConfig['smtphost'])
    # Send email to controls
    s.sendmail(fromEmailAddr, toEmailAddrList, msg.as_string())
    s.quit()


def cleanupScreenIOCs():
    # Delete temporary screeniocs folder if it exists
    if hasattr(globals, 'tempFolderPath'):
        if globals.verbose:
            print("Deleting {}".format(globals.tempFolderPath))
        shutil.rmtree(globals.tempFolderPath)

def cleanupLog():
    # Close and delete temporary log file
    if globals.verbose:
        print("Deleting {}".format(globals.logPath))
    logging.shutdown()
    os.remove(globals.logPath)


def die(message):
    # Print error message and clean up
    print(message)
    cleanupScreenIOCs()
    cleanupLog()
    sys.exit(-1)


def updateScreenIOCsEntry(iocName, screeniocsentry, screeniocsFileName, appName):
    '''Update the current entry in screeniocs for the given IOC; if present
    For the purposes of this method, screeniocs is treated as a file with name value lines.
    As inputs, we expect four arguments
    iocName - name of the IOC
    screeniocsentry - the complete line for the IOC; this includes the name of the IOC as the first word (such that startswith for the iocName).
    For example, a valid screeniocsentry is like so
    "vioc-b34-mp03         ioc-b34-mp03          laci     /afs/slac/g/lcls/epics/iocCommon/vioc-b34-mp03/iocSpecificRelease/bin/linuxRT_glibc-x86_64/mpsBSAProcessor # MPS BSA EPICS IOC on top of linuxRT"
    screeniocsFileName - path of screeniocs
    appName - name of current application
    We return the updated screeniocs entry and whether it was newly created.
    '''
    currentEntry = getScreenIOCsEntry(iocName, screeniocsFileName)
    if not currentEntry:
        # The IOC is not in screeniocs
        created = True
        modified = True
        with open(screeniocsFileName, 'a') as f:
            f.write(screeniocsentry)
    elif re.sub(' +', ' ', currentEntry.rstrip()) != re.sub(' +', ' ', screeniocsentry.rstrip()):
        # The IOC is in screeniocs but needs to be modified
        created = False
        modified = True
        with open(screeniocsFileName, 'r') as f:
            lines = f.readlines()
        with open(screeniocsFileName, 'w') as f:
            for line in lines:
                if line.startswith("#"):
                    f.write(line)
                elif line.startswith(iocName):
                    f.write(screeniocsentry)
                    if not screeniocsentry.endswith("\n"):
                        f.write("\n")
                else:
                    f.write(line)
    else:
        # The IOC entry in screeniocs is the same as what we provided
        created = False
        modified = False

    return getScreenIOCsEntry(iocName, screeniocsFileName), created, modified


def checkoutScreenIOCs(facility):
    '''Check out screeniocs to a temporary directory'''
    initialDir = os.getcwd()

    # Get repository where screeniocs lives
    commandsAndArgs = []
    commandsAndArgs.append(('env', 'IOC_SCREEN'))
    results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
    if globals.verbose:
        print(results)
    iocScreenPath = results[0]
    repositoryFilePath = os.path.join(iocScreenPath, 'CVS', 'Repository')
    if not os.path.exists(repositoryFilePath):
        die("Error: {} does not exist".format(repositoryFilePath))
    with open(repositoryFilePath, 'r') as f:
        repository = f.readline().rstrip()

    # Create temporary folder to check out screeniocs
    homeFolder = os.path.expanduser('~')
    tempFolder = time.strftime("cram_init_%Y%m%d_%H%M%S")
    tempFolderPath = os.path.join(homeFolder, tempFolder)
    if os.path.exists(tempFolderPath):
        # Very unlikely because of timestamp
        die("Error: directory {} already exists".format(tempFolderPath))
    if globals.verbose:
        print("Creating temporary directory {}".format(tempFolderPath))
    os.mkdir(tempFolderPath)
    if globals.verbose:
        print("Entering {}".format(tempFolderPath))
    os.chdir(tempFolderPath)

    # Check out screeniocs
    screenIOCsRepoPath = os.path.join(repository, 'screeniocs')
    tempScreenIOCsPath = os.path.join(tempFolderPath, 'screeniocs')
    if os.path.exists(tempScreenIOCsPath):
        # Should never be the case since we just created this directory
        die("Error: {} already exists".format(tempScreenIOCsPath))
    if globals.verbose:
        print("Checking out {}".format(screenIOCsRepoPath))
    if globals.testDir:
        # Test mode: copy <testDir>/iocCommon/screeniocs instead of
        # checking out $IOC_SCREEN/facility/screeniocs
        shutil.copy2(os.path.join(globals.testDir, 'iocCommon', 'screeniocs'), tempScreenIOCsPath)
    else:
        subprocess.call(['cvs', 'co', '-d', tempFolderPath, screenIOCsRepoPath])
    if globals.verbose:
        print("Exiting {}".format(tempFolderPath))
    os.chdir(initialDir)

    # Save screeniocs paths for later
    globals.tempFolderPath = tempFolderPath
    globals.tempScreenIOCsPath = tempScreenIOCsPath

    # Return path of local screeniocs
    return tempScreenIOCsPath


def addModificationToHeader(screeniocsFileName, initializedIOCs, modifiedIOCs):
    '''Record modifications to screeniocs in header'''
    with open(screeniocsFileName, 'r') as f:
        lines = f.readlines()
    with open(screeniocsFileName, 'w') as f:
        for line in lines:
            f.write(line)
            if line.startswith("#") and line.rstrip().endswith("Mod:"):
                # Write modifications
                today = datetime.date.today().strftime('%d-%b-%Y')
                userDetails = pwd.getpwuid(os.getuid())
                fullName = userDetails.pw_gecos
                initialFirst = initializeName(fullName)
                username = userDetails.pw_name
                f.write("#     {}, {} ({})\n".format(today, initialFirst, username))
                for ioc in initializedIOCs:
                    f.write("#         Add {}\n".format(ioc))
                for ioc in modifiedIOCs:
                    f.write("#         Change {}\n".format(ioc))


def initializeName(name):
    '''Turn Firstname M. Lastname into F. Lastname'''
    # Get rid of middle initial
    noMiddle = re.sub(r' [A-Z]\. ', ' ', name)

    # Initialize first name
    return re.sub(r'^([a-zA-Z])[^ .]* ', r'\1. ', noMiddle)


def sendEmailToControls(facility, attachments, initializedIOCs, modifiedIOCs, failedIOCs, iocsNotInDhcpConf, siocsNotInEtcInitd):
    '''Send email to controls-naming and controls-cd regarding status of initialized IOCs'''
    userDetails = pwd.getpwuid(os.getuid())
    if initializedIOCs:
        initializedIOCsText = '</b></div>\n<div>&emsp;<b>'.join(initializedIOCs)
        createdPart = '<p>The following IOCs have been initialized in {}:</p>\n<font color="green">\n<div>&emsp;<b>{}</b></div>\n</font>'.format(facility, initializedIOCsText)
    else:
        createdPart = ""
    if modifiedIOCs:
        modifiedIOCsText = '</b></div>\n<div>&emsp;<b>'.join(modifiedIOCs)
        modifiedPart = '<p>The following IOCs have been modified in screeniocs:</p>\n<font color="orange">\n<div>&emsp;<b>{}</b></div>\n</font>'.format(modifiedIOCsText)
    else:
        modifiedPart = ""
    if failedIOCs:
        failedIOCsText = '</b></div>\n<div>&emsp;<b>'.join(failedIOCs)
        failedPart = '<p>The following IOCs failed to initialize:</p>\n<font color="red">\n<div>&emsp;<b>{}</b></div>\n</font>'.format(failedIOCsText)
    else:
        failedPart = ""
    if iocsNotInDhcpConf:
        iocsNotInDhcpText = '</b></font></div>\n<div>&emsp;<b>'.join(iocsNotInDhcpConf)
        iocsNotInDhcpPart = '<p>The following IOCs do not have DHCP entries. Please send a message containing the MAC address and booting info of each IOC to the systems team.</p>\n<div>&emsp;<b>{}</b></div>'.format(iocsNotInDhcpText)
    else:
        iocsNotInDhcpPart = ""
    if siocsNotInEtcInitd:
        siocsNotInEtcInitdText = '</b></div>\n<div>&emsp;<b>'.join(siocsNotInEtcInitd)
        siocsNotInEtcInitdPart = '<p>Please ask the systems team to create <code>/etc/init.d</code> entries for automatically starting the following SIOCS in {}:</p>\n<div>&emsp;<b>{}</b></div>\n'.format(facility, siocsNotInEtcInitdText)
    else:
        siocsNotInEtcInitdPart = ""
    message = '<p>Hello,</p>\n{}\n{}\n{}\n{}\n{}\n<p>Please review the attachment(s).</p>\n<p>Thanks,</p>\n<p>{}</p>'.format(createdPart, modifiedPart, failedPart, iocsNotInDhcpPart, siocsNotInEtcInitdPart, userDetails.pw_gecos)
    sendEmailWithAttachments(facility, "New IOCs created in {}".format(facility), message, attachments)


def assertExistsInIOCBoot(facility, iocName, appName, appVersion, appType):
    '''Ensures that an IOC exists in iocBoot'''
    if globals.testDir:
        # Test mode: use <testDir>/iocTop instead of $EPICS_IOC_TOP
        packageReleaseFolder = os.path.join(globals.testDir, 'iocTop', appName)
    else:
        facilityConfig = toolconfig.getConfigForFacility(facility)
        packageReleaseFolder = os.path.join(facilityConfig[appType]['releaseFolder'], appName)
    # If appVersion is blank, just use "current"
    if appVersion:
        iocBootFolder = os.path.join(packageReleaseFolder, appVersion, 'iocBoot', iocName)
    else:
        iocBootFolder = os.path.join(packageReleaseFolder, 'current', 'iocBoot', iocName)
    if globals.verbose:
        print("Checking whether {} exists".format(iocBootFolder))

    if not os.path.exists(iocBootFolder):
        raise IOError("Cannot find {}".format(iocBootFolder))


def remoteFileExists(user, host, fileName):
    '''Checks if a file exists on a remote host via SSH'''
    sshArgs = ['ssh', '-x', '-o', 'BatchMode=yes', '-l', user, host]
    command = ['[', '-f', fileName, ']']
    return subprocess.call(sshArgs + command) == 0


def autoRestartScriptExists(siocName, siocHost):
    '''Checks if an SIOC's auto-startup script exists in /etc/init.d
       on the host running the SIOC
    '''
    iocOwner = os.path.expandvars("$IOC_OWNER")
    initdScriptPath = os.path.join('/etc/init.d', 'st.{}'.format(siocName))
    return remoteFileExists(iocOwner, siocHost, initdScriptPath)


class CheckoutScreenIOCs(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        appName = wizardContext.appName
        if hasattr(globals, 'tempFolderPath'):
            # screeniocs already checked out
            return True

        # Check out screeniocs
        tempScreenIOCsPath = checkoutScreenIOCs(facility)

        return True

class CheckDHCPConfExists(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        # Check if DHCP entry exists
        commandsAndArgs = []
        commandsAndArgs.append(('env', 'TFTPBOOT'))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        if globals.verbose:
            print(results)
        tftpbootDir = results[0]
        dhcpConfFile = os.path.join(tftpbootDir, 'dhcpd.conf')
        dhcpConfExists = os.path.exists(dhcpConfFile)
        if not dhcpConfExists:
            globals.logger.debug("Warning: Cannot find {}".format(dhcpConfFile))
        wizardContext.dhcpConfExists = dhcpConfExists
        wizardContext.dhcpConfFile = dhcpConfFile
        return True

class CheckReleaseFolderExists(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        if globals.testDir:
            # Test mode: use <testDir>/iocTop instead of $EPICS_IOC_TOP
            packageReleaseFolder = os.path.join(globals.testDir, 'iocTop', wizardContext.appName)
        else:
            facilityConfig = toolconfig.getConfigForFacility(facility)
            packageReleaseFolder = os.path.join(facilityConfig[wizardContext.appType]['releaseFolder'], wizardContext.appName)
        createFolderIfItDoesNotExist(facility, packageReleaseFolder, 0o775)
        return True

class LinkToExistingRelease(WizardStep):
    def enter(self, wizardContext):
        if 'currentLinkExists' in wizardContext.data and wizardContext.data['currentLinkExists']:
            return True
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        upgradeToVersion = wizardContext.data['linkExistingRelease']
        if globals.verbose:
            print("Upgrading the MASTER release to {} in facility {}".format(upgradeToVersion, facility))
        upgradeRelease.upgradeReleaseOnFacility(wizardContext.appName, wizardContext.appType, facility, "MASTER", upgradeToVersion)
        return True

class NextIOCToInitialize(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        if globals.verbose:
            print("Selecting next IOC to initialize")
        currIOCRelPath = wizardContext.data['IOCsToInitialize'].pop(0)
        currIOCName = os.path.split(currIOCRelPath)[1]
        wizardContext.data['currIOCBeingInitialized'] = currIOCName
        wizardContext.data['currIOCRelPath'] = currIOCRelPath
        if globals.verbose:
            print("Initializing IOC", wizardContext.data['currIOCBeingInitialized'])
            print("Remaining IOCS", wizardContext.data['IOCsToInitialize'])
        return True


class IOCCommonInitialization(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        if 'currIOCBeingInitialized' not in wizardContext.data:
            return True
        iocName = wizardContext.data['currIOCBeingInitialized']
        if globals.testDir:
            # Test mode: use <testDir>/iocCommon instead of $EPICS_IOCS
            iocCommonFolder = os.path.join(globals.testDir, 'iocCommon', iocName)
        else:
            iocCommonFolder = os.path.join(facilityConfig[wizardContext.appType]['linkFolder'], iocName)
        createFolderIfItDoesNotExist(facility, iocCommonFolder, 0o777)

        # Create iocSpecificRelease link
        appName = wizardContext.appName
        if 'appVersion' in wizardContext.data and not '' == wizardContext.data['appVersion']:
            appVersion = wizardContext.data['appVersion']
        else:
            appVersion = 'current'
        iocSpecificPath = os.path.join(iocCommonFolder, 'iocSpecificRelease')
        if globals.testDir:
            # Test mode: use <testDir>/iocTop instead of $EPICS_IOC_TOP
            iocTopCurrentPath = os.path.join(globals.testDir, 'iocTop', appName, appVersion)
        else:
            iocTopCurrentPath = os.path.join(facilityConfig[wizardContext.appType]['releaseFolder'], appName, appVersion)

        commandsAndArgs = []
        commandsAndArgs.append(('fileinfo', [iocSpecificPath]))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        if globals.verbose:
            print(results)
        linkInfo = results[0]
        if iocSpecificPath in linkInfo and not wizardContext.remakeLinks:
            # The link exists and --remake-links is not set -> do nothing
            globals.logger.debug("IOC {} seems to have an iocSpecificRelease".format(iocName))
        elif iocSpecificPath in linkInfo and wizardContext.remakeLinks and not appVersion:
            # The link exists and --remake-links is set, but the version is blank -> do nothing
            globals.logger.debug("Application version not set for IOC {}, not relinking iocSpecificRelease".format(iocName))
        else:
            # Now treat blank application version as "current"
            if not appVersion:
                appVersion = 'current'

            if iocSpecificPath in linkInfo:
                oldVersion = os.path.basename(linkInfo[iocSpecificPath]['pointingTo'])
                if oldVersion != appVersion:
                    globals.logger.debug("Warning: Changing iocSpecificRelease version from {} to {}".format(oldVersion, appVersion))

            commandsAndArgs = []
            commandsAndArgs.append(('link', {'link' : iocSpecificPath, 'target' : iocTopCurrentPath}))
            results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        if globals.verbose:
            print("Done creating iocSpecificRelease link", results)

        if globals.testDir:
            # Test mode: use template at <testDir>/iocCommon/screenrc instead of
            # ${EPICS_IOCS}/facility/screenrc
            templateReplaceFile(facility,
                os.path.join(globals.testDir, 'iocCommon', 'screenrc'),
                os.path.join(globals.testDir, 'iocCommon', iocName, 'screenrc'),
                {}
                )
        else:
            templateReplaceFile(facility,
                "${EPICS_IOCS}/facility/screenrc",
                os.path.join(facilityConfig[wizardContext.appType]['linkFolder'], iocName, 'screenrc'),
                {}
                )
        return True

class IOCDataInitialization(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        if 'currIOCBeingInitialized' not in wizardContext.data:
            return True
        iocName = wizardContext.data['currIOCBeingInitialized']
        if globals.testDir:
            # Test mode: use <testDir>/iocData instead of $IOC_DATA
            iocDataFolder = os.path.join(os.path.join(globals.testDir, 'iocData'), iocName)
        else:
            iocDataFolder = os.path.join("${IOC_DATA}", iocName)
        folders = [
            iocDataFolder,
            os.path.join(iocDataFolder, 'autosave'),
            os.path.join(iocDataFolder, 'autosave-req'),
            os.path.join(iocDataFolder, 'iocInfo'),
            os.path.join(iocDataFolder, 'yaml')
        ]
        createFoldersIfTheyDoNotExist(facility, folders, 0o776)


        return True

class ScreenIOCsParameters(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        if 'currIOCBeingInitialized' not in wizardContext.data:
            return True
        executableFiles = []
        for root, dirs, files in os.walk("bin"):
            for f in files:
                if os.access(os.path.join(root, f), os.X_OK):
                    executableFiles.append(os.path.relpath(os.path.join(root, f), "."))
        return TemplateWithValues("screeniocs.html",
                {"userText" : "Please choose the <code>screeniocs</code> parameters for <b>{}</b>.".format(wizardContext.data['currIOCBeingInitialized']),
                 "executablePaths" : executableFiles
                 },
                wizardContext)

class CheckTerminalPort(WizardStep):
    def enter(self, wizardContext):
        tempScreenIOCsPath = globals.tempScreenIOCsPath

        # This only applies to hard IOCs
        iocType = wizardContext.data['iocType']
        if iocType != 'HIOC':
            return True
        if 'CPU' in wizardContext.data:
            iocName = wizardContext.data['CPU']
        else:
            iocName = wizardContext.data['currIOCBeingInitialized']

        # Make sure port is a number
        terminalServer = wizardContext.data['terminalServer']
        terminalPort = wizardContext.data['terminalPort']
        if globals.verbose:
            print("Checking if port {} is a number".format(terminalPort))
        try:
            int(terminalPort)
        except ValueError:
            return self.showError(wizardContext, "Error: invalid port number {}".format(terminalPort))

        # See if another IOC is using the same port
        if globals.verbose:
            print("Checking if port {} on {} is already in use".format(terminalPort, terminalServer))
        iocAtPort = getIOCAtPort(tempScreenIOCsPath, terminalServer, terminalPort)
        if iocAtPort and iocAtPort != iocName:
            return self.showError(wizardContext, "Error: port {} on {} is already in use by {}".format(terminalPort, terminalServer, iocAtPort))

        return True

    def showError(self, wizardContext, message):
        if wizardContext.silentInstall:
            raise ValueError(message)

        # Display error message and go back to previous step
        return TemplateWithValues("error.html", {
              "userText" : "<div>{}. Please try again.</div>".format(message)},
              wizardContext)

class RunIOCWithCexpOrNot(WizardStep):
    def enter(self, wizardContext):
        if wizardContext.data['iocType'] not in ['SIOC', 'VIOC']:
            return True
        iocName = wizardContext.data['currIOCBeingInitialized']
        choices = [{'label': "<b>No</b>, don't start {} with Cexp.".format(iocName), 'value': "No"}, {'label' : "<b>Yes</b>, please start {} with Cexp.".format(iocName), 'value': "Yes"}]
        # Set No as the default
        if 'useCexp' not in wizardContext.data:
            wizardContext.data['useCexp'] = 'No'
        return TemplateWithValues("selectOne.html", {
                      "selections" : choices,
                      "stateName" : "useCexp",
                      "userText" : "Would you like to start <b>{}</b> with Cexp?".format(iocName)},
                      wizardContext)

class ScreenIOCsInitialization(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        if 'currIOCBeingInitialized' not in wizardContext.data:
            return True

        iocName = wizardContext.data['currIOCBeingInitialized']
        iocType = wizardContext.data['iocType']
        if globals.testDir:
            # Test mode: use <testDir>/iocCommon instead of $EPICS_IOCS
            iocCommonFolder = os.path.join(globals.testDir, 'iocCommon', iocName)
        else:
            iocCommonFolder = os.path.join(facilityConfig[wizardContext.appType]['linkFolder'], iocName)

        if iocType == 'SIOC':
            templateName = 'startup.cmd.soft'
        elif iocType == 'VIOC':
            templateName = 'startup.cmd.linuxRT'
        elif iocType == 'HIOC':
            templateName = 'startup.cmd.rtems'
        else:
            raise Exception("Unknown IOC type", iocType)

        if 'useCexp' in wizardContext.data and wizardContext.data['useCexp'] == 'Yes':
            templateName += '.cexp'

        if wizardContext.newStartupCmd:
            startupCmdFileName = 'startup.cmd.new'
        else:
            startupCmdFileName = 'startup.cmd'
        startupCmdTemplate = os.path.join(os.path.dirname(__file__), 'templates', templateName)
        startupCmdDict = self.generateStartupCmdDict(wizardContext)
        with open(startupCmdTemplate, 'r') as f:
            content = f.read()
        lines = string.Template(content).substitute(startupCmdDict)
        if globals.verbose:
            print("Template being sent over is")
            print(lines)
        commandsAndArgs = []
        commandsAndArgs.append(('templateReplace', {
            'content': lines,
            'dest': os.path.join(iocCommonFolder, startupCmdFileName),
            'templateVars' :  startupCmdDict,
            'force': True
            }))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        if globals.verbose:
            print(results)

        appName = wizardContext.appName
        results, created, modified = updateScreenIOCsEntry(iocName, self.generateScreenIOCSLine(wizardContext, iocCommonFolder), globals.tempScreenIOCsPath, appName)
        if globals.verbose:
            print("Done creating startup.cmd link", results, created)

        if created:
            wizardContext.initializedIOCs.append(iocName)
        elif modified:
            wizardContext.modifiedIOCs.append(iocName)

        # Check if IOC has been added to dhcp config file
        if wizardContext.dhcpConfExists and iocType == 'HIOC':
            with open(wizardContext.dhcpConfFile, 'r') as f:
                found = False
                for line in f:
                    if os.path.split(iocName)[1] in line:
                        found = True
                        break
                if not found:
                    wizardContext.iocsNotInDhcpConf.append(iocName)

        # Check if SIOC has an auto-restart script in /etc/init.d
        if wizardContext.data['iocType'] == 'SIOC' and facility != 'Dev':
            siocHost = wizardContext.data['screenHost']
            if not autoRestartScriptExists(iocName, siocHost):
                wizardContext.siocsNotInEtcInitd.append(iocName)

        return True
    def generateStartupCmdDict(self, wizardContext):
        ''' Variables we have
        ${IOC_NAME}
        ${IOC_REL_PATH}
        ${IOC_EXECUTABLE}
        ${IOC_DESCRIPTION}
        ${DATE}
        ${USER}
        ${IOC_FACILITY}
        '''
        stCmdDict = {}
        stCmdDict['IOC_NAME'] = wizardContext.data['currIOCBeingInitialized']
        stCmdDict['IOC_REL_PATH'] = wizardContext.data['currIOCRelPath']
        if wizardContext.data['iocType'] in ['SIOC', 'VIOC']:
            stCmdDict['IOC_EXECUTABLE'] = wizardContext.data['executable']
        stCmdDict['IOC_DESCRIPTION'] = wizardContext.data['description']
        stCmdDict['DATE'] = datetime.datetime.now().strftime("%Y/%b/%d %H:%M")
        userDetails = pwd.getpwuid(os.getuid())
        stCmdDict['USER'] = "{} ({})".format(userDetails.pw_name, userDetails.pw_gecos)
        facility = wizardContext.data['facility']
        if facility == 'TestFac':
            stCmdDict['IOC_FACILITY'] = 'g/acctest'
        elif facility == 'Dev':
            stCmdDict['IOC_FACILITY'] = 'g/lcls'
        elif facility == 'SPEARDEV':
            stCmdDict['IOC_FACILITY'] = 'g/spear'
        else:
            stCmdDict['IOC_FACILITY'] = facility.lower()
        return stCmdDict
    def generateScreenIOCSLine(self, wizardContext, iocCommonFolder):
        # TODO: move logic to parseScreenIOCS
        iocName = wizardContext.data['currIOCBeingInitialized']
        iocType = wizardContext.data['iocType']
        description = wizardContext.data['description']
        screenIOCsFormat = getScreenIOCsFormat()
        if iocType == 'HIOC':
            terminalServer = wizardContext.data['terminalServer']
            terminalPort = wizardContext.data['terminalPort']
            screenHost = wizardContext.data['screenHost']
            iocOwner = os.path.expandvars("$IOC_OWNER")
            screenHostInfo = "{}@{}".format(iocOwner, screenHost)
            terminalServerInfo = "root@{}:{}".format(terminalServer, terminalPort)
            if screenIOCsFormat == 0:
                return "{:<25} {:<20} {:<8} {:<20}      # {}\n".format(iocName, terminalServer, terminalPort, screenHost, description)
            else:
                return "{:<8} {:<25} {:<25} {:<34}      # {}\n".format(iocType, iocName, screenHostInfo, terminalServerInfo, description)
        elif iocType == 'VIOC':
            CPU = wizardContext.data['CPU']
            user = wizardContext.data['user']
            cpuInfo = "{}@{}".format(user, CPU)
            executable = os.path.join(iocCommonFolder, 'iocSpecificRelease', wizardContext.data['executable'])
            if screenIOCsFormat == 0:
                return "{:<25} {:<20} {:<8} {}      # {}\n".format(iocName, CPU, user, executable, description)
            else:
                return "{:<8} {:<25} {:<28} {}      # {}\n".format(iocType, iocName, cpuInfo, executable, description)
        elif iocType == 'SIOC':
            executable = os.path.join(iocCommonFolder, 'iocSpecificRelease', wizardContext.data['executable'])
            screenHost = wizardContext.data['screenHost']
            iocOwner = os.path.expandvars("$IOC_OWNER")
            screenHostInfo = "{}@{}".format(iocOwner, screenHost)
            if screenIOCsFormat == 0:
                return "{:<25} {}   sioc   {:<20}      # {}\n".format(iocName, executable, screenHost, description)
            else:
                return "{:<8} {:<25} {:<25} {}      # {}\n".format(iocType, iocName, screenHostInfo, executable, description)
        else:
            raise ValueError("Invalid IOC type for {}: {}".format(iocName, iocType))


class ScreenCPUsParameters(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        if 'currIOCBeingInitialized' not in wizardContext.data:
            return True
        iocName = wizardContext.data['currIOCBeingInitialized']
        iocType = wizardContext.data['iocType']
        if iocType != 'VIOC':
            return True

        description = wizardContext.data['description']
        CPU = wizardContext.data['CPU']
        # See if the CPU has a screeniocs's entry
        screeniocsentry = getScreenIOCsEntry(CPU, globals.tempScreenIOCsPath)
        commandsAndArgs = []
        commandsAndArgs.append(('env', 'EPICS_CPUS'))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        if globals.verbose:
            print("Checking if CPU has screenIOC entry", screeniocsentry, results)
        epicsCpusEnvVarExists = results[0]
        if epicsCpusEnvVarExists:
            if globals.verbose:
                print("Using EPICS_CPUS for the CPUS folder")
            wizardContext.data['useEPICSCPUs'] = True
        else:
            wizardContext.data['useEPICSCPUs'] = False
        if not screeniocsentry:
            wizardContext.data['updateScreenIOCsForCPU'] = True
            return TemplateWithValues("screeniocs.html",
                {"userText" : "Please choose the <code>screeniocs</code> parameters for CPU <b>{}</b>.".format(CPU),
                 "entriesForCPU" : True
                 },
                wizardContext)
        else:
            wizardContext.data['updateScreenIOCsForCPU'] = False

        return True

class ScreenCPUsInitialization(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        if 'currIOCBeingInitialized' not in wizardContext.data:
            return True
        iocType = wizardContext.data['iocType']
        if 'CPU' not in wizardContext.data:
            if globals.verbose:
                print("No CPU defined in context data; probably not a VIOC")
            return True
        if wizardContext.silentInstall and iocType == 'VIOC':
            if globals.verbose:
                print("CPU initialized separately during silent install, skipping")
            return True
        cpuName = wizardContext.data['CPU']
        if globals.testDir:
            # Test mode: use <testDir>/iocCommon instead of $EPICS_IOCS
            cpuRootFolder = os.path.join(globals.testDir, 'iocCommon')
            cpuCommonFolder = os.path.join(globals.testDir, 'iocCommon', cpuName)
        else:
            cpuRootFolder = "${EPICS_IOCS}"
            cpuCommonFolder = os.path.join("${EPICS_IOCS}", cpuName)
        if 'useEPICSCPUs' in wizardContext.data and wizardContext.data['useEPICSCPUs']:
            if globals.testDir:
                # Test mode: use <testDir>/cpuBoot instead of $EPICS_CPUS
                cpuRootFolder = os.path.join(globals.testDir, 'cpuBoot')
                cpuCommonFolder = os.path.join(globals.testDir, 'cpuBoot', cpuName)
            else:
                cpuRootFolder = "${EPICS_CPUS}"
                cpuCommonFolder = os.path.join("${EPICS_CPUS}", cpuName)

        if globals.testDir:
            # Test mode: use <testDir>/iocData instead of $IOC_DATA
            folders = [
                cpuCommonFolder,
                os.path.join(globals.testDir, 'iocData', cpuName),
                os.path.join(globals.testDir, 'iocData', cpuName, 'scanner')
            ]
        else:
            folders = [
                cpuCommonFolder,
                os.path.join("${IOC_DATA}", cpuName),
                os.path.join("${IOC_DATA}", cpuName, "scanner")
            ]
        results = createFoldersIfTheyDoNotExist(facility, folders, 0o777)
        cpuCommonFolderCreated = results[0]

        if wizardContext.newStartupCmd:
            startupCmdFileName = 'startup.cmd.new'
        else:
            startupCmdFileName = 'startup.cmd'
        linkToFile(facility, os.path.join(cpuCommonFolder, startupCmdFileName), os.path.join(cpuRootFolder, 'common', 'st.cmd.linuxRT.cpu'))
        linkToFile(facility, os.path.join(cpuCommonFolder, 'screenrc'), os.path.join(cpuRootFolder, 'screenrc'))

        kernelModulesCmdTemplate = os.path.join(os.path.dirname(__file__), 'templates', 'kernel-modules.cmd')
        with open(kernelModulesCmdTemplate, 'r') as f:
            content = f.read()

        commandsAndArgs = []
        commandsAndArgs.append(('templateReplace', {
            'content': content,
            'dest': os.path.join(cpuCommonFolder, 'kernel-modules.cmd'),
            'templateVars' :  {},
            'force': True
            }))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        if globals.verbose:
            print(results)

        # Create epicsSpecificRelease link
        epicsSpecificPath = os.path.join(cpuCommonFolder, 'epicsSpecificRelease')
        epicsReleasePath = self.getEPICSReleasePath(wizardContext)

        commandsAndArgs = []
        commandsAndArgs.append(('fileinfo', [epicsSpecificPath]))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        if globals.verbose:
            print(results)
        linkInfo = results[0]
        if epicsSpecificPath in linkInfo and not wizardContext.remakeLinks:
            globals.logger.debug("CPU {} seems to have an epicsSpecificRelease".format(cpuName))
        else:
            if epicsSpecificPath in linkInfo:
                oldVersion = os.path.basename(linkInfo[epicsSpecificPath]['pointingTo'])
                newVersion = os.path.basename(epicsReleasePath)
                if oldVersion != newVersion:
                    globals.logger.debug("Warning: Changing epicsSpecificRelease version from {} to {}".format(oldVersion, newVersion))

            commandsAndArgs = []
            commandsAndArgs.append(('link', {'link' : epicsSpecificPath, 'target' : epicsReleasePath}))
            results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
            if globals.verbose:
                print("Done creating epicsSpecificRelease link", results)

        # Create cpuSpecificRelease link
        appName = wizardContext.appName
        appVersion = wizardContext.data['appVersion']
        cpuSpecificPath = os.path.join(cpuCommonFolder, 'cpuSpecificRelease')
        if globals.testDir:
            # Test mode: use <testDir>/iocTop instead of $EPICS_IOC_TOP
            iocTopCurrentPath = os.path.join(globals.testDir, 'iocTop', appName, appVersion)
        else:
            iocTopCurrentPath = os.path.join(facilityConfig[wizardContext.appType]['releaseFolder'], appName, appVersion)

        commandsAndArgs = []
        commandsAndArgs.append(('fileinfo', [cpuSpecificPath]))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        if globals.verbose:
            print(results)
        linkInfo = results[0]
        if cpuSpecificPath in linkInfo and not wizardContext.remakeLinks:
            # The link exists and --remake-links is not set -> do nothing
            globals.logger.debug("CPU {} seems to have a cpuSpecificRelease".format(cpuName))
        elif cpuSpecificPath in linkInfo and wizardContext.remakeLinks and not appVersion:
            # The link exists and --remake-links is set, but the version is blank -> do nothing
            globals.logger.debug("Application version not set for CPU {}, not relinking cpuSpecificRelease".format(cpuName))
        else:
            # Now treat blank application version as "current"
            if not appVersion:
                appVersion = 'current'

            if cpuSpecificPath in linkInfo:
                oldVersion = os.path.basename(linkInfo[cpuSpecificPath]['pointingTo'])
                if oldVersion != appVersion:
                    globals.logger.debug("Warning: Changing cpuSpecificRelease version from {} to {}".format(oldVersion, appVersion))

            commandsAndArgs = []
            commandsAndArgs.append(('link', {'link' : cpuSpecificPath, 'target' : iocTopCurrentPath}))
            results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
            if globals.verbose:
                print("Done creating cpuSpecificRelease link", results)

        if wizardContext.data['updateScreenIOCsForCPU']:
            appName = wizardContext.appName
            results, created, modified = updateScreenIOCsEntry(cpuName, self.generateScreensIOCSLine(wizardContext, cpuName), globals.tempScreenIOCsPath, appName)
            if globals.verbose:
                print("Done updating screen iocs for CPU", results, created)
            if created:
                wizardContext.initializedIOCs.append(cpuName)
            elif modified:
                wizardContext.modifiedIOCs.append(cpuName)
        return True
    def getEPICSReleasePath(self, wizardContext):
        if globals.testDir:
            # Use <testDir>/iocCommon instead of $EPICS_IOCS
            cpuRootFolder = os.path.join(globals.testDir, 'iocCommon')
        else:
            cpuRootFolder = "${EPICS_IOCS}"
        epicsBinFolder = os.path.join(cpuRootFolder, 'common', 'bin')

        # Look at RELEASE_SITE file to determine EPICS base version
        releaseSiteFilename = 'RELEASE_SITE'
        if not os.path.isfile(releaseSiteFilename):
            if globals.verbose:
                print("File RELEASE_SITE not found, using EPICS base version R3-14-12")
            return os.path.join(epicsBinFolder, 'R3.14.12')

        # Look for line containing EPICS_BASE_VER
        with open(releaseSiteFilename, 'r') as f:
            for line in f:
                if line.startswith('EPICS_BASE_VER'):
                    if globals.verbose:
                        print("Found setting {} in RELEASE_SITE".format(line.strip()))
                    prefix = 'EPICS_BASE_VER=base-'
                    version = line.strip().replace(prefix, '')
                    version = version.replace('-', '.').replace('_', '-')
                    epicsReleasePath = os.path.join(epicsBinFolder, version)
                    # Make sure that folder actually exists
                    if not os.path.exists(epicsReleasePath):
                        if globals.verbose:
                            print("EPICS release path {} not found, using base version R3-14-12")
                            return os.path.join(epicsBinFolder, 'R3.14.12')
                    if globals.verbose:
                        print("EPICS release path according to RELEASE_SITE is {}".format(epicsReleasePath))
                    return epicsReleasePath

        # EPICS_BASE_VER not found
        if globals.verbose:
            print("EPICS_BASE_VER not found in RELEASE_SITE, using EPICS base version R3-14-12")
        return os.path.join(epicsBinFolder, 'R3.14.12')
    def generateScreensIOCSLine(self, wizardContext, cpuName):
        description = wizardContext.data['description']
        if 'cpuDescription' in wizardContext.data:
            # For silent installs, use the cpuDescription.
            description = wizardContext.data['cpuDescription']
        terminalServer = wizardContext.data['terminalServer']
        terminalPort = wizardContext.data['terminalPort']
        screenHost = wizardContext.data['screenHost']
        return "{:<25} {:<20} {:<8} {:<20}      # {}\n".format(cpuName, terminalServer, terminalPort, screenHost, description)


class AnyMoreIOCs(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        wizardContext.data['IOCsInitDone'].append(wizardContext.data['currIOCBeingInitialized'])

        if globals.verbose:
            print("AnyMoreIOCs: IOCsToInitialize", wizardContext.data['IOCsToInitialize'], "Num steps", len(wizardContext.steps))

        if len(wizardContext.data['IOCsToInitialize']) > 0:
            del wizardContext.data['currIOCBeingInitialized']
            wizardContext.gotToStep(wizardContext.data['iocInitializationStepId'])
        elif wizardContext.lastIOC:
            wizardContext.steps.insert(wizardContext.currentStep + 1, TemplateWithValuesWizardStep("message.html", {
                  "userText" : "<div>The following IOC's were initialized in facility <b>{}</b>.<ul>{}</ul></div><div>Thank you for using cram init</div>".format(facility, "\n".join(["<li>" + x + "</li>" for x in wizardContext.data['IOCsInitDone']])),
                  "finalStep" : True
                  }))
            if wizardContext.initializedIOCs or wizardContext.modifiedIOCs:
                wizardContext.steps.insert(wizardContext.currentStep + 1, SortScreenIOCs())
                wizardContext.steps.insert(wizardContext.currentStep + 2, AskCommitAndUpdateScreenIOCs())
                wizardContext.steps.insert(wizardContext.currentStep + 3, DoCommitAndUpdateScreenIOCs())
            else:
                cleanupScreenIOCs()
        return True


class ProcessIOCSelections(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        if 'IOCsToInitialize' not in wizardContext.data or  not wizardContext.data['IOCsToInitialize'] or len(wizardContext.data['IOCsToInitialize']) <= 0:
            # Allow initialization of CPU without specifying IOCs (silent mode only)
            if 'CPU' in wizardContext.data:
                if globals.verbose:
                    print("No IOCs to process, initializing {}".format(wizardContext.data['CPU']))
                wizardContext.steps.insert(wizardContext.currentStep + 1, ScreenCPUsParameters())
                wizardContext.steps.insert(wizardContext.currentStep + 2, CheckTerminalPort())
                wizardContext.steps.insert(wizardContext.currentStep + 3, ScreenCPUsInitialization())
                wizardContext.steps.insert(wizardContext.currentStep + 4, AnyMoreIOCs())
                return True
            wizardContext.steps.insert(wizardContext.currentStep + 1, TemplateWithValuesWizardStep("message.html", {
                  "userText" : "<div>You did not choose to initialize any IOC's this time around in facility <b>{}</b>.</div><div>Please run cram init again if/when you need to this</div>".format(facility),
                  "finalStep" : True
                  }))
            return True;
        # We now transition into IOC initialization
        # We reset the wizard previous steps as now don't want to go back.
        wizardContext.resetPreviousSteps()
        wizardContext.data['iocInitializationStepId'] = wizardContext.currentStep
        if globals.verbose:
            print("Adding IOC initialization steps at", wizardContext.currentStep)
        wizardContext.steps.insert(wizardContext.currentStep + 1, NextIOCToInitialize())
        wizardContext.steps.insert(wizardContext.currentStep + 2, IOCCommonInitialization())
        wizardContext.steps.insert(wizardContext.currentStep + 3, IOCDataInitialization())
        wizardContext.steps.insert(wizardContext.currentStep + 4, ScreenIOCsParameters())
        wizardContext.steps.insert(wizardContext.currentStep + 5, CheckTerminalPort())
        wizardContext.steps.insert(wizardContext.currentStep + 6, RunIOCWithCexpOrNot())
        wizardContext.steps.insert(wizardContext.currentStep + 7, ScreenIOCsInitialization())
        wizardContext.steps.insert(wizardContext.currentStep + 8, ScreenCPUsParameters())
        wizardContext.steps.insert(wizardContext.currentStep + 9, ScreenCPUsInitialization())
        wizardContext.steps.insert(wizardContext.currentStep + 10, AnyMoreIOCs())

        return True



class FindUninitializedIOCs(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        iocsInApp = loadIOCRelPaths("iocBoot")
        commandsAndArgs = []
        arg = {}
        arg['name'] = wizardContext.appName
        if globals.testDir:
            # Test mode: use <testDir>/iocTop and <testDir>/iocCommon instead of
            # $EPICS_IOC_TOP and $EPICS_IOCS
            arg['releaseFolder'] = os.path.join(globals.testDir, 'iocTop')
            arg['linkFolder'] = os.path.join(globals.testDir, 'iocCommon')
        else:
            arg['releaseFolder'] = toolconfig.determineReleaseFolder(facility, wizardContext.appName, wizardContext.appType)
            arg['linkFolder'] = toolconfig.determineLinkFolder(facility, wizardContext.appName, wizardContext.appType)
        commandsAndArgs.append(('determineIOCVersions', arg))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        iocVersionResult = results[0]
        if globals.verbose:
            print("After remote execution, results is ", iocVersionResult)
        if 'iocs' in iocVersionResult and iocVersionResult['iocs']:
            for iocInfo in iocVersionResult['iocs']:
                if globals.verbose:
                    print("{} is already pointing to release {}".format(iocInfo['ioc'], iocInfo['release']))
                iocsInApp = [ioc for ioc in iocsInApp if os.path.split(ioc)[1] != iocInfo['ioc']]
        if iocsInApp:
            wizardContext.steps.insert(wizardContext.currentStep + 1, TemplateWithValuesWizardStep("selectMultiple.html", {
                "selections" : iocsInApp,
                "stateName" : "IOCsToInitialize",
                "userText" : "Please choose the IOCs you want to initialize in <b>" + facility + "</b>"}))
            wizardContext.steps.insert(wizardContext.currentStep + 2, ProcessIOCSelections())
            wizardContext.data['IOCsInitDone'] = []
        else:
            wizardContext.steps.insert(wizardContext.currentStep + 1, TemplateWithValuesWizardStep("message.html", {
                  "userText" : "<div>All IOCs <i>(in iocBoot)</i> have been initialized in facility <b>{0}</b>.</div><div>Thank you for using cram init</div>".format(facility),
                  "finalStep" : True
                  }))

        return True



class PushCurrentFolderAndLink(WizardStep):
    def enter(self, wizardContext):
        if 'currentLinkExists' in wizardContext.data and wizardContext.data['currentLinkExists']:
            return True
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        if wizardContext.data['pushCurrentRelease'] == 'Yes':
            if globals.verbose:
                print("Pushing the current folder and using it as the 'current' release to facility", facility)
            # No need to freshen the release as it does not exists if we are here.
            pushRelease.pushReleaseToFacility(wizardContext.appName, wizardContext.appType, facility, False)
            currentVersion = os.path.basename(os.getcwd())
            if globals.verbose:
                print("Upgrading the MASTER release to {} in facility {}".format(currentVersion, facility))
            upgradeRelease.upgradeReleaseOnFacility(wizardContext.appName, wizardContext.appType, facility, "MASTER", currentVersion)
        else:
            if globals.verbose:
                print("Creating a dummy UpgradeMe release in facility", facility)
            commandsAndArgs = []
            if globals.testDir:
                # Test mode: use <testDir>/iocTop instead of $EPICS_IOC_TOP
                packageReleaseFolder = os.path.join(globals.testDir, 'iocTop')
            else:
                packageReleaseFolder = os.path.join(facilityConfig[wizardContext.appType]['releaseFolder'], wizardContext.appName)
            commandsAndArgs.append(('mkdirs', {'name': os.path.join(packageReleaseFolder, 'UpgradeMe')}))
            results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
            if globals.verbose:
                print(results)
            upgradeRelease.upgradeReleaseOnFacility(wizardContext.appName, wizardContext.appType, facility, "MASTER", "UpgradeMe")
        if wizardContext.appType in ['HIOC', 'SIOC']:
            iocsInApp = loadIOCRelPaths("iocBoot")
            if iocsInApp:
                wizardContext.steps.insert(wizardContext.currentStep + 1, FindUninitializedIOCs())
            else:
                # All done for IOC without any IOC's in iocBoot
                wizardContext.steps.insert(wizardContext.currentStep + 1, TemplateWithValuesWizardStep("message.html", {
                  "userText" : "<div>No IOC's were found in iocBoot. Finished initializing {0} in <b>{1}</b>.</div><div>Thank you for using cram init</div>".format(wizardContext.appName, facility),
                  "finalStep" : True
                  }))
        else:
            # All done for HLA/Tools/PyDM etc; if we want to run post init scripts; here's a place to do it.
            wizardContext.steps.insert(wizardContext.currentStep + 1, TemplateWithValuesWizardStep("message.html", {
              "userText" : "<div>Finished initializing {0} in facility <b>{1}</b>.</div><div>Thank you for using cram init</div>".format(wizardContext.appName, facility),
              "finalStep" : True
              }))

        return True

class CheckReleaseLinkExists(WizardStep):
    ''' Makes sure we have the softlink for HLA/Matlab apps/PyDM displays and the "current" link for IOC apps'''
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        facilityConfig = toolconfig.getConfigForFacility(facility)
        commandsAndArgs = []
        # See if we have any releases.
        if globals.testDir:
            # Test mode: use <testDir>/iocTop instead of $EPICS_IOC_TOP
            packageReleaseFolder = os.path.join(globals.testDir, 'iocTop', wizardContext.appName)
        else:
            packageReleaseFolder = os.path.join(facilityConfig[wizardContext.appType]['releaseFolder'], wizardContext.appName)
        if wizardContext.appType in ['SIOC', 'HIOC']:
            releaseLink = os.path.join(packageReleaseFolder, 'current')
        else:
            if globals.testDir:
                # Test mode: use <testDir>/iocCommon instead of $EPICS_IOCS
                releaseLink = os.path.join(globals.testDir, 'iocCommon', wizardContext.appName)
            else:
                releaseLink = os.path.join(facilityConfig[wizardContext.appType]['linkFolder'], wizardContext.appName)
        commandsAndArgs.append(('fileinfo', [releaseLink]))
        commandsAndArgs.append(('ls', [packageReleaseFolder]))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)

        linkInfo = results[0]
        releaseFolderContents = results[1]

        if releaseLink in linkInfo:
            if globals.verbose:
                print("The release link {} seems to exist in facility {} pointing to {}".format(releaseLink, facility, linkInfo[releaseLink]['pointingTo'] if 'pointingTo' in linkInfo[releaseLink] else 'N/A'))
            wizardContext.data['currentLinkExists'] = True
            if wizardContext.appType in ['HIOC', 'SIOC']:
                iocsInApp = loadIOCRelPaths("iocBoot")
                if iocsInApp:
                    wizardContext.steps.insert(wizardContext.currentStep + 1, FindUninitializedIOCs())
                else:
                    # All done for IOC without any IOC's in iocBoot
                    wizardContext.steps.insert(wizardContext.currentStep + 1, TemplateWithValuesWizardStep("message.html", {
                      "userText" : "<div>No IOC's found. Finished initializing {0} in facility <b>{1}</b></div><div>Thank you for using cram init</div>".format(wizardContext.appName, facility),
                      "finalStep" : True
                      }))
        else:
            # See if there any any releases available
            availableReleases = []
            for path in releaseFolderContents:
                if path['isFolder'] and not path['isLink']:
                    availableReleases.append(path['name'])
            if availableReleases:
                wizardContext.data['availableReleases'] = availableReleases
                wizardContext.steps.insert(wizardContext.currentStep + 1, TemplateWithValuesWizardStep("selectOne.html", {
                      # The | character in the label for the No option is used to specify both the value and the label
                      "selections" : availableReleases,
                      "stateName" : "linkExistingRelease",
                      "userText" : "There are already some releases in facility " + wizardContext.data["facility"] + ". Please pick one of these release to use as the current release."}))
                wizardContext.steps.insert(wizardContext.currentStep + 2, LinkToExistingRelease())
            else:
                # Add an action to ask the user if we should push the current release.
                # Perhaps we should not do this if the current release is called MAIN_TRUNK
                wizardContext.steps.insert(wizardContext.currentStep + 1, TemplateWithValuesWizardStep("selectOne.html", {
                      # The | character in the label for the No option is used to specify both the value and the label
                      "selections" : [{ 'label': "No, create a dummy <code>UpgradeMe</code> release instead (to remind me to upgrade later)", 'value': "No"}, {'label' : "Yes, push this folder and use it as the current release", 'value': "Yes"}],
                      "stateName" : "pushCurrentRelease",
                      "userText" : "Do you want to push the release in the current folder <code>" + os.path.basename(os.getcwd()) + "</code> to <b>" + wizardContext.data["facility"] + "</b>?"}))
                wizardContext.steps.insert(wizardContext.currentStep + 2, PushCurrentFolderAndLink())

        return True

class SortScreenIOCs(WizardStep):
    def enter(self, wizardContext):
        # Sort screeniocs file
        tempScreenIOCsPath = globals.tempScreenIOCsPath
        sortedScreenIOCsPath = tempScreenIOCsPath + '.sorted'
        sortScreenIOCs(tempScreenIOCsPath, sortedScreenIOCsPath)
        shutil.move(sortedScreenIOCsPath, tempScreenIOCsPath)

        return True

class AskCommitAndUpdateScreenIOCs(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        choices = [{'label': "<b>No</b>, don't commit or update screeniocs just yet.", 'value': "No"}, {'label' : "<b>Yes</b>, please commit and update screeniocs now.", 'value': "Yes"}]
        # Set Yes as the default
        wizardContext.data['updateScreenIOCs'] = 'Yes'
        return TemplateWithValues("selectOne.html", {
                      "selections" : choices,
                      "stateName" : "updateScreenIOCs",
                      "userText" : "Would you like to commit and update screeniocs on <b>{}</b>?".format(facility)},
                      wizardContext)

class DoCommitAndUpdateScreenIOCs(WizardStep):
    def enter(self, wizardContext):
        facility = wizardContext.data['facility']
        initializedIOCs = wizardContext.initializedIOCs
        modifiedIOCs = wizardContext.modifiedIOCs
        tempFolderPath = globals.tempFolderPath
        tempScreenIOCsPath = globals.tempScreenIOCsPath

        # Record modification in header if any IOCs were added or modified
        if wizardContext.initializedIOCs or wizardContext.modifiedIOCs:
            if globals.verbose:
                print("Adding screeniocs modifications to header")
            addModificationToHeader(tempScreenIOCsPath, initializedIOCs, modifiedIOCs)

        # Do not commit or update screeniocs if No was selected
        if wizardContext.data['updateScreenIOCs'] == 'No':
            return TemplateWithValues("message.html", {
                "userText" : "<p>A local copy of screeniocs was saved in the following directory:</p><ul><li>{}</li></ul><p>Please commit and update screeniocs on <b>{}</b> after making any additional changes.</p>".format(tempFolderPath, facility),
                },
                wizardContext)
        initialDir = os.getcwd()

        if globals.testDir:
            # Test mode: copy modified screeniocs to <testDir>/iocCommon instead of
            # committing to CVS
            shutil.copy2(globals.tempScreenIOCsPath, os.path.join(globals.testDir, 'iocCommon', 'screeniocs'))
            cleanupScreenIOCs()
            return True

        # Commit changes to screeniocs
        if globals.verbose:
            print("Entering {}".format(tempFolderPath))
        os.chdir(tempFolderPath)
        if globals.verbose:
            print("Committing changes to screeniocs")
        userDetails = pwd.getpwuid(os.getuid())
        username = userDetails.pw_name
        message = 'Add iocs for {} via cram init'.format(username)
        subprocess.call(['cvs', 'commit', '-m', message, 'screeniocs'])

        # Update screeniocs on prod
        if globals.verbose:
            print("Updating screeniocs on {}".format(facility))
        commandsAndArgs = []
        commandsAndArgs.append(('updateScreenIOCs', ''))
        results = multi_facility_process_helper_client.executeCommandsOnFacility(facility, commandsAndArgs)
        if globals.verbose:
            print(results)

        # Delete temporary screeniocs folder
        cleanupScreenIOCs()
        os.chdir(initialDir)

        return True


def execute(args):
    parser = argparse.ArgumentParser(prog="cram init")
    parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose logging')
    parser.add_argument('-q', '--quiet', action='store_true', help='Suppresses all output except for errors')
    parser.add_argument('-j', '--jsconsole', action='store_true', help='Send Javascript Console to stdout. However, QT will core dump on exit. Use for debugging only')
    parser.add_argument('-r', '--remake-links', action='store_true', help='Remake links for IOCs if they already exist')
    parser.add_argument('-N', '--new-startup-cmd', action='store_true', help='Create startup.cmd.new instead of startup.cmd')
    parser.add_argument('-i', '--ignore-iocboot', action='store_true', help='Do not check if each IOC exists in iocBoot')
    parser.add_argument('-n', '--name', help='Specify the application name; we automatically pick this up from the package info if we are running this command in the folder containing the release')
    parser.add_argument('-t', '--type', help='Specifies the type of application from the deployment perspective. Possible values are ' + ", ".join(toolconfig.appTypes.keys()))
    parser.add_argument('-b', '--batch', help='Parses a CSV file that contains all the IOCs and their information and performs a silent install')
    parser.add_argument('-f', '--facility', help='Specifies the facility where the IOCS are to be installed')
    parser.add_argument('-T', '--test-dir', help='Activates test mode. Specify a temporary directory containing iocCommon, iocTop, and iocData. iocCommon must contain the "common" directory (can be a link to $EPICS_IOCS/common) and the file "screenrc" (a link to common/screenrc). iocTop must contain your application(s) as they appear in $EPICS_IOC_TOP. Please copy $IOC_SCREEN/screeniocs to iocCommon inside your test directory')

    args = parser.parse_args(args)
    globals.verbose = args.verbose
    globals.testDir = args.test_dir
    globals.ignoreIocBoot = args.ignore_iocboot

    # Initialize logger
    logger = logging.getLogger()
    homeFolder = os.path.expanduser('~')
    logFile = time.strftime("cram_init_%Y%m%d_%H%M%S.log")
    logPath = os.path.join(homeFolder, logFile)
    if os.path.exists(logPath):
        raise IOError("Log file {} already exists".format(logPath))

    logger.setLevel(logging.DEBUG)
    fh = logging.FileHandler(logPath)
    logger.addHandler(fh)
    ch = logging.StreamHandler(sys.stdout)
    if args.quiet:
        ch.setLevel(logging.ERROR)
    else:
        ch.setLevel(logging.DEBUG)
    logger.addHandler(ch)
    globals.logger = logger
    globals.logPath = logPath

    if not args.batch:
        packageInfo = toolconfig.determinePackageInfoFromArgs(args)
        appName = packageInfo['name']
        appType = packageInfo['type']

        if not appName or not appType:
            die("Cannot determine the name and type of application. Please run cram describe before attempting to install the package.")
    else:
        # If running with --batch, appName and appType will be determined as we iterate through csv file
        appName = ''
        appType = ''

    # Validate the facility, if provided
    facilityList = toolconfig.getListOfFacilities(appType)
    if args.facility:
        if args.facility not in facilityList:
            parser.error("Invalid facility {}. Possible facilities are {}".format(args.facility, ", ".join(facilityList)))
        steps = []
    else:
        steps = [
        TemplateWithValuesWizardStep("selectOne.html", {
            "selections" : facilityList,
            "stateName" : "facility",
            "userText" : "Please choose the facility you want to install this package in"})
        ]
    if globals.verbose:
        print("Installing package {} of type {}".format(appName, appType))
    steps += [
        CheckoutScreenIOCs(),
        CheckDHCPConfExists(),
        CheckReleaseFolderExists(),
        CheckReleaseLinkExists()
    ]
    wizard = Wizard("Initialize " + appName, os.path.dirname(__file__), steps, verbose=globals.verbose, appName=appName, appType=appType, remakeLinks=args.remake_links, newStartupCmd=args.new_startup_cmd)
    wizard.context.data['facility'] = args.facility
    wizard.context.lastIOC = True
    wizard.context.initializedIOCs = []
    wizard.context.siocsNotInEtcInitd = []
    wizard.context.modifiedIOCs = []
    wizard.context.iocsNotInDhcpConf = []
    updateScreenIOCsEntry.currAppName = None

    # This is a convenience to send the javascript console to the terminal; however, QT seems to dump core when we do this. So, use only when debugging
    if args.jsconsole:
        wizard.showConsoleOnTerminal = True

    if args.batch:
        # Make sure facility is specified
        if not args.facility:
            parser.error("Please specify the facility with -f or --facility")

        # Parse CSV file then generate a list of dictionaries containing IOC information
        dataForSilentInstall, iocNameList = makeIOCDataLists(appName, args.facility, args.batch)
        csvPath = os.path.join(os.getcwd(), args.batch)
        wizard.context.silentInstall = True

        # Make a copy of the initial steps to that we do not keep adding to the same list
        initialSteps = list(steps)

        # Silent install data file has a list of things to install
        failedIOCs = []
        numIOCs = len(dataForSilentInstall)
        for i, (dataForIOC, iocName) in enumerate(zip(dataForSilentInstall, iocNameList)):
            try:
                # Go to application directory
                facilityConfig = toolconfig.getConfigForFacility(args.facility)
                appName = dataForIOC['appName']
                if dataForIOC.has_key(appVersion) and not ('' == dataForIOC['appVersion']):
                    appVersion = dataForIOC['appVersion']
                else:
                    # If version is blank, just use "current" for now since we
                    # need it to find out the package type
                    appVersion = 'current'
                commandsAndArgs = []
                commandsAndArgs.append(('env', 'EPICS_IOC_TOP'))
                results = multi_facility_process_helper_client.executeCommandsOnFacility(args.facility, commandsAndArgs)
                if globals.verbose:
                    print(results)
                iocTopPath = results[0]
                appPath = os.path.join(iocTopPath, appName, appVersion)
                os.chdir(appPath)
                packageInfo = toolconfig.determinePackageInfoFromArgs({})
                appType = packageInfo['type']

                # Initialize silent wizard
                logger.debug("Currently installing {} ({}/{})".format(iocName, i + 1, numIOCs))
                if i > 0:
                    # Skip checking out screeniocs and checking if release folder and dhcpd.conf exist after the first IOC
                    wizard.context.resetForSilentInstall(list(initialSteps)[3:])
                else:
                    wizard.context.resetForSilentInstall(list(initialSteps))
                wizard.context.appName = appName
                wizard.context.appType = appType
                wizard.context.data = dataForIOC
                wizard.context.lastIOC = (i == len(iocNameList) - 1)
                wizard.launchWizard()
            except Exception as e:
                logger.error(traceback.format_exc())
                shortIocName = os.path.split(iocName)[1]
                failedIOCs.append(shortIocName)
    else:
        wizard.launchWizard()

    # Send email to controls-naming and controls-cd if any IOCs were initialized or modified
    facility = wizard.context.data['facility']
    initializedIOCs = wizard.context.initializedIOCs
    siocsNotInEtcInitd = wizard.context.siocsNotInEtcInitd
    modifiedIOCs = wizard.context.modifiedIOCs
    iocsNotInDhcpConf = wizard.context.iocsNotInDhcpConf
    if initializedIOCs or modifiedIOCs:
        if globals.verbose:
            print("Sending email to controls-naming and controls-cd")
        if args.batch:
            sendEmailToControls(args.facility, [csvPath, logPath], initializedIOCs, modifiedIOCs, failedIOCs, iocsNotInDhcpConf, siocsNotInEtcInitd)
        else:
            sendEmailToControls(facility, [logPath], initializedIOCs, modifiedIOCs, [], iocsNotInDhcpConf, siocsNotInEtcInitd)
    else:
        if globals.verbose:
            print("No IOCs initialized or modified, not sending email")

    # Delete log file
    cleanupLog()

if __name__ == "__main__":
    pass
    #execute(sys.argv[1:])
