И, наконец, часть вторая, заключительная. Восстановить неправильные права в описанном выше случае, как оказалось, можно несколькими способами.
Первый способ (совсем простой и неинтересный).
Код: Выделить всё
# umask 022
# git checkout
# etckeeper init
Кстати, etckeeper из squeeze неправильно работает с пробелами в именах файлов (такие файлы создает, например, NM, если название подключения написано с пробелом). Вот патч из последующих версий etckeeper-а, который исправляет проблему:
Код: Выделить всё
--- a/etckeeper/init.d/20restore-etckeeper
+++ b/etckeeper/init.d/20restore-etckeeper
@@ -7,7 +7,7 @@ maybe () {
command="$1"
shift 1
- if eval [ -e "\$$#" ]; then
+ if eval [ -e "\"\$$#\"" ]; then
"$command" "$@"
fi
}
Второй способ (запутанный и сложный, зато интересный).
Воспользоваться скриптом, который, используя метаданные, сохраненные etckeeper-ом, восстановит метаданные для остальных файлов (которые должны были быть 644 или 755). Вот скрипт
Код: Выделить всё
#!/bin/sh
# Restore incorrect /etc permissions after `git checkout` if umask was not
# 022, using '.etckeeper' metadata save file.
#
# `etckeeper init` assumes, that `git checkout` sets permissions to either 644
# (no 'x') or to 755 ('x'), and, hence, does not remember mode for files with
# such modes. But this is true only, if umask was 022. Otherwise, some files,
# which were originally 644 or 755, can become something like 640 or 750
# (umask 027) after `git checkout`. `etckeeper init` can not restore mode for
# these files, because it simply does not save mode for them.
#
# However, for most cases etckeeper's metadata info is still enough to restore
# all modes correctly. Files, which are not in the '.etckeeper' have had mode
# either 644 or 755 originally. So, i just need to determine which one was
# executable and which one was not. I can do this by looking at executable
# bits unaffected by umask. In other words, i should find which (user, group
# or other) 'x' bit is not set in umask and check it in the file. If it is
# set, then `git` sets it and file was originally 755. Otherwise file was
# originally 644.
#
# But, unfortunately, this is not all. Any not remembered in '.etckeeper' file
# can be either tracked by vcs and had mode 644 or 755 originally, or
# _untracked_ by vcs and have any mode originally and now (its mode remains
# the same). These ignored/untracked files can have arbitrary modes, which
# may be accidently considered as "broken 644" or "broken 755" and
# overwritten. To avoid this i should obtain list of such files from vcs and
# exclude it as well.
# - Use '-f' to avoid interpreting special characters in filenames.
set -eufC
OIFS="$IFS"
readonly ret_success=0
readonly ret_error=1
readonly save_stdout=7
readonly save_pipe=9
readonly newline='
'
######
# This script works with git only.
VCS='git'
readonly etckeeper_meta='/etc/.etckeeper'
readonly etc_repository='/etc'
#####
# I'll use newline as IFS.
novcs="-wholename${newline}./.git${newline}-prune"
perm=''
# Files with 644 mode (no 'x' bit).
files=''
files_len=0
# Files with 755 mode.
x_files=''
x_files_len=0
# Files from etckeeper's metadata file with neither 644 nor 755 mode.
other_files=''
other_files_len=0
# Untracked by vcs files.
untracked_files=''
untracked_files_len=0
check_tmp_file()
{
# Generate temp file name and check, that file can be created, but do
# _not_ create. File name will have the form
#
# "<user_defined_file_name>-<md5_of_file_content>"
#
# If file exist, such name allow to check its content. If md5 will not
# match, then file probably created by someone else and this is error.
# File name returned on stdout.
# Args:
# 1 - file name (full path).
# 2 - md5 of file content (may be with filename, may be not - it'll be
# deleted anyway).
#
# Must _always_ be invoked in subshell.
if [ $# -lt 2 ]; then
return $ret_success
fi
local OIFS="$IFS"
local ret=''
local file_name="$1"
local file_md5="$(echo "$2" | cut -d' ' -f1)"
set -eufC
eval "exec $save_pipe>&1 1>&$save_stdout"
IFS="$newline"
if [ "x$file_name" = 'x' ]; then
return $ret_succes
fi
if [ ! -d "$(dirname "$file_name")" ]; then
echo "$0: check_tmp_file(): Directory(s) does not exist for file '$file_name'"
return $ret_error
fi
file_name="${file_name}-${file_md5}"
if [ -f "$file_name" ]; then
if [ "$(md5sum "$file_name" | cut -d' ' -f1)" != "$file_md5" ]; then
echo "$0: check_tmp_file(): File exist, but has unexpected content."
file_name=''
ret=$ret_error
fi
fi
eval "exec 1>&$save_pipe $save_pipe>&-"
echo -n "$file_name"
IFS="$OIFS"
return $ret
}
exclude_files()
{
# Exclude file names specified in arguments from list in stdin and write
# result to stdout.
# Args:
# 1.. - file names to exclude.
#
# Must _always_ be invoked in subshell.
local OIFS="$IFS"
# - Ensure, that '-e' grep option have arg (use ^\(.\)).
local add_e_opt_regex='s/^\(.\)/-e\1/p'
local grep_regex_args=''
local grep_cmd=''
local max_args=4
local excl_patterns_file='/tmp/restore_etc'
set -eufC
eval "exec $save_pipe>&1 1>&$save_stdout"
IFS="$newline"
excl_patterns_file="$(
check_tmp_file "$excl_patterns_file" "$(echo "$*" | md5sum)" || true
)"
if [ "x$excl_patterns_file" = 'x' ]; then
# FIXME: Exceed max number of arguments.
# - Can't join several patterns into one arg, because of `grep -F`.
# - Can't call grep several times, because i need apply all patterns
# to stdin (i.e. i need pipe).
# - Can't call grep from pipe in eval with fixed number of args,
# because this require escaping special characters in args.
# It seems, that file is the only good solution. But if it fails..
# - Omit empty args (sed -ne's///p').
grep_regex_args="$(echo "$*" | sed -ne"$add_e_opt_regex")"
else
grep_regex_args="-f${newline}$excl_patterns_file"
if [ ! -f "$excl_patterns_file" ]; then
echo "$*" > "$excl_patterns_file"
fi
fi
eval "exec 1>&$save_pipe $save_pipe>&-"
# - Use '-F' to avoid interpreting special characters in filenames.
# - Use '-x' to ensure correct matching.
grep -v -Fx ${grep_regex_args:--e ''}
IFS="$OIFS"
return $ret_success
}
set_mode()
{
# Set specified mode for files specified in arguments.
# Args:
# 1 - octal file mode.
# 2.. - files, which mode to set.
if [ $# -lt 2 ]; then
return $ret_success
fi
local OIFS="$IFS"
local perm="$1"
shift
if echo "$perm" | grep -q -e'[^[:digit:]]'; then
echo "$0: set_mode(): Incorrect mode '$perm'"
return $ret_error
fi
echo "Files which should be '$perm':"
echo "$*" \
| xargs -d'\n' stat -c '%a %n' \
| sed -ne"/^$perm /!s/[^ ]\+ //p" \
| xargs -d'\n' bash -c 'echo "## $#:"; for ((i = 1; i <= $#; i++)); do echo "$i: ${!i}"; done' bash $perm
# FIXME: Replace above line with
# | xargs -r -d'\n' chmod -c $perm
# for actual operation.
IFS="$OIFS"
return $ret_success
}
eval "exec $save_stdout>&1"
IFS='
'
# Set all 'x' bits in finds '-perm /ZZZ', which is not affected by umask. I
# need at least one unaffected by umask 'x' bit to continue.
perm="$(printf "%o" $(( ($(umask) & 0111) ^ 0111 )) )"
if [ "$perm" -eq '0' ]; then
echo "$0: Can't restore mode with umask '$(umask)'"
exit $ret_error
fi
perm="-perm${newline}/$perm"
cd "$etc_repository"
# Generate exclude file lists.
if [ -f "$etckeeper_meta" ]; then
# - Omit empty files (use '\(.\+\)').
other_files="$(
sed -ne"/^maybe chmod/s/^\([^ ]\+ \+\)\{3\}'\(.\+\)'/\2/p" "$etckeeper_meta"
)"
other_files_len="$(echo "$other_files" | wc -l)"
fi
if [ "$VCS" = 'git' ]; then
untracked_files="$(git ls-files -o | sed -e's:^:\./:')"
else
echo "$0: Can't generate untracked files list"
fi
# Generate 644 and 755 mode file lists.
files="$(
find . $novcs -o -type f -not $perm -print \
| exclude_files ${other_files:-} ${untracked_files:-}
)"
x_files="$(
find . $novcs -o -type f $perm -print \
| exclude_files ${other_files:-} ${untracked_files:-}
)"
files_len="$(echo "$files" | wc -l)"
x_files_len="$(echo "$x_files" | wc -l)"
echo "644 files = $files_len"
echo "755 files = $x_files_len"
echo "other files = $other_files_len"
# Use '-d\n' for separating arguments by newline instead of blanks (default),
# and for interpreting all characters literally (disable escape characters).
set_mode 644 "$files"
set_mode 755 "$x_files"
eval "exec $save_stdout>&-"
IFS="$OIFS"
Третий способ (самый надежный, наверное -).
Запускаете где-то на неиспорченном /etc
Код: Выделить всё
# find -type f | xargs -d'\n' stat -c '%a %n' >./ref_mode.txt
а затем проверяете свой /etc как-то так
Код: Выделить всё
$ cat ./ref_mode.txt | xargs -d'\n' -L1 bash -c 'if [ ! -f "${1#* }" ]; then exit 0; fi; if [ "$(stat -c "%a %n" "${1#* }")" != "$1" ]; then echo "Mode not match for \"$1\""; fi' bash
Вот, собственно, и все.
PS. Если что, продолжение обсуждения здесь
bugs.debian.org/649701 -)