#!/bin/bash
###############################################################################
#                                                                             #
# IPFire.org - A linux based firewall                                         #
# Copyright (C) 2010  Michael Tremer & Christian Schmidt                      #
#                                                                             #
# This program is free software: you can redistribute it and/or modify        #
# it under the terms of the GNU General Public License as published by        #
# the Free Software Foundation, either version 3 of the License, or           #
# (at your option) any later version.                                         #
#                                                                             #
# This program is distributed in the hope that it will be useful,             #
# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
# GNU General Public License for more details.                                #
#                                                                             #
# You should have received a copy of the GNU General Public License           #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
#                                                                             #
###############################################################################

. /usr/lib/network/header-zone

HOOK_MANPAGE="network-zone-bridge"

HOOK_SETTINGS="HOOK STP STP_FORWARD_DELAY STP_HELLO STP_MAXAGE STP_MODE"
HOOK_SETTINGS="${HOOK_SETTINGS} STP_PRIORITY MAC MTU"

HOOK_PORT_SETTINGS="COST PRIORITY"

# Default values
MAC=""
MTU=1500
STP="on"
STP_MODE="rstp"
STP_FORWARD_DELAY=0
STP_HELLO=2
STP_MAXAGE=20
STP_PRIORITY=512

hook_check_settings() {
	assert ismac MAC
	assert isbool STP
	assert isoneof STP_MODE stp rstp
	assert isinteger STP_HELLO
	assert isinteger STP_FORWARD_DELAY
	assert isinteger STP_PRIORITY
	assert isinteger MTU
}

hook_parse_cmdline() {
	while [ $# -gt 0 ]; do
		case "${1}" in
			--stp=*)
				STP=${1#--stp=}
				;;
			--stp-mode=*)
				STP_MODE=${1#--stp-mode=}
				;;
			--stp-hello=*)
				STP_HELLO=${1#--stp-hello=}
				;;
			--stp-forward-delay=*)
				STP_FORWARD_DELAY=${1#--stp-forward-delay=}
				;;
			--stp-priority=*)
				STP_PRIORITY=${1#--stp-priority=}
				;;
			--mtu=*)
				MTU=${1#--mtu=}
				;;
			--mac=*)
				MAC=${1#--mac=}
				;;
			*)
				warning "Ignoring unknown option '${1}'"
				;;
		esac
		shift
	done

	# Generate a random MAC address if the user passed no one
	isset MAC || MAC="$(mac_generate)"
}

hook_up() {
	local zone=${1}
	assert isset zone

	zone_settings_read "${zone}"

	# Create the bridge if it does not already exist.
	if ! device_exists "${zone}"; then
		bridge_create "${zone}" \
			--address="${MAC}" \
			--mtu="${MTU}"
	fi

	# Enable STP
	if enabled STP; then
		stp_enable "${zone}"

		if isset STP_FORWARD_DELAY; then
			stp_bridge_set_forward_delay "${zone}" "${STP_FORWARD_DELAY}"
		fi

		if isset STP_HELLO; then
			stp_bridge_set_hello_time "${zone}" "${STP_HELLO}"
		fi

		if isset STP_MAXAGE; then
			stp_bridge_set_max_age "${zone}" "${STP_MAXAGE}"
		fi

		if isset STP_PRIORITY; then
			stp_bridge_set_priority "${zone}" "${STP_PRIORITY}"
		fi
	else
		stp_disable "${zone}"
	fi

	device_set_up "${zone}"

	# XXX Currently, there is a bug (in the linux kernel?) that we need to
	# set our bridges to promisc mode.
	device_set_promisc "${zone}" on

	# Bring up all ports
	zone_ports_create "${zone}"
	zone_ports_up "${zone}"

	# Bring up all configurations
	zone_configs_up "${zone}"

	exit ${EXIT_OK}
}

hook_hotplug() {
	local zone="${1}"
	assert isset zone

	case "$(hotplug_action)" in
		add)
			# Handle ports of this zone that have just been added
			if hotplug_event_interface_is_port_of_zone "${zone}"; then
				# Bring up the zone if it is enabled but not active, yet.
				if zone_is_enabled "${zone}" && ! zone_is_active "${zone}"; then
					zone_start "${zone}"
				fi

				hook_port_up "${zone}" "${INTERFACE}"
			fi
			;;
		remove)
			# Handle ports of this zone that have just been removed
			if hotplug_event_interface_is_port_of_zone "${zone}"; then
				hook_port_down "${zone}" "${INTERFACE}"
			fi
			;;
		*)
			exit ${EXIT_NOT_HANDLED}
			;;
	esac

	exit ${EXIT_OK}
}

hook_down() {
	local zone="${1}"
	assert isset zone

	if ! device_is_up "${zone}"; then
		warning "Zone '${zone}' is not up"
		exit ${EXIT_OK}
	fi

	# Stop all the configs.
	zone_configs_down "${zone}"

	# Bring down all the ports.
	zone_ports_down "${zone}"
	zone_ports_remove "${zone}"

	# Remove the bridge.
	device_set_down "${zone}"
	bridge_delete "${zone}"

	exit ${EXIT_OK}
}

