MAIN_M4_SOURCES =
HOSTS =
+## Where to install the scripts.
+FIREWALL = /etc/init.d/firewall
+
+## How to achieve root privileges.
+ROOT = sudo
+
+## Throw additional scripts in here to have them installed.
SCRIPTS =
+sbindir = /usr/local/sbin
+## Establish the default target early, so that targets in `local.mk' don't
+## override it.
default: all
.PHONY: default
TARGETS = $(addsuffix .sh,$(HOSTS))
+###--------------------------------------------------------------------------
+### Prologue testing.
+
+TARGETS += dummy.sh
+dummy.sh: base.m4 prologue.m4 dummy-payload.m4
+ $(V_M4) $^ >$@.new && chmod +x $@.new && mv $@.new $@
+
+TARGETS += dummy-inst.sh
+dummy-inst.sh: dummy.sh
+ $(V_GEN)sed '/dummy_action=/s/lose/win/' $< >$@.new
+ $(V_AT)chmod +x $@.new && mv $@.new $@
+
###--------------------------------------------------------------------------
### Building.
clean:; rm -f $(TARGETS) *.new
.PHONY: clean
+###--------------------------------------------------------------------------
+### Installation.
+
+## The local machine doesn't want the complicated SSH stuff.
+THISHOST = $(shell hostname)
+
+## Testing.
+check: $(THISHOST).sh
+ $(ROOT) ./$(THISHOST).sh test
+
+## Installation on a local host,
+install/$(THISHOST): $(THISHOST).sh
+ [ "x$(SCRIPTS)" = x ] || $(ROOT) install -m755 $(SCRIPTS) $(sbindir)
+ $(ROOT) ./$(THISHOST).sh replace
+
+## Installation on a remote host.
+install/%: %.sh
+ if [ "x$(SCRIPTS)" != x ]; then \
+ for i in $(SCRIPTS); do \
+ $(ROOT) scp $$i root@$*:$(sbindir)/$$i.new && \
+ $(ROOT) ssh root@$* \
+ 'cd $(sbindir) && chmod 755 $$i.new && mv $$i.new $i' || \
+ exit 1; \
+ done; \
+ fi
+ $(ROOT) scp $*.sh root@$*:$(FIREWALL).new
+ $(ROOT) ssh root@$* $(FIREWALL) remote-prepare
+ $(ROOT) ssh root@$* $(FIREWALL) remote-commit
+ $(ROOT) ssh root@$* rm -f $(FIREWALL).new
+
+## General installation target.
+install: all install/$(THISHOST) $(addprefix install/,$(HOSTS))
+.PHONY: install install/$(THISHOST)
+
###----- That's all, folks --------------------------------------------------
--- /dev/null
+### -*-sh-*-
+###
+### Dummy payload for testing the prologue
+###
+### (c) 2011 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### 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 2 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, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+m4_divert(5)m4_dnl
+###--------------------------------------------------------------------------
+### Configuration.
+
+defconf(firewall_script, ./dummy-inst.sh)
+defconf(firewall_failsafe, ./dummy-failsafe.sh)
+defconf(dummy_action, lose)
+
+m4_divert(20)m4_dnl
+###--------------------------------------------------------------------------
+### Produce some output, and fail or succeed as instructed.
+
+case "$dummy_action" in
+ lose)
+ echo "dummy-payload (stdout): losing"
+ echo >&2 "dummy-payload (stderr): still losing"
+ false
+ ;;
+ win)
+ echo "dummy-payload (stdout): winning"
+ echo >&2 "dummy-payload (stderr): still winning"
+ true
+ ;;
+ *)
+ eval "$dummy_action"
+ ;;
+esac
+
+m4_divert(-1)
+###----- That's all, folks --------------------------------------------------
### Local configuration makefile.
+## Common configuration for all hosts.
MAIN_M4_SOURCES += local.m4
+## The avaiable hosts.
HOSTS += metalzone
HOSTS += vampire
-THISHOST = $(shell hostname)
-
-ROOT = sudo
-
-## Testing.
-check: $(THISHOST).sh
- firewall_script=./$(THISHOST).sh && \
- firewall_failsafe=/etc/init.d/firewall && \
- export firewall_script firewall_failsafe && \
- [ -x $$firewall_failsafe ] && \
- $(ROOT) ./$$firewall_script
-
-## Installation.
-install: all check
- for i in $(HOSTS); do \
- $(ROOT) scp $$i.sh $$i:/etc/init.d/firewall; \
- if [ "$(SCRIPTS)" ]; then \
- for j in $(SCRIPTS); do \
- $(ROOT) ssh $$i <$$j " \
- cd /usr/local/sbin && \
- rm -f $$j.new && \
- cat >$$j.new && \
- chmod 755 $$j.new && \
- mv $$j.new $$j"; \
- done; \
- fi; \
- done
### Failsafe prologue.
revert () {
- echo "$1! Retreating to safe version..."
- if ! "$firewall_failsafe" revert; then
+ escape=$1 badness=$2
+ ## Report a firewall script failure and retreat to a safe place.
+
+ echo "$2! Retreating to safe version..."
+ if ! "$1" revert; then
echo >&2 "Safe firewall failed. You're screwed. Good luck."
exit 1
fi
exit 0
}
+try () {
+ old=$1 new=$2
+ ## Install the NEW firewall rules. If it fails, revert to the OLD ones.
+ ## Updating firewall rules can fail spectacularly, so be careful. Leave a
+ ## timebomb in the form of NEW.errors: if this isn't removed in 10 seconds
+ ## after the NEW rules complete successfully, then revert. Write errors to
+ ## NEW.errors.
+
+ ## Make sure we have an escape route.
+ if [ ! -x "$old" ]; then
+ echo >&2 "$0: no escape plan: \`$old' is missing"
+ exit 1
+ fi
+
+ ## Clear the air and make the errors file.
+ rm -f "$new.errors" "$new.timebomb" "$new.grabbed"
+ exec >"$new.errors" 2>&1
+
+ ## Now try to install the new firewall.
+ "$new" install || revert "$old" "Failed"
+
+ ## Set up the time bomb. Leave the errors file there if we failed.
+ (sleep 10
+ if [ -f "$new.errors" ]; then
+ mv "$new.errors" "$new.timebomb"
+ revert "$old" "Time bomb"
+ fi)&
+}
+
+catch () {
+ new=$1
+ ## Report successful installation of the script.
+
+ if mv "$new.errors" "$new.grabbed" 2>/dev/null; then
+ rc=0
+ echo "Installed OK."
+ else
+ mv "$new.timebomb" "$new.grabbed"
+ echo "Timebomb went off."
+ rc=1
+ fi
+ cat "$new.grabbed" >&2
+ rm -f "$new.grabbed"
+ return $rc
+}
+
exit_after_clearing=:
export FWCOOKIE=magical
-case "${1-update}" in
- start | restart | reload | force-reload)
+case "$#,${1-update}" in
+ 1,start | 1,restart | 1,reload | 1,force-reload)
echo -n "Starting up firewall... "
- "$firewall_script" install || revert "Failed"
+ "$firewall_script" install || revert "$firewall_failsafe" "Failed"
finished
;;
- stop)
+ 1,stop)
echo -n "Shutting down firewall... "
exit_after_clearing=finished
;;
- update)
- echo -n "Installing new firewall... "
- "$firewall_script" install || revert "Failed"
- echo "Done."
+ 1,replace | 1,test)
+ echo -n "Running new firewall... "
+ if ! (try "$firewall_script" "$0"); then
+ echo "FAILED."
+ cat "$0.errors" >&2
+ exit
+ fi
+ echo "done."
echo "Can you hear me?"
- parent=$$
- (sleep 5; kill $parent; revert "Timeout")&
- child=$!
+ (trap 'exit 127' TERM
+ while :; do
+ if [ -f "$0.timebomb" ]; then
+ kill $$
+ echo "Timebomb went off!"
+ cat "$0.timebomb"
+ exit 1
+ fi
+ sleep 1
+ done)&
read answer
- kill $child
- case "$answer" in
- y* | Y*)
- echo "Cool. We're done here."
+ kill $!
+ catch "$0"
+ case "$1,$answer" in
+ replace,y* | replace,Y*)
+ install -m755 "$0" "$firewall_script"
+ echo "Cool. Firewall script replaced."
exit 0
;;
+ test,y* | test,Y*)
+ echo "Cool. Everything seems good."
+ exit 0
+ ;;
+ *)
+ revert "$firewall_script" "Bogus"
+ ;;
esac
- revert "Bogus"
;;
- install | revert)
+ 1,remote-prepare)
+ try "$firewall_script" "$0"
+ exit 0
+ ;;
+ 1,remote-commit)
+ catch "$0"
+ install -m755 "$0" "$firewall_script"
+ exit 0
+ ;;
+ 1,install | 1,revert)
;;
*)
- echo >&2 "Usage: firewall start|stop|reload|restart|force-reload|update|install|revert"
+ cat >&2 <<EOF
+Usage:
+ $0 start|stop|reload|restart|force-reload
+ $0 replace|test|remote-prepare|remote-commit
+EOF
exit 1
;;
esac