1212import time
1313try :
1414 # Python 3
15- from configparser import ConfigParser
15+ from configparser import ConfigParser , RawConfigParser
1616except ImportError :
1717 # Python 2
18- import ConfigParser
18+ from ConfigParser import ConfigParser , RawConfigParser
1919
2020try :
2121 input = raw_input
3030 class NullHandler (logging .Handler ):
3131 def emit (self , record ): pass
3232
33- try :
34- import configparser
35- except ImportError :
36- import ConfigParser as configparser
37-
3833logging .basicConfig (format = '%(levelname)s %(asctime)s %(module)s: %(message)s' )
3934logger = logging .getLogger ('pepper' )
4035logger .addHandler (NullHandler ())
4136
4237
4338class PepperCli (object ):
44- def __init__ (self , default_timeout_in_seconds = 60 * 60 , seconds_to_wait = 3 ):
39+ def __init__ (self , seconds_to_wait = 3 ):
4540 self .seconds_to_wait = seconds_to_wait
4641 self .parser = self .get_parser ()
4742 self .parser .option_groups .extend ([self .add_globalopts (),
4843 self .add_tgtopts (),
4944 self .add_authopts ()])
50- self .parser .defaults .update ({'timeout' : default_timeout_in_seconds ,
51- 'fail_if_minions_dont_respond' : False ,
52- 'expr_form' : 'glob' })
5345
5446 def get_parser (self ):
5547 return optparse .OptionParser (
@@ -93,7 +85,7 @@ def add_globalopts(self):
9385 "Mimic the ``salt`` CLI" )
9486
9587 optgroup .add_option ('-t' , '--timeout' , dest = 'timeout' , type = 'int' ,
96- help = textwrap .dedent ('''\
88+ default = 60 , help = textwrap .dedent ('''\
9789 Specify wait time (in seconds) before returning control to the
9890 shell''' ))
9991
@@ -102,14 +94,21 @@ def add_globalopts(self):
10294 specify the salt-api client to use (local, local_async,
10395 runner, etc)''' ))
10496
97+ optgroup .add_option ('--json' , dest = 'json_input' ,
98+ help = textwrap .dedent ('''\
99+ Enter JSON at the CLI instead of positional (text) arguments. This
100+ is useful for arguments that need complex data structures.
101+ Specifying this argument will cause positional arguments to be
102+ ignored.''' ))
103+
105104 # optgroup.add_option('--out', '--output', dest='output',
106105 # help="Specify the output format for the command output")
107106
108107 # optgroup.add_option('--return', default='', metavar='RETURNER',
109108 # help="Redirect the output from a command to a persistent data store")
110109
111110 optgroup .add_option ('--fail-if-incomplete' , action = 'store_true' ,
112- dest = 'fail_if_minions_dont_respond' ,
111+ dest = 'fail_if_minions_dont_respond' , default = False ,
113112 help = textwrap .dedent ('''\
114113 Return a failure exit code if not all minions respond. This option
115114 requires the authenticated user have access to run the
@@ -124,6 +123,8 @@ def add_tgtopts(self):
124123 optgroup = optparse .OptionGroup (self .parser , "Targeting Options" ,
125124 "Target which minions to run commands on" )
126125
126+ optgroup .defaults .update ({'expr_form' : 'glob' })
127+
127128 optgroup .add_option ('-E' , '--pcre' , dest = 'expr_form' ,
128129 action = 'store_const' , const = 'pcre' ,
129130 help = "Target hostnames using PCRE regular expressions" )
@@ -140,6 +141,14 @@ def add_tgtopts(self):
140141 action = 'store_const' , const = 'grain_pcre' ,
141142 help = "Target based on PCRE matches on system properties" )
142143
144+ optgroup .add_option ('-I' , '--pillar' , dest = 'expr_form' ,
145+ action = 'store_const' , const = 'pillar' ,
146+ help = "Target based on pillar values" )
147+
148+ optgroup .add_option ('--pillar-pcre' , dest = 'expr_form' ,
149+ action = 'store_const' , const = 'pillar_pcre' ,
150+ help = "Target based on PCRE matches on pillar values" )
151+
143152 optgroup .add_option ('-R' , '--range' , dest = 'expr_form' ,
144153 action = 'store_const' , const = 'range' ,
145154 help = "Target based on range expression" )
@@ -187,12 +196,12 @@ def add_authopts(self):
187196 action = 'store_false' , dest = 'interactive' , help = textwrap .dedent ("""\
188197 Optional, fail rather than waiting for input""" ), default = True )
189198
190- # optgroup.add_option('-T', '--make-token', default=False,
191- # dest='mktoken', action='store_true',
192- # help=textwrap.dedent("""\
193- # Generate and save an authentication token for re-use. The token is
194- # generated and made available for the period defined in the Salt
195- # Master."""))
199+ optgroup .add_option ('-T' , '--make-token' , default = False ,
200+ dest = 'mktoken' , action = 'store_true' ,
201+ help = textwrap .dedent ("""\
202+ Generate and save an authentication token for re-use. The token is
203+ generated and made available for the period defined in the Salt
204+ Master.""" ))
196205
197206 return optgroup
198207
@@ -206,33 +215,28 @@ def get_login_details(self):
206215
207216 # setting default values
208217 results = {
209- 'SALTAPI_URL' : 'https://localhost:8000/' ,
210218 'SALTAPI_USER' : None ,
211219 'SALTAPI_PASS' : None ,
212220 'SALTAPI_EAUTH' : 'auto' ,
213221 }
214222
215223 try :
216224 config = ConfigParser (interpolation = None )
217- except TypeError as e :
218- config = ConfigParser . RawConfigParser ()
225+ except TypeError :
226+ config = RawConfigParser ()
219227 config .read (self .options .config )
220228
221229 # read file
222230 profile = 'main'
223231 if config .has_section (profile ):
224- for key , value in config .items (profile ):
225- key = key . upper ()
226- results [key ] = config .get (profile , key )
232+ for key , value in list ( results .items () ):
233+ if config . has_option ( profile , key ):
234+ results [key ] = config .get (profile , key )
227235
228236 # get environment values
229237 for key , value in list (results .items ()):
230238 results [key ] = os .environ .get (key , results [key ])
231239
232- # get eauth prompt options
233- if self .options .saltapiurl :
234- results ['SALTAPI_URL' ] = self .options .saltapiurl
235-
236240 if results ['SALTAPI_EAUTH' ] == 'kerberos' :
237241 results ['SALTAPI_PASS' ] = None
238242
@@ -245,36 +249,72 @@ def get_login_details(self):
245249 logger .error ("SALTAPI_USER required" )
246250 raise SystemExit (1 )
247251 else :
248- if self .options .username is not None : results ['SALTAPI_USER' ] = self .options .username
252+ if self .options .username is not None :
253+ results ['SALTAPI_USER' ] = self .options .username
249254 if self .options .password is None and results ['SALTAPI_PASS' ] is None :
250255 if self .options .interactive :
251256 results ['SALTAPI_PASS' ] = getpass .getpass (prompt = 'Password: ' )
252257 else :
253258 logger .error ("SALTAPI_PASS required" )
254259 raise SystemExit (1 )
255260 else :
256- if self .options .password is not None : results ['SALTAPI_PASS' ] = self .options .password
261+ if self .options .password is not None :
262+ results ['SALTAPI_PASS' ] = self .options .password
257263
258264 return results
259265
266+ def parse_url (self ):
267+ '''
268+ Determine api url
269+ '''
270+ url = 'https://localhost:8000/'
271+
272+ try :
273+ config = ConfigParser (interpolation = None )
274+ except TypeError :
275+ config = RawConfigParser ()
276+ config .read (self .options .config )
277+
278+ # read file
279+ profile = 'main'
280+ if config .has_section (profile ):
281+ if config .has_option (profile , "SALTAPI_URL" ):
282+ url = config .get (profile , "SALTAPI_URL" )
283+
284+ # get environment values
285+ url = os .environ .get ("SALTAPI_URL" , url )
286+
287+ # get eauth prompt options
288+ if self .options .saltapiurl :
289+ url = self .options .saltapiurl
290+
291+ return url
292+
260293 def parse_login (self ):
261294 '''
262295 Extract the authentication credentials
263296 '''
264297 login_details = self .get_login_details ()
265298
266- # Auth values placeholder; grab interactively at CLI or from config file
267- url = login_details ['SALTAPI_URL' ]
299+ # Auth values placeholder; grab interactively at CLI or from config
268300 user = login_details ['SALTAPI_USER' ]
269301 passwd = login_details ['SALTAPI_PASS' ]
270302 eauth = login_details ['SALTAPI_EAUTH' ]
271303
272- return url , user , passwd , eauth
304+ return user , passwd , eauth
273305
274306 def parse_cmd (self ):
275307 '''
276308 Extract the low data for a command from the passed CLI params
277309 '''
310+ # Short-circuit if JSON was given.
311+ if self .options .json_input :
312+ try :
313+ return json .loads (self .options .json_input )
314+ except ValueError :
315+ logger .error ("Invalid JSON given." )
316+ raise SystemExit (1 )
317+
278318 args = list (self .args )
279319
280320 client = self .options .client if not self .options .batch else 'local_batch'
@@ -292,8 +332,28 @@ def parse_cmd(self):
292332 elif client .startswith ('runner' ):
293333 low ['fun' ] = args .pop (0 )
294334 for arg in args :
295- key , value = arg .split ('=' , 1 )
296- low [key ] = value
335+ if '=' in arg :
336+ key , value = arg .split ('=' , 1 )
337+ low [key ] = value
338+ else :
339+ low .setdefault ('args' , []).append (arg )
340+ elif client .startswith ('wheel' ):
341+ low ['fun' ] = args .pop (0 )
342+ for arg in args :
343+ if '=' in arg :
344+ key , value = arg .split ('=' , 1 )
345+ low [key ] = value
346+ else :
347+ low .setdefault ('args' , []).append (arg )
348+ elif client .startswith ('ssh' ):
349+ if len (args ) < 2 :
350+ self .parser .error ("Command or target not specified" )
351+
352+ low ['expr_form' ] = self .options .expr_form
353+ low ['tgt' ] = args .pop (0 )
354+ low ['fun' ] = args .pop (0 )
355+ low ['batch' ] = self .options .batch
356+ low ['arg' ] = args
297357 else :
298358 if len (args ) < 1 :
299359 self .parser .error ("Command not specified" )
@@ -312,29 +372,37 @@ def poll_for_returns(self, api, load):
312372 async_ret = api .low (load )
313373 jid = async_ret ['return' ][0 ]['jid' ]
314374 nodes = async_ret ['return' ][0 ]['minions' ]
375+ ret_nodes = []
376+ exit_code = 1
315377
316378 # keep trying until all expected nodes return
317- total_time = self .seconds_to_wait
379+ total_time = 0
380+ start_time = time .time ()
318381 ret = {}
319382 exit_code = 0
320383 while True :
384+ total_time = time .time () - start_time
321385 if total_time > self .options .timeout :
386+ exit_code = 1
322387 break
323388
324389 jid_ret = api .lookup_jid (jid )
390+ responded = set (jid_ret ['return' ][0 ].keys ()) ^ set (ret_nodes )
391+ for node in responded :
392+ yield None , "{{{}: {}}}" .format (
393+ node ,
394+ jid_ret ['return' ][0 ][node ])
325395 ret_nodes = list (jid_ret ['return' ][0 ].keys ())
326396
327397 if set (ret_nodes ) == set (nodes ):
328- ret = jid_ret
329398 exit_code = 0
330399 break
331400 else :
332- exit_code = 1
333401 time .sleep (self .seconds_to_wait )
334- continue
335402
336403 exit_code = exit_code if self .options .fail_if_minions_dont_respond else 0
337- return exit_code , ret
404+ yield exit_code , "{{Failed: {}}}" .format (
405+ list (set (ret_nodes ) ^ set (nodes )))
338406
339407 def run (self ):
340408 '''
@@ -347,15 +415,39 @@ def run(self):
347415 logger .setLevel (max (logging .ERROR - (self .options .verbose * 10 ), 1 ))
348416
349417 load = self .parse_cmd ()
350- creds = iter (self .parse_login ())
351418
352- api = pepper .Pepper (next (creds ), debug_http = self .options .debug_http , ignore_ssl_errors = self .options .ignore_ssl_certificate_errors )
353- auth = api .login (* list (creds ))
419+ api = pepper .Pepper (
420+ self .parse_url (),
421+ debug_http = self .options .debug_http ,
422+ ignore_ssl_errors = self .options .ignore_ssl_certificate_errors )
423+ if self .options .mktoken :
424+ token_file = os .path .join (os .path .expanduser ('~' ), '.peppercache' )
425+ try :
426+ with open (token_file , 'rt' ) as f :
427+ api .auth = json .load (f )
428+ if api .auth ['expire' ] < time .time ()+ 30 :
429+ logger .error ('Login token expired' )
430+ raise Exception ('Login token expired' )
431+ except Exception as e :
432+ if e .args [0 ] is not 2 :
433+ logger .error ('Unable to load login token from ~/.peppercache ' + str (e ))
434+ auth = api .login (* self .parse_login ())
435+ try :
436+ oldumask = os .umask (0 )
437+ fdsc = os .open (token_file , os .O_WRONLY | os .O_CREAT , 0o600 )
438+ with os .fdopen (fdsc , 'wt' ) as f :
439+ json .dump (auth , f )
440+ except Exception as e :
441+ logger .error ('Unable to save token to ~/.pepperache ' + str (e ))
442+ finally :
443+ os .umask (oldumask )
444+ else :
445+ auth = api .login (* self .parse_login ())
354446
355447 if self .options .fail_if_minions_dont_respond :
356- exit_code , ret = self .poll_for_returns (api , load )
448+ for exit_code , ret in self .poll_for_returns (api , load ):
449+ yield exit_code , json .dumps (ret , sort_keys = True , indent = 4 )
357450 else :
358451 ret = api .low (load )
359452 exit_code = 0
360-
361- return (exit_code , json .dumps (ret , sort_keys = True , indent = 4 ))
453+ yield exit_code , json .dumps (ret , sort_keys = True , indent = 4 )
0 commit comments