PageRenderTime 73ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/salt/states/file.py

https://github.com/vitaliyf/salt
Python | 3882 lines | 3766 code | 28 blank | 88 comment | 58 complexity | 425da95a095315035529e76a74c2f07d MD5 | raw file
Possible License(s): Apache-2.0
  1. # -*- coding: utf-8 -*-
  2. '''
  3. Operations on regular files, special files, directories, and symlinks
  4. =====================================================================
  5. Salt States can aggressively manipulate files on a system. There are a number
  6. of ways in which files can be managed.
  7. Regular files can be enforced with the ``managed`` function. This function
  8. downloads files from the salt master and places them on the target system.
  9. The downloaded files can be rendered as a jinja, mako, or wempy template,
  10. adding a dynamic component to file management. An example of ``file.managed``
  11. which makes use of the jinja templating system would look like this:
  12. .. code-block:: yaml
  13. /etc/http/conf/http.conf:
  14. file.managed:
  15. - source: salt://apache/http.conf
  16. - user: root
  17. - group: root
  18. - mode: 644
  19. - template: jinja
  20. - defaults:
  21. custom_var: "default value"
  22. other_var: 123
  23. {% if grains['os'] == 'Ubuntu' %}
  24. - context:
  25. custom_var: "override"
  26. {% endif %}
  27. .. note::
  28. When using both the ``defaults`` and ``context`` arguments, note the extra
  29. indentation (four spaces instead of the normal two). This is due to an
  30. idiosyncrasy of how PyYAML loads nested dictionaries, and is explained in
  31. greater detail :ref:`here <nested-dict-indentation>`.
  32. If using a template, any user-defined template variables in the file defined in
  33. ``source`` must be passed in using the ``defaults`` and/or ``context``
  34. arguments. The general best practice is to place default values in
  35. ``defaults``, with conditional overrides going into ``context``, as seen above.
  36. The template will receive a variable ``custom_var``, which would be accessed in
  37. the template using ``{{ custom_var }}``. If the operating system is Ubuntu, the
  38. value of the variable ``custom_var`` would be *override*, otherwise it is the
  39. default *default value*
  40. The ``source`` parameter can be specified as a list. If this is done, then the
  41. first file to be matched will be the one that is used. This allows you to have
  42. a default file on which to fall back if the desired file does not exist on the
  43. salt fileserver. Here's an example:
  44. .. code-block:: yaml
  45. /etc/foo.conf:
  46. file.managed:
  47. - source:
  48. - salt://foo.conf.{{ grains['fqdn'] }}
  49. - salt://foo.conf.fallback
  50. - user: foo
  51. - group: users
  52. - mode: 644
  53. - backup: minion
  54. .. note::
  55. Salt supports backing up managed files via the backup option. For more
  56. details on this functionality please review the
  57. :doc:`backup_mode documentation </ref/states/backup_mode>`.
  58. The ``source`` parameter can also specify a file in another Salt environment.
  59. In this example ``foo.conf`` in the ``dev`` environment will be used instead.
  60. .. code-block:: yaml
  61. /etc/foo.conf:
  62. file.managed:
  63. - source:
  64. - salt://foo.conf?saltenv=dev
  65. - user: foo
  66. - group: users
  67. - mode: '0644'
  68. .. warning::
  69. When using a mode that includes a leading zero you must wrap the
  70. value in single quotes. If the value is not wrapped in quotes it
  71. will be read by YAML as an integer and evaluated as an octal.
  72. Special files can be managed via the ``mknod`` function. This function will
  73. create and enforce the permissions on a special file. The function supports the
  74. creation of character devices, block devices, and fifo pipes. The function will
  75. create the directory structure up to the special file if it is needed on the
  76. minion. The function will not overwrite or operate on (change major/minor
  77. numbers) existing special files with the exception of user, group, and
  78. permissions. In most cases the creation of some special files require root
  79. permisisons on the minion. This would require that the minion to be run as the
  80. root user. Here is an example of a character device:
  81. .. code-block:: yaml
  82. /var/named/chroot/dev/random:
  83. file.mknod:
  84. - ntype: c
  85. - major: 1
  86. - minor: 8
  87. - user: named
  88. - group: named
  89. - mode: 660
  90. Here is an example of a block device:
  91. .. code-block:: yaml
  92. /var/named/chroot/dev/loop0:
  93. file.mknod:
  94. - ntype: b
  95. - major: 7
  96. - minor: 0
  97. - user: named
  98. - group: named
  99. - mode: 660
  100. Here is an example of a fifo pipe:
  101. .. code-block:: yaml
  102. /var/named/chroot/var/log/logfifo:
  103. file.mknod:
  104. - ntype: p
  105. - user: named
  106. - group: named
  107. - mode: 660
  108. Directories can be managed via the ``directory`` function. This function can
  109. create and enforce the permissions on a directory. A directory statement will
  110. look like this:
  111. .. code-block:: yaml
  112. /srv/stuff/substuf:
  113. file.directory:
  114. - user: fred
  115. - group: users
  116. - mode: 755
  117. - makedirs: True
  118. If you need to enforce user and/or group ownership or permissions recursively
  119. on the directory's contents, you can do so by adding a ``recurse`` directive:
  120. .. code-block:: yaml
  121. /srv/stuff/substuf:
  122. file.directory:
  123. - user: fred
  124. - group: users
  125. - mode: 755
  126. - makedirs: True
  127. - recurse:
  128. - user
  129. - group
  130. - mode
  131. As a default, ``mode`` will resolve to ``dir_mode`` and ``file_mode``, to
  132. specify both directory and file permissions, use this form:
  133. .. code-block:: yaml
  134. /srv/stuff/substuf:
  135. file.directory:
  136. - user: fred
  137. - group: users
  138. - file_mode: 744
  139. - dir_mode: 755
  140. - makedirs: True
  141. - recurse:
  142. - user
  143. - group
  144. - mode
  145. Symlinks can be easily created; the symlink function is very simple and only
  146. takes a few arguments:
  147. .. code-block:: yaml
  148. /etc/grub.conf:
  149. file.symlink:
  150. - target: /boot/grub/grub.conf
  151. Recursive directory management can also be set via the ``recurse``
  152. function. Recursive directory management allows for a directory on the salt
  153. master to be recursively copied down to the minion. This is a great tool for
  154. deploying large code and configuration systems. A state using ``recurse``
  155. would look something like this:
  156. .. code-block:: yaml
  157. /opt/code/flask:
  158. file.recurse:
  159. - source: salt://code/flask
  160. - include_empty: True
  161. A more complex ``recurse`` example:
  162. .. code-block:: yaml
  163. {% set site_user = 'testuser' %}
  164. {% set site_name = 'test_site' %}
  165. {% set project_name = 'test_proj' %}
  166. {% set sites_dir = 'test_dir' %}
  167. django-project:
  168. file.recurse:
  169. - name: {{ sites_dir }}/{{ site_name }}/{{ project_name }}
  170. - user: {{ site_user }}
  171. - dir_mode: 2775
  172. - file_mode: '0644'
  173. - template: jinja
  174. - source: salt://project/templates_dir
  175. - include_empty: True
  176. '''
  177. # Import python libs
  178. import difflib
  179. import json
  180. import logging
  181. import os
  182. import pprint
  183. import shutil
  184. import traceback
  185. import yaml
  186. # Import salt libs
  187. import salt.utils
  188. import salt.utils.templates
  189. from salt.exceptions import CommandExecutionError
  190. from salt.utils.serializers import yaml as yaml_serializer
  191. from salt.utils.serializers import json as json_serializer
  192. from salt._compat import string_types, integer_types
  193. log = logging.getLogger(__name__)
  194. COMMENT_REGEX = r'^([[:space:]]*){0}[[:space:]]?'
  195. _ACCUMULATORS = {}
  196. _ACCUMULATORS_DEPS = {}
  197. def _check_user(user, group):
  198. '''
  199. Checks if the named user and group are present on the minion
  200. '''
  201. err = ''
  202. if user:
  203. uid = __salt__['file.user_to_uid'](user)
  204. if uid == '':
  205. err += 'User {0} is not available '.format(user)
  206. if group:
  207. gid = __salt__['file.group_to_gid'](group)
  208. if gid == '':
  209. err += 'Group {0} is not available'.format(group)
  210. return err
  211. def _gen_keep_files(name, require):
  212. '''
  213. Generate the list of files that need to be kept when a dir based function
  214. like directory or recurse has a clean.
  215. '''
  216. keep = set()
  217. keep.add(name)
  218. if isinstance(require, list):
  219. for comp in require:
  220. if 'file' in comp:
  221. keep.add(comp['file'])
  222. if os.path.isdir(comp['file']):
  223. for root, dirs, files in os.walk(comp['file']):
  224. for name in files:
  225. keep.add(os.path.join(root, name))
  226. for name in dirs:
  227. keep.add(os.path.join(root, name))
  228. return list(keep)
  229. def _check_file(name):
  230. ret = True
  231. msg = ''
  232. if not os.path.isabs(name):
  233. ret = False
  234. msg = 'Specified file {0} is not an absolute path'.format(name)
  235. elif not os.path.exists(name):
  236. ret = False
  237. msg = '{0}: file not found'.format(name)
  238. return ret, msg
  239. def _clean_dir(root, keep, exclude_pat):
  240. '''
  241. Clean out all of the files and directories in a directory (root) while
  242. preserving the files in a list (keep) and part of exclude_pat
  243. '''
  244. removed = set()
  245. real_keep = set()
  246. real_keep.add(root)
  247. if isinstance(keep, list):
  248. for fn_ in keep:
  249. if not os.path.isabs(fn_):
  250. continue
  251. real_keep.add(fn_)
  252. while True:
  253. fn_ = os.path.dirname(fn_)
  254. real_keep.add(fn_)
  255. if fn_ in ['/', ''.join([os.path.splitdrive(fn_)[0], '\\'])]:
  256. break
  257. for roots, dirs, files in os.walk(root):
  258. for name in dirs:
  259. nfn = os.path.join(roots, name)
  260. if os.path.islink(nfn):
  261. # the "directory" is in fact a symlink and cannot be
  262. # removed by shutil.rmtree
  263. files.append(nfn)
  264. continue
  265. if nfn not in real_keep:
  266. # -- check if this is a part of exclude_pat(only). No need to
  267. # check include_pat
  268. if not salt.utils.check_include_exclude(
  269. nfn[len(root) + 1:], None, exclude_pat):
  270. continue
  271. removed.add(nfn)
  272. if not __opts__['test']:
  273. shutil.rmtree(nfn)
  274. for name in files:
  275. nfn = os.path.join(roots, name)
  276. if nfn not in real_keep:
  277. # -- check if this is a part of exclude_pat(only). No need to
  278. # check include_pat
  279. if not salt.utils.check_include_exclude(
  280. nfn[len(root) + 1:], None, exclude_pat):
  281. continue
  282. removed.add(nfn)
  283. if not __opts__['test']:
  284. os.remove(nfn)
  285. return list(removed)
  286. def _error(ret, err_msg):
  287. ret['result'] = False
  288. ret['comment'] = err_msg
  289. return ret
  290. def _get_recurse_dest(prefix, fn_, source, env):
  291. '''
  292. Return the destination path to copy the file path, fn_(as returned by
  293. a call to __salt__['cp.cache_dir']), to.
  294. '''
  295. local_roots = []
  296. if __opts__['file_client'] == 'local':
  297. local_roots = __opts__['file_roots'][env]
  298. local_roots.sort(key=len, reverse=True)
  299. srcpath = source[7:] # the path after "salt://"
  300. # in solo mode(ie, file_client=='local'), fn_ is a path below
  301. # a file root; in remote mode, fn_ is a path below the cache_dir.
  302. for root in local_roots:
  303. rootlen = len(root)
  304. # if root is the longest prefix path of fn_
  305. if root == fn_[:rootlen]:
  306. cachedir = os.path.join(root, srcpath)
  307. break
  308. else:
  309. cachedir = os.path.join(
  310. __opts__['cachedir'], 'files', env, srcpath)
  311. return os.path.join(prefix, os.path.relpath(fn_, cachedir))
  312. def _check_directory(name,
  313. user,
  314. group,
  315. recurse,
  316. mode,
  317. clean,
  318. require,
  319. exclude_pat):
  320. '''
  321. Check what changes need to be made on a directory
  322. '''
  323. changes = {}
  324. if recurse:
  325. if not set(['user', 'group', 'mode']) >= set(recurse):
  326. return False, 'Types for "recurse" limited to "user", ' \
  327. '"group" and "mode"'
  328. if 'user' not in recurse:
  329. user = None
  330. if 'group' not in recurse:
  331. group = None
  332. if 'mode' not in recurse:
  333. mode = None
  334. for root, dirs, files in os.walk(name):
  335. for fname in files:
  336. fchange = {}
  337. path = os.path.join(root, fname)
  338. stats = __salt__['file.stats'](
  339. path, 'md5', follow_symlinks=False
  340. )
  341. if user is not None and user != stats.get('user'):
  342. fchange['user'] = user
  343. if group is not None and group != stats.get('group'):
  344. fchange['group'] = group
  345. if fchange:
  346. changes[path] = fchange
  347. for name_ in dirs:
  348. path = os.path.join(root, name_)
  349. fchange = _check_dir_meta(path, user, group, mode)
  350. if fchange:
  351. changes[path] = fchange
  352. else:
  353. fchange = _check_dir_meta(name, user, group, mode)
  354. if fchange:
  355. changes[name] = fchange
  356. if clean:
  357. keep = _gen_keep_files(name, require)
  358. for root, dirs, files in os.walk(name):
  359. for fname in files:
  360. fchange = {}
  361. path = os.path.join(root, fname)
  362. if path not in keep:
  363. if not salt.utils.check_include_exclude(
  364. path[len(name) + 1:], None, exclude_pat):
  365. continue
  366. fchange['removed'] = 'Removed due to clean'
  367. changes[path] = fchange
  368. for name_ in dirs:
  369. fchange = {}
  370. path = os.path.join(root, name_)
  371. if path not in keep:
  372. if not salt.utils.check_include_exclude(
  373. path[len(name) + 1:], None, exclude_pat):
  374. continue
  375. fchange['removed'] = 'Removed due to clean'
  376. changes[path] = fchange
  377. if not os.path.isdir(name):
  378. changes[name] = {'directory': 'new'}
  379. if changes:
  380. comments = ['The following files will be changed:\n']
  381. for fn_ in changes:
  382. key, val = changes[fn_].keys()[0], changes[fn_].values()[0]
  383. comments.append('{0}: {1} - {2}\n'.format(fn_, key, val))
  384. return None, ''.join(comments)
  385. return True, 'The directory {0} is in the correct state'.format(name)
  386. def _check_dir_meta(name,
  387. user,
  388. group,
  389. mode):
  390. '''
  391. Check the changes in directory metadata
  392. '''
  393. stats = __salt__['file.stats'](name, follow_symlinks=False)
  394. changes = {}
  395. if not stats:
  396. changes['directory'] = 'new'
  397. return changes
  398. if user is not None and user != stats['user']:
  399. changes['user'] = user
  400. if group is not None and group != stats['group']:
  401. changes['group'] = group
  402. # Normalize the dir mode
  403. smode = __salt__['config.manage_mode'](stats['mode'])
  404. mode = __salt__['config.manage_mode'](mode)
  405. if mode is not None and mode != smode:
  406. changes['mode'] = mode
  407. return changes
  408. def _check_touch(name, atime, mtime):
  409. '''
  410. Check to see if a file needs to be updated or created
  411. '''
  412. if not os.path.exists(name):
  413. return None, 'File {0} is set to be created'.format(name)
  414. stats = __salt__['file.stats'](name, follow_symlinks=False)
  415. if atime is not None:
  416. if str(atime) != str(stats['atime']):
  417. return None, 'Times set to be updated on file {0}'.format(name)
  418. if mtime is not None:
  419. if str(mtime) != str(stats['mtime']):
  420. return None, 'Times set to be updated on file {0}'.format(name)
  421. return True, 'File {0} exists and has the correct times'.format(name)
  422. def _get_symlink_ownership(path):
  423. return (
  424. __salt__['file.get_user'](path, follow_symlinks=False),
  425. __salt__['file.get_group'](path, follow_symlinks=False)
  426. )
  427. def _check_symlink_ownership(path, user, group):
  428. '''
  429. Check if the symlink ownership matches the specified user and group
  430. '''
  431. cur_user, cur_group = _get_symlink_ownership(path)
  432. return (cur_user == user) and (cur_group == group)
  433. def _set_symlink_ownership(path, user, group):
  434. '''
  435. Set the ownership of a symlink and return a boolean indicating
  436. success/failure
  437. '''
  438. try:
  439. __salt__['file.lchown'](path, user, group)
  440. except OSError:
  441. pass
  442. return _check_symlink_ownership(path, user, group)
  443. def _symlink_check(name, target, force, user, group):
  444. '''
  445. Check the symlink function
  446. '''
  447. if not os.path.exists(name) and not __salt__['file.is_link'](name):
  448. return None, 'Symlink {0} to {1} is set for creation'.format(
  449. name, target
  450. )
  451. if __salt__['file.is_link'](name):
  452. if __salt__['file.readlink'](name) != target:
  453. return None, 'Link {0} target is set to be changed to {1}'.format(
  454. name, target
  455. )
  456. else:
  457. result = True
  458. msg = 'The symlink {0} is present'.format(name)
  459. if not _check_symlink_ownership(name, user, group):
  460. result = None
  461. msg += (
  462. ', but the ownership of the symlink would be changed '
  463. 'from {2}:{3} to {0}:{1}'
  464. ).format(user, group, *_get_symlink_ownership(name))
  465. return result, msg
  466. else:
  467. if force:
  468. return None, ('The file or directory {0} is set for removal to '
  469. 'make way for a new symlink targeting {1}'
  470. .format(name, target))
  471. return False, ('File or directory exists where the symlink {0} '
  472. 'should be. Did you mean to use force?'.format(name))
  473. def _test_owner(kwargs, user=None):
  474. '''
  475. Convert owner to user, since other config management tools use owner,
  476. no need to punish people coming from other systems.
  477. PLEASE DO NOT DOCUMENT THIS! WE USE USER, NOT OWNER!!!!
  478. '''
  479. if user:
  480. return user
  481. if 'owner' in kwargs:
  482. log.warning(
  483. 'Use of argument owner found, "owner" is invalid, please '
  484. 'use "user"'
  485. )
  486. return kwargs['owner']
  487. return user
  488. def _unify_sources_and_hashes(source=None, source_hash=None,
  489. sources=None, source_hashes=None):
  490. '''
  491. Silly little function to give us a standard tuple list for sources and
  492. source_hashes
  493. '''
  494. if sources is None:
  495. sources = []
  496. if source_hashes is None:
  497. source_hashes = []
  498. if source and sources:
  499. return (False,
  500. "source and sources are mutually exclusive", [])
  501. if source_hash and source_hashes:
  502. return (False,
  503. "source_hash and source_hashes are mutually exclusive", [])
  504. if source:
  505. return (True, '', [(source, source_hash)])
  506. # Make a nice neat list of tuples exactly len(sources) long..
  507. return (True, '', map(None, sources, source_hashes[:len(sources)]))
  508. def _get_template_texts(source_list=None,
  509. template='jinja',
  510. defaults=None,
  511. context=None,
  512. **kwargs):
  513. '''
  514. Iterate a list of sources and process them as templates.
  515. Returns a list of 'chunks' containing the rendered templates.
  516. '''
  517. ret = {'name': '_get_template_texts',
  518. 'changes': {},
  519. 'result': True,
  520. 'comment': '',
  521. 'data': []}
  522. if source_list is None:
  523. return _error(ret,
  524. '_get_template_texts called with empty source_list')
  525. txtl = []
  526. for (source, source_hash) in source_list:
  527. tmpctx = defaults if defaults else {}
  528. if context:
  529. tmpctx.update(context)
  530. rndrd_templ_fn = __salt__['cp.get_template'](
  531. source,
  532. '',
  533. template=template,
  534. saltenv=__env__,
  535. context=tmpctx,
  536. **kwargs
  537. )
  538. msg = 'cp.get_template returned {0} (Called with: {1})'
  539. log.debug(msg.format(rndrd_templ_fn, source))
  540. if rndrd_templ_fn:
  541. tmplines = None
  542. with salt.utils.fopen(rndrd_templ_fn, 'rb') as fp_:
  543. tmplines = fp_.readlines()
  544. if not tmplines:
  545. msg = 'Failed to read rendered template file {0} ({1})'
  546. log.debug(msg.format(rndrd_templ_fn, source))
  547. ret['name'] = source
  548. return _error(ret, msg.format(rndrd_templ_fn, source))
  549. txtl.append(''.join(tmplines))
  550. else:
  551. msg = 'Failed to load template file {0}'.format(source)
  552. log.debug(msg)
  553. ret['name'] = source
  554. return _error(ret, msg)
  555. ret['data'] = txtl
  556. return ret
  557. def symlink(
  558. name,
  559. target,
  560. force=False,
  561. backupname=None,
  562. makedirs=False,
  563. user=None,
  564. group=None,
  565. mode=None,
  566. **kwargs):
  567. '''
  568. Create a symlink
  569. If the file already exists and is a symlink pointing to any location other
  570. than the specified target, the symlink will be replaced. If the symlink is
  571. a regular file or directory then the state will return False. If the
  572. regular file or directory is desired to be replaced with a symlink pass
  573. force: True, if it is to be renamed, pass a backupname.
  574. name
  575. The location of the symlink to create
  576. target
  577. The location that the symlink points to
  578. force
  579. If the name of the symlink exists and is not a symlink and
  580. force is set to False, the state will fail. If force is set to
  581. True, the file or directory in the way of the symlink file
  582. will be deleted to make room for the symlink, unless
  583. backupname is set, when it will be renamed
  584. backupname
  585. If the name of the symlink exists and is not a symlink, it will be
  586. renamed to the backupname. If the backupname already
  587. exists and force is False, the state will fail. Otherwise, the
  588. backupname will be removed first.
  589. makedirs
  590. If the location of the symlink does not already have a parent directory
  591. then the state will fail, setting makedirs to True will allow Salt to
  592. create the parent directory
  593. user
  594. The user to own the file, this defaults to the user salt is running as
  595. on the minion
  596. group
  597. The group ownership set for the file, this defaults to the group salt
  598. is running as on the minion. On Windows, this is ignored
  599. mode
  600. The permissions to set on this file, aka 644, 0775, 4664. Not supported
  601. on Windows
  602. '''
  603. # Make sure that leading zeros stripped by YAML loader are added back
  604. mode = __salt__['config.manage_mode'](mode)
  605. user = _test_owner(kwargs, user=user)
  606. ret = {'name': name,
  607. 'changes': {},
  608. 'result': True,
  609. 'comment': ''}
  610. if user is None:
  611. user = __opts__['user']
  612. if salt.utils.is_windows():
  613. if group is not None:
  614. log.warning(
  615. 'The group argument for {0} has been ignored as this '
  616. 'is a Windows system.'.format(name)
  617. )
  618. group = user
  619. if group is None:
  620. group = __salt__['file.gid_to_group'](
  621. __salt__['user.info'](user).get('gid', 0)
  622. )
  623. preflight_errors = []
  624. uid = __salt__['file.user_to_uid'](user)
  625. gid = __salt__['file.group_to_gid'](group)
  626. if uid == '':
  627. preflight_errors.append('User {0} does not exist'.format(user))
  628. if gid == '':
  629. preflight_errors.append('Group {0} does not exist'.format(group))
  630. if not os.path.isabs(name):
  631. preflight_errors.append(
  632. 'Specified file {0} is not an absolute path'.format(name)
  633. )
  634. if preflight_errors:
  635. msg = '. '.join(preflight_errors)
  636. if len(preflight_errors) > 1:
  637. msg += '.'
  638. return _error(ret, msg)
  639. if __opts__['test']:
  640. ret['result'], ret['comment'] = _symlink_check(name, target, force,
  641. user, group)
  642. return ret
  643. if not os.path.isdir(os.path.dirname(name)):
  644. if makedirs:
  645. __salt__['file.makedirs'](
  646. name,
  647. user=user,
  648. group=group,
  649. mode=mode)
  650. else:
  651. return _error(
  652. ret,
  653. 'Directory {0} for symlink is not present'.format(
  654. os.path.dirname(name)
  655. )
  656. )
  657. if __salt__['file.is_link'](name):
  658. # The link exists, verify that it matches the target
  659. if __salt__['file.readlink'](name) != target:
  660. # The target is wrong, delete the link
  661. os.remove(name)
  662. else:
  663. if _check_symlink_ownership(name, user, group):
  664. # The link looks good!
  665. ret['comment'] = ('Symlink {0} is present and owned by '
  666. '{1}:{2}'.format(name, user, group))
  667. else:
  668. if _set_symlink_ownership(name, user, group):
  669. ret['comment'] = ('Set ownership of symlink {0} to '
  670. '{1}:{2}'.format(name, user, group))
  671. ret['changes']['ownership'] = '{0}:{1}'.format(user, group)
  672. else:
  673. ret['result'] = False
  674. ret['comment'] += (
  675. 'Failed to set ownership of symlink {0} to '
  676. '{1}:{2}'.format(name, user, group)
  677. )
  678. return ret
  679. elif os.path.isfile(name) or os.path.isdir(name):
  680. # It is not a link, but a file or dir
  681. if backupname is not None:
  682. # Make a backup first
  683. if os.path.lexists(backupname):
  684. if not force:
  685. return _error(ret, ((
  686. 'File exists where the backup target {0} should go'
  687. ).format(backupname)))
  688. elif os.path.isfile(backupname):
  689. os.remove(backupname)
  690. elif os.path.isdir(backupname):
  691. shutil.rmtree(backupname)
  692. else:
  693. return _error(ret, ((
  694. 'Something exists where the backup target {0}'
  695. 'should go'
  696. ).format(backupname)))
  697. os.rename(name, backupname)
  698. elif force:
  699. # Remove whatever is in the way
  700. if os.path.isfile(name):
  701. os.remove(name)
  702. ret['changes']['forced'] = 'Symlink was forcibly replaced'
  703. else:
  704. shutil.rmtree(name)
  705. else:
  706. # Otherwise throw an error
  707. if os.path.isfile(name):
  708. return _error(ret,
  709. ('File exists where the symlink {0} should be'
  710. .format(name)))
  711. else:
  712. return _error(ret, ((
  713. 'Directory exists where the symlink {0} should be'
  714. ).format(name)))
  715. if not os.path.exists(name):
  716. # The link is not present, make it
  717. try:
  718. __salt__['file.symlink'](target, name)
  719. except OSError as exc:
  720. ret['result'] = False
  721. ret['comment'] = ('Unable to create new symlink {0} -> '
  722. '{1}: {2}'.format(name, target, exc))
  723. return ret
  724. else:
  725. ret['comment'] = ('Created new symlink {0} -> '
  726. '{1}'.format(name, target))
  727. ret['changes']['new'] = name
  728. if not _check_symlink_ownership(name, user, group):
  729. if not _set_symlink_ownership(name, user, group):
  730. ret['result'] = False
  731. ret['comment'] += (', but was unable to set ownership to '
  732. '{0}:{1}'.format(user, group))
  733. return ret
  734. def absent(name):
  735. '''
  736. Make sure that the named file or directory is absent. If it exists, it will
  737. be deleted. This will work to reverse any of the functions in the file
  738. state module.
  739. name
  740. The path which should be deleted
  741. '''
  742. ret = {'name': name,
  743. 'changes': {},
  744. 'result': True,
  745. 'comment': ''}
  746. if not os.path.isabs(name):
  747. return _error(
  748. ret, 'Specified file {0} is not an absolute path'.format(name)
  749. )
  750. if name == '/':
  751. return _error(ret, 'Refusing to make "/" absent')
  752. if os.path.isfile(name) or os.path.islink(name):
  753. if __opts__['test']:
  754. ret['result'] = None
  755. ret['comment'] = 'File {0} is set for removal'.format(name)
  756. return ret
  757. try:
  758. __salt__['file.remove'](name)
  759. ret['comment'] = 'Removed file {0}'.format(name)
  760. ret['changes']['removed'] = name
  761. return ret
  762. except CommandExecutionError as exc:
  763. return _error(ret, '{0}'.format(exc))
  764. elif os.path.isdir(name):
  765. if __opts__['test']:
  766. ret['result'] = None
  767. ret['comment'] = 'Directory {0} is set for removal'.format(name)
  768. return ret
  769. try:
  770. shutil.rmtree(name)
  771. ret['comment'] = 'Removed directory {0}'.format(name)
  772. ret['changes']['removed'] = name
  773. return ret
  774. except (OSError, IOError):
  775. return _error(ret, 'Failed to remove directory {0}'.format(name))
  776. ret['comment'] = 'File {0} is not present'.format(name)
  777. return ret
  778. def exists(name):
  779. '''
  780. Verify that the named file or directory is present or exists.
  781. Ensures pre-requisites outside of Salt's purview
  782. (e.g., keytabs, private keys, etc.) have been previously satisfied before
  783. deployment.
  784. name
  785. Absolute path which must exist
  786. '''
  787. ret = {'name': name,
  788. 'changes': {},
  789. 'result': True,
  790. 'comment': ''}
  791. if not os.path.exists(name):
  792. return _error(ret, ('Specified path {0} does not exist').format(name))
  793. ret['comment'] = 'Path {0} exists'.format(name)
  794. return ret
  795. def missing(name):
  796. '''
  797. Verify that the named file or directory is missing, this returns True only
  798. if the named file is missing but does not remove the file if it is present.
  799. name
  800. Absolute path which must NOT exist
  801. '''
  802. ret = {'name': name,
  803. 'changes': {},
  804. 'result': True,
  805. 'comment': ''}
  806. if os.path.exists(name):
  807. return _error(ret, ('Specified path {0} exists').format(name))
  808. ret['comment'] = 'Path {0} is missing'.format(name)
  809. return ret
  810. def managed(name,
  811. source=None,
  812. source_hash='',
  813. user=None,
  814. group=None,
  815. mode=None,
  816. template=None,
  817. makedirs=False,
  818. dir_mode=None,
  819. context=None,
  820. replace=True,
  821. defaults=None,
  822. env=None,
  823. backup='',
  824. show_diff=True,
  825. create=True,
  826. contents=None,
  827. contents_pillar=None,
  828. contents_grains=None,
  829. contents_newline=True,
  830. follow_symlinks=True,
  831. check_cmd=None,
  832. **kwargs):
  833. '''
  834. Manage a given file, this function allows for a file to be downloaded from
  835. the salt master and potentially run through a templating system.
  836. name
  837. The location of the file to manage
  838. source
  839. The source file to download to the minion, this source file can be
  840. hosted on either the salt master server, or on an HTTP or FTP server.
  841. Both HTTPS and HTTP are supported as well as downloading directly
  842. from Amazon S3 compatible URLs with both pre-configured and automatic
  843. IAM credentials. (see s3.get state documentation)
  844. File retrieval from Openstack Swift object storage is supported via
  845. swift://container/object_path URLs, see swift.get documentation.
  846. For files hosted on the salt file server, if the file is located on
  847. the master in the directory named spam, and is called eggs, the source
  848. string is salt://spam/eggs. If source is left blank or None
  849. (use ~ in YAML), the file will be created as an empty file and
  850. the content will not be managed
  851. If the file is hosted on a HTTP or FTP server then the source_hash
  852. argument is also required
  853. source_hash
  854. This can be one of the following:
  855. 1. a source hash string
  856. 2. the URI of a file that contains source hash strings
  857. The function accepts the first encountered long unbroken alphanumeric
  858. string of correct length as a valid hash, in order from most secure to
  859. least secure::
  860. Type Length
  861. ====== ======
  862. sha512 128
  863. sha384 96
  864. sha256 64
  865. sha224 56
  866. sha1 40
  867. md5 32
  868. The file can contain several checksums for several files. Each line
  869. must contain both the file name and the hash. If no file name is
  870. matched, the first hash encountered will be used, otherwise the most
  871. secure hash with the correct source file name will be used.
  872. Debian file type ``*.dsc`` is supported.
  873. Examples::
  874. /etc/rc.conf ef6e82e4006dee563d98ada2a2a80a27
  875. sha254c8525aee419eb649f0233be91c151178b30f0dff8ebbdcc8de71b1d5c8bcc06a /etc/resolv.conf
  876. ead48423703509d37c4a90e6a0d53e143b6fc268
  877. Known issues:
  878. If the remote server URL has the hash file as an apparent
  879. sub-directory of the source file, the module will discover that it
  880. has already cached a directory where a file should be cached. For
  881. example:
  882. .. code-block:: yaml
  883. tomdroid-src-0.7.3.tar.gz:
  884. file.managed:
  885. - name: /tmp/tomdroid-src-0.7.3.tar.gz
  886. - source: https://launchpad.net/tomdroid/beta/0.7.3/+download/tomdroid-src-0.7.3.tar.gz
  887. - source_hash: https://launchpad.net/tomdroid/beta/0.7.3/+download/tomdroid-src-0.7.3.tar.gz/+md5
  888. user
  889. The user to own the file, this defaults to the user salt is running as
  890. on the minion
  891. group
  892. The group ownership set for the file, this defaults to the group salt
  893. is running as on the minion On Windows, this is ignored
  894. mode
  895. The permissions to set on this file, aka 644, 0775, 4664. Not supported
  896. on Windows
  897. template
  898. If this setting is applied then the named templating engine will be
  899. used to render the downloaded file, currently jinja, mako, and wempy
  900. are supported
  901. makedirs
  902. If the file is located in a path without a parent directory, then
  903. the state will fail. If makedirs is set to True, then the parent
  904. directories will be created to facilitate the creation of the named
  905. file.
  906. dir_mode
  907. If directories are to be created, passing this option specifies the
  908. permissions for those directories. If this is not set, directories
  909. will be assigned permissions from the 'mode' argument.
  910. replace
  911. If this file should be replaced. If false, this command will
  912. not overwrite file contents but will enforce permissions if the file
  913. exists already. Default is True.
  914. context
  915. Overrides default context variables passed to the template.
  916. defaults
  917. Default context passed to the template.
  918. backup
  919. Overrides the default backup mode for this specific file.
  920. show_diff
  921. If set to False, the diff will not be shown.
  922. create
  923. Default is True, if create is set to False then the file will only be
  924. managed if the file already exists on the system.
  925. contents
  926. Default is None. If specified, will use the given string as the
  927. contents of the file. Should not be used in conjunction with a source
  928. file of any kind. Ignores hashes and does not use a templating engine.
  929. contents_pillar
  930. .. versionadded:: 0.17.0
  931. Operates like ``contents``, but draws from a value stored in pillar,
  932. using the pillar path syntax used in :mod:`pillar.get
  933. <salt.modules.pillar.get>`. This is useful when the pillar value
  934. contains newlines, as referencing a pillar variable using a jinja/mako
  935. template can result in YAML formatting issues due to the newlines
  936. causing indentation mismatches.
  937. For example, the following could be used to deploy an SSH private key:
  938. .. code-block:: yaml
  939. /home/deployer/.ssh/id_rsa:
  940. file.managed:
  941. - user: deployer
  942. - group: deployer
  943. - mode: 600
  944. - contents_pillar: userdata:deployer:id_rsa
  945. This would populate ``/home/deployer/.ssh/id_rsa`` with the contents of
  946. ``pillar['userdata']['deployer']['id_rsa']``. An example of this pillar
  947. setup would be like so:
  948. .. code-block:: yaml
  949. userdata:
  950. deployer:
  951. id_rsa: |
  952. -----BEGIN RSA PRIVATE KEY-----
  953. MIIEowIBAAKCAQEAoQiwO3JhBquPAalQF9qP1lLZNXVjYMIswrMe2HcWUVBgh+vY
  954. U7sCwx/dH6+VvNwmCoqmNnP+8gTPKGl1vgAObJAnMT623dMXjVKwnEagZPRJIxDy
  955. B/HaAre9euNiY3LvIzBTWRSeMfT+rWvIKVBpvwlgGrfgz70m0pqxu+UyFbAGLin+
  956. GpxzZAMaFpZw4sSbIlRuissXZj/sHpQb8p9M5IeO4Z3rjkCP1cxI
  957. -----END RSA PRIVATE KEY-----
  958. .. note::
  959. The private key above is shortened to keep the example brief, but
  960. shows how to do multiline string in YAML. The key is followed by a
  961. pipe character, and the mutliline string is indented two more
  962. spaces.
  963. contents_grains
  964. .. versionadded:: Helium
  965. Same as contents_pillar, but with grains
  966. contents_newline
  967. .. versionadded:: Helium
  968. When using contents, contents_pillar, or contents_grains, a newline is
  969. inserted into the data. When loading some data this newline is better
  970. left off. Setting contents_newline to False will omit this newline.
  971. follow_symlinks : True
  972. .. versionadded:: Helium
  973. If the desired path is a symlink follow it and make changes to the
  974. file to which the symlink points.
  975. check_cmd
  976. .. versionadded:: Helium
  977. Do run the state only if the check_cmd succeeds
  978. '''
  979. # Make sure that leading zeros stripped by YAML loader are added back
  980. mode = __salt__['config.manage_mode'](mode)
  981. # If no source is specified, set replace to False, as there is nothing
  982. # to replace the file with.
  983. src_defined = source or contents or contents_pillar or contents_grains
  984. if not src_defined and replace:
  985. replace = False
  986. log.warning(
  987. 'Neither \'source\' nor \'contents\' nor \'contents_pillar\' nor \'contents_grains\' '
  988. 'was defined, yet \'replace\' was set to \'True\'. As there is '
  989. 'no source to replace the file with, \'replace\' has been set '
  990. 'to \'False\' to avoid reading the file unnecessarily'.format(name)
  991. )
  992. user = _test_owner(kwargs, user=user)
  993. if salt.utils.is_windows():
  994. if group is not None:
  995. log.warning(
  996. 'The group argument for {0} has been ignored as this '
  997. 'is a Windows system.'.format(name)
  998. )
  999. group = user
  1000. ret = {'changes': {},
  1001. 'comment': '',
  1002. 'name': name,
  1003. 'result': True}
  1004. if not create:
  1005. if not os.path.isfile(name):
  1006. # Don't create a file that is not already present
  1007. ret['comment'] = ('File {0} is not present and is not set for '
  1008. 'creation').format(name)
  1009. return ret
  1010. u_check = _check_user(user, group)
  1011. if u_check:
  1012. # The specified user or group do not exist
  1013. return _error(ret, u_check)
  1014. if not os.path.isabs(name):
  1015. return _error(
  1016. ret, 'Specified file {0} is not an absolute path'.format(name))
  1017. if isinstance(env, salt._compat.string_types):
  1018. msg = (
  1019. 'Passing a salt environment should be done using \'saltenv\' not '
  1020. '\'env\'. This warning will go away in Salt Boron and this '
  1021. 'will be the default and expected behaviour. Please update your '
  1022. 'state files.'
  1023. )
  1024. salt.utils.warn_until('Boron', msg)
  1025. ret.setdefault('warnings', []).append(msg)
  1026. # No need to set __env__ = env since that's done in the state machinery
  1027. if os.path.isdir(name):
  1028. ret['comment'] = 'Specified target {0} is a directory'.format(name)
  1029. ret['result'] = False
  1030. return ret
  1031. if context is None:
  1032. context = {}
  1033. elif not isinstance(context, dict):
  1034. return _error(
  1035. ret, 'Context must be formed as a dict')
  1036. if len(filter(None, [contents, contents_pillar, contents_grains])) > 1:
  1037. return _error(
  1038. ret, 'Only one of contents, contents_pillar, and contents_grains is permitted')
  1039. # If contents_pillar was used, get the pillar data
  1040. if contents_pillar:
  1041. contents = __salt__['pillar.get'](contents_pillar)
  1042. if contents_grains:
  1043. contents = __salt__['grains.get'](contents_grains)
  1044. if contents_newline:
  1045. # Make sure file ends in newline
  1046. if contents and not contents.endswith('\n'):
  1047. contents += '\n'
  1048. if not replace and os.path.exists(name):
  1049. # Check and set the permissions if necessary
  1050. ret, _ = __salt__['file.check_perms'](name, ret, user, group, mode, follow_symlinks)
  1051. if __opts__['test']:
  1052. ret['comment'] = 'File {0} not updated'.format(name)
  1053. elif not ret['changes'] and ret['result']:
  1054. ret['comment'] = ('File {0} exists with proper permissions. '
  1055. 'No changes made.'.format(name))
  1056. return ret
  1057. if name in _ACCUMULATORS:
  1058. if not context:
  1059. context = {}
  1060. context['accumulator'] = _ACCUMULATORS[name]
  1061. try:
  1062. if __opts__['test']:
  1063. ret['result'], ret['comment'] = __salt__['file.check_managed'](
  1064. name,
  1065. source,
  1066. source_hash,
  1067. user,
  1068. group,
  1069. mode,
  1070. template,
  1071. context,
  1072. defaults,
  1073. __env__,
  1074. contents,
  1075. **kwargs
  1076. )
  1077. return ret
  1078. # If the source is a list then find which file exists
  1079. source, source_hash = __salt__['file.source_list'](
  1080. source,
  1081. source_hash,
  1082. __env__
  1083. )
  1084. except CommandExecutionError as exc:
  1085. ret['result'] = False
  1086. ret['comment'] = 'Unable to manage file: {0}'.format(exc)
  1087. return ret
  1088. # Gather the source file from the server
  1089. try:
  1090. sfn, source_sum, comment_ = __salt__['file.get_managed'](
  1091. name,
  1092. template,
  1093. source,
  1094. source_hash,
  1095. user,
  1096. group,
  1097. mode,
  1098. __env__,
  1099. context,
  1100. defaults,
  1101. **kwargs
  1102. )
  1103. except Exception as exc:
  1104. ret['changes'] = {}
  1105. log.debug(traceback.format_exc())
  1106. return _error(ret, 'Unable to manage file: {0}'.format(exc))
  1107. if check_cmd:
  1108. tmp_filename = salt.utils.mkstemp()
  1109. # if exists copy existing file to tmp to compare
  1110. if __salt__['file.file_exists'](name):
  1111. try:
  1112. __salt__['file.copy'](name, tmp_filename)
  1113. except Exception as exc:
  1114. return _error(ret, 'Unable to copy file {0} to {1}: {2}'.format(name, tmp_filename, exc))
  1115. try:
  1116. ret = __salt__['file.manage_file'](
  1117. tmp_filename,
  1118. sfn,
  1119. ret,
  1120. source,
  1121. source_sum,
  1122. user,
  1123. group,
  1124. mode,
  1125. __env__,
  1126. backup,
  1127. makedirs,
  1128. template,
  1129. show_diff,
  1130. contents,
  1131. dir_mode,
  1132. follow_symlinks)
  1133. except Exception as exc:
  1134. ret['changes'] = {}
  1135. log.debug(traceback.format_exc())
  1136. return _error(ret, 'Unable to check_cmd file: {0}'.format(exc))
  1137. # file being updated to verify using check_cmd
  1138. if ret['changes']:
  1139. # Reset ret
  1140. ret = {'changes': {},
  1141. 'comment': '',
  1142. 'name': name,
  1143. 'result': True}
  1144. cret = mod_run_check_cmd(
  1145. check_cmd, tmp_filename
  1146. )
  1147. if isinstance(cret, dict):
  1148. ret.update(cret)
  1149. return ret
  1150. else:
  1151. ret = {'changes': {},
  1152. 'comment': '',
  1153. 'name': name,
  1154. 'result': True}
  1155. if comment_ and contents is None:
  1156. return _error(ret, comment_)
  1157. else:
  1158. try:
  1159. return __salt__['file.manage_file'](
  1160. name,
  1161. sfn,
  1162. ret,
  1163. source,
  1164. source_sum,
  1165. user,
  1166. group,
  1167. mode,
  1168. __env__,
  1169. backup,
  1170. makedirs,
  1171. template,
  1172. show_diff,
  1173. contents,
  1174. dir_mode,
  1175. follow_symlinks)
  1176. except Exception as exc:
  1177. ret['changes'] = {}
  1178. log.debug(traceback.format_exc())
  1179. return _error(ret, 'Unable to manage file: {0}'.format(exc))
  1180. def directory(name,
  1181. user=None,
  1182. group=None,
  1183. recurse=None,
  1184. dir_mode=None,
  1185. file_mode=None,
  1186. makedirs=False,
  1187. clean=False,
  1188. require=None,
  1189. exclude_pat=None,
  1190. follow_symlinks=False,
  1191. **kwargs):
  1192. '''
  1193. Ensure that a named directory is present and has the right perms
  1194. name
  1195. The location to create or manage a directory
  1196. user
  1197. The user to own the directory; this defaults to the user salt is
  1198. running as on the minion
  1199. group
  1200. The group ownership set for the directory; this defaults to the group
  1201. salt is running as on the minion. On Windows, this is ignored
  1202. recurse
  1203. Enforce user/group ownership and mode of directory recursively. Accepts
  1204. a list of strings representing what you would like to recurse.
  1205. Example:
  1206. .. code-block:: yaml
  1207. /var/log/httpd:
  1208. file.directory:
  1209. - user: root
  1210. - group: root
  1211. - dir_mode: 755
  1212. - file_mode: 644
  1213. - recurse:
  1214. - user
  1215. - group
  1216. - mode
  1217. dir_mode / mode
  1218. The permissions mode to set any directories created. Not supported on
  1219. Windows
  1220. file_mode
  1221. The permissions mode to set any files created if 'mode' is ran in
  1222. 'recurse'. This defaults to dir_mode. Not supported on Windows
  1223. makedirs
  1224. If the directory is located in a path without a parent directory, then
  1225. the state will fail. If makedirs is set to True, then the parent
  1226. directories will be created to facilitate the creation of the named
  1227. file.
  1228. clean
  1229. Make sure that only files that are set up by salt and required by this
  1230. function are kept. If this option is set then everything in this
  1231. directory will be deleted unless it is required.
  1232. require
  1233. Require other resources such as packages or files
  1234. exclude_pat
  1235. When 'clean' is set to True, exclude this pattern from removal list
  1236. and preserve in the destination.
  1237. follow_symlinks : False
  1238. If the desired path is a symlink (or ``recurse`` is defined and a
  1239. symlink is encountered while recursing), follow it and check the
  1240. permissions of the directory/file to which the symlink points.
  1241. .. versionadded:: 2014.1.4
  1242. '''
  1243. # Remove trailing slash, if present
  1244. if name[-1] == '/':
  1245. name = name[:-1]
  1246. user = _test_owner(kwargs, user=user)
  1247. if salt.utils.is_windows():
  1248. if group is not None:
  1249. log.warning(
  1250. 'The group argument for {0} has been ignored as this is '
  1251. 'a Windows system.'.format(name)
  1252. )
  1253. group = user
  1254. if 'mode' in kwargs and not dir_mode:
  1255. dir_mode = kwargs.get('mode', [])
  1256. if not file_mode:
  1257. file_mode = dir_mode
  1258. # Make sure that leading zeros stripped by YAML loader are added back
  1259. dir_mode = __salt__['config.manage_mode'](dir_mode)
  1260. file_mode = __salt__['config.manage_mode'](file_mode)
  1261. ret = {'name': name,
  1262. 'changes': {},
  1263. 'result': True,
  1264. 'comment': ''}
  1265. u_check = _check_user(user, group)
  1266. if u_check:
  1267. # The specified user or group do not exist
  1268. return _error(ret, u_check)
  1269. if not os.path.isabs(name):
  1270. return _error(
  1271. ret, 'Specified file {0} is not an absolute path'.format(name))
  1272. if os.path.isfile(name):
  1273. return _error(
  1274. ret, 'Specified location {0} exists and is a file'.format(name))
  1275. if __opts__['test']:
  1276. ret['result'], ret['comment'] = _check_directory(
  1277. name,
  1278. user,
  1279. group,
  1280. recurse or [],
  1281. dir_mode,
  1282. clean,
  1283. require,
  1284. exclude_pat)
  1285. return ret
  1286. if not os.path.isdir(name):
  1287. # The dir does not exist, make it
  1288. if not os.path.isdir(os.path.dirname(name)):
  1289. # The parent directory does not exist, create them
  1290. if makedirs:
  1291. __salt__['file.makedirs'](
  1292. name, user=user, group=group, mode=dir_mode
  1293. )
  1294. else:
  1295. return _error(
  1296. ret, 'No directory to create {0} in'.format(name))
  1297. __salt__['file.mkdir'](
  1298. name, user=user, group=group, mode=dir_mode
  1299. )
  1300. ret['changes'][name] = 'New Dir'
  1301. if not os.path.isdir(name):
  1302. return _error(ret, 'Failed to create directory {0}'.format(name))
  1303. # Check permissions
  1304. ret, perms = __salt__['file.check_perms'](name,
  1305. ret,
  1306. user,
  1307. group,
  1308. dir_mode,
  1309. follow_symlinks)
  1310. if recurse:
  1311. if not isinstance(recurse, list):
  1312. ret['result'] = False
  1313. ret['comment'] = '"recurse" must be formed as a list of strings'
  1314. elif not set(['user', 'group', 'mode']) >= set(recurse):
  1315. ret['result'] = False
  1316. ret['comment'] = 'Types for "recurse" limited to "user", ' \
  1317. '"group" and "mode"'
  1318. else:
  1319. if 'user' in recurse:
  1320. if user:
  1321. uid = __salt__['file.user_to_uid'](user)
  1322. # file.user_to_uid returns '' if user does not exist. Above
  1323. # check for user is not fatal, so we need to be sure user
  1324. # exists.
  1325. if isinstance(uid, string_types):
  1326. ret['result'] = False
  1327. ret['comment'] = 'Failed to enforce ownership for ' \
  1328. 'user {0} (user does not ' \
  1329. 'exist)'.format(user)
  1330. else:
  1331. ret['result'] = False
  1332. ret['comment'] = 'user not specified, but configured as ' \
  1333. 'a target for recursive ownership ' \
  1334. 'management'
  1335. else:
  1336. user = None
  1337. if 'group' in recurse:
  1338. if group:
  1339. gid = __salt__['file.group_to_gid'](group)
  1340. # As above with user, we need to make sure group exists.
  1341. if isinstance(gid, string_types):
  1342. ret['result'] = False
  1343. ret['comment'] = 'Failed to enforce group ownership ' \
  1344. 'for group {0}'.format(group, user)
  1345. else:
  1346. ret['result'] = False
  1347. ret['comment'] = 'group not specified, but configured ' \
  1348. 'as a target for recursive ownership ' \
  1349. 'management'
  1350. else:
  1351. group = None
  1352. if 'mode' not in recurse:
  1353. file_mode = None
  1354. dir_mode = None
  1355. for root, dirs, files in os.walk(name):
  1356. for fn_ in files:
  1357. full = os.path.join(root, fn_)
  1358. ret, perms = __salt__['file.check_perms'](
  1359. full,
  1360. ret,
  1361. user,
  1362. group,
  1363. file_mode,
  1364. follow_symlinks)
  1365. for dir_ in dirs:
  1366. full = os.path.join(root, dir_)
  1367. ret, perms = __salt__['file.check_perms'](
  1368. full,
  1369. ret,
  1370. user,
  1371. group,
  1372. dir_mode,
  1373. follow_symlinks)
  1374. if clean:
  1375. keep = _gen_keep_files(name, require)
  1376. removed = _clean_dir(name, list(keep), exclude_pat)
  1377. if removed:
  1378. ret['changes']['removed'] = removed
  1379. ret['comment'] = 'Files cleaned from directory {0}'.format(name)
  1380. if not ret['comment']:
  1381. ret['comment'] = 'Directory {0} updated'.format(name)
  1382. if __opts__['test']:
  1383. ret['comment'] = 'Directory {0} not updated'.format(name)
  1384. elif not ret['changes'] and ret['result']:
  1385. ret['comment'] = 'Directory {0} is in the correct state'.format(name)
  1386. return ret
  1387. def recurse(name,
  1388. source,
  1389. clean=False,
  1390. require=None,
  1391. user=None,
  1392. group=None,
  1393. dir_mode=None,
  1394. file_mode=None,
  1395. sym_mode=None,
  1396. template=None,
  1397. context=None,
  1398. defaults=None,
  1399. env=None,
  1400. include_empty=False,
  1401. backup='',
  1402. include_pat=None,
  1403. exclude_pat=None,
  1404. maxdepth=None,
  1405. keep_symlinks=False,
  1406. force_symlinks=False,
  1407. **kwargs):
  1408. '''
  1409. Recurse through a subdirectory on the master and copy said subdirectory
  1410. over to the specified path.
  1411. name
  1412. The directory to set the recursion in
  1413. source
  1414. The source directory, this directory is located on the salt master file
  1415. server and is specified with the salt:// protocol. If the directory is
  1416. located on the master in the directory named spam, and is called eggs,
  1417. the source string is salt://spam/eggs
  1418. clean
  1419. Make sure that only files that are set up by salt and required by this
  1420. function are kept. If this option is set then everything in this
  1421. directory will be deleted unless it is required.
  1422. require
  1423. Require other resources such as packages or files
  1424. user
  1425. The user to own the directory. This defaults to the user salt is
  1426. running as on the minion
  1427. group
  1428. The group ownership set for the directory. This defaults to the group
  1429. salt is running as on the minion. On Windows, this is ignored
  1430. dir_mode
  1431. The permissions mode to set on any directories created. Not supported on
  1432. Windows
  1433. file_mode
  1434. The permissions mode to set on any files created. Not supported on
  1435. Windows
  1436. sym_mode
  1437. The permissions mode to set on any symlink created. Not supported on
  1438. Windows
  1439. template
  1440. If this setting is applied then the named templating engine will be
  1441. used to render the downloaded file. Supported templates are:
  1442. `jinja`, `mako` and `wempy`.
  1443. .. note::
  1444. The template option is required when recursively applying templates.
  1445. context
  1446. Overrides default context variables passed to the template.
  1447. defaults
  1448. Default context passed to the template.
  1449. include_empty
  1450. Set this to True if empty directories should also be created
  1451. (default is False)
  1452. include_pat
  1453. When copying, include only this pattern from the source. Default
  1454. is glob match; if prefixed with 'E@', then regexp match.
  1455. Example:
  1456. .. code-block:: yaml
  1457. - include_pat: hello* :: glob matches 'hello01', 'hello02'
  1458. ... but not 'otherhello'
  1459. - include_pat: E@hello :: regexp matches 'otherhello',
  1460. 'hello01' ...
  1461. exclude_pat
  1462. Exclude this pattern from the source when copying. If both
  1463. `include_pat` and `exclude_pat` are supplied, then it will apply
  1464. conditions cumulatively. i.e. first select based on include_pat, and
  1465. then within that result apply exclude_pat.
  1466. Also, when 'clean=True', exclude this pattern from the removal
  1467. list and preserve in the destination.
  1468. Example:
  1469. .. code-block:: yaml
  1470. - exclude_pat: APPDATA* :: glob matches APPDATA.01,
  1471. APPDATA.02,.. for exclusion
  1472. - exclude_pat: E@(APPDATA)|(TEMPDATA) :: regexp matches APPDATA
  1473. or TEMPDATA for exclusion
  1474. maxdepth
  1475. When copying, only copy paths which are of depth `maxdepth` from the
  1476. source path.
  1477. Example:
  1478. .. code-block:: yaml
  1479. - maxdepth: 0 :: Only include files located in the source
  1480. directory
  1481. - maxdepth: 1 :: Only include files located in the source
  1482. or immediate subdirectories
  1483. keep_symlinks
  1484. Keep symlinks when copying from the source. This option will cause
  1485. the copy operation to terminate at the symlink. If desire behavior
  1486. similar to rsync, then set this to True.
  1487. force_symlinks
  1488. Force symlink creation. This option will force the symlink creation.
  1489. If a file or directory is obstructing symlink creation it will be
  1490. recursively removed so that symlink creation can proceed. This
  1491. option is usually not needed except in special circumstances.
  1492. '''
  1493. user = _test_owner(kwargs, user=user)
  1494. if salt.utils.is_windows():
  1495. if group is not None:
  1496. log.warning(
  1497. 'The group argument for {0} has been ignored as this '
  1498. 'is a Windows system.'.format(name)
  1499. )
  1500. group = user
  1501. ret = {
  1502. 'name': name,
  1503. 'changes': {},
  1504. 'result': True,
  1505. 'comment': {} # { path: [comment, ...] }
  1506. }
  1507. try:
  1508. source = source.rstrip('/')
  1509. except AttributeError:
  1510. ret['result'] = False
  1511. ret['comment'] = '\'source\' parameter must be a string'
  1512. return ret
  1513. if 'mode' in kwargs:
  1514. ret['result'] = False
  1515. ret['comment'] = (
  1516. '\'mode\' is not allowed in \'file.recurse\'. Please use '
  1517. '\'file_mode\' and \'dir_mode\'.'
  1518. )
  1519. return ret
  1520. # Make sure that leading zeros stripped by YAML loader are added back
  1521. dir_mode = __salt__['config.manage_mode'](dir_mode)
  1522. file_mode = __salt__['config.manage_mode'](file_mode)
  1523. u_check = _check_user(user, group)
  1524. if u_check:
  1525. # The specified user or group do not exist
  1526. return _error(ret, u_check)
  1527. if not os.path.isabs(name):
  1528. return _error(
  1529. ret, 'Specified file {0} is not an absolute path'.format(name))
  1530. if isinstance(env, salt._compat.string_types):
  1531. msg = (
  1532. 'Passing a salt environment should be done using \'saltenv\' not '
  1533. '\'env\'. This warning will go away in Salt Boron and this '
  1534. 'will be the default and expected behaviour. Please update your '
  1535. 'state files.'
  1536. )
  1537. salt.utils.warn_until('Boron', msg)
  1538. ret.setdefault('warnings', []).append(msg)
  1539. # No need to set __env__ = env since that's done in the state machinery
  1540. # Handle corner case where someone uses a numeric source
  1541. if isinstance(source, (integer_types, float)):
  1542. ret['result'] = False
  1543. ret['comment'] = ('Invalid source {0} (cannot be numeric)'
  1544. .format(source))
  1545. return ret
  1546. # Make sure that only salt fileserver paths are being used (no http(s)/ftp)
  1547. if isinstance(source, string_types):
  1548. source_precheck = [source]
  1549. else:
  1550. source_precheck = source
  1551. for precheck in source_precheck:
  1552. if not precheck.startswith('salt://'):
  1553. ret['result'] = False
  1554. ret['comment'] = ('Invalid source {0!r} (must be a salt:// URI)'
  1555. .format(precheck))
  1556. return ret
  1557. # If source is a list, find which in the list actually exists
  1558. try:
  1559. source, source_hash = __salt__['file.source_list'](source, '', __env__)
  1560. except CommandExecutionError as exc:
  1561. ret['result'] = False
  1562. ret['comment'] = 'Recurse failed: {0}'.format(exc)
  1563. return ret
  1564. # Check source path relative to fileserver root, make sure it is a
  1565. # directory
  1566. source_rel = source.partition('://')[2]
  1567. master_dirs = __salt__['cp.list_master_dirs'](__env__)
  1568. if source_rel not in master_dirs \
  1569. and not any((x for x in master_dirs
  1570. if x.startswith(source_rel + '/'))):
  1571. ret['result'] = False
  1572. ret['comment'] = (
  1573. 'The directory {0!r} does not exist on the salt fileserver '
  1574. 'in saltenv {1!r}'.format(source, __env__)
  1575. )
  1576. return ret
  1577. # Verify the target directory
  1578. if not os.path.isdir(name):
  1579. if os.path.exists(name):
  1580. # it is not a dir, but it exists - fail out
  1581. return _error(
  1582. ret, 'The path {0} exists and is not a directory'.format(name))
  1583. if not __opts__['test']:
  1584. __salt__['file.makedirs_perms'](
  1585. name, user, group, int(str(dir_mode), 8) if dir_mode else None)
  1586. def add_comment(path, comment):
  1587. comments = ret['comment'].setdefault(path, [])
  1588. if isinstance(comment, string_types):
  1589. comments.append(comment)
  1590. else:
  1591. comments.extend(comment)
  1592. def merge_ret(path, _ret):
  1593. # Use the most "negative" result code (out of True, None, False)
  1594. if _ret['result'] is False or ret['result'] is True:
  1595. ret['result'] = _ret['result']
  1596. # Only include comments about files that changed
  1597. if _ret['result'] is not True and _ret['comment']:
  1598. add_comment(path, _ret['comment'])
  1599. if _ret['changes']:
  1600. ret['changes'][path] = _ret['changes']
  1601. def manage_file(path, source):
  1602. source = '{0}|{1}'.format(source[:7], source[7:])
  1603. if clean and os.path.exists(path) and os.path.isdir(path):
  1604. _ret = {'name': name, 'changes': {}, 'result': True, 'comment': ''}
  1605. if __opts__['test']:
  1606. _ret['comment'] = 'Replacing directory {0} with a ' \
  1607. 'file'.format(path)
  1608. _ret['result'] = None
  1609. merge_ret(path, _ret)
  1610. return
  1611. else:
  1612. shutil.rmtree(path)
  1613. _ret['changes'] = {'diff': 'Replaced directory with a '
  1614. 'new file'}
  1615. merge_ret(path, _ret)
  1616. # Conflicts can occur if some kwargs are passed in here
  1617. pass_kwargs = {}
  1618. faults = ['mode', 'makedirs']
  1619. for key in kwargs:
  1620. if key not in faults:
  1621. pass_kwargs[key] = kwargs[key]
  1622. _ret = managed(
  1623. path,
  1624. source=source,
  1625. user=user,
  1626. group=group,
  1627. mode=file_mode,
  1628. template=template,
  1629. makedirs=True,
  1630. context=context,
  1631. defaults=defaults,
  1632. backup=backup,
  1633. **pass_kwargs)
  1634. merge_ret(path, _ret)
  1635. def manage_directory(path):
  1636. if os.path.basename(path) == '..':
  1637. return
  1638. if clean and os.path.exists(path) and not os.path.isdir(path):
  1639. _ret = {'name': name, 'changes': {}, 'result': True, 'comment': ''}
  1640. if __opts__['test']:
  1641. _ret['comment'] = 'Replacing {0} with a directory'.format(path)
  1642. _ret['result'] = None
  1643. merge_ret(path, _ret)
  1644. return
  1645. else:
  1646. os.remove(path)
  1647. _ret['changes'] = {'diff': 'Replaced file with a directory'}
  1648. merge_ret(path, _ret)
  1649. _ret = directory(
  1650. path,
  1651. user=user,
  1652. group=group,
  1653. recurse=[],
  1654. dir_mode=dir_mode,
  1655. file_mode=None,
  1656. makedirs=True,
  1657. clean=False,
  1658. require=None)
  1659. merge_ret(path, _ret)
  1660. # Process symlinks and return the updated filenames list
  1661. def process_symlinks(filenames, symlinks):
  1662. for lname, ltarget in symlinks.items():
  1663. if not salt.utils.check_include_exclude(
  1664. os.path.relpath(lname, srcpath), include_pat, exclude_pat):
  1665. continue
  1666. srelpath = os.path.relpath(lname, srcpath)
  1667. # Check for max depth
  1668. if maxdepth is not None:
  1669. srelpieces = srelpath.split('/')
  1670. if not srelpieces[-1]:
  1671. srelpieces = srelpieces[:-1]
  1672. if len(srelpieces) > maxdepth + 1:
  1673. continue
  1674. # Check for all paths that begin with the symlink
  1675. # and axe it leaving only the dirs/files below it.
  1676. # This needs to use list() otherwise they reference
  1677. # the same list.
  1678. _filenames = list(filenames)
  1679. for filename in _filenames:
  1680. if filename.startswith(lname):
  1681. log.debug('** skipping file ** {0}, it intersects a '
  1682. 'symlink'.format(filename))
  1683. filenames.remove(filename)
  1684. # Create the symlink along with the necessary dirs.
  1685. # The dir perms/ownership will be adjusted later
  1686. # if needed
  1687. _ret = symlink(os.path.join(name, srelpath),
  1688. ltarget,
  1689. makedirs=True,
  1690. force=force_symlinks,
  1691. user=user,
  1692. group=group,
  1693. mode=sym_mode)
  1694. if not _ret:
  1695. continue
  1696. merge_ret(os.path.join(name, srelpath), _ret)
  1697. # Add the path to the keep set in case clean is set to True
  1698. keep.add(os.path.join(name, srelpath))
  1699. vdir.update(keep)
  1700. return filenames
  1701. keep = set()
  1702. vdir = set()
  1703. srcpath = source[7:]
  1704. if not srcpath.endswith('/'):
  1705. #we're searching for things that start with this *directory*.
  1706. # use '/' since #master only runs on POSIX
  1707. srcpath = srcpath + '/'
  1708. fns_ = __salt__['cp.list_master'](__env__, srcpath)
  1709. # If we are instructed to keep symlinks, then process them.
  1710. if keep_symlinks:
  1711. # Make this global so that emptydirs can use it if needed.
  1712. symlinks = __salt__['cp.list_master_symlinks'](__env__, srcpath)
  1713. fns_ = process_symlinks(fns_, symlinks)
  1714. for fn_ in fns_:
  1715. if not fn_.strip():
  1716. continue
  1717. # fn_ here is the absolute (from file_roots) source path of
  1718. # the file to copy from; it is either a normal file or an
  1719. # empty dir(if include_empty==true).
  1720. relname = os.path.relpath(fn_, srcpath)
  1721. if relname.startswith('..'):
  1722. continue
  1723. # Check for maxdepth of the relative path
  1724. if maxdepth is not None:
  1725. # Since paths are all master, just use POSIX separator
  1726. relpieces = relname.split('/')
  1727. # Handle empty directories (include_empty==true) by removing the
  1728. # the last piece if it is an empty string
  1729. if not relpieces[-1]:
  1730. relpieces = relpieces[:-1]
  1731. if len(relpieces) > maxdepth + 1:
  1732. continue
  1733. #- Check if it is to be excluded. Match only part of the path
  1734. # relative to the target directory
  1735. if not salt.utils.check_include_exclude(
  1736. relname, include_pat, exclude_pat):
  1737. continue
  1738. dest = os.path.join(name, relname)
  1739. dirname = os.path.dirname(dest)
  1740. keep.add(dest)
  1741. if dirname not in vdir:
  1742. # verify the directory perms if they are set
  1743. manage_directory(dirname)
  1744. vdir.add(dirname)
  1745. src = 'salt://{0}'.format(fn_)
  1746. manage_file(dest, src)
  1747. if include_empty:
  1748. mdirs = __salt__['cp.list_master_dirs'](__env__, srcpath)
  1749. for mdir in mdirs:
  1750. if not salt.utils.check_include_exclude(
  1751. os.path.relpath(mdir, srcpath), include_pat, exclude_pat):
  1752. continue
  1753. mdest = os.path.join(name, os.path.relpath(mdir, srcpath))
  1754. # Check for symlinks that happen to point to an empty dir.
  1755. if keep_symlinks:
  1756. islink = False
  1757. for link in symlinks.keys():
  1758. if mdir.startswith(link, 0):
  1759. log.debug('** skipping empty dir ** {0}, it intersects'
  1760. ' a symlink'.format(mdir))
  1761. islink = True
  1762. break
  1763. if islink:
  1764. continue
  1765. manage_directory(mdest)
  1766. keep.add(mdest)
  1767. keep = list(keep)
  1768. if clean:
  1769. # TODO: Use directory(clean=True) instead
  1770. keep += _gen_keep_files(name, require)
  1771. removed = _clean_dir(name, list(keep), exclude_pat)
  1772. if removed:
  1773. if __opts__['test']:
  1774. if ret['result']:
  1775. ret['result'] = None
  1776. add_comment('removed', removed)
  1777. else:
  1778. ret['changes']['removed'] = removed
  1779. # Flatten comments until salt command line client learns
  1780. # to display structured comments in a readable fashion
  1781. ret['comment'] = '\n'.join('\n#### {0} ####\n{1}'.format(
  1782. k, v if isinstance(v, string_types) else '\n'.join(v)
  1783. ) for (k, v) in ret['comment'].iteritems()).strip()
  1784. if not ret['comment']:
  1785. ret['comment'] = 'Recursively updated {0}'.format(name)
  1786. if not ret['changes'] and ret['result']:
  1787. ret['comment'] = 'The directory {0} is in the correct state'.format(
  1788. name
  1789. )
  1790. return ret
  1791. def replace(name,
  1792. pattern,
  1793. repl,
  1794. count=0,
  1795. flags=0,
  1796. bufsize=1,
  1797. append_if_not_found=False,
  1798. prepend_if_not_found=False,
  1799. not_found_content=None,
  1800. backup='.bak',
  1801. show_changes=True):
  1802. '''
  1803. Maintain an edit in a file
  1804. .. versionadded:: 0.17.0
  1805. Params are identical to :py:func:`~salt.modules.file.replace`.
  1806. '''
  1807. ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
  1808. check_res, check_msg = _check_file(name)
  1809. if not check_res:
  1810. return _error(ret, check_msg)
  1811. changes = __salt__['file.replace'](name,
  1812. pattern,
  1813. repl,
  1814. count=count,
  1815. flags=flags,
  1816. bufsize=bufsize,
  1817. append_if_not_found=append_if_not_found,
  1818. prepend_if_not_found=prepend_if_not_found,
  1819. not_found_content=not_found_content,
  1820. backup=backup,
  1821. dry_run=__opts__['test'],
  1822. show_changes=show_changes)
  1823. if changes:
  1824. ret['changes'] = {'diff': changes}
  1825. ret['comment'] = ('Changes were made'
  1826. if not __opts__['test'] else 'Changes would have been made')
  1827. else:
  1828. ret['comment'] = 'No changes were made'
  1829. ret['result'] = True if not __opts__['test'] else None
  1830. return ret
  1831. def blockreplace(
  1832. name,
  1833. marker_start='#-- start managed zone --',
  1834. marker_end='#-- end managed zone --',
  1835. content='',
  1836. append_if_not_found=False,
  1837. prepend_if_not_found=False,
  1838. backup='.bak',
  1839. show_changes=True):
  1840. '''
  1841. Maintain an edit in a file in a zone delimited by two line markers
  1842. .. versionadded:: 2014.1.0
  1843. A block of content delimited by comments can help you manage several lines
  1844. entries without worrying about old entries removal. This can help you
  1845. maintaining an un-managed file containing manual edits.
  1846. Note: this function will store two copies of the file in-memory
  1847. (the original version and the edited version) in order to detect changes
  1848. and only edit the targeted file if necessary.
  1849. :param name: Filesystem path to the file to be edited
  1850. :param marker_start: The line content identifying a line as the start of
  1851. the content block. Note that the whole line containing this marker will
  1852. be considered, so whitespaces or extra content before or after the
  1853. marker is included in final output
  1854. :param marker_end: The line content identifying a line as the end of
  1855. the content block. Note that the whole line containing this marker will
  1856. be considered, so whitespaces or extra content before or after the
  1857. marker is included in final output.
  1858. Note: you can use file.accumulated and target this state. All
  1859. accumulated data dictionaries content will be added as new lines in the
  1860. content.
  1861. :param content: The content to be used between the two lines identified by
  1862. marker_start and marker_stop.
  1863. :param append_if_not_found: False by default, if markers are not found and
  1864. set to True then the markers and content will be appended to the file
  1865. :param prepend_if_not_found: False by default, if markers are not found and
  1866. set to True then the markers and content will be prepended to the file
  1867. :param backup: The file extension to use for a backup of the file if any
  1868. edit is made. Set to ``False`` to skip making a backup.
  1869. :param dry_run: Don't make any edits to the file
  1870. :param show_changes: Output a unified diff of the old file and the new
  1871. file. If ``False`` return a boolean if any changes were made.
  1872. :rtype: bool or str
  1873. Example of usage with an accumulator and with a variable::
  1874. {% set myvar = 42 %}
  1875. hosts-config-block-{{ myvar }}:
  1876. file.blockreplace:
  1877. - name: /etc/hosts
  1878. - marker_start: "# START managed zone {{ myvar }} -DO-NOT-EDIT-"
  1879. - marker_end: "# END managed zone {{ myvar }} --"
  1880. - content: 'First line of content'
  1881. - append_if_not_found: True
  1882. - backup: '.bak'
  1883. - show_changes: True
  1884. hosts-config-block-{{ myvar }}-accumulated1:
  1885. file.accumulated:
  1886. - filename: /etc/hosts
  1887. - name: my-accumulator-{{ myvar }}
  1888. - text: "text 2"
  1889. - require_in:
  1890. - file: hosts-config-block-{{ myvar }}
  1891. hosts-config-block-{{ myvar }}-accumulated2:
  1892. file.accumulated:
  1893. - filename: /etc/hosts
  1894. - name: my-accumulator-{{ myvar }}
  1895. - text: |
  1896. text 3
  1897. text 4
  1898. - require_in:
  1899. - file: hosts-config-block-{{ myvar }}
  1900. will generate and maintain a block of content in ``/etc/hosts``::
  1901. # START managed zone 42 -DO-NOT-EDIT-
  1902. First line of content
  1903. text 2
  1904. text 3
  1905. text 4
  1906. # END managed zone 42 --
  1907. '''
  1908. ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
  1909. check_res, check_msg = _check_file(name)
  1910. if not check_res:
  1911. return _error(ret, check_msg)
  1912. if name in _ACCUMULATORS:
  1913. accumulator = _ACCUMULATORS[name]
  1914. # if we have multiple accumulators for a file, only apply the one
  1915. # required at a time
  1916. deps = _ACCUMULATORS_DEPS.get(name, [])
  1917. filtered = [a for a in deps if
  1918. __low__['__id__'] in deps[a] and a in accumulator]
  1919. if not filtered:
  1920. filtered = [a for a in accumulator]
  1921. for acc in filtered:
  1922. acc_content = accumulator[acc]
  1923. for line in acc_content:
  1924. if content == '':
  1925. content = line
  1926. else:
  1927. content += "\n" + line
  1928. changes = __salt__['file.blockreplace'](
  1929. name,
  1930. marker_start,
  1931. marker_end,
  1932. content=content,
  1933. append_if_not_found=append_if_not_found,
  1934. prepend_if_not_found=prepend_if_not_found,
  1935. backup=backup,
  1936. dry_run=__opts__['test'],
  1937. show_changes=show_changes
  1938. )
  1939. if changes:
  1940. ret['changes'] = {'diff': changes}
  1941. if __opts__['test']:
  1942. ret['result'] = None
  1943. ret['comment'] = 'Changes would be made'
  1944. else:
  1945. ret['result'] = True
  1946. ret['comment'] = 'Changes were made'
  1947. else:
  1948. ret['result'] = True
  1949. ret['comment'] = 'No changes needed to be made'
  1950. return ret
  1951. def sed(name,
  1952. before,
  1953. after,
  1954. limit='',
  1955. backup='.bak',
  1956. options='-r -e',
  1957. flags='g',
  1958. negate_match=False):
  1959. '''
  1960. .. deprecated:: 0.17.0
  1961. Use :py:func:`~salt.states.file.replace` instead.
  1962. Maintain a simple edit to a file
  1963. The file will be searched for the ``before`` pattern before making the
  1964. edit. In general the ``limit`` pattern should be as specific as possible
  1965. and ``before`` and ``after`` should contain the minimal text to be changed.
  1966. before
  1967. A pattern that should exist in the file before the edit.
  1968. after
  1969. A pattern that should exist in the file after the edit.
  1970. limit
  1971. An optional second pattern that can limit the scope of the before
  1972. pattern.
  1973. backup : '.bak'
  1974. The extension for the backed-up version of the file before the edit. If
  1975. no backups is desired, pass in the empty string: ''
  1976. options : ``-r -e``
  1977. Any options to pass to the ``sed`` command. ``-r`` uses extended
  1978. regular expression syntax and ``-e`` denotes that what follows is an
  1979. expression that sed will execute.
  1980. flags : ``g``
  1981. Any flags to append to the sed expression. ``g`` specifies the edit
  1982. should be made globally (and not stop after the first replacement).
  1983. negate_match : False
  1984. Negate the search command (``!``)
  1985. .. versionadded:: 0.17.0
  1986. Usage::
  1987. # Disable the epel repo by default
  1988. /etc/yum.repos.d/epel.repo:
  1989. file.sed:
  1990. - before: 1
  1991. - after: 0
  1992. - limit: ^enabled=
  1993. # Remove ldap from nsswitch
  1994. /etc/nsswitch.conf:
  1995. file.sed:
  1996. - before: 'ldap'
  1997. - after: ''
  1998. - limit: '^passwd:'
  1999. .. versionadded:: 0.9.5
  2000. '''
  2001. ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
  2002. check_res, check_msg = _check_file(name)
  2003. if not check_res:
  2004. return _error(ret, check_msg)
  2005. # Mandate that before and after are strings
  2006. before = str(before)
  2007. after = str(after)
  2008. # Look for the pattern before attempting the edit
  2009. if not __salt__['file.sed_contains'](name,
  2010. before,
  2011. limit=limit,
  2012. flags=flags):
  2013. # Pattern not found; don't try to guess why, just tell the user there
  2014. # were no changes made, as the changes should only be made once anyway.
  2015. # This makes it so users can use backreferences without the state
  2016. # coming back as failed all the time.
  2017. ret['comment'] = '"before" pattern not found, no changes made'
  2018. ret['result'] = True
  2019. return ret
  2020. if __opts__['test']:
  2021. ret['comment'] = 'File {0} is set to be updated'.format(name)
  2022. ret['result'] = None
  2023. return ret
  2024. with salt.utils.fopen(name, 'rb') as fp_:
  2025. slines = fp_.readlines()
  2026. # should be ok now; perform the edit
  2027. retcode = __salt__['file.sed'](path=name,
  2028. before=before,
  2029. after=after,
  2030. limit=limit,
  2031. backup=backup,
  2032. options=options,
  2033. flags=flags,
  2034. negate_match=negate_match)['retcode']
  2035. if retcode != 0:
  2036. ret['result'] = False
  2037. ret['comment'] = ('There was an error running sed. '
  2038. 'Return code {0}').format(retcode)
  2039. return ret
  2040. with salt.utils.fopen(name, 'rb') as fp_:
  2041. nlines = fp_.readlines()
  2042. if slines != nlines:
  2043. if not salt.utils.istextfile(name):
  2044. ret['changes']['diff'] = 'Replace binary file'
  2045. else:
  2046. # Changes happened, add them
  2047. ret['changes']['diff'] = ''.join(difflib.unified_diff(slines,
  2048. nlines))
  2049. # Don't check the result -- sed is not designed to be able to check
  2050. # the result, because of backreferences and so forth. Just report
  2051. # that sed was run, and assume it was successful (no error!)
  2052. ret['result'] = True
  2053. ret['comment'] = 'sed ran without error'
  2054. else:
  2055. ret['result'] = True
  2056. ret['comment'] = 'sed ran without error, but no changes were made'
  2057. return ret
  2058. def comment(name, regex, char='#', backup='.bak'):
  2059. '''
  2060. Comment out specified lines in a file.
  2061. name
  2062. The full path to the file to be edited
  2063. regex
  2064. A regular expression used to find the lines that are to be commented;
  2065. this pattern will be wrapped in parenthesis and will move any
  2066. preceding/trailing ``^`` or ``$`` characters outside the parenthesis
  2067. (e.g., the pattern ``^foo$`` will be rewritten as ``^(foo)$``)
  2068. Note that you _need_ the leading ^, otherwise each time you run
  2069. highstate, another comment char will be inserted.
  2070. char : ``#``
  2071. The character to be inserted at the beginning of a line in order to
  2072. comment it out
  2073. backup : ``.bak``
  2074. The file will be backed up before edit with this file extension
  2075. .. warning::
  2076. This backup will be overwritten each time ``sed`` / ``comment`` /
  2077. ``uncomment`` is called. Meaning the backup will only be useful
  2078. after the first invocation.
  2079. Usage::
  2080. /etc/fstab:
  2081. file.comment:
  2082. - regex: ^bind 127.0.0.1
  2083. .. versionadded:: 0.9.5
  2084. '''
  2085. ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
  2086. check_res, check_msg = _check_file(name)
  2087. if not check_res:
  2088. return _error(ret, check_msg)
  2089. unanchor_regex = regex.lstrip('^').rstrip('$')
  2090. # Make sure the pattern appears in the file before continuing
  2091. if not __salt__['file.contains_regex_multiline'](name, regex):
  2092. if __salt__['file.contains_regex_multiline'](name, unanchor_regex):
  2093. ret['comment'] = 'Pattern already commented'
  2094. ret['result'] = True
  2095. return ret
  2096. else:
  2097. return _error(ret, '{0}: Pattern not found'.format(unanchor_regex))
  2098. if __opts__['test']:
  2099. ret['comment'] = 'File {0} is set to be updated'.format(name)
  2100. ret['result'] = None
  2101. return ret
  2102. with salt.utils.fopen(name, 'rb') as fp_:
  2103. slines = fp_.readlines()
  2104. # Perform the edit
  2105. __salt__['file.comment'](name, regex, char, backup)
  2106. with salt.utils.fopen(name, 'rb') as fp_:
  2107. nlines = fp_.readlines()
  2108. # Check the result
  2109. ret['result'] = __salt__['file.contains_regex_multiline'](name,
  2110. unanchor_regex)
  2111. if slines != nlines:
  2112. if not salt.utils.istextfile(name):
  2113. ret['changes']['diff'] = 'Replace binary file'
  2114. else:
  2115. # Changes happened, add them
  2116. ret['changes']['diff'] = (
  2117. ''.join(difflib.unified_diff(slines, nlines))
  2118. )
  2119. if ret['result']:
  2120. ret['comment'] = 'Commented lines successfully'
  2121. else:
  2122. ret['comment'] = 'Expected commented lines not found'
  2123. return ret
  2124. def uncomment(name, regex, char='#', backup='.bak'):
  2125. '''
  2126. Uncomment specified commented lines in a file
  2127. name
  2128. The full path to the file to be edited
  2129. regex
  2130. A regular expression used to find the lines that are to be uncommented.
  2131. This regex should not include the comment character. A leading ``^``
  2132. character will be stripped for convenience (for easily switching
  2133. between comment() and uncomment()). The regex will be searched for
  2134. from the beginning of the line, ignoring leading spaces (we prepend
  2135. '^[ \\t]*')
  2136. char : ``#``
  2137. The character to remove in order to uncomment a line
  2138. backup : ``.bak``
  2139. The file will be backed up before edit with this file extension;
  2140. **WARNING:** each time ``sed``/``comment``/``uncomment`` is called will
  2141. overwrite this backup
  2142. Usage::
  2143. /etc/adduser.conf:
  2144. file.uncomment:
  2145. - regex: EXTRA_GROUPS
  2146. .. versionadded:: 0.9.5
  2147. '''
  2148. ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
  2149. check_res, check_msg = _check_file(name)
  2150. if not check_res:
  2151. return _error(ret, check_msg)
  2152. # Make sure the pattern appears in the file
  2153. if __salt__['file.contains_regex_multiline'](
  2154. name, '^[ \t]*{0}'.format(regex.lstrip('^'))):
  2155. ret['comment'] = 'Pattern already uncommented'
  2156. ret['result'] = True
  2157. return ret
  2158. elif __salt__['file.contains_regex_multiline'](
  2159. name, '{0}[ \t]*{1}'.format(char, regex.lstrip('^'))):
  2160. # Line exists and is commented
  2161. pass
  2162. else:
  2163. return _error(ret, '{0}: Pattern not found'.format(regex))
  2164. if __opts__['test']:
  2165. ret['comment'] = 'File {0} is set to be updated'.format(name)
  2166. ret['result'] = None
  2167. return ret
  2168. with salt.utils.fopen(name, 'rb') as fp_:
  2169. slines = fp_.readlines()
  2170. # Perform the edit
  2171. __salt__['file.uncomment'](name, regex, char, backup)
  2172. with salt.utils.fopen(name, 'rb') as fp_:
  2173. nlines = fp_.readlines()
  2174. # Check the result
  2175. ret['result'] = __salt__['file.contains_regex_multiline'](
  2176. name, '^[ \t]*{0}'.format(regex.lstrip('^'))
  2177. )
  2178. if slines != nlines:
  2179. if not salt.utils.istextfile(name):
  2180. ret['changes']['diff'] = 'Replace binary file'
  2181. else:
  2182. # Changes happened, add them
  2183. ret['changes']['diff'] = (
  2184. ''.join(difflib.unified_diff(slines, nlines))
  2185. )
  2186. if ret['result']:
  2187. ret['comment'] = 'Uncommented lines successfully'
  2188. else:
  2189. ret['comment'] = 'Expected uncommented lines not found'
  2190. return ret
  2191. def append(name,
  2192. text=None,
  2193. makedirs=False,
  2194. source=None,
  2195. source_hash=None,
  2196. template='jinja',
  2197. sources=None,
  2198. source_hashes=None,
  2199. defaults=None,
  2200. context=None):
  2201. '''
  2202. Ensure that some text appears at the end of a file.
  2203. The text will not be appended if it already exists in the file.
  2204. A single string of text or a list of strings may be appended.
  2205. name
  2206. The location of the file to append to.
  2207. text
  2208. The text to be appended, which can be a single string or a list
  2209. of strings.
  2210. makedirs
  2211. If the file is located in a path without a parent directory,
  2212. then the state will fail. If makedirs is set to True, then
  2213. the parent directories will be created to facilitate the
  2214. creation of the named file. Defaults to False.
  2215. source
  2216. A single source file to append. This source file can be hosted on either
  2217. the salt master server, or on an HTTP or FTP server. Both HTTPS and
  2218. HTTP are supported as well as downloading directly from Amazon S3
  2219. compatible URLs with both pre-configured and automatic IAM credentials
  2220. (see s3.get state documentation). File retrieval from Openstack Swift
  2221. object storage is supported via swift://container/object_path URLs
  2222. (see swift.get documentation).
  2223. For files hosted on the salt file server, if the file is located on
  2224. the master in the directory named spam, and is called eggs, the source
  2225. string is salt://spam/eggs.
  2226. If the file is hosted on an HTTP or FTP server, the source_hash argument
  2227. is also required.
  2228. source_hash
  2229. This can be one of the following:
  2230. 1. a source hash string
  2231. 2. the URI of a file that contains source hash strings
  2232. The function accepts the first encountered long unbroken alphanumeric
  2233. string of correct length as a valid hash, in order from most secure to
  2234. least secure::
  2235. Type Length
  2236. ====== ======
  2237. sha512 128
  2238. sha384 96
  2239. sha256 64
  2240. sha224 56
  2241. sha1 40
  2242. md5 32
  2243. The file can contain several checksums for several files. Each line
  2244. must contain both the file name and the hash. If no file name is
  2245. matched, the first hash encountered will be used, otherwise the most
  2246. secure hash with the correct source file name will be used.
  2247. Debian file type ``*.dsc`` is supported.
  2248. Examples::
  2249. /etc/rc.conf ef6e82e4006dee563d98ada2a2a80a27
  2250. sha254c8525aee419eb649f0233be91c151178b30f0dff8ebbdcc8de71b1d5c8bcc06a /etc/resolv.conf
  2251. ead48423703509d37c4a90e6a0d53e143b6fc268
  2252. Known issues:
  2253. If the remote server URL has the hash file as an apparent
  2254. sub-directory of the source file, the module will discover that it
  2255. has already cached a directory where a file should be cached. For
  2256. example:
  2257. .. code-block:: yaml
  2258. tomdroid-src-0.7.3.tar.gz:
  2259. file.managed:
  2260. - name: /tmp/tomdroid-src-0.7.3.tar.gz
  2261. - source: https://launchpad.net/tomdroid/beta/0.7.3/+download/tomdroid-src-0.7.3.tar.gz
  2262. - source_hash: https://launchpad.net/tomdroid/beta/0.7.3/+download/tomdroid-src-0.7.3.tar.gz/+md5
  2263. template : ``jinja``
  2264. The named templating engine will be used to render the appended-to
  2265. file. Defaults to jinja.
  2266. sources
  2267. A list of source files to append. If the files are hosted on an HTTP or
  2268. FTP server, the source_hashes argument is also required.
  2269. source_hashes
  2270. A list of source_hashes corresponding to the sources list specified in
  2271. the sources argument.
  2272. defaults
  2273. Default context passed to the template.
  2274. context
  2275. Overrides default context variables passed to the template.
  2276. Multi-line example::
  2277. /etc/motd:
  2278. file.append:
  2279. - text: |
  2280. Thou hadst better eat salt with the Philosophers of Greece,
  2281. than sugar with the Courtiers of Italy.
  2282. - Benjamin Franklin
  2283. Multiple lines of text::
  2284. /etc/motd:
  2285. file.append:
  2286. - text:
  2287. - Trust no one unless you have eaten much salt with him.
  2288. - "Salt is born of the purest of parents: the sun and the sea."
  2289. Gather text from multiple template files::
  2290. /etc/motd:
  2291. file:
  2292. - append
  2293. - template: jinja
  2294. - sources:
  2295. - salt://motd/devops-messages.tmpl
  2296. - salt://motd/hr-messages.tmpl
  2297. - salt://motd/general-messages.tmpl
  2298. .. versionadded:: 0.9.5
  2299. '''
  2300. ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
  2301. if sources is None:
  2302. sources = []
  2303. if source_hashes is None:
  2304. source_hashes = []
  2305. # Add sources and source_hashes with template support
  2306. # NOTE: FIX 'text' and any 'source' are mutually exclusive as 'text'
  2307. # is re-assigned in the original code.
  2308. (ok_, err, sl_) = _unify_sources_and_hashes(source=source,
  2309. source_hash=source_hash,
  2310. sources=sources,
  2311. source_hashes=source_hashes)
  2312. if not ok_:
  2313. return _error(ret, err)
  2314. if makedirs is True:
  2315. dirname = os.path.dirname(name)
  2316. if not __salt__['file.directory_exists'](dirname):
  2317. __salt__['file.makedirs'](name)
  2318. check_res, check_msg = _check_directory(
  2319. dirname, None, None, False, None, False, False, None
  2320. )
  2321. if not check_res:
  2322. return _error(ret, check_msg)
  2323. # Make sure that we have a file
  2324. __salt__['file.touch'](name)
  2325. check_res, check_msg = _check_file(name)
  2326. if not check_res:
  2327. touch(name, makedirs=makedirs)
  2328. retry_res, retry_msg = _check_file(name)
  2329. if not retry_res:
  2330. return _error(ret, check_msg)
  2331. #Follow the original logic and re-assign 'text' if using source(s)...
  2332. if sl_:
  2333. tmpret = _get_template_texts(source_list=sl_,
  2334. template=template,
  2335. defaults=defaults,
  2336. context=context)
  2337. if not tmpret['result']:
  2338. return tmpret
  2339. text = tmpret['data']
  2340. for index, item in enumerate(text):
  2341. if isinstance(item, integer_types):
  2342. text[index] = str(item)
  2343. if isinstance(text, string_types):
  2344. text = (text,)
  2345. with salt.utils.fopen(name, 'rb') as fp_:
  2346. slines = fp_.readlines()
  2347. count = 0
  2348. test_lines = []
  2349. try:
  2350. for chunk in text:
  2351. if __salt__['file.contains_regex_multiline'](
  2352. name, salt.utils.build_whitespace_split_regex(chunk)):
  2353. continue
  2354. try:
  2355. lines = chunk.splitlines()
  2356. except AttributeError:
  2357. log.debug(
  2358. 'Error appending text to {0}; given object is: {1}'.format(
  2359. name, type(chunk)
  2360. )
  2361. )
  2362. return _error(ret, 'Given text is not a string')
  2363. for line in lines:
  2364. if __opts__['test']:
  2365. ret['comment'] = 'File {0} is set to be updated'.format(name)
  2366. ret['result'] = None
  2367. test_lines.append('{0}\n'.format(line))
  2368. else:
  2369. __salt__['file.append'](name, line)
  2370. count += 1
  2371. except TypeError:
  2372. ret['comment'] = 'No text found to append. Nothing appended'
  2373. ret['result'] = False
  2374. return ret
  2375. if __opts__['test']:
  2376. nlines = slines + test_lines
  2377. ret['result'] = None
  2378. if slines != nlines:
  2379. if not salt.utils.istextfile(name):
  2380. ret['changes']['diff'] = 'Replace binary file'
  2381. else:
  2382. # Changes happened, add them
  2383. ret['changes']['diff'] = (
  2384. ''.join(difflib.unified_diff(slines, nlines))
  2385. )
  2386. else:
  2387. ret['comment'] = 'File {0} is in correct state'.format(name)
  2388. return ret
  2389. with salt.utils.fopen(name, 'rb') as fp_:
  2390. nlines = fp_.readlines()
  2391. if slines != nlines:
  2392. if not salt.utils.istextfile(name):
  2393. ret['changes']['diff'] = 'Replace binary file'
  2394. else:
  2395. # Changes happened, add them
  2396. ret['changes']['diff'] = (
  2397. ''.join(difflib.unified_diff(slines, nlines))
  2398. )
  2399. if count:
  2400. ret['comment'] = 'Appended {0} lines'.format(count)
  2401. else:
  2402. ret['comment'] = 'File {0} is in correct state'.format(name)
  2403. ret['result'] = True
  2404. return ret
  2405. def prepend(name,
  2406. text=None,
  2407. makedirs=False,
  2408. source=None,
  2409. source_hash=None,
  2410. template='jinja',
  2411. sources=None,
  2412. source_hashes=None,
  2413. defaults=None,
  2414. context=None):
  2415. '''
  2416. Ensure that some text appears at the beginning of a file
  2417. The text will not be prepended again if it already exists in the file. You
  2418. may specify a single line of text or a list of lines to append.
  2419. Multi-line example::
  2420. /etc/motd:
  2421. file.prepend:
  2422. - text: |
  2423. Thou hadst better eat salt with the Philosophers of Greece,
  2424. than sugar with the Courtiers of Italy.
  2425. - Benjamin Franklin
  2426. Multiple lines of text::
  2427. /etc/motd:
  2428. file.prepend:
  2429. - text:
  2430. - Trust no one unless you have eaten much salt with him.
  2431. - "Salt is born of the purest of parents: the sun and the sea."
  2432. Gather text from multiple template files::
  2433. /etc/motd:
  2434. file:
  2435. - prepend
  2436. - template: jinja
  2437. - sources:
  2438. - salt://motd/devops-messages.tmpl
  2439. - salt://motd/hr-messages.tmpl
  2440. - salt://motd/general-messages.tmpl
  2441. .. versionadded:: Helium
  2442. '''
  2443. ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
  2444. if sources is None:
  2445. sources = []
  2446. if source_hashes is None:
  2447. source_hashes = []
  2448. # Add sources and source_hashes with template support
  2449. # NOTE: FIX 'text' and any 'source' are mutually exclusive as 'text'
  2450. # is re-assigned in the original code.
  2451. (ok_, err, sl_) = _unify_sources_and_hashes(source=source,
  2452. source_hash=source_hash,
  2453. sources=sources,
  2454. source_hashes=source_hashes)
  2455. if not ok_:
  2456. return _error(ret, err)
  2457. if makedirs is True:
  2458. dirname = os.path.dirname(name)
  2459. if not __salt__['file.directory_exists'](dirname):
  2460. __salt__['file.makedirs'](name)
  2461. check_res, check_msg = _check_directory(
  2462. dirname, None, None, False, None, False, False, None
  2463. )
  2464. if not check_res:
  2465. return _error(ret, check_msg)
  2466. # Make sure that we have a file
  2467. __salt__['file.touch'](name)
  2468. check_res, check_msg = _check_file(name)
  2469. if not check_res:
  2470. return _error(ret, check_msg)
  2471. #Follow the original logic and re-assign 'text' if using source(s)...
  2472. if sl_:
  2473. tmpret = _get_template_texts(source_list=sl_,
  2474. template=template,
  2475. defaults=defaults,
  2476. context=context)
  2477. if not tmpret['result']:
  2478. return tmpret
  2479. text = tmpret['data']
  2480. if isinstance(text, string_types):
  2481. text = (text,)
  2482. with salt.utils.fopen(name, 'rb') as fp_:
  2483. slines = fp_.readlines()
  2484. count = 0
  2485. test_lines = []
  2486. preface = []
  2487. for chunk in text:
  2488. if __salt__['file.contains_regex_multiline'](
  2489. name, salt.utils.build_whitespace_split_regex(chunk)):
  2490. continue
  2491. try:
  2492. lines = chunk.splitlines()
  2493. except AttributeError:
  2494. log.debug(
  2495. 'Error appending text to {0}; given object is: {1}'.format(
  2496. name, type(chunk)
  2497. )
  2498. )
  2499. return _error(ret, 'Given text is not a string')
  2500. for line in lines:
  2501. if __opts__['test']:
  2502. ret['comment'] = 'File {0} is set to be updated'.format(name)
  2503. ret['result'] = None
  2504. test_lines.append('{0}\n'.format(line))
  2505. else:
  2506. preface.append(line)
  2507. count += 1
  2508. if __opts__['test']:
  2509. nlines = test_lines + slines
  2510. ret['result'] = None
  2511. if slines != nlines:
  2512. if not salt.utils.istextfile(name):
  2513. ret['changes']['diff'] = 'Replace binary file'
  2514. else:
  2515. # Changes happened, add them
  2516. ret['changes']['diff'] = (
  2517. ''.join(difflib.unified_diff(slines, nlines))
  2518. )
  2519. else:
  2520. ret['comment'] = 'File {0} is in correct state'.format(name)
  2521. return ret
  2522. __salt__['file.prepend'](name, *preface)
  2523. with salt.utils.fopen(name, 'rb') as fp_:
  2524. nlines = fp_.readlines()
  2525. if slines != nlines:
  2526. if not salt.utils.istextfile(name):
  2527. ret['changes']['diff'] = 'Replace binary file'
  2528. else:
  2529. # Changes happened, add them
  2530. ret['changes']['diff'] = (
  2531. ''.join(difflib.unified_diff(slines, nlines))
  2532. )
  2533. if count:
  2534. ret['comment'] = 'Prepended {0} lines'.format(count)
  2535. else:
  2536. ret['comment'] = 'File {0} is in correct state'.format(name)
  2537. ret['result'] = True
  2538. return ret
  2539. def patch(name,
  2540. source=None,
  2541. hash=None,
  2542. options='',
  2543. dry_run_first=True,
  2544. env=None,
  2545. **kwargs):
  2546. '''
  2547. Apply a patch to a file. Note: a suitable ``patch`` executable must be
  2548. available on the minion when using this state function.
  2549. name
  2550. The file to with the patch will be applied.
  2551. source
  2552. The source patch to download to the minion, this source file must be
  2553. hosted on the salt master server. If the file is located in the
  2554. directory named spam, and is called eggs, the source string is
  2555. salt://spam/eggs. A source is required.
  2556. hash
  2557. Hash of the patched file. If the hash of the target file matches this
  2558. value then the patch is assumed to have been applied. The hash string
  2559. is the hash algorithm followed by the hash of the file:
  2560. md5=e138491e9d5b97023cea823fe17bac22
  2561. options
  2562. Extra options to pass to patch.
  2563. dry_run_first : ``True``
  2564. Run patch with ``--dry-run`` first to check if it will apply cleanly.
  2565. env
  2566. Specify the environment from which to retrieve the patch file indicated
  2567. by the ``source`` parameter. If not provided, this defaults to the
  2568. environment from which the state is being executed.
  2569. Usage::
  2570. # Equivalent to ``patch --forward /opt/file.txt file.patch``
  2571. /opt/file.txt:
  2572. file.patch:
  2573. - source: salt://file.patch
  2574. - hash: md5=e138491e9d5b97023cea823fe17bac22
  2575. '''
  2576. ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
  2577. check_res, check_msg = _check_file(name)
  2578. if not check_res:
  2579. return _error(ret, check_msg)
  2580. if not source:
  2581. return _error(ret, 'Source is required')
  2582. if hash is None:
  2583. return _error(ret, 'Hash is required')
  2584. if __salt__['file.check_hash'](name, hash):
  2585. ret.update(result=True, comment='Patch is already applied')
  2586. return ret
  2587. if isinstance(env, salt._compat.string_types):
  2588. msg = (
  2589. 'Passing a salt environment should be done using \'saltenv\' not '
  2590. '\'env\'. This warning will go away in Salt Boron and this '
  2591. 'will be the default and expected behaviour. Please update your '
  2592. 'state files.'
  2593. )
  2594. salt.utils.warn_until('Boron', msg)
  2595. ret.setdefault('warnings', []).append(msg)
  2596. # No need to set __env__ = env since that's done in the state machinery
  2597. # get cached file or copy it to cache
  2598. cached_source_path = __salt__['cp.cache_file'](source, __env__)
  2599. if not cached_source_path:
  2600. ret['comment'] = ('Unable to cache {0} from saltenv {1!r}'
  2601. .format(source, __env__))
  2602. return ret
  2603. log.debug(
  2604. 'State patch.applied cached source {0} -> {1}'.format(
  2605. source, cached_source_path
  2606. )
  2607. )
  2608. if dry_run_first or __opts__['test']:
  2609. ret['changes'] = __salt__['file.patch'](
  2610. name, cached_source_path, options=options, dry_run=True
  2611. )
  2612. if __opts__['test']:
  2613. ret['comment'] = 'File {0} will be patched'.format(name)
  2614. ret['result'] = None
  2615. return ret
  2616. if ret['changes']['retcode']:
  2617. return ret
  2618. ret['changes'] = __salt__['file.patch'](
  2619. name, cached_source_path, options=options
  2620. )
  2621. ret['result'] = not ret['changes']['retcode']
  2622. if ret['result'] and not __salt__['file.check_hash'](name, hash):
  2623. ret.update(
  2624. result=False,
  2625. comment='File {0} hash mismatch after patch was applied'.format(
  2626. name
  2627. )
  2628. )
  2629. return ret
  2630. def touch(name, atime=None, mtime=None, makedirs=False):
  2631. '''
  2632. Replicate the 'nix "touch" command to create a new empty
  2633. file or update the atime and mtime of an existing file.
  2634. Note that if you just want to create a file and don't care about atime or
  2635. mtime, you should use ``file.managed`` instead, as it is more
  2636. feature-complete. (Just leave out the ``source``/``template``/``contents``
  2637. arguments, and it will just create the file and/or check its permissions,
  2638. without messing with contents)
  2639. name
  2640. name of the file
  2641. atime
  2642. atime of the file
  2643. mtime
  2644. mtime of the file
  2645. makedirs
  2646. whether we should create the parent directory/directories in order to
  2647. touch the file
  2648. Usage::
  2649. /var/log/httpd/logrotate.empty:
  2650. file.touch
  2651. .. versionadded:: 0.9.5
  2652. '''
  2653. ret = {
  2654. 'name': name,
  2655. 'changes': {},
  2656. }
  2657. if not os.path.isabs(name):
  2658. return _error(
  2659. ret, 'Specified file {0} is not an absolute path'.format(name)
  2660. )
  2661. if __opts__['test']:
  2662. ret['result'], ret['comment'] = _check_touch(name, atime, mtime)
  2663. return ret
  2664. if makedirs:
  2665. __salt__['file.makedirs'](name)
  2666. if not os.path.isdir(os.path.dirname(name)):
  2667. return _error(
  2668. ret, 'Directory not present to touch file {0}'.format(name)
  2669. )
  2670. extant = os.path.exists(name)
  2671. ret['result'] = __salt__['file.touch'](name, atime, mtime)
  2672. if not extant and ret['result']:
  2673. ret['comment'] = 'Created empty file {0}'.format(name)
  2674. ret['changes']['new'] = name
  2675. elif extant and ret['result']:
  2676. ret['comment'] = 'Updated times on {0} {1}'.format(
  2677. 'directory' if os.path.isdir(name) else 'file', name
  2678. )
  2679. ret['changes']['touched'] = name
  2680. return ret
  2681. def copy(name, source, force=False, makedirs=False):
  2682. '''
  2683. If the source file exists on the system, copy it to the named file. The
  2684. named file will not be overwritten if it already exists unless the force
  2685. option is set to True.
  2686. name
  2687. The location of the file to copy to
  2688. source
  2689. The location of the file to copy to the location specified with name
  2690. force
  2691. If the target location is present then the file will not be moved,
  2692. specify "force: True" to overwrite the target file
  2693. makedirs
  2694. If the target subdirectories don't exist create them
  2695. '''
  2696. ret = {
  2697. 'name': name,
  2698. 'changes': {},
  2699. 'comment': '',
  2700. 'result': True}
  2701. if not os.path.isabs(name):
  2702. return _error(
  2703. ret, 'Specified file {0} is not an absolute path'.format(name))
  2704. if not os.path.exists(source):
  2705. return _error(ret, 'Source file "{0}" is not present'.format(source))
  2706. if os.path.lexists(source) and os.path.lexists(name):
  2707. if not force:
  2708. ret['comment'] = ('The target file "{0}" exists and will not be '
  2709. 'overwritten'.format(name))
  2710. ret['result'] = True
  2711. return ret
  2712. elif not __opts__['test']:
  2713. # Remove the destination to prevent problems later
  2714. try:
  2715. if os.path.islink(name):
  2716. os.unlink(name)
  2717. elif os.path.isfile(name):
  2718. os.remove(name)
  2719. else:
  2720. shutil.rmtree(name)
  2721. except (IOError, OSError):
  2722. return _error(
  2723. ret,
  2724. 'Failed to delete "{0}" in preparation for '
  2725. 'forced move'.format(name)
  2726. )
  2727. if __opts__['test']:
  2728. ret['comment'] = 'File "{0}" is set to be copied to "{1}"'.format(
  2729. source,
  2730. name
  2731. )
  2732. ret['result'] = None
  2733. return ret
  2734. # Run makedirs
  2735. dname = os.path.dirname(name)
  2736. if not os.path.isdir(dname):
  2737. if makedirs:
  2738. __salt__['file.makedirs'](dname)
  2739. else:
  2740. return _error(
  2741. ret,
  2742. 'The target directory {0} is not present'.format(dname))
  2743. # All tests pass, move the file into place
  2744. try:
  2745. shutil.copy(source, name)
  2746. except (IOError, OSError):
  2747. return _error(
  2748. ret, 'Failed to copy "{0}" to "{1}"'.format(source, name))
  2749. ret['comment'] = 'Copied "{0}" to "{1}"'.format(source, name)
  2750. ret['changes'] = {name: source}
  2751. return ret
  2752. def rename(name, source, force=False, makedirs=False):
  2753. '''
  2754. If the source file exists on the system, rename it to the named file. The
  2755. named file will not be overwritten if it already exists unless the force
  2756. option is set to True.
  2757. name
  2758. The location of the file to rename to
  2759. source
  2760. The location of the file to move to the location specified with name
  2761. force
  2762. If the target location is present then the file will not be moved,
  2763. specify "force: True" to overwrite the target file
  2764. makedirs
  2765. If the target subdirectories don't exist create them
  2766. '''
  2767. ret = {
  2768. 'name': name,
  2769. 'changes': {},
  2770. 'comment': '',
  2771. 'result': True}
  2772. if not os.path.isabs(name):
  2773. return _error(
  2774. ret, 'Specified file {0} is not an absolute path'.format(name))
  2775. if not os.path.lexists(source):
  2776. ret['comment'] = ('Source file "{0}" has already been moved out of '
  2777. 'place').format(source)
  2778. return ret
  2779. if os.path.lexists(source) and os.path.lexists(name):
  2780. if not force:
  2781. ret['comment'] = ('The target file "{0}" exists and will not be '
  2782. 'overwritten'.format(name))
  2783. ret['result'] = False
  2784. return ret
  2785. elif not __opts__['test']:
  2786. # Remove the destination to prevent problems later
  2787. try:
  2788. if os.path.islink(name):
  2789. os.unlink(name)
  2790. elif os.path.isfile(name):
  2791. os.remove(name)
  2792. else:
  2793. shutil.rmtree(name)
  2794. except (IOError, OSError):
  2795. return _error(
  2796. ret,
  2797. 'Failed to delete "{0}" in preparation for '
  2798. 'forced move'.format(name)
  2799. )
  2800. if __opts__['test']:
  2801. ret['comment'] = 'File "{0}" is set to be moved to "{1}"'.format(
  2802. source,
  2803. name
  2804. )
  2805. ret['result'] = None
  2806. return ret
  2807. # Run makedirs
  2808. dname = os.path.dirname(name)
  2809. if not os.path.isdir(dname):
  2810. if makedirs:
  2811. __salt__['file.makedirs'](dname)
  2812. else:
  2813. return _error(
  2814. ret,
  2815. 'The target directory {0} is not present'.format(dname))
  2816. # All tests pass, move the file into place
  2817. try:
  2818. if os.path.islink(source):
  2819. linkto = os.readlink(source)
  2820. os.symlink(linkto, name)
  2821. os.unlink(source)
  2822. else:
  2823. shutil.move(source, name)
  2824. except (IOError, OSError):
  2825. return _error(
  2826. ret, 'Failed to move "{0}" to "{1}"'.format(source, name))
  2827. ret['comment'] = 'Moved "{0}" to "{1}"'.format(source, name)
  2828. ret['changes'] = {name: source}
  2829. return ret
  2830. def accumulated(name, filename, text, **kwargs):
  2831. '''
  2832. Prepare accumulator which can be used in template in file.managed state.
  2833. Accumulator dictionary becomes available in template. It can also be used
  2834. in file.blockreplace.
  2835. name
  2836. Accumulator name
  2837. filename
  2838. Filename which would receive this accumulator (see file.managed state
  2839. documentation about ``name``)
  2840. text
  2841. String or list for adding in accumulator
  2842. require_in / watch_in
  2843. One of them required for sure we fill up accumulator before we manage
  2844. the file. Probably the same as filename
  2845. Example:
  2846. Given the following::
  2847. animals_doing_things:
  2848. file.accumulated:
  2849. - filename: /tmp/animal_file.txt
  2850. - text: ' jumps over the lazy dog.'
  2851. - require_in:
  2852. - file: animal_file
  2853. animal_file:
  2854. file.managed:
  2855. - name: /tmp/animal_file.txt
  2856. - source: salt://animal_file.txt
  2857. - template: jinja
  2858. One might write a template for animal_file.txt like the following::
  2859. The quick brown fox{% for animal in accumulator['animals_doing_things'] %}{{ animal }}{% endfor %}
  2860. Collectively, the above states and template file will produce::
  2861. The quick brown fox jumps over the lazy dog.
  2862. Multiple accumulators can be "chained" together.
  2863. .. note::
  2864. The 'accumulator' data structure is a Python dictionary.
  2865. Do not expect any loop over the keys in a deterministic order!
  2866. '''
  2867. ret = {
  2868. 'name': name,
  2869. 'changes': {},
  2870. 'result': True,
  2871. 'comment': ''
  2872. }
  2873. require_in = __low__.get('require_in', [])
  2874. watch_in = __low__.get('watch_in', [])
  2875. deps = require_in + watch_in
  2876. if not filter(lambda x: 'file' in x, deps):
  2877. ret['result'] = False
  2878. ret['comment'] = 'Orphaned accumulator {0} in {1}:{2}'.format(
  2879. name,
  2880. __low__['__sls__'],
  2881. __low__['__id__']
  2882. )
  2883. return ret
  2884. if isinstance(text, string_types):
  2885. text = (text,)
  2886. if filename not in _ACCUMULATORS:
  2887. _ACCUMULATORS[filename] = {}
  2888. if filename not in _ACCUMULATORS_DEPS:
  2889. _ACCUMULATORS_DEPS[filename] = {}
  2890. if name not in _ACCUMULATORS_DEPS[filename]:
  2891. _ACCUMULATORS_DEPS[filename][name] = []
  2892. for accumulator in deps:
  2893. _ACCUMULATORS_DEPS[filename][name].extend(accumulator.values())
  2894. if name not in _ACCUMULATORS[filename]:
  2895. _ACCUMULATORS[filename][name] = []
  2896. for chunk in text:
  2897. if chunk not in _ACCUMULATORS[filename][name]:
  2898. _ACCUMULATORS[filename][name].append(chunk)
  2899. ret['comment'] = ('Accumulator {0} for file {1} '
  2900. 'was charged by text'.format(name, filename))
  2901. return ret
  2902. def _merge_dict(obj, k, v):
  2903. changes = {}
  2904. if k in obj:
  2905. if type(obj[k]) is list:
  2906. if type(v) is list:
  2907. for a in v:
  2908. if a not in obj[k]:
  2909. changes[k] = a
  2910. obj[k].append(a)
  2911. else:
  2912. if obj[k] != v:
  2913. changes[k] = v
  2914. obj[k] = v
  2915. elif type(obj[k]) is dict:
  2916. if type(v) is dict:
  2917. for a, b in v.iteritems():
  2918. if (type(b) is dict) or (type(b) is list):
  2919. updates = _merge_dict(obj[k], a, b)
  2920. for x, y in updates.iteritems():
  2921. changes[k + "." + x] = y
  2922. else:
  2923. if obj[k][a] != b:
  2924. changes[k + "." + a] = b
  2925. obj[k][a] = b
  2926. else:
  2927. if obj[k] != v:
  2928. changes[k] = v
  2929. obj[k] = v
  2930. else:
  2931. if obj[k] != v:
  2932. changes[k] = v
  2933. obj[k] = v
  2934. else:
  2935. changes[k] = v
  2936. obj[k] = v
  2937. return changes
  2938. def serialize(name,
  2939. dataset,
  2940. user=None,
  2941. group=None,
  2942. mode=None,
  2943. env=None,
  2944. backup='',
  2945. makedirs=False,
  2946. show_diff=True,
  2947. create=True,
  2948. merge_if_exists=False,
  2949. **kwargs):
  2950. '''
  2951. Serializes dataset and store it into managed file. Useful for sharing
  2952. simple configuration files.
  2953. name
  2954. The location of the file to create
  2955. dataset
  2956. the dataset that will be serialized
  2957. formatter
  2958. Write the data as this format. Supported output formats:
  2959. * JSON
  2960. * YAML
  2961. * Python (via pprint.pformat)
  2962. user
  2963. The user to own the directory, this defaults to the user salt is
  2964. running as on the minion
  2965. group
  2966. The group ownership set for the directory, this defaults to the group
  2967. salt is running as on the minion
  2968. mode
  2969. The permissions to set on this file, aka 644, 0775, 4664
  2970. backup
  2971. Overrides the default backup mode for this specific file.
  2972. makedirs
  2973. Create parent directories for destination file.
  2974. .. versionadded:: 2014.1.3
  2975. show_diff
  2976. If set to False, the diff will not be shown.
  2977. create
  2978. Default is True, if create is set to False then the file will only be
  2979. managed if the file already exists on the system.
  2980. merge_if_exists
  2981. Default is False, if merge_if_exists is True then the existing file will
  2982. be parsed and the dataset passed in will be merged with the existing
  2983. content
  2984. .. versionadded:: Helium
  2985. For example, this state::
  2986. /etc/dummy/package.json:
  2987. file.serialize:
  2988. - dataset:
  2989. name: naive
  2990. description: A package using naive versioning
  2991. author: A confused individual <iam@confused.com>
  2992. dependencies:
  2993. express: >= 1.2.0
  2994. optimist: >= 0.1.0
  2995. engine: node 0.4.1
  2996. - formatter: json
  2997. will manage the file ``/etc/dummy/package.json``::
  2998. {
  2999. "author": "A confused individual <iam@confused.com>",
  3000. "dependencies": {
  3001. "express": ">= 1.2.0",
  3002. "optimist": ">= 0.1.0"
  3003. },
  3004. "description": "A package using naive versioning",
  3005. "engine": "node 0.4.1"
  3006. "name": "naive",
  3007. }
  3008. '''
  3009. ret = {'changes': {},
  3010. 'comment': '',
  3011. 'name': name,
  3012. 'result': True}
  3013. if isinstance(env, salt._compat.string_types):
  3014. msg = (
  3015. 'Passing a salt environment should be done using \'saltenv\' not '
  3016. '\'env\'. This warning will go away in Salt Boron and this '
  3017. 'will be the default and expected behaviour. Please update your '
  3018. 'state files.'
  3019. )
  3020. salt.utils.warn_until('Boron', msg)
  3021. ret.setdefault('warnings', []).append(msg)
  3022. # No need to set __env__ = env since that's done in the state machinery
  3023. if not create:
  3024. if not os.path.isfile(name):
  3025. # Don't create a file that is not already present
  3026. ret['comment'] = ('File {0} is not present and is not set for '
  3027. 'creation').format(name)
  3028. return ret
  3029. formatter = kwargs.pop('formatter', 'yaml').lower()
  3030. if merge_if_exists:
  3031. if os.path.isfile(name):
  3032. if formatter == 'yaml':
  3033. existing_data = yaml.safe_load(file(name, 'r'))
  3034. elif formatter == 'json':
  3035. existing_data = json.load(file(name, 'r'))
  3036. else:
  3037. return {'changes': {},
  3038. 'comment': ('{0} format is not supported for merging'
  3039. .format(formatter.capitalized())),
  3040. 'name': name,
  3041. 'result': False}
  3042. if existing_data is not None:
  3043. for k, v in dataset.iteritems():
  3044. if k in existing_data:
  3045. ret['changes'].update(_merge_dict(existing_data, k, v))
  3046. else:
  3047. ret['changes'][k] = v
  3048. existing_data[k] = v
  3049. dataset = existing_data
  3050. if formatter == 'yaml':
  3051. contents = yaml_serializer.serialize(dataset,
  3052. default_flow_style=False)
  3053. elif formatter == 'json':
  3054. contents = json_serializer.serialize(dataset,
  3055. indent=2,
  3056. separators=(',', ': '),
  3057. sort_keys=True)
  3058. elif formatter == 'python':
  3059. # round-trip this through JSON to avoid OrderedDict types
  3060. # there's probably a more performant way to do this...
  3061. # TODO remove json round-trip when all dataset will use
  3062. # utils.serializers
  3063. contents = pprint.pformat(
  3064. json.loads(
  3065. json.dumps(dataset),
  3066. object_hook=salt.utils.decode_dict
  3067. )
  3068. )
  3069. else:
  3070. return {'changes': {},
  3071. 'comment': '{0} format is not supported'.format(
  3072. formatter.capitalized()),
  3073. 'name': name,
  3074. 'result': False
  3075. }
  3076. return __salt__['file.manage_file'](name=name,
  3077. sfn='',
  3078. ret=ret,
  3079. source=None,
  3080. source_sum={},
  3081. user=user,
  3082. group=group,
  3083. mode=mode,
  3084. saltenv=__env__,
  3085. backup=backup,
  3086. makedirs=makedirs,
  3087. template=None,
  3088. show_diff=show_diff,
  3089. contents=contents)
  3090. def mknod(name, ntype, major=0, minor=0, user=None, group=None, mode='0600'):
  3091. '''
  3092. Create a special file similar to the 'nix mknod command. The supported
  3093. device types are ``p`` (fifo pipe), ``c`` (character device), and ``b``
  3094. (block device). Provide the major and minor numbers when specifying a
  3095. character device or block device. A fifo pipe does not require this
  3096. information. The command will create the necessary dirs if needed. If a
  3097. file of the same name not of the same type/major/minor exists, it will not
  3098. be overwritten or unlinked (deleted). This is logically in place as a
  3099. safety measure because you can really shoot yourself in the foot here and
  3100. it is the behavior of 'nix ``mknod``. It is also important to note that not
  3101. just anyone can create special devices. Usually this is only done as root.
  3102. If the state is executed as none other than root on a minion, you may
  3103. receive a permission error.
  3104. name
  3105. name of the file
  3106. ntype
  3107. node type 'p' (fifo pipe), 'c' (character device), or 'b'
  3108. (block device)
  3109. major
  3110. major number of the device
  3111. does not apply to a fifo pipe
  3112. minor
  3113. minor number of the device
  3114. does not apply to a fifo pipe
  3115. user
  3116. owning user of the device/pipe
  3117. group
  3118. owning group of the device/pipe
  3119. mode
  3120. permissions on the device/pipe
  3121. Usage::
  3122. /dev/chr:
  3123. file.mknod:
  3124. - ntype: c
  3125. - major: 180
  3126. - minor: 31
  3127. - user: root
  3128. - group: root
  3129. - mode: 660
  3130. /dev/blk:
  3131. file.mknod:
  3132. - ntype: b
  3133. - major: 8
  3134. - minor: 999
  3135. - user: root
  3136. - group: root
  3137. - mode: 660
  3138. /dev/fifo:
  3139. file.mknod:
  3140. - ntype: p
  3141. - user: root
  3142. - group: root
  3143. - mode: 660
  3144. .. versionadded:: 0.17.0
  3145. '''
  3146. ret = {'name': name,
  3147. 'changes': {},
  3148. 'comment': '',
  3149. 'result': False}
  3150. if ntype == 'c':
  3151. # Check for file existence
  3152. if __salt__['file.file_exists'](name):
  3153. ret['comment'] = (
  3154. 'File exists and is not a character device {0}. Cowardly '
  3155. 'refusing to continue'.format(name)
  3156. )
  3157. # Check if it is a character device
  3158. elif not __salt__['file.is_chrdev'](name):
  3159. if __opts__['test']:
  3160. ret['comment'] = (
  3161. 'Character device {0} is set to be created'
  3162. ).format(name)
  3163. ret['result'] = None
  3164. else:
  3165. ret = __salt__['file.mknod'](name,
  3166. ntype,
  3167. major,
  3168. minor,
  3169. user,
  3170. group,
  3171. mode)
  3172. # Check the major/minor
  3173. else:
  3174. devmaj, devmin = __salt__['file.get_devmm'](name)
  3175. if (major, minor) != (devmaj, devmin):
  3176. ret['comment'] = (
  3177. 'Character device {0} exists and has a different '
  3178. 'major/minor {1}/{2}. Cowardly refusing to continue'
  3179. .format(name, devmaj, devmin)
  3180. )
  3181. # Check the perms
  3182. else:
  3183. ret = __salt__['file.check_perms'](name,
  3184. None,
  3185. user,
  3186. group,
  3187. mode)[0]
  3188. if not ret['changes']:
  3189. ret['comment'] = (
  3190. 'Character device {0} is in the correct state'.format(
  3191. name
  3192. )
  3193. )
  3194. elif ntype == 'b':
  3195. # Check for file existence
  3196. if __salt__['file.file_exists'](name):
  3197. ret['comment'] = (
  3198. 'File exists and is not a block device {0}. Cowardly '
  3199. 'refusing to continue'.format(name)
  3200. )
  3201. # Check if it is a block device
  3202. elif not __salt__['file.is_blkdev'](name):
  3203. if __opts__['test']:
  3204. ret['comment'] = (
  3205. 'Block device {0} is set to be created'
  3206. ).format(name)
  3207. ret['result'] = None
  3208. else:
  3209. ret = __salt__['file.mknod'](name,
  3210. ntype,
  3211. major,
  3212. minor,
  3213. user,
  3214. group,
  3215. mode)
  3216. # Check the major/minor
  3217. else:
  3218. devmaj, devmin = __salt__['file.get_devmm'](name)
  3219. if (major, minor) != (devmaj, devmin):
  3220. ret['comment'] = (
  3221. 'Block device {0} exists and has a different major/minor '
  3222. '{1}/{2}. Cowardly refusing to continue'.format(
  3223. name, devmaj, devmin
  3224. )
  3225. )
  3226. # Check the perms
  3227. else:
  3228. ret = __salt__['file.check_perms'](name,
  3229. None,
  3230. user,
  3231. group,
  3232. mode)[0]
  3233. if not ret['changes']:
  3234. ret['comment'] = (
  3235. 'Block device {0} is in the correct state'.format(name)
  3236. )
  3237. elif ntype == 'p':
  3238. # Check for file existence
  3239. if __salt__['file.file_exists'](name):
  3240. ret['comment'] = (
  3241. 'File exists and is not a fifo pipe {0}. Cowardly refusing '
  3242. 'to continue'.format(name)
  3243. )
  3244. # Check if it is a fifo
  3245. elif not __salt__['file.is_fifo'](name):
  3246. if __opts__['test']:
  3247. ret['comment'] = 'Fifo pipe {0} is set to be created'.format(
  3248. name
  3249. )
  3250. ret['result'] = None
  3251. else:
  3252. ret = __salt__['file.mknod'](name,
  3253. ntype,
  3254. major,
  3255. minor,
  3256. user,
  3257. group,
  3258. mode)
  3259. # Check the perms
  3260. else:
  3261. ret = __salt__['file.check_perms'](name,
  3262. None,
  3263. user,
  3264. group,
  3265. mode)[0]
  3266. if not ret['changes']:
  3267. ret['comment'] = (
  3268. 'Fifo pipe {0} is in the correct state'.format(name)
  3269. )
  3270. else:
  3271. ret['comment'] = (
  3272. 'Node type unavailable: {0!r}. Available node types are '
  3273. 'character (\'c\'), block (\'b\'), and pipe (\'p\')'.format(ntype)
  3274. )
  3275. return ret
  3276. def mod_run_check_cmd(cmd, filename):
  3277. '''
  3278. Execute the check_cmd logic
  3279. Return a result dict if:
  3280. * check_cmd succeeded (check_cmd == 0)
  3281. else return True
  3282. '''
  3283. log.debug('running our check_cmd')
  3284. _cmd = '{0} {1}'.format(cmd, filename)
  3285. if __salt__['cmd.retcode'](_cmd) != 0:
  3286. return {'comment': 'check_cmd execution failed',
  3287. 'result': True}
  3288. # No reason to stop, return True
  3289. return True