saltstack/saltstack.xml (27 lines of code) (raw):
<?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 "local" 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>