From eb851ae32ee068dd368a8cb6db9b04e526316643 Mon Sep 17 00:00:00 2001 From: Sergey Poznyakoff Date: Mon, 18 May 2015 14:33:22 +0300 Subject: New sub-package: dgd --- Makefile | 4 +- dgd/GNUmakefile | 2 + dgd/MANIFEST | 5 + dgd/Makefile.PL | 38 +++ dgd/dgd | 843 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dgd/rc.uplink | 117 ++++++++ 6 files changed, 1007 insertions(+), 2 deletions(-) create mode 100644 dgd/GNUmakefile create mode 100644 dgd/MANIFEST create mode 100644 dgd/Makefile.PL create mode 100755 dgd/dgd create mode 100755 dgd/rc.uplink diff --git a/Makefile b/Makefile index 36cc766..333fb41 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # This file is part of dnstools -# Copyright (C) 2012, 2014 Sergey Poznyakoff +# Copyright (C) 2012, 2014, 2015 Sergey Poznyakoff # # 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 @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -SUBDIRS = dnsdbck rpsl2acl axfr2acl vhostcname renewck whoseip nsdbimport +SUBDIRS = dnsdbck rpsl2acl axfr2acl vhostcname renewck whoseip nsdbimport dgd include Make.vars diff --git a/dgd/GNUmakefile b/dgd/GNUmakefile new file mode 100644 index 0000000..e61b3c3 --- /dev/null +++ b/dgd/GNUmakefile @@ -0,0 +1,2 @@ +include ../Make.vars +include ../Make.rules diff --git a/dgd/MANIFEST b/dgd/MANIFEST new file mode 100644 index 0000000..d0f3868 --- /dev/null +++ b/dgd/MANIFEST @@ -0,0 +1,5 @@ +MANIFEST +Makefile.PL +inc/ExtUtils/AutoInstall.pm +dgd +rc.uplink diff --git a/dgd/Makefile.PL b/dgd/Makefile.PL new file mode 100644 index 0000000..0f418f2 --- /dev/null +++ b/dgd/Makefile.PL @@ -0,0 +1,38 @@ +# -*- perl -*- +# Copyright (C) 2015 Sergey Poznyakoff +# +# 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, 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 . + +use lib 'inc'; +use ExtUtils::AutoInstall ( + -core => [ + 'Getopt::Long' => 2.34, + 'Pod::Usage' => 1.51, + 'Pod::Man' => 2.25, + 'Net::Ping' => 2.38, + 'Proc::Daemon' => 0.19, + 'Data::Dumper' => 2.135 + ] +); + +# See lib/ExtUtils/MakeMaker.pm for details of how to influence +# the contents of the Makefile that is written. +WriteMakefile( + 'NAME' => 'dgd', + 'AUTHOR' => 'Sergey Poznyakoff ', + 'ABSTRACT' => 'Dead gateway detector', + 'FIRST_MAKEFILE' => 'Makefile', + 'VERSION' => '1.00', + 'EXE_FILES' => [ 'dgd' ], +); diff --git a/dgd/dgd b/dgd/dgd new file mode 100755 index 0000000..0afae81 --- /dev/null +++ b/dgd/dgd @@ -0,0 +1,843 @@ +#!/usr/bin/perl +# Copyright (C) 2015 Sergey Poznyakoff +# +# 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, 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 . + +use strict; +use Getopt::Long qw(:config gnu_getopt no_ignore_case); +use File::Basename; +use Net::Ping; +use Pod::Usage; +use Pod::Man; +use Proc::Daemon; +use Sys::Syslog; +use Data::Dumper; + +my $progname = basename($0); +my $progdescr = "Dead gateway detector"; +my $confdir = "/etc/dgd"; +my $conffile = "$confdir/dgd.conf"; + +my $debug; +my $use_syslog; + +my @links; +my $active_link; + +my %config = ( + core => { + interval => 60, + timeout => 3, + probes => 3, + tolerance => 2 + } +); + +my $rxip = '\d{1,3}\.\d{1,3}.\d{1,3}.\d{1,3}'; + +sub check_prog { + my @cmd = split(/\s+/, shift); + my $loc = shift; + unless (-f $cmd[0]) { + error("$loc: file $cmd[0] does not exist"); + return 0; + } + unless (-x $cmd[0]) { + error("$loc: file $cmd[0] is not executable"); + return 0; + } + return 1; +} + +my %kw = ( + core => { + section => { + interval => { re => '^\d+$' }, + timeout => { re => '^\d+$' }, + probes => { re => '^\d+$' }, + tolerance => { re => '^\d+$' }, + active => 1, + up => { check => \&check_prog }, + down => { check => \&check_prog }, + logpriority => { re => '^0|1$' } + } + }, + syslog => { + section => { + facility => { re => '^((auth(priv)?)|cron|daemon|ftp|(local[0-7])|user)$' }, + tag => 1 + } + }, + link => { + section => { + name => { mandatory => 1 }, + if => 1, + ip => { re => "^$rxip\$" }, + gw => { re => "^$rxip\$", mandatory => 1 }, + ns => 1, + net => 1, + up => { check => \&check_prog }, + down => { check => \&check_prog }, + priority => { re => '^\d+$' } + }, + mandatory => 1 + } +); + +use constant EX_OK => 0; +use constant EX_USAGE => 64; # command line usage error +use constant EX_DATAERR => 65; # data format error +use constant EX_NOINPUT => 66; # cannot open input file +use constant EX_UNAVAILABLE => 69; # service unavailable +use constant EX_SOFTWARE => 70; # internal software error (not used yet) +use constant EX_OSFILE => 72; # critical OS file missing +use constant EX_CANTCREAT => 73; # can't create (user) output file +use constant EX_CONFIG => 78; # configuration error + +sub diag { + my $prio = shift; + my $msg = shift; + local %_ = @_; + + $msg = "$prio: $msg" if $config{core}{logpriority}; + $msg = "$_{prefix}: $msg" if defined($_{prefix}); + + if ($use_syslog) { + syslog($prio, $msg); + } else { + print STDERR "$progname: " if defined($progname); + print STDERR "$msg\n" + } +} + +sub error { + diag('err', @_); +} + +sub debug { + my $l = shift; + diag('debug', join(' ',@_)) if $debug >= $l; +} + +sub abend { + my $code = shift; + diag('crit', @_); + exit $code; +} + +sub parse_section { + my ($conf, $input) = @_; + my $ref = $conf; + my $quote; + my $rootname; + while ($input ne '') { + my $name; + if (!defined($quote)) { + if ($input =~ /^"(.*)/) { + $quote = ''; + $input = $1; + } elsif ($input =~ /^(.+?)(?:\s+|")(.*)/) { + $name = $1; + $input = $2; + } else { + $name = $input; + $input = ''; + } + } else { + if ($input =~ /^([^\\"]*)\\(.)(.*)/) { + $quote .= $1 . $2; + $input = $3; + } elsif ($input =~ /^([^\\"]*)"\s*(.*)/) { + $name = $quote . $1; + $input = $2; + $quote = undef; + } else { + die "unparsable input $input"; + } + } + + if (defined($name)) { + $rootname = $name unless defined $rootname; + $ref->{$name} = {} unless ref($ref->{$name}) eq 'HASH'; + $ref = $ref->{$name}; + $name = undef; + } + } + return ($ref, $rootname); +} + +sub check_mandatory { + my ($section, $kw, $loc, $s) = @_; + my $err = 0; + while (my ($k, $d) = each %{$kw}) { + if (ref($d) eq 'HASH' + and $d->{mandatory} + and !exists($section->{$k})) { + if (exists($d->{section})) { + if ($s) { + error("$loc: mandatory section [$k] not present"); + ++$err; + } + } else { + error("$loc: mandatory variable \"$k\" not set"); + ++$err; + } + } + } + return $err; +} + +sub readconfig { + my $file = shift; + my $conf = shift; + my %param = @_; + + open(my $fd, "<", $file) + or do { + return 1 if $param{include}; + abend(EX_NOINPUT, "can't open configuration file $file: $!"); + }; + + my $line; + my $err; + my $section = $conf; + my $kw = $param{kw}; + my $include = 0; + my $rootname; + + while (<$fd>) { + ++$line; + chomp; + if (/\\$/) { + chop; + $_ .= <$fd>; + redo; + } + + s/^\s+//; + s/\s+$//; + s/#.*//; + next if ($_ eq ""); + + if (/^\[(.+?)\]$/) { + $err += check_mandatory($section, $kw, "$file:$line") + if defined $kw; + + $include = 0; + my $arg = $1; + $arg =~ s/^\s+//; + $arg =~ s/\s+$//; + if ($arg eq 'include') { + $include = 1; + } else { + ($section, $rootname) = parse_section($conf, $1); + if (ref($param{kw}) eq 'HASH') { + if (defined($rootname)) { + if (ref($param{kw}{$rootname}) eq 'HASH' + and exists($param{kw}{$rootname}{section})) { + $kw = $param{kw}{$rootname}{section}; + } else { + error("$file:$line: unknown section"); + $kw = undef; + } + } else { + $kw = $param{kw}; + } + } + } + } elsif (/([\w_-]+)\s*=\s*(.*)/) { + my ($k, $v) = ($1, $2); + $k = lc($k) if $param{ci}; + + if ($include) { + if ($k eq 'path') { + $err += readconfig($v, $conf, include => 1, @_); + } elsif ($k eq 'glob') { + foreach my $file (glob $v) { + $err += readconfig($file, $conf, @_); + } + } else { + error("$file:$line: unknown keyword"); + ++$err; + } + next; + } + + if (defined($kw)) { + my $x = $kw->{$k}; + if (!defined($x)) { + error("$file:$line: unknown keyword $k"); + ++$err; + next; + } elsif (ref($x) eq 'HASH') { + if (exists($x->{re})) { + if ($v !~ /$x->{re}/) { + error("$file:$line: invalid value for $k"); + ++$err; + next; + } + if (exists($x->{check}) + and !&{$x->{check}}($v, "$file:$line")) { + ++$err; + next; + } + } elsif (exists($x->{check})) { + if (!&{$x->{check}}($v, "$file:$line")) { + ++$err; + next; + } + } elsif (!exists($x->{var}) and !exists($x->{mandatory})) { + error("$file:$line: unknown keyword $k"); + ++$err; + next; + } + } + } + + $section->{$k} = $v; + } else { + error("$file:$line: malformed line"); + ++$err; + next; + } + } + close $fd; + if (defined($param{kw}) and !$param{include}) { + $err += check_mandatory($conf, $param{kw}, "$file:$line", 1) + } + return $err; +} + +sub get_default_gw { + my $gw; + open(my $fd, '-|', "netstat -rn") + or abend(EX_OSFILE, "can't start netstat: $!"); + while (<$fd>) { + if (/^0\.0\.0\.0\s+(\S+)/) { + $gw = $1; + last; + } + } + close $fd; + return $gw; +} + +sub scan_links { + @links = sort { $a->{priority} <=> $b->{priority} } + map { $_->{priority} = 100 unless exists $_->{priority}; + debug(1, "registered link $_->{name} via $_->{if}"); + $_ + } values %{$config{link}}; + + abend(EX_CONFIG, "no links configured") + if $#links == -1; + + if (defined($config{core}{active})) { + for ($active_link = 0; $active_link <= $#links; ++$active_link) { + last if $links[$active_link]->{name} eq $config{core}{active}; + } + + abend(EX_CONFIG, + "no link corresponding to the active one") + if ($active_link > $#links); + } else { + my $gw = get_default_gw(); + debug(1, "default gw $gw"); + if (defined($gw)) { + for ($active_link = 0; $active_link <= $#links; ++$active_link) { + last if $links[$active_link]->{gw} eq $gw; + } + + abend(EX_CONFIG, "no active link configured") + if ($active_link > $#links); + + debug(1, "active link $links[$active_link]->{name}"); + } else { + abend(EX_CONFIG, "no active link configured") + } + } + +} + +sub runcmd { + my $cmdline = shift; + debug(2, "running $cmdline"); + if (open(my $fd, '-|', "$cmdline 2>&1")) { + while (<$fd>) { + diag('notice', "\"$cmdline\": $_"); + } + close($fd); + } elsif ($? == -1) { + error("failed to execute \"$cmdline\": $!"); + } elsif ($? & 127) { + error("\"$cmdline\" died with signal " . ($? & 127)); + } elsif ($? >> 8) { + error("\"$cmdline\" exited with code " . ($? >> 8)); + } else { + error("failed to execute \"$cmdline\": $!"); + } +} + +sub updown { + my ($what, $name) = @_; + + my $cmd = defined($config{link}{$name}{$what}) + ? $config{link}{$name}{$what} + : $config{core}{$what}; + if (defined($cmd)) { + runcmd("$cmd $name"); + } elsif ($what eq 'up') { + runcmd("route add default gw $config{link}{gw}"); + } elsif ($what eq 'down') { + runcmd("route del default gw"); + } else { + die "unrecognized action $what"; + } +} + +sub link_is_alive { + my $arg = shift; + $arg = $links[$arg] if ref($arg) ne 'HASH'; + return $arg->{alive} >= $config{core}{tolerance}; +} + +sub check_links { + my $link; + foreach $link (@links) { + $link->{alive} = 0; + } + debug(1, "checking links"); + my $p = Net::Ping->new("icmp", $config{core}{timeout}); + for (my $i = 0; $i < $config{core}{probes}; $i++) { + foreach $link (@links) { + debug(3, "ping $link->{name}"); + next if link_is_alive($link); + ++$link->{alive} if $p->ping($link->{gw}); + } + } + $p->close(); + if (!defined($active_link)) { + return 1; + } elsif (!link_is_alive($active_link)) { + my $name = $links[$active_link]->{name}; + error("link $name went down"); + updown('down', $name); + $active_link = undef; + return 1; + } + return 0; +} + +sub newlink { + debug(1, "looking for available link"); + for (my $i = 0; $i <= $#links; $i++) { + #next if $i == $active_link; + debug(2, "$links[$i]->{name}: $links[$i]->{alive}/$config{core}{tolerance}"); + if (link_is_alive($i)) { + my $name = $links[$i]->{name}; + diag('info', "switching to link $name"); + updown('up', $name); + $active_link = $i; + return; + } + } + error("no fallback link"); +} + +# ######## +my $foreground; + +GetOptions("h" => sub { + pod2usage(-message => "$progname: $progdescr", + -exitstatus => 0); + }, + "help" => sub { + pod2usage(-exitstatus => EX_OK, -verbose => 2); + }, + "usage" => sub { + pod2usage(-exitstatus => EX_OK, -verbose => 0); + }, + "debug|d+" => \$debug, + "foreground|f" => \$foreground, + "config|c=s" => \$conffile) or exit(1); + +if (readconfig($conffile, \%config, kw => \%kw)) { + exit(EX_CONFIG); +} + +abend(EX_USAGE, "too many arguments") if $#ARGV >= 0; + +print Data::Dumper->Dump([\%config], [qw(config)]) if $debug >= 4; + +scan_links; + +unless ($foreground) { + $config{core}{syslog}{facility} = 'daemon' + unless exists $config{core}{syslog}{facility}; +} + +if (defined($config{core}{syslog}{facility})) { + my $tag = $config{core}{syslog}{tag} || $progname; + openlog($tag, "pid", $config{core}{syslog}{facility}); +} + +my $pidfile = $config{core}{pidfile}; +$SIG{TERM} = $SIG{INT} = $SIG{QUIT} = sub { + unlink($pidfile) if defined($pidfile); + exit(EX_OK); +}; + +unless ($foreground) { + my $daemon = Proc::Daemon->new(work_dir => '/'); + my $pid = $daemon->Init; + if ($pid) { + if (defined($pidfile)) { + if (open(my $fd, '>', $pidfile)) { + print $fd "$pid\n"; + close $fd; + } else { + error("can't open $pidfile: $!"); + } + } + exit(EX_OK); + } +} + +$SIG{HUP} = sub { + my %t; + diag('info', "re-reading configuration file"); + if (readconfig($conffile, \%t)) { + %config = %t; + @links = (); + scan_links; + } +}; + +$use_syslog = defined $config{syslog}{facility}; + +while (1) { + if (check_links()) { + newlink(); + } elsif ($active_link > 0) { + for (my $i = 0; $i < $active_link; $i++) { + if (link_is_alive($i) + and $links[$i]->{priority} < $links[$active_link]->{priority}) { + diag('info', "switching to higher priority link $links[$i]->{name}"); + updown('down', $links[$active_link]->{name}); + + $active_link = $i; + updown('up', $links[$active_link]->{name}); + } + } + } + sleep($config{core}{interval}); +} + +=head1 NAME + +dgd - dead gateway detector + +=head1 SYNOPSIS + +B +[B<-df>] +[B<-c> I] +[B<--config=>I] +[B<--debug>] +[B<--foreground>] + +B B<-h> | B<--help> | B<--usage> + +=head1 DESCRIPTION + +Monitors a set of network links, by sending ICMP echo requests to +remote gateways. One of the links is assumed to be I, i.e., +to be used as a default gateway, others are used as fallback. + +When active link goes down, B executes a preconfigured B +action for that link, then it selects next available link and executes +an B action for it. Default B action sets up default gateway +via the selected link. Default B action removes default +gateway from the routing table. + +Each link can be assigned a I, an integer value between 0 +and 100 (default). When selecting fallback link to replace the dead one, +the link with lowest priority is given preference. + +When a link goes up, its priority is compared to that of the active one. +If it is numerically less, the new link is made active, using the same +procedure as described above. + +=head1 CONFIGURATION FILE + +The configuration is read from file F. The syntax is +somewhat traditional. Whitespace is mostly ignored. The B<#> character +begins a comment to the end of line. Blank lines are ignored. + +The file consists of sections and variabes. A section begins with +the name of the section in square brackets and continues until the +next section begins. Each variable must belong to some section. +Names of sections and variables are case-sensitive. + +Sections can be further divided into subsections, by listing subsection +name after the section name and a white space, e.g.: + + [section subsection] + +Subsection name must be enclosed in double quotes if it contains +whitespace or double-quote characters. Otherwise, quoting is optional. +Within a quoted subsection name, double-quote and backslash have to be +escaped as B<\"> and B<\\>, respectively. + +Variables are assigned using the following syntax + +=over 8 + +=item I B<=> I + +=back + +Whitespace is allowed on both sides of the equals sign. I is read +verbatim, it can contain any characters, including whitespace. Very long +values can be split over several physical lines, by ending each line +excepting the last with a backslash immediately followed by a newline character. + +=head2 Section B<[include]> + +Special section B<[include]> can be used to include one or more files into +another file. It can contain following variables: + +=over 4 + +=item B F + +Include contents of F. The file must exist. + +=item B I + +Include all files matching B(7) I. It is OK if no +file matches the pattern. + +=back + +=head2 Section B<[core]> + +Controls core functionality of the program. + +=over 4 + +=item B I + +Check links each I seconds (default 60). + +=item B I + +Ping timeout, in seconds (default 3). + +=item B I + +Number of ping probes to run for each gateway (default 3). + +=item B I + +Number of probes that must succeed in order for the link to be +marked as I. Default is 2 + +=item B I + +Name of the active link. If not defined, the link whose B variable +matches the default gateway address is assumed active. + +=item B B<0>|B<1> + +If B<1>, log the priority with each diagnostic message. + +=item B I + +Name of the external command to run when a link goes up. Name of +the link in question is passed as argument to I. + +=item B I + +Name of the external command to run when a link goes up. Name of +the link in question is passed as argument to I. + +=back + +=head2 Section B<[syslog]> + +=over 4 + +=item B I + +Syslog facility to use. Allowed values for I are: B, +B, B, B, B, B, and B +through B. Default is B. + +=item B I + +Tag messages with I (default -- name of the program). + +=back + +=head2 Section B<[link]> + +Defines a link. At least one link must be defined. + +=over 4 + +=item B I + +Name of this link. This variable is mandatory, + +=item B I + +IP address of the remote gateway. This variable is mandatory, + +=item B I + +Name of the network interface. + +=item B I + +Name of the external command to run when this link goes up. Overrides +B. + +=item B I + +Name of the external command to run when this link goes up. Overrides +B. + +=item B I + +Priority of the link, an integer number between 0 (highest priority) and +100 (lowest priority). Default is 100. + +=back + +The variables below are not used directly by B. They are intended +to pass additional information to B and B scripts: + +=over 4 + +=item B I + +IP address assigned to the interface. + +=item B I [I...] + +Whitespace-separated list of ip addresses of NS servers. + +=item B I + +Network that is routed through this link. + +=back + +=head1 OPTIONS + +=over 4 + +=item B<-c>, B<--config=>I + +Read configuration parameters from I, instead of F. + +=item B<-d>, B<--debug> + +Increase debug level. + +=item B<-f>, B<--foreground> + +Remain in the foreground. Print diagnostics on standard error, unless +B<[syslog]> configuration section is present. + +=back + +The following options cause B to print informational message on +the standard error and exit: + +=over 4 + +=item B<-h> + +Print short usage summary. + +=item B<--help> + +Display this manual. + +=item B<--usage> + +Print invocation syntax summary. + +=back + +=head1 FILES + +=over 4 + +=item F + +Default configuration file. + +=back + +=head1 SIGNALS + +=over 4 + +=item B + +Instructs B to re-read its configuration file. The new configuration +takes effect only if there were no errors in it. + +=item B, B, B + +Program terminates. + +=back + +=head1 EXIT CODES + +=over 4 + +=item B<0> + +Successful termination. + +=item B<64> + +Command line usage error. + +=item B<66> + +Couldn't open input file. + +=item B<72> + +Failed to run external command. + +=item B<78> + +Error in configuration file. + +=back + +=head1 AUTHOR + +Sergey Poznyakoff + +=cut + + diff --git a/dgd/rc.uplink b/dgd/rc.uplink new file mode 100755 index 0000000..ac0488a --- /dev/null +++ b/dgd/rc.uplink @@ -0,0 +1,117 @@ +#!/bin/bash + +PATH=/sbin:/usr/sbin:$PATH +rt_tables=/etc/iproute2/rt_tables +confdir=/etc/dgd +linkdir=$confdir/links +active=$confdir/active +forwarders=/var/named/forwarders + +# 1 2 3 4 5 6 7 +# linksetup NAME IF IP GW NS1 NS2 [NET] +linksetup() { + n=$(awk ' +BEGIN { i = -1 } +/^#/ { next } +/^$/ { next } +$2=="'$1'" { i = $1; exit(0) } +{ t[$1] = $2; if ($1>n) n = $1 } +END { + if (i == -1) { + for (i=0; i < n && t[i]; i++); + print i + } +}' $rt_tables) + if [ -n "$n" ]; then + echo "$n $1" >> $rt_tables + fi + ip rule add from $3 table $1 + ip rule add from $5 table $1 + if [ -n "$6" ]; then + ip rule add from $6 table $1 + fi + ip route add default via $4 table $1 + if [ -n "$7" ]; then + ip route add $7 dev $2 src $3 table $1 + fi + test -d $linkdir || mkdir -p $linkdir + cat > "$linkdir/$1" <&2 "$0: $linkdir/$name: gw not set" + exit 1 + fi + ip route add default via $link_gw + cat > $active < $forwarders + rndc reload + else + echo >&2 "$0: $linkdir/$name: no such file" + exit 1 + fi +} + +#linkdown NAME +linkdown() { + local name=$1 link_if link_ip link_gw link_ns link_net link_active + shift + if [ -f "$linkdir/$name" ]; then + readlinkinfo "$linkdir/$name" + if [ -z "$link_gw" ]; then + echo >&2 "$0: $linkdir/$name: gw not set" + exit 1 + fi + ip route del default via $link_gw + if [ -r "$active" ]; then + readlinkinfo $active + if [ "$link_name" = "$name" ]; then + rm $active + fi + fi + else + echo >&2 "$0: $linkdir/$name: no such file" + exit 1 + fi +} + +command=$1 +shift +case $command in + setup) linksetup "$@";; + cleanup) linkcleanup "$@";; + up) linkup "$@";; + down) linkdown "$@";; + *) echo >&2 "$0: unrecognized command";; +esac -- cgit v1.2.1