~canonical-ci-engineering/ubuntu-ci-services-itself/ansible

« back to all changes in this revision

Viewing changes to .pc/743027-fix.patch/lib/ansible/module_utils/basic.py

  • Committer: Package Import Robot
  • Author(s): Harlan Lieberman-Berg
  • Date: 2014-04-01 22:00:24 UTC
  • mfrom: (1.1.9)
  • Revision ID: package-import@ubuntu.com-20140401220024-jkxyhf2ggcv7xmqa
Tags: 1.5.4+dfsg-1
* Pull missing manpages from upstream development branch.
* New upstream version 1.5.4, security update.
* Add patch to correct directory_mode functionality. (Closes: #743027)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# This code is part of Ansible, but is an independent component.
 
2
# This particular file snippet, and this file snippet only, is BSD licensed.
 
3
# Modules you write using this snippet, which is embedded dynamically by Ansible
 
4
# still belong to the author of the module, and may assign their own license
 
5
# to the complete work.
 
6
 
7
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
 
8
# All rights reserved.
 
9
#
 
10
# Redistribution and use in source and binary forms, with or without modification, 
 
11
# are permitted provided that the following conditions are met:
 
12
#
 
13
#    * Redistributions of source code must retain the above copyright 
 
14
#      notice, this list of conditions and the following disclaimer.
 
15
#    * Redistributions in binary form must reproduce the above copyright notice, 
 
16
#      this list of conditions and the following disclaimer in the documentation 
 
17
#      and/or other materials provided with the distribution.
 
18
#
 
19
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 
20
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 
21
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 
22
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 
23
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 
24
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 
25
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 
26
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 
 
27
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
28
#
 
29
 
 
30
# == BEGIN DYNAMICALLY INSERTED CODE ==
 
31
 
 
32
MODULE_ARGS = "<<INCLUDE_ANSIBLE_MODULE_ARGS>>"
 
33
MODULE_LANG = "<<INCLUDE_ANSIBLE_MODULE_LANG>>"
 
34
MODULE_COMPLEX_ARGS = "<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"
 
35
 
 
36
BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 1]
 
37
BOOLEANS_FALSE = ['no', 'off', '0', 'false', 0]
 
38
BOOLEANS = BOOLEANS_TRUE + BOOLEANS_FALSE
 
39
 
 
40
# ansible modules can be written in any language.  To simplify
 
41
# development of Python modules, the functions available here
 
42
# can be inserted in any module source automatically by including
 
43
# #<<INCLUDE_ANSIBLE_MODULE_COMMON>> on a blank line by itself inside
 
44
# of an ansible module. The source of this common code lives
 
45
# in lib/ansible/module_common.py
 
46
 
 
47
import os
 
48
import re
 
49
import shlex
 
50
import subprocess
 
51
import sys
 
52
import syslog
 
53
import types
 
54
import time
 
55
import shutil
 
56
import stat
 
57
import traceback
 
58
import grp
 
59
import pwd
 
60
import platform
 
61
import errno
 
62
import tempfile
 
63
 
 
64
try:
 
65
    import json
 
66
except ImportError:
 
67
    try:
 
68
        import simplejson as json
 
69
    except ImportError:
 
70
        sys.stderr.write('Error: ansible requires a json module, none found!')
 
71
        sys.exit(1)
 
72
    except SyntaxError:
 
73
        sys.stderr.write('SyntaxError: probably due to json and python being for different versions')
 
74
        sys.exit(1)
 
75
 
 
76
HAVE_SELINUX=False
 
77
try:
 
78
    import selinux
 
79
    HAVE_SELINUX=True
 
80
except ImportError:
 
81
    pass
 
82
 
 
83
HAVE_HASHLIB=False
 
84
try:
 
85
    from hashlib import md5 as _md5
 
86
    HAVE_HASHLIB=True
 
87
except ImportError:
 
88
    from md5 import md5 as _md5
 
89
 
 
90
try:
 
91
    from hashlib import sha256 as _sha256
 
92
except ImportError:
 
93
    pass
 
94
 
 
95
try:
 
96
    from systemd import journal
 
97
    has_journal = True
 
98
except ImportError:
 
99
    import syslog
 
100
    has_journal = False
 
101
 
 
102
FILE_COMMON_ARGUMENTS=dict(
 
103
    src = dict(),
 
104
    mode = dict(),
 
105
    owner = dict(),
 
106
    group = dict(),
 
107
    seuser = dict(),
 
108
    serole = dict(),
 
109
    selevel = dict(),
 
110
    setype = dict(),
 
111
    # not taken by the file module, but other modules call file so it must ignore them.
 
112
    content = dict(),
 
113
    backup = dict(),
 
114
    force = dict(),
 
115
    remote_src = dict(), # used by assemble
 
116
)
 
117
 
 
118
 
 
119
def get_platform():
 
120
    ''' what's the platform?  example: Linux is a platform. '''
 
121
    return platform.system()
 
122
 
 
123
def get_distribution():
 
124
    ''' return the distribution name '''
 
125
    if platform.system() == 'Linux':
 
126
        try:
 
127
            distribution = platform.linux_distribution()[0].capitalize()
 
128
            if not distribution and os.path.isfile('/etc/system-release'):
 
129
                distribution = platform.linux_distribution(supported_dists=['system'])[0].capitalize()
 
130
                if 'Amazon' in distribution:
 
131
                    distribution = 'Amazon'
 
132
                else:
 
133
                    distribution = 'OtherLinux'
 
134
        except:
 
135
            # FIXME: MethodMissing, I assume?
 
136
            distribution = platform.dist()[0].capitalize()
 
137
    else:
 
138
        distribution = None
 
139
    return distribution
 
140
 
 
141
def load_platform_subclass(cls, *args, **kwargs):
 
