#!/bin/sh

#common part
po_domain="alterator-mass-management"
alterator_api_version=1

. alterator-sh-functions
. shell-quote
. shell-config

BASE_DIR=/var/lib/alterator-mass-management
ANSIBLE_CONFIG=$BASE_DIR/ansible.cfg
ANSIBLE_HOSTS=$BASE_DIR/hosts
ANSIBLE_USER="_alterator_mass_management_pull"
HOST_KEYS=/etc/openssh/authorized_keys/"$ANSIBLE_USER"
TASKS_DIR=$BASE_DIR/tasks
COMMON_TASKS_DIR=/usr/share/alterator-mass-management/common-ansible-tasks
LOGS_DIR=/var/log/alterator-mass-management/
KEYS_DIR=$BASE_DIR/keys
MODE_CONFIG=$BASE_DIR/amm-pull.cfg
GIT_REPO=$TASKS_DIR/git
CLIENT_CRON=/etc/cron.d/ansible-pull

# Unfortunately we can't use mktemp: there are no way
# to delete temporary file  after export
TMP_HOSTS_FILE="/var/lib/alterator-mass-management/tmp_hosts_export.list"

TASKS_SERIAL="$TASKS_DIR"/serial

# task status codes:
TASK_CODE_DONE=0
TASK_CODE_UNKNOWN=1
TASK_CODE_NEW=2
TASK_CODE_FAILED=3
TASK_CODE_AWAITING=4

#turn off auto expansion
set -f

# Common helpers
var_is_digit()
{
	local n="$1"; shift

	[ -n "${n##*[!0-9]*}" ]
}

check_profile_name()
{
	[ -n "$1" -a "$1" != '#f' ]
}
###

# Profiles
write_ansible_task()
{
	local profile="$1"; shift
	local cmd="$1"; shift

	check_profile_name "$profile" && [ -n "$cmd" ] || return

	local f="$TASKS_DIR/roles/$profile/tasks/main.yml"
	[ -s "$f" ] || echo '---' >"$f"

	echo "- include: $COMMON_TASKS_DIR/$cmd.yml role=$profile" >>"$f"
}

is_ansible_task_present()
{
	local profile="$1"; shift
	local cmd="$1"; shift

	check_profile_name "$profile" && [ -n "$cmd" ] || return 1

	grep -qs "^- include: .*/$cmd\.yml" "$TASKS_DIR/roles/$profile/tasks/main.yml"
}

ansible_task_list()
{
	local profile="$1"; shift

	check_profile_name "$profile" || return

	sed -r -n 's;^- include: .*/(.+)\.yml role=.*$;\1;p' "$TASKS_DIR/roles/$profile/tasks/main.yml"
}

write_action_var()
{
	local profile="$1"; shift
	local variable="$1"; shift
	local value="$1"; shift
	local action="${variable%%_*}"

	check_profile_name "$profile" && [ -n "$action" -a -n "$variable" ] || return

	shell_config_set "$TASKS_DIR/roles/$profile/files/${action}_data" "$variable" "$value"
}

read_action_var()
{
	local profile="$1"; shift
	local variable="$1"; shift
	local action="${variable%%_*}"

	check_profile_name "$profile" && [ -n "$action" -a -n "$variable" ] || return

	shell_config_get "$TASKS_DIR/roles/$profile/files/${action}_data" "$variable"
}

add_action_list_var()
{
	local profile="$1"; shift
	local variable="$1"; shift
	local value="$1"; shift
	local list= i=

	[ -n "$value" ] || return

	list="$(read_action_var "$profile" "$variable")"
	for i in $list; do
		if [ "$i" = "$value" ]; then
			write_error "`_ "$i was added to list already"`"
			return
		fi
	done

	list="$list${list:+ }$value"
	write_action_var "$profile" "$variable" "$list"
}

del_action_list_var()
{
	local profile="$1"; shift
	local variable="$1"; shift
	local value="$1"; shift
	local list= i=

	[ -n "$value" ] || return

	for i in $(read_action_var "$profile" "$variable"); do
		[ "$i" != "$value" ] || continue
		list="$list${list:+ }$i"
	done

	write_action_var "$profile" "$variable" "$list"
}
list_profiles()
{
	find "$TASKS_DIR/roles/" -mindepth 1 -maxdepth 1 -type d -printf '%f\n'
}

add_profile()
{
	local profile="$1"; shift

	check_profile_name "$profile" || return
	if [ -e "$TASKS_DIR/roles/$profile" ]; then
		write_error "`_ "Profile $profile already exists"`"
		return
	fi
	mkdir -p "$TASKS_DIR/roles/$profile/files/"
	mkdir -p "$TASKS_DIR/roles/$profile/tasks/"
}

del_profile()
{
	local profile="$1"; shift

	check_profile_name "$profile" || return

	rm -r -- "$TASKS_DIR/roles/$profile"
}

_read_action_status()
{
	local profile="$1"; shift
	local action="$1"; shift
	local change= status=

	is_ansible_task_present "$profile" "$action" &&
		change=true || change=false
	write_bool_param "${action}_change" "$change"

	test_bool "$(read_action_var "$profile" "${action}_status")" &&
		status=true || status=false
	write_bool_param "${action}_status" "$status"
}

read_profile()
{
	local profile="$1"; shift

	check_profile_name "$profile" || return

	# neteth
	_read_action_status "$profile" neteth

	# routes
	_read_action_status "$profile" routes

	# dhcpd
	_read_action_status "$profile" dhcpd

	write_string_param dhcpd_iface_name "$(read_action_var "$profile" dhcpd_iface_name)"
	write_string_param dhcpd_ip_start "$(read_action_var "$profile" dhcpd_ip_start)"
	write_string_param dhcpd_ip_end "$(read_action_var "$profile" dhcpd_ip_end)"
	write_string_param dhcpd_client_time "$(read_action_var "$profile" dhcpd_client_time)"
	write_string_param dhcpd_client_dns "$(read_action_var "$profile" dhcpd_client_dns)"
	write_string_param dhcpd_client_search "$(read_action_var "$profile" dhcpd_client_search)"
	write_string_param dhcpd_client_gw "$(read_action_var "$profile" dhcpd_client_gw)"

	# firewall
	local def_firewall_table="filter/INPUT"

	_read_action_status "$profile" firewall

	write_string_param firewall_tables "$def_firewall_table"
	read_firewall_rules "$profile" "$def_firewall_table"

	# timesync
	_read_action_status "$profile" timesync

	write_string_param timesync_ntp_pool "$(read_action_var "$profile" timesync_ntp_pool)"

}

