Skip to content

Commit de89d98

Browse files
author
Mike Place
authored
Merge pull request #114 from whiteinge/use-run-flag
Add --run-uri flag to use the /token and /run endpoints instead of session tokens
2 parents 17bc651 + 7b05bbf commit de89d98

4 files changed

Lines changed: 213 additions & 40 deletions

File tree

pepper/cli.py

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,12 @@ def add_authopts(self):
196196
dest='password', help=textwrap.dedent("""\
197197
Optional, but will be prompted unless --non-interactive"""))
198198

199+
optgroup.add_option('--token-expire',
200+
dest='token_expire', help=textwrap.dedent("""\
201+
Set eauth token expiry in seconds. Must be allowed per
202+
user. See the `token_expire_user_override` Master setting
203+
for more info."""))
204+
199205
optgroup.add_option('--non-interactive',
200206
action='store_false', dest='interactive', help=textwrap.dedent("""\
201207
Optional, fail rather than waiting for input"""), default=True)
@@ -207,6 +213,13 @@ def add_authopts(self):
207213
generated and made available for the period defined in the Salt
208214
Master."""))
209215

216+
optgroup.add_option('-r', '--run-uri', default=False,
217+
dest='userun', action='store_true',
218+
help=textwrap.dedent("""\
219+
Use an eauth token from /token and send commands through the
220+
/run URL instead of the traditional session token
221+
approach."""))
222+
210223
optgroup.add_option('-x', dest='cache',
211224
default=os.environ.get('PEPPERCACHE',
212225
os.path.join(os.path.expanduser('~'), '.peppercache')),
@@ -253,6 +266,8 @@ def get_login_details(self):
253266

254267
if self.options.eauth:
255268
results['SALTAPI_EAUTH'] = self.options.eauth
269+
if self.options.token_expire:
270+
results['SALTAPI_TOKEN_EXPIRE'] = self.options.token_expire
256271
if self.options.username is None and results['SALTAPI_USER'] is None:
257272
if self.options.interactive:
258273
results['SALTAPI_USER'] = input('Username: ')
@@ -308,11 +323,17 @@ def parse_login(self):
308323
login_details = self.get_login_details()
309324

310325
# Auth values placeholder; grab interactively at CLI or from config
311-
user = login_details['SALTAPI_USER']
312-
passwd = login_details['SALTAPI_PASS']
326+
username = login_details['SALTAPI_USER']
327+
password = login_details['SALTAPI_PASS']
313328
eauth = login_details['SALTAPI_EAUTH']
314329

315-
return user, passwd, eauth
330+
ret = dict(username=username, password=password, eauth=eauth)
331+
332+
token_expire = login_details.get('SALTAPI_TOKEN_EXPIRE', None)
333+
if token_expire:
334+
ret['token_expire'] = int(token_expire)
335+
336+
return ret
316337

317338
def parse_cmd(self):
318339
'''
@@ -396,7 +417,7 @@ def poll_for_returns(self, api, load):
396417
cache for returns for the job.
397418
'''
398419
load[0]['client'] = 'local_async'
399-
async_ret = api.low(load)
420+
async_ret = self.low(api, load)
400421
jid = async_ret['return'][0]['jid']
401422
nodes = async_ret['return'][0]['minions']
402423
ret_nodes = []
@@ -413,7 +434,14 @@ def poll_for_returns(self, api, load):
413434
exit_code = 1
414435
break
415436

416-
jid_ret = api.lookup_jid(jid)
437+
jid_ret = self.low(api, [{
438+
'client': 'runner',
439+
'fun': 'jobs.lookup_jid',
440+
'kwarg': {
441+
'jid': jid,
442+
},
443+
}])
444+
417445
responded = set(jid_ret['return'][0].keys()) ^ set(ret_nodes)
418446
for node in responded:
419447
yield None, "{{{}: {}}}".format(
@@ -431,51 +459,71 @@ def poll_for_returns(self, api, load):
431459
yield exit_code, "{{Failed: {}}}".format(
432460
list(set(ret_nodes) ^ set(nodes)))
433461

434-
def run(self):
435-
'''
436-
Parse all arguments and call salt-api
437-
'''
438-
self.parse()
439-
440-
# move logger instantiation to method?
441-
logger.addHandler(logging.StreamHandler())
442-
logger.setLevel(max(logging.ERROR - (self.options.verbose * 10), 1))
443-
444-
load = self.parse_cmd()
462+
def login(self, api):
463+
login = api.token if self.options.userun else api.login
445464

446-
api = pepper.Pepper(
447-
self.parse_url(),
448-
debug_http=self.options.debug_http,
449-
ignore_ssl_errors=self.options.ignore_ssl_certificate_errors)
450465
if self.options.mktoken:
451466
token_file = self.options.cache
452467
try:
453468
with open(token_file, 'rt') as f:
454-
api.auth = json.load(f)
455-
if api.auth['expire'] < time.time()+30:
469+
auth = json.load(f)
470+
if auth['expire'] < time.time()+30:
456471
logger.error('Login token expired')
457472
raise Exception('Login token expired')
458-
api.req('/stats')
459473
except Exception as e:
460474
if e.args[0] is not 2:
461-
logger.error('Unable to load login token from {0} {1}'.format(token_file, str(e)))
462-
auth = api.login(*self.parse_login())
475+
logger.error('Unable to load login token from {0} {1}'
476+
.format(token_file, str(e)))
477+
auth = login(**self.parse_login())
463478
try:
464479
oldumask = os.umask(0)
465480
fdsc = os.open(token_file, os.O_WRONLY | os.O_CREAT, 0o600)
466481
with os.fdopen(fdsc, 'wt') as f:
467482
json.dump(auth, f)
468483
except Exception as e:
469-
logger.error('Unable to save token to {0} {1}'.format(token_file, str(e)))
484+
logger.error('Unable to save token to {0} {1}'
485+
.format(token_file, str(e)))
470486
finally:
471487
os.umask(oldumask)
472488
else:
473-
auth = api.login(*self.parse_login())
489+
auth = login(**self.parse_login())
490+
491+
api.auth = auth
492+
self.auth = auth
493+
return auth
494+
495+
def low(self, api, load):
496+
path = '/run' if self.options.userun else '/'
497+
498+
if self.options.userun:
499+
for i in load:
500+
i['token'] = self.auth['token']
501+
502+
return api.low(load, path=path)
503+
504+
def run(self):
505+
'''
506+
Parse all arguments and call salt-api
507+
'''
508+
self.parse()
509+
510+
# move logger instantiation to method?
511+
logger.addHandler(logging.StreamHandler())
512+
logger.setLevel(max(logging.ERROR - (self.options.verbose * 10), 1))
513+
514+
load = self.parse_cmd()
515+
516+
api = pepper.Pepper(
517+
self.parse_url(),
518+
debug_http=self.options.debug_http,
519+
ignore_ssl_errors=self.options.ignore_ssl_certificate_errors)
520+
521+
self.login(api)
474522

475523
if self.options.fail_if_minions_dont_respond:
476524
for exit_code, ret in self.poll_for_returns(api, load):
477525
yield exit_code, json.dumps(ret, sort_keys=True, indent=4)
478526
else:
479-
ret = api.low(load)
527+
ret = self.low(api, load)
480528
exit_code = 0
481529
yield exit_code, json.dumps(ret, sort_keys=True, indent=4)

pepper/libpepper.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ class Pepper(object):
5757
u'ms-4': True}]}
5858
5959
'''
60-
def __init__(self, api_url='https://localhost:8000', debug_http=False, ignore_ssl_errors=False):
60+
def __init__(self,
61+
api_url='https://localhost:8000',
62+
debug_http=False,
63+
ignore_ssl_errors=False):
6164
'''
6265
Initialize the class with the URL of the API
6366
@@ -188,7 +191,8 @@ def req(self, path, data=None):
188191
:rtype: dictionary
189192
190193
'''
191-
if (hasattr(data, 'get') and data.get('eauth') == 'kerberos') or self.auth.get('eauth') == 'kerberos':
194+
if ((hasattr(data, 'get') and data.get('eauth') == 'kerberos')
195+
or self.auth.get('eauth') == 'kerberos'):
192196
return self.req_requests(path, data)
193197

194198
headers = {
@@ -324,7 +328,7 @@ def local(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
324328
if ret:
325329
low['ret'] = ret
326330

327-
return self.low([low], path='/')
331+
return self.low([low])
328332

329333
def local_async(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
330334
timeout=None, ret=None):
@@ -354,7 +358,7 @@ def local_async(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
354358
if ret:
355359
low['ret'] = ret
356360

357-
return self.low([low], path='/')
361+
return self.low([low])
358362

359363
def local_batch(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
360364
batch='50%', ret=None):
@@ -384,7 +388,7 @@ def local_batch(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
384388
if ret:
385389
low['ret'] = ret
386390

387-
return self.low([low], path='/')
391+
return self.low([low])
388392

389393
def lookup_jid(self, jid):
390394
'''
@@ -411,7 +415,7 @@ def runner(self, fun, arg=None, **kwargs):
411415

412416
low.update(kwargs)
413417

414-
return self.low([low], path='/')
418+
return self.low([low])
415419

416420
def wheel(self, fun, arg=None, kwarg=None, **kwargs):
417421
'''
@@ -432,19 +436,26 @@ def wheel(self, fun, arg=None, kwarg=None, **kwargs):
432436

433437
low.update(kwargs)
434438

435-
return self.low([low], path='/')
439+
return self.low([low])
436440

437-
def login(self, username, password, eauth):
441+
def _send_auth(self, path, **kwargs):
442+
return self.req(path, kwargs)
443+
444+
def login(self, **kwargs):
438445
'''
439446
Authenticate with salt-api and return the user permissions and
440447
authentication token or an empty dict
441448
442449
'''
443-
self.auth = self.req('/login', {
444-
'username': username,
445-
'password': password,
446-
'eauth': eauth}).get('return', [{}])[0]
450+
self.auth = self._send_auth('/login', **kwargs).get('return', [{}])[0]
451+
return self.auth
447452

453+
def token(self, **kwargs):
454+
'''
455+
Get an eauth token from Salt for use with the /run URL
456+
457+
'''
458+
self.auth = self._send_auth('/token', **kwargs)[0]
448459
return self.auth
449460

450461
def _construct_url(self, path):

tests/__init__.py

Whitespace-only changes.

tests/integration.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/usr/bin/env python
2+
'''
3+
These integration tests will execute non-destructive commands against a real
4+
Salt and salt-api instance. Those must be set up and started independently.
5+
This will take several minutes to run.
6+
7+
Usage:
8+
9+
SALTAPI_URL=http://localhost:8000 \
10+
SALTAPI_USER=saltdev \
11+
SALTAPI_PASS=saltdev \
12+
SALTAPI_EAUTH=auto \
13+
python -m unittest tests.integration
14+
'''
15+
import itertools
16+
import json
17+
import os
18+
import shutil
19+
import subprocess
20+
import tempfile
21+
import time
22+
import unittest
23+
24+
try:
25+
SALTAPI_URL = os.environ['SALTAPI_URL']
26+
SALTAPI_USER = os.environ['SALTAPI_USER']
27+
SALTAPI_PASS = os.environ['SALTAPI_PASS']
28+
SALTAPI_EAUTH = os.environ['SALTAPI_EAUTH']
29+
except KeyError:
30+
raise SystemExit('The following environment variables must be set: '
31+
'SALTAPI_URL, SALTAPI_USER, SALTAPI_PASS, SALTAPI_EAUTH.')
32+
33+
34+
def _pepper(*args):
35+
'''
36+
Wrapper to invoke Pepper with common params and inside an empty env
37+
'''
38+
def_args = [
39+
'pepper',
40+
'--saltapi-url={0}'.format(SALTAPI_URL),
41+
'--username={0}'.format(SALTAPI_USER),
42+
'--password={0}'.format(SALTAPI_PASS),
43+
'--eauth={0}'.format(SALTAPI_EAUTH),
44+
]
45+
46+
return subprocess.check_output(itertools.chain(def_args, args))
47+
48+
49+
class TestVanilla(unittest.TestCase):
50+
def _pepper(self, *args):
51+
return json.loads(_pepper(*args))['return'][0]
52+
53+
def test_local(self):
54+
'''Sanity-check: Has at least one minion'''
55+
ret = self._pepper('*', 'test.ping')
56+
self.assertTrue(ret.values()[0])
57+
58+
def test_run(self):
59+
'''Run command via /run URI'''
60+
ret = self._pepper('--run-uri', '*', 'test.ping')
61+
self.assertTrue(ret.values()[0])
62+
63+
def test_long_local(self):
64+
'''Test a long call blocks until the return'''
65+
ret = self._pepper('*', 'test.sleep', '30')
66+
self.assertTrue(ret.values()[0])
67+
68+
69+
class TestPoller(unittest.TestCase):
70+
def _pepper(self, *args):
71+
return _pepper(*args).splitlines()[0]
72+
73+
def test_local_poll(self):
74+
'''Test the returns poller for localclient'''
75+
ret = self._pepper('--run-uri', '--fail-if-incomplete', '*', 'test.sleep', '30')
76+
self.assertTrue('True' in ret)
77+
78+
79+
class TestTokens(unittest.TestCase):
80+
def setUp(self):
81+
self.tokdir = tempfile.mkdtemp()
82+
self.tokfile = os.path.join(self.tokdir, 'peppertok.json')
83+
84+
def tearDown(self):
85+
shutil.rmtree(self.tokdir)
86+
87+
def _pepper(self, *args):
88+
return json.loads(_pepper(*args))['return'][0]
89+
90+
def test_local_token(self):
91+
'''Test local execution with token file'''
92+
ret = self._pepper('-x', self.tokfile,
93+
'--make-token', '--run-uri', '*', 'test.ping')
94+
self.assertTrue(ret.values()[0])
95+
96+
def test_runner_token(self):
97+
'''Test runner execution with token file'''
98+
ret = self._pepper('-x', self.tokfile,
99+
'--make-token', '--run-uri',
100+
'--client', 'runner', 'test.metasyntactic')
101+
self.assertTrue(ret[0] == 'foo')
102+
103+
def test_token_expire(self):
104+
'''Test token override param'''
105+
now = time.time()
106+
self._pepper('-x', self.tokfile, '--make-token', '--run-uri',
107+
'--token-expire', '94670856',
108+
'*', 'test.ping')
109+
110+
with open(self.tokfile, 'r') as f:
111+
token = json.load(f)
112+
diff = (now + float(94670856)) - token['expire']
113+
# Allow for 10-second window between request and master-side auth.
114+
self.assertTrue(diff < 10)

0 commit comments

Comments
 (0)