From e03b610795be46d0ce99c94de06703a164b28881 Mon Sep 17 00:00:00 2001 From: Gennadiy Shvetsov Date: Mon, 5 Sep 2016 18:41:40 +0200 Subject: [PATCH 01/15] client=ssh added --- pepper/cli.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pepper/cli.py b/pepper/cli.py index 0def2a5..daa5393 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -294,6 +294,15 @@ def parse_cmd(self): for arg in args: key, value = arg.split('=', 1) low[key] = value + elif client.startswith('ssh'): + 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['batch'] = self.options.batch + low['arg'] = args else: if len(args) < 1: self.parser.error("Command not specified") From 7cef8636dfb6fd8bcaad79fbebf8a6a308b7655f Mon Sep 17 00:00:00 2001 From: Ken Crowell Date: Tue, 6 Sep 2016 15:59:11 -0300 Subject: [PATCH 02/15] Add pillar targeting options --- pepper/cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pepper/cli.py b/pepper/cli.py index 0def2a5..b29899a 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -140,6 +140,14 @@ def add_tgtopts(self): action='store_const', const='grain_pcre', help="Target based on PCRE matches on system properties") + optgroup.add_option('-I', '--pillar', dest='expr_form', + action='store_const', const='pillar', + help="Target based on pillar values") + + optgroup.add_option('--pillar-pcre', dest='expr_form', + action='store_const', const='pillar_pcre', + help="Target based on PCRE matches on pillar values") + optgroup.add_option('-R', '--range', dest='expr_form', action='store_const', const='range', help="Target based on range expression") From 94bb0a5279baebfd538ce386705c721e5d08d0ec Mon Sep 17 00:00:00 2001 From: Oliver Tupman Date: Tue, 11 Oct 2016 07:50:01 +0100 Subject: [PATCH 03/15] Resolves reference error If one uses `req` to call Salt API endpoints rather than run commands then `req` is not called with a value for `data`; this would break because the variable `postdata`, which must be defined, is only defined if `data is not None`. --- pepper/libpepper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pepper/libpepper.py b/pepper/libpepper.py index 7e9219e..ba6413c 100644 --- a/pepper/libpepper.py +++ b/pepper/libpepper.py @@ -110,6 +110,8 @@ def req(self, path, data=None): if data is not None: postdata = json.dumps(data).encode() clen = len(postdata) + else: + postdata = None # Create request object url = self._construct_url(path) From 7a4d6d0133a5df942681b5efcfff42a910cd3e75 Mon Sep 17 00:00:00 2001 From: m03 Date: Wed, 19 Oct 2016 15:06:42 -0700 Subject: [PATCH 04/15] Add runner examples --- README.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index eedb20e..991a1af 100644 --- a/README.rst +++ b/README.rst @@ -37,7 +37,14 @@ Basic usage is in heavy flux. export SALTAPI_USER=saltdev SALTAPI_PASS=saltdev SALTAPI_EAUTH=pam pepper '*' test.ping pepper '*' test.kwarg hello=dolly - + +Examples leveraging the runner client. + +.. code-block:: bash + + pepper --client runner reactor.list + pepper --client runner reactor.add event='test/provision/*' reactors='/srv/salt/state/reactor/test-provision.sls' + Configuration ------------- From 8b75f317fa4989dc1532bbfbea1dba3f52cdeedb Mon Sep 17 00:00:00 2001 From: Seth House Date: Thu, 20 Oct 2016 10:29:16 -0600 Subject: [PATCH 05/15] Fix 'Pepper' object has no attribute 'ignore_ssl_errors' for kerberos --- pepper/libpepper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pepper/libpepper.py b/pepper/libpepper.py index 7e9219e..f0fc84c 100644 --- a/pepper/libpepper.py +++ b/pepper/libpepper.py @@ -173,7 +173,6 @@ def req_requests(self, path, data=None): if self.auth and 'token' in self.auth and self.auth['token']: headers.setdefault('X-Auth-Token', self.auth['token']) # Optionally toggle SSL verification - self._ssl_verify = self.ignore_ssl_errors params = {'url': self._construct_url(path), 'headers': headers, 'verify': self._ssl_verify == True, From 34ca904b6a82da8a90ef1b3932799cae5a151ab3 Mon Sep 17 00:00:00 2001 From: Leo Zhou Date: Thu, 27 Oct 2016 00:03:11 +0800 Subject: [PATCH 06/15] Fix #88: Resovle the infinite loop issue when some minions did not return --- pepper/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pepper/cli.py b/pepper/cli.py index 7a20452..65f9c14 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -331,11 +331,14 @@ def poll_for_returns(self, api, load): nodes = async_ret['return'][0]['minions'] # keep trying until all expected nodes return - total_time = self.seconds_to_wait + total_time = 0 + start_time = time.time() ret = {} exit_code = 0 while True: + total_time = time.time() - start_time if total_time > self.options.timeout: + exit_code = 1 break jid_ret = api.lookup_jid(jid) From 1ae30f036c78e9f730bad8d1a28b2d551358ae9d Mon Sep 17 00:00:00 2001 From: nbuchanan Date: Fri, 14 Oct 2016 11:00:18 -0600 Subject: [PATCH 07/15] Added default timeout value Separated methods to get salt-master url and creds Implemented continues printing of async results until timeout reached Implemented token storage --- pepper/cli.py | 122 ++++++++++++++++++++++++++++++-------------- pepper/libpepper.py | 5 +- scripts/pepper | 8 +-- 3 files changed, 89 insertions(+), 46 deletions(-) diff --git a/pepper/cli.py b/pepper/cli.py index 65f9c14..e2ca877 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -7,6 +7,7 @@ import logging import optparse import os +import pickle import textwrap import getpass import time @@ -41,15 +42,12 @@ def emit(self, record): pass class PepperCli(object): - def __init__(self, default_timeout_in_seconds=60 * 60, seconds_to_wait=3): + def __init__(self, seconds_to_wait=3): self.seconds_to_wait = seconds_to_wait self.parser = self.get_parser() self.parser.option_groups.extend([self.add_globalopts(), self.add_tgtopts(), self.add_authopts()]) - self.parser.defaults.update({'timeout': default_timeout_in_seconds, - 'fail_if_minions_dont_respond': False, - 'expr_form': 'glob'}) def get_parser(self): return optparse.OptionParser( @@ -93,7 +91,7 @@ def add_globalopts(self): "Mimic the ``salt`` CLI") optgroup.add_option('-t', '--timeout', dest='timeout', type='int', - help=textwrap.dedent('''\ + default=60, help=textwrap.dedent('''\ Specify wait time (in seconds) before returning control to the shell''')) @@ -109,7 +107,7 @@ def add_globalopts(self): # help="Redirect the output from a command to a persistent data store") optgroup.add_option('--fail-if-incomplete', action='store_true', - dest='fail_if_minions_dont_respond', + dest='fail_if_minions_dont_respond', default=False, help=textwrap.dedent('''\ Return a failure exit code if not all minions respond. This option requires the authenticated user have access to run the @@ -124,6 +122,8 @@ def add_tgtopts(self): optgroup = optparse.OptionGroup(self.parser, "Targeting Options", "Target which minions to run commands on") + optgroup.defaults.update({'expr_form': 'glob'}) + optgroup.add_option('-E', '--pcre', dest='expr_form', action='store_const', const='pcre', help="Target hostnames using PCRE regular expressions") @@ -195,12 +195,12 @@ def add_authopts(self): action='store_false', dest='interactive', help=textwrap.dedent("""\ Optional, fail rather than waiting for input"""), default=True) - # optgroup.add_option('-T', '--make-token', default=False, - # dest='mktoken', action='store_true', - # help=textwrap.dedent("""\ - # Generate and save an authentication token for re-use. The token is - # generated and made available for the period defined in the Salt - # Master.""")) + optgroup.add_option('-T', '--make-token', default=False, + dest='mktoken', action='store_true', + help=textwrap.dedent("""\ + Generate and save an authentication token for re-use. The token is + generated and made available for the period defined in the Salt + Master.""")) return optgroup @@ -214,7 +214,6 @@ def get_login_details(self): # setting default values results = { - 'SALTAPI_URL': 'https://localhost:8000/', 'SALTAPI_USER': None, 'SALTAPI_PASS': None, 'SALTAPI_EAUTH': 'auto', @@ -229,18 +228,14 @@ def get_login_details(self): # read file profile = 'main' if config.has_section(profile): - for key, value in config.items(profile): - key = key.upper() - results[key] = config.get(profile, key) + for key, value in list(results.items()): + if config.has_option(profile, key): + results[key] = config.get(profile, key) # get environment values for key, value in list(results.items()): results[key] = os.environ.get(key, results[key]) - # get eauth prompt options - if self.options.saltapiurl: - results['SALTAPI_URL'] = self.options.saltapiurl - if results['SALTAPI_EAUTH'] == 'kerberos': results['SALTAPI_PASS'] = None @@ -253,7 +248,8 @@ def get_login_details(self): logger.error("SALTAPI_USER required") raise SystemExit(1) else: - if self.options.username is not None: results['SALTAPI_USER'] = self.options.username + if self.options.username is not None: + results['SALTAPI_USER'] = self.options.username if self.options.password is None and results['SALTAPI_PASS'] is None: if self.options.interactive: results['SALTAPI_PASS'] = getpass.getpass(prompt='Password: ') @@ -261,23 +257,50 @@ def get_login_details(self): logger.error("SALTAPI_PASS required") raise SystemExit(1) else: - if self.options.password is not None: results['SALTAPI_PASS'] = self.options.password + if self.options.password is not None: + results['SALTAPI_PASS'] = self.options.password return results + def parse_url(self): + ''' + Determine api url + ''' + url = 'https://localhost:8000/' + + try: + config = ConfigParser(interpolation=None) + except TypeError as e: + config = ConfigParser.RawConfigParser() + config.read(self.options.config) + + # read file + profile = 'main' + if config.has_section(profile): + if config.has_option(profile, "SALTAPI_URL"): + url = config.get(profile, "SALTAPI_URL") + + # get environment values + url = os.environ.get("SALTAPI_URL", url) + + # get eauth prompt options + if self.options.saltapiurl: + url = self.options.saltapiurl + + return url + 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'] + # Auth values placeholder; grab interactively at CLI or from config user = login_details['SALTAPI_USER'] passwd = login_details['SALTAPI_PASS'] eauth = login_details['SALTAPI_EAUTH'] - return url, user, passwd, eauth + return user, passwd, eauth def parse_cmd(self): ''' @@ -329,6 +352,8 @@ def poll_for_returns(self, api, load): async_ret = api.low(load) jid = async_ret['return'][0]['jid'] nodes = async_ret['return'][0]['minions'] + ret_nodes = [] + exit_code = 1 # keep trying until all expected nodes return total_time = 0 @@ -342,19 +367,22 @@ def poll_for_returns(self, api, load): break jid_ret = api.lookup_jid(jid) + responded = set(jid_ret['return'][0].keys()) ^ set(ret_nodes) + for node in responded: + yield None, "{{{}: {}}}".format( + node, + jid_ret['return'][0][node]) ret_nodes = list(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 + yield exit_code, "{{Failed: {}}}".format( + list(set(ret_nodes) ^ set(nodes))) def run(self): ''' @@ -362,20 +390,36 @@ def run(self): ''' 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() - creds = iter(self.parse_login()) - api = pepper.Pepper(next(creds), debug_http=self.options.debug_http, ignore_ssl_errors=self.options.ignore_ssl_certificate_errors) - auth = api.login(*list(creds)) + 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: + try: + api.auth = pickle.load( + open(os.path.join(os.path.expanduser('~'), '.peppercache'), "rb")) + if api.auth['expire'] < time.time()+30: + logger.error('Login token expired') + raise + except Exception as e: + if e.args[0] is not 2: + logger.error('Unable to load login token from ~/.peppercache '+str(e)) + auth = api.login(*self.parse_login()) + try: + pickle.dump( + auth, + open(os.path.join(os.path.expanduser('~'), '.peppercache'), 'wb')) + except Exception as e: + logger.error('Unable to save token to ~/.pepperache '+str(e)) + else: + auth = api.login(*self.parse_login()) if self.options.fail_if_minions_dont_respond: - exit_code, ret = self.poll_for_returns(api, load) + 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) exit_code = 0 - - return (exit_code, json.dumps(ret, sort_keys=True, indent=4)) + yield exit_code, json.dumps(ret, sort_keys=True, indent=4) diff --git a/pepper/libpepper.py b/pepper/libpepper.py index ba6413c..5a1a9f2 100644 --- a/pepper/libpepper.py +++ b/pepper/libpepper.py @@ -74,7 +74,7 @@ def __init__(self, api_url='https://localhost:8000', debug_http=False, ignore_ss ''' split = urlparse.urlsplit(api_url) - if not split.scheme in ['http', 'https']: + if split.scheme not in ['http', 'https']: raise PepperException("salt-api URL missing HTTP(s) protocol: {0}" .format(api_url)) @@ -178,7 +178,7 @@ def req_requests(self, path, data=None): self._ssl_verify = self.ignore_ssl_errors params = {'url': self._construct_url(path), 'headers': headers, - 'verify': self._ssl_verify == True, + 'verify': if self._ssl_verify is True, 'auth': auth, 'data': json.dumps(data), } @@ -301,7 +301,6 @@ def lookup_jid(self, jid): return self.runner('jobs.lookup_jid', jid='{0}'.format(jid)) - def runner(self, fun, **kwargs): ''' Run a single command using the ``runner`` client diff --git a/scripts/pepper b/scripts/pepper index d1788f4..d3fd1c8 100755 --- a/scripts/pepper +++ b/scripts/pepper @@ -23,10 +23,10 @@ logger.addHandler(NullHandler()) if __name__ == '__main__': try: cli = PepperCli() - exit_code, results = cli.run() - # TODO: temporary printing until Salt outputters are in place - print(results) - raise SystemExit(exit_code) + for exit_code, result in cli.run(): + print(result) + if exit_code is not None: + raise SystemExit(exit_code) except PepperException as exc: print('Pepper error: {0}'.format(exc), file=sys.stderr) raise SystemExit(1) From a847d4f1c4ca303ee1a1fb70436f6b6b85d1d35e Mon Sep 17 00:00:00 2001 From: Seth House Date: Wed, 26 Oct 2016 09:45:27 -0600 Subject: [PATCH 08/15] Merge remote-tracking branch 'upstream/pr/85' into develop --- pepper/cli.py | 17 ++++++----------- pepper/libpepper.py | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pepper/cli.py b/pepper/cli.py index e2ca877..1fb073e 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -13,10 +13,10 @@ import time try: # Python 3 - from configparser import ConfigParser + from configparser import ConfigParser, RawConfigParser except ImportError: # Python 2 - import ConfigParser + from ConfigParser import ConfigParser, RawConfigParser try: input = raw_input @@ -31,11 +31,6 @@ class NullHandler(logging.Handler): def emit(self, record): pass -try: - import configparser -except ImportError: - import ConfigParser as configparser - logging.basicConfig(format='%(levelname)s %(asctime)s %(module)s: %(message)s') logger = logging.getLogger('pepper') logger.addHandler(NullHandler()) @@ -221,8 +216,8 @@ def get_login_details(self): try: config = ConfigParser(interpolation=None) - except TypeError as e: - config = ConfigParser.RawConfigParser() + except TypeError: + config = RawConfigParser() config.read(self.options.config) # read file @@ -270,8 +265,8 @@ def parse_url(self): try: config = ConfigParser(interpolation=None) - except TypeError as e: - config = ConfigParser.RawConfigParser() + except TypeError: + config = RawConfigParser() config.read(self.options.config) # read file diff --git a/pepper/libpepper.py b/pepper/libpepper.py index 5a1a9f2..996273f 100644 --- a/pepper/libpepper.py +++ b/pepper/libpepper.py @@ -178,7 +178,7 @@ def req_requests(self, path, data=None): self._ssl_verify = self.ignore_ssl_errors params = {'url': self._construct_url(path), 'headers': headers, - 'verify': if self._ssl_verify is True, + 'verify': self._ssl_verify is True, 'auth': auth, 'data': json.dumps(data), } From 0bee5a7c0df2c0bc604c3844650aabf0136a0b8b Mon Sep 17 00:00:00 2001 From: Seth House Date: Fri, 28 Oct 2016 17:58:55 -0600 Subject: [PATCH 09/15] Store the token in JSON format Still stdlib and avoids potential malicious code in the cache file. --- pepper/cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pepper/cli.py b/pepper/cli.py index 1fb073e..46c9a68 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -7,7 +7,6 @@ import logging import optparse import os -import pickle import textwrap import getpass import time @@ -393,7 +392,7 @@ def run(self): ignore_ssl_errors=self.options.ignore_ssl_certificate_errors) if self.options.mktoken: try: - api.auth = pickle.load( + api.auth = json.load( open(os.path.join(os.path.expanduser('~'), '.peppercache'), "rb")) if api.auth['expire'] < time.time()+30: logger.error('Login token expired') @@ -403,7 +402,7 @@ def run(self): logger.error('Unable to load login token from ~/.peppercache '+str(e)) auth = api.login(*self.parse_login()) try: - pickle.dump( + json.dump( auth, open(os.path.join(os.path.expanduser('~'), '.peppercache'), 'wb')) except Exception as e: From 333c19a29a7d006ee956dc22f58b3a2297a8dd6c Mon Sep 17 00:00:00 2001 From: Seth House Date: Fri, 28 Oct 2016 18:07:03 -0600 Subject: [PATCH 10/15] Whitespace cleanup; use with block for file open --- pepper/cli.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pepper/cli.py b/pepper/cli.py index 46c9a68..6ed45b7 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -391,22 +391,22 @@ def run(self): debug_http=self.options.debug_http, ignore_ssl_errors=self.options.ignore_ssl_certificate_errors) if self.options.mktoken: + token_file = os.path.join(os.path.expanduser('~'), '.peppercache') try: - api.auth = json.load( - open(os.path.join(os.path.expanduser('~'), '.peppercache'), "rb")) + with open(token_file, 'rb') as f: + api.auth = json.load(f) if api.auth['expire'] < time.time()+30: logger.error('Login token expired') - raise + raise Exception('Login token expired') except Exception as e: - if e.args[0] is not 2: - logger.error('Unable to load login token from ~/.peppercache '+str(e)) - auth = api.login(*self.parse_login()) - try: - json.dump( - auth, - open(os.path.join(os.path.expanduser('~'), '.peppercache'), 'wb')) - except Exception as e: - logger.error('Unable to save token to ~/.pepperache '+str(e)) + if e.args[0] is not 2: + logger.error('Unable to load login token from ~/.peppercache '+str(e)) + auth = api.login(*self.parse_login()) + try: + with open(token_file, 'wb') as f: + json.dump(auth, f) + except Exception as e: + logger.error('Unable to save token to ~/.pepperache '+str(e)) else: auth = api.login(*self.parse_login()) From facf97aad34e9e9c989b915f2e2276368138d68f Mon Sep 17 00:00:00 2001 From: Seth House Date: Fri, 28 Oct 2016 18:24:42 -0600 Subject: [PATCH 11/15] Ensure the peppercache file is written with user-only perms --- pepper/cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pepper/cli.py b/pepper/cli.py index 6ed45b7..4289fdc 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -403,10 +403,14 @@ def run(self): logger.error('Unable to load login token from ~/.peppercache '+str(e)) auth = api.login(*self.parse_login()) try: - with open(token_file, 'wb') as f: + oldumask = os.umask(0) + fdsc = os.open(token_file, os.O_WRONLY | os.O_CREAT, 0o600) + with os.fdopen(fdsc, 'wb') as f: json.dump(auth, f) except Exception as e: logger.error('Unable to save token to ~/.pepperache '+str(e)) + finally: + os.umask(oldumask) else: auth = api.login(*self.parse_login()) From 8468f03e4f84ba19065f21e9539e728ae7a2be3b Mon Sep 17 00:00:00 2001 From: nbuchanan Date: Wed, 2 Nov 2016 08:55:46 -0600 Subject: [PATCH 12/15] Open peppercache in text mode --- pepper/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pepper/cli.py b/pepper/cli.py index 4289fdc..94a8603 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -393,7 +393,7 @@ def run(self): if self.options.mktoken: token_file = os.path.join(os.path.expanduser('~'), '.peppercache') try: - with open(token_file, 'rb') as f: + with open(token_file, 'rt') as f: api.auth = json.load(f) if api.auth['expire'] < time.time()+30: logger.error('Login token expired') @@ -405,7 +405,7 @@ def run(self): try: oldumask = os.umask(0) fdsc = os.open(token_file, os.O_WRONLY | os.O_CREAT, 0o600) - with os.fdopen(fdsc, 'wb') as f: + with os.fdopen(fdsc, 'wt') as f: json.dump(auth, f) except Exception as e: logger.error('Unable to save token to ~/.pepperache '+str(e)) From f7630a7238b4370633fe0b41ca1a975f7c74b2c7 Mon Sep 17 00:00:00 2001 From: nbuchanan Date: Fri, 18 Nov 2016 11:18:34 -0700 Subject: [PATCH 13/15] pass wheel parameters Separate args and kwargs for runner and wheel clients --- pepper/cli.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pepper/cli.py b/pepper/cli.py index 94a8603..15a2c38 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -317,8 +317,19 @@ def parse_cmd(self): elif client.startswith('runner'): low['fun'] = args.pop(0) for arg in args: - key, value = arg.split('=', 1) - low[key] = value + if '=' in arg: + key, value = arg.split('=', 1) + low[key] = value + else: + low.setdefault('args', []).append(arg) + elif client.startswith('wheel'): + low['fun'] = args.pop(0) + for arg in args: + if '=' in arg: + key, value = arg.split('=', 1) + low[key] = value + else: + low.setdefault('args', []).append(arg) elif client.startswith('ssh'): if len(args) < 2: self.parser.error("Command or target not specified") From f41d0c2a95e34409723e4942122619657dd7bdb9 Mon Sep 17 00:00:00 2001 From: Seth House Date: Wed, 7 Dec 2016 18:29:35 -0700 Subject: [PATCH 14/15] Re-add the logger verbosity code This was accidentally deleted in a commit with some code-shuffling. --- pepper/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pepper/cli.py b/pepper/cli.py index 94a8603..e3e0855 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -384,6 +384,10 @@ def run(self): ''' 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( From 7bfbeb0b47c93eaa24383f73e46b2c4cc5cf247d Mon Sep 17 00:00:00 2001 From: Seth House Date: Thu, 29 Dec 2016 12:38:34 -0700 Subject: [PATCH 15/15] Allow JSON input as the request body --- pepper/cli.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pepper/cli.py b/pepper/cli.py index 8c42859..0962c3e 100644 --- a/pepper/cli.py +++ b/pepper/cli.py @@ -94,6 +94,13 @@ def add_globalopts(self): specify the salt-api client to use (local, local_async, runner, etc)''')) + optgroup.add_option('--json', dest='json_input', + help=textwrap.dedent('''\ + Enter JSON at the CLI instead of positional (text) arguments. This + is useful for arguments that need complex data structures. + Specifying this argument will cause positional arguments to be + ignored.''')) + # optgroup.add_option('--out', '--output', dest='output', # help="Specify the output format for the command output") @@ -300,6 +307,14 @@ def parse_cmd(self): ''' Extract the low data for a command from the passed CLI params ''' + # Short-circuit if JSON was given. + if self.options.json_input: + try: + return json.loads(self.options.json_input) + except ValueError: + logger.error("Invalid JSON given.") + raise SystemExit(1) + args = list(self.args) client = self.options.client if not self.options.batch else 'local_batch'