write_profile()
{
	local profile="$1"; shift

	check_profile_name "$profile" || return

	rm -f -- "$TASKS_DIR/roles/$profile/tasks/main.yml"

	# neteth
	if test_bool "$in_neteth_change" && [ -n "$in_neteth_iface" ]; then
		write_ansible_task "$profile" neteth
		write_neteth_iface "$profile" "$in_neteth_iface" "$in_neteth_iface_configuration" \
			"$in_neteth_iface_default_gw" "$in_neteth_iface_dns" "$in_neteth_iface_search"
	fi

	# routes
	local routes_file="$TASKS_DIR/roles/$profile/files/routes.list"
	if test_bool "$in_routes_change"; then
		write_ansible_task "$profile" routes
		[ -s "$routes_file" ] || touch "$routes_file"
	else
		rm -f -- "$routes_file"
	fi

	# dhcpd
	if test_bool "$in_dhcpd_change"; then
		write_ansible_task "$profile" dhcpd
		write_action_var "$profile" dhcpd_status "${in_dhcpd_status:-#f}"
		write_action_var "$profile" dhcpd_iface_name "$in_dhcpd_iface_name"
		write_action_var "$profile" dhcpd_ip_start "$in_dhcpd_ip_start"
		write_action_var "$profile" dhcpd_ip_end "$in_dhcpd_ip_end"
		write_action_var "$profile" dhcpd_client_time "$in_dhcpd_client_time"
		write_action_var "$profile" dhcpd_client_dns "$in_dhcpd_client_dns"
		write_action_var "$profile" dhcpd_client_search "$in_dhcpd_client_search"
		write_action_var "$profile" dhcpd_client_gw "$in_dhcpd_client_gw"
	fi

	# firewall
	if test_bool "$in_firewall_change"; then
		write_ansible_task "$profile" firewall
		write_action_var "$profile" firewall_status "${in_firewall_status:-#f}"
	fi

	# timesync
	if test_bool "$in_timesync_change"; then
		write_ansible_task "$profile" timesync
		write_action_var "$profile" timesync_status "${in_timesync_status:-#f}"
		write_action_var "$profile" timesync_ntp_pool "$in_timesync_ntp_pool"
	fi
}

# Profile routes
add_profile_route()
{
	local profile="$1"; shift
	local iface="$1"; shift
	local route_src="$1"; shift
	local route_src_mask="$1"; shift
	local route_dst_ip="$1"; shift
	local route_dst_iface="$1"; shift
	local route_metric="$1"; shift
	local route_str=

	check_profile_name "$profile" || return

	if [ -z "$iface" ]; then
		 write_error "`_ "Invalid interface"`"
		 return
	fi

	if [ -z "$route_src" -o -z "$route_src_mask" ]; then
		 write_error "`_ "Invalid source address $route_src/$route_src_mask"`"
		 return
	fi

	route_str="$iface $route_src/$route_src_mask"

	[ -z "$route_dst_ip" ] || route_str="$route_str via $route_dst_ip"
	[ -z "$route_dst_iface" ] || route_str="$route_str dev $route_dst_iface"
	[ -z "$route_metric" ] || route_str="$route_str metric $route_metric"

	echo "$route_str" >>"$TASKS_DIR/roles/$profile/files/routes.list"
}

del_profile_route()
{
	local profile="$1"; shift
	local route_str=
	local f="$TASKS_DIR/roles/$profile/files/routes.list"

	quote_sed_regexp_variable route_str "$1"

	check_profile_name "$profile" && [ -n "$route_str" -a -s "$f" ] || return

	sed -i "/$route_str/d" "$f"
}

list_profile_routes()
{
	local profile="$1"; shift
	local f="$TASKS_DIR/roles/$profile/files/routes.list"

	check_profile_name "$profile" && [ -s "$f" ] || return

	while read line; do
		write_enum_item "$line" "$line"
	done <"$f"
}

# Network interfaces
list_neteth_ifaces()
{
	local profile="$1"; shift
	local i=

	check_profile_name "$profile" || return
	for i in $(read_action_var "$profile" neteth_ifaces_list); do
		write_enum_item "$i" "$i"
	done
}

neteth_add_iface()
{
	local profile="$1"; shift
	local iface="$1"; shift

	check_profile_name "$profile" || return

	if [ -z "$iface" ]; then
		 write_error "`_ "Invalid interface"`"
		 return
	fi

	add_action_list_var "$profile" neteth_ifaces_list "$iface"
}

neteth_del_iface()
{
	local profile="$1"; shift
	local iface="$1"; shift
	local iface_quoted=

	check_profile_name "$profile" || return

	if [ -z "$iface" ]; then
		 write_error "`_ "Invalid interface"`"
		 return
	fi

	del_action_list_var "$profile" neteth_ifaces_list "$iface"
	quote_sed_regexp_variable iface_quoted "$iface"
	sed -i "/^neteth_[[:alpha:]_]\+_$iface=/d" "$TASKS_DIR/roles/$profile/files/neteth_data"
}

readonly _neteth_iface_vars="configuration default_gw dns search"

write_neteth_iface()
{
	local profile="$1"; shift
	local iface="$1"; shift
	local configuration="$1"; shift
	local default_gw="$1"; shift
	local dns="$1"; shift
	local search="$1"; shift
	local var= v=

	check_profile_name "$profile" || return

	if [ -z "$iface" ]; then
		 write_error "`_ "Invalid interface"`"
		 return
	fi

	for var in $_neteth_iface_vars; do
		eval "v=\"\$$var\""
		write_action_var "$profile" "neteth_iface_${var}_${iface}" "$v"
	done
}