142
    '''
 
143
    used by modules like User to have different implementations based on detected platform.  See User
 
144
    module for an example.
 
145
    '''
 
146
 
 
147
    this_platform = get_platform()
 
148
    distribution = get_distribution()
 
149
    subclass = None
 
150
 
 
151
    # get the most specific superclass for this platform
 
152
    if distribution is not None:
 
153
        for sc in cls.__subclasses__():
 
154
            if sc.distribution is not None and sc.distribution == distribution and sc.platform == this_platform:
 
155
                subclass = sc
 
156
    if subclass is None:
 
157
        for sc in cls.__subclasses__():
 
158
            if sc.platform == this_platform and sc.distribution is None:
 
159
                subclass = sc
 
160
    if subclass is None:
 
161
        subclass = cls
 
162
 
 
163
    return super(cls, subclass).__new__(subclass)
 
164
 
 
165
 
 
166
class AnsibleModule(object):
 
167
 
 
168
    def __init__(self, argument_spec, bypass_checks=False, no_log=False,
 
169
        check_invalid_arguments=True, mutually_exclusive=None, required_together=None,
 
170
        required_one_of=None, add_file_common_args=False, supports_check_mode=False):
 
171
 
 
172
        '''
 
173
        common code for quickly building an ansible module in Python
 
174
        (although you can write modules in anything that can return JSON)
 
175
        see library/* for examples
 
176
        '''
 
177
 
 
178
        self.argument_spec = argument_spec
 
179
        self.supports_check_mode = supports_check_mode
 
180
        self.check_mode = False
 
181
        self.no_log = no_log
 
182
        
 
183
        self.aliases = {}
 
184
        
 
185
        if add_file_common_args:
 
186
            for k, v in FILE_COMMON_ARGUMENTS.iteritems():
 
187
                if k not in self.argument_spec:
 
188
                    self.argument_spec[k] = v
 
189
 
 
190
        os.environ['LANG'] = MODULE_LANG
 
191
        (self.params, self.args) = self._load_params()
 
192
 
 
193
        self._legal_inputs = ['CHECKMODE', 'NO_LOG']
 
194
        
 
195
        self.aliases = self._handle_aliases()
 
196
 
 
197
        if check_invalid_arguments:
 
198
            self._check_invalid_arguments()
 
199
        self._check_for_check_mode()
 
200
        self._check_for_no_log()
 
201
 
 
202
        # check exclusive early 
 
203
        if not bypass_checks:
 
204
            self._check_mutually_exclusive(mutually_exclusive)
 
205
 
 
206
        self._set_defaults(pre=True)
 
207
 
 
208
        if not bypass_checks:
 
209
            self._check_required_arguments()
 
210
            self._check_argument_values()
 
211
            self._check_argument_types()
 
212
            self._check_required_together(required_together)
 
213
            self._check_required_one_of(required_one_of)
 
214
 
 
215
        self._set_defaults(pre=False)
 
216
        if not self.no_log:
 
217
            self._log_invocation()
 
218
 
 
219
    def load_file_common_arguments(self, params):
 
220
        '''
 
221
        many modules deal with files, this encapsulates common
 
222
        options that the file module accepts such that it is directly
 
223
        available to all modules and they can share code.
 
224
        '''
 
225
 
 
226
        path = params.get('path', params.get('dest', None))
 
227
        if path is None:
 
228
            return {}
 
229
        else:
 
230
            path = os.path.expanduser(path)
 
231
 
 
232
        mode   = params.get('mode', None)
 
233
        owner  = params.get('owner', None)
 
234
        group  = params.get('group', None)
 
235
 
 
236
        # selinux related options
 
237
        seuser    = params.get('seuser', None)
 
238
        serole    = params.get('serole', None)
 
239
        setype    = params.get('setype', None)
 
240
        selevel   = params.get('selevel', None)
 
241
        secontext = [seuser, serole, setype]
 
242
 
 
243
        if self.selinux_mls_enabled():
 
244
            secontext.append(selevel)
 
245
 
 
246
        default_secontext = self.selinux_default_context(path)
 
247
        for i in range(len(default_secontext)):
 
248
            if i is not None and secontext[i] == '_default':
 
249
                secontext[i] = default_secontext[i]
 
250
 
 
251
        return dict(
 
252
            path=path, mode=mode, owner=owner, group=group,
 
253
            seuser=seuser, serole=serole, setype=setype,
 
254
            selevel=selevel, secontext=secontext,
 
255
        )
 
256
 
 
257
 
 
258
    # Detect whether using selinux that is MLS-aware.
 
259
    # While this means you can set the level/range with
 
260
    # selinux.lsetfilecon(), it may or may not mean that you
 
261
    # will get the selevel as part of the context returned
 
262
    # by selinux.lgetfilecon().
 
263
 
 
264
    def selinux_mls_enabled(self):
 
265
        if not HAVE_SELINUX:
 
266
            return False
 
267
        if selinux.is_selinux_mls_enabled() == 1:
 
268
            return True
 
269
        else:
 
270
            return False
 
271
 
 
272
    def selinux_enabled(self):
 
273
        if not HAVE_SELINUX:
 
274
            seenabled = self.get_bin_path('selinuxenabled')
 
275
            if seenabled is not None:
 
276
                (rc,out,err) = self.run_command(seenabled)
 
277
                if rc == 0:
 
278
                    self.fail_json(msg="Aborting, target uses selinux but python bindings (libselinux-python) aren't installed!")
 
279
            return False
 
280
        if selinux.is_selinux_enabled() == 1:
 
281
            return True
 
282
        else:
 
283
            return False
 
284
 
 
285
    # Determine whether we need a placeholder for selevel/mls
 
286
    def selinux_initial_context(self):
 
287
        context = [None, None, None]
 
288
        if self.selinux_mls_enabled():
 
289
            context.append(None)
 
