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 &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>