<?xml version="1.0" encoding="UTF-8"?>
<meta-runner name="SaltStack">
  <description>Run a SaltStack execution module through the salt-api service</description>
  <settings>
    <parameters>
      <param name="system.SALTARGUMENTS" value="" spec="text description='Argument for the salt module. for state.sls this will be the name of the state.' label='Argument for the salt module' validationMode='any' display='normal'" />
      <param name="system.SALTBATCHSIZE" value="" spec="text description='Salt will apply the module to minion in batches as specified here.  Select a number or percentage.  Example: 50% or 1' label='Batch size' validationMode='any' display='normal'" />
      <param name="system.SALTCLIENT" value="local" spec="text description='This is the client for salt.  Only use &quot;local&quot; for now.  In the future we may add runners and other salt functions' label='Salt Client' validationMode='not_empty' display='normal'" />
      <param name="system.SALTEAUTH" value="ldap" spec="text description='Enter the auth type for the users specified.  Examples: |'ldap|' or |'pam|'' label='Salt auth type' validationMode='not_empty' display='normal'" />
      <param name="system.SALTEXPRFORM" value="grain" spec="text description='The target expression type.  Examples: |'glob|', |'grain|', |'list|', |'pillar|',  See salt docs for more.' label='Salt expression form' validationMode='not_empty' display='normal'" />
      <param name="system.SALTFUNCTION" value="state.sls" spec="text description='The salt module to execute.  Example: state.sls https://docs.saltstack.com/en/latest/ref/modules/all/' label='Salt module function' validationMode='not_empty' display='normal'" />
      <param name="system.SALTKWARGS" value="" spec="text description='Extra arguments' label='kwargs' display='normal'" />
      <param name="system.SALTPILLAR" value="" spec="text description='The path to a pillar file to use with state.apply.  This pillar will overwrite any other pillar assigned to the minion.  The pillar must be valid yaml, no jinja markup is allowed here.' label='Pillar File' display='normal'" />
      <param name="system.SALTPASSWORD" value="" spec="password description='Password for the saltstack user' label='Salt user password' display='normal'" />
      <param name="system.SALTSTATEOUTPUT" value="" spec="text description='Only supported option is changes, which will only output state output that has changed' label='state-output' display='normal'" />
      <param name="system.SALTSUBSET" value="" spec="text description='Pick a random number of minions from this parameter from the target' label='subset' display='normal'" />
      <param name="system.SALTTARGET" value="" spec="text description='Select which minions should be targeted.  Be certain to follow the format selected for SALTEXPRFORM.  Example: |'grain:value|'  if expr_form is grain' label='Minion target' validationMode='not_empty' display='normal'" />
      <param name="system.SALTURL" value="https://salt-master:8000" spec="text description='The http url to the salt master: Example: https://192.168.1.1:8000' label='Salt master url' validationMode='not_empty' display='normal'" />
      <param name="system.SALTUSERNAME" value="" spec="text description='The username for the salt user.  User must have permission to the module as specified under external_auth in the salt master config.' label='Salt username' validationMode='not_empty' display='normal'" />
    </parameters>
    <build-runners>
      <runner name="Deploy" type="python">
        <parameters>
          <param name="bitness" value="*" />
          <param name="python-exe" value="%Python.2%" />
          <param name="python-kind" value="*" />
          <param name="python-script-code"><![CDATA[# -*- coding: utf-8 -*-
'''
Send a message to a SaltStack API
'''
import urllib2, urllib, json, yaml, ssl, sys
try:
    import salt.output as salt_outputter
except:
    raise("the salt python module is required.  Install it with 'pip install salt' on the TeamCity agent.  The salt-minion and salt-master packages are not required.")

def __init__():
    '''
    Set gloval vars.  These are replaced with env variables from TeamCity
    '''
    global saltclient, salturl, eauth, username, password, batch, target, expr_form, function, arguments, pillar, batch_size, subset, kwargs, state_output
    saltclient = '%system.SALTCLIENT%'
    salturl = '%system.SALTURL%'
    eauth = '%system.SALTEAUTH%'
    username = '%system.SALTUSERNAME%'
    password = '%system.SALTPASSWORD%'
    target = '%system.SALTTARGET%'
    expr_form = '%system.SALTEXPRFORM%'
    function = '%system.SALTFUNCTION%'
    arguments = '%system.SALTARGUMENTS%'
    pillar = '%system.SALTPILLAR%'
    batch_size = '%system.SALTBATCHSIZE%'
    subset = '%system.SALTSUBSET%'
    kwargs = '%system.SALTKWARGS%'
    state_output = '%system.SALTSTATEOUTPUT%'

    if batch_size and subset:
        raise SystemExit('SALTBATCHSIZE and SALTSUBSET cannot be used together.  Erase one of them please')

def local_client():
    '''
    This will run a normal salt command using the saltstack localclient.
    Example: salt 'minion' some.module
    '''
    args = {
        'tgt': target,
        'expr_form': expr_form,
        'client': 'local',
        'fun': function,
        'arg': []
    }
    if kwargs:
        args.update(dict(kwarg.split("=") for kwarg in kwargs.split(" ")))
    if arguments:
        args['arg'] = arguments.split(" ")
    if pillar:
        with open(pillar, 'r') as stream:
            try:
                args['arg'].append('pillar='+json.dumps(yaml.load(stream)))
            except yaml.YAMLError as e:
                print(e)
    if batch_size:
        args['batch'] = batch_size
        args['client'] = 'local_batch'
    if subset:
        args['sub'] = int(subset)
        args['client'] = 'local_subset'
    return exec_rest_call(args)

def exec_rest_call(args):
    '''
    Execute the restful call to the saltmaster
    '''
    #Login and get a token
    token = get_token()

    headers = { 'X-Auth-Token' : token, 'Accept' : 'application/json', 'Content-Type' : 'application/json' }
    data = json.dumps(args)
    gcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
    request = urllib2.Request(salturl, data, headers=headers)

    try: 
        d = urllib2.urlopen(request, timeout=3600, context=gcontext).read()
    except urllib2.HTTPError, e:
        print("Error in API Call: " + e.read())
        sys.exit(1)
    except urllib2.URLError, e:
        print("Error in API Call: " + str(e.reason))
        sys.exit(1)
    except httplib.HTTPException, e:
        print("Error in API Call: socket error")
        sys.exit(1)
    except: 
        print("Error Getting in API Call")
        sys.exit(1)

    #print("Raw Return:" +d)
    try:
        return json.loads(d)
    except:
        print("Return data is not JSON")
        sys.exit(1)
def get_token():
    '''
    Login and get a auth token from the salt master
    '''
    url = salturl + '/login'
    params = urllib.urlencode({
        'username': username,
        'password': password,
        'eauth': eauth
    })
    gcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1)

    try:
        auth = urllib2.urlopen(url, params, context=gcontext).read()
        return json.loads(auth)['return'][0]['token']
    except urllib2.HTTPError, e:
        print("Error Getting Token: " + e.read())
        sys.exit(1)
    except urllib2.URLError, e:
        print("Error Getting Token: " + str(e.reason))
        sys.exit(1)
    except httplib.HTTPException, e:
        print("Error Getting Token: socket error")
        sys.exit(1)
    except:
        print("Error Getting Token")
        sys.exit(1)

def normalize_local(results):
    '''
    Data is returned in different structure when in batch mode: https://github.com/saltstack/salt/issues/32459
    Make these results the same shape as batch returns here
    '''
    e = {"return": []}
    for key, value in results['return'][0].iteritems():
        e['return'].append({key:value })
    return e

def valid_return(return_data):
    '''
    Check the return data for any failures.  Since every salt module returns data in a different manner this will be hard to do accurately.
    1.  If the function is a state.appply, state.sls or state.highstate return true only if all state id's return true
    2.  For any other function look for a return of true
    *.  Otherwise return false
    '''

    failure = 0
    if function.startswith('state'):
        if type(return_data) is not dict:
            if type(return_data) is list:
                for error in return_data:
                    sys.stderr.write(error+'\n')
            elif type(return_data) is str:
                sys.stderr.write(return_data)
            failure = 1
        else:
            if return_data['return']:
                for miniondata in return_data['return']:
                    for minion, data in miniondata.iteritems():
                        if type(data) is not dict:
                            sys.stderr.write(minion+': '+str(data)+'\n')
                            failure = 1
                        else:
                            if "retcode" in data:
                                if data["retcode"] != 0:
                                    failure = 1
                            else:
                                for state, results in data.iteritems():
                                    if results['result'] == False:
                                        failure = 1
            else:
                sys.stderr.write('ERROR: No minions responded\n')
                failure = 1
    else:
        for miniondata in return_data['return']:
            for minion, data in miniondata.iteritems():
                if data == False:
                    failure = 1
    return(failure)

__init__()
if saltclient == 'local':
    results = local_client()

    if not results:
        sys.stderr.write('ERROR: No return received\n')
        sys.exit(2)

    opts = {"color": True, "color_theme": None, "extension_modules": "/"}
    if state_output == "changes":
        opts.update({"state_verbose": False})
    else:
        opts.update({"state_verbose": True})
    #local returns comes back in a weird shape.  But batch returns are ok.  Juts make all returns look like batch.  I still need to test this with subset
    if not batch_size:
        results = normalize_local(results)
    if function.startswith('state'):
        out="highstate"
    else:
        out=None

    for minion_result in results['return']:
        for minion, data in minion_result.iteritems():
            if "ret" in data:
                data[minion] = data.pop('ret')
                salt_outputter.display_output(data, out=out, opts=opts)
            else:
                salt_outputter.display_output(minion_result, out=out, opts=opts)
    sys.exit(valid_return(results))
]]></param>
          <param name="python-script-mode" value="code" />
          <param name="python-ver" value="2" />
          <param name="teamcity.step.mode" value="default" />
        </parameters>
      </runner>
    </build-runners>
    <requirements />
  </settings>
</meta-runner>