290
        return context
 
291
 
 
292
    def _to_filesystem_str(self, path):
 
293
        '''Returns filesystem path as a str, if it wasn't already.
 
294
 
 
295
        Used in selinux interactions because it cannot accept unicode
 
296
        instances, and specifying complex args in a playbook leaves
 
297
        you with unicode instances.  This method currently assumes
 
298
        that your filesystem encoding is UTF-8.
 
299
 
 
300
        '''
 
301
        if isinstance(path, unicode):
 
302
            path = path.encode("utf-8")
 
303
        return path
 
304
 
 
305
    # If selinux fails to find a default, return an array of None
 
306
    def selinux_default_context(self, path, mode=0):
 
307
        context = self.selinux_initial_context()
 
308
        if not HAVE_SELINUX or not self.selinux_enabled():
 
309
            return context
 
310
        try:
 
311
            ret = selinux.matchpathcon(self._to_filesystem_str(path), mode)
 
312
        except OSError:
 
313
            return context
 
314
        if ret[0] == -1:
 
315
            return context
 
316
        # Limit split to 4 because the selevel, the last in the list,
 
317
        # may contain ':' characters
 
318
        context = ret[1].split(':', 3)
 
319
        return context
 
320
 
 
321
    def selinux_context(self, path):
 
322
        context = self.selinux_initial_context()
 
323
        if not HAVE_SELINUX or not self.selinux_enabled():
 
324
            return context
 
325
        try:
 
326
            ret = selinux.lgetfilecon_raw(self._to_filesystem_str(path))
 
327
        except OSError, e:
 
328
            if e.errno == errno.ENOENT:
 
329
                self.fail_json(path=path, msg='path %s does not exist' % path)
 
330
            else:
 
331
                self.fail_json(path=path, msg='failed to retrieve selinux context')
 
332
        if ret[0] == -1:
 
333
            return context
 
334
        # Limit split to 4 because the selevel, the last in the list,
 
335
        # may contain ':' characters
 
336
        context = ret[1].split(':', 3)
 
337
        return context
 
338
 
 
339
    def user_and_group(self, filename):
 
340
        filename = os.path.expanduser(filename)
 
341
        st = os.lstat(filename)
 
342
        uid = st.st_uid
 
343
        gid = st.st_gid
 
344
        return (uid, gid)
 
345
 
 
346
    def set_default_selinux_context(self, path, changed):
 
347
        if not HAVE_SELINUX or not self.selinux_enabled():
 
348
            return changed
 
349
        context = self.selinux_default_context(path)
 
350
        return self.set_context_if_different(path, context, False)
 
351
 
 
352
    def set_context_if_different(self, path, context, changed):
 
353
 
 
354
        if not HAVE_SELINUX or not self.selinux_enabled():
 
355
            return changed
 
356
        cur_context = self.selinux_context(path)
 
357
        new_context = list(cur_context)
 
358
        # Iterate over the current context instead of the
 
359
        # argument context, which may have selevel.
 
360
 
 
361
        for i in range(len(cur_context)):
 
362
            if len(context) > i:
 
363
                if context[i] is not None and context[i] != cur_context[i]:
 
364
                    new_context[i] = context[i]
 
365
                if context[i] is None:
 
366
                    new_context[i] = cur_context[i]
 
367
 
 
368
        if cur_context != new_context:
 
369
            try:
 
370
                if self.check_mode:
 
371
                    return True
 
372
                rc = selinux.lsetfilecon(self._to_filesystem_str(path),
 
373
                                         str(':'.join(new_context)))
 
374
            except OSError:
 
375
                self.fail_json(path=path, msg='invalid selinux context', new_context=new_context, cur_context=cur_context, input_was=context)
 
376
            if rc != 0:
 
377
                self.fail_json(path=path, msg='set selinux context failed')
 
378
            changed = True
 
379
        return changed
 
380
 
 
381
    def set_owner_if_different(self, path, owner, changed):
 
382
        path = os.path.expanduser(path)
 
383
        if owner is None:
 
384
            return changed
 
385
        orig_uid, orig_gid = self.user_and_group(path)
 
386
        try:
 
387
            uid = int(owner)
 
388
        except ValueError:
 
389
            try:
 
390
                uid = pwd.getpwnam(owner).pw_uid
 
391
            except KeyError:
 
392
                self.fail_json(path=path, msg='chown failed: failed to look up user %s' % owner)
 
393
        if orig_uid != uid:
 
394
            if self.check_mode:
 
395
                return True
 
396
            try:
 
397
                os.lchown(path, uid, -1)
 
398
            except OSError:
 
399
                self.fail_json(path=path, msg='chown failed')
 
400
            changed = True
 
401
        return changed
 
402
 
 
403
    def set_group_if_different(self, path, group, changed):
 
404
        path = os.path.expanduser(path)
 
405
        if group is None:
 
406
            return changed
 
407
        orig_uid, orig_gid = self.user_and_group(path)
 
408
        try:
 
409
            gid = int(group)
 
410
        except ValueError:
 
411
            try:
 
412
                gid = grp.getgrnam(group).gr_gid
 
413
            except KeyError:
 
414
                self.fail_json(path=path, msg='chgrp failed: failed to look up group %s' % group)
 
415
        if orig_gid != gid:
 
416
            if self.check_mode:
 
417
                return True
 
418
            try:
 
419
                os.lchown(path, -1, gid)
 
420
            except OSError:
 
421
                self.fail_json(path=path, msg='chgrp failed')
 
422
            changed = True
 
423
        return changed
 
424
 
 
425
    def set_mode_if_different(self, path, mode, changed):
 
426
        path = os.path.expanduser(path)
 
427
        if mode is None:
 
428
            return changed
 
429
        try:
 
430
            # FIXME: support English modes
 
431
            if not isinstance(mode, int):
 
