Skip to content

Commit 59fcfd4

Browse files
committed
Merge remote-tracking branch 'upstream/develop'
2 parents 29b2b6a + 3084763 commit 59fcfd4

4 files changed

Lines changed: 155 additions & 56 deletions

File tree

README.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,14 @@ Basic usage is in heavy flux.
3737
export SALTAPI_USER=saltdev SALTAPI_PASS=saltdev SALTAPI_EAUTH=pam
3838
pepper '*' test.ping
3939
pepper '*' test.kwarg hello=dolly
40-
40+
41+
Examples leveraging the runner client.
42+
43+
.. code-block:: bash
44+
45+
pepper --client runner reactor.list
46+
pepper --client runner reactor.add event='test/provision/*' reactors='/srv/salt/state/reactor/test-provision.sls'
47+
4148
Configuration
4249
-------------
4350

pepper/cli.py

Lines changed: 139 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
import time
1313
try:
1414
# Python 3
15-
from configparser import ConfigParser
15+
from configparser import ConfigParser, RawConfigParser
1616
except ImportError:
1717
# Python 2
18-
import ConfigParser
18+
from ConfigParser import ConfigParser, RawConfigParser
1919

2020
try:
2121
input = raw_input
@@ -30,26 +30,18 @@
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-
3833
logging.basicConfig(format='%(levelname)s %(asctime)s %(module)s: %(message)s')
3934
logger = logging.getLogger('pepper')
4035
logger.addHandler(NullHandler())
4136

4237

4338
class 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

Comments
 (0)