salt-pepper-0.5.2/0000755000076500000240000000000013210122354014322 5ustar danielstaff00000000000000salt-pepper-0.5.2/PKG-INFO0000644000076500000240000000157013210122354015422 0ustar danielstaff00000000000000Metadata-Version: 1.1 Name: salt-pepper Version: 0.5.2 Summary: A CLI front-end to a running salt-api system Home-page: http://saltstack.com Author: Seth House Author-email: shouse@saltstack.com License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Cython Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Topic :: System :: Clustering Classifier: Topic :: System :: Distributed Computing salt-pepper-0.5.2/setup.py0000644000076500000240000000733313210122254016041 0ustar danielstaff00000000000000#!/usr/bin/env python ''' A CLI front-end to a running salt-api system ''' import json import os import re from distutils.core import setup from distutils.dist import Distribution from distutils.command import sdist, install_data setup_kwargs = { 'name': 'salt-pepper', 'description': __doc__.strip(), 'author': 'Seth House', 'author_email': 'shouse@saltstack.com', 'url': 'http://saltstack.com', 'classifiers': [ 'Programming Language :: Python', 'Programming Language :: Cython', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', 'Operating System :: POSIX :: Linux', 'Topic :: System :: Clustering', 'Topic :: System :: Distributed Computing', ], 'packages': [ 'pepper', ], 'package_data': { 'pepper': ['version.json'], }, 'scripts': [ 'scripts/pepper', ] } def read_version_tag(): git_dir = os.path.join(os.path.dirname(__file__), '.git') if os.path.isdir(git_dir): import subprocess try: p = subprocess.Popen(['git', 'describe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() except Exception: pass else: return out.strip() or None else: return read_version_from_json_file() return None def read_version_from_json_file(): with open(os.path.join(os.path.dirname(__file__), "pepper", "version.json")) as f: return json.load(f)['version'] def parse_version_tag(tag): ''' Parse the output from Git describe Returns a tuple of the version number, number of commits (if any), and the Git SHA (if available). ''' if isinstance(tag, bytes): tag = tag.decode() if not tag or '-g' not in tag: return tag, None, None match = re.search('(?P.*)-(?P[0-9]+)-g(?P[0-9a-fA-F]+)', tag) if not match: return tag, None, None match_dict = match.groupdict() return ( match_dict.get('version'), match_dict.get('num_commits'), match_dict.get('sha')) def get_version(): ''' Return a tuple of the version and Git SHA ''' version, num_commits, sha = parse_version_tag(read_version_tag()) if sha: version = '{0}.dev{1}'.format(version, num_commits) return version, sha def write_version_file(base_dir): ver_path = os.path.join(base_dir, 'pepper', 'version.json') version, sha = get_version() with open(ver_path, 'wb') as f: json.dump({'version': version, 'sha': sha}, f) class PepperSdist(sdist.sdist): ''' Write the version.json file to the sdist tarball build directory ''' def make_release_tree(self, base_dir, files): sdist.sdist.make_release_tree(self, base_dir, files) write_version_file(base_dir) class PepperInstallData(install_data.install_data): ''' Write the version.json file to the installation directory ''' def run(self): install_cmd = self.get_finalized_command('install') install_dir = getattr(install_cmd, 'install_lib') write_version_file(install_dir) return install_data.install_data.run(self) if __name__ == '__main__': version, sha = get_version() setup(cmdclass={ 'sdist': PepperSdist, 'install_data': PepperInstallData, }, version=version, git_sha=sha, **setup_kwargs) salt-pepper-0.5.2/scripts/0000755000076500000240000000000013210122354016011 5ustar danielstaff00000000000000salt-pepper-0.5.2/scripts/pepper0000755000076500000240000000227613137170346017255 0ustar danielstaff00000000000000#!/usr/bin/env python ''' A CLI interface to a remote salt-api instance ''' from __future__ import print_function import sys import logging from pepper.cli import PepperCli from pepper import PepperException try: from logging import NullHandler except ImportError: # Python < 2.7 class NullHandler(logging.Handler): def emit(self, record): pass logging.basicConfig(format='%(levelname)s %(asctime)s %(module)s: %(message)s') logger = logging.getLogger('pepper') logger.addHandler(NullHandler()) if __name__ == '__main__': try: cli = PepperCli() 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) except KeyboardInterrupt: # TODO: mimic CLI and output JID on ctrl-c raise SystemExit(0) except Exception as e: print(e) print('Uncaught Pepper error (increase verbosity for the full traceback).', file=sys.stderr) logger.debug('Uncaught traceback:', exc_info=True) raise SystemExit(1) salt-pepper-0.5.2/pepper/0000755000076500000240000000000013210122354015615 5ustar danielstaff00000000000000salt-pepper-0.5.2/pepper/version.json0000644000076500000240000000004113210122354020170 0ustar danielstaff00000000000000{"sha": null, "version": "0.5.2"}salt-pepper-0.5.2/pepper/libpepper.py0000644000076500000240000003254313210100616020155 0ustar danielstaff00000000000000''' A Python library for working with Salt's REST API (Specifically the rest_cherrypy netapi module.) ''' import json import logging import ssl try: ssl._create_default_https_context = ssl._create_stdlib_context except: pass try: from urllib.request import HTTPHandler, Request, urlopen, \ install_opener, build_opener from urllib.error import HTTPError, URLError import urllib.parse as urlparse except ImportError: from urllib2 import HTTPHandler, Request, urlopen, install_opener, build_opener, \ HTTPError, URLError import urlparse logger = logging.getLogger('pepper') class PepperException(Exception): pass class Pepper(object): ''' A thin wrapper for making HTTP calls to the salt-api rest_cherrpy REST interface >>> api = Pepper('https://localhost:8000') >>> api.login('saltdev', 'saltdev', 'pam') {"return": [ { "eauth": "pam", "expire": 1370434219.714091, "perms": [ "test.*" ], "start": 1370391019.71409, "token": "c02a6f4397b5496ba06b70ae5fd1f2ab75de9237", "user": "saltdev" } ] } >>> api.low([{'client': 'local', 'tgt': '*', 'fun': 'test.ping'}]) {u'return': [{u'ms-0': True, u'ms-1': True, u'ms-2': True, u'ms-3': True, u'ms-4': True}]} ''' def __init__(self, api_url='https://localhost:8000', debug_http=False, ignore_ssl_errors=False): ''' Initialize the class with the URL of the API :param api_url: Host or IP address of the salt-api URL; include the port number :param debug_http: Add a flag to urllib2 to output the HTTP exchange :param ignore_ssl_errors: Add a flag to urllib2 to ignore invalid SSL certificates :raises PepperException: if the api_url is misformed ''' split = urlparse.urlsplit(api_url) if split.scheme not in ['http', 'https']: raise PepperException("salt-api URL missing HTTP(s) protocol: {0}" .format(api_url)) self.api_url = api_url self.debug_http = int(debug_http) self._ssl_verify = not ignore_ssl_errors self.auth = {} def req_stream(self, path): ''' A thin wrapper to get a response from saltstack api. The body of the response will not be downloaded immediately. Make sure to close the connection after use. api = Pepper('http://ipaddress/api/') print(api.login('salt','salt','pam')) response = api.req_stream('/events') :param path: The path to the salt api resource :return: :class:`Response ` object :rtype: requests.Response ''' import requests headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', } if self.auth and 'token' in self.auth and self.auth['token']: headers.setdefault('X-Auth-Token', self.auth['token']) else: raise PepperException('Authentication required') return # Optionally toggle SSL verification #self._ssl_verify = self.ignore_ssl_errors params = {'url': self._construct_url(path), 'headers': headers, 'verify': self._ssl_verify == True, 'stream': True } try: resp = requests.get(**params) if resp.status_code == 401: raise PepperException(str(resp.status_code) + ':Authentication denied') return if resp.status_code == 500: raise PepperException(str(resp.status_code) + ':Server error.') return if resp.status_code == 404: raise PepperException(str(resp.status_code) +' :This request returns nothing.') return except PepperException as e: print(e) return return resp def req_get(self, path): ''' A thin wrapper from get http method of saltstack api api = Pepper('http://ipaddress/api/') print(api.login('salt','salt','pam')) print(api.req_get('/keys')) ''' import requests headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', } if self.auth and 'token' in self.auth and self.auth['token']: headers.setdefault('X-Auth-Token', self.auth['token']) else: raise PepperException('Authentication required') return # Optionally toggle SSL verification #self._ssl_verify = self.ignore_ssl_errors params = {'url': self._construct_url(path), 'headers': headers, 'verify': self._ssl_verify == True, } try: resp = requests.get(**params) if resp.status_code == 401: raise PepperException(str(resp.status_code) + ':Authentication denied') return if resp.status_code == 500: raise PepperException(str(resp.status_code) + ':Server error.') return if resp.status_code == 404: raise PepperException(str(resp.status_code) +' :This request returns nothing.') return except PepperException as e: print(e) return return resp.json() def req(self, path, data=None): ''' A thin wrapper around urllib2 to send requests and return the response If the current instance contains an authentication token it will be attached to the request as a custom header. :rtype: dictionary ''' if (hasattr(data, 'get') and data.get('eauth') == 'kerberos') or self.auth.get('eauth') == 'kerberos': return self.req_requests(path, data) headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', } handler = HTTPHandler(debuglevel=self.debug_http) opener = build_opener(handler) install_opener(opener) # Build POST data if data is not None: postdata = json.dumps(data).encode() clen = len(postdata) else: postdata = None # Create request object url = self._construct_url(path) req = Request(url, postdata, headers) # Add POST data to request if data is not None: req.add_header('Content-Length', clen) # Add auth header to request if self.auth and 'token' in self.auth and self.auth['token']: req.add_header('X-Auth-Token', self.auth['token']) # Send request try: if not (self._ssl_verify): con = ssl.SSLContext(ssl.PROTOCOL_SSLv23) #con.check_hostname = False #con.verify_mode = ssl.CERT_NONE f = urlopen(req, context=con) else: f = urlopen(req) ret = json.loads(f.read().decode('utf-8')) except (HTTPError, URLError) as exc: logger.debug('Error with request', exc_info=True) status = getattr(exc, 'code', None) if status == 401: raise PepperException('Authentication denied') if status == 500: raise PepperException('Server error.') logger.error('Error with request: {0}'.format(exc)) raise except AttributeError: logger.debug('Error converting response from JSON', exc_info=True) raise PepperException('Unable to parse the server response.') return ret def req_requests(self, path, data=None): ''' A thin wrapper around request and request_kerberos to send requests and return the response If the current instance contains an authentication token it will be attached to the request as a custom header. :rtype: dictionary ''' import requests from requests_kerberos import HTTPKerberosAuth, OPTIONAL auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL) headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', } if self.auth and 'token' in self.auth and self.auth['token']: headers.setdefault('X-Auth-Token', self.auth['token']) # Optionally toggle SSL verification params = {'url': self._construct_url(path), 'headers': headers, 'verify': self._ssl_verify is True, 'auth': auth, 'data': json.dumps(data), } logger.debug('postdata {0}'.format(params)) resp = requests.post(**params) if resp.status_code == 401: # TODO should be resp.raise_from_status raise PepperException('Authentication denied') if resp.status_code == 500: # TODO should be resp.raise_from_status raise PepperException('Server error.') return resp.json() def low(self, lowstate, path='/'): ''' Execute a command through salt-api and return the response :param string path: URL path to be joined with the API hostname :param list lowstate: a list of lowstate dictionaries ''' return self.req(path, lowstate) def local(self, tgt, fun, arg=None, kwarg=None, expr_form='glob', timeout=None, ret=None): ''' Run a single command using the ``local`` client Wraps :meth:`low`. ''' low = { 'client': 'local', 'tgt': tgt, 'fun': fun, } if arg: low['arg'] = arg if kwarg: low['kwarg'] = kwarg if expr_form: low['expr_form'] = expr_form if timeout: low['timeout'] = timeout if ret: low['ret'] = ret return self.low([low], path='/') def local_async(self, tgt, fun, arg=None, kwarg=None, expr_form='glob', timeout=None, ret=None): ''' Run a single command using the ``local_async`` client Wraps :meth:`low`. ''' low = { 'client': 'local_async', 'tgt': tgt, 'fun': fun, } if arg: low['arg'] = arg if kwarg: low['kwarg'] = kwarg if expr_form: low['expr_form'] = expr_form if timeout: low['timeout'] = timeout if ret: low['ret'] = ret return self.low([low], path='/') def local_batch(self, tgt, fun, arg=None, kwarg=None, expr_form='glob', batch='50%', ret=None): ''' Run a single command using the ``local_batch`` client Wraps :meth:`low`. ''' low = { 'client': 'local_batch', 'tgt': tgt, 'fun': fun, } if arg: low['arg'] = arg if kwarg: low['kwarg'] = kwarg if expr_form: low['expr_form'] = expr_form if batch: low['batch'] = batch if ret: low['ret'] = ret return self.low([low], path='/') def lookup_jid(self, jid): ''' Get job results Wraps :meth:`runner`. ''' return self.runner('jobs.lookup_jid', jid='{0}'.format(jid)) def runner(self, fun, arg=None, **kwargs): ''' Run a single command using the ``runner`` client Usage:: runner('jobs.lookup_jid', jid=12345) ''' low = { 'client': 'runner', 'fun': fun, } if arg: low['arg'] = arg low.update(kwargs) return self.low([low], path='/') def wheel(self, fun, arg=None, kwarg=None, **kwargs): ''' Run a single command using the ``wheel`` client Usage:: wheel('key.accept', match='myminion') ''' low = { 'client': 'wheel', 'fun': fun, } if arg: low['arg'] = arg if kwarg: low['kwarg'] = kwarg low.update(kwargs) return self.low([low], path='/') def login(self, username, password, eauth): ''' 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] return self.auth def _construct_url(self, path): ''' Construct the url to salt-api for the given path Args: path: the path to the salt-api resource >>> api = Pepper('https://localhost:8000/salt-api/') >>> api._construct_url('/login') 'https://localhost:8000/salt-api/login' ''' relative_path = path.lstrip('/') return urlparse.urljoin(self.api_url, relative_path) salt-pepper-0.5.2/pepper/__init__.py0000644000076500000240000000164413137170346017747 0ustar danielstaff00000000000000''' Pepper is a CLI front-end to salt-api ''' import json import os from pepper.libpepper import Pepper, PepperException __all__ = ('__version__', '__gitrev__', 'Pepper', 'PepperException') try: # First try to grab the version from the version.json build file. vfile = os.path.join(os.path.dirname(__file__), 'version.json') with open(vfile, 'rb') as f: data = f.read().decode("utf-8") ret = json.loads(data) version = ret.get('version') sha = ret.get('sha') except IOError: # Build version file doesn't exist; we may be running from a clone. setup_file = os.path.join(os.path.dirname(__file__), os.pardir, 'setup.py') if os.path.exists(setup_file): import imp setup = imp.load_source('pepper_setup', setup_file) version, sha = setup.get_version() else: version, sha = None, None __version__ = version or 'Unknown' __gitrev__ = sha salt-pepper-0.5.2/pepper/cli.py0000644000076500000240000004235713210100616016746 0ustar danielstaff00000000000000''' A CLI interface to a remote salt-api instance ''' from __future__ import print_function import json import logging import optparse import os import textwrap import getpass import time try: # Python 3 from configparser import ConfigParser, RawConfigParser except ImportError: # Python 2 from ConfigParser import ConfigParser, RawConfigParser try: input = raw_input except NameError: pass import pepper try: from logging import NullHandler except ImportError: # Python < 2.7 class NullHandler(logging.Handler): def emit(self, record): pass logging.basicConfig(format='%(levelname)s %(asctime)s %(module)s: %(message)s') logger = logging.getLogger('pepper') logger.addHandler(NullHandler()) class PepperCli(object): 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()]) def get_parser(self): return optparse.OptionParser( description=__doc__, usage='%prog [opts]', version=pepper.__version__) def parse(self): ''' Parse all args ''' self.parser.add_option('-c', dest='config', default=os.environ.get('PEPPERRC', os.path.join(os.path.expanduser('~'), '.pepperrc')), help=textwrap.dedent('''\ Configuration file location. Default is a file path in the "PEPPERRC" environment variable or ~/.pepperrc.''')) self.parser.add_option('-v', dest='verbose', default=0, action='count', help=textwrap.dedent('''\ Increment output verbosity; may be specified multiple times''')) self.parser.add_option('-H', '--debug-http', dest='debug_http', default=False, action='store_true', help=textwrap.dedent('''\ Output the HTTP request/response headers on stderr''')) self.parser.add_option('--ignore-ssl-errors', action='store_true', dest='ignore_ssl_certificate_errors', default=False, help=textwrap.dedent('''\ Ignore any SSL certificate that may be encountered. Note that it is recommended to resolve certificate errors for production.''')) self.options, self.args = self.parser.parse_args() def add_globalopts(self): ''' Misc global options ''' optgroup = optparse.OptionGroup(self.parser, "Pepper ``salt`` Options", "Mimic the ``salt`` CLI") optgroup.add_option('-t', '--timeout', dest='timeout', type='int', default=60, 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, 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('--json-file', dest='json_file', help=textwrap.dedent('''\ Specify file containing the JSON to be used by pepper''')) # optgroup.add_option('--out', '--output', dest='output', # help="Specify the output format for the command output") # optgroup.add_option('--return', default='', metavar='RETURNER', # 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', 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 `jobs.list_jobs` runner function.''')) return optgroup def add_tgtopts(self): ''' Targeting ''' 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") optgroup.add_option('-L', '--list', dest='expr_form', action='store_const', const='list', help="Specify a comma delimited list of hostnames") optgroup.add_option('-G', '--grain', dest='expr_form', action='store_const', const='grain', help="Target based on system properties") optgroup.add_option('--grain-pcre', dest='expr_form', 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") optgroup.add_option('-C', '--compound', dest='expr_form', action='store_const', const='compound', help="Target based on compound expression") optgroup.add_option('-N', '--nodegroup', dest='expr_form', action='store_const', const='nodegroup', help="Target based on a named nodegroup") optgroup.add_option('--batch', dest='batch', default=None) return optgroup def add_authopts(self): ''' Authentication options ''' optgroup = optparse.OptionGroup(self.parser, "Authentication Options", textwrap.dedent("""\ Authentication credentials can optionally be supplied via the environment variables: SALTAPI_URL, SALTAPI_USER, SALTAPI_PASS, SALTAPI_EAUTH. """)) optgroup.add_option('-u', '--saltapi-url', dest='saltapiurl', help="Specify the host url. Defaults to https://localhost:8080") optgroup.add_option('-a', '--auth', '--eauth', '--extended-auth', dest='eauth', help=textwrap.dedent("""\ Specify the external_auth backend to authenticate against and interactively prompt for credentials""")) optgroup.add_option('--username', dest='username', help=textwrap.dedent("""\ Optional, defaults to user name. will be prompt if empty unless --non-interactive""")) optgroup.add_option('--password', dest='password', help=textwrap.dedent("""\ Optional, but will be prompted unless --non-interactive""")) optgroup.add_option('--non-interactive', 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('-x', dest='cache', default=os.environ.get('PEPPERCACHE', os.path.join(os.path.expanduser('~'), '.peppercache')), help=textwrap.dedent('''\ Cache file location. Default is a file path in the "PEPPERCACHE" environment variable or ~/.peppercache.''')) return optgroup def get_login_details(self): ''' This parses the config file, environment variables and command line options and returns the config values Order of parsing: command line options, ~/.pepperrc, environment, defaults ''' # setting default values results = { 'SALTAPI_USER': None, 'SALTAPI_PASS': None, 'SALTAPI_EAUTH': 'auto', } try: config = ConfigParser(interpolation=None) except TypeError: config = RawConfigParser() config.read(self.options.config) # read file profile = 'main' if config.has_section(profile): 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]) if results['SALTAPI_EAUTH'] == 'kerberos': results['SALTAPI_PASS'] = None if self.options.eauth: results['SALTAPI_EAUTH'] = self.options.eauth if self.options.username is None and results['SALTAPI_USER'] is None: if self.options.interactive: results['SALTAPI_USER'] = input('Username: ') else: 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.password is None and results['SALTAPI_PASS'] is None: if self.options.interactive: results['SALTAPI_PASS'] = getpass.getpass(prompt='Password: ') else: logger.error("SALTAPI_PASS required") raise SystemExit(1) else: 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: config = 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 user = login_details['SALTAPI_USER'] passwd = login_details['SALTAPI_PASS'] eauth = login_details['SALTAPI_EAUTH'] return user, passwd, eauth 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) if self.options.json_file: try: with open(self.options.json_file, 'r') as json_content: try: return json.load(json_content) except ValueError: logger.error("Invalid JSON given.") raise SystemExit(1) except Exception as e: logger.error( 'Cannot open file: {0}, {1}'.format( self.options.json_file, repr(e) ) ) raise SystemExit(1) args = list(self.args) client = self.options.client if not self.options.batch else 'local_batch' 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['batch'] = self.options.batch low['arg'] = args elif client.startswith('runner'): 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('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") 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") low['fun'] = args.pop(0) low['arg'] = args 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'] ret_nodes = [] exit_code = 1 # keep trying until all expected nodes return 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) 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): exit_code = 0 break else: time.sleep(self.seconds_to_wait) exit_code = exit_code if self.options.fail_if_minions_dont_respond else 0 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() 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: 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()) 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))) finally: os.umask(oldumask) else: auth = api.login(*self.parse_login()) 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) exit_code = 0 yield exit_code, json.dumps(ret, sort_keys=True, indent=4)