Create a symbolic link (symlink, soft link)

    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.
        An absolute path OR a basename file/directory name must be provided.
        The latter will be placed relative to the symlink destination's parent
        directory.

    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

    copy_target_user : False
        If True, set the symlink's owner to that of the target. If ``user`` is
        also passed, it will be used instead. This option requires the target
        to exist.

        .. versionadded:: 2019.2.0

    copy_target_group : False
        If True, set the symlink's group to that of the target. If ``group`` is
        also passed, it will be used instead. This option requires the target
        to exist.

        .. versionadded:: 2019.2.0

    mode
        The permissions to set on this file, aka 644, 0775, 4664. Not supported
        on Windows.

        The default mode for new files and directories corresponds umask of salt
        process. For existing files and directories it's not enforced.

    win_owner : None
        The owner of the symlink and directories if ``makedirs`` is True. If
        this is not passed, ``user`` will be used. If ``user`` is not passed,
        the account under which Salt is running will be used.

        .. versionadded:: 2017.7.7

    win_perms : None
        A dictionary containing permissions to grant

        .. versionadded:: 2017.7.7

    win_deny_perms : None
        A dictionary containing permissions to deny

        .. versionadded:: 2017.7.7

    win_inheritance : None
        True to inherit permissions from parent, otherwise False

        .. versionadded:: 2017.7.7