read_neteth_iface()
{
	local profile="$1"; shift
	local iface="$1"; shift
	local var= tmp=

	check_profile_name "$profile" || return

	if [ -z "$iface" ]; then
		tmp="$(read_action_var "$profile" neteth_ifaces_list)"
		iface="${tmp%% *}"
	fi

	write_string_param neteth_iface "$iface"
	for var in $_neteth_iface_vars; do
		write_string_param "neteth_iface_${var}" "$(read_action_var "$profile" "neteth_iface_${var}_${iface}")"
	done
}

add_neteth_ip()
{
	local profile="$1"; shift
	local iface="$1"; shift
	local ip="$1"; shift
	local mask="$1"; shift

	check_profile_name "$profile" || return

	if [ -z "$iface" ]; then
		 write_error "`_ "Invalid interface"`"
		 return
	fi

	if [ -z "$ip" -o -z "$mask" ]; then
		 write_error "`_ "Invalid address $ip/$mask"`"
		 return
	fi

	add_action_list_var "$profile" "neteth_iface_addresses_${iface}" "$ip/$mask"
}

del_neteth_ip()
{
	local profile="$1"; shift
	local iface="$1"; shift
	local ip_str="$1"; shift

	check_profile_name "$profile" || return

	if [ -z "$iface" ]; then
		 write_error "`_ "Invalid interface"`"
		 return
	fi

	del_action_list_var "$profile" "neteth_iface_addresses_${iface}" "$ip_str"
}

list_neteth_addresses()
{
	local profile="$1"; shift
	local iface="$1"; shift
	local a=

	[ -n "$iface" ] || return

	for a in $(read_action_var "$profile" "neteth_iface_addresses_${iface}"); do
		write_enum_item "$a" "$a"
	done
}

# firewall
write_firewall_rules()
{
	local profile="$1"; shift
	local firewall_table="$1"; shift
	local firewall_rules_text="$1"; shift

	check_profile_name "$profile" && [ -n "$firewall_table" ] || return

	mkdir -p "$TASKS_DIR/roles/$profile/files/firewall-rules/${firewall_table%/*}"
	echo "$firewall_rules_text" >"$TASKS_DIR/roles/$profile/files/firewall-rules/$firewall_table"
}

read_firewall_rules()
{
	local profile="$1"; shift
	local firewall_table="$1"; shift
	local f="$TASKS_DIR/roles/$profile/files/firewall-rules/$firewall_table"
	local text=

	check_profile_name "$profile" && [ -n "$firewall_table" ] || return

	if [ -s "$f" ]; then
		text="$(cat "$f")"
	fi

	write_string_param firewall_rules_text "$text"
}
###

### Task
write_serial()
{
	local name="$1"; shift
	local value="$1"; shift

	[ -n "$name" ] && var_is_digit "$value" || return

	echo "$value" >"$TASKS_DIR/${name}_serial"
}

read_serial()
{
	local name="$1"; shift

	[ -n "$name" ] || return

	[ -s "$TASKS_DIR/${name}_serial" ] || write_serial "$name" 1
	egrep -o '^[[:digit:]]+$' "$TASKS_DIR/${name}_serial"
}

read_task_group()
{
	local number="$1"; shift

	var_is_digit "$number" && [ -s "$TASKS_DIR/task-$number.yml" ] || return

	sed -r -n 's;^-[[:blank:]]+hosts:[[:blank:]]+(.+)$;\1;p' "$TASKS_DIR/task-$number.yml"
}

read_task_profile()
{
	local number="$1"; shift

	var_is_digit "$number" && [ -s "$TASKS_DIR/task-$number.yml" ] || return

	sed -r -n '/^[[:blank:]]+roles:/,+1{s;^[[:blank:]]+-[[:blank:]]+(.+)$;\1;p}' "$TASKS_DIR/task-$number.yml"
}

get_git_task_number()
{
	local group="$1"; shift

	is_pull_group "$group" &&
		[ -d "$GIT_REPO/$group/" ] || return 1

	cd "$GIT_REPO/$group/" >/dev/null 2>&1
	git describe --exact-match 2>/dev/null | sed -r 's;^task-([[:digit:]]+)\.[[:digit:]]+$;\1;'
	cd - >/dev/null 2>&1
}

read_new_task()
{
	local number=

	number="$(read_serial tasks)"
	if ! var_is_digit "$number"; then
		write_error "`_ "Couldn't determine task number"`"
		return
	fi

	write_string_param new_task_number "$number"

	write_string_param new_task_group "$(read_task_group "$number")"
	write_string_param new_task_profile "$(read_task_profile "$number")"
}