432
                mode = int(mode, 8)
 
433
        except Exception, e:
 
434
            self.fail_json(path=path, msg='mode needs to be something octalish', details=str(e))
 
435
 
 
436
        st = os.lstat(path)
 
437
        prev_mode = stat.S_IMODE(st[stat.ST_MODE])
 
438
 
 
439
        if prev_mode != mode:
 
440
            if self.check_mode:
 
441
                return True
 
442
            # FIXME: comparison against string above will cause this to be executed
 
443
            # every time
 
444
            try:
 
445
                if 'lchmod' in dir(os):
 
446
                    os.lchmod(path, mode)
 
447
                else:
 
448
                    os.chmod(path, mode)
 
449
            except OSError, e:
 
450
                if os.path.islink(path) and e.errno == errno.EPERM:  # Can't set mode on symbolic links
 
451
                    pass
 
452
                elif e.errno == errno.ENOENT: # Can't set mode on broken symbolic links
 
453
                    pass
 
454
                else:
 
455
                    raise e
 
456
            except Exception, e:
 
457
                self.fail_json(path=path, msg='chmod failed', details=str(e))
 
458
 
 
459
            st = os.lstat(path)
 
460
            new_mode = stat.S_IMODE(st[stat.ST_MODE])
 
461
 
 
462
            if new_mode != prev_mode:
 
463
                changed = True
 
464
        return changed
 
465
 
 
466
    def set_file_attributes_if_different(self, file_args, changed):
 
467
        # set modes owners and context as needed
 
468
        changed = self.set_context_if_different(
 
469
            file_args['path'], file_args['secontext'], changed
 
470
        )
 
471
        changed = self.set_owner_if_different(
 
472
            file_args['path'], file_args['owner'], changed
 
473
        )
 
474
        changed = self.set_group_if_different(
 
475
            file_args['path'], file_args['group'], changed
 
476
        )
 
477
        changed = self.set_mode_if_different(
 
478
            file_args['path'], file_args['mode'], changed
 
479
        )
 
480
        return changed
 
481
 
 
482
    def set_directory_attributes_if_different(self, file_args, changed):
 
483
        changed = self.set_context_if_different(
 
484
            file_args['path'], file_args['secontext'], changed
 
485
        )
 
486
        changed = self.set_owner_if_different(
 
487
            file_args['path'], file_args['owner'], changed
 
488
        )
 
489
        changed = self.set_group_if_different(
 
490
            file_args['path'], file_args['group'], changed
 
491
        )
 
492
        changed = self.set_mode_if_different(
 
493
            file_args['path'], file_args['mode'], changed
 
494
        )
 
495
        return changed
 
496
 
 
497
    def add_path_info(self, kwargs):
 
498
        '''
 
499
        for results that are files, supplement the info about the file
 
500
        in the return path with stats about the file path.
 
501
        '''
 
502
 
 
503
        path = kwargs.get('path', kwargs.get('dest', None))
 
504
        if path is None:
 
505
            return kwargs
 
506
        if os.path.exists(path):
 
507
            (uid, gid) = self.user_and_group(path)
 
508
            kwargs['uid'] = uid
 
509
            kwargs['gid'] = gid
 
510
            try:
 
511
                user = pwd.getpwuid(uid)[0]
 
512
            except KeyError:
 
513
                user = str(uid)
 
514
            try:
 
515
                group = grp.getgrgid(gid)[0]
 
516
            except KeyError:
 
517
                group = str(gid)
 
518
            kwargs['owner'] = user
 
519
            kwargs['group'] = group
 
520
            st = os.lstat(path)
 
521
            kwargs['mode']  = oct(stat.S_IMODE(st[stat.ST_MODE]))
 
522
            # secontext not yet supported
 
523
            if os.path.islink(path):
 
524
                kwargs['state'] = 'link'
 
525
            elif os.path.isdir(path):
 
526
                kwargs['state'] = 'directory'
 
527
            elif os.stat(path).st_nlink > 1:
 
528
                kwargs['state'] = 'hard'
 
529
            else:
 
530
                kwargs['state'] = 'file'
 
531
            if HAVE_SELINUX and self.selinux_enabled():
 
532
                kwargs['secontext'] = ':'.join(self.selinux_context(path))
 
533
            kwargs['size'] = st[stat.ST_SIZE]
 
534
        else:
 
535
            kwargs['state'] = 'absent'
 
536
        return kwargs
 
537
 
 
538
 
 
539
    def _handle_aliases(self):
 
540
        aliases_results = {} #alias:canon
 
541
        for (k,v) in self.argument_spec.iteritems():
 
542
            self._legal_inputs.append(k)
 
543
            aliases = v.get('aliases', None)
 
544
            default = v.get('default', None)
 
545
            required = v.get('required', False)
 
546
            if default is not None and required:
 
547
                # not alias specific but this is a good place to check this
 
548
                self.fail_json(msg="internal error: required and default are mutally exclusive for %s" % k)
 
549
            if aliases is None:
 
550
                continue
 
551
            if type(aliases) != list:
 
552
                self.fail_json(msg='internal error: aliases must be a list')
 
553
            for alias in aliases:
 
554
                self._legal_inputs.append(alias)
 
555
                aliases_results[alias] = k
 
556
                if alias in self.params:
 
557
                    self.params[k] = self.params[alias]
 
558
        
 
559
        return aliases_results
 
560
 
 
561
    def _check_for_check_mode(self):
 
562
        for (k,v) in self.params.iteritems():
 
563
            if k == 'CHECKMODE':
 
564
                if not self.supports_check_mode:
 
565
                    self.exit_json(skipped=True, msg="remote module does not support check mode")
 
566
                if self.supports_check_mode:
 
567
                    self.check_mode = True
 
568
 
 
569
    def _check_for_no_log(self):
 
