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.
7
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
10
# Redistribution and use in source and binary forms, with or without modification,
11
# are permitted provided that the following conditions are met:
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.
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.
30
# == BEGIN DYNAMICALLY INSERTED CODE ==
32
MODULE_ARGS = "<<INCLUDE_ANSIBLE_MODULE_ARGS>>"
33
MODULE_LANG = "<<INCLUDE_ANSIBLE_MODULE_LANG>>"
34
MODULE_COMPLEX_ARGS = "<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"
36
BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 1]
37
BOOLEANS_FALSE = ['no', 'off', '0', 'false', 0]
38
BOOLEANS = BOOLEANS_TRUE + BOOLEANS_FALSE
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
68
import simplejson as json
70
sys.stderr.write('Error: ansible requires a json module, none found!')
73
sys.stderr.write('SyntaxError: probably due to json and python being for different versions')
85
from hashlib import md5 as _md5
88
from md5 import md5 as _md5
91
from hashlib import sha256 as _sha256
96
from systemd import journal
102
FILE_COMMON_ARGUMENTS=dict(
111
# not taken by the file module, but other modules call file so it must ignore them.
115
remote_src = dict(), # used by assemble
120
''' what's the platform? example: Linux is a platform. '''
121
return platform.system()
123
def get_distribution():
124
''' return the distribution name '''
125
if platform.system() == 'Linux':
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'
133
distribution = 'OtherLinux'
135
# FIXME: MethodMissing, I assume?
136
distribution = platform.dist()[0].capitalize()
141
def load_platform_subclass(cls, *args, **kwargs):
143
used by modules like User to have different implementations based on detected platform. See User
144
module for an example.
147
this_platform = get_platform()
148
distribution = get_distribution()
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:
157
for sc in cls.__subclasses__():
158
if sc.platform == this_platform and sc.distribution is None:
163
return super(cls, subclass).__new__(subclass)
166
class AnsibleModule(object):
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):
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
178
self.argument_spec = argument_spec
179
self.supports_check_mode = supports_check_mode
180
self.check_mode = False
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
190
os.environ['LANG'] = MODULE_LANG
191
(self.params, self.args) = self._load_params()
193
self._legal_inputs = ['CHECKMODE', 'NO_LOG']
195
self.aliases = self._handle_aliases()
197
if check_invalid_arguments:
198
self._check_invalid_arguments()
199
self._check_for_check_mode()
200
self._check_for_no_log()
202
# check exclusive early
203
if not bypass_checks:
204
self._check_mutually_exclusive(mutually_exclusive)
206
self._set_defaults(pre=True)
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)
215
self._set_defaults(pre=False)
217
self._log_invocation()
219
def load_file_common_arguments(self, params):
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.
226
path = params.get('path', params.get('dest', None))
230
path = os.path.expanduser(path)
232
mode = params.get('mode', None)
233
owner = params.get('owner', None)
234
group = params.get('group', None)
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]
243
if self.selinux_mls_enabled():
244
secontext.append(selevel)
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]
252
path=path, mode=mode, owner=owner, group=group,
253
seuser=seuser, serole=serole, setype=setype,
254
selevel=selevel, secontext=secontext,
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().
264
def selinux_mls_enabled(self):
267
if selinux.is_selinux_mls_enabled() == 1:
272
def selinux_enabled(self):
274
seenabled = self.get_bin_path('selinuxenabled')
275
if seenabled is not None:
276
(rc,out,err) = self.run_command(seenabled)
278
self.fail_json(msg="Aborting, target uses selinux but python bindings (libselinux-python) aren't installed!")
280
if selinux.is_selinux_enabled() == 1:
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():
292
def _to_filesystem_str(self, path):
293
'''Returns filesystem path as a str, if it wasn't already.
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.
301
if isinstance(path, unicode):
302
path = path.encode("utf-8")
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():
311
ret = selinux.matchpathcon(self._to_filesystem_str(path), mode)
316
# Limit split to 4 because the selevel, the last in the list,
317
# may contain ':' characters
318
context = ret[1].split(':', 3)
321
def selinux_context(self, path):
322
context = self.selinux_initial_context()
323
if not HAVE_SELINUX or not self.selinux_enabled():
326
ret = selinux.lgetfilecon_raw(self._to_filesystem_str(path))
328
if e.errno == errno.ENOENT:
329
self.fail_json(path=path, msg='path %s does not exist' % path)
331
self.fail_json(path=path, msg='failed to retrieve selinux context')
334
# Limit split to 4 because the selevel, the last in the list,
335
# may contain ':' characters
336
context = ret[1].split(':', 3)
339
def user_and_group(self, filename):
340
filename = os.path.expanduser(filename)
341
st = os.lstat(filename)
346
def set_default_selinux_context(self, path, changed):
347
if not HAVE_SELINUX or not self.selinux_enabled():
349
context = self.selinux_default_context(path)
350
return self.set_context_if_different(path, context, False)
352
def set_context_if_different(self, path, context, changed):
354
if not HAVE_SELINUX or not self.selinux_enabled():
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.
361
for i in range(len(cur_context)):
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]
368
if cur_context != new_context:
372
rc = selinux.lsetfilecon(self._to_filesystem_str(path),
373
str(':'.join(new_context)))
375
self.fail_json(path=path, msg='invalid selinux context', new_context=new_context, cur_context=cur_context, input_was=context)
377
self.fail_json(path=path, msg='set selinux context failed')
381
def set_owner_if_different(self, path, owner, changed):
382
path = os.path.expanduser(path)
385
orig_uid, orig_gid = self.user_and_group(path)
390
uid = pwd.getpwnam(owner).pw_uid
392
self.fail_json(path=path, msg='chown failed: failed to look up user %s' % owner)
397
os.lchown(path, uid, -1)
399
self.fail_json(path=path, msg='chown failed')
403
def set_group_if_different(self, path, group, changed):
404
path = os.path.expanduser(path)
407
orig_uid, orig_gid = self.user_and_group(path)
412
gid = grp.getgrnam(group).gr_gid
414
self.fail_json(path=path, msg='chgrp failed: failed to look up group %s' % group)
419
os.lchown(path, -1, gid)
421
self.fail_json(path=path, msg='chgrp failed')
425
def set_mode_if_different(self, path, mode, changed):
426
path = os.path.expanduser(path)
430
# FIXME: support English modes
431
if not isinstance(mode, int):
434
self.fail_json(path=path, msg='mode needs to be something octalish', details=str(e))
437
prev_mode = stat.S_IMODE(st[stat.ST_MODE])
439
if prev_mode != mode:
442
# FIXME: comparison against string above will cause this to be executed
445
if 'lchmod' in dir(os):
446
os.lchmod(path, mode)
450
if os.path.islink(path) and e.errno == errno.EPERM: # Can't set mode on symbolic links
452
elif e.errno == errno.ENOENT: # Can't set mode on broken symbolic links
457
self.fail_json(path=path, msg='chmod failed', details=str(e))
460
new_mode = stat.S_IMODE(st[stat.ST_MODE])
462
if new_mode != prev_mode:
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
471
changed = self.set_owner_if_different(
472
file_args['path'], file_args['owner'], changed
474
changed = self.set_group_if_different(
475
file_args['path'], file_args['group'], changed
477
changed = self.set_mode_if_different(
478
file_args['path'], file_args['mode'], changed
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
486
changed = self.set_owner_if_different(
487
file_args['path'], file_args['owner'], changed
489
changed = self.set_group_if_different(
490
file_args['path'], file_args['group'], changed
492
changed = self.set_mode_if_different(
493
file_args['path'], file_args['mode'], changed
497
def add_path_info(self, kwargs):
499
for results that are files, supplement the info about the file
500
in the return path with stats about the file path.
503
path = kwargs.get('path', kwargs.get('dest', None))
506
if os.path.exists(path):
507
(uid, gid) = self.user_and_group(path)
511
user = pwd.getpwuid(uid)[0]
515
group = grp.getgrgid(gid)[0]
518
kwargs['owner'] = user
519
kwargs['group'] = group
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'
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]
535
kwargs['state'] = 'absent'
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)
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]
559
return aliases_results
561
def _check_for_check_mode(self):
562
for (k,v) in self.params.iteritems():
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
569
def _check_for_no_log(self):
570
for (k,v) in self.params.iteritems():
572
self.no_log = self.boolean(v)
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'):
579
if k not in self._legal_inputs:
580
self.fail_json(msg="unsupported parameter for module: %s" % k)
582
def _count_terms(self, check):
585
if term in self.params:
589
def _check_mutually_exclusive(self, spec):
593
count = self._count_terms(check)
595
self.fail_json(msg="parameters are mutually exclusive: %s" % check)
597
def _check_required_one_of(self, spec):
601
count = self._count_terms(check)
603
self.fail_json(msg="one of the following is required: %s" % ','.join(check))
605
def _check_required_together(self, 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:
613
self.fail_json(msg="parameters are required together: %s" % check)
615
def _check_required_arguments(self):
616
''' ensure all required arguments are present '''
618
for (k,v) in self.argument_spec.iteritems():
619
required = v.get('required', False)
620
if required and k not in self.params:
623
self.fail_json(msg="missing required arguments: %s" % ",".join(missing))
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)
631
if type(choices) == list:
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)
638
self.fail_json(msg="internal error: do not know how to interpret argument_spec")
640
def safe_eval(self, str, locals=None, include_exceptions=False):
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:
648
if re.search(r'\w\.\w+\(', str):
649
if include_exceptions:
652
# do not allow imports
653
if re.search(r'import \w+', str):
654
if include_exceptions:
662
result = eval(str, None, locals)
663
if include_exceptions:
664
return (result, None)
668
if include_exceptions:
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)
679
if k not in self.params:
682
value = self.params[k]
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(",")
694
elif wanted == 'dict':
695
if not isinstance(value, dict):
696
if isinstance(value, basestring):
697
if value.startswith("{"):
699
self.params[k] = json.loads(value)
701
(result, exc) = self.safe_eval(value, dict(), include_exceptions=True)
703
self.fail_json(msg="unable to evaluate dictionary for %s" % k)
704
self.params[k] = result
706
self.params[k] = dict([x.split("=", 1) for x in value.split(",")])
708
self.fail_json(msg="dictionary requested, could not parse JSON or key=value")
711
elif wanted == 'bool':
712
if not isinstance(value, bool):
713
if isinstance(value, basestring):
714
self.params[k] = self.boolean(value)
717
elif wanted == 'int':
718
if not isinstance(value, int):
719
if isinstance(value, basestring):
720
self.params[k] = int(value)
723
elif wanted == 'float':
724
if not isinstance(value, float):
725
if isinstance(value, basestring):
726
self.params[k] = float(value)
730
self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k))
733
self.fail_json(msg="argument %s is of invalid type: %s, required: %s" % (k, type(value), wanted))
735
def _set_defaults(self, pre=True):
736
for (k,v) in self.argument_spec.iteritems():
737
default = v.get('default', None)
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
743
# make sure things without a default still get set None
744
if k not in self.params:
745
self.params[k] = default
747
def _load_params(self):
748
''' read the input and return a dictionary and the arguments string '''
750
items = shlex.split(args)
754
(k, v) = x.split("=",1)
756
self.fail_json(msg="this module requires key=value arguments (%s)" % (items))
758
params2 = json.loads(MODULE_COMPLEX_ARGS)
759
params2.update(params)
760
return (params2, args)
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.
767
passwd_keys = ['password', 'login_password']
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>\@.*)$'),
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)
781
log_args[param] = 'NOT_LOGGING_PARAMETER'
782
elif param in passwd_keys:
783
log_args[param] = 'NOT_LOGGING_PASSWORD'
786
for filter in filter_re:
787
if isinstance(self.params[param], unicode):
788
m = filter.match(self.params[param])
790
m = filter.match(str(self.params[param]))
793
log_args[param] = d['before'] + "********" + d['after']
797
log_args[param] = self.params[param]
799
module = 'ansible-%s' % os.path.basename(__file__)
802
if isinstance(log_args[arg], unicode):
803
msg = msg + arg + '=' + log_args[arg] + ' '
805
msg = msg + arg + '=' + str(log_args[arg]) + ' '
807
msg = 'Invoked with %s' % msg
812
journal_args = ["MESSAGE=%s %s" % (module, msg)]
813
journal_args.append("MODULE=%s" % os.path.basename(__file__))
815
journal_args.append(arg.upper() + "=" + str(log_args[arg]))
817
journal.sendv(*journal_args)
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'))
823
syslog.openlog(str(module), 0, syslog.LOG_USER)
824
syslog.syslog(syslog.LOG_NOTICE, unicode(msg).encode('utf8'))
826
def get_bin_path(self, arg, required=False, opt_dirs=[]):
828
find system executable in PATH.
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
834
sbin_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin']
837
if d is not None and os.path.exists(d):
839
paths += os.environ.get('PATH', '').split(os.pathsep)
841
# mangle PATH to include /sbin dirs
843
if p not in paths and os.path.exists(p):
846
path = os.path.join(d, arg)
847
if os.path.exists(path) and self.is_executable(path):
850
if required and bin_path is None:
851
self.fail_json(msg='Failed to find required executable %s' % arg)
854
def boolean(self, arg):
855
''' return a bool for the arg '''
856
if arg is None or type(arg) == bool:
858
if type(arg) in types.StringTypes:
860
if arg in BOOLEANS_TRUE:
862
elif arg in BOOLEANS_FALSE:
865
self.fail_json(msg='Boolean %s not in either boolean list' % arg)
867
def jsonify(self, data):
868
for encoding in ("utf-8", "latin-1", "unicode_escape"):
870
return json.dumps(data, encoding=encoding)
871
except UnicodeDecodeError, e:
873
self.fail_json(msg='Invalid unicode encoding encountered')
875
def from_json(self, data):
876
return json.loads(data)
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)
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)
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])
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):
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)
912
block = infile.read(blocksize)
914
return digest.hexdigest()
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())
920
def sha256(self, filename):
921
''' Return SHA-256 hex digest of local file using digest_from_file(). '''
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())
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)
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))
938
def cleanup(self,tmpfile):
939
if os.path.exists(tmpfile):
943
sys.stderr.write("could not cleanup %s: %s" % (tmpfile, e))
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'''
950
if os.path.exists(dest):
953
os.chmod(src, st.st_mode & 07777)
954
os.chown(src, st.st_uid, st.st_gid)
956
if e.errno != errno.EPERM:
958
if self.selinux_enabled():
959
context = self.selinux_context(dest)
961
if self.selinux_enabled():
962
context = self.selinux_default_context(dest)
965
# Optimistically try a rename, solves some corner cases and can avoid useless work.
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))
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())
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)
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))
989
if self.selinux_enabled():
990
# rename might not preserve context
991
self.set_context_if_different(dest, context, False)
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):
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.
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().
1005
- executable (string) See documentation for subprocess.Popen().
1010
if isinstance(args, list):
1012
elif isinstance(args, basestring) and use_unsafe_shell:
1014
elif isinstance(args, basestring):
1015
args = shlex.split(args)
1017
msg = "Argument 'args' to run_command must be list or string"
1018
self.fail_json(rc=257, cmd=args, msg=msg)
1020
# expand things like $HOME and ~
1022
args = [ os.path.expandvars(os.path.expanduser(x)) for x in args ]
1028
# Set a temporart env path if a prefix is passed
1031
env['PATH']="%s:%s" % (path_prefix, env['PATH'])
1034
st_in = subprocess.PIPE
1037
executable=executable,
1039
close_fds=close_fds,
1041
stdout=subprocess.PIPE,
1042
stderr=subprocess.PIPE
1047
if cwd and os.path.isdir(cwd):
1052
# make sure we're in the right working directory
1053
if cwd and os.path.isdir(cwd):
1056
cmd = subprocess.Popen(args, **kwargs)
1061
out, err = cmd.communicate(input=data)
1063
except (OSError, IOError), e:
1064
self.fail_json(rc=e.errno, msg=str(e), cmd=args)
1066
self.fail_json(rc=257, msg=traceback.format_exc(), cmd=args)
1067
if rc != 0 and check_rc:
1069
self.fail_json(cmd=args, rc=rc, stdout=out, stderr=err, msg=msg)
1070
return (rc, out, err)
1072
def append_to_file(self, filename, str):
1073
filename = os.path.expandvars(os.path.expanduser(filename))
1074
fh = open(filename, 'a')
1078
def pretty_bytes(self,size):
1089
for limit, suffix in ranges:
1092
return '%.2f %s' % (float(size)/ limit, suffix)