write_new_task()
{
	local number="$1"; shift
	local group="$1"; shift
	local profile="$1"; shift

	var_is_digit "$number" &&
		check_profile_name "$profile" &&
		[ -n "$group" -a -d "$TASKS_DIR/roles/$profile" ] || return 1

	if is_pull_group "$group" && [ -e "$GIT_REPO/$group/" ]; then
		local git_task_number="$(get_git_task_number "$group")"
		if [ $git_task_number -ne $number -a \
				$(read_task_status_code "$git_task_number") -ne $TASK_CODE_DONE ]; then
			write_error "`_ "Pull task for group $group already exists (task #$git_task_number)"`"
			return 1
		fi
	fi

	mkdir -p "$TASKS_DIR/task-$number"
	cat >"$TASKS_DIR/task-$number.yml" <<EOF
---
- hosts: $group
  pre_tasks:
    - file: path=/tmp/amm-task-{{ task }}.{{ try }} state=directory
  roles:
    - $profile
  post_tasks:
    - file: path=/tmp/amm-task-{{ task }}.{{ try }} state=absent
EOF

	return 0
}

__parse_recap_eregexp='[[:blank:]]+:'\
'[[:blank:]]+ok=([[:digit:]]+)'\
'[[:blank:]]+changed=([[:digit:]]+)'\
'[[:blank:]]+unreachable=([[:digit:]]+)'\
'[[:blank:]]+failed=([[:digit:]]+)'\
'[[:blank:]]*'

ansible_rawlog_section()
{
	local logfile="$1"; shift
	local begin="$1"; shift
	local end="${1-^$}"

	sed -n -r "/$begin/,/$end/p" "$logfile"
}

raw_to_db()
{
	local number="$1"; shift
	local try="$1"; shift
	local logfile="${1:-$LOGS_DIR/task-$number.$try.raw.log}"
	local profile="$(read_task_profile "$number")"
	local t=

	mkdir -p "$TASKS_DIR/task-$number/$try"
	list_hosts "$(read_task_group "$number")" | while read host; do
		local host_quoted=
		quote_sed_regexp_variable host_quoted "$host"
		if [ -s "$TASKS_DIR/task-$number/$try/hosts_skiplist" ] &&
				grep -qs "^$host_quoted$" "$TASKS_DIR/task-$number/$try/hosts_skiplist"; then
			continue
		fi
		mkdir -p "$TASKS_DIR/task-$number/$try/$host"
		ansible_rawlog_section "$logfile" '^PLAY RECAP \**[[:blank:]]*$' '' |
			sed -r -n "s;^$host_quoted$__parse_recap_eregexp$;ok=\1\nchanged=\2\nunreachable=\3\nfailed=\4\n;p" >"$TASKS_DIR/task-$number/$try/$host/stat"
	done

	for t in $(ansible_task_list "$profile"); do
		ansible_rawlog_section "$logfile" "^TASK: \[$profile \| $t\] " | egrep '^[^[:blank:]]+: ' |\
			sed 's;\\r\\n;;g' |\
			while read line; do
				local b="${line%%:*}" curr_host= vars=

				case "$b" in
					ok|changed|unreachable|failed)
						curr_host="${line#$b: \[}"
						curr_host="${curr_host%%\]*}"
						if [ -z "$curr_host" -o ! -d "$TASKS_DIR/task-$number/$try/$curr_host/" ]; then
							echo "WARNING: unknown host $curr_host" 1>&2
							continue
						fi
						echo "result=$b" >>"$TASKS_DIR/task-$number/$try/$curr_host/$t"
						vars="${line#$b: \[$curr_host\] => \{}"
						vars="${line%\}*}"
						(IFS=',' v=
						for v in $vars; do
							v="${v#*\"}"; echo "${v%%\": *}=${v#*: }" >>"$TASKS_DIR/task-$number/$try/$curr_host/$t"
						done)
						;;
					stderr|stdout|msg)
						if [ -z "$curr_host" -o ! -d "$TASKS_DIR/task-$number/$try/$curr_host/" ]; then
							echo "WARNING: unknown host $curr_host" 1>&2
							continue
						fi
						echo "$b=\"${line#$b: }\"">>"$TASKS_DIR/task-$number/$try/$curr_host/$t"
						;;
					TASK|FATAL) continue
						;;
					*)
						echo "WARNING: unknown token $b" 1>&2
						;;
				esac
			done
	done
}

read_task_date()
{
	local number="$1"; shift
	local try="$(read_serial "task-$number/try")"
	local logfile=

	var_is_digit "$number" && var_is_digit "$try" || return

	try=$(($try-1))

	logfile="$LOGS_DIR/task-$number.$try.raw.log"
	if [ ! -e "$logfile" ]; then
		# Just find a first of per-host pull-task's logs
		logfile="$(find "$LOGS_DIR/" -mindepth 1 -maxdepth 1 -name "*-task-$number.$try.raw.log" -print -quit)"
		[ -n "$logfile" -a -e "$logfile" ] ||
			logfile="$TASKS_DIR/task-$number/$try/task_status"
	fi

	date '+%d.%m.%Y %H:%M' -r "$logfile" 2>/dev/null
}

read_task_status_code()
{
	local number="$1"; shift
	local try="$(read_serial "task-$number/try")"
	local stat_file=
	local cache_task_stat_file=
	local status=

	if ! var_is_digit "$number" || ! var_is_digit "$try"; then
		echo "$TASK_CODE_UNKNOWN"
		return
	fi

	try=$(($try-1))
	if [ $try -eq 0 ]; then
		echo "$TASK_CODE_NEW"
		return
	fi

	cache_task_stat_file="$TASKS_DIR/task-$number/$try/task_status"

	if [ -d "$TASKS_DIR/task-$number/$try/" ]; then
		# Read cache if exist
		if [ -s "$cache_task_stat_file" ]; then
			cat "$cache_task_stat_file"
			return
		fi

		for stat_file in $(find "$TASKS_DIR/task-$number/$try/" -mindepth 2 -maxdepth 2 -name stat); do
			if [ ! -s "$stat_file" ] ||
					! grep -qs '^unreachable=0$' "$stat_file" ||
					! grep -qs '^failed=0$' "$stat_file"; then
				status="$TASK_CODE_FAILED"
				break
			fi
		done
	fi

	if [ -z "$status" ]; then
		if [ -n "$stat_file" ]; then
			status="$TASK_CODE_DONE"
		else
			status="$TASK_CODE_UNKNOWN"
		fi
	fi

	echo "$status" >"$cache_task_stat_file"
	echo "$status"
}

list_tasks()
{
	local number=
	local profile=
	local group=
	local result=
	local stat_code=

	for number in $(find "$TASKS_DIR/" -mindepth 1 -maxdepth 1 -name 'task-*.yml' -printf '%f\n' |\
			sed -r -n 's;^task-([[:digit:]]+)\.yml$;\1;p' | sort -n); do
		stat_code="$(read_task_status_code "$number")"
		date="$(read_task_date "$number")"
		profile="$(read_task_profile "$number")"
		group="$(read_task_group "$number")"

		case "$stat_code" in
			$TASK_CODE_DONE)
				result="`_ 'DONE'`"
				;;
			$TASK_CODE_UNKNOWN)
				result="`_ 'UNKNOWN'`"
				;;
			$TASK_CODE_NEW)
				result="`_ 'NEW'`"
				;;
			$TASK_CODE_FAILED)
				result="`_ 'FAILED'`"
				;;
			$TASK_CODE_AWAITING)
				result="`_ 'AWAITING'`"
				;;
			default)
				write_error "`_ "Unknown task code: $stat_code"`"
				return
				;;
		esac

		write_table_item \
			name "$number" \
			task_num "$number" \
			task_date "$date" \
			task_profile "$profile" \
			task_group "$group" \
			task_result "$result"
	done

}