570
        for (k,v) in self.params.iteritems():
 
571
            if k == 'NO_LOG':
 
572
                self.no_log = self.boolean(v)
 
573
 
 
574
    def _check_invalid_arguments(self):
 
575
        for (k,v) in self.params.iteritems():
 
576
            # these should be in legal inputs already
 
577
            #if k in ('CHECKMODE', 'NO_LOG'):
 
578
            #    continue
 
579
            if k not in self._legal_inputs:
 
580
                self.fail_json(msg="unsupported parameter for module: %s" % k)
 
581
 
 
582
    def _count_terms(self, check):
 
583
        count = 0
 
584
        for term in check:
 
585
            if term in self.params:
 
586
                count += 1
 
587
        return count
 
588
 
 
589
    def _check_mutually_exclusive(self, spec):
 
590
        if spec is None:
 
591
            return
 
592
        for check in spec:
 
593
            count = self._count_terms(check)
 
594
            if count > 1:
 
595
                self.fail_json(msg="parameters are mutually exclusive: %s" % check)
 
596
 
 
597
    def _check_required_one_of(self, spec):
 
598
        if spec is None:
 
599
            return
 
600
        for check in spec:
 
601
            count = self._count_terms(check)
 
602
            if count == 0:
 
603
                self.fail_json(msg="one of the following is required: %s" % ','.join(check))
 
604
 
 
605
    def _check_required_together(self, spec):
 
606
        if spec is None:
 
607
            return
 
608
        for check in spec:
 
609
            counts = [ self._count_terms([field]) for field in check ]
 
610
            non_zero = [ c for c in counts if c > 0 ]
 
611
            if len(non_zero) > 0:
 
612
                if 0 in counts:
 
613
                    self.fail_json(msg="parameters are required together: %s" % check)
 
614
 
 
615
    def _check_required_arguments(self):
 
616
        ''' ensure all required arguments are present '''
 
617
        missing = []
 
618
        for (k,v) in self.argument_spec.iteritems():
 
619
            required = v.get('required', False)
 
620
            if required and k not in self.params:
 
621
                missing.append(k)
 
622
        if len(missing) > 0:
 
623
            self.fail_json(msg="missing required arguments: %s" % ",".join(missing))
 
624
 
 
625
    def _check_argument_values(self):
 
626
        ''' ensure all arguments have the requested values, and there are no stray arguments '''
 
627
        for (k,v) in self.argument_spec.iteritems():
 
628
            choices = v.get('choices',None)
 
629
            if choices is None:
 
630
                continue
 
631
            if type(choices) == list:
 
632
                if k in self.params:
 
633
                    if self.params[k] not in choices:
 
634
                        choices_str=",".join([str(c) for c in choices])
 
635
                        msg="value of %s must be one of: %s, got: %s" % (k, choices_str, self.params[k])
 
636
                        self.fail_json(msg=msg)
 
637
            else:
 
638
                self.fail_json(msg="internal error: do not know how to interpret argument_spec")
 
639
 
 
640
    def safe_eval(self, str, locals=None, include_exceptions=False):
 
641
 
 
642
        # do not allow method calls to modules
 
643
        if not isinstance(str, basestring):
 
644
            # already templated to a datastructure, perhaps?
 
645
            if include_exceptions:
 
646
                return (str, None)
 
647
            return str
 
648
        if re.search(r'\w\.\w+\(', str):
 
649
            if include_exceptions:
 
650
                return (str, None)
 
651
            return str
 
652
        # do not allow imports
 
653
        if re.search(r'import \w+', str):
 
654
            if include_exceptions:
 
655
                return (str, None)
 
656
            return str
 
657
        try:
 
658
            result = None
 
659
            if not locals:
 
660
                result = eval(str)
 
661
            else:
 
662
                result = eval(str, None, locals)
 
663
            if include_exceptions:
 
664
                return (result, None)
 
665
            else:
 
666
                return result
 
667
        except Exception, e:
 
668
            if include_exceptions:
 
669
                return (str, e)
 
670
            return str
 
671
 
 
672
 
 
673
    def _check_argument_types(self):
 
674
        ''' ensure all arguments have the requested type '''
 
675
        for (k, v) in self.argument_spec.iteritems():
 
676
            wanted = v.get('type', None)
 
677
            if wanted is None:
 
678
                continue
 
679
            if k not in self.params:
 
680
                continue
 
681
 
 
682
            value = self.params[k]
 
683
            is_invalid = False
 
684
 
 
685
            if wanted == 'str':
 
686
                if not isinstance(value, basestring):
 
687
                    self.params[k] = str(value)
 
688
            elif wanted == 'list':
 
689
                if not isinstance(value, list):
 
690
                    if isinstance(value, basestring):
 
691
                        self.params[k] = value.split(",")
 
692
                    else:
 
693
                        is_invalid = True
 
694
            elif wanted == 'dict':
 
695
                if not isinstance(value, dict):
 
696
                    if isinstance(value, basestring):
 
697
                        if value.startswith("{"):
 
698
                            try:
 
699
                                self.params[k] = json.loads(value)
 
700
                            except:
 
701
                                (result, exc) = self.safe_eval(value, dict(), include_exceptions=True)
 
702
                                if exc is not None:
 
703
                                    self.fail_json(msg="unable to evaluate dictionary for %s" % k)
 
704
                                self.params[k] = result
 
705
                        elif '=' in value:
 
706
                            self.params[k] = dict([x.split("=", 1) for x in value.split(",")])
 
707
                        else:
 
708
                            self.fail_json(msg="dictionary requested, could not parse JSON or key=value")
 
709
                    else:
 
710
                        is_invalid = True
 
711
            elif wanted == 'bool':
 
712
                if not isinstance(value, bool):
 
713
                    if isinstance(value, basestring):
 
714
                        self.params[k] = self.boolean(value)
 