hook_status() {
	local zone="${1}"
	assert isset zone

	# Print the default header.
	cli_device_headline "${zone}"

	# Exit if zone is down
	if ! zone_is_up "${zone}"; then
		echo # Empty line
		exit ${EXIT_ERROR}
	fi

	cli_headline 2 "Spanning Tree Protocol information"
	if stp_is_enabled "${zone}"; then
		local proto=$(stp_bridge_get_protocol ${zone})

		cli_print_fmt1 2 "Version"	"$(stp_get_name ${proto})"
		cli_print_fmt1 2 "ID"		"$(stp_bridge_get_id ${zone})"
		cli_print_fmt1 2 "Priority"	"$(stp_bridge_get_priority ${zone})"

		if stp_bridge_is_root ${zone}; then
			cli_print 2 "This bridge is root."
		else
			cli_print_fmt1 2 "Designated root" \
				"$(stp_bridge_get_designated_root ${zone})"
			cli_print_fmt1 2 "Root path cost" \
				"$(stp_bridge_get_root_path_cost ${zone})"
		fi
		cli_space

		# Topology information
		cli_print_fmt1 2 "Topology changing" \
			"$(stp_bridge_get_topology_change_detected ${zone})"
		cli_print_fmt1 2 "Topology change time" \
			"$(beautify_time $(stp_bridge_get_topology_change_timer ${zone}))"
		cli_print_fmt1 2 "Topology change count" \
			"$(stp_bridge_get_topology_change_count ${zone})"
		cli_space
	else
		cli_print 2 "Disabled"
		cli_space
	fi

	cli_headline 2 "Ports"
	zone_ports_status "${zone}"
	cli_space

	cli_headline 2 "Configurations"
	zone_configs_cmd status "${zone}"
	cli_space

	exit ${EXIT_OK}
}

hook_check_port_settings() {
	if isset COST; then
		assert isinteger COST
	fi

	if isset PRIORITY; then
		assert isinteger PRIORITY
	fi
}

hook_port_attach() {
	# Excepting at least two arguments here
	assert [ $# -ge 2 ]

	local zone="${1}"
	local port="${2}"
	shift 2

	if zone_has_port "${zone}" "${port}"; then
		zone_port_settings_read "${zone}" "${port}"
	fi

	local arg
	local val
	while read arg; do
		case "${arg}" in
			--cost=*)
				COST="$(cli_get_val "${arg}")"
				;;
			--priority=*)
				PRIORITY="$(cli_get_val "${arg}")"
				;;
		esac
	done <<< "$(args $@)"

	if ! zone_port_settings_write "${zone}" "${port}"; then
		exit ${EXIT_ERROR}
	fi

	exit ${EXIT_OK}
}

hook_port_detach() {
	assert [ $# -eq 2 ]

	local zone="${1}"
	local port="${2}"

	# Shut down the port (if possible)
	port_down "${port}"

	if ! zone_port_settings_remove "${zone}" "${port}"; then
		exit ${EXIT_ERROR}
	fi

	exit ${EXIT_OK}
}

hook_port_edit() {
	hook_port_attach $@
}

hook_port_up() {
	assert [ $# -eq 2 ]

	local zone="${1}"
	local port="${2}"

	# Try bringing up the port if it has not been
	# brought up before.
	# We will get here as soon as the port device has
	# been created and will then connect it with the bridge.
	if ! device_exists "${port}"; then
		port_up "${port}"

		exit ${EXIT_OK}
	fi

	# Read configuration values
	zone_port_settings_read "${zone}" "${port}" ${HOOK_PORT_SETTINGS}

	# Attach the port to the bridge
	bridge_attach_device "${zone}" "${port}"

	# Set STP configuration
	if isset COST; then
		stp_port_set_cost "${zone}" "${port}" "${COST}"
	fi

	# TODO Apply priority (#10609)

	# Make sure that the port is up
	port_up "${port}"

	exit ${EXIT_OK}
}

hook_port_down() {
	assert [ $# -eq 2 ]

	local zone="${1}"
	local port="${2}"

	if device_exists "${port}"; then
		bridge_detach_device "${zone}" "${port}"

		port_down "${port}"
	fi

	exit ${EXIT_OK}
}

hook_port_status() {
	assert [ $# -eq 2 ]

	local zone="${1}"
	local port="${2}"

	# Do nothing for devices which are not up and running.
	device_exists "${port}" || exit ${EXIT_OK}

	local status

	# Check if the device is down.
	if ! device_is_up "${port}"; then
		status="${MSG_DEVICE_STATUS_DOWN}"

	# Check if the device has no carrier.
	elif ! device_has_carrier "${port}"; then
		status="${MSG_DEVICE_STATUS_NOCARRIER}"

	# Check for STP information.
	elif stp_is_enabled "${zone}"; then
		local state="$(stp_port_get_state "${zone}" "${port}")"
		state="MSG_STP_${state}"
		status="${!state}"

		status="${status} - DSR: $(stp_port_get_designated_root "${zone}" "${port}")"
		status="${status} - Cost: $(stp_port_get_cost "${zone}" "${port}")"
	else
		status="${MSG_DEVICE_STATUS_UP}"
	fi
	cli_statusline 3 "${port}" "${status}"

	exit ${EXIT_OK}
}