read_can_start()
{
	local number="$1"; shift

	case "$(read_task_status_code "$number")" in
		"$TASK_CODE_DONE"|"$TASK_CODE_AWAITING")
			write_bool_param can_start no
			;;
		*)
			write_bool_param can_start yes
			;;
	esac
}

get_db_num_var()
{
	local var="$1"; shift
	local file="$1"; shift
	local def="${1-}"
	local v=

	v="$(shell_config_get "$file" "$var")"

	var_is_digit "$v" || v="$def"

	echo $v
}

list_tries()
{
	local number="$1"; shift
	local tries="$(read_serial "task-$number/try")"
	local try=
	local stat_file=
	local actions_list=

	var_is_digit "$number" && var_is_digit "$tries" || return

	tries=$(($tries-1))

	actions_list="$(ansible_task_list "$(read_task_profile "$number")")"

	for try in $(seq 1 $tries); do
		local unreachable=0 failed=0 skipped=0 success=0 tmp= action= action_failed=
		for stat_file in $(find "$TASKS_DIR/task-$number/$try/" -mindepth 2 -maxdepth 2 -type f -name stat); do
			if [ -s "$stat_file" ]; then
				tmp=$(get_db_num_var unreachable "$stat_file" 0)
				if [ $tmp -gt 0 ]; then
					unreachable=$(($unreachable + 1))
					continue
				fi

				action_failed=
				for action in $actions_list; do
					tmp="$(shell_config_get "${stat_file%/stat}/$action" result)"
					case "$tmp" in
						ok|changed)
							;;
						*)
							action_failed=1
							break
							;;
					esac
				done

				if [ -n "$action_failed" ]; then
					failed=$(($failed + 1))
				else
					success=$(($success + 1))
				fi
			fi
		done

		if [ -s "$TASKS_DIR/task-$number/$try/hosts_skiplist" ]; then
			skipped="$(wc -l "$TASKS_DIR/task-$number/$try/hosts_skiplist" | cut -f1 -d' ')"
		fi

		write_table_item \
			name "$try" \
			try_num "$try" \
			try_failed "$failed" \
			try_unreachable "$unreachable" \
			try_skipped "$skipped" \
			try_success "$success"
	done
}

list_hosts_details()
{
	local number="$1"; shift
	local try="$1"; shift
	local status= stat_file=

	var_is_digit "$number" && var_is_digit "$try" || return

	for host in $(list_hosts "$(read_task_group "$number")"); do
		stat_file="$TASKS_DIR/task-$number/$try/$host/stat"
		status="`_ 'unknown'`"
		if [ -s "$stat_file" ]; then
			if [ $(get_db_num_var unreachable "$stat_file") -gt 0 ]; then
				status="`_ 'unreachable'`"
			elif [ $(get_db_num_var failed "$stat_file") -gt 0 ]; then
				status="`_ 'failed'`"
			else
				local action= tmp= unknown_result=
				for action in $(ansible_task_list "$(read_task_profile "$number")"); do
					tmp="$(shell_config_get "$TASKS_DIR/task-$number/$try/$host/$action" result)"
					case "$tmp" in
						ok|changed)
							;;
						*)
							echo "$action: Unknown result '$tmp'" 1>&2
							unknown_result=1
							break
							;;
					esac
				done

				[ -n "$unknown_result" ] || status='ok'
			fi
		elif [ -s "$TASKS_DIR/task-$number/$try/hosts_skiplist" ]; then
			local host_quoted=
			quote_sed_regexp_variable host_quoted "$host"
			grep -qs "^$host_quoted$" "$TASKS_DIR/task-$number/$try/hosts_skiplist" &&
				status="`_ 'skipped'`"
		fi

		write_table_item \
			name "$host" \
			hosts_host "$host" \
			hosts_status "$status"
	done
}

list_actions_details()
{
	local number="$1"; shift
	local try="$1"; shift
	local host="$1"; shift
	local file=
	local action= rc=
	local result="`_ 'unknown'`"

	var_is_digit "$number" && var_is_digit "$try" && [ -n "$host" ] || return

	for action in $(ansible_task_list "$(read_task_profile "$number")"); do
		file="$TASKS_DIR/task-$number/$try/$host/$action"
		if [ -s "$file" ]; then
			rc="$(get_db_num_var rc "$file")"
			if [ -n "$rc" -a $rc -eq 0 ]; then
				result='ok'
			else
				result="`_ 'failed'`"
			fi
		elif [ -s "$TASKS_DIR/task-$number/$try/hosts_skiplist" ]; then
			local host_quoted=
			quote_sed_regexp_variable host_quoted "$host"
			grep -qs "^$host_quoted$" "$TASKS_DIR/task-$number/$try/hosts_skiplist" &&
				result="`_ 'skipped'`"
		fi

		write_table_item \
			name "$action" \
			actions_action "$action" \
			actions_result "$result"
	done
}

start_task()
{
	local number="$1"; shift
	local serial="$(read_serial tasks)"
	local try="$(read_serial "task-$number/try")"
	local limit=

	var_is_digit "$number" && var_is_digit "$serial" && var_is_digit "$try" || return

	if [ $serial -eq $number ]; then
		write_serial tasks $(($number+1))
	fi
	write_serial "task-$number/try" $(($try+1))
	if [ -s "$TASKS_DIR/task-$number.retry" ]; then
		local tmp_hosts="$TASKS_DIR/task-$number/$try/hosts_group.tmp"
		local tmp_retry="$TASKS_DIR/task-$number/$try/hosts_retry.tmp"
		mkdir -p "$TASKS_DIR/task-$number/$try"
		list_hosts "$(read_task_group "$number")" | sort >"$tmp_hosts"
		sort "$TASKS_DIR/task-$number.retry" >"$tmp_retry"
		comm -13 "$tmp_retry" "$tmp_hosts" >>"$TASKS_DIR/task-$number/$try/hosts_skiplist"
		rm -- "$tmp_hosts" "$tmp_retry"
		limit="--limit @$TASKS_DIR/task-$number.retry"
	fi
	ANSIBLE_CONFIG="$ANSIBLE_CONFIG" ansible-playbook -v $limit -e "task=$number try=$try" "$TASKS_DIR/task-$number.yml" >"$LOGS_DIR/task-$number.$try.raw.log"
	raw_to_db "$number" "$try"
}

