/salt/states/file.py
Python | 3882 lines | 3766 code | 28 blank | 88 comment | 58 complexity | 425da95a095315035529e76a74c2f07d MD5 | raw file
Possible License(s): Apache-2.0
Large files files are truncated, but you can click here to view the full file
- # -*- coding: utf-8 -*-
- '''
- Operations on regular files, special files, directories, and symlinks
- =====================================================================
- Salt States can aggressively manipulate files on a system. There are a number
- of ways in which files can be managed.
- Regular files can be enforced with the ``managed`` function. This function
- downloads files from the salt master and places them on the target system.
- The downloaded files can be rendered as a jinja, mako, or wempy template,
- adding a dynamic component to file management. An example of ``file.managed``
- which makes use of the jinja templating system would look like this:
- .. code-block:: yaml
- /etc/http/conf/http.conf:
- file.managed:
- - source: salt://apache/http.conf
- - user: root
- - group: root
- - mode: 644
- - template: jinja
- - defaults:
- custom_var: "default value"
- other_var: 123
- {% if grains['os'] == 'Ubuntu' %}
- - context:
- custom_var: "override"
- {% endif %}
- .. note::
- When using both the ``defaults`` and ``context`` arguments, note the extra
- indentation (four spaces instead of the normal two). This is due to an
- idiosyncrasy of how PyYAML loads nested dictionaries, and is explained in
- greater detail :ref:`here <nested-dict-indentation>`.
- If using a template, any user-defined template variables in the file defined in
- ``source`` must be passed in using the ``defaults`` and/or ``context``
- arguments. The general best practice is to place default values in
- ``defaults``, with conditional overrides going into ``context``, as seen above.
- The template will receive a variable ``custom_var``, which would be accessed in
- the template using ``{{ custom_var }}``. If the operating system is Ubuntu, the
- value of the variable ``custom_var`` would be *override*, otherwise it is the
- default *default value*
- The ``source`` parameter can be specified as a list. If this is done, then the
- first file to be matched will be the one that is used. This allows you to have
- a default file on which to fall back if the desired file does not exist on the
- salt fileserver. Here's an example:
- .. code-block:: yaml
- /etc/foo.conf:
- file.managed:
- - source:
- - salt://foo.conf.{{ grains['fqdn'] }}
- - salt://foo.conf.fallback
- - user: foo
- - group: users
- - mode: 644
- - backup: minion
- .. note::
- Salt supports backing up managed files via the backup option. For more
- details on this functionality please review the
- :doc:`backup_mode documentation </ref/states/backup_mode>`.
- The ``source`` parameter can also specify a file in another Salt environment.
- In this example ``foo.conf`` in the ``dev`` environment will be used instead.
- .. code-block:: yaml
- /etc/foo.conf:
- file.managed:
- - source:
- - salt://foo.conf?saltenv=dev
- - user: foo
- - group: users
- - mode: '0644'
- .. warning::
- When using a mode that includes a leading zero you must wrap the
- value in single quotes. If the value is not wrapped in quotes it
- will be read by YAML as an integer and evaluated as an octal.
- Special files can be managed via the ``mknod`` function. This function will
- create and enforce the permissions on a special file. The function supports the
- creation of character devices, block devices, and fifo pipes. The function will
- create the directory structure up to the special file if it is needed on the
- minion. The function will not overwrite or operate on (change major/minor
- numbers) existing special files with the exception of user, group, and
- permissions. In most cases the creation of some special files require root
- permisisons on the minion. This would require that the minion to be run as the
- root user. Here is an example of a character device:
- .. code-block:: yaml
- /var/named/chroot/dev/random:
- file.mknod:
- - ntype: c
- - major: 1
- - minor: 8
- - user: named
- - group: named
- - mode: 660
- Here is an example of a block device:
- .. code-block:: yaml
- /var/named/chroot/dev/loop0:
- file.mknod:
- - ntype: b
- - major: 7
- - minor: 0
- - user: named
- - group: named
- - mode: 660
- Here is an example of a fifo pipe:
- .. code-block:: yaml
- /var/named/chroot/var/log/logfifo:
- file.mknod:
- - ntype: p
- - user: named
- - group: named
- - mode: 660
- Directories can be managed via the ``directory`` function. This function can
- create and enforce the permissions on a directory. A directory statement will
- look like this:
- .. code-block:: yaml
- /srv/stuff/substuf:
- file.directory:
- - user: fred
- - group: users
- - mode: 755
- - makedirs: True
- If you need to enforce user and/or group ownership or permissions recursively
- on the directory's contents, you can do so by adding a ``recurse`` directive:
- .. code-block:: yaml
- /srv/stuff/substuf:
- file.directory:
- - user: fred
- - group: users
- - mode: 755
- - makedirs: True
- - recurse:
- - user
- - group
- - mode
- As a default, ``mode`` will resolve to ``dir_mode`` and ``file_mode``, to
- specify both directory and file permissions, use this form:
- .. code-block:: yaml
- /srv/stuff/substuf:
- file.directory:
- - user: fred
- - group: users
- - file_mode: 744
- - dir_mode: 755
- - makedirs: True
- - recurse:
- - user
- - group
- - mode
- Symlinks can be easily created; the symlink function is very simple and only
- takes a few arguments:
- .. code-block:: yaml
- /etc/grub.conf:
- file.symlink:
- - target: /boot/grub/grub.conf
- Recursive directory management can also be set via the ``recurse``
- function. Recursive directory management allows for a directory on the salt
- master to be recursively copied down to the minion. This is a great tool for
- deploying large code and configuration systems. A state using ``recurse``
- would look something like this:
- .. code-block:: yaml
- /opt/code/flask:
- file.recurse:
- - source: salt://code/flask
- - include_empty: True
- A more complex ``recurse`` example:
- .. code-block:: yaml
- {% set site_user = 'testuser' %}
- {% set site_name = 'test_site' %}
- {% set project_name = 'test_proj' %}
- {% set sites_dir = 'test_dir' %}
- django-project:
- file.recurse:
- - name: {{ sites_dir }}/{{ site_name }}/{{ project_name }}
- - user: {{ site_user }}
- - dir_mode: 2775
- - file_mode: '0644'
- - template: jinja
- - source: salt://project/templates_dir
- - include_empty: True
- '''
- # Import python libs
- import difflib
- import json
- import logging
- import os
- import pprint
- import shutil
- import traceback
- import yaml
- # Import salt libs
- import salt.utils
- import salt.utils.templates
- from salt.exceptions import CommandExecutionError
- from salt.utils.serializers import yaml as yaml_serializer
- from salt.utils.serializers import json as json_serializer
- from salt._compat import string_types, integer_types
- log = logging.getLogger(__name__)
- COMMENT_REGEX = r'^([[:space:]]*){0}[[:space:]]?'
- _ACCUMULATORS = {}
- _ACCUMULATORS_DEPS = {}
- def _check_user(user, group):
- '''
- Checks if the named user and group are present on the minion
- '''
- err = ''
- if user:
- uid = __salt__['file.user_to_uid'](user)
- if uid == '':
- err += 'User {0} is not available '.format(user)
- if group:
- gid = __salt__['file.group_to_gid'](group)
- if gid == '':
- err += 'Group {0} is not available'.format(group)
- return err
- def _gen_keep_files(name, require):
- '''
- Generate the list of files that need to be kept when a dir based function
- like directory or recurse has a clean.
- '''
- keep = set()
- keep.add(name)
- if isinstance(require, list):
- for comp in require:
- if 'file' in comp:
- keep.add(comp['file'])
- if os.path.isdir(comp['file']):
- for root, dirs, files in os.walk(comp['file']):
- for name in files:
- keep.add(os.path.join(root, name))
- for name in dirs:
- keep.add(os.path.join(root, name))
- return list(keep)
- def _check_file(name):
- ret = True
- msg = ''
- if not os.path.isabs(name):
- ret = False
- msg = 'Specified file {0} is not an absolute path'.format(name)
- elif not os.path.exists(name):
- ret = False
- msg = '{0}: file not found'.format(name)
- return ret, msg
- def _clean_dir(root, keep, exclude_pat):
- '''
- Clean out all of the files and directories in a directory (root) while
- preserving the files in a list (keep) and part of exclude_pat
- '''
- removed = set()
- real_keep = set()
- real_keep.add(root)
- if isinstance(keep, list):
- for fn_ in keep:
- if not os.path.isabs(fn_):
- continue
- real_keep.add(fn_)
- while True:
- fn_ = os.path.dirname(fn_)
- real_keep.add(fn_)
- if fn_ in ['/', ''.join([os.path.splitdrive(fn_)[0], '\\'])]:
- break
- for roots, dirs, files in os.walk(root):
- for name in dirs:
- nfn = os.path.join(roots, name)
- if os.path.islink(nfn):
- # the "directory" is in fact a symlink and cannot be
- # removed by shutil.rmtree
- files.append(nfn)
- continue
- if nfn not in real_keep:
- # -- check if this is a part of exclude_pat(only). No need to
- # check include_pat
- if not salt.utils.check_include_exclude(
- nfn[len(root) + 1:], None, exclude_pat):
- continue
- removed.add(nfn)
- if not __opts__['test']:
- shutil.rmtree(nfn)
- for name in files:
- nfn = os.path.join(roots, name)
- if nfn not in real_keep:
- # -- check if this is a part of exclude_pat(only). No need to
- # check include_pat
- if not salt.utils.check_include_exclude(
- nfn[len(root) + 1:], None, exclude_pat):
- continue
- removed.add(nfn)
- if not __opts__['test']:
- os.remove(nfn)
- return list(removed)
- def _error(ret, err_msg):
- ret['result'] = False
- ret['comment'] = err_msg
- return ret
- def _get_recurse_dest(prefix, fn_, source, env):
- '''
- Return the destination path to copy the file path, fn_(as returned by
- a call to __salt__['cp.cache_dir']), to.
- '''
- local_roots = []
- if __opts__['file_client'] == 'local':
- local_roots = __opts__['file_roots'][env]
- local_roots.sort(key=len, reverse=True)
- srcpath = source[7:] # the path after "salt://"
- # in solo mode(ie, file_client=='local'), fn_ is a path below
- # a file root; in remote mode, fn_ is a path below the cache_dir.
- for root in local_roots:
- rootlen = len(root)
- # if root is the longest prefix path of fn_
- if root == fn_[:rootlen]:
- cachedir = os.path.join(root, srcpath)
- break
- else:
- cachedir = os.path.join(
- __opts__['cachedir'], 'files', env, srcpath)
- return os.path.join(prefix, os.path.relpath(fn_, cachedir))
- def _check_directory(name,
- user,
- group,
- recurse,
- mode,
- clean,
- require,
- exclude_pat):
- '''
- Check what changes need to be made on a directory
- '''
- changes = {}
- if recurse:
- if not set(['user', 'group', 'mode']) >= set(recurse):
- return False, 'Types for "recurse" limited to "user", ' \
- '"group" and "mode"'
- if 'user' not in recurse:
- user = None
- if 'group' not in recurse:
- group = None
- if 'mode' not in recurse:
- mode = None
- for root, dirs, files in os.walk(name):
- for fname in files:
- fchange = {}
- path = os.path.join(root, fname)
- stats = __salt__['file.stats'](
- path, 'md5', follow_symlinks=False
- )
- if user is not None and user != stats.get('user'):
- fchange['user'] = user
- if group is not None and group != stats.get('group'):
- fchange['group'] = group
- if fchange:
- changes[path] = fchange
- for name_ in dirs:
- path = os.path.join(root, name_)
- fchange = _check_dir_meta(path, user, group, mode)
- if fchange:
- changes[path] = fchange
- else:
- fchange = _check_dir_meta(name, user, group, mode)
- if fchange:
- changes[name] = fchange
- if clean:
- keep = _gen_keep_files(name, require)
- for root, dirs, files in os.walk(name):
- for fname in files:
- fchange = {}
- path = os.path.join(root, fname)
- if path not in keep:
- if not salt.utils.check_include_exclude(
- path[len(name) + 1:], None, exclude_pat):
- continue
- fchange['removed'] = 'Removed due to clean'
- changes[path] = fchange
- for name_ in dirs:
- fchange = {}
- path = os.path.join(root, name_)
- if path not in keep:
- if not salt.utils.check_include_exclude(
- path[len(name) + 1:], None, exclude_pat):
- continue
- fchange['removed'] = 'Removed due to clean'
- changes[path] = fchange
- if not os.path.isdir(name):
- changes[name] = {'directory': 'new'}
- if changes:
- comments = ['The following files will be changed:\n']
- for fn_ in changes:
- key, val = changes[fn_].keys()[0], changes[fn_].values()[0]
- comments.append('{0}: {1} - {2}\n'.format(fn_, key, val))
- return None, ''.join(comments)
- return True, 'The directory {0} is in the correct state'.format(name)
- def _check_dir_meta(name,
- user,
- group,
- mode):
- '''
- Check the changes in directory metadata
- '''
- stats = __salt__['file.stats'](name, follow_symlinks=False)
- changes = {}
- if not stats:
- changes['directory'] = 'new'
- return changes
- if user is not None and user != stats['user']:
- changes['user'] = user
- if group is not None and group != stats['group']:
- changes['group'] = group
- # Normalize the dir mode
- smode = __salt__['config.manage_mode'](stats['mode'])
- mode = __salt__['config.manage_mode'](mode)
- if mode is not None and mode != smode:
- changes['mode'] = mode
- return changes
- def _check_touch(name, atime, mtime):
- '''
- Check to see if a file needs to be updated or created
- '''
- if not os.path.exists(name):
- return None, 'File {0} is set to be created'.format(name)
- stats = __salt__['file.stats'](name, follow_symlinks=False)
- if atime is not None:
- if str(atime) != str(stats['atime']):
- return None, 'Times set to be updated on file {0}'.format(name)
- if mtime is not None:
- if str(mtime) != str(stats['mtime']):
- return None, 'Times set to be updated on file {0}'.format(name)
- return True, 'File {0} exists and has the correct times'.format(name)
- def _get_symlink_ownership(path):
- return (
- __salt__['file.get_user'](path, follow_symlinks=False),
- __salt__['file.get_group'](path, follow_symlinks=False)
- )
- def _check_symlink_ownership(path, user, group):
- '''
- Check if the symlink ownership matches the specified user and group
- '''
- cur_user, cur_group = _get_symlink_ownership(path)
- return (cur_user == user) and (cur_group == group)
- def _set_symlink_ownership(path, user, group):
- '''
- Set the ownership of a symlink and return a boolean indicating
- success/failure
- '''
- try:
- __salt__['file.lchown'](path, user, group)
- except OSError:
- pass
- return _check_symlink_ownership(path, user, group)
- def _symlink_check(name, target, force, user, group):
- '''
- Check the symlink function
- '''
- if not os.path.exists(name) and not __salt__['file.is_link'](name):
- return None, 'Symlink {0} to {1} is set for creation'.format(
- name, target
- )
- if __salt__['file.is_link'](name):
- if __salt__['file.readlink'](name) != target:
- return None, 'Link {0} target is set to be changed to {1}'.format(
- name, target
- )
- else:
- result = True
- msg = 'The symlink {0} is present'.format(name)
- if not _check_symlink_ownership(name, user, group):
- result = None
- msg += (
- ', but the ownership of the symlink would be changed '
- 'from {2}:{3} to {0}:{1}'
- ).format(user, group, *_get_symlink_ownership(name))
- return result, msg
- else:
- if force:
- return None, ('The file or directory {0} is set for removal to '
- 'make way for a new symlink targeting {1}'
- .format(name, target))
- return False, ('File or directory exists where the symlink {0} '
- 'should be. Did you mean to use force?'.format(name))
- def _test_owner(kwargs, user=None):
- '''
- Convert owner to user, since other config management tools use owner,
- no need to punish people coming from other systems.
- PLEASE DO NOT DOCUMENT THIS! WE USE USER, NOT OWNER!!!!
- '''
- if user:
- return user
- if 'owner' in kwargs:
- log.warning(
- 'Use of argument owner found, "owner" is invalid, please '
- 'use "user"'
- )
- return kwargs['owner']
- return user
- def _unify_sources_and_hashes(source=None, source_hash=None,
- sources=None, source_hashes=None):
- '''
- Silly little function to give us a standard tuple list for sources and
- source_hashes
- '''
- if sources is None:
- sources = []
- if source_hashes is None:
- source_hashes = []
- if source and sources:
- return (False,
- "source and sources are mutually exclusive", [])
- if source_hash and source_hashes:
- return (False,
- "source_hash and source_hashes are mutually exclusive", [])
- if source:
- return (True, '', [(source, source_hash)])
- # Make a nice neat list of tuples exactly len(sources) long..
- return (True, '', map(None, sources, source_hashes[:len(sources)]))
- def _get_template_texts(source_list=None,
- template='jinja',
- defaults=None,
- context=None,
- **kwargs):
- '''
- Iterate a list of sources and process them as templates.
- Returns a list of 'chunks' containing the rendered templates.
- '''
- ret = {'name': '_get_template_texts',
- 'changes': {},
- 'result': True,
- 'comment': '',
- 'data': []}
- if source_list is None:
- return _error(ret,
- '_get_template_texts called with empty source_list')
- txtl = []
- for (source, source_hash) in source_list:
- tmpctx = defaults if defaults else {}
- if context:
- tmpctx.update(context)
- rndrd_templ_fn = __salt__['cp.get_template'](
- source,
- '',
- template=template,
- saltenv=__env__,
- context=tmpctx,
- **kwargs
- )
- msg = 'cp.get_template returned {0} (Called with: {1})'
- log.debug(msg.format(rndrd_templ_fn, source))
- if rndrd_templ_fn:
- tmplines = None
- with salt.utils.fopen(rndrd_templ_fn, 'rb') as fp_:
- tmplines = fp_.readlines()
- if not tmplines:
- msg = 'Failed to read rendered template file {0} ({1})'
- log.debug(msg.format(rndrd_templ_fn, source))
- ret['name'] = source
- return _error(ret, msg.format(rndrd_templ_fn, source))
- txtl.append(''.join(tmplines))
- else:
- msg = 'Failed to load template file {0}'.format(source)
- log.debug(msg)
- ret['name'] = source
- return _error(ret, msg)
- ret['data'] = txtl
- return ret
- def symlink(
- name,
- target,
- force=False,
- backupname=None,
- makedirs=False,
- user=None,
- group=None,
- mode=None,
- **kwargs):
- '''
- Create a symlink
- If the file already exists and is a symlink pointing to any location other
- than the specified target, the symlink will be replaced. If the symlink is
- a regular file or directory then the state will return False. If the
- regular file or directory is desired to be replaced with a symlink pass
- force: True, if it is to be renamed, pass a backupname.
- name
- The location of the symlink to create
- target
- The location that the symlink points to
- force
- If the name of the symlink exists and is not a symlink and
- force is set to False, the state will fail. If force is set to
- True, the file or directory in the way of the symlink file
- will be deleted to make room for the symlink, unless
- backupname is set, when it will be renamed
- backupname
- If the name of the symlink exists and is not a symlink, it will be
- renamed to the backupname. If the backupname already
- exists and force is False, the state will fail. Otherwise, the
- backupname will be removed first.
- makedirs
- If the location of the symlink does not already have a parent directory
- then the state will fail, setting makedirs to True will allow Salt to
- create the parent directory
- user
- The user to own the file, this defaults to the user salt is running as
- on the minion
- group
- The group ownership set for the file, this defaults to the group salt
- is running as on the minion. On Windows, this is ignored
- mode
- The permissions to set on this file, aka 644, 0775, 4664. Not supported
- on Windows
- '''
- # Make sure that leading zeros stripped by YAML loader are added back
- mode = __salt__['config.manage_mode'](mode)
- user = _test_owner(kwargs, user=user)
- ret = {'name': name,
- 'changes': {},
- 'result': True,
- 'comment': ''}
- if user is None:
- user = __opts__['user']
- if salt.utils.is_windows():
- if group is not None:
- log.warning(
- 'The group argument for {0} has been ignored as this '
- 'is a Windows system.'.format(name)
- )
- group = user
- if group is None:
- group = __salt__['file.gid_to_group'](
- __salt__['user.info'](user).get('gid', 0)
- )
- preflight_errors = []
- uid = __salt__['file.user_to_uid'](user)
- gid = __salt__['file.group_to_gid'](group)
- if uid == '':
- preflight_errors.append('User {0} does not exist'.format(user))
- if gid == '':
- preflight_errors.append('Group {0} does not exist'.format(group))
- if not os.path.isabs(name):
- preflight_errors.append(
- 'Specified file {0} is not an absolute path'.format(name)
- )
- if preflight_errors:
- msg = '. '.join(preflight_errors)
- if len(preflight_errors) > 1:
- msg += '.'
- return _error(ret, msg)
- if __opts__['test']:
- ret['result'], ret['comment'] = _symlink_check(name, target, force,
- user, group)
- return ret
- if not os.path.isdir(os.path.dirname(name)):
- if makedirs:
- __salt__['file.makedirs'](
- name,
- user=user,
- group=group,
- mode=mode)
- else:
- return _error(
- ret,
- 'Directory {0} for symlink is not present'.format(
- os.path.dirname(name)
- )
- )
- if __salt__['file.is_link'](name):
- # The link exists, verify that it matches the target
- if __salt__['file.readlink'](name) != target:
- # The target is wrong, delete the link
- os.remove(name)
- else:
- if _check_symlink_ownership(name, user, group):
- # The link looks good!
- ret['comment'] = ('Symlink {0} is present and owned by '
- '{1}:{2}'.format(name, user, group))
- else:
- if _set_symlink_ownership(name, user, group):
- ret['comment'] = ('Set ownership of symlink {0} to '
- '{1}:{2}'.format(name, user, group))
- ret['changes']['ownership'] = '{0}:{1}'.format(user, group)
- else:
- ret['result'] = False
- ret['comment'] += (
- 'Failed to set ownership of symlink {0} to '
- '{1}:{2}'.format(name, user, group)
- )
- return ret
- elif os.path.isfile(name) or os.path.isdir(name):
- # It is not a link, but a file or dir
- if backupname is not None:
- # Make a backup first
- if os.path.lexists(backupname):
- if not force:
- return _error(ret, ((
- 'File exists where the backup target {0} should go'
- ).format(backupname)))
- elif os.path.isfile(backupname):
- os.remove(backupname)
- elif os.path.isdir(backupname):
- shutil.rmtree(backupname)
- else:
- return _error(ret, ((
- 'Something exists where the backup target {0}'
- 'should go'
- ).format(backupname)))
- os.rename(name, backupname)
- elif force:
- # Remove whatever is in the way
- if os.path.isfile(name):
- os.remove(name)
- ret['changes']['forced'] = 'Symlink was forcibly replaced'
- else:
- shutil.rmtree(name)
- else:
- # Otherwise throw an error
- if os.path.isfile(name):
- return _error(ret,
- ('File exists where the symlink {0} should be'
- .format(name)))
- else:
- return _error(ret, ((
- 'Directory exists where the symlink {0} should be'
- ).format(name)))
- if not os.path.exists(name):
- # The link is not present, make it
- try:
- __salt__['file.symlink'](target, name)
- except OSError as exc:
- ret['result'] = False
- ret['comment'] = ('Unable to create new symlink {0} -> '
- '{1}: {2}'.format(name, target, exc))
- return ret
- else:
- ret['comment'] = ('Created new symlink {0} -> '
- '{1}'.format(name, target))
- ret['changes']['new'] = name
- if not _check_symlink_ownership(name, user, group):
- if not _set_symlink_ownership(name, user, group):
- ret['result'] = False
- ret['comment'] += (', but was unable to set ownership to '
- '{0}:{1}'.format(user, group))
- return ret
- def absent(name):
- '''
- Make sure that the named file or directory is absent. If it exists, it will
- be deleted. This will work to reverse any of the functions in the file
- state module.
- name
- The path which should be deleted
- '''
- ret = {'name': name,
- 'changes': {},
- 'result': True,
- 'comment': ''}
- if not os.path.isabs(name):
- return _error(
- ret, 'Specified file {0} is not an absolute path'.format(name)
- )
- if name == '/':
- return _error(ret, 'Refusing to make "/" absent')
- if os.path.isfile(name) or os.path.islink(name):
- if __opts__['test']:
- ret['result'] = None
- ret['comment'] = 'File {0} is set for removal'.format(name)
- return ret
- try:
- __salt__['file.remove'](name)
- ret['comment'] = 'Removed file {0}'.format(name)
- ret['changes']['removed'] = name
- return ret
- except CommandExecutionError as exc:
- return _error(ret, '{0}'.format(exc))
- elif os.path.isdir(name):
- if __opts__['test']:
- ret['result'] = None
- ret['comment'] = 'Directory {0} is set for removal'.format(name)
- return ret
- try:
- shutil.rmtree(name)
- ret['comment'] = 'Removed directory {0}'.format(name)
- ret['changes']['removed'] = name
- return ret
- except (OSError, IOError):
- return _error(ret, 'Failed to remove directory {0}'.format(name))
- ret['comment'] = 'File {0} is not present'.format(name)
- return ret
- def exists(name):
- '''
- Verify that the named file or directory is present or exists.
- Ensures pre-requisites outside of Salt's purview
- (e.g., keytabs, private keys, etc.) have been previously satisfied before
- deployment.
- name
- Absolute path which must exist
- '''
- ret = {'name': name,
- 'changes': {},
- 'result': True,
- 'comment': ''}
- if not os.path.exists(name):
- return _error(ret, ('Specified path {0} does not exist').format(name))
- ret['comment'] = 'Path {0} exists'.format(name)
- return ret
- def missing(name):
- '''
- Verify that the named file or directory is missing, this returns True only
- if the named file is missing but does not remove the file if it is present.
- name
- Absolute path which must NOT exist
- '''
- ret = {'name': name,
- 'changes': {},
- 'result': True,
- 'comment': ''}
- if os.path.exists(name):
- return _error(ret, ('Specified path {0} exists').format(name))
- ret['comment'] = 'Path {0} is missing'.format(name)
- return ret
- def managed(name,
- source=None,
- source_hash='',
- user=None,
- group=None,
- mode=None,
- template=None,
- makedirs=False,
- dir_mode=None,
- context=None,
- replace=True,
- defaults=None,
- env=None,
- backup='',
- show_diff=True,
- create=True,
- contents=None,
- contents_pillar=None,
- contents_grains=None,
- contents_newline=True,
- follow_symlinks=True,
- check_cmd=None,
- **kwargs):
- '''
- Manage a given file, this function allows for a file to be downloaded from
- the salt master and potentially run through a templating system.
- name
- The location of the file to manage
- source
- The source file to download to the minion, this source file can be
- hosted on either the salt master server, or on an HTTP or FTP server.
- Both HTTPS and HTTP are supported as well as downloading directly
- from Amazon S3 compatible URLs with both pre-configured and automatic
- IAM credentials. (see s3.get state documentation)
- File retrieval from Openstack Swift object storage is supported via
- swift://container/object_path URLs, see swift.get documentation.
- For files hosted on the salt file server, if the file is located on
- the master in the directory named spam, and is called eggs, the source
- string is salt://spam/eggs. If source is left blank or None
- (use ~ in YAML), the file will be created as an empty file and
- the content will not be managed
- If the file is hosted on a HTTP or FTP server then the source_hash
- argument is also required
- source_hash
- This can be one of the following:
- 1. a source hash string
- 2. the URI of a file that contains source hash strings
- The function accepts the first encountered long unbroken alphanumeric
- string of correct length as a valid hash, in order from most secure to
- least secure::
- Type Length
- ====== ======
- sha512 128
- sha384 96
- sha256 64
- sha224 56
- sha1 40
- md5 32
- The file can contain several checksums for several files. Each line
- must contain both the file name and the hash. If no file name is
- matched, the first hash encountered will be used, otherwise the most
- secure hash with the correct source file name will be used.
- Debian file type ``*.dsc`` is supported.
- Examples::
- /etc/rc.conf ef6e82e4006dee563d98ada2a2a80a27
- sha254c8525aee419eb649f0233be91c151178b30f0dff8ebbdcc8de71b1d5c8bcc06a /etc/resolv.conf
- ead48423703509d37c4a90e6a0d53e143b6fc268
- Known issues:
- If the remote server URL has the hash file as an apparent
- sub-directory of the source file, the module will discover that it
- has already cached a directory where a file should be cached. For
- example:
- .. code-block:: yaml
- tomdroid-src-0.7.3.tar.gz:
- file.managed:
- - name: /tmp/tomdroid-src-0.7.3.tar.gz
- - source: https://launchpad.net/tomdroid/beta/0.7.3/+download/tomdroid-src-0.7.3.tar.gz
- - source_hash: https://launchpad.net/tomdroid/beta/0.7.3/+download/tomdroid-src-0.7.3.tar.gz/+md5
- user
- The user to own the file, this defaults to the user salt is running as
- on the minion
- group
- The group ownership set for the file, this defaults to the group salt
- is running as on the minion On Windows, this is ignored
- mode
- The permissions to set on this file, aka 644, 0775, 4664. Not supported
- on Windows
- template
- If this setting is applied then the named templating engine will be
- used to render the downloaded file, currently jinja, mako, and wempy
- are supported
- makedirs
- If the file is located in a path without a parent directory, then
- the state will fail. If makedirs is set to True, then the parent
- directories will be created to facilitate the creation of the named
- file.
- dir_mode
- If directories are to be created, passing this option specifies the
- permissions for those directories. If this is not set, directories
- will be assigned permissions from the 'mode' argument.
- replace
- If this file should be replaced. If false, this command will
- not overwrite file contents but will enforce permissions if the file
- exists already. Default is True.
- context
- Overrides default context variables passed to the template.
- defaults
- Default context passed to the template.
- backup
- Overrides the default backup mode for this specific file.
- show_diff
- If set to False, the diff will not be shown.
- create
- Default is True, if create is set to False then the file will only be
- managed if the file already exists on the system.
- contents
- Default is None. If specified, will use the given string as the
- contents of the file. Should not be used in conjunction with a source
- file of any kind. Ignores hashes and does not use a templating engine.
- contents_pillar
- .. versionadded:: 0.17.0
- Operates like ``contents``, but draws from a value stored in pillar,
- using the pillar path syntax used in :mod:`pillar.get
- <salt.modules.pillar.get>`. This is useful when the pillar value
- contains newlines, as referencing a pillar variable using a jinja/mako
- template can result in YAML formatting issues due to the newlines
- causing indentation mismatches.
- For example, the following could be used to deploy an SSH private key:
- .. code-block:: yaml
- /home/deployer/.ssh/id_rsa:
- file.managed:
- - user: deployer
- - group: deployer
- - mode: 600
- - contents_pillar: userdata:deployer:id_rsa
- This would populate ``/home/deployer/.ssh/id_rsa`` with the contents of
- ``pillar['userdata']['deployer']['id_rsa']``. An example of this pillar
- setup would be like so:
- .. code-block:: yaml
- userdata:
- deployer:
- id_rsa: |
- -----BEGIN RSA PRIVATE KEY-----
- MIIEowIBAAKCAQEAoQiwO3JhBquPAalQF9qP1lLZNXVjYMIswrMe2HcWUVBgh+vY
- U7sCwx/dH6+VvNwmCoqmNnP+8gTPKGl1vgAObJAnMT623dMXjVKwnEagZPRJIxDy
- B/HaAre9euNiY3LvIzBTWRSeMfT+rWvIKVBpvwlgGrfgz70m0pqxu+UyFbAGLin+
- GpxzZAMaFpZw4sSbIlRuissXZj/sHpQb8p9M5IeO4Z3rjkCP1cxI
- -----END RSA PRIVATE KEY-----
- .. note::
- The private key above is shortened to keep the example brief, but
- shows how to do multiline string in YAML. The key is followed by a
- pipe character, and the mutliline string is indented two more
- spaces.
- contents_grains
- .. versionadded:: Helium
- Same as contents_pillar, but with grains
- contents_newline
- .. versionadded:: Helium
- When using contents, contents_pillar, or contents_grains, a newline is
- inserted into the data. When loading some data this newline is better
- left off. Setting contents_newline to False will omit this newline.
- follow_symlinks : True
- .. versionadded:: Helium
- If the desired path is a symlink follow it and make changes to the
- file to which the symlink points.
- check_cmd
- .. versionadded:: Helium
- Do run the state only if the check_cmd succeeds
- '''
- # Make sure that leading zeros stripped by YAML loader are added back
- mode = __salt__['config.manage_mode'](mode)
- # If no source is specified, set replace to False, as there is nothing
- # to replace the file with.
- src_defined = source or contents or contents_pillar or contents_grains
- if not src_defined and replace:
- replace = False
- log.warning(
- 'Neither \'source\' nor \'contents\' nor \'contents_pillar\' nor \'contents_grains\' '
- 'was defined, yet \'replace\' was set to \'True\'. As there is '
- 'no source to replace the file with, \'replace\' has been set '
- 'to \'False\' to avoid reading the file unnecessarily'.format(name)
- )
- user = _test_owner(kwargs, user=user)
- if salt.utils.is_windows():
- if group is not None:
- log.warning(
- 'The group argument for {0} has been ignored as this '
- 'is a Windows system.'.format(name)
- )
- group = user
- ret = {'changes': {},
- 'comment': '',
- 'name': name,
- 'result': True}
- if not create:
- if not os.path.isfile(name):
- # Don't create a file that is not already present
- ret['comment'] = ('File {0} is not present and is not set for '
- 'creation').format(name)
- return ret
- u_check = _check_user(user, group)
- if u_check:
- # The specified user or group do not exist
- return _error(ret, u_check)
- if not os.path.isabs(name):
- return _error(
- ret, 'Specified file {0} is not an absolute path'.format(name))
- if isinstance(env, salt._compat.string_types):
- msg = (
- 'Passing a salt environment should be done using \'saltenv\' not '
- '\'env\'. This warning will go away in Salt Boron and this '
- 'will be the default and expected behaviour. Please update your '
- 'state files.'
- )
- salt.utils.warn_until('Boron', msg)
- ret.setdefault('warnings', []).append(msg)
- # No need to set __env__ = env since that's done in the state machinery
- if os.path.isdir(name):
- ret['comment'] = 'Specified target {0} is a directory'.format(name)
- ret['result'] = False
- return ret
- if context is None:
- context = {}
- elif not isinstance(context, dict):
- return _error(
- ret, 'Context must be formed as a dict')
- if len(filter(None, [contents, contents_pillar, contents_grains])) > 1:
- return _error(
- ret, 'Only one of contents, contents_pillar, and contents_grains is permitted')
- # If contents_pillar was used, get the pillar data
- if contents_pillar:
- contents = __salt__['pillar.get'](contents_pillar)
- if contents_grains:
- contents = __salt__['grains.get'](contents_grains)
- if contents_newline:
- # Make sure file ends in newline
- if contents and not contents.endswith('\n'):
- contents += '\n'
- if not replace and os.path.exists(name):
- # Check and set the permissions if necessary
- ret, _ = __salt__['file.check_perms'](name, ret, user, group, mode, follow_symlinks)
- if __opts__['test']:
- ret['comment'] = 'File {0} not updated'.format(name)
- elif not ret['changes'] and ret['result']:
- ret['comment'] = ('File {0} exists with proper permissions. '
- 'No changes made.'.format(name))
- return ret
- if name in _ACCUMULATORS:
- if not context:
- context = {}
- context['accumulator'] = _ACCUMULATORS[name]
- try:
- if __opts__['test']:
- ret['result'], ret['comment'] = __salt__['file.check_managed'](
- name,
- source,
- source_hash,
- user,
- group,
- mode,
- template,
- context,
- defaults,
- __env__,
- contents,
- **kwargs
- )
- return ret
- # If the source is a list then find which file exists
- source, source_hash = __salt__['file.source_list'](
- source,
- source_hash,
- __env__
- )
- except CommandExecutionError as exc:
- ret['result'] = False
- ret['comment'] = 'Unable to manage file: {0}'.format(exc)
- return ret
- # Gather the source file from the server
- try:
- sfn, source_sum, comment_ = __salt__['file.get_managed'](
- name,
- template,
- source,
- source_hash,
- user,
- group,
- mode,
- __env__,
- context,
- defaults,
- **kwargs
- )
- except Exception as exc:
- ret['changes'] = {}
- log.debug(traceback.format_exc())
- return _error(ret, 'Unable to manage file: {0}'.format(exc))
- if check_cmd:
- tmp_filename = salt.utils.mkstemp()
- # if exists copy existing file to tmp to compare
- if __salt__['file.file_exists'](name):
- try:
- __salt__['file.copy'](name, tmp_filename)
- except Exception as exc:
- return _error(ret, 'Unable to copy file {0} to {1}: {2}'.format(name, tmp_filename, exc))
- try:
- ret = __salt__['file.manage_file'](
- tmp_filename,
- sfn,
- ret,
- source,
- source_sum,
- user,
- group,
- mode,
- __env__,
- backup,
- makedirs,
- template,
- show_diff,
- contents,
- dir_mode,
- follow_symlinks)
- except Exception as exc:
- ret['changes'] = {}
- log.debug(traceback.format_exc())
- return _error(ret, 'Unable to check_cmd file: {0}'.format(exc))
- # file being updated to verify using check_cmd
- if ret['changes']:
- # Reset ret
- ret = {'changes': {},
- 'comment': '',
- 'name': name,
- 'result': True}
- cret = mod_run_check_cmd(
- check_cmd, tmp_filename
- )
- if isinstance(cret, dict):
- ret.update(cret)
- return ret
- else:
- ret = {'changes': {},
- 'comment': '',
- 'name': name,
- 'result': True}
- if comment_ and contents is None:
- return _error(ret, comment_)
- else:
- try:
- return __salt__['file.manage_file'](
- name,
- sfn,
- ret,
- source,
- source_sum,
- user,
- group,
- mode,
- __env__,
- backup,
- makedirs,
- template,
- show_diff,
- contents,
- dir_mode,
- follow_symlinks)
- except Exception as exc:
- ret['changes'] = {}
- log.debug(traceback.format_exc())
- return _error(ret, 'Unable to manage file: {0}'.format(exc))
- def directory(name,
- user=None,
- group=None,
- recurse=None,
- dir_mode=None,
- file_mode=None,
- makedirs=False,
- clean=False,
- require=None,
- exclude_pat=None,
- follow_symlinks=False,
- **kwargs):
- '''
- Ensure that a named directory is present and has the right perms
- name
- The location to create or manage a directory
- user
- The user to own the directory; this defaults to the user salt is
- running as on the minion
- group
- The group ownership set for the directory; this defaults to the group
- salt is running as on the minion. On Windows, this is ignored
- recurse
- Enforce user/group ownership and mode of directory recursively. Accepts
- a list of strings representing what you would like to recurse.
- Example:
- .. code-block:: yaml
- /var/log/httpd:
- file.directory:
- - user: root
- - group: root
- - dir_mode: 755
- - file_mode: 644
- - recurse:
- - user
- - group
- - mode
- dir_mode / mode
- The permissions mode to set any directories created. Not supported on
- Windows
- file_mode
- The permissions mode to set any files created if 'mode' is ran in
- 'recurse'. This defaults to dir_mode. Not supported on Windows
- makedirs
- If the directory is located in a path without a parent directory, then
- the state will fail. If makedirs is set to True, then the parent
- directories will be created to facilitate the creation of the named
- file.
- clean
- Make sure that only files that are set up by salt and required by this
- function are kept. If this option is set then everything in this
- directory will be deleted unless it is required.
- require
- Require other resources such as packages or files
- exclude_pat
- When 'clean' is set to True, exclude this pattern from removal list
- and preserve in the destination.
- follow_symlinks : False
- If the desired path is a symlink (or ``recurse`` is defined and a
- symlink is encountered while recursing), follow it and check the
- permissions of the directory/file to which the symlink points.
- .. versionadded:: 2014.1.4
- '''
- # Remove trailing slash, if present
- if name[-1] == '/':
- name = name[:-1]
- user = _test_owner(kwargs, user=user)
- if salt.utils.is_windows():
- if group is not None:
- log.warning(
- 'The group argument for {0} has been ignored as this is '
- 'a Windows system.'.format(name)
- )
- group = user
- if 'mode' in kwargs and not dir_mode:
- dir_mode = kwargs.get('mode', [])
- if not file_mode:
- file_mode = dir_mode
- # Make sure that leading zeros stripped by YAML loader are added back
- dir_mode = __salt__['config.manage_mode'](dir_mode)
- file_mode = __salt__['config.manage_mode'](file_mode)
- ret = {'name': name,
- 'changes': {},
- 'result': True,
- 'comment': ''}
- u_check = _check_user(user, group)
- if u_check:
- # The specified user or group do not exist
- return _error(ret, u_check)
- if not os.path.isabs(name):
- return…
Large files files are truncated, but you can click here to view the full file