def symlink(
        name,
        target,
        force=False,
        backupname=None,
        makedirs=False,
        user=None,
        group=None,
        copy_target_user=False,
        copy_target_group=False,
        mode=None,
        win_owner=None,
        win_perms=None,
        win_deny_perms=None,
        win_inheritance=None,
        **kwargs):
    '''
    Create a symbolic link (symlink, soft link)

    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.
        An absolute path OR a basename file/directory name must be provided.
        The latter will be placed relative to the symlink destination's parent
        directory.

    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

    copy_target_user : False
        If True, set the symlink's owner to that of the target. If ``user`` is
        also passed, it will be used instead. This option requires the target
        to exist.

        .. versionadded:: 2019.2.0

    copy_target_group : False
        If True, set the symlink's group to that of the target. If ``group`` is
        also passed, it will be used instead. This option requires the target
        to exist.

        .. versionadded:: 2019.2.0

    mode
        The permissions to set on this file, aka 644, 0775, 4664. Not supported
        on Windows.

        The default mode for new files and directories corresponds umask of salt
        process. For existing files and directories it's not enforced.

    win_owner : None
        The owner of the symlink and directories if ``makedirs`` is True. If
        this is not passed, ``user`` will be used. If ``user`` is not passed,
        the account under which Salt is running will be used.

        .. versionadded:: 2017.7.7

    win_perms : None
        A dictionary containing permissions to grant

        .. versionadded:: 2017.7.7

    win_deny_perms : None
        A dictionary containing permissions to deny

        .. versionadded:: 2017.7.7

    win_inheritance : None
        True to inherit permissions from parent, otherwise False

        .. versionadded:: 2017.7.7
    '''
    name = os.path.expanduser(name)

    # Make sure that leading zeros stripped by YAML loader are added back
    mode = salt.utils.files.normalize_mode(mode)

    user = _test_owner(kwargs, user=user)
    ret = {'name': name,
           'changes': {},
           'result': True,
           'comment': ''}
    if not name:
        return _error(ret, 'Must provide name to file.symlink')

    if copy_target_user:
        if user:
            log.warning('`copy_target_user` and `user` are mutually exclusive. Using `user`.')
        else:
            try:
                user = __salt__['file.get_user'](target)
            except CommandExecutionError:
                ret['result'] = False
                ret['comment'] = '`copy_target_user` is set, but target `{0}` does not exist.'.format(target)
                return ret

    if user is None:
        user = __opts__['user']

    if salt.utils.platform.is_windows():

        # Make sure the user exists in Windows
        # Salt default is 'root'
        if not __salt__['user.info'](user):
            # User not found, use the account salt is running under
            # If username not found, use System
            user = __salt__['user.current']()
            if not user:
                user = 'SYSTEM'

        # If win_owner is not passed, use user
        if win_owner is None:
            win_owner = user if user else None

        # Group isn't relevant to Windows, use win_perms/win_deny_perms
        if group is not None:
            log.warning(
                'The group argument for %s has been ignored as this '
                'is a Windows system. Please use the `win_*` parameters to set '
                'permissions in Windows.', name
            )

        if copy_target_group is not None:
            log.warning(
                'The copy_target_group argument for {0} has been ignored as this '
                'is a Windows system. Please use the `win_*` parameters to set '
                'permissions in Windows.'.format(name)
            )

        group = user

    if copy_target_group:
        if group:
            log.warning('`copy_target_group` and `group` are mutually exclusive. Using `group`.')
        else:
            try:
                group = __salt__['file.get_group'](target)
            except CommandExecutionError:
                ret['result'] = False
                ret['comment'] = '`copy_target_group` is set, but target `{0}` does not exist.'.format(target)
                return ret

    if group is None:
        group = __salt__['file.gid_to_group'](
            __salt__['user.info'](user).get('gid', 0)
        )

    preflight_errors = []
    if salt.utils.platform.is_windows():
        # Make sure the passed owner exists
        try:
            salt.utils.win_functions.get_sid_from_name(win_owner)
        except CommandExecutionError as exc:
            preflight_errors.append('User {0} does not exist'.format(win_owner))

        # Make sure users passed in win_perms exist
        if win_perms:
            for name_check in win_perms:
                try:
                    salt.utils.win_functions.get_sid_from_name(name_check)
                except CommandExecutionError as exc:
                    preflight_errors.append('User {0} does not exist'.format(name_check))

        # Make sure users passed in win_deny_perms exist
        if win_deny_perms:
            for name_check in win_deny_perms:
                try:
                    salt.utils.win_functions.get_sid_from_name(name_check)
                except CommandExecutionError as exc:
                    preflight_errors.append('User {0} does not exist'.format(name_check))
    else:
        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)

    presult, pcomment, pchanges = _symlink_check(name,
                                                 target,
                                                 force,
                                                 user,
                                                 group,
                                                 win_owner)

    if not os.path.isdir(os.path.dirname(name)):
        if makedirs:
            if __opts__['test']:
                pcomment += '\n{0} will be created'.format(os.path.dirname(name))
            else:
                try:
                    _makedirs(name=name,
                              user=user,
                              group=group,
                              dir_mode=mode,
                              win_owner=win_owner,
                              win_perms=win_perms,
                              win_deny_perms=win_deny_perms,
                              win_inheritance=win_inheritance)
                except CommandExecutionError as exc:
                    return _error(ret, 'Drive {0} is not mapped'.format(exc.message))
        else:
            if __opts__['test']:
                pcomment += '\nDirectory {0} for symlink is not present' \
                            ''.format(os.path.dirname(name))
            else:
                return _error(
                    ret,
                    'Directory {0} for symlink is not present'.format(
                        os.path.dirname(name)
                    )
                )

    if __opts__['test']:
        ret['result'] = presult
        ret['comment'] = pcomment
        ret['changes'] = pchanges
        return ret

    if __salt__['file.is_link'](name):
        # The link exists, verify that it matches the target
        if os.path.normpath(__salt__['file.readlink'](name)) != os.path.normpath(target):
            # The target is wrong, delete the link
            os.remove(name)
        else:
            if _check_symlink_ownership(name, user, group, win_owner):
                # The link looks good!
                if salt.utils.platform.is_windows():
                    ret['comment'] = ('Symlink {0} is present and owned by {1}'
                                      ''.format(name, win_owner))
                else:
                    ret['comment'] = ('Symlink {0} is present and owned by '
                                      '{1}:{2}'.format(name, user, group))
            else:
                if _set_symlink_ownership(name, user, group, win_owner):
                    if salt.utils.platform.is_windows():
                        ret['comment'] = ('Set ownership of symlink {0} to '
                                          '{1}'.format(name, win_owner))
                        ret['changes']['ownership'] = win_owner
                    else:
                        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
                    if salt.utils.platform.is_windows():
                        ret['comment'] += (
                            'Failed to set ownership of symlink '
                            '{0} to {1}'.format(name, win_owner))
                    else:
                        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:
            if not os.path.isabs(backupname):
                if backupname == os.path.basename(backupname):
                    backupname = os.path.join(
                        os.path.dirname(os.path.normpath(name)),
                        backupname)
                else:
                    return _error(ret, (('Backupname must be an absolute path '
                                         'or a file name: {0}').format(backupname)))
            # Make a backup first
            if os.path.lexists(backupname):
                if not force:
                    return _error(ret, (('Symlink & backup dest exists and Force not set.'
                                         ' {0} -> {1} - backup: {2}').format(
                                             name, target, backupname)))
                else:
                    __salt__['file.remove'](backupname)
            try:
                __salt__['file.move'](name, backupname)
            except Exception as exc:
                ret['changes'] = {}
                log.debug(
                    'Encountered error renaming %s to %s',
                    name, backupname, exc_info=True
                )
                return _error(ret, ('Unable to rename {0} to backup {1} -> '
                                    ': {2}'.format(name, backupname, exc)))
        elif force:
            # Remove whatever is in the way
            if __salt__['file.is_link'](name):
                __salt__['file.remove'](name)
                ret['changes']['forced'] = 'Symlink was forcibly replaced'
            else:
                __salt__['file.remove'](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, win_owner):
            if not _set_symlink_ownership(name, user, group, win_owner):
                ret['result'] = False
                ret['comment'] += (', but was unable to set ownership to '
                                   '{0}:{1}'.format(user, group))
    return ret