start_new_pull_task()
{
	local number="$1"; shift
	local group="$1"; shift
	local profile="$1"; shift
	local serial="$(read_serial tasks)"
	local try="$(read_serial "task-$number/try")"
	local limit=

	var_is_digit "$number" &&
		check_profile_name "$profile" &&
		[ -n "$group" -a -d "$TASKS_DIR/roles/$profile" ] &&
		var_is_digit "$serial" &&
		var_is_digit "$try" || return


	if [ $serial -eq $number ]; then
		write_serial tasks $(($number+1))
	fi
	write_serial "task-$number/try" $(($try+1))

	# Delete old task if it exists
	rm -rf -- "$GIT_REPO/$group/roles/"
	find "$GIT_REPO/$group/" -type f -name '*.yml' -delete

	mkdir -p "$GIT_REPO/$group/roles"
	cp -r "$TASKS_DIR/roles/$profile/" "$GIT_REPO/$group/roles"
	cp "$TASKS_DIR/task-$number.yml" "$GIT_REPO/$group/"
	cat >"$GIT_REPO/$group/local.yml" <<EOF
---
- include: task-$number.yml
  vars:
    task: $number
    try: 1
EOF

	chown -R "$ANSIBLE_USER:$ANSIBLE_USER" "$GIT_REPO/"

	cd "$GIT_REPO/$group/" >/dev/null 2>&1
	if [ ! -d "$GIT_REPO/$group/.git" ]; then
		git init >/dev/null 2>&1
	fi
	git add . >/dev/null 2>&1
	GIT_AUTHOR_NAME="Alterator" GIT_AUTHOR_EMAIL="alterator@localhost" \
		git commit -m "task $number, try 1" >/dev/null 2>&1
	GIT_AUTHOR_NAME="Alterator" GIT_AUTHOR_EMAIL="alterator@localhost" \
		git tag -a -m "task $number, try 1" "task-$number.1" >/dev/null 2>&1
	cd - >/dev/null 2>&1
	mkdir -p "$TASKS_DIR/task-$number/1/"
	echo "$TASK_CODE_AWAITING" >"$TASKS_DIR/task-$number/1/task_status"
}

restart_pull_task()
{
	local number="$1"; shift
	local group="$1"; shift
	local try="$(read_serial "task-$number/try")"

	var_is_digit "$number" &&
		var_is_digit "$try" &&
		[ -n "$group" -a -d "$GIT_REPO/$group" ] || return

	local git_task_number="$(get_git_task_number "$group")"
	if ! var_is_digit "$git_task_number" || [ $git_task_number -ne $number ]; then
		write_error "`_ "Invalid task number"`"
		return 1
	fi

	write_serial "task-$number/try" $(($try+1))

	cd "$GIT_REPO/$group" >/dev/null 2>&1
	sed -r -i "s/try: [[:digit:]]+/try: $try/" local.yml
	git add local.yml >/dev/null 2>&1
	GIT_AUTHOR_NAME="Alterator" GIT_AUTHOR_EMAIL="alterator@localhost" \
		git commit -m "task $number, try $try" >/dev/null 2>&1
	GIT_AUTHOR_NAME="Alterator" GIT_AUTHOR_EMAIL="alterator@localhost" \
		git tag -a -m "task $number, try $try" "task-$number.$try" >/dev/null 2>&1
	cd - >/dev/null 2>&1
	mkdir -p "$TASKS_DIR/task-$number/$try/"
	echo "$TASK_CODE_AWAITING" >"$TASKS_DIR/task-$number/$try/task_status"
}

check_pull_tasks()
{
	local raw_log= f= tt= task= try= hostname=
	for raw_log in $(find "$GIT_REPO" -type f -name '*.raw.log'); do
		f="${raw_log##*/}"
		f="${f%.raw.log}"
		tt="${f##*-task-}"
		task="${tt%%.*}"
		try="${tt##*.}"

		raw_to_db "$task" "$try" "$raw_log"

		mv "$raw_log" "$LOGS_DIR/"
		rm -f -- "$TASKS_DIR/task-$task/$try/task_status"
	done
}
###

# Hosts
list_groups()
{
	[ -s "$ANSIBLE_HOSTS" ] && sed -r -n 's;^\[([[:alnum:]_-]+)\][[:blank:]]*$;\1;p' "$ANSIBLE_HOSTS"
	[ -s "$HOST_KEYS" ] && sed -r -n 's;^#\[([[:alnum:]_-]+)\][[:blank:]]*$;\1;p' "$HOST_KEYS" | sed -r "s/^(.+)$/\1 \1 `_ "(indirect access)"`/"
}

is_pull_group()
{
	local group="$1"; shift

	[ -n "$group" ] || return 1
	quote_sed_regexp_variable group "$group"
	if [ -s "$HOST_KEYS" ] && grep -qs "^#\[$group\]$" "$HOST_KEYS"; then
		return 0
	fi

	return 1
}

list_hosts()
{
	local group="$1"; shift
	local full="$1"; shift

	[ -z "$group" ] && return 0
	if is_pull_group "$group"; then
		# Pull group
		if [ "$full" = "yes" ]; then 
			sed -n -r "/^#\[$group\]/,/^#\[/{/^[^#[]/p}" "$HOST_KEYS"
		else
			sed -n -r "/^#\[$group\]/,/^#\[/{/^[^#[]/p}" "$HOST_KEYS" | cut -f3 -d' '
		fi
	else
		# Ordinary group
		[ -s "$ANSIBLE_HOSTS" ] || return
		sed -n -r "/^\[$group\]/,/^\[/{/^[^[]/p}" "$ANSIBLE_HOSTS"
	fi
}

add_group()
{
	local group="$1"; shift
	local pull_group="$1"; shift

	if echo "$group" | grep -qsv "^[[:alnum:]_-]\+$"; then
		write_error "`_ "Bad name of new group. Name must contains latin letters, numbers, symbols _ and -."`"
		return
	fi

	# Check group name
	if list_groups | grep -qs "^$group[[:space:]]\?$"; then
		write_error "$(printf "`_ "Group %s already exists"`" "$group")"
		return
	fi

	# Create empty group
	if [ "$pull_group" = "#t" ]; then
		echo "#[$group]" >>"$HOST_KEYS"
	else
		echo "[$group]" >>"$ANSIBLE_HOSTS"
	fi
}

