From 75fb093ffbe095cec5c5cfe0c3367b265f32a096 Mon Sep 17 00:00:00 2001 From: Seth House Date: Fri, 17 Jul 2015 07:32:18 -0600 Subject: [PATCH 1/5] Do not automatically run jobs on behalf of the user We can not assume the user has permissions for certain commands as part of the default operation. This must be an opt-in. Fixes #40. --- pepper/cli.py | 41 +++++++---------------------------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/pepper/cli.py b/pepper/cli.py index 2d4bb05..4c86fd8 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -222,7 +222,8 @@ def run(self): if len(self.args) < 2: self.parser.error("Command not specified") - tgt, fun = self.args[0:2] + args = list(self.args) + tgt, fun = args.pop(0), args.pop(0) login_details = self.get_login_details() @@ -234,36 +235,8 @@ def run(self): api = pepper.Pepper(salturl, debug_http=self.options.debug_http) auth = api.login(saltuser, saltpass, salteauth) - nodesJidRet = api.local_async(tgt=tgt, fun='test.ping', expr_form=self.options.expr_form) - nodesJid = nodesJidRet['return'][0]['jid'] - time.sleep(self.seconds_to_wait) - nodesRet = api.lookup_jid(nodesJid) - - if fun == 'test.ping': - return (0,json.dumps(nodesRet['return'][0], sort_keys=True, indent=4)) - - nodes = nodesRet['return'][0].keys() - if nodes == []: - return (0,json.dumps({})) - - commandJidRet = api.local_async(tgt=nodes, fun=fun, arg=self.args[2:], expr_form='list') - commandJid = commandJidRet['return'][0]['jid'] - # keep trying until all expected nodes return - commandRet = api.lookup_jid(commandJid) - returnedNodes = commandRet['return'][0].keys() - total_time = self.seconds_to_wait - - while set(returnedNodes) != set(nodes): - if total_time > self.options.timeout : - break - - time.sleep(self.seconds_to_wait) - commandRet = api.lookup_jid(commandJid) - returnedNodes = commandRet['return'][0].keys() - - if set(returnedNodes) != set(nodes) and self.options.fail_if_minions_dont_respond is True: - exit_code = 1 - else: - exit_code = 0 - - return (exit_code,json.dumps(commandRet['return'][0], sort_keys=True, indent=4)) + + ret = api.local(tgt, fun, arg=args, kwarg=None, expr_form=self.options.expr_form) + exit_code = 0 + + return (exit_code, json.dumps(ret, sort_keys=True, indent=4)) From 4e69b94c8ec0fac1bbca5bb9f6968351e617a9d6 Mon Sep 17 00:00:00 2001 From: Seth House Date: Fri, 17 Jul 2015 08:51:23 -0600 Subject: [PATCH 2/5] Extract fetching auth creds and function into methods --- pepper/cli.py | 61 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/pepper/cli.py b/pepper/cli.py index 4c86fd8..c0aa515 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -209,6 +209,46 @@ def get_login_details(self): return results + def parse_login(self): + ''' + Extract the authentication credentials + ''' + login_details = self.get_login_details() + + # Auth values placeholder; grab interactively at CLI or from config file + url = login_details['SALTAPI_URL'] + user = login_details['SALTAPI_USER'] + passwd = login_details['SALTAPI_PASS'] + eauth = login_details['SALTAPI_EAUTH'] + + return url, user, passwd, eauth + + def parse_cmd(self): + ''' + Extract the low data for a command from the passed CLI params + ''' + args = list(self.args) + + client = self.options.client + low = {'client': client} + + if client.startswith('local'): + if len(args) < 2: + self.parser.error("Command or target not specified") + + low['expr_form'] = self.options.expr_form + low['tgt'] = args.pop(0) + low['fun'] = args.pop(0) + low['arg'] = args + else: + if len(args) < 1: + self.parser.error("Command not specified") + + low['fun'] = args.pop(0) + low['arg'] = args + + return [low] + def run(self): ''' Parse all arguments and call salt-api @@ -219,24 +259,13 @@ def run(self): logger.addHandler(logging.StreamHandler()) logger.setLevel(max(logging.ERROR - (self.options.verbose * 10), 1)) - if len(self.args) < 2: - self.parser.error("Command not specified") - - args = list(self.args) - tgt, fun = args.pop(0), args.pop(0) - - login_details = self.get_login_details() - - # Auth values placeholder; grab interactively at CLI or from config file - salturl = login_details['SALTAPI_URL'] - saltuser = login_details['SALTAPI_USER'] - saltpass = login_details['SALTAPI_PASS'] - salteauth = login_details['SALTAPI_EAUTH'] + load = self.parse_cmd() + creds = iter(self.parse_login()) - api = pepper.Pepper(salturl, debug_http=self.options.debug_http) - auth = api.login(saltuser, saltpass, salteauth) + api = pepper.Pepper(creds.next(), debug_http=self.options.debug_http) + auth = api.login(*list(creds)) - ret = api.local(tgt, fun, arg=args, kwarg=None, expr_form=self.options.expr_form) + ret = api.low(load) exit_code = 0 return (exit_code, json.dumps(ret, sort_keys=True, indent=4)) From 62e8dc97edc70910e096fe208c3e02daa550ec80 Mon Sep 17 00:00:00 2001 From: Seth House Date: Fri, 17 Jul 2015 08:51:57 -0600 Subject: [PATCH 3/5] Add --client CLI flag to change the client type for the request --- pepper/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pepper/cli.py b/pepper/cli.py index c0aa515..4a1de61 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -73,6 +73,10 @@ def add_globalopts(self): optgroup.add_option('-t', '--timeout', dest='timeout', type ='int', help="Specify wait time (in seconds) before returning control to the shell") + optgroup.add_option('--client', dest='client', default='local', + help=textwrap.dedent('''\ + specify the salt-api client to use (local, local_async, + runner, etc)''')) # optgroup.add_option('--out', '--output', dest='output', # help="Specify the output format for the command output") From c4c779e43df9b522bc5b95184b180285fe29cdeb Mon Sep 17 00:00:00 2001 From: Seth House Date: Fri, 17 Jul 2015 08:52:24 -0600 Subject: [PATCH 4/5] Fix > 80 columns help text --- pepper/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pepper/cli.py b/pepper/cli.py index 4a1de61..bf6fd4b 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -72,7 +72,10 @@ def add_globalopts(self): "Mimic the ``salt`` CLI") optgroup.add_option('-t', '--timeout', dest='timeout', type ='int', - help="Specify wait time (in seconds) before returning control to the shell") + help=textwrap.dedent('''\ + Specify wait time (in seconds) before returning control to the + shell''')) + optgroup.add_option('--client', dest='client', default='local', help=textwrap.dedent('''\ specify the salt-api client to use (local, local_async, From d8613c23124df6a9b51281cbf008185078e79eb2 Mon Sep 17 00:00:00 2001 From: Seth House Date: Fri, 17 Jul 2015 08:53:02 -0600 Subject: [PATCH 5/5] Move polling for minion returns into method; add perms warning to help --- pepper/cli.py | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/pepper/cli.py b/pepper/cli.py index bf6fd4b..d46b63b 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -89,7 +89,10 @@ def add_globalopts(self): optgroup.add_option('--fail-if-incomplete', action='store_true', dest='fail_if_minions_dont_respond', - help="Optional, return a failure exit code if not all minions respond") + help=textwrap.dedent('''\ + Return a failure exit code if not all minions respond. This option + requires the authenticated user have access to run the + `jobs.list_jobs` runner function.''')) return optgroup @@ -256,6 +259,39 @@ def parse_cmd(self): return [low] + def poll_for_returns(self, api, load): + ''' + Run a command with the local_async client and periodically poll the job + cache for returns for the job. + ''' + load[0]['client'] = 'local_async' + async_ret = api.low(load) + jid = async_ret['return'][0]['jid'] + nodes = async_ret['return'][0]['minions'] + + # keep trying until all expected nodes return + total_time = self.seconds_to_wait + ret = {} + exit_code = 0 + while True: + if total_time > self.options.timeout: + break + + jid_ret = api.lookup_jid(jid) + ret_nodes = jid_ret['return'][0].keys() + + if set(ret_nodes) == set(nodes): + ret = jid_ret + exit_code = 0 + break + else: + exit_code = 1 + time.sleep(self.seconds_to_wait) + continue + + exit_code = exit_code if self.options.fail_if_minions_dont_respond else 0 + return exit_code, ret + def run(self): ''' Parse all arguments and call salt-api @@ -272,7 +308,10 @@ def run(self): api = pepper.Pepper(creds.next(), debug_http=self.options.debug_http) auth = api.login(*list(creds)) - ret = api.low(load) - exit_code = 0 + if self.options.fail_if_minions_dont_respond: + exit_code, ret = self.poll_for_returns(api, load) + else: + ret = api.low(load) + exit_code = 0 return (exit_code, json.dumps(ret, sort_keys=True, indent=4))