This file is indexed.

/usr/lib/python3/dist-packages/pepper/cli.py is in salt-pepper 0.5.2-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
'''
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)