del_group()
{
	local group="$1"; shift

	[ -n "$group" ] || return

	quote_sed_regexp_variable group "$group"
	sed -r -i "/^\[$group\]/,/^\[/{/^(\[$group\]|[^[])/d}" "$ANSIBLE_HOSTS"
	sed -r -i "/^#\[$group\]/,/^#\[/{/^(#\[$group\]|[^#[])/d}"  "$HOST_KEYS"
}

add_host()
{
	local group="$1"; shift
	local host=
	local key=
	local host_quoted=

	[ -n "$group" -a -n "$1" ] || return

	if is_pull_group "$group"; then
		key="$(cat "$1")"
		host="${key##* }"
		if [ -z "$host" ]; then
			write_error "`_ "Invalid SSH key"`"
			return
		fi
	else
		host="$1"; shift
	fi

	quote_sed_regexp_variable host_quoted "$host"
	if list_hosts "$group" | grep -qs "^$host_quoted$"; then
		write_error "`_ "Host $host already present in the group $group"`"
		return
	fi

	if is_pull_group "$group"; then
		sed -r -i "/^#\[$group\]/a $key" "$HOST_KEYS"
	else
		sed -r -i "/^\[$group\]/a $host" "$ANSIBLE_HOSTS"
	fi
}

add_hosts_from_file()
{
	local group="$1"; shift
	local file="$1"; shift
	local host=

	[ -n "$group" -a -n "$file" -a -s "$file" ] || return

	cat "$file" | tr -d '\r' | while read host; do
		# Ignore already added host
		if is_pull_group "$group"; then
			quote_sed_regexp_variable host_quoted "${host/* }"
		else
			quote_sed_regexp_variable host_quoted "$host"
		fi
		list_hosts "$group" | grep -qs "^$host_quoted[[:space:]]*.*$" || add_host "$group" "$host"
	done
}

del_host()
{
	local group="$1"; shift
	local host="$1"; shift

	[ -n "$group" -a -n "$host" ] || return

	quote_sed_regexp_variable host "$host"

	if is_pull_group "$group"; then
		sed -i "/^#\[$group\]/,/^#\[/{/^.* $host$/d}" "$HOST_KEYS"
	else
		sed -i "/^\[$group\]/,/^\[/{/^$host$/d}" "$ANSIBLE_HOSTS"
	fi
}
###

# Server mode
get_mode()
{
	local mode="$(shell_config_get "$MODE_CONFIG" mode)"

	[ -n "$mode" ] || mode="servant"

	echo "$mode"
}

read_mode()
{
	# Default mode
	write_string_param mode "$(get_mode)"

	# Other values
	write_string_param master_server "$(shell_config_get "$MODE_CONFIG" master_server)"
	write_string_param master_group "$(shell_config_get "$MODE_CONFIG" master_group)"
	write_string_param interval "$(shell_config_get "$MODE_CONFIG" interval)"

}

write_mode()
{
	local mode="$1";            shift
	local master_server="$1";   shift
	local master_group="$1";    shift
	local interval="$1"; shift

	if [ "$mode" = "master" ]; then
		# Setup access to SSH
		if [ ! -e "$HOST_KEYS" ]; then
			install -Dm0600 /dev/null "$HOST_KEYS"
			chown "$ANSIBLE_USER":"$ANSIBLE_USER" "$HOST_KEYS"
		fi
	else
	  if [ "$mode" = "slave" ]; then
		# Setup environment (on client)
		if [ "$(shell_config_get "$MODE_CONFIG" mode)" != "$mode" -o \
				"$(shell_config_get "$MODE_CONFIG" master_server)" != "$master_server" -o \
				"$(shell_config_get "$MODE_CONFIG" master_group)" != "$master_group" ]; then
			rm -rf -- "$GIT_REPO/"
		fi
		mkdir -p "$GIT_REPO"

		# Install crontab (on client)
		if [ -n "$master_server" ]; then
			local cron_period=""
			local server_group="$master_group"
			local dt="*"
			local ht="*"
			local mt="*"

			# Generate some parameters
			test -n "$interval" || interval=15
			d=$[interval/1400]
			h=$[(interval-d*1400)/60]
			m=$[interval-d*1400-h*60]

			[ $d -gt 0 ] && dt="*/$d"
			if [ $h -gt 0 ]; then
				if [ $d -gt 0 ]; then
					ht="$h"
				else
					ht="*/$h"
				fi
			fi
			if [ $m -gt 0 ]; then
				if [ $d -gt 0 -o $h -gt 0 ]; then
					mt="$m"
				else
					mt="*/$m"
				fi
			fi

			cron_period="$mt $ht $dt"


			del_group "$server_group"
			add_group "$server_group" '#f'
			add_host "$server_group" "$(hostname)"

			rm -f "$CLIENT_CRON"
			for group in $server_group; do
				printf '%s * *\troot\t/usr/lib/alterator-mass-management/scripts/amm-pull-cronjob "%s@%s:%s/%s" "%s" &>/dev/null\n' \
					"$cron_period" \
					"$ANSIBLE_USER" \
					"$master_server" \
					"$GIT_REPO" \
					"$server_group" \
					"$GIT_REPO" \
					>> "$CLIENT_CRON"
			done
		fi

		# Configure access to server (on client)
		if [ -n "$master_server" ]; then
			quote_sed_regexp_variable host_quoted "$master_server"
			test -d /root/.ssh || mkdir -p /root/.ssh
			grep -qs "^$host_quoted " /root/.ssh/known_hosts || ssh-keyscan "$master_server" >> /root/.ssh/known_hosts 2>/dev/null
		fi
	  fi
	fi

	# Save configuration of rule
	[ -e "$MODE_CONFIG" ] || touch "$MODE_CONFIG"

	shell_config_set "$MODE_CONFIG" mode "$mode"
	shell_config_set "$MODE_CONFIG" master_server "$master_server"
	shell_config_set "$MODE_CONFIG" master_group  "$master_group"
	shell_config_set "$MODE_CONFIG" interval "$interval"
}

###

