Skip to content

Commit

Permalink
Merge pull request #114 from whiteinge/use-run-flag
Browse files Browse the repository at this point in the history
Add --run-uri flag to use the /token and /run endpoints instead of session tokens
  • Loading branch information
Mike Place authored Jan 23, 2018
2 parents 17bc651 + 7b05bbf commit de89d98
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 40 deletions.
104 changes: 76 additions & 28 deletions pepper/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ def add_authopts(self):
dest='password', help=textwrap.dedent("""\
Optional, but will be prompted unless --non-interactive"""))

optgroup.add_option('--token-expire',
dest='token_expire', help=textwrap.dedent("""\
Set eauth token expiry in seconds. Must be allowed per
user. See the `token_expire_user_override` Master setting
for more info."""))

optgroup.add_option('--non-interactive',
action='store_false', dest='interactive', help=textwrap.dedent("""\
Optional, fail rather than waiting for input"""), default=True)
Expand All @@ -207,6 +213,13 @@ def add_authopts(self):
generated and made available for the period defined in the Salt
Master."""))

optgroup.add_option('-r', '--run-uri', default=False,
dest='userun', action='store_true',
help=textwrap.dedent("""\
Use an eauth token from /token and send commands through the
/run URL instead of the traditional session token
approach."""))

optgroup.add_option('-x', dest='cache',
default=os.environ.get('PEPPERCACHE',
os.path.join(os.path.expanduser('~'), '.peppercache')),
Expand Down Expand Up @@ -253,6 +266,8 @@ def get_login_details(self):

if self.options.eauth:
results['SALTAPI_EAUTH'] = self.options.eauth
if self.options.token_expire:
results['SALTAPI_TOKEN_EXPIRE'] = self.options.token_expire
if self.options.username is None and results['SALTAPI_USER'] is None:
if self.options.interactive:
results['SALTAPI_USER'] = input('Username: ')
Expand Down Expand Up @@ -308,11 +323,17 @@ def parse_login(self):
login_details = self.get_login_details()

# Auth values placeholder; grab interactively at CLI or from config
user = login_details['SALTAPI_USER']
passwd = login_details['SALTAPI_PASS']
username = login_details['SALTAPI_USER']
password = login_details['SALTAPI_PASS']
eauth = login_details['SALTAPI_EAUTH']

return user, passwd, eauth
ret = dict(username=username, password=password, eauth=eauth)

token_expire = login_details.get('SALTAPI_TOKEN_EXPIRE', None)
if token_expire:
ret['token_expire'] = int(token_expire)

return ret

def parse_cmd(self):
'''
Expand Down Expand Up @@ -396,7 +417,7 @@ def poll_for_returns(self, api, load):
cache for returns for the job.
'''
load[0]['client'] = 'local_async'
async_ret = api.low(load)
async_ret = self.low(api, load)
jid = async_ret['return'][0]['jid']
nodes = async_ret['return'][0]['minions']
ret_nodes = []
Expand All @@ -413,7 +434,14 @@ def poll_for_returns(self, api, load):
exit_code = 1
break

jid_ret = api.lookup_jid(jid)
jid_ret = self.low(api, [{
'client': 'runner',
'fun': 'jobs.lookup_jid',
'kwarg': {
'jid': jid,
},
}])

responded = set(jid_ret['return'][0].keys()) ^ set(ret_nodes)
for node in responded:
yield None, "{{{}: {}}}".format(
Expand All @@ -431,51 +459,71 @@ def poll_for_returns(self, api, load):
yield exit_code, "{{Failed: {}}}".format(
list(set(ret_nodes) ^ set(nodes)))

def run(self):
'''
Parse all arguments and call salt-api
'''
self.parse()

# move logger instantiation to method?
logger.addHandler(logging.StreamHandler())
logger.setLevel(max(logging.ERROR - (self.options.verbose * 10), 1))

load = self.parse_cmd()
def login(self, api):
login = api.token if self.options.userun else api.login

api = pepper.Pepper(
self.parse_url(),
debug_http=self.options.debug_http,
ignore_ssl_errors=self.options.ignore_ssl_certificate_errors)
if self.options.mktoken:
token_file = self.options.cache
try:
with open(token_file, 'rt') as f:
api.auth = json.load(f)
if api.auth['expire'] < time.time()+30:
auth = json.load(f)
if auth['expire'] < time.time()+30:
logger.error('Login token expired')
raise Exception('Login token expired')
api.req('/stats')
except Exception as e:
if e.args[0] is not 2:
logger.error('Unable to load login token from {0} {1}'.format(token_file, str(e)))
auth = api.login(*self.parse_login())
logger.error('Unable to load login token from {0} {1}'
.format(token_file, str(e)))
auth = login(**self.parse_login())
try:
oldumask = os.umask(0)
fdsc = os.open(token_file, os.O_WRONLY | os.O_CREAT, 0o600)
with os.fdopen(fdsc, 'wt') as f:
json.dump(auth, f)
except Exception as e:
logger.error('Unable to save token to {0} {1}'.format(token_file, str(e)))
logger.error('Unable to save token to {0} {1}'
.format(token_file, str(e)))
finally:
os.umask(oldumask)
else:
auth = api.login(*self.parse_login())
auth = login(**self.parse_login())

api.auth = auth
self.auth = auth
return auth

def low(self, api, load):
path = '/run' if self.options.userun else '/'

if self.options.userun:
for i in load:
i['token'] = self.auth['token']

return api.low(load, path=path)

def run(self):
'''
Parse all arguments and call salt-api
'''
self.parse()

# move logger instantiation to method?
logger.addHandler(logging.StreamHandler())
logger.setLevel(max(logging.ERROR - (self.options.verbose * 10), 1))

load = self.parse_cmd()

api = pepper.Pepper(
self.parse_url(),
debug_http=self.options.debug_http,
ignore_ssl_errors=self.options.ignore_ssl_certificate_errors)

self.login(api)

if self.options.fail_if_minions_dont_respond:
for exit_code, ret in self.poll_for_returns(api, load):
yield exit_code, json.dumps(ret, sort_keys=True, indent=4)
else:
ret = api.low(load)
ret = self.low(api, load)
exit_code = 0
yield exit_code, json.dumps(ret, sort_keys=True, indent=4)
35 changes: 23 additions & 12 deletions pepper/libpepper.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ class Pepper(object):
u'ms-4': True}]}
'''
def __init__(self, api_url='https://localhost:8000', debug_http=False, ignore_ssl_errors=False):
def __init__(self,
api_url='https://localhost:8000',
debug_http=False,
ignore_ssl_errors=False):
'''
Initialize the class with the URL of the API
Expand Down Expand Up @@ -188,7 +191,8 @@ def req(self, path, data=None):
:rtype: dictionary
'''
if (hasattr(data, 'get') and data.get('eauth') == 'kerberos') or self.auth.get('eauth') == 'kerberos':
if ((hasattr(data, 'get') and data.get('eauth') == 'kerberos')
or self.auth.get('eauth') == 'kerberos'):
return self.req_requests(path, data)

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

return self.low([low], path='/')
return self.low([low])

def local_async(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
timeout=None, ret=None):
Expand Down Expand Up @@ -354,7 +358,7 @@ def local_async(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
if ret:
low['ret'] = ret

return self.low([low], path='/')
return self.low([low])

def local_batch(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
batch='50%', ret=None):
Expand Down Expand Up @@ -384,7 +388,7 @@ def local_batch(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
if ret:
low['ret'] = ret

return self.low([low], path='/')
return self.low([low])

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

low.update(kwargs)

return self.low([low], path='/')
return self.low([low])

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

low.update(kwargs)

return self.low([low], path='/')
return self.low([low])

def login(self, username, password, eauth):
def _send_auth(self, path, **kwargs):
return self.req(path, kwargs)

def login(self, **kwargs):
'''
Authenticate with salt-api and return the user permissions and
authentication token or an empty dict
'''
self.auth = self.req('/login', {
'username': username,
'password': password,
'eauth': eauth}).get('return', [{}])[0]
self.auth = self._send_auth('/login', **kwargs).get('return', [{}])[0]
return self.auth

def token(self, **kwargs):
'''
Get an eauth token from Salt for use with the /run URL
'''
self.auth = self._send_auth('/token', **kwargs)[0]
return self.auth

def _construct_url(self, path):
Expand Down
Empty file added tests/__init__.py
Empty file.
114 changes: 114 additions & 0 deletions tests/integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env python
'''
These integration tests will execute non-destructive commands against a real
Salt and salt-api instance. Those must be set up and started independently.
This will take several minutes to run.
Usage:
SALTAPI_URL=http://localhost:8000 \
SALTAPI_USER=saltdev \
SALTAPI_PASS=saltdev \
SALTAPI_EAUTH=auto \
python -m unittest tests.integration
'''
import itertools
import json
import os
import shutil
import subprocess
import tempfile
import time
import unittest

try:
SALTAPI_URL = os.environ['SALTAPI_URL']
SALTAPI_USER = os.environ['SALTAPI_USER']
SALTAPI_PASS = os.environ['SALTAPI_PASS']
SALTAPI_EAUTH = os.environ['SALTAPI_EAUTH']
except KeyError:
raise SystemExit('The following environment variables must be set: '
'SALTAPI_URL, SALTAPI_USER, SALTAPI_PASS, SALTAPI_EAUTH.')


def _pepper(*args):
'''
Wrapper to invoke Pepper with common params and inside an empty env
'''
def_args = [
'pepper',
'--saltapi-url={0}'.format(SALTAPI_URL),
'--username={0}'.format(SALTAPI_USER),
'--password={0}'.format(SALTAPI_PASS),
'--eauth={0}'.format(SALTAPI_EAUTH),
]

return subprocess.check_output(itertools.chain(def_args, args))


class TestVanilla(unittest.TestCase):
def _pepper(self, *args):
return json.loads(_pepper(*args))['return'][0]

def test_local(self):
'''Sanity-check: Has at least one minion'''
ret = self._pepper('*', 'test.ping')
self.assertTrue(ret.values()[0])

def test_run(self):
'''Run command via /run URI'''
ret = self._pepper('--run-uri', '*', 'test.ping')
self.assertTrue(ret.values()[0])

def test_long_local(self):
'''Test a long call blocks until the return'''
ret = self._pepper('*', 'test.sleep', '30')
self.assertTrue(ret.values()[0])


class TestPoller(unittest.TestCase):
def _pepper(self, *args):
return _pepper(*args).splitlines()[0]

def test_local_poll(self):
'''Test the returns poller for localclient'''
ret = self._pepper('--run-uri', '--fail-if-incomplete', '*', 'test.sleep', '30')
self.assertTrue('True' in ret)


class TestTokens(unittest.TestCase):
def setUp(self):
self.tokdir = tempfile.mkdtemp()
self.tokfile = os.path.join(self.tokdir, 'peppertok.json')

def tearDown(self):
shutil.rmtree(self.tokdir)

def _pepper(self, *args):
return json.loads(_pepper(*args))['return'][0]

def test_local_token(self):
'''Test local execution with token file'''
ret = self._pepper('-x', self.tokfile,
'--make-token', '--run-uri', '*', 'test.ping')
self.assertTrue(ret.values()[0])

def test_runner_token(self):
'''Test runner execution with token file'''
ret = self._pepper('-x', self.tokfile,
'--make-token', '--run-uri',
'--client', 'runner', 'test.metasyntactic')
self.assertTrue(ret[0] == 'foo')

def test_token_expire(self):
'''Test token override param'''
now = time.time()
self._pepper('-x', self.tokfile, '--make-token', '--run-uri',
'--token-expire', '94670856',
'*', 'test.ping')

with open(self.tokfile, 'r') as f:
token = json.load(f)
diff = (now + float(94670856)) - token['expire']
# Allow for 10-second window between request and master-side auth.
self.assertTrue(diff < 10)

0 comments on commit de89d98

Please sign in to comment.