diff --git a/alrmrc b/alrmrc index 8f84c3b..592d062 100644 --- a/alrmrc +++ b/alrmrc @@ -1,11 +1,13 @@ set interval 30 -monitor host gateway address 10.11.135.100 - monitor group webservers host www1.example.com address 10.11.135.101 check ping host www2.example.com address 10.11.135.102 - check ping + # comments can occur at the beginning + check ping # or the end of a line + check ping # checks are not named, so multiple is okay + +monitor host gateway address 10.11.135.100 monitor host database diff --git a/check.go b/check/check.go similarity index 56% rename from check.go rename to check/check.go index e50daf7..ce33f6b 100644 --- a/check.go +++ b/check/check.go @@ -1,13 +1,18 @@ -package main +package check import ( "fmt" ) +type AlrmCheck interface { + Parse(string) (bool, error) + Check() error +} + func NewCheck(name string, addr string) (AlrmCheck, error) { switch name { case "ping": - return &CheckPing{Address: addr}, nil + return &CheckPing{Type: "ping", Address: addr}, nil default: return nil, fmt.Errorf("unknown check name \"%s\"", name) } diff --git a/check_ping.go b/check/check_ping.go similarity index 86% rename from check_ping.go rename to check/check_ping.go index 6c20ed4..c25f5a1 100644 --- a/check_ping.go +++ b/check/check_ping.go @@ -1,6 +1,7 @@ -package main +package check type CheckPing struct { + Type string Address string } diff --git a/check/ping/ping.go b/check/ping/ping.go new file mode 100644 index 0000000..3eab173 --- /dev/null +++ b/check/ping/ping.go @@ -0,0 +1,660 @@ +// Package ping is an ICMP ping library seeking to emulate the unix "ping" +// command. +// +// Here is a very simple example that sends & receives 3 packets: +// +// pinger, err := ping.NewPinger("www.google.com") +// if err != nil { +// panic(err) +// } +// +// pinger.Count = 3 +// pinger.Run() // blocks until finished +// stats := pinger.Statistics() // get send/receive/rtt stats +// +// Here is an example that emulates the unix ping command: +// +// pinger, err := ping.NewPinger("www.google.com") +// if err != nil { +// fmt.Printf("ERROR: %s\n", err.Error()) +// return +// } +// +// pinger.OnRecv = func(pkt *ping.Packet) { +// fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n", +// pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt) +// } +// pinger.OnFinish = func(stats *ping.Statistics) { +// fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr) +// fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n", +// stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss) +// fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n", +// stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt) +// } +// +// fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr()) +// pinger.Run() +// +// It sends ICMP packet(s) and waits for a response. If it receives a response, +// it calls the "receive" callback. When it's finished, it calls the "finish" +// callback. +// +// For a full ping example, see "cmd/ping/ping.go". +// +package ping + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math" + "math/rand" + "net" + "runtime" + "sync" + "syscall" + "time" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +const ( + timeSliceLength = 8 + trackerLength = 8 + protocolICMP = 1 + protocolIPv6ICMP = 58 +) + +var ( + ipv4Proto = map[string]string{"icmp": "ip4:icmp", "udp": "udp4"} + ipv6Proto = map[string]string{"icmp": "ip6:ipv6-icmp", "udp": "udp6"} +) + +// New returns a new Pinger struct pointer. +func New(addr string) *Pinger { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + return &Pinger{ + Count: -1, + Interval: time.Second, + RecordRtts: true, + Size: timeSliceLength, + Timeout: time.Second * 100000, + Tracker: r.Int63n(math.MaxInt64), + + addr: addr, + done: make(chan bool), + id: r.Intn(math.MaxInt16), + ipaddr: nil, + ipv4: false, + network: "ip", + protocol: "udp", + } +} + +// NewPinger returns a new Pinger and resolves the address. +func NewPinger(addr string) (*Pinger, error) { + p := New(addr) + return p, p.Resolve() +} + +// Pinger represents a packet sender/receiver. +type Pinger struct { + // Interval is the wait time between each packet send. Default is 1s. + Interval time.Duration + + // Timeout specifies a timeout before ping exits, regardless of how many + // packets have been received. + Timeout time.Duration + + // Count tells pinger to stop after sending (and receiving) Count echo + // packets. If this option is not specified, pinger will operate until + // interrupted. + Count int + + // Debug runs in debug mode + Debug bool + + // Number of packets sent + PacketsSent int + + // Number of packets received + PacketsRecv int + + // If true, keep a record of rtts of all received packets. + // Set to false to avoid memory bloat for long running pings. + RecordRtts bool + + // rtts is all of the Rtts + rtts []time.Duration + + // OnSend is called when Pinger sends a packet + OnSend func(*Packet) + + // OnRecv is called when Pinger receives and processes a packet + OnRecv func(*Packet) + + // OnFinish is called when Pinger exits + OnFinish func(*Statistics) + + // Size of packet being sent + Size int + + // Tracker: Used to uniquely identify packet when non-priviledged + Tracker int64 + + // Source is the source IP address + Source string + + // stop chan bool + done chan bool + + ipaddr *net.IPAddr + addr string + + ipv4 bool + id int + sequence int + // network is one of "ip", "ip4", or "ip6". + network string + // protocol is "icmp" or "udp". + protocol string +} + +type packet struct { + bytes []byte + nbytes int + ttl int +} + +// Packet represents a received and processed ICMP echo packet. +type Packet struct { + // Rtt is the round-trip time it took to ping. + Rtt time.Duration + + // IPAddr is the address of the host being pinged. + IPAddr *net.IPAddr + + // Addr is the string address of the host being pinged. + Addr string + + // NBytes is the number of bytes in the message. + Nbytes int + + // Seq is the ICMP sequence number. + Seq int + + // TTL is the Time To Live on the packet. + Ttl int +} + +// Statistics represent the stats of a currently running or finished +// pinger operation. +type Statistics struct { + // PacketsRecv is the number of packets received. + PacketsRecv int + + // PacketsSent is the number of packets sent. + PacketsSent int + + // PacketLoss is the percentage of packets lost. + PacketLoss float64 + + // IPAddr is the address of the host being pinged. + IPAddr *net.IPAddr + + // Addr is the string address of the host being pinged. + Addr string + + // Rtts is all of the round-trip times sent via this pinger. + Rtts []time.Duration + + // MinRtt is the minimum round-trip time sent via this pinger. + MinRtt time.Duration + + // MaxRtt is the maximum round-trip time sent via this pinger. + MaxRtt time.Duration + + // AvgRtt is the average round-trip time sent via this pinger. + AvgRtt time.Duration + + // StdDevRtt is the standard deviation of the round-trip times sent via + // this pinger. + StdDevRtt time.Duration +} + +// SetIPAddr sets the ip address of the target host. +func (p *Pinger) SetIPAddr(ipaddr *net.IPAddr) { + p.ipv4 = isIPv4(ipaddr.IP) + + p.ipaddr = ipaddr + p.addr = ipaddr.String() +} + +// IPAddr returns the ip address of the target host. +func (p *Pinger) IPAddr() *net.IPAddr { + return p.ipaddr +} + +// Resolve does the DNS lookup for the Pinger address and sets IP protocol. +func (p *Pinger) Resolve() error { + if len(p.addr) == 0 { + return errors.New("addr cannot be empty") + } + ipaddr, err := net.ResolveIPAddr(p.network, p.addr) + if err != nil { + return err + } + + p.ipv4 = isIPv4(ipaddr.IP) + + p.ipaddr = ipaddr + + return nil +} + +// SetAddr resolves and sets the ip address of the target host, addr can be a +// DNS name like "www.google.com" or IP like "127.0.0.1". +func (p *Pinger) SetAddr(addr string) error { + oldAddr := p.addr + p.addr = addr + err := p.Resolve() + if err != nil { + p.addr = oldAddr + return err + } + return nil +} + +// Addr returns the string ip address of the target host. +func (p *Pinger) Addr() string { + return p.addr +} + +// SetNetwork allows configuration of DNS resolution. +// * "ip" will automatically select IPv4 or IPv6. +// * "ip4" will select IPv4. +// * "ip6" will select IPv6. +func (p *Pinger) SetNetwork(n string) { + switch n { + case "ip4": + p.network = "ip4" + case "ip6": + p.network = "ip6" + default: + p.network = "ip" + } +} + +// SetPrivileged sets the type of ping pinger will send. +// false means pinger will send an "unprivileged" UDP ping. +// true means pinger will send a "privileged" raw ICMP ping. +// NOTE: setting to true requires that it be run with super-user privileges. +func (p *Pinger) SetPrivileged(privileged bool) { + if privileged { + p.protocol = "icmp" + } else { + p.protocol = "udp" + } +} + +// Privileged returns whether pinger is running in privileged mode. +func (p *Pinger) Privileged() bool { + return p.protocol == "icmp" +} + +// Run runs the pinger. This is a blocking function that will exit when it's +// done. If Count or Interval are not specified, it will run continuously until +// it is interrupted. +func (p *Pinger) Run() error { + var conn *icmp.PacketConn + var err error + if p.ipaddr == nil { + err = p.Resolve() + } + if err != nil { + return err + } + if p.ipv4 { + if conn, err = p.listen(ipv4Proto[p.protocol]); err != nil { + return err + } + if err = conn.IPv4PacketConn().SetControlMessage(ipv4.FlagTTL, true); runtime.GOOS != "windows" && err != nil { + return err + } + } else { + if conn, err = p.listen(ipv6Proto[p.protocol]); err != nil { + return err + } + if err = conn.IPv6PacketConn().SetControlMessage(ipv6.FlagHopLimit, true); runtime.GOOS != "windows" && err != nil { + return err + } + } + defer conn.Close() + defer p.finish() + + var wg sync.WaitGroup + recv := make(chan *packet, 5) + defer close(recv) + wg.Add(1) + //nolint:errcheck + go p.recvICMP(conn, recv, &wg) + + err = p.sendICMP(conn) + if err != nil { + return err + } + + timeout := time.NewTicker(p.Timeout) + defer timeout.Stop() + interval := time.NewTicker(p.Interval) + defer interval.Stop() + + for { + select { + case <-p.done: + wg.Wait() + return nil + case <-timeout.C: + close(p.done) + wg.Wait() + return nil + case <-interval.C: + if p.Count > 0 && p.PacketsSent >= p.Count { + continue + } + err = p.sendICMP(conn) + if err != nil { + // FIXME: this logs as FATAL but continues + fmt.Println("FATAL: ", err.Error()) + } + case r := <-recv: + err := p.processPacket(r) + if err != nil { + // FIXME: this logs as FATAL but continues + fmt.Println("FATAL: ", err.Error()) + } + } + if p.Count > 0 && p.PacketsRecv >= p.Count { + close(p.done) + wg.Wait() + return nil + } + } +} + +func (p *Pinger) Stop() { + close(p.done) +} + +func (p *Pinger) finish() { + handler := p.OnFinish + if handler != nil { + s := p.Statistics() + handler(s) + } +} + +// Statistics returns the statistics of the pinger. This can be run while the +// pinger is running or after it is finished. OnFinish calls this function to +// get it's finished statistics. +func (p *Pinger) Statistics() *Statistics { + loss := float64(p.PacketsSent-p.PacketsRecv) / float64(p.PacketsSent) * 100 + var min, max, total time.Duration + if len(p.rtts) > 0 { + min = p.rtts[0] + max = p.rtts[0] + } + for _, rtt := range p.rtts { + if rtt < min { + min = rtt + } + if rtt > max { + max = rtt + } + total += rtt + } + s := Statistics{ + PacketsSent: p.PacketsSent, + PacketsRecv: p.PacketsRecv, + PacketLoss: loss, + Rtts: p.rtts, + Addr: p.addr, + IPAddr: p.ipaddr, + MaxRtt: max, + MinRtt: min, + } + if len(p.rtts) > 0 { + s.AvgRtt = total / time.Duration(len(p.rtts)) + var sumsquares time.Duration + for _, rtt := range p.rtts { + sumsquares += (rtt - s.AvgRtt) * (rtt - s.AvgRtt) + } + s.StdDevRtt = time.Duration(math.Sqrt( + float64(sumsquares / time.Duration(len(p.rtts))))) + } + return &s +} + +func (p *Pinger) recvICMP( + conn *icmp.PacketConn, + recv chan<- *packet, + wg *sync.WaitGroup, +) error { + defer wg.Done() + for { + select { + case <-p.done: + return nil + default: + bytes := make([]byte, 512) + if err := conn.SetReadDeadline(time.Now().Add(time.Millisecond * 100)); err != nil { + return err + } + var n, ttl int + var err error + if p.ipv4 { + var cm *ipv4.ControlMessage + n, cm, _, err = conn.IPv4PacketConn().ReadFrom(bytes) + if cm != nil { + ttl = cm.TTL + } + } else { + var cm *ipv6.ControlMessage + n, cm, _, err = conn.IPv6PacketConn().ReadFrom(bytes) + if cm != nil { + ttl = cm.HopLimit + } + } + if err != nil { + if neterr, ok := err.(*net.OpError); ok { + if neterr.Timeout() { + // Read timeout + continue + } else { + close(p.done) + return err + } + } + } + + select { + case <-p.done: + return nil + case recv <- &packet{bytes: bytes, nbytes: n, ttl: ttl}: + } + } + } +} + +func (p *Pinger) processPacket(recv *packet) error { + receivedAt := time.Now() + var proto int + if p.ipv4 { + proto = protocolICMP + } else { + proto = protocolIPv6ICMP + } + + var m *icmp.Message + var err error + if m, err = icmp.ParseMessage(proto, recv.bytes); err != nil { + return fmt.Errorf("error parsing icmp message: %s", err.Error()) + } + + if m.Type != ipv4.ICMPTypeEchoReply && m.Type != ipv6.ICMPTypeEchoReply { + // Not an echo reply, ignore it + return nil + } + + outPkt := &Packet{ + Nbytes: recv.nbytes, + IPAddr: p.ipaddr, + Addr: p.addr, + Ttl: recv.ttl, + } + + switch pkt := m.Body.(type) { + case *icmp.Echo: + // If we are priviledged, we can match icmp.ID + if p.protocol == "icmp" { + // Check if reply from same ID + if pkt.ID != p.id { + return nil + } + } + + if len(pkt.Data) < timeSliceLength+trackerLength { + return fmt.Errorf("insufficient data received; got: %d %v", + len(pkt.Data), pkt.Data) + } + + tracker := bytesToInt(pkt.Data[timeSliceLength:]) + timestamp := bytesToTime(pkt.Data[:timeSliceLength]) + + if tracker != p.Tracker { + return nil + } + + outPkt.Rtt = receivedAt.Sub(timestamp) + outPkt.Seq = pkt.Seq + p.PacketsRecv++ + default: + // Very bad, not sure how this can happen + return fmt.Errorf("invalid ICMP echo reply; type: '%T', '%v'", pkt, pkt) + } + + if p.RecordRtts { + p.rtts = append(p.rtts, outPkt.Rtt) + } + handler := p.OnRecv + if handler != nil { + handler(outPkt) + } + + return nil +} + +func (p *Pinger) sendICMP(conn *icmp.PacketConn) error { + var typ icmp.Type + if p.ipv4 { + typ = ipv4.ICMPTypeEcho + } else { + typ = ipv6.ICMPTypeEchoRequest + } + + var dst net.Addr = p.ipaddr + if p.protocol == "udp" { + dst = &net.UDPAddr{IP: p.ipaddr.IP, Zone: p.ipaddr.Zone} + } + + t := append(timeToBytes(time.Now()), intToBytes(p.Tracker)...) + if remainSize := p.Size - timeSliceLength - trackerLength; remainSize > 0 { + t = append(t, bytes.Repeat([]byte{1}, remainSize)...) + } + + body := &icmp.Echo{ + ID: p.id, + Seq: p.sequence, + Data: t, + } + + msg := &icmp.Message{ + Type: typ, + Code: 0, + Body: body, + } + + msgBytes, err := msg.Marshal(nil) + if err != nil { + return err + } + + for { + if _, err := conn.WriteTo(msgBytes, dst); err != nil { + if neterr, ok := err.(*net.OpError); ok { + if neterr.Err == syscall.ENOBUFS { + continue + } + } + } + handler := p.OnSend + if handler != nil { + outPkt := &Packet{ + Nbytes: len(msgBytes), + IPAddr: p.ipaddr, + Addr: p.addr, + Seq: p.sequence, + } + handler(outPkt) + } + + p.PacketsSent++ + p.sequence++ + break + } + + return nil +} + +func (p *Pinger) listen(netProto string) (*icmp.PacketConn, error) { + conn, err := icmp.ListenPacket(netProto, p.Source) + if err != nil { + close(p.done) + return nil, err + } + return conn, nil +} + +func bytesToTime(b []byte) time.Time { + var nsec int64 + for i := uint8(0); i < 8; i++ { + nsec += int64(b[i]) << ((7 - i) * 8) + } + return time.Unix(nsec/1000000000, nsec%1000000000) +} + +func isIPv4(ip net.IP) bool { + return len(ip.To4()) == net.IPv4len +} + +func timeToBytes(t time.Time) []byte { + nsec := t.UnixNano() + b := make([]byte, 8) + for i := uint8(0); i < 8; i++ { + b[i] = byte((nsec >> ((7 - i) * 8)) & 0xff) + } + return b +} + +func bytesToInt(b []byte) int64 { + return int64(binary.BigEndian.Uint64(b)) +} + +func intToBytes(tracker int64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, uint64(tracker)) + return b +} diff --git a/config.go b/config.go deleted file mode 100644 index 0c1301c..0000000 --- a/config.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -type AlrmConfig struct { - Groups []*AlrmGroup - Interval int -} - -func (ac *AlrmConfig) NewGroup() *AlrmGroup { - group := &AlrmGroup{} - ac.Groups = append(ac.Groups, group) - return group -} - -type AlrmGroup struct { - Name string - Hosts []*AlrmHost -} - -func (ag *AlrmGroup) NewHost() *AlrmHost { - host := &AlrmHost{} - ag.Hosts = append(ag.Hosts, host) - return host -} - -type AlrmHost struct { - Name string - Address string - Checks []AlrmCheck -} - -func (ah *AlrmHost) GetAddress() string { - if ah.Address != "" { - return ah.Address - } - return ah.Name -} - -type AlrmCheck interface { - Parse(string) (bool, error) - Check() error -} - -func ReadConfig(fn string) (*AlrmConfig, error) { - parser := &Parser{} - config, err := parser.Parse(fn) - if err != nil { - return nil, err - } - return config, nil -} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..50139c6 --- /dev/null +++ b/config/config.go @@ -0,0 +1,44 @@ +package config + +import ( + "fmt" + "strconv" +) + +type AlrmConfig struct { + Groups map[string]*AlrmGroup + Interval int +} + +func (ac *AlrmConfig) NewGroup(name string) (*AlrmGroup, error) { + if ac.Groups == nil { + ac.Groups = make(map[string]*AlrmGroup) + } + + if _, exists := ac.Groups[name]; exists { + return nil, fmt.Errorf("group %s already exists", name) + } + + group := &AlrmGroup{Name: name} + ac.Groups[name] = group + return group, nil +} + +func (ac *AlrmConfig) SetInterval(val string) error { + interval, err := strconv.Atoi(val) + if err != nil { + return err + } + + ac.Interval = interval + return nil +} + +func ReadConfig(fn string, debuglvl int) (*AlrmConfig, error) { + parser := &Parser{DebugLevel: debuglvl} + config, err := parser.Parse(fn) + if err != nil { + return nil, err + } + return config, nil +} diff --git a/config/group.go b/config/group.go new file mode 100644 index 0000000..7e46185 --- /dev/null +++ b/config/group.go @@ -0,0 +1,24 @@ +package config + +import ( + "fmt" +) + +type AlrmGroup struct { + Name string + Hosts map[string]*AlrmHost +} + +func (ag *AlrmGroup) NewHost(name string) (*AlrmHost, error) { + if ag.Hosts == nil { + ag.Hosts = make(map[string]*AlrmHost) + } + + if _, exists := ag.Hosts[name]; exists { + return nil, fmt.Errorf("host %s already exists", name) + } + + host := &AlrmHost{Name: name} + ag.Hosts[name] = host + return host, nil +} diff --git a/config/host.go b/config/host.go new file mode 100644 index 0000000..bf97e42 --- /dev/null +++ b/config/host.go @@ -0,0 +1,27 @@ +package config + +import ( + "alrm/check" +) + +type AlrmHost struct { + Name string + Address string + Checks []check.AlrmCheck +} + +func (ah *AlrmHost) GetAddress() string { + if ah.Address != "" { + return ah.Address + } + return ah.Name +} + +func (ah *AlrmHost) NewCheck(name string) (check.AlrmCheck, error) { + chk, err := check.NewCheck(name, ah.GetAddress()) + if err != nil { + return nil, err + } + ah.Checks = append(ah.Checks, chk) + return chk, nil +} diff --git a/parser.go b/config/parser.go similarity index 74% rename from parser.go rename to config/parser.go index 2665f7d..f41f551 100644 --- a/parser.go +++ b/config/parser.go @@ -1,10 +1,10 @@ -package main +package config import ( + "alrm/check" "bufio" "fmt" "os" - "strconv" "strings" ) @@ -18,8 +18,12 @@ const ( ) type Parser struct { - Line int - states []int + DebugLevel int + Line int + states []int + lasthost *AlrmHost + lastgroup *AlrmGroup + lastcheck check.AlrmCheck } func (p *Parser) Parse(fn string) (*AlrmConfig, error) { @@ -30,9 +34,6 @@ func (p *Parser) Parse(fn string) (*AlrmConfig, error) { defer file.Close() config := &AlrmConfig{} - var group *AlrmGroup - var host *AlrmHost - var check AlrmCheck scan := bufio.NewScanner(file) scan.Split(p.Split) @@ -61,7 +62,7 @@ func (p *Parser) Parse(fn string) (*AlrmConfig, error) { value := scan.Text() switch key { case "interval": - config.Interval, err = strconv.Atoi(value) + err := config.SetInterval(value) if err != nil { return nil, fmt.Errorf( "invalid number for interval in %s, line %d: \"%s\"", @@ -78,12 +79,9 @@ func (p *Parser) Parse(fn string) (*AlrmConfig, error) { case TK_MONITOR: switch strings.ToLower(tk) { case "host": - group = config.NewGroup() - host = group.NewHost() p.setState(TK_HOST) case "group": - group = config.NewGroup() p.setState(TK_GROUP) default: @@ -92,33 +90,43 @@ func (p *Parser) Parse(fn string) (*AlrmConfig, error) { } case TK_GROUP: - if group == nil { - return nil, fmt.Errorf("group without initialization") + if p.lastgroup == nil { + p.lastgroup, err = config.NewGroup(tk) + if err != nil { + return nil, fmt.Errorf("%s in %s, line %d", + err.Error(), fn, p.Line+1, + ) + } + continue } switch strings.ToLower(tk) { case "host": - host = group.NewHost() p.setState(TK_HOST) - continue default: - if group.Name == "" { - group.Name = tk - continue - } - p.prevState() goto stateswitch } case TK_HOST: - if host == nil { - return nil, fmt.Errorf("host token without initialization") + // If a host has no group, inherit the host name + if p.lastgroup == nil { + p.lastgroup, err = config.NewGroup(tk) + if err != nil { + return nil, fmt.Errorf("%s in %s, line %d", + err.Error(), fn, p.Line+1, + ) + } } - if host.Name == "" { - host.Name = tk + if p.lasthost == nil { + p.lasthost, err = p.lastgroup.NewHost(tk) + if err != nil { + return nil, fmt.Errorf("%s in %s, line %d", + err.Error(), fn, p.Line+1, + ) + } continue } @@ -128,10 +136,9 @@ func (p *Parser) Parse(fn string) (*AlrmConfig, error) { return nil, fmt.Errorf("empty address for host in %s, line %d", fn, p.Line+1) } - host.Address = scan.Text() + p.lasthost.Address = scan.Text() case "check": - check = nil p.setState(TK_CHECK) default: @@ -140,23 +147,20 @@ func (p *Parser) Parse(fn string) (*AlrmConfig, error) { } case TK_CHECK: - if check == nil { - if host == nil { - return nil, fmt.Errorf("check token without initialization") - } - check, err = NewCheck(strings.ToLower(tk), host.GetAddress()) + if p.lastcheck == nil { + p.lastcheck, err = p.lasthost.NewCheck(tk) if err != nil { return nil, fmt.Errorf("%s in %s, line %d", err.Error(), fn, p.Line+1) } - host.Checks = append(host.Checks, check) continue } - cont, err := check.Parse(tk) + cont, err := p.lastcheck.Parse(tk) if err != nil { return nil, err } if !cont { + p.lastcheck = nil p.prevState() goto stateswitch } @@ -179,9 +183,24 @@ func (p *Parser) state() int { } func (p *Parser) setState(state int) { - // fmt.Printf("%s", p.stateName()) + switch state { + case TK_SET, TK_MONITOR: + fallthrough + case TK_GROUP: + p.lastgroup = nil + fallthrough + case TK_HOST: + p.lasthost = nil + p.lastcheck = nil + } + + if p.DebugLevel > 0 { + fmt.Printf("Parser state: %s", p.stateName()) + } p.states = append(p.states, state) - // fmt.Printf(" -> %s\n", p.stateName()) + if p.DebugLevel > 0 { + fmt.Printf(" -> %s\n", p.stateName()) + } } func (p *Parser) prevState() int { @@ -222,7 +241,7 @@ func (p *Parser) Split(data []byte, atEOF bool) (int, []byte, error) { for i := 0; i < len(data); i++ { c := data[i] -// fmt.Printf("%c (%t) (%t)\n", c, started, ignoreline) + // fmt.Printf("%c (%t) (%t)\n", c, started, ignoreline) switch c { case '\f', '\n', '\r': p.Line++ diff --git a/main.go b/main.go index b40f2e2..07c5e98 100644 --- a/main.go +++ b/main.go @@ -6,43 +6,51 @@ import ( "flag" "os" "strings" + "alrm/config" ) func main() { - configPath := flag.String("config", "", "path to configuration file") + cfgPath := flag.String("c", "", "path to configuration file") + debuglvl := flag.Int("d", 0, "debug level") flag.Parse() - if *configPath == "" { + if *cfgPath == "" { if _, err := os.Stat("./alrmrc"); err == nil { - *configPath = "./alrmrc" + *cfgPath = "./alrmrc" } if _, err := os.Stat("/etc/alrmrc"); err == nil { - *configPath = "/etc/alrmrc" + *cfgPath = "/etc/alrmrc" } - if *configPath == "" { + if *cfgPath == "" { fmt.Fprintf(os.Stderr, "Cannot find configuration\n") os.Exit(1) } } - config, err := ReadConfig(*configPath) - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err.Error()) - os.Exit(1) - } - command := strings.ToLower(flag.Arg(0)) switch command { case "json": - o, err := json.MarshalIndent(config, "", " ") + cfg, err := config.ReadConfig(*cfgPath, *debuglvl) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + + o, err := json.MarshalIndent(cfg, "", " ") if err != nil { fmt.Fprintf(os.Stderr, "JSON error: %s\n", err.Error()) os.Exit(1) } - fmt.Fprintf(os.Stdout, "%s", string(o)) + fmt.Fprintf(os.Stdout, "%s\n", string(o)) case "", "config": + _, err := config.ReadConfig(*cfgPath, *debuglvl) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + fmt.Fprintf(os.Stdout, "Config is OK.\n") os.Exit(0)