on_message()
{
	date  >> /tmp/mode.log
	echo "$(set|grep -a "in_")" >> /tmp/mode.log
	case "$in_action" in
	    type)
			write_type_item hostlist hostname-list
			#write_type_item add_host hostname
			;;
	    list)
			case "${in__objects##*/}" in
				avail_profiles)
					list_profiles | write_enum
					;;
				avail_profile_routes)
					list_profile_routes "$in_profile"
					;;
				avail_neteth_ifaces)
					list_neteth_ifaces "$in_profile"
					;;
				avail_neteth_addresses)
					list_neteth_addresses "$in_profile" "$in_neteth_iface"
					;;
				avail_groups)
					list_groups | write_enum
					;;
				avail_hosts)
					list_hosts "$in_group" | write_enum
					;;
				tasks)
					check_pull_tasks
					list_tasks
					;;
				tries)
					list_tries "$in_task"
					;;
				hosts_details)
					list_hosts_details "$in_task" "$in_try"
					;;
				actions_details)
					list_actions_details "$in_task" "$in_try" "$in_host"
					;;
			esac
			;;
	    read)
			case "${in__objects##*/}" in
				profile)
					read_profile "$in_profile"
					;;
				firewall_rules)
					read_firewall_rules "$in_profile" "$in_firewall_table"
					;;
				export_firewall)
					if check_profile_name "$in_profile" && [ -n "$in_table" ]; then
						write_string_param firewall_rules "$TASKS_DIR/roles/$in_profile/files/firewall-rules/$in_table"
					fi
					;;
				neteth_iface)
					read_neteth_iface "$in_profile" "$in_neteth_iface"
					;;
				new_task)
					read_new_task
					;;
				task_can_start)
					read_can_start "$in_task"
					;;
				server_key)
					if [ ! -s "$KEYS_DIR/id_rsa" -o ! -s "$KEYS_DIR/id_rsa.pub" ]; then
						rm -f -- "$KEYS_DIR/id_rsa" "$KEYS_DIR/id_rsa.pub"
						yes no 2>/dev/null | ssh-keygen -q -t rsa -b 2048 \
							               -f "$KEYS_DIR/id_rsa" -N '' \
							               -C "$(hostname)"
					fi
					write_string_param pub_key "$KEYS_DIR/id_rsa.pub"
					;;
				export_hosts)
					local full=""
					if [ -n "$in_group" ]; then
						is_pull_group "$in_group" && full="yes"
						list_hosts "$in_group" $full >"$TMP_HOSTS_FILE"
						write_string_param hosts_list "$TMP_HOSTS_FILE"
					fi
					;;
				mode)
					read_mode
					;;
				is_pull_group)
					is_pull_group "$in_group" && write_bool_param pull_group yes ||
						write_bool_param pull_group no
					;;
			esac
		    ;;
	    write)
			case "${in__objects##*/}" in
				add_profile)
					add_profile "$in_new_profile"
					;;
				del_profile)
					del_profile "$in_profile"
					;;
				add_profile_route)
					add_profile_route "$in_profile" "$in_route_iface_name" "$in_route_src" "$in_route_src_mask" \
						"$in_route_dst_ip" "$in_route_dst_iface" "$in_route_metric"
					;;
				del_profile_route)
					del_profile_route "$in_profile" "$in_routes_list"
					;;
				add_neteth_iface)
					neteth_add_iface "$in_profile" "$in_neteth_iface"
					;;
				del_neteth_iface)
					neteth_del_iface "$in_profile" "$in_neteth_iface"
					;;
				add_neteth_ip)
					add_neteth_ip "$in_profile" "$in_neteth_iface" "$in_neteth_addip" "$in_neteth_mask"
					;;
				del_neteth_ip)
					del_neteth_ip "$in_profile" "$in_neteth_iface" "$in_neteth_delip"
					;;
				neteth_iface)
					write_neteth_iface "$in_profile" "$in_neteth_iface" "$in_neteth_iface_configuration" \
						"$in_neteth_iface_default_gw" "$in_neteth_iface_dns" "$in_neteth_iface_search"
					;;
				firewall_rules)
					write_firewall_rules "$in_profile" "$in_firewall_table" "$in_firewall_rules_text"
					;;
				import_firewall)
					if check_profile_name "$in_profile" && [ -n "$in_table" -a -s "$in_import_firewall_rules" ]; then
						mkdir -p "$TASKS_DIR/roles/$in_profile/files/firewall-rules/${in_table%%/*}"
						cp -f "$in_import_firewall_rules" "$TASKS_DIR/roles/$in_profile/files/firewall-rules/$in_table"
						sed -i 's;\r;;' "$TASKS_DIR/roles/$in_profile/files/firewall-rules/$in_table"
					fi
					;;
				add_group)
					add_group "$in_group" "$in_pull_group"
					;;
				del_group)
					del_group "$in_group"
					;;
				add_host)
					add_host "$in_group" "$in_add_host"
					;;
				add_key)
					add_host "$in_group" "$in_host_key_file"
					;;
				del_host)
					del_host "$in_group" "$in_del_host"
					;;
				import_hosts)
					add_hosts_from_file "$in_group" "$in_import_hosts_file"
					;;
				mode)
					write_mode "$in_mode" "$in_master_server" "$in_master_group" "$in_interval"
					;;
				profile)
					write_profile "$in_profilelist"
					;;
				new_task)
					if [ "$(get_mode)" = master ]; then
						if write_new_task "$in_new_task_number" "$in_new_task_group" "$in_new_task_profile"; then
							if is_pull_group "$in_new_task_group"; then
								start_new_pull_task "$in_new_task_number" "$in_new_task_group" "$in_new_task_profile"
							else
								start_task "$in_new_task_number"
							fi
						fi
					else
						write_error "`_ "Can't create task in current mode"`"
					fi
					;;
				task)
					if [ "$(get_mode)" = master ]; then
						local group="$(read_task_group "$in_task")"
						if is_pull_group "$group"; then
							restart_pull_task "$in_task" "$group"
						else
							start_task "$in_task"
						fi
					else
						write_error "`_ "Can't create task in current mode"`"
					fi
					;;
			esac
		    ;;
            *)
                    ;;
	esac
}
message_loop