715
                    else:
 
716
                        is_invalid = True
 
717
            elif wanted == 'int':
 
718
                if not isinstance(value, int):
 
719
                    if isinstance(value, basestring):
 
720
                        self.params[k] = int(value)
 
721
                    else:
 
722
                        is_invalid = True
 
723
            elif wanted == 'float':
 
724
                if not isinstance(value, float):
 
725
                    if isinstance(value, basestring):
 
726
                        self.params[k] = float(value)
 
727
                    else:
 
728
                        is_invalid = True
 
729
            else:
 
730
                self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k))
 
731
 
 
732
            if is_invalid:
 
733
                self.fail_json(msg="argument %s is of invalid type: %s, required: %s" % (k, type(value), wanted))
 
734
 
 
735
    def _set_defaults(self, pre=True):
 
736
        for (k,v) in self.argument_spec.iteritems():
 
737
            default = v.get('default', None)
 
738
            if pre == True:
 
739
                # this prevents setting defaults on required items
 
740
                if default is not None and k not in self.params:
 
741
                    self.params[k] = default
 
742
            else:
 
743
                # make sure things without a default still get set None
 
744
                if k not in self.params:
 
745
                    self.params[k] = default
 
746
 
 
747
    def _load_params(self):
 
748
        ''' read the input and return a dictionary and the arguments string '''
 
749
        args = MODULE_ARGS
 
750
        items   = shlex.split(args)
 
751
        params = {}
 
752
        for x in items:
 
753
            try:
 
754
                (k, v) = x.split("=",1)
 
755
            except Exception, e:
 
756
                self.fail_json(msg="this module requires key=value arguments (%s)" % (items))
 
757
            params[k] = v
 
758
        params2 = json.loads(MODULE_COMPLEX_ARGS)
 
759
        params2.update(params)
 
760
        return (params2, args)
 
761
 
 
762
    def _log_invocation(self):
 
763
        ''' log that ansible ran the module '''
 
764
        # TODO: generalize a separate log function and make log_invocation use it
 
765
        # Sanitize possible password argument when logging.
 
766
        log_args = dict()
 
767
        passwd_keys = ['password', 'login_password']
 
768
 
 
769
        filter_re = [
 
770
            # filter out things like user:pass@foo/whatever
 
771
            # and http://username:pass@wherever/foo
 
772
            re.compile('^(?P<before>.*:)(?P<password>.*)(?P<after>\@.*)$'), 
 
773
        ]
 
774
 
 
775
        for param in self.params:
 
776
            canon  = self.aliases.get(param, param)
 
777
            arg_opts = self.argument_spec.get(canon, {})
 
778
            no_log = arg_opts.get('no_log', False)
 
779
                
 
780
            if no_log:
 
781
                log_args[param] = 'NOT_LOGGING_PARAMETER'
 
782
            elif param in passwd_keys:
 
783
                log_args[param] = 'NOT_LOGGING_PASSWORD'
 
784
            else:
 
785
                found = False
 
786
                for filter in filter_re:
 
787
                    if isinstance(self.params[param], unicode):
 
788
                        m = filter.match(self.params[param])
 
789
                    else:
 
790
                        m = filter.match(str(self.params[param]))
 
791
                    if m:
 
792
                        d = m.groupdict()
 
793
                        log_args[param] = d['before'] + "********" + d['after']
 
794
                        found = True
 
795
                        break
 
796
                if not found:
 
797
                    log_args[param] = self.params[param]
 
798
 
 
799
        module = 'ansible-%s' % os.path.basename(__file__)
 
800
        msg = ''
 
801
        for arg in log_args:
 
802
            if isinstance(log_args[arg], unicode):
 
803
                msg = msg + arg + '=' + log_args[arg] + ' '
 
804
            else:
 
805
                msg = msg + arg + '=' + str(log_args[arg]) + ' '
 
806
        if msg:
 
807
            msg = 'Invoked with %s' % msg
 
808
        else:
 
809
            msg = 'Invoked'
 
810
 
 
811
        if (has_journal):
 
812
            journal_args = ["MESSAGE=%s %s" % (module, msg)]
 
813
            journal_args.append("MODULE=%s" % os.path.basename(__file__))
 
814
            for arg in log_args:
 
815
                journal_args.append(arg.upper() + "=" + str(log_args[arg]))
 
816
            try:
 
817
                journal.sendv(*journal_args)
 
818
            except IOError, e:
 
819
                # fall back to syslog since logging to journal failed
 
820
                syslog.openlog(str(module), 0, syslog.LOG_USER)
 
821
                syslog.syslog(syslog.LOG_NOTICE, unicode(msg).encode('utf8'))
 
822
        else:
 
823
            syslog.openlog(str(module), 0, syslog.LOG_USER)
 
824
            syslog.syslog(syslog.LOG_NOTICE, unicode(msg).encode('utf8'))
 
825
 
 
826
    def get_bin_path(self, arg, required=False, opt_dirs=[]):
 
827
        '''
 
828
        find system executable in PATH.
 
829
        Optional arguments:
 
830
           - required:  if executable is not found and required is true, fail_json
 
831
           - opt_dirs:  optional list of directories to search in addition to PATH
 
832
        if found return full path; otherwise return None
 
833
        '''
 
834
        sbin_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin']
 
835
        paths = []
 
836
        for d in opt_dirs:
 
837
            if d is not None and os.path.exists(d):
 
838
                paths.append(d)
 
839
        paths += os.environ.get('PATH', '').split(os.pathsep)
 
840
        bin_path = None
 
841
        # mangle PATH to include /sbin dirs
 
842
        for p in sbin_paths:
 
843
            if p not in paths and os.path.exists(p):
 
844
                paths.append(p)
 
845
        for d in paths:
 
846
            path = os.path.join(d, arg)
 
847
            if os.path.exists(path) and self.is_executable(path):
 
848
                bin_path = path
 
849
                break
 
850
        if required and bin_path is None:
 
851
            self.fail_json(msg='Failed to find required executable %s' % arg)
 
852
        return bin_path
 
853
 
 
854
    def boolean(self, arg):
 
855
        ''' return a bool for the arg '''
 
856
        if arg is None or type(arg) == bool:
 
857
            return arg
 
858
        if type(arg) in types.StringTypes:
 
859
            arg = arg.lower()
 
860
        if arg in BOOLEANS_TRUE:
 
861
            return True
 
862
        elif arg in BOOLEANS_FALSE:
 
863
            return False
 
864
        else:
 
865
            self.fail_json(msg='Boolean %s not in either boolean list' % arg)
 
866
 
 
867
    def jsonify(self, data):
 
868
        for encoding in ("utf-8", "latin-1", "unicode_escape"):
 
869
            try:
 
870
                return json.dumps(data, encoding=encoding)
 
871
            except UnicodeDecodeError, e:
 
872
                continue
 
873
        self.fail_json(msg='Invalid unicode encoding encountered')
 
874
 
 
875
    def from_json(self, data):
 
876
        return json.loads(data)
 
877
 
 
878
    def exit_json(self, **kwargs):
 
879
        ''' return from the module, without error '''
 
880
        self.add_path_info(kwargs)
 
881
        if not 'changed' in kwargs:
 
882
            kwargs['changed'] = False
 
883
        print self.jsonify(kwargs)
 
884
        sys.exit(0)
 
885
 
 
886
    def fail_json(self, **kwargs):
 
887
        ''' return from the module, with an error message '''
 
888
        self.add_path_info(kwargs)
 
889
        assert 'msg' in kwargs, "implementation error -- msg to explain the error is required"
 
890
        kwargs['failed'] = True
 
891
        print self.jsonify(kwargs)
 
892
        sys.exit(1)
 
893
 
 
894
    def is_executable(self, path):
 
895
        '''is the given path executable?'''
 
896
        return (stat.S_IXUSR & os.stat(path)[stat.ST_MODE]
 
897
                or stat.S_IXGRP & os.stat(path)[stat.ST_MODE]
 
898
                or stat.S_IXOTH & os.stat(path)[stat.ST_MODE])
 
899
 
 
900
    def digest_from_file(self, filename, digest_method):
 
901
        ''' Return hex digest of local file for a given digest_method, or None if file is not present. '''
 
902
        if not os.path.exists(filename):
 
903
            return None
 
904
        if os.path.isdir(filename):
 
905
            self.fail_json(msg="attempted to take checksum of directory: %s" % filename)
 
906
        digest = digest_method
 
907
        blocksize = 64 * 1024
 
908
        infile = open(filename, 'rb')
 
909
        block = infile.read(blocksize)
 
910
        while block:
 
911
            digest.update(block)
 
912
            block = infile.read(blocksize)
 
913
        infile.close()
 
914
        return digest.hexdigest()
 
915
 
 
916
    def md5(self, filename):
 
917
        ''' Return MD5 hex digest of local file using digest_from_file(). '''
 
918
        return self.digest_from_file(filename, _md5())
 
919
 
 
920
    def sha256(self, filename):
 
921
        ''' Return SHA-256 hex digest of local file using digest_from_file(). '''
 
922
        if not HAVE_HASHLIB:
 
923
            self.fail_json(msg="SHA-256 checksums require hashlib, which is available in Python 2.5 and higher")
 
924
        return self.digest_from_file(filename, _sha256())
 
925
 
 
926
    def backup_local(self, fn):
 
927
        '''make a date-marked backup of the specified file, return True or False on success or failure'''
 
928
        # backups named basename-YYYY-MM-DD@HH:MM~
 
929
        ext = time.strftime("%Y-%m-%d@%H:%M~", time.localtime(time.time()))
 
930
        backupdest = '%s.%s' % (fn, ext)
 
931
 
 
932
        try:
 
933
            shutil.copy2(fn, backupdest)
 
934
        except shutil.Error, e:
 
935
            self.fail_json(msg='Could not make backup of %s to %s: %s' % (fn, backupdest, e))
 
936
        return backupdest
 
937
 
 
938
    def cleanup(self,tmpfile):
 
939
        if os.path.exists(tmpfile):
 
940
            try:
 
941
                os.unlink(tmpfile)
 
942
            except OSError, e:
 
943
                sys.stderr.write("could not cleanup %s: %s" % (tmpfile, e))
 
944
 
 
945
    def atomic_move(self, src, dest):
 
946
        '''atomically move src to dest, copying attributes from dest, returns true on success
 
947
        it uses os.rename to ensure this as it is an atomic operation, rest of the function is
 
948
        to work around limitations, corner cases and ensure selinux context is saved if possible'''
 
949
        context = None
 
950
        if os.path.exists(dest):
 
951
            try:
 
952
                st = os.stat(dest)
 
953
                os.chmod(src, st.st_mode & 07777)
 
954
                os.chown(src, st.st_uid, st.st_gid)
 
955
            except OSError, e:
 
956
                if e.errno != errno.EPERM:
 
957
                    raise
 
958
            if self.selinux_enabled():
 
959
                context = self.selinux_context(dest)
 
960
        else:
 
961
            if self.selinux_enabled():
 
962
                context = self.selinux_default_context(dest)
 
963
 
 
964
        try:
 
965
            # Optimistically try a rename, solves some corner cases and can avoid useless work.
 
966
            os.rename(src, dest)
 
967
        except (IOError,OSError), e:
 
968
            # only try workarounds for errno 18 (cross device), 1 (not permited) and 13 (permission denied)
 
969
            if e.errno != errno.EPERM and e.errno != errno.EXDEV and e.errno != errno.EACCES:
 
970
                self.fail_json(msg='Could not replace file: %s to %s: %s' % (src, dest, e))
 
971
 
 
972
            dest_dir = os.path.dirname(dest)
 
973
            dest_file = os.path.basename(dest)
 
974
            tmp_dest = "%s/.%s.%s.%s" % (dest_dir,dest_file,os.getpid(),time.time())
 
975
 
 
976
            try: # leaves tmp file behind when sudo and  not root
 
977
                if os.getenv("SUDO_USER") and os.getuid() != 0:
 
978
                    # cleanup will happen by 'rm' of tempdir
 
979
                    shutil.copy(src, tmp_dest)
 
980
                else:
 
981
                    shutil.move(src, tmp_dest)
 
982
                if self.selinux_enabled():
 
983
                    self.set_context_if_different(tmp_dest, context, False)
 
984
                os.rename(tmp_dest, dest)
 
985
            except (shutil.Error, OSError, IOError), e:
 
986
                self.cleanup(tmp_dest)
 
987
                self.fail_json(msg='Could not replace file: %s to %s: %s' % (src, dest, e))
 
988
 
 
989
        if self.selinux_enabled():
 
990
            # rename might not preserve context
 
991
            self.set_context_if_different(dest, context, False)
 
992
 
 
993
    def run_command(self, args, check_rc=False, close_fds=False, executable=None, data=None, binary_data=False, path_prefix=None, cwd=None, use_unsafe_shell=False):
 
994
        '''
 
995
        Execute a command, returns rc, stdout, and stderr.
 
996
        args is the command to run
 
997
        If args is a list, the command will be run with shell=False.
 
998
        If args is a string and use_unsafe_shell=False it will split args to a list and run with shell=False
 
999
        If args is a string and use_unsafe_shell=True it run with shell=True.
 
1000
        Other arguments:
 
1001
        - check_rc (boolean)  Whether to call fail_json in case of
 
1002
                              non zero RC.  Default is False.
 
1003
        - close_fds (boolean) See documentation for subprocess.Popen().
 
1004
                              Default is False.
 
1005
        - executable (string) See documentation for subprocess.Popen().
 
1006
                              Default is None.
 
1007
        '''
 
1008
 
 
1009
        shell = False
 
1010
        if isinstance(args, list):
 
1011
            pass
 
1012
        elif isinstance(args, basestring) and use_unsafe_shell:
 
1013
            shell = True
 
1014
        elif isinstance(args, basestring):
 
1015
            args = shlex.split(args)
 
1016
        else:
 
1017
            msg = "Argument 'args' to run_command must be list or string"
 
1018
            self.fail_json(rc=257, cmd=args, msg=msg)
 
1019
 
 
1020
        # expand things like $HOME and ~
 
1021
        if not shell:
 
1022
            args = [ os.path.expandvars(os.path.expanduser(x)) for x in args ]
 
1023
 
 
1024
        rc = 0
 
1025
        msg = None
 
1026
        st_in = None
 
1027
 
 
1028
        # Set a temporart env path if a prefix is passed
 
1029
        env=os.environ
 
1030
        if path_prefix:
 
1031
            env['PATH']="%s:%s" % (path_prefix, env['PATH'])
 
1032
 
 
1033
        if data:
 
1034
            st_in = subprocess.PIPE
 
1035
 
 
1036
        kwargs = dict(
 
1037
            executable=executable,
 
1038
            shell=shell,
 
1039
            close_fds=close_fds,
 
1040
            stdin= st_in,
 
1041
            stdout=subprocess.PIPE,
 
1042
            stderr=subprocess.PIPE 
 
1043
        )
 
1044
 
 
1045
        if path_prefix:
 
1046
            kwargs['env'] = env
 
1047
        if cwd and os.path.isdir(cwd):
 
1048
            kwargs['cwd'] = cwd
 
1049
 
 
1050
 
 
1051
        try:
 
1052
            # make sure we're in the right working directory
 
1053
            if cwd and os.path.isdir(cwd):
 
1054
                os.chdir(cwd)
 
1055
 
 
1056
            cmd = subprocess.Popen(args, **kwargs)
 
1057
 
 
1058
            if data:
 
1059
                if not binary_data:
 
1060
                    data += '\n'
 
1061
            out, err = cmd.communicate(input=data)
 
1062
            rc = cmd.returncode
 
1063
        except (OSError, IOError), e:
 
1064
            self.fail_json(rc=e.errno, msg=str(e), cmd=args)
 
1065
        except:
 
1066
            self.fail_json(rc=257, msg=traceback.format_exc(), cmd=args)
 
1067
        if rc != 0 and check_rc:
 
1068
            msg = err.rstrip()
 
1069
            self.fail_json(cmd=args, rc=rc, stdout=out, stderr=err, msg=msg)
 
1070
        return (rc, out, err)
 
1071
 
 
1072
    def append_to_file(self, filename, str):
 
1073
        filename = os.path.expandvars(os.path.expanduser(filename))
 
1074
        fh = open(filename, 'a')
 
1075
        fh.write(str)
 
1076
        fh.close()
 
1077
 
 
1078
    def pretty_bytes(self,size):
 
1079
        ranges = (
 
1080
                (1<<70L, 'ZB'),
 
1081
                (1<<60L, 'EB'),
 
1082
                (1<<50L, 'PB'),
 
1083
                (1<<40L, 'TB'),
 
1084
                (1<<30L, 'GB'),
 
1085
                (1<<20L, 'MB'),
 
1086
                (1<<10L, 'KB'),
 
1087
                (1, 'Bytes')
 
1088
            )
 
1089
        for limit, suffix in ranges:
 
1090
            if size >= limit:
 
1091
                break
 
1092
        return '%.2f %s' % (float(size)/ limit, suffix